From 0ce3b3409361b51c03b0710825e37a057b26449e Mon Sep 17 00:00:00 2001 From: Harald Wolff Date: Fri, 20 Jul 2018 14:40:58 +0200 Subject: [PATCH] Initial Commit --- .gitignore | 41 ++++ Program.cs | 33 ++++ Properties/AssemblyInfo.cs | 26 +++ appsrv.csproj | 83 ++++++++ attributes/WebCallable.cs | 20 ++ connector/Connector.cs | 18 ++ connector/Http.cs | 84 +++++++++ exceptions/ApplicationServerException.cs | 28 +++ exceptions/IllegalRequestException.cs | 11 ++ exceptions/ResourceNotFoundException.cs | 11 ++ http/HttpStatusCodes.cs | 22 +++ http/QueryStringParameters.cs | 65 +++++++ mime/MimeHelper.cs | 26 +++ resources/DirectoryResource.cs | 56 ++++++ resources/FileResource.cs | 38 ++++ resources/Resource.cs | 141 ++++++++++++++ resources/ResourceLink.cs | 28 +++ resources/StaticClassResource.cs | 92 +++++++++ server/Application.cs | 13 ++ server/ApplicationServer.cs | 41 ++++ server/HttpRequest.cs | 229 +++++++++++++++++++++++ test/StaticTest.cs | 21 +++ www/index.html | 9 + 23 files changed, 1136 insertions(+) create mode 100644 .gitignore create mode 100644 Program.cs create mode 100644 Properties/AssemblyInfo.cs create mode 100644 appsrv.csproj create mode 100644 attributes/WebCallable.cs create mode 100644 connector/Connector.cs create mode 100644 connector/Http.cs create mode 100644 exceptions/ApplicationServerException.cs create mode 100644 exceptions/IllegalRequestException.cs create mode 100644 exceptions/ResourceNotFoundException.cs create mode 100644 http/HttpStatusCodes.cs create mode 100644 http/QueryStringParameters.cs create mode 100644 mime/MimeHelper.cs create mode 100644 resources/DirectoryResource.cs create mode 100644 resources/FileResource.cs create mode 100644 resources/Resource.cs create mode 100644 resources/ResourceLink.cs create mode 100644 resources/StaticClassResource.cs create mode 100644 server/Application.cs create mode 100644 server/ApplicationServer.cs create mode 100644 server/HttpRequest.cs create mode 100644 test/StaticTest.cs create mode 100644 www/index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf793ed --- /dev/null +++ b/.gitignore @@ -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 diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..c90245b --- /dev/null +++ b/Program.cs @@ -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(); + + } + } +} diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..2f21269 --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -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("")] diff --git a/appsrv.csproj b/appsrv.csproj new file mode 100644 index 0000000..a5298de --- /dev/null +++ b/appsrv.csproj @@ -0,0 +1,83 @@ + + + + Debug + x86 + {FD508FE5-5879-4C60-91D8-CA408E06361F} + Exe + appsrv + appsrv + v4.6.1 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + true + x86 + + + true + bin\Release + prompt + 4 + true + x86 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + + + + + + + + + + \ No newline at end of file diff --git a/attributes/WebCallable.cs b/attributes/WebCallable.cs new file mode 100644 index 0000000..3d01f28 --- /dev/null +++ b/attributes/WebCallable.cs @@ -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() + { + } + + } +} diff --git a/connector/Connector.cs b/connector/Connector.cs new file mode 100644 index 0000000..18bc252 --- /dev/null +++ b/connector/Connector.cs @@ -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(); + + } +} diff --git a/connector/Http.cs b/connector/Http.cs new file mode 100644 index 0000000..9f9e80a --- /dev/null +++ b/connector/Http.cs @@ -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"); + } + } + + + + + + } +} diff --git a/exceptions/ApplicationServerException.cs b/exceptions/ApplicationServerException.cs new file mode 100644 index 0000000..4375e17 --- /dev/null +++ b/exceptions/ApplicationServerException.cs @@ -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; + } + + } +} diff --git a/exceptions/IllegalRequestException.cs b/exceptions/IllegalRequestException.cs new file mode 100644 index 0000000..3ac55b8 --- /dev/null +++ b/exceptions/IllegalRequestException.cs @@ -0,0 +1,11 @@ +using System; +namespace appsrv.exceptions +{ + public class IllegalRequestException : Exception + { + public IllegalRequestException(String requestLine) + :base(requestLine) + { + } + } +} diff --git a/exceptions/ResourceNotFoundException.cs b/exceptions/ResourceNotFoundException.cs new file mode 100644 index 0000000..6392c8e --- /dev/null +++ b/exceptions/ResourceNotFoundException.cs @@ -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)) + { + } + } +} diff --git a/http/HttpStatusCodes.cs b/http/HttpStatusCodes.cs new file mode 100644 index 0000000..6a95dc8 --- /dev/null +++ b/http/HttpStatusCodes.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Remoting.Messaging; +namespace appsrv.http +{ + public class HttpStatusCodes + { + static Dictionary statusMessages = new Dictionary() + { + { 200, "Ok" }, + { 403, "Access denied" }, + { 404, "Not Found" } + }; + + public static String GetStatusMessage(int code){ + if (statusMessages.ContainsKey(code)) + return statusMessages[code]; + return "Unknown Status"; + } + + } +} diff --git a/http/QueryStringParameters.cs b/http/QueryStringParameters.cs new file mode 100644 index 0000000..36134bf --- /dev/null +++ b/http/QueryStringParameters.cs @@ -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 + { + Dictionary parameters = new Dictionary(); + + 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 Keys => parameters.Keys; + public ICollection 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 item) => throw new NotImplementedException(); + public void Clear() => throw new NotImplementedException(); + public bool Remove(string key) => throw new NotImplementedException(); + public bool Remove(KeyValuePair item) => throw new NotImplementedException(); + + public bool Contains(KeyValuePair item) => parameters.Contains(item); + public bool ContainsKey(string key) => parameters.ContainsKey(key); + public void CopyTo(KeyValuePair[] array, int arrayIndex) => ((IDictionary)parameters).CopyTo(array, arrayIndex); + public IEnumerator> 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(); + } + } +} diff --git a/mime/MimeHelper.cs b/mime/MimeHelper.cs new file mode 100644 index 0000000..bee8830 --- /dev/null +++ b/mime/MimeHelper.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +namespace appsrv.mime +{ + public static class MimeHelper + { + static Dictionary extToMIME = new Dictionary() + { + {"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"; + } + + } +} diff --git a/resources/DirectoryResource.cs b/resources/DirectoryResource.cs new file mode 100644 index 0000000..c6f134e --- /dev/null +++ b/resources/DirectoryResource.cs @@ -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 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); + } + } + } + + } +} diff --git a/resources/FileResource.cs b/resources/FileResource.cs new file mode 100644 index 0000000..a1463be --- /dev/null +++ b/resources/FileResource.cs @@ -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 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(); + } + } + } + + } +} diff --git a/resources/Resource.cs b/resources/Resource.cs new file mode 100644 index 0000000..4172a20 --- /dev/null +++ b/resources/Resource.cs @@ -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 resources = new Dictionary(); + + + 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 Resources { get => new HashSet(resources.Values); } + + public Resource Root { + get { + if (Container == null) + return this; + else + return Container.Root; + } + } + + public IList PathList { + get { + if (Container != null) + { + IList pl = Container.PathList; + pl.Add(Name); + return pl; + } else { + return new List(); + } + } + } + + 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 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 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 path) + { + return FindByPath(path.GetEnumerator()); + } + public Resource FindByPath(IEnumerator path) + { + if (path.MoveNext()) + { + return this[path.Current].FindByPath(path); + } + else + { + return this; + } + } + + + + protected void HandleException(ApplicationServerException ase){ + Console.WriteLine("ASE: " + ase.ToString()); + } + + + } +} diff --git a/resources/ResourceLink.cs b/resources/ResourceLink.cs new file mode 100644 index 0000000..a0848d2 --- /dev/null +++ b/resources/ResourceLink.cs @@ -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 requestPath, HttpRequest request) + { + Target.Request(requestPath, request); + } + + } +} diff --git a/resources/StaticClassResource.cs b/resources/StaticClassResource.cs new file mode 100644 index 0000000..3483b4e --- /dev/null +++ b/resources/StaticClassResource.cs @@ -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 callableFields = new Dictionary(); + + 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(); + 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() != null ? + (methodInfo.GetCustomAttribute().Name != null) ? methodInfo.GetCustomAttribute().Name : methodInfo.Name + :methodInfo.Name, + container + ) + { + MethodInfo = methodInfo; + WebCallable = methodInfo.GetCustomAttribute(); + } + + 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 requestPath, HttpRequest request) + { + object[] p = ReadParameters(request); + object result = MethodInfo.Invoke(null, p); + SerializeResult(request, result); + } + + } + } +} diff --git a/server/Application.cs b/server/Application.cs new file mode 100644 index 0000000..f0eaeec --- /dev/null +++ b/server/Application.cs @@ -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; + } + } +} diff --git a/server/ApplicationServer.cs b/server/ApplicationServer.cs new file mode 100644 index 0000000..df16c2e --- /dev/null +++ b/server/ApplicationServer.cs @@ -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 roots = new Dictionary(); + + 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 requestPath = new Stack(request.URI.AbsolutePath.Split(new char[] { '/' },StringSplitOptions.RemoveEmptyEntries).Reverse()); + + rootResource.Request(requestPath, request); + } + + } +} diff --git a/server/HttpRequest.cs b/server/HttpRequest.cs new file mode 100644 index 0000000..f11d9ac --- /dev/null +++ b/server/HttpRequest.cs @@ -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 currentRequests = new List(); + + private MemoryStream responseStream; + private StreamWriter responseWriter; + + Dictionary requestHeaders = new Dictionary(); + Dictionary responseHeaders = new Dictionary(); + + 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 tokens = new LinkedList(); + 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(); + } + } +} diff --git a/test/StaticTest.cs b/test/StaticTest.cs new file mode 100644 index 0000000..8af9ae2 --- /dev/null +++ b/test/StaticTest.cs @@ -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"); + } + } +} diff --git a/www/index.html b/www/index.html new file mode 100644 index 0000000..fe18bbf --- /dev/null +++ b/www/index.html @@ -0,0 +1,9 @@ + + + appsrv test page + + +

appsrv test page

+

A paragraph to see the file is displayed.

+ + \ No newline at end of file