Initial Commit
commit
0ce3b34093
|
@ -0,0 +1,41 @@
|
||||||
|
# Autosave files
|
||||||
|
*~
|
||||||
|
|
||||||
|
# build
|
||||||
|
[Oo]bj/
|
||||||
|
[Bb]in/
|
||||||
|
packages/
|
||||||
|
TestResults/
|
||||||
|
|
||||||
|
# globs
|
||||||
|
Makefile.in
|
||||||
|
*.DS_Store
|
||||||
|
*.sln.cache
|
||||||
|
*.suo
|
||||||
|
*.cache
|
||||||
|
*.pidb
|
||||||
|
*.userprefs
|
||||||
|
*.usertasks
|
||||||
|
config.log
|
||||||
|
config.make
|
||||||
|
config.status
|
||||||
|
aclocal.m4
|
||||||
|
install-sh
|
||||||
|
autom4te.cache/
|
||||||
|
*.user
|
||||||
|
*.tar.gz
|
||||||
|
tarballs/
|
||||||
|
test-results/
|
||||||
|
Thumbs.db
|
||||||
|
.vs/
|
||||||
|
|
||||||
|
# Mac bundle stuff
|
||||||
|
*.dmg
|
||||||
|
*.app
|
||||||
|
|
||||||
|
# resharper
|
||||||
|
*_Resharper.*
|
||||||
|
*.Resharper
|
||||||
|
|
||||||
|
# dotCover
|
||||||
|
*.dotCover
|
|
@ -0,0 +1,33 @@
|
||||||
|
using System;
|
||||||
|
using appsrv.server;
|
||||||
|
using appsrv.protocol;
|
||||||
|
using System.Threading;
|
||||||
|
using appsrv.resources;
|
||||||
|
using System.IO;
|
||||||
|
using appsrv.test;
|
||||||
|
|
||||||
|
namespace appsrv
|
||||||
|
{
|
||||||
|
class MainClass
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
ApplicationServer server = new ApplicationServer();
|
||||||
|
|
||||||
|
Resource root = new DirectoryResource(new DirectoryInfo("./www"));
|
||||||
|
server.AddRoot("localhost",root);
|
||||||
|
|
||||||
|
StaticClassResource staticClassResource = new StaticClassResource(typeof(StaticTest),root);
|
||||||
|
|
||||||
|
|
||||||
|
Http http = new Http(server);
|
||||||
|
|
||||||
|
http.Start();
|
||||||
|
|
||||||
|
Thread.Sleep(10000);
|
||||||
|
|
||||||
|
http.Stop();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
// Information about this assembly is defined by the following attributes.
|
||||||
|
// Change them to the values specific to your project.
|
||||||
|
|
||||||
|
[assembly: AssemblyTitle("appsrv")]
|
||||||
|
[assembly: AssemblyDescription("")]
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("")]
|
||||||
|
[assembly: AssemblyProduct("")]
|
||||||
|
[assembly: AssemblyCopyright("")]
|
||||||
|
[assembly: AssemblyTrademark("")]
|
||||||
|
[assembly: AssemblyCulture("")]
|
||||||
|
|
||||||
|
// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
|
||||||
|
// The form "{Major}.{Minor}.*" will automatically update the build and revision,
|
||||||
|
// and "{Major}.{Minor}.{Build}.*" will update just the revision.
|
||||||
|
|
||||||
|
[assembly: AssemblyVersion("1.0.*")]
|
||||||
|
|
||||||
|
// The following attributes are used to specify the signing key for the assembly,
|
||||||
|
// if desired. See the Mono documentation for more information about signing.
|
||||||
|
|
||||||
|
//[assembly: AssemblyDelaySign(false)]
|
||||||
|
//[assembly: AssemblyKeyFile("")]
|
|
@ -0,0 +1,83 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">x86</Platform>
|
||||||
|
<ProjectGuid>{FD508FE5-5879-4C60-91D8-CA408E06361F}</ProjectGuid>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<RootNamespace>appsrv</RootNamespace>
|
||||||
|
<AssemblyName>appsrv</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<Optimize>false</Optimize>
|
||||||
|
<OutputPath>bin\Debug</OutputPath>
|
||||||
|
<DefineConstants>DEBUG;</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
<ExternalConsole>true</ExternalConsole>
|
||||||
|
<PlatformTarget>x86</PlatformTarget>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<OutputPath>bin\Release</OutputPath>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
<ExternalConsole>true</ExternalConsole>
|
||||||
|
<PlatformTarget>x86</PlatformTarget>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="System" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Program.cs" />
|
||||||
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
<Compile Include="connector\Http.cs" />
|
||||||
|
<Compile Include="server\ApplicationServer.cs" />
|
||||||
|
<Compile Include="connector\Connector.cs" />
|
||||||
|
<Compile Include="server\Application.cs" />
|
||||||
|
<Compile Include="server\HttpRequest.cs" />
|
||||||
|
<Compile Include="exceptions\IllegalRequestException.cs" />
|
||||||
|
<Compile Include="resources\Resource.cs" />
|
||||||
|
<Compile Include="resources\FileResource.cs" />
|
||||||
|
<Compile Include="resources\DirectoryResource.cs" />
|
||||||
|
<Compile Include="exceptions\ApplicationServerException.cs" />
|
||||||
|
<Compile Include="exceptions\ResourceNotFoundException.cs" />
|
||||||
|
<Compile Include="mime\MimeHelper.cs" />
|
||||||
|
<Compile Include="http\HttpStatusCodes.cs" />
|
||||||
|
<Compile Include="resources\ResourceLink.cs" />
|
||||||
|
<Compile Include="resources\StaticClassResource.cs" />
|
||||||
|
<Compile Include="attributes\WebCallable.cs" />
|
||||||
|
<Compile Include="test\StaticTest.cs" />
|
||||||
|
<Compile Include="http\QueryStringParameters.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="connector\" />
|
||||||
|
<Folder Include="server\" />
|
||||||
|
<Folder Include="exceptions\" />
|
||||||
|
<Folder Include="resources\" />
|
||||||
|
<Folder Include="www\" />
|
||||||
|
<Folder Include="www\gfx\" />
|
||||||
|
<Folder Include="mime\" />
|
||||||
|
<Folder Include="http\" />
|
||||||
|
<Folder Include="attributes\" />
|
||||||
|
<Folder Include="test\" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="www\index.html">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
|
||||||
|
<ProjectExtensions>
|
||||||
|
<MonoDevelop>
|
||||||
|
<Properties>
|
||||||
|
<Policies>
|
||||||
|
<DotNetNamingPolicy ResourceNamePolicy="FileFormatDefault" DirectoryNamespaceAssociation="PrefixedHierarchical" />
|
||||||
|
</Policies>
|
||||||
|
</Properties>
|
||||||
|
</MonoDevelop>
|
||||||
|
</ProjectExtensions>
|
||||||
|
</Project>
|
|
@ -0,0 +1,20 @@
|
||||||
|
using System;
|
||||||
|
namespace appsrv.attributes
|
||||||
|
{
|
||||||
|
public enum Serialization {
|
||||||
|
PLAIN,
|
||||||
|
JSON,
|
||||||
|
XML
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WebCallable : Attribute
|
||||||
|
{
|
||||||
|
public String Name { get; set; }
|
||||||
|
public Serialization Serialization { get; set; } = Serialization.PLAIN;
|
||||||
|
|
||||||
|
public WebCallable()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
using System;
|
||||||
|
using appsrv.server;
|
||||||
|
namespace appsrv.connector
|
||||||
|
{
|
||||||
|
public abstract class Connector
|
||||||
|
{
|
||||||
|
public ApplicationServer ApplicationServer { get; private set; }
|
||||||
|
|
||||||
|
public Connector(ApplicationServer applicationServer)
|
||||||
|
{
|
||||||
|
this.ApplicationServer = applicationServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void Start();
|
||||||
|
public abstract void Stop();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using appsrv.connector;
|
||||||
|
using appsrv.server;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace appsrv.protocol
|
||||||
|
{
|
||||||
|
public class Http : Connector
|
||||||
|
{
|
||||||
|
public static int backlog = 5;
|
||||||
|
public static int defaultPort = 8080;
|
||||||
|
public static bool exclusivePortListener = false;
|
||||||
|
|
||||||
|
IPEndPoint endPoint;
|
||||||
|
TcpListener tcpListener;
|
||||||
|
Thread tAccept;
|
||||||
|
|
||||||
|
public Http(ApplicationServer appServer)
|
||||||
|
:this(appServer,new IPEndPoint(IPAddress.Any, defaultPort))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Http(ApplicationServer appServer,IPEndPoint endPoint)
|
||||||
|
: base(appServer)
|
||||||
|
{
|
||||||
|
this.endPoint = endPoint;
|
||||||
|
this.tcpListener = new TcpListener(this.endPoint);
|
||||||
|
this.tcpListener.ExclusiveAddressUse = exclusivePortListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Start()
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
(tAccept == null) ||
|
||||||
|
(!tAccept.IsAlive))
|
||||||
|
{
|
||||||
|
this.tcpListener.Start(backlog);
|
||||||
|
tAccept = new Thread(new ThreadStart(() => acceptRequests()));
|
||||||
|
tAccept.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Stop()
|
||||||
|
{
|
||||||
|
this.tcpListener.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void acceptRequests(){
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
TcpClient client = this.tcpListener.AcceptTcpClient();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HttpRequest request = new HttpRequest(this.ApplicationServer, client);
|
||||||
|
|
||||||
|
Console.WriteLine("new request: {0}",request);
|
||||||
|
|
||||||
|
Thread t = new Thread(() => request.Handle());
|
||||||
|
t.Start();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Exception: {0}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (SocketException e){
|
||||||
|
Console.WriteLine("Http connector interupted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
using System;
|
||||||
|
namespace appsrv.exceptions
|
||||||
|
{
|
||||||
|
public class ApplicationServerException : Exception
|
||||||
|
{
|
||||||
|
public int StatusCode { get; } = 500;
|
||||||
|
|
||||||
|
public ApplicationServerException(String message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
public ApplicationServerException(String message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
public ApplicationServerException(int statusCode,String message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
StatusCode = statusCode;
|
||||||
|
}
|
||||||
|
public ApplicationServerException(int statusCode,String message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
StatusCode = statusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
using System;
|
||||||
|
namespace appsrv.exceptions
|
||||||
|
{
|
||||||
|
public class IllegalRequestException : Exception
|
||||||
|
{
|
||||||
|
public IllegalRequestException(String requestLine)
|
||||||
|
:base(requestLine)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
using System;
|
||||||
|
namespace appsrv.exceptions
|
||||||
|
{
|
||||||
|
public class ResourceNotFoundException : ApplicationServerException
|
||||||
|
{
|
||||||
|
public ResourceNotFoundException(String resourcePath, String nextResource)
|
||||||
|
: base(404, String.Format("Could not find resource \"{0}\" within \"{1}\"", nextResource, resourcePath))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.Remoting.Messaging;
|
||||||
|
namespace appsrv.http
|
||||||
|
{
|
||||||
|
public class HttpStatusCodes
|
||||||
|
{
|
||||||
|
static Dictionary<int, string> statusMessages = new Dictionary<int, string>()
|
||||||
|
{
|
||||||
|
{ 200, "Ok" },
|
||||||
|
{ 403, "Access denied" },
|
||||||
|
{ 404, "Not Found" }
|
||||||
|
};
|
||||||
|
|
||||||
|
public static String GetStatusMessage(int code){
|
||||||
|
if (statusMessages.ContainsKey(code))
|
||||||
|
return statusMessages[code];
|
||||||
|
return "Unknown Status";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
namespace appsrv.http
|
||||||
|
{
|
||||||
|
public class QueryStringParameters : IDictionary<String,String>
|
||||||
|
{
|
||||||
|
Dictionary<string, string> parameters = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
public QueryStringParameters(String query)
|
||||||
|
{
|
||||||
|
if (query.StartsWith("?"))
|
||||||
|
query = query.Substring(1);
|
||||||
|
|
||||||
|
String[] pairs = query.Split(new char[] { '&' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
foreach (String pair in pairs)
|
||||||
|
{
|
||||||
|
String[] kv = pair.Split(new char[] { '=' }, 2, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
string key = Uri.UnescapeDataString(kv[0].Replace('+',' '));
|
||||||
|
string value = kv.Length == 2 ? Uri.UnescapeDataString(kv[1].Replace('+',' ')) : "";
|
||||||
|
if (!key.Equals(String.Empty)){
|
||||||
|
parameters[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string this[string key] {
|
||||||
|
get => parameters[key];
|
||||||
|
set => throw new NotImplementedException(); }
|
||||||
|
|
||||||
|
public ICollection<string> Keys => parameters.Keys;
|
||||||
|
public ICollection<string> Values => parameters.Values;
|
||||||
|
public int Count => parameters.Count;
|
||||||
|
public bool IsReadOnly => true;
|
||||||
|
|
||||||
|
public void Add(string key, string value) => throw new NotImplementedException();
|
||||||
|
public void Add(KeyValuePair<string, string> item) => throw new NotImplementedException();
|
||||||
|
public void Clear() => throw new NotImplementedException();
|
||||||
|
public bool Remove(string key) => throw new NotImplementedException();
|
||||||
|
public bool Remove(KeyValuePair<string, string> item) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public bool Contains(KeyValuePair<string, string> item) => parameters.Contains(item);
|
||||||
|
public bool ContainsKey(string key) => parameters.ContainsKey(key);
|
||||||
|
public void CopyTo(KeyValuePair<string, string>[] array, int arrayIndex) => ((IDictionary<string, string>)parameters).CopyTo(array, arrayIndex);
|
||||||
|
public IEnumerator<KeyValuePair<string, string>> GetEnumerator() => parameters.GetEnumerator();
|
||||||
|
public bool TryGetValue(string key, out string value) => parameters.TryGetValue(key, out value);
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => parameters.GetEnumerator();
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
StringBuilder stringBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
stringBuilder.Append("[Query");
|
||||||
|
foreach (String key in parameters.Keys){
|
||||||
|
stringBuilder.AppendFormat(" {0}={1}", key, parameters[key]);
|
||||||
|
}
|
||||||
|
stringBuilder.Append("]");
|
||||||
|
|
||||||
|
return stringBuilder.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
namespace appsrv.mime
|
||||||
|
{
|
||||||
|
public static class MimeHelper
|
||||||
|
{
|
||||||
|
static Dictionary<string, string> extToMIME = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{"html","text/html"},
|
||||||
|
{"htm","text/html"},
|
||||||
|
{"xml","text/xml"},
|
||||||
|
{"txt","text/plain"}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static String GuessMIMEFromFilename(String filename){
|
||||||
|
foreach (String ext in extToMIME.Keys){
|
||||||
|
if (filename.EndsWith(ext))
|
||||||
|
return extToMIME[ext];
|
||||||
|
}
|
||||||
|
return "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using appsrv.server;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using appsrv.exceptions;
|
||||||
|
|
||||||
|
namespace appsrv.resources
|
||||||
|
{
|
||||||
|
public class DirectoryResource : Resource
|
||||||
|
{
|
||||||
|
public DirectoryInfo DirectoryInfo { get; }
|
||||||
|
public bool IndexingEnabled { get; set; }
|
||||||
|
|
||||||
|
public DirectoryResource(DirectoryInfo directoryInfo)
|
||||||
|
:this(directoryInfo,null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
public DirectoryResource(DirectoryInfo directoryInfo,Resource container)
|
||||||
|
:base(directoryInfo.Name,container)
|
||||||
|
{
|
||||||
|
DirectoryInfo = directoryInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Hit(Stack<string> requestPath, HttpRequest request)
|
||||||
|
{
|
||||||
|
if (requestPath.Count > 0)
|
||||||
|
{
|
||||||
|
String nextResourceName = requestPath.Pop();
|
||||||
|
|
||||||
|
DirectoryInfo[] directoryInfos = DirectoryInfo.GetDirectories(nextResourceName);
|
||||||
|
if (directoryInfos.Length == 1){
|
||||||
|
DirectoryResource directoryResource = new DirectoryResource(directoryInfos[0],this);
|
||||||
|
directoryResource.Request(requestPath, request);
|
||||||
|
} else {
|
||||||
|
FileInfo[] fileInfos = DirectoryInfo.GetFiles(nextResourceName);
|
||||||
|
if (fileInfos.Length == 1){
|
||||||
|
FileResource fileResource = new FileResource(fileInfos[0],this);
|
||||||
|
fileResource.Request(requestPath, request);
|
||||||
|
} else {
|
||||||
|
throw new ResourceNotFoundException(Path, nextResourceName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (IndexingEnabled){
|
||||||
|
|
||||||
|
// ToDo: Create index...
|
||||||
|
|
||||||
|
} else {
|
||||||
|
base.Hit(requestPath, request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
using System;
|
||||||
|
using appsrv.server;
|
||||||
|
using System.IO;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using appsrv.exceptions;
|
||||||
|
using appsrv.mime;
|
||||||
|
|
||||||
|
namespace appsrv.resources
|
||||||
|
{
|
||||||
|
public class FileResource : Resource
|
||||||
|
{
|
||||||
|
public FileInfo FileInfo { get; }
|
||||||
|
public bool DiscardRequestPath { get; set; }
|
||||||
|
|
||||||
|
public FileResource(FileInfo fileInfo,Resource container)
|
||||||
|
:base(fileInfo.Name,container)
|
||||||
|
{
|
||||||
|
FileInfo = fileInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Hit(Stack<string> requestPath, HttpRequest request)
|
||||||
|
{
|
||||||
|
if ((requestPath.Count > 0) && !DiscardRequestPath){
|
||||||
|
throw new ApplicationServerException(String.Format("No resources below {0}",Path));
|
||||||
|
} else {
|
||||||
|
|
||||||
|
request.SetResponseHeader("Content-Type", MimeHelper.GuessMIMEFromFilename(FileInfo.Name));
|
||||||
|
|
||||||
|
using (FileStream fileStream = new FileStream(FileInfo.FullName,FileMode.Open))
|
||||||
|
{
|
||||||
|
fileStream.CopyTo(request.ResponseStream);
|
||||||
|
fileStream.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,141 @@
|
||||||
|
using System;
|
||||||
|
using appsrv.server;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Dynamic;
|
||||||
|
using appsrv.exceptions;
|
||||||
|
using System.Linq;
|
||||||
|
namespace appsrv.resources
|
||||||
|
{
|
||||||
|
public abstract class Resource
|
||||||
|
{
|
||||||
|
public Resource Container { get; }
|
||||||
|
public String Name { get; }
|
||||||
|
|
||||||
|
Dictionary<String, Resource> resources = new Dictionary<string, Resource>();
|
||||||
|
|
||||||
|
|
||||||
|
public Resource(String name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
public Resource(String name,Resource container)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Container = container;
|
||||||
|
if (container != null)
|
||||||
|
container.Add(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void Add(Resource resource){
|
||||||
|
resources.Add(resource.Name, resource);
|
||||||
|
}
|
||||||
|
protected virtual void Remove(Resource resource){
|
||||||
|
if (resources.ContainsValue(resource)){
|
||||||
|
resources.Remove(resource.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Contains(String resName)
|
||||||
|
{
|
||||||
|
return resources.ContainsKey(resName);
|
||||||
|
}
|
||||||
|
public bool Contains(Resource resource)
|
||||||
|
{
|
||||||
|
return resources.ContainsValue(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ISet<Resource> Resources { get => new HashSet<Resource>(resources.Values); }
|
||||||
|
|
||||||
|
public Resource Root {
|
||||||
|
get {
|
||||||
|
if (Container == null)
|
||||||
|
return this;
|
||||||
|
else
|
||||||
|
return Container.Root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IList<String> PathList {
|
||||||
|
get {
|
||||||
|
if (Container != null)
|
||||||
|
{
|
||||||
|
IList<String> pl = Container.PathList;
|
||||||
|
pl.Add(Name);
|
||||||
|
return pl;
|
||||||
|
} else {
|
||||||
|
return new List<String>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String Path {
|
||||||
|
get {
|
||||||
|
return String.Format("/{0}",String.Join("/",PathList));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public virtual Resource this[string name]{
|
||||||
|
get => resources[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Request(Stack<String> requestPath, HttpRequest request)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if ((requestPath.Count > 0) && (Contains(requestPath.Peek())))
|
||||||
|
{
|
||||||
|
this[requestPath.Pop()].Request(requestPath, request);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Hit(requestPath, request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ApplicationServerException ase)
|
||||||
|
{
|
||||||
|
HandleException(ase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Hit(Stack<String> requestPath, HttpRequest request){
|
||||||
|
if (requestPath.Count > 0){
|
||||||
|
throw new ResourceNotFoundException(Path, requestPath.Peek());
|
||||||
|
} else {
|
||||||
|
throw new ApplicationServerException("unimplemented resource has been hit");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Resource FindByPath(String path)
|
||||||
|
{
|
||||||
|
String[] pathTokens = path.Split(new char[]{'/'},StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (path[0] == '/')
|
||||||
|
return Root.FindByPath(pathTokens);
|
||||||
|
else
|
||||||
|
return FindByPath(pathTokens);
|
||||||
|
}
|
||||||
|
public Resource FindByPath(IEnumerable<String> path)
|
||||||
|
{
|
||||||
|
return FindByPath(path.GetEnumerator());
|
||||||
|
}
|
||||||
|
public Resource FindByPath(IEnumerator<String> path)
|
||||||
|
{
|
||||||
|
if (path.MoveNext())
|
||||||
|
{
|
||||||
|
return this[path.Current].FindByPath(path);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
protected void HandleException(ApplicationServerException ase){
|
||||||
|
Console.WriteLine("ASE: " + ase.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using appsrv.server;
|
||||||
|
|
||||||
|
namespace appsrv.resources
|
||||||
|
{
|
||||||
|
public class ResourceLink : Resource
|
||||||
|
{
|
||||||
|
Resource Target { get; }
|
||||||
|
|
||||||
|
public ResourceLink(String name,Resource container,Resource target)
|
||||||
|
:base(name,container)
|
||||||
|
{
|
||||||
|
Target = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Add(Resource resource)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException("This resource can't have children");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Request(Stack<string> requestPath, HttpRequest request)
|
||||||
|
{
|
||||||
|
Target.Request(requestPath, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using appsrv.attributes;
|
||||||
|
using appsrv.server;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace appsrv.resources
|
||||||
|
{
|
||||||
|
public class StaticClassResource : Resource
|
||||||
|
{
|
||||||
|
Type Type { get; }
|
||||||
|
|
||||||
|
|
||||||
|
Dictionary<string, FieldInfo> callableFields = new Dictionary<string, FieldInfo>();
|
||||||
|
|
||||||
|
public StaticClassResource(Type type,Resource container)
|
||||||
|
:base(type.Name,container)
|
||||||
|
{
|
||||||
|
Type = type;
|
||||||
|
|
||||||
|
foreach (MethodInfo methodInfo in Type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)){
|
||||||
|
WebCallable webCallable = methodInfo.GetCustomAttribute<WebCallable>();
|
||||||
|
if (webCallable != null){
|
||||||
|
new CallableMethodResource(methodInfo, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class CallableMethodResource : Resource
|
||||||
|
{
|
||||||
|
MethodInfo MethodInfo { get; }
|
||||||
|
WebCallable WebCallable { get; }
|
||||||
|
|
||||||
|
public CallableMethodResource(MethodInfo methodInfo,Resource container)
|
||||||
|
:base(
|
||||||
|
methodInfo.GetCustomAttribute<WebCallable>() != null ?
|
||||||
|
(methodInfo.GetCustomAttribute<WebCallable>().Name != null) ? methodInfo.GetCustomAttribute<WebCallable>().Name : methodInfo.Name
|
||||||
|
:methodInfo.Name,
|
||||||
|
container
|
||||||
|
)
|
||||||
|
{
|
||||||
|
MethodInfo = methodInfo;
|
||||||
|
WebCallable = methodInfo.GetCustomAttribute<WebCallable>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private object[] ReadParameters(HttpRequest request)
|
||||||
|
{
|
||||||
|
ParameterInfo[] parameters = MethodInfo.GetParameters();
|
||||||
|
object[] p = new object[ parameters.Length ];
|
||||||
|
|
||||||
|
for (int n = 0; n < parameters.Length;n++)
|
||||||
|
{
|
||||||
|
ParameterInfo parameterInfo = parameters[n];
|
||||||
|
object pvalue = request.Query[parameterInfo.Name];
|
||||||
|
p[n] = Convert.ChangeType(pvalue, parameterInfo.ParameterType );
|
||||||
|
}
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SerializeResult(HttpRequest request,object result){
|
||||||
|
switch (WebCallable.Serialization){
|
||||||
|
case Serialization.PLAIN:
|
||||||
|
SerializePlain(request, result);
|
||||||
|
break;
|
||||||
|
case Serialization.JSON:
|
||||||
|
break;
|
||||||
|
case Serialization.XML:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SerializePlain(HttpRequest request,object result){
|
||||||
|
request.SetResponseHeader("Content-Type", "text/plain");
|
||||||
|
byte[] plain = Encoding.UTF8.GetBytes(result.ToString());
|
||||||
|
request.ResponseStream.Write(plain, 0, plain.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Hit(Stack<string> requestPath, HttpRequest request)
|
||||||
|
{
|
||||||
|
object[] p = ReadParameters(request);
|
||||||
|
object result = MethodInfo.Invoke(null, p);
|
||||||
|
SerializeResult(request, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
using System;
|
||||||
|
namespace appsrv.server
|
||||||
|
{
|
||||||
|
public class Application
|
||||||
|
{
|
||||||
|
public ApplicationServer ApplicationServer { get; private set; }
|
||||||
|
|
||||||
|
public Application(ApplicationServer applicationServer)
|
||||||
|
{
|
||||||
|
this.ApplicationServer = applicationServer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using appsrv.resources;
|
||||||
|
using System.Linq;
|
||||||
|
namespace appsrv.server
|
||||||
|
{
|
||||||
|
public class ApplicationServer
|
||||||
|
{
|
||||||
|
public Resource DefaultRoot { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
Dictionary<String, Resource> roots = new Dictionary<string, Resource>();
|
||||||
|
|
||||||
|
public ApplicationServer()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddRoot(String name,Resource rootResource){
|
||||||
|
roots.Add(name, rootResource);
|
||||||
|
if (DefaultRoot == null){
|
||||||
|
DefaultRoot = rootResource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Resource FindRoot(String rootName)
|
||||||
|
{
|
||||||
|
if (roots.ContainsKey(rootName))
|
||||||
|
return roots[rootName];
|
||||||
|
|
||||||
|
return DefaultRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HandleRequest(HttpRequest request){
|
||||||
|
Resource rootResource = FindRoot(request.Hostname);
|
||||||
|
Stack<String> requestPath = new Stack<string>(request.URI.AbsolutePath.Split(new char[] { '/' },StringSplitOptions.RemoveEmptyEntries).Reverse());
|
||||||
|
|
||||||
|
rootResource.Request(requestPath, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,229 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using appsrv.exceptions;
|
||||||
|
using appsrv.http;
|
||||||
|
|
||||||
|
namespace appsrv.server
|
||||||
|
{
|
||||||
|
public class HttpRequest
|
||||||
|
{
|
||||||
|
private Stream stream;
|
||||||
|
private StreamReader streamReader;
|
||||||
|
private List<HttpRequest> currentRequests = new List<HttpRequest>();
|
||||||
|
|
||||||
|
private MemoryStream responseStream;
|
||||||
|
private StreamWriter responseWriter;
|
||||||
|
|
||||||
|
Dictionary<String, String> requestHeaders = new Dictionary<string, string>();
|
||||||
|
Dictionary<String, String> responseHeaders = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
public EndPoint Client { get; private set; }
|
||||||
|
|
||||||
|
public ApplicationServer ApplicationServer { get; private set; }
|
||||||
|
public Uri URI { get; private set; }
|
||||||
|
|
||||||
|
public String Method { get; private set; }
|
||||||
|
public String RequestURL { get; private set; }
|
||||||
|
public String Protocol { get; private set; }
|
||||||
|
|
||||||
|
public String Hostname { get; private set; }
|
||||||
|
public int Port { get; private set; }
|
||||||
|
|
||||||
|
public QueryStringParameters Query { get; private set; }
|
||||||
|
|
||||||
|
|
||||||
|
public int StatusCode { get; set; } = 200;
|
||||||
|
|
||||||
|
public HttpRequest(ApplicationServer applicationServer, TcpClient client)
|
||||||
|
{
|
||||||
|
this.ApplicationServer = applicationServer;
|
||||||
|
|
||||||
|
this.Client = client.Client.RemoteEndPoint;
|
||||||
|
this.stream = client.GetStream();
|
||||||
|
this.streamReader = new StreamReader(this.stream);
|
||||||
|
|
||||||
|
String rLine = this.streamReader.ReadLine();
|
||||||
|
String[] rTokens = SplitWhitespace(
|
||||||
|
rLine
|
||||||
|
);
|
||||||
|
Console.WriteLine("Tokens: {0}", rTokens);
|
||||||
|
|
||||||
|
if (rTokens.Length != 3){
|
||||||
|
throw new IllegalRequestException(rLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Method = rTokens[0];
|
||||||
|
this.RequestURL = rTokens[1];
|
||||||
|
this.Protocol = rTokens[2];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Format("[HttpRequest: Client={0}, ApplicationServer={1}, Hostname={6} Port={7} URI={2}, Method={3}, RequestURL={4}, Protocol={5} Query={8}]", Client, ApplicationServer, URI, Method, RequestURL, Protocol, Hostname, Port,Query);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String GetRequestHeader(String name, String def = "")
|
||||||
|
{
|
||||||
|
name = name.ToLowerInvariant();
|
||||||
|
|
||||||
|
if (requestHeaders.ContainsKey(name))
|
||||||
|
return requestHeaders[name];
|
||||||
|
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String GetResponseHeader(String name, String def = "")
|
||||||
|
{
|
||||||
|
name = name.ToLowerInvariant();
|
||||||
|
|
||||||
|
if (responseHeaders.ContainsKey(name))
|
||||||
|
return responseHeaders[name];
|
||||||
|
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
public void SetResponseHeader(String name,String value){
|
||||||
|
name = name.ToLowerInvariant();
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
this.responseHeaders.Remove(name);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.responseHeaders[name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream ResponseStream
|
||||||
|
{
|
||||||
|
get {
|
||||||
|
if (responseStream == null)
|
||||||
|
responseStream = new MemoryStream();
|
||||||
|
|
||||||
|
return responseStream;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextWriter ResponseWriter
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (this.responseWriter == null)
|
||||||
|
{
|
||||||
|
this.responseWriter = new StreamWriter(this.responseStream);
|
||||||
|
}
|
||||||
|
return this.responseWriter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private String[] SplitWhitespace(String line){
|
||||||
|
LinkedList<String> tokens = new LinkedList<string>();
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
for (int n = 0; n < line.Length;)
|
||||||
|
{
|
||||||
|
for (; n < line.Length && !Char.IsWhiteSpace(line[n]); n++)
|
||||||
|
sb.Append(line[n]);
|
||||||
|
|
||||||
|
tokens.AddLast(sb.ToString());
|
||||||
|
sb.Clear();
|
||||||
|
|
||||||
|
for (; n < line.Length && Char.IsWhiteSpace(line[n]); n++) {}
|
||||||
|
}
|
||||||
|
return tokens.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReadHttpHeaders(){
|
||||||
|
String headerLine = this.streamReader.ReadLine();
|
||||||
|
String hName = null;
|
||||||
|
|
||||||
|
while (!headerLine.Equals(String.Empty)){
|
||||||
|
if (Char.IsWhiteSpace(headerLine[0]) && hName != null)
|
||||||
|
{
|
||||||
|
requestHeaders[hName] = requestHeaders[hName] + "," + headerLine.Trim();
|
||||||
|
} else {
|
||||||
|
String[] split = headerLine.Split(new char[] { ':' }, 2);
|
||||||
|
if (split.Length != 2){
|
||||||
|
throw new IllegalRequestException("malformed header");
|
||||||
|
}
|
||||||
|
|
||||||
|
hName = split[0].ToLowerInvariant();
|
||||||
|
requestHeaders[hName] = split[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
headerLine = this.streamReader.ReadLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (String hname in requestHeaders.Keys.ToArray()){
|
||||||
|
requestHeaders[hname] = requestHeaders[hname].Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InterpretRequestHeaders()
|
||||||
|
{
|
||||||
|
String host = GetRequestHeader("host");
|
||||||
|
String[] hostTokens = host.Split(':');
|
||||||
|
Hostname = hostTokens[0];
|
||||||
|
if (hostTokens.Length > 1){
|
||||||
|
Port = int.Parse(hostTokens[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
URI = new Uri(String.Format("http://{0}:{1}/{2}", Hostname, Port, RequestURL));
|
||||||
|
|
||||||
|
Query = new QueryStringParameters(URI.Query);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendResponse()
|
||||||
|
{
|
||||||
|
using (StreamWriter writer = new StreamWriter(this.stream))
|
||||||
|
{
|
||||||
|
ResponseStream.Position = 0;
|
||||||
|
SetResponseHeader("Content-Length", responseStream.Length.ToString());
|
||||||
|
|
||||||
|
writer.WriteLine("{0} {1} {2}", Protocol, StatusCode, HttpStatusCodes.GetStatusMessage(StatusCode));
|
||||||
|
foreach (String rhName in responseHeaders.Keys){
|
||||||
|
writer.WriteLine("{0}: {1}", rhName, responseHeaders[rhName]);
|
||||||
|
}
|
||||||
|
writer.WriteLine();
|
||||||
|
writer.Flush();
|
||||||
|
|
||||||
|
responseStream.CopyTo(this.stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Handle(){
|
||||||
|
this.ReadHttpHeaders();
|
||||||
|
this.InterpretRequestHeaders();
|
||||||
|
|
||||||
|
Console.WriteLine("Request Handled: {0}",this);
|
||||||
|
|
||||||
|
foreach (String key in this.requestHeaders.Keys){
|
||||||
|
Console.WriteLine("HH: {0} = {1}",key,this.requestHeaders[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplicationServer.HandleRequest(this);
|
||||||
|
|
||||||
|
SendResponse();
|
||||||
|
|
||||||
|
this.streamReader.Close();
|
||||||
|
this.stream.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
using System;
|
||||||
|
using appsrv.attributes;
|
||||||
|
namespace appsrv.test
|
||||||
|
{
|
||||||
|
|
||||||
|
public class StaticTest
|
||||||
|
{
|
||||||
|
|
||||||
|
[WebCallable]
|
||||||
|
public static int Add(int a,int b){
|
||||||
|
Console.WriteLine("StaticTest.Add({0},{1})", a, b);
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
[WebCallable]
|
||||||
|
public static void Debug()
|
||||||
|
{
|
||||||
|
Console.WriteLine("StaticTest.Debug() has been called");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>appsrv test page</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>appsrv test page</h1>
|
||||||
|
<p>A paragraph to see the file is displayed.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue