From 259c1275a5844935e5edb0a0067986b95100011d Mon Sep 17 00:00:00 2001 From: "U-WALDRENNACH\\haraldwolff" Date: Tue, 17 Nov 2020 23:46:07 +0100 Subject: [PATCH] Initial Commit --- .gitignore | 41 ++ AuthenticationProvider.cs | 38 + AuthorizationMask.cs | 22 + HTTPServer.cs | 245 +++++++ HTTPServerConnection.cs | 157 ++++ HttpCookie.cs | 41 ++ HttpHeader.cs | 31 + HttpHeaders.cs | 36 + HttpReader.cs | 320 ++++++++ HttpRequest.cs | 272 +++++++ HttpResponse.cs | 135 ++++ HttpRouter.cs | 36 + HttpStatusCodes.cs | 25 + HttpUser.cs | 30 + IHTTPResource.cs | 8 + IHttpRouter.cs | 9 + QueryStringParameters.cs | 72 ++ cert/CertContainer.cs | 59 ++ client/CookieContainer.cs | 41 ++ client/HttpClient.cs | 18 + client/HttpClientRequest.cs | 107 +++ client/HttpClientResponse.cs | 13 + connections/Connection.cs | 86 +++ connections/HttpConnection.cs | 41 ++ connections/HttpsConnection.cs | 46 ++ exceptions/BadRequestException.cs | 20 + exceptions/DisposeConnectionException.cs | 10 + exceptions/HttpException.cs | 29 + exceptions/IllegalRequestException.cs | 11 + exceptions/MethodNotAllowedException.cs | 21 + exceptions/ResourceNotFoundException.cs | 11 + exceptions/UnsupportedMediaTypeException.cs | 20 + io/UnbufferedStreamreader.cs | 68 ++ listener/HttpListener.cs | 53 ++ listener/HttpsListener.cs | 27 + listener/Listener.cs | 43 ++ ln.http.csproj | 24 + message/Header.cs | 159 ++++ message/HeaderContainer.cs | 76 ++ message/Message.cs | 153 ++++ message/MimeTypeMap.cs | 767 ++++++++++++++++++++ message/TokenReader.cs | 68 ++ message/parser/HTTP.cs | 40 + message/parser/MIME.cs | 43 ++ mime/MimeTypeMap.cs | 767 ++++++++++++++++++++ rest/CRUDObjectContainer.RegisteredType.cs | 71 ++ rest/CRUDObjectContainer.cs | 98 +++ rest/FieldDescriptor.cs | 47 ++ rest/FieldValueList.cs | 23 + rest/RestAPIAdapter.cs | 22 + router/FileRouter.cs | 35 + router/HttpRoutingContext.cs | 53 ++ router/LoggingRouter.cs | 59 ++ router/RouterTarget.cs | 61 ++ router/SimpleRouter.cs | 97 +++ router/StaticRouter.cs | 67 ++ router/VirtualHostRouter.cs | 48 ++ router/WebsocketRouter.cs | 32 + session/Session.cs | 69 ++ session/SessionCache.cs | 75 ++ websocket/WebSocket.cs | 201 +++++ websocket/WebSocketEventArgs.cs | 36 + websocket/WebSocketFrame.cs | 171 +++++ 63 files changed, 5604 insertions(+) create mode 100644 .gitignore create mode 100644 AuthenticationProvider.cs create mode 100644 AuthorizationMask.cs create mode 100644 HTTPServer.cs create mode 100644 HTTPServerConnection.cs create mode 100644 HttpCookie.cs create mode 100644 HttpHeader.cs create mode 100644 HttpHeaders.cs create mode 100644 HttpReader.cs create mode 100644 HttpRequest.cs create mode 100644 HttpResponse.cs create mode 100644 HttpRouter.cs create mode 100644 HttpStatusCodes.cs create mode 100644 HttpUser.cs create mode 100644 IHTTPResource.cs create mode 100644 IHttpRouter.cs create mode 100644 QueryStringParameters.cs create mode 100644 cert/CertContainer.cs create mode 100644 client/CookieContainer.cs create mode 100644 client/HttpClient.cs create mode 100644 client/HttpClientRequest.cs create mode 100644 client/HttpClientResponse.cs create mode 100644 connections/Connection.cs create mode 100644 connections/HttpConnection.cs create mode 100644 connections/HttpsConnection.cs create mode 100644 exceptions/BadRequestException.cs create mode 100644 exceptions/DisposeConnectionException.cs create mode 100644 exceptions/HttpException.cs create mode 100644 exceptions/IllegalRequestException.cs create mode 100644 exceptions/MethodNotAllowedException.cs create mode 100644 exceptions/ResourceNotFoundException.cs create mode 100644 exceptions/UnsupportedMediaTypeException.cs create mode 100644 io/UnbufferedStreamreader.cs create mode 100644 listener/HttpListener.cs create mode 100644 listener/HttpsListener.cs create mode 100644 listener/Listener.cs create mode 100644 ln.http.csproj create mode 100644 message/Header.cs create mode 100644 message/HeaderContainer.cs create mode 100644 message/Message.cs create mode 100644 message/MimeTypeMap.cs create mode 100644 message/TokenReader.cs create mode 100644 message/parser/HTTP.cs create mode 100644 message/parser/MIME.cs create mode 100644 mime/MimeTypeMap.cs create mode 100644 rest/CRUDObjectContainer.RegisteredType.cs create mode 100644 rest/CRUDObjectContainer.cs create mode 100644 rest/FieldDescriptor.cs create mode 100644 rest/FieldValueList.cs create mode 100644 rest/RestAPIAdapter.cs create mode 100644 router/FileRouter.cs create mode 100644 router/HttpRoutingContext.cs create mode 100644 router/LoggingRouter.cs create mode 100644 router/RouterTarget.cs create mode 100644 router/SimpleRouter.cs create mode 100644 router/StaticRouter.cs create mode 100644 router/VirtualHostRouter.cs create mode 100644 router/WebsocketRouter.cs create mode 100644 session/Session.cs create mode 100644 session/SessionCache.cs create mode 100644 websocket/WebSocket.cs create mode 100644 websocket/WebSocketEventArgs.cs create mode 100644 websocket/WebSocketFrame.cs 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/AuthenticationProvider.cs b/AuthenticationProvider.cs new file mode 100644 index 0000000..f9922dc --- /dev/null +++ b/AuthenticationProvider.cs @@ -0,0 +1,38 @@ +// /** +// * File: AuthenticationProvider.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.Collections.Generic; +namespace ln.http +{ + public abstract class AuthenticationProvider + { + public AuthenticationProvider() + { + } + + public abstract IEnumerable EnumerateUsers(); + public abstract HttpUser Authenticate(HttpRequest httpRequest); + + public virtual HttpUser GetHttpUser(String authenticationName) + { + foreach (HttpUser httpUser in EnumerateUsers()) + { + if (httpUser.AuthenticationName.Equals(authenticationName)) + { + return httpUser; + } + } + throw new KeyNotFoundException(); + } + + + + } +} diff --git a/AuthorizationMask.cs b/AuthorizationMask.cs new file mode 100644 index 0000000..d77d21f --- /dev/null +++ b/AuthorizationMask.cs @@ -0,0 +1,22 @@ +// /** +// * File: AuthorizationMask.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +namespace ln.http +{ + public static class AuthorizationMask + { + public static readonly long A_ACCESS = (1l << 0); + public static readonly long A_READ = (1l << 0); + public static readonly long A_WRITE = (1l << 0); + public static readonly long A_EXEC = (1l << 0); + + public static readonly long A_SUPER = (-1); + } +} diff --git a/HTTPServer.cs b/HTTPServer.cs new file mode 100644 index 0000000..57cf82b --- /dev/null +++ b/HTTPServer.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections.Generic; +using ln.logging; +using ln.threading; +using ln.application; +using ln.http.listener; +using ln.http.connections; +using System.Globalization; +using ln.http.exceptions; +using System.Threading; +using ln.type; +using ln.http.router; + + +namespace ln.http +{ + public class HTTPServer + { + public static int backlog = 5; + public static int defaultPort = 8080; + public static bool exclusivePortListener = false; + + public IHttpRouter Router { get; set; } + + public bool IsRunning => !shutdown && (threadPool.CurrentPoolSize > 0); + public Logger Logger { get; set; } + + bool shutdown = false; + + List listeners = new List(); + public Listener[] Listeners => listeners.ToArray(); + + DynamicPool threadPool; + public DynamicPool ThreadPool => threadPool; + + HashSet currentConnections = new HashSet(); + public IEnumerable CurrentConnections => currentConnections; + + public HTTPServer() + { + Logger = Logger.Default; + threadPool = new DynamicPool(1024); + } + public HTTPServer(IHttpRouter router) + : this() + { + Router = router; + } + public HTTPServer(Listener listener, IHttpRouter router) + : this(router) + { + AddListener(listener); + } + public HTTPServer(Endpoint endpoint, IHttpRouter router) + : this(new HttpListener(endpoint), router) { } + + public void AddListener(Listener listener) + { + listeners.Add(listener); + if (IsRunning) + StartListener(listener); + } + + public void StartListener(Listener listener) + { + listener.Open(); + + threadPool.Enqueue( + () => listener.AcceptMany( + (connection) => threadPool.Enqueue( + () => this.HandleConnection(connection) + ) + ) + ); + } + + public void StopListener(Listener listener) + { + listener.Close(); + } + + + public void AddEndpoint(Endpoint endpoint) + { + AddListener(new HttpListener(endpoint)); + } + + public void Start() + { + threadPool.Start(); + + foreach (Listener listener in listeners) + StartListener(listener); + } + + public void Stop() + { + lock (this) + { + this.shutdown = true; + } + foreach (Listener listener in listeners) + StopListener(listener); + + for (int n = 0; n < 150; n++) + { + lock (currentConnections) + { + if (currentConnections.Count == 0) + break; + if ((n % 20) == 0) + { + Logging.Log(LogLevel.INFO, "HTTPServer: still waiting for {0} connections to close", currentConnections.Count); + } + } + Thread.Sleep(100); + } + + lock (currentConnections) + { + foreach (Connection connection in currentConnections) + { + connection.Close(); + } + } + + threadPool.Stop(true); + } + + private void HandleConnection(Connection connection) + { + lock (this.currentConnections) + currentConnections.Add(connection); + + try + { + HttpRequest httpRequest = null; + bool keepAlive = true; + try + { + do + { + httpRequest = connection.ReadRequest(this); + if (httpRequest == null) + break; + + HttpResponse response; + try + { + response = Router.Route(new HttpRoutingContext(httpRequest),httpRequest); + } + catch (HttpException httpExc) + { + response = new HttpResponse(httpRequest); + response.StatusCode = httpExc.StatusCode; + response.StatusMessage = httpExc.Message; + response.ContentWriter.WriteLine(httpExc.Message); + } + + if (response != null) + { + keepAlive = httpRequest.GetRequestHeader("connection", "keep-alive").Equals("keep-alive") && response.GetHeader("connection", "keep-alive").Equals("keep-alive"); + response.SetHeader("connection", keepAlive ? "keep-alive" : "close"); + + connection.SendResponse(response); + } + else + { + keepAlive = false; + } + + response?.ContentStream?.Dispose(); + + } while (keepAlive); + } + catch (Exception e) + { + Logging.Log(e); + if (httpRequest != null) + { + HttpResponse httpResponse = new HttpResponse(httpRequest); + httpResponse.StatusCode = 500; + httpResponse.ContentWriter.WriteLine("500 Internal Server Error"); + httpResponse.ContentWriter.Flush(); + + connection.SendResponse(httpResponse); + } + } + + HttpRequest.ClearCurrent(); + connection.GetStream().Close(); + } finally + { + lock (currentConnections) + currentConnections.Remove(connection); + } + } + + public void Log(DateTime startTime,double duration,HttpRequest httpRequest,HttpResponse httpResponse) + { + Logger.Log(LogLevel.INFO, "{0} {1} {2} {3}",startTime.ToString("yyyyMMdd-HH:mm:ss"),duration.ToString(CultureInfo.InvariantCulture),httpRequest.Hostname,httpRequest.RequestURL); + } + + + + public static void StartSimpleServer(string[] arguments) + { + ArgumentContainer argumentContainer = new ArgumentContainer(new Argument[] + { + new Argument('p',"port",8080), + new Argument('l',"listen","127.0.0.1"), + new Argument('c', "catch",null) + }); + + argumentContainer.Parse(ref arguments); + + SimpleRouter router = new SimpleRouter(); + router.AddSimpleRoute("/*", new RouterTarget((request) => + { + HttpResponse response = new HttpResponse(request); + response.StatusCode = 404; + response.SetHeader("content-type", "text/plain"); + response.ContentWriter.WriteLine("404 Not Found"); + response.ContentWriter.Flush(); + return response; + }), -100); + + foreach (String path in arguments) + { + StaticRouter staticRouter = new StaticRouter(path); + staticRouter.AddIndex("index.html"); + staticRouter.AddIndex("index.htm"); + router.AddSimpleRoute("/*", staticRouter); + } + + if (argumentContainer['c'].Value != null) + router.AddSimpleRoute("/*", new RouterTarget((request) => router.Route(new HttpRoutingContext(request, argumentContainer['c'].Value), request)), 0); + + HTTPServer server = new HTTPServer(new Endpoint(IPv6.Parse(argumentContainer['l'].Value),int.Parse(argumentContainer['p'].Value)), + new LoggingRouter(router)); + server.Start(); + } + + } +} diff --git a/HTTPServerConnection.cs b/HTTPServerConnection.cs new file mode 100644 index 0000000..b99a875 --- /dev/null +++ b/HTTPServerConnection.cs @@ -0,0 +1,157 @@ +using System; +using System.Net.Sockets; +using ln.threading; +using System.Net; +using ln.type; +using ln.logging; +using System.IO; +using System.Threading; +using System.Collections.Generic; +using System.Linq; +namespace ln.http +{ + //public delegate void HTTPServerConnectionEvent(HTTPServerConnection connection); + + //public class HTTPServerConnection : PoolJob + //{ + // public static ThreadLocal Current { get; } = new ThreadLocal(); + // static HashSet currentConnections = new HashSet(); + // public static HTTPServerConnection[] CurrentConnections => currentConnections.ToArray(); + + // public HTTPServer HTTPServer { get; } + // public TcpClient TcpClient { get; } + + // public HttpRequest CurrentRequest { get; protected set; } + + // public event HTTPServerConnectionEvent AbortRequested; + + // public DateTime Created { get; } + // public DateTime Interpreted { get; set; } + // public DateTime Finished { get; set; } + + // public HTTPServerConnection(HTTPServer httpServer,TcpClient tcpClient) + // { + // HTTPServer = httpServer; + // TcpClient = tcpClient; + // Created = DateTime.Now; + // } + + // public virtual HttpResponse GetResponse(HttpRequest httpRequest,HttpApplication httpApplication) => httpApplication.GetResponse(httpRequest); + + // public virtual void Abort() + // { + // if (AbortRequested != null) + // AbortRequested(this); + // } + + + // public override void RunJob() + // { + // HTTPServerConnection saveCurrent = Current.Value; + // Current.Value = this; + // lock (currentConnections) + // currentConnections.Add(this); + + // try + // { + // setState("reading http request"); + + // HttpReader httpReader = new HttpReader(TcpClient.GetStream()); + // httpReader.Read(); + + // if (!httpReader.Valid) + // return; + + // HttpResponse response = null; + + // using (CurrentRequest = new HttpRequest(this.HTTPServer,httpReader, (IPEndPoint)TcpClient.Client.LocalEndPoint)) + // { + // Interpreted = DateTime.Now; + + // try + // { + // HttpApplication application = HTTPServer.GetHttpApplication(new URI(CurrentRequest.BaseURI.ToString())); + + // application.Authenticate(CurrentRequest); + // application.Authorize(CurrentRequest); + + // setState("handling http request"); + + // response = GetResponse(CurrentRequest, application); + // } + // catch (Exception e) + // { + // setState("handling exception"); + + // response = new HttpResponse(CurrentRequest, "text/plain"); + // response.StatusCode = 500; + // response.ContentWriter.WriteLine("Exception caught: {0}", e); + // } + + // setState("sending response"); + + // if (response == null) + // { + // Logging.Log(LogLevel.DEBUG, "Request {0} returned no Response", CurrentRequest); + // } + // else + // { + // if (!response.HasCustomContentStream) + // { + // response.ContentWriter.Flush(); + // MemoryStream cstream = (MemoryStream)response.ContentStream; + // cstream.Position = 0; + + // response.SetHeader("content-length", cstream.Length.ToString()); + // } + + // if (CurrentRequest.Session != null) + // HTTPServer?.SessionCache?.ApplySessionID(response, CurrentRequest.Session); + + // response.AddCookie("LN_SEEN", DateTime.Now.ToString()); + + // SendResponse(TcpClient.GetStream(), response); + // TcpClient.Close(); + + // Finished = DateTime.Now; + + // HTTPServer.Log(Created, (Finished - Created).TotalMilliseconds, CurrentRequest, response); + // } + // } + // } + // finally + // { + // Current.Value = saveCurrent; + // lock (currentConnections) + // currentConnections.Remove(this); + // } + // } + + // public static void SendResponse(Stream stream, HttpResponse response) + // { + // StreamWriter streamWriter = new StreamWriter(stream); + // streamWriter.NewLine = "\r\n"; + + // streamWriter.WriteLine("{0} {1} {2}", response.HttpRequest.Protocol, response.StatusCode, response.StatusMessage); + // foreach (String headerName in response.GetHeaderNames()) + // { + // streamWriter.WriteLine("{0}: {1}", headerName, response.GetHeader(headerName)); + // } + + // foreach (HttpCookie httpCookie in response.Cookies) + // { + // streamWriter.WriteLine("Set-Cookie: {0}", httpCookie.ToString()); + // } + + // streamWriter.WriteLine(); + // streamWriter.Flush(); + + // response.ContentStream.CopyTo(stream); + // response.ContentStream.Close(); + // response.ContentStream.Dispose(); + + // streamWriter.Flush(); + // } + + //} +} diff --git a/HttpCookie.cs b/HttpCookie.cs new file mode 100644 index 0000000..f836f94 --- /dev/null +++ b/HttpCookie.cs @@ -0,0 +1,41 @@ +using System; +using System.IO; +using System.Text; +namespace ln.http +{ + public class HttpCookie + { + public String Name { get; set; } + public String Value { get; set; } + + public bool Secure { get; set; } + public DateTime Expires { get; set; } + public String Path { get; set; } + public String Domain { get; set; } + + public HttpCookie(String name) + { + Name = name; + } + public HttpCookie(String name,String value) + { + Name = name; + Value = value; + } + + public override string ToString() + { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.AppendFormat("{0}={1}", Name, Value); + if (Secure) + stringBuilder.Append(";SECURE"); + + if (Path != null) + stringBuilder.AppendFormat(";Path={0}", Path); + + return stringBuilder.ToString(); + } + + + } +} diff --git a/HttpHeader.cs b/HttpHeader.cs new file mode 100644 index 0000000..2779d32 --- /dev/null +++ b/HttpHeader.cs @@ -0,0 +1,31 @@ +using System; +namespace ln.http +{ + public class HttpHeader + { + public String Name { get; } + public String Value { get; } + + public HttpHeader(String rawHeader) + { + int colon = rawHeader.IndexOf(':'); + if (colon < 0) + throw new FormatException("rawHeader must contain at least one colon"); + + Name = rawHeader.Substring(0, colon).Trim().ToUpper(); + Value = rawHeader.Substring(colon + 1).Trim(); + } + + public HttpHeader(String headerName,String headerValue) + { + if (String.Empty.Equals(headerName)) + throw new ArgumentException("headerName needs to contain at least one character", nameof(headerName)); + + Name = headerName.ToUpper(); + Value = headerValue.ToUpper(); + } + + + + } +} diff --git a/HttpHeaders.cs b/HttpHeaders.cs new file mode 100644 index 0000000..16c6f9f --- /dev/null +++ b/HttpHeaders.cs @@ -0,0 +1,36 @@ +using System; +using ln.collections; +using System.Collections.Generic; +using System.Collections; + +namespace ln.http +{ + public class HttpHeaders : IEnumerable + { + MappingBTree headers = new MappingBTree((value) => value.Name); + + public HttpHeaders() + { + } + + public HttpHeader this[string headerName] => headers[headerName.ToUpper()]; + public bool Contains(string headerName) => headers.ContainsKey(headerName.ToUpper()); + + public void Add(String headerName, String headerValue) => Add(new HttpHeader(headerName, headerValue)); + public void Add(HttpHeader httpHeader) => headers.Add(httpHeader); + + public void Remove(HttpHeader httpHeader) => headers.Remove(httpHeader); + public void Remove(string headerName) => headers.RemoveKey(headerName.ToUpper()); + + public void Set(String headerName, String headerValue) + { + if (headers.ContainsKey(headerName)) + Remove(headerName); + + Add(new HttpHeader(headerName, headerValue)); + } + + public IEnumerator GetEnumerator() => headers.Values.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => headers.Values.GetEnumerator(); + } +} diff --git a/HttpReader.cs b/HttpReader.cs new file mode 100644 index 0000000..ce3aab6 --- /dev/null +++ b/HttpReader.cs @@ -0,0 +1,320 @@ +using System; +using System.IO; +using System.Text; +using System.Collections.Generic; +using System.Net; +using ln.type; +using ln.http.message; +using ln.http.io; +using ln.http.message.parser; + +namespace ln.http +{ + //public class HttpReader + //{ + // delegate bool ReadCondition(int b); + + // public Stream Stream { get; } + + // private byte[] buffer = new byte[8192]; + // private int hlen; + // private int blen; + // private int bptr; + + // public Endpoint RemoteEndpoint { get; private set; } + + // public HeaderContainer Headers { get; private set; } + + // public String Method { get; private set; } + // public String URL { get; private set; } + // public String Protocol { get; private set; } + + // //public Dictionary Headers { get; } = new Dictionary(); + + // public bool Valid { get; private set; } = false; + + // public HttpReader(Stream stream) + // { + // Stream = stream; + // } + // public HttpReader(Stream stream,Endpoint remoteEndpoint) + // { + // Stream = stream; + // RemoteEndpoint = remoteEndpoint; + // } + + // public void Read() + // { + // UnbufferedStreamReader reader = new UnbufferedStreamReader(Stream); + // string requestLine = reader.ReadLine(); + // string[] requestTokens = requestLine.Split(new char[0], StringSplitOptions.RemoveEmptyEntries); + + // if (requestTokens.Length != 3) + // throw new FormatException("request line malformed"); + + // Method = requestTokens[0]; + // URL = requestTokens[1]; + // Protocol = requestTokens[2]; + + // Headers = HTTP.ReadHeader(reader); + + // Valid = true; + // } + + // public int ReadByte() + // { + // if (bptr >= blen) + // return -1; + // return buffer[bptr++]; + // } + + // public int Current + // { + // get + // { + // if (bptr >= blen) + // return -1; + // return buffer[bptr]; + // } + // } + + // public void Reverse() + // { + // if ((bptr > 0) && (bptr < blen)) + // bptr--; + // } + + // public void ReadRequestHead() + // { + // bptr = 0; + // do + // { + // int rlen = Stream.Read(buffer, blen, buffer.Length - blen); + // if (rlen == 0) + // throw new IOException(); + + // blen += rlen; + // while (bptr <= (blen - 4)) + // { + // if ( + // (buffer[bptr + 0] == '\r') && + // (buffer[bptr + 1] == '\n') && + // (buffer[bptr + 2] == '\r') && + // (buffer[bptr + 3] == '\n') + // ) + // { + // hlen = bptr; + // bptr = 0; + // return; + // } + // bptr++; + // } + + // byte[] nbuffer = new byte[buffer.Length << 1]; + // Array.Copy(buffer, nbuffer, buffer.Length); + // buffer = nbuffer; + // } while (blen >= buffer.Length); + // bptr = 0; + // } + + // private String ReadConditional(ReadCondition readCondition,bool reverse = true) + // { + // StringBuilder stringBuilder = new StringBuilder(); + // int b = ReadByte(); + // while ((b != -1) && (readCondition(b)) ) + // { + // stringBuilder.Append((char)b); + // b = ReadByte(); + // } + // if (reverse) + // Reverse(); + + // return stringBuilder.ToString(); + // } + + // public void SkipWhiteSpace() + // { + // while (Char.IsWhiteSpace((char)ReadByte())) { }; + // Reverse(); + // } + + // public String ReadToken() + // { + // return ReadConditional((b) => !Char.IsWhiteSpace((char)b)); + // } + + // public String ReadLine() + // { + // int p = bptr; + // while (p < blen - 1) + // { + // if ((buffer[p] == '\r') && (buffer[p + 1] == '\n')) + // { + // break; + // } + // p++; + // } + + // string result = Encoding.ASCII.GetString(buffer, bptr, p - bptr); + // bptr = p + 2; + // return result; + // } + + // public String ReadHeaderName() + // { + // return ReadConditional((b) => b != ':', false).ToUpper(); + // } + // public String ReadHeaderValue() + // { + // String value = ReadLine(); + // while (Char.IsWhiteSpace((char)Current)) + // { + // value = value + ReadLine(); + // } + // return value.Trim(); + // } + + // //public void ReadHeaders() + // //{ + // // while (bptr < hlen) + // // { + // // String name = ReadHeaderName(); + // // String value = ReadHeaderValue(); + + // // Headers.Add(name, value); + // // } + // //} + + // public int ReadRequestBody(byte[] dst,int offset,int length) + // { + // //int nRead = 0; + // //if (bptr < blen) + // //{ + // // int len = Math.Min(length, blen - bptr); + // // Array.Copy(buffer, bptr, dst, offset, len); + // // bptr += len; + // // length -= len; + // // offset += len; + // // nRead += len; + // //} + // int nRead = 0; + // while (length > 0) + // { + // int nr = Stream.Read(dst, offset, length); + // if (nr > 0) + // { + // nRead += nr; + // length -= nr; + // offset += nr; + // } + // } + // return nRead; + // } + + + + // /* + // byte[] buffer; + // int blen = 0; + // int bptr = 0; + + // public HttpReader(Stream stream) + // { + // Stream = stream; + // buffer = new byte[1024]; + // } + // public HttpReader(Stream stream, int buffersize) + // { + // Stream = stream; + // buffer = new byte[buffersize]; + // } + + // private int read() + // { + // if (bptr >= blen) + // { + // bptr = 0; + // blen = Stream.Read(buffer, 0, buffer.Length); + // if (blen <= 0) + // throw new EndOfStreamException(); + // } + // return buffer[bptr++]; + // } + + // private int ReadTo(byte[] b,int offset,byte[] mark) + // { + // int pm = 0; + // int p = offset; + // while (p < b.Length) + // { + // b[p] = (byte)read(); + // if (b[p] == mark[pm]) + // { + // pm++; + // if (pm >= mark.Length) + // { + // p++; + // break; + // } + // } + // else + // { + // pm = 0; + // } + // p++; + // } + // return p; + // } + + + // public String ReadRequestLine(int maxSize = 1024) + // { + // byte[] b = new byte[maxSize]; + // int l = ReadTo(b, 0, new byte[] { 0x0d, 0x0a }); + // return Encoding.ASCII.GetString(b, 0, l); + // } + + // public Dictionary ReadHTTPHeaders() + // { + // byte[] b = new byte[8192]; + // int hlen = ReadTo(b, 0, new byte[] { 0x0d, 0x0a, 0x0d, 0x0a }); + + // Dictionary headers = new Dictionary(); + + // string rawHeaders = Encoding.ASCII.GetString(b, 0, hlen); + // String[] rawLines = rawHeaders.Split(new String[] { "\r\n" }, StringSplitOptions.None); + + // for (int n = rawLines.Length-1; n >= 0 ; n--) + // { + // if ((rawLines[n].Length > 0) && (Char.IsWhiteSpace(rawLines[n][0]))) + // { + // rawLines[n - 1] = rawLines[n - 1] + rawLines[n]; + // rawLines[n] = null; + // } + // } + + // foreach (String rawLine in rawLines) + // { + // if (rawLine != null) + // { + // int colon = rawLine.IndexOf(':'); + // if (colon > 0) + // { + // String name = rawLine.Substring(0, colon).Trim().ToUpper(); + // String value = rawLine.Substring(colon + 1).Trim(); + // headers.Add(name, value); + // } + // } + // } + + // return headers; + // } + + // public byte[] ReadRequestBody(byte[] buffer) + // { + // return null; + // } + // */ + //} + +} diff --git a/HttpRequest.cs b/HttpRequest.cs new file mode 100644 index 0000000..29b3d50 --- /dev/null +++ b/HttpRequest.cs @@ -0,0 +1,272 @@ +using System; +using System.IO; +using System.Collections.Generic; +using System.Linq; +using ln.type; +using ln.http.exceptions; +using System.Threading; +using ln.http.session; +using ln.http.message; +using ln.http.io; +using ln.http.message.parser; + +namespace ln.http +{ + public class HttpRequest : IDisposable + { + static ThreadLocal current = new ThreadLocal(); + static public HttpRequest Current => current.Value; + + //Dictionary requestHeaders; + HeaderContainer requestHeaders; + Dictionary requestCookies; + Dictionary requestParameters; + + public HTTPServer HTTPServer { get; } + + public Endpoint RemoteEndpoint { get; private set; } + public Endpoint LocalEndpoint { get; private set; } + + public Uri BaseURI { get; 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 Session Session { get; set; } + public HttpUser CurrentUser => Session.CurrentUser; + + public HeaderContainer RequestHeaders => requestHeaders; + + MemoryStream contentStream; + public MemoryStream ContentStream + { + get + { + if (contentStream == null) + ReadRequestBody(); + return contentStream; + } + } + public TextReader ContentReader + { + get + { + if (contentReader == null) + contentReader = new StreamReader(ContentStream); + return contentReader; + } + } + + int requestBodyLength; + byte[] requestBody; + StreamReader contentReader; + + Stream connectionStream; + UnbufferedStreamReader connectionReader; + + public Stream GetConnectionStream() => connectionStream; + + public HttpRequest(HTTPServer httpServer, Stream clientStream, Endpoint localEndpoint, Endpoint remoteEndpoint) + { + HTTPServer = httpServer; + connectionStream = clientStream; + connectionReader = new UnbufferedStreamReader(connectionStream); + + LocalEndpoint = localEndpoint; + RemoteEndpoint = remoteEndpoint; + + ReadRequestLine(); + + requestHeaders = HTTP.ReadHeader(connectionReader); + requestCookies = new Dictionary(); + requestParameters = new Dictionary(); + + Setup(); + + requestBodyLength = int.Parse(GetRequestHeader("content-length", "0")); + } + + void ReadRequestLine() + { + string requestLine = connectionReader.ReadLine(); + string[] requestTokens = requestLine.Split(new char[0], StringSplitOptions.RemoveEmptyEntries); + + if (requestTokens.Length != 3) + throw new BadRequestException(); + + Method = requestTokens[0]; + RequestURL = requestTokens[1]; + Protocol = requestTokens[2]; + } + + public void ReadRequestBody() + { + requestBody = new byte[requestBodyLength]; + + if (requestBodyLength > 0) + { + int nRead = 0; + int length = requestBodyLength; + + while (length > 0) + { + int nr = connectionStream.Read(requestBody, nRead, length); + if (nr > 0) + { + nRead += nr; + length -= nr; + } + } + } + + contentStream = new MemoryStream(requestBody); + } + + public void FinishRequest() + { + if ((requestBodyLength > 0) && (requestBody == null)) + { + int nRead = 0; + int length = requestBodyLength; + byte[] discard = new byte[8192]; + + while (length > 0) + { + int nr = connectionStream.Read(discard,0,length > discard.Length ? discard.Length : length); + if (nr > 0) + { + nRead += nr; + length -= nr; + } + } + } + } + + public void MakeCurrent() => current.Value = this; + public static void ClearCurrent() => current.Value = null; + + private void Setup() + { + SetupResourceURI(); + SetupCookies(); + } + + /* + * SetupResourceURI() + * + * Setup the following fields: + * + * - Hostname + * - Port + * - BaseURI + * - URI + * - Query + * + */ + private void SetupResourceURI() + { + String host = GetRequestHeader("HOST"); + String[] hostTokens = host.Split(':'); + + Hostname = hostTokens[0]; + Port = (hostTokens.Length > 1) ? int.Parse(hostTokens[1]) : LocalEndpoint.Port; + BaseURI = new UriBuilder("http", Hostname, Port).Uri; + URI = new Uri(BaseURI, RequestURL); + Query = new QueryStringParameters(URI.Query); + } + + private void SetupCookies() + { + string cookies = GetRequestHeader("COOKIE"); + foreach (String cookie in cookies.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) + { + string[] c = cookie.Split(new char[] { '=' }, 2); + string cn, cv; + cn = c[0].Trim(); + if (c.Length > 1) + cv = c[1].Trim(); + else + cv = ""; + + if (!this.requestCookies.ContainsKey(cn)) + this.requestCookies.Add(cn, cv); + } + } + + public override string ToString() + { + //return string.Format("[HttpRequest: RemoteEndpoint={0}, Hostname={1} Port={2} URI={4}, Method={4}, RequestURL={5}, Protocol={6} Query={7}]", RemoteEndpoint, URI, Method, RequestURL, Protocol, Hostname, Port,Query); + return base.ToString(); + } + + public String GetRequestHeader(String name) + { + return GetRequestHeader(name, ""); + } + public String GetRequestHeader(String name, String def) + { + name = name.ToUpper(); + + if (requestHeaders.ContainsKey(name)) + return requestHeaders[name].Value; + + return def; + } + + public String[] RequestHeaderNames => requestHeaders.Keys.ToArray(); + + public String[] CookieNames => requestCookies.Keys.ToArray(); + public bool ContainsCookie(String name) + { + return this.requestCookies.ContainsKey(name); + } + public String GetCookie(String name) + { + return requestCookies[name]; + } + + public bool ContainsParameter(string parameterName) => requestParameters.ContainsKey(parameterName); + public String GetParameter(String parameterName) => GetParameter(parameterName, null); + public String GetParameter(String parameterName,String defaultValue) + { + if (!requestParameters.TryGetValue(parameterName, out string value)) + value = defaultValue; + return value; + } + public void SetParameter(String parameterName,String parameterValue) => requestParameters[parameterName] = parameterValue; + public IEnumerable ParameterNames => requestParameters.Keys; + + + public string self() + { + return BaseURI.ToString(); + } + + + public HttpResponse Redirect(string location, params object[] p) => Redirect(303, location, p); + public HttpResponse Redirect(int status, string location, params object[] p) + { + location = string.Format(location, p); + + HttpResponse response = new HttpResponse(this); + response.StatusCode = status; + response.SetHeader("location", location); + return response; + + } + + public void Dispose() + { + contentReader?.Dispose(); + ContentStream?.Dispose(); + } + + } +} diff --git a/HttpResponse.cs b/HttpResponse.cs new file mode 100644 index 0000000..afc0aa1 --- /dev/null +++ b/HttpResponse.cs @@ -0,0 +1,135 @@ +using System; +using System.IO; +using System.Collections.Generic; +using System.Linq; + +namespace ln.http +{ + public class HttpResponse + { + public HttpRequest HttpRequest { get; } + + public Stream ContentStream { get; private set; } + public TextWriter ContentWriter { get; private set; } + public bool HasCustomContentStream { get; private set; } + + int statusCode; + string statusMessage; + Dictionary> headers = new Dictionary>(); + List cookies = new List(); + + public HttpResponse(HttpRequest httpRequest) + { + HttpRequest = httpRequest; + ContentStream = new MemoryStream(); + ContentWriter = new StreamWriter(ContentStream); + + StatusCode = 200; + SetHeader("content-type", "text/html"); + } + public HttpResponse(HttpRequest httpRequest,string contentType) + :this(httpRequest) + { + SetHeader("content-type", contentType); + } + + public HttpResponse(HttpRequest httpRequest, Stream contentStream) + { + HttpRequest = httpRequest; + ContentStream = contentStream; + ContentWriter = null; + HasCustomContentStream = true; + + StatusCode = 200; + SetHeader("content-type", "text/html"); + } + public HttpResponse(HttpRequest httpRequest, Stream contentStream,string contentType) + :this(httpRequest,contentStream) + { + SetHeader("content-type", contentType); + } + + public String GetHeader(string name) => GetHeader(name, null); + public String GetHeader(string name, string defValue) + { + return headers.ContainsKey(name) ? String.Join(",", headers[name.ToUpper()]) : defValue; + } + + public String[] GetHeaderValues(string name) + { + return headers[name.ToUpper()].ToArray(); + } + public String[] GetHeaderNames() + { + return headers.Keys.ToArray(); + } + + public void SetHeader(String name, String value) + { + name = name.ToUpper(); + + headers[name] = new List(); + headers[name].Add(value); + } + public void AddHeader(String name, String value) + { + name = name.ToUpper(); + + if (!headers.ContainsKey(name)) + headers[name] = new List(); + + headers[name].Add(value); + } + public void RemoveHeader(String name) + { + headers.Remove(name.ToUpper()); + } + + public bool ContainsHeader(string headerName) + { + return headers.ContainsKey(headerName.ToUpper()); + } + + public void AddCookie(string name,string value) + { + AddCookie(new HttpCookie(name, value)); + } + public void AddCookie(HttpCookie httpCookie) + { + cookies.Add(httpCookie); + } + public void RemoveCookie(HttpCookie httpCookie) + { + cookies.Remove(httpCookie); + } + public HttpCookie[] Cookies => cookies.ToArray(); + + + + public int StatusCode + { + get + { + return statusCode; + } + set { + statusCode = value; + statusMessage = HttpStatusCodes.GetStatusMessage(statusCode); + } + } + public String StatusMessage + { + get { + return statusMessage; + } + set { + statusMessage = value; + } + } + + + + + + } +} diff --git a/HttpRouter.cs b/HttpRouter.cs new file mode 100644 index 0000000..b58fc57 --- /dev/null +++ b/HttpRouter.cs @@ -0,0 +1,36 @@ +using System; +using ln.logging; +using ln.http.router; +namespace ln.http +{ + //public abstract class HttpRouter : IHttpRouter + //{ + // public HttpRouter() + // { + // } + + // public abstract IHTTPResource FindResource(HttpRequest httpRequest); + + // public virtual HttpResponse Route(HttpRoutingContext routingContext, HttpRequest httpRequest) + // { + // try + // { + // IHTTPResource resource = FindResource(httpRequest); + // return resource.GetResponse(httpRequest); + // } catch (Exception e) + // { + // Logging.Log(e); + // if (httpRequest != null) + // { + // HttpResponse httpResponse = new HttpResponse(httpRequest); + // httpResponse.StatusCode = 500; + // httpResponse.ContentWriter.WriteLine("500 Internal Server Error"); + + // return httpResponse; + // } + // return null; + // } + // } + + //} +} diff --git a/HttpStatusCodes.cs b/HttpStatusCodes.cs new file mode 100644 index 0000000..d417cd8 --- /dev/null +++ b/HttpStatusCodes.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +namespace ln.http +{ + public class HttpStatusCodes + { + static Dictionary statusMessages = new Dictionary() + { + { 200, "Ok" }, + { 201, "Created" }, + { 202, "Accepted" }, + { 204, "No Content" }, + { 403, "Access denied" }, + { 404, "Not Found" }, + { 500, "Internal Error" } + }; + + public static String GetStatusMessage(int code){ + if (statusMessages.ContainsKey(code)) + return statusMessages[code]; + return ""; + } + + } +} diff --git a/HttpUser.cs b/HttpUser.cs new file mode 100644 index 0000000..79cef7c --- /dev/null +++ b/HttpUser.cs @@ -0,0 +1,30 @@ +// /** +// * File: HttpUser.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; + +namespace ln.http +{ + public class HttpUser + { + public String AuthenticationName { get; private set; } + public virtual String DisplayName { get; private set; } + + public long AccessRightsMask { get; private set; } + + public HttpUser() + { + AuthenticationName = ""; + DisplayName = "Anonymous"; + AccessRightsMask = 0; + } + + + } +} diff --git a/IHTTPResource.cs b/IHTTPResource.cs new file mode 100644 index 0000000..b48c503 --- /dev/null +++ b/IHTTPResource.cs @@ -0,0 +1,8 @@ +using System; +namespace ln.http +{ + public interface IHTTPResource + { + HttpResponse GetResponse(HttpRequest httpRequest); + } +} diff --git a/IHttpRouter.cs b/IHttpRouter.cs new file mode 100644 index 0000000..3934d8a --- /dev/null +++ b/IHttpRouter.cs @@ -0,0 +1,9 @@ +using System; +using ln.http.router; +namespace ln.http +{ + public interface IHttpRouter + { + HttpResponse Route(HttpRoutingContext routingContext, HttpRequest httpRequest); + } +} diff --git a/QueryStringParameters.cs b/QueryStringParameters.cs new file mode 100644 index 0000000..211ed3b --- /dev/null +++ b/QueryStringParameters.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +namespace ln.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 string GetValue(string key,string defaultValue) + { + if (!ContainsKey(key)) + return defaultValue; + return parameters[key]; + } + + 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/cert/CertContainer.cs b/cert/CertContainer.cs new file mode 100644 index 0000000..4f05846 --- /dev/null +++ b/cert/CertContainer.cs @@ -0,0 +1,59 @@ +// /** +// * File: CertContainer.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.Security.Cryptography.X509Certificates; +using System.Collections.Generic; +using System.IO; + +namespace ln.http.cert +{ + public class CertContainer + { + public string SearchPath { get; set; } + + Dictionary certificates = new Dictionary(); + + public CertContainer(){ } + public CertContainer(string searchPath) + { + SearchPath = searchPath; + } + + public void AddCertificate(string targetHost, X509Certificate certificate) => certificates[targetHost] = certificate; + + public virtual X509Certificate LookupCertificate(string targetHost) + { + String p = Path.Combine(SearchPath, String.Format("{0}.pem",targetHost)); + if (File.Exists(p)) + { + return X509Certificate.CreateFromCertFile(p); + } + return null; + } + + public X509Certificate SelectCertificate(object sender, string targetHost, X509CertificateCollection localCertificates, X509Certificate remoteCertificate, string[] acceptableIssuers) + { + if (!certificates.ContainsKey(targetHost) && (SearchPath != null)) + { + X509Certificate certificate = LookupCertificate(targetHost); + if (certificate != null) + { + certificates[targetHost] = certificate; + } + else + { + return null; + } + } + return certificates[targetHost]; + } + + } +} diff --git a/client/CookieContainer.cs b/client/CookieContainer.cs new file mode 100644 index 0000000..a46b322 --- /dev/null +++ b/client/CookieContainer.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +namespace ln.http.client +{ + public class CookieContainer : IEnumerable + { + public HttpCookie[] Cookies => cookies.ToArray(); + + List cookies = new List(); + + public CookieContainer() + { + } + + public void Add(HttpCookie httpCookie) + { + foreach (HttpCookie cookie in Get(httpCookie.Domain)) + { + if (cookie.Name.Equals(httpCookie.Name)) + Remove(cookie); + } + cookies.Add(httpCookie); + } + + public void Remove(HttpCookie httpCookie) + { + cookies.Remove(httpCookie); + } + + public IEnumerable Get(string domain) + { + return Cookies.Where((c) => c.Domain.Equals(domain)); + } + + + public IEnumerator GetEnumerator() => cookies.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => cookies.GetEnumerator(); + } +} diff --git a/client/HttpClient.cs b/client/HttpClient.cs new file mode 100644 index 0000000..d22cc17 --- /dev/null +++ b/client/HttpClient.cs @@ -0,0 +1,18 @@ +using System; +using ln.type; +namespace ln.http.client +{ + public class HttpClient + { + CookieContainer Cookies { get; set; } = new CookieContainer(); + + public HttpClient() + { + } + + public HttpClientRequest CreateRequest(URI uri) + { + return new HttpClientRequest(this, uri); + } + } +} diff --git a/client/HttpClientRequest.cs b/client/HttpClientRequest.cs new file mode 100644 index 0000000..3e272f3 --- /dev/null +++ b/client/HttpClientRequest.cs @@ -0,0 +1,107 @@ +using System; +using ln.type; +using System.IO; +using System.Net.Sockets; +using System.Net.Security; +namespace ln.http.client +{ + public class HttpClientRequest + { + public HttpClient HttpClient { get; } + public URI URI + { + get => uri; + set + { + if (!value.Scheme.Equals("http") && !value.Scheme.Equals("https")) + throw new ArgumentOutOfRangeException(nameof(value), String.Format("unsupported url scheme: {0}", value.Scheme)); + uri = value; + } + } + + public HttpHeaders Headers { get; } = new HttpHeaders(); + public String Method { get; set; } + + URI uri; + MemoryStream contentStream; + HttpClientResponse clientResponse; + + public HttpClientRequest(HttpClient httpClient, URI uri) + { + HttpClient = httpClient; + URI = uri; + + Headers.Add("user-agent", "ln.http.client"); + + } + + public Stream GetContentStream() + { + if (clientResponse != null) + throw new NotSupportedException("Request has already been executed, content stream has been disposed"); + + if (contentStream == null) + contentStream = new MemoryStream(); + + return contentStream; + } + + public HttpClientResponse GetResponse() + { + if (clientResponse == null) + { + executeRequest(); + } + return clientResponse; + } + + private Stream OpenConnection() + { + TcpClient tcpClient = null; + SslStream sslStream = null; + + try + { + tcpClient = new TcpClient(); + tcpClient.ExclusiveAddressUse = false; + + tcpClient.Connect(URI.Host, int.Parse(URI.Port)); + if (!tcpClient.Connected) + throw new IOException(String.Format("could not connect to host {0}",uri.Host)); + + if (uri.Scheme.Equals("https")) + { + sslStream = new SslStream(tcpClient.GetStream()); + return sslStream; + } + return tcpClient.GetStream(); + } catch (Exception) + { + if (sslStream != null) + sslStream.Dispose(); + if (tcpClient != null) + tcpClient.Dispose(); + + throw; + } + } + + private void executeRequest() + { + if (contentStream.Length > 0) + { + byte[] requestContent = contentStream.ToArray(); + Headers.Add("content-length", requestContent.Length.ToString()); + } + + Stream connectionStream = OpenConnection(); + + + clientResponse = new HttpClientResponse(this); + + + contentStream.Dispose(); + } + + } +} diff --git a/client/HttpClientResponse.cs b/client/HttpClientResponse.cs new file mode 100644 index 0000000..073d243 --- /dev/null +++ b/client/HttpClientResponse.cs @@ -0,0 +1,13 @@ +using System; +namespace ln.http.client +{ + public class HttpClientResponse + { + public HttpClientRequest ClientRequest { get; } + + public HttpClientResponse(HttpClientRequest clientRequest) + { + ClientRequest = clientRequest; + } + } +} diff --git a/connections/Connection.cs b/connections/Connection.cs new file mode 100644 index 0000000..4b40f2c --- /dev/null +++ b/connections/Connection.cs @@ -0,0 +1,86 @@ +// /** +// * File: Connection.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using ln.type; +using System.IO; +using ln.logging; +using ln.http.listener; +namespace ln.http.connections +{ + public abstract class Connection : IDisposable + { + public Listener Listener { get; private set; } + + public abstract IPv6 RemoteHost { get; } + public abstract int RemotePort { get; } + + public abstract Stream GetStream(); + + public Connection(Listener listener) + { + Listener = listener; + } + + public virtual HttpRequest ReadRequest(HTTPServer httpServer) + { + try + { + return new HttpRequest(httpServer, GetStream(), Listener.LocalEndpoint, new Endpoint(RemoteHost, RemotePort)); + } catch (IOException) + { + return null; + } catch (Exception e) + { + Logging.Log(e); + return null; + } + } + + public abstract void Close(); + public virtual void Dispose() + { + Close(); + Listener = null; + } + + public virtual void SendResponse(HttpResponse response) => SendResponse(GetStream(), response); + + public static void SendResponse(Stream stream, HttpResponse response) + { + response.HttpRequest.FinishRequest(); + + response.SetHeader("Content-Length", response.ContentStream.Length.ToString()); + + StreamWriter streamWriter = new StreamWriter(stream); + streamWriter.NewLine = "\r\n"; + + streamWriter.WriteLine("{0} {1} {2}", response.HttpRequest.Protocol, response.StatusCode, response.StatusMessage); + foreach (String headerName in response.GetHeaderNames()) + { + streamWriter.WriteLine("{0}: {1}", headerName, response.GetHeader(headerName)); + } + + foreach (HttpCookie httpCookie in response.Cookies) + { + streamWriter.WriteLine("Set-Cookie: {0}", httpCookie.ToString()); + } + + streamWriter.WriteLine(); + streamWriter.Flush(); + + response.ContentStream.Position = 0; + response.ContentStream.CopyTo(stream); + response.ContentStream.Close(); + response.ContentStream.Dispose(); + + stream.Flush(); + } + } +} diff --git a/connections/HttpConnection.cs b/connections/HttpConnection.cs new file mode 100644 index 0000000..bd4c692 --- /dev/null +++ b/connections/HttpConnection.cs @@ -0,0 +1,41 @@ +// /** +// * File: HttpConnection.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.IO; +using ln.type; +using System.Net.Sockets; +using ln.http.listener; + +namespace ln.http.connections +{ + public class HttpConnection : Connection + { + public TcpClient TcpClient { get; } + public Endpoint RemoteEndpoint { get; } + + public HttpConnection(Listener listener, TcpClient tcpClient) + :base(listener) + { + TcpClient = tcpClient; + RemoteEndpoint = new Endpoint(TcpClient.Client.RemoteEndPoint); + } + + public override IPv6 RemoteHost => RemoteEndpoint.Address; + public override int RemotePort => RemoteEndpoint.Port; + + public override Stream GetStream() => TcpClient.GetStream(); + + public override void Close() + { + TcpClient.Close(); + } + + } +} diff --git a/connections/HttpsConnection.cs b/connections/HttpsConnection.cs new file mode 100644 index 0000000..3c7c894 --- /dev/null +++ b/connections/HttpsConnection.cs @@ -0,0 +1,46 @@ +// /** +// * File: HttpsConnection.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.IO; +using System.Security.AccessControl; +using ln.type; +using System.Net.Security; +using ln.http.listener; + +namespace ln.http.connections +{ + public class HttpsConnection : Connection + { + Connection Connection { get; } + SslStream sslStream { get; } + + public override IPv6 RemoteHost => Connection.RemoteHost; + public override int RemotePort => Connection.RemotePort; + + public HttpsConnection(Listener listener,Connection connection,LocalCertificateSelectionCallback localCertificateSelectionCallback) + :base(listener) + { + Connection = connection; + sslStream = new SslStream(connection.GetStream(),false, null, localCertificateSelectionCallback); + } + + public override HttpRequest ReadRequest(HTTPServer server) + { + throw new NotImplementedException(); + } + + public override Stream GetStream() => sslStream; + + public override void Close() + { + sslStream.Close(); + } + } +} diff --git a/exceptions/BadRequestException.cs b/exceptions/BadRequestException.cs new file mode 100644 index 0000000..2a62857 --- /dev/null +++ b/exceptions/BadRequestException.cs @@ -0,0 +1,20 @@ +// /** +// * File: BadRequest.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +namespace ln.http.exceptions +{ + public class BadRequestException: HttpException + { + public BadRequestException() + :base(400,"Bad Request") + { + } + } +} diff --git a/exceptions/DisposeConnectionException.cs b/exceptions/DisposeConnectionException.cs new file mode 100644 index 0000000..720b421 --- /dev/null +++ b/exceptions/DisposeConnectionException.cs @@ -0,0 +1,10 @@ +using System; +namespace ln.http.exceptions +{ + public class DisposeConnectionException : Exception + { + public DisposeConnectionException() + { + } + } +} diff --git a/exceptions/HttpException.cs b/exceptions/HttpException.cs new file mode 100644 index 0000000..b57d44c --- /dev/null +++ b/exceptions/HttpException.cs @@ -0,0 +1,29 @@ +using System; + +namespace ln.http.exceptions +{ + public class HttpException : Exception + { + public int StatusCode { get; } = 500; + + public HttpException(String message) + : base(message) + { + } + public HttpException(String message, Exception innerException) + : base(message, innerException) + { + } + public HttpException(int statusCode,String message) + : base(message) + { + StatusCode = statusCode; + } + public HttpException(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..4ee86dc --- /dev/null +++ b/exceptions/IllegalRequestException.cs @@ -0,0 +1,11 @@ +using System; +namespace ln.http.exceptions +{ + public class IllegalRequestException : Exception + { + public IllegalRequestException(String requestLine) + :base(requestLine) + { + } + } +} diff --git a/exceptions/MethodNotAllowedException.cs b/exceptions/MethodNotAllowedException.cs new file mode 100644 index 0000000..687f32a --- /dev/null +++ b/exceptions/MethodNotAllowedException.cs @@ -0,0 +1,21 @@ +// /** +// * File: MethodNotSupportedException.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.Net; +namespace ln.http.exceptions +{ + public class MethodNotAllowedException : HttpException + { + public MethodNotAllowedException() + :base(405,"Method not allowed") + { + } + } +} diff --git a/exceptions/ResourceNotFoundException.cs b/exceptions/ResourceNotFoundException.cs new file mode 100644 index 0000000..81869f0 --- /dev/null +++ b/exceptions/ResourceNotFoundException.cs @@ -0,0 +1,11 @@ +using System; +namespace ln.http.exceptions +{ + public class ResourceNotFoundException : HttpException + { + public ResourceNotFoundException(String resourcePath, String nextResource) + : base(404, String.Format("Could not find resource \"{0}\" within \"{1}\"", nextResource, resourcePath)) + { + } + } +} diff --git a/exceptions/UnsupportedMediaTypeException.cs b/exceptions/UnsupportedMediaTypeException.cs new file mode 100644 index 0000000..ee795d0 --- /dev/null +++ b/exceptions/UnsupportedMediaTypeException.cs @@ -0,0 +1,20 @@ +// /** +// * File: UnsupportedMediaTypeException.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +namespace ln.http.exceptions +{ + public class UnsupportedMediaTypeException : HttpException + { + public UnsupportedMediaTypeException() + : base(415, "Unsupported Media Type") + { + } + } +} diff --git a/io/UnbufferedStreamreader.cs b/io/UnbufferedStreamreader.cs new file mode 100644 index 0000000..6d8b16d --- /dev/null +++ b/io/UnbufferedStreamreader.cs @@ -0,0 +1,68 @@ +// /** +// * File: UnbufferedStreamreader.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.IO; +using System.Text; +namespace ln.http.io +{ + public class UnbufferedStreamReader : TextReader + { + public Stream Stream { get; } + + public UnbufferedStreamReader(Stream stream) + { + Stream = stream; + } + + public override int Read() => Stream.ReadByte(); + + public override string ReadLine() + { + StringBuilder stringBuilder = new StringBuilder(); + + char ch; + + while ((ch = (char)Stream.ReadByte()) != -1) + { + if (ch == '\r') + { + ch = (char)Stream.ReadByte(); + if (ch == '\n') + return stringBuilder.ToString(); + + stringBuilder.Append('\r'); + } + stringBuilder.Append(ch); + } + + if ((ch == -1) && (stringBuilder.Length == 0)) + return null; + + return stringBuilder.ToString(); + } + + public string ReadToken() + { + StringBuilder stringBuilder = new StringBuilder(); + char ch = (char)Stream.ReadByte(); + + while (char.IsWhiteSpace(ch)) + ch = (char)Stream.ReadByte(); + + while (!char.IsWhiteSpace(ch)) + { + stringBuilder.Append(ch); + ch = (char)Stream.ReadByte(); + } + + return stringBuilder.ToString(); + } + } +} diff --git a/listener/HttpListener.cs b/listener/HttpListener.cs new file mode 100644 index 0000000..7147e7d --- /dev/null +++ b/listener/HttpListener.cs @@ -0,0 +1,53 @@ +// /** +// * File: HttpListener.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System.Net.Sockets; +using ln.type; +using ln.http.connections; + +namespace ln.http.listener +{ + public class HttpListener : Listener + { + + protected TcpListener tcpListener; + + public HttpListener(int port) :this(IPv6.ANY,port){} + public HttpListener(Endpoint endpoint) : this(endpoint.Address, endpoint.Port) {} + public HttpListener(IPv6 listen, int port) + : base(listen, port) + { + } + + public override Connection Accept() => new HttpConnection(this,tcpListener.AcceptTcpClient()); + + public override bool IsOpen => (tcpListener != null); + + public override void Open() + { + tcpListener = new TcpListener(Listen, Port); + tcpListener.Start(); + } + public override void Close() + { + if (tcpListener != null) + { + tcpListener.Stop(); + tcpListener = null; + } + } + + public override void Dispose() + { + if (IsOpen) + Close(); + } + + } +} diff --git a/listener/HttpsListener.cs b/listener/HttpsListener.cs new file mode 100644 index 0000000..20b2f30 --- /dev/null +++ b/listener/HttpsListener.cs @@ -0,0 +1,27 @@ +// /** +// * File: HttpsListener.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using ln.type; +using ln.http.connections; +using ln.http.cert; +namespace ln.http.listener +{ + public class HttpsListener : HttpListener + { + public HttpsListener(int port) : this(IPv6.ANY, port) { } + public HttpsListener(IPv6 listen, int port) : base(listen, port) { } + + public CertContainer CertContainer { get; set; } = new CertContainer(); + + public override Connection Accept() + { + return new HttpsConnection(this, base.Accept(),CertContainer.SelectCertificate); + } + } +} diff --git a/listener/Listener.cs b/listener/Listener.cs new file mode 100644 index 0000000..7960226 --- /dev/null +++ b/listener/Listener.cs @@ -0,0 +1,43 @@ +// /** +// * File: Listener.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using ln.http.connections; +using ln.type; +namespace ln.http.listener +{ + public abstract class Listener : IDisposable + { + public IPv6 Listen { get; } + public int Port { get; } + + public Endpoint LocalEndpoint => new Endpoint(Listen, Port); + + public abstract bool IsOpen { get; } + + protected Listener(IPv6 listen, int port) + { + Listen = listen; + Port = port; + } + + public virtual void AcceptMany(Action handler) + { + while (IsOpen) + handler(Accept()); + } + + public abstract void Open(); + public abstract void Close(); + + public abstract Connection Accept(); + + public abstract void Dispose(); + } +} diff --git a/ln.http.csproj b/ln.http.csproj new file mode 100644 index 0000000..91e4233 --- /dev/null +++ b/ln.http.csproj @@ -0,0 +1,24 @@ + + + + netcoreapp3.1 + true + 0.1.0 + Harald Wolff-Thobaben + l--n.de + + (c) 2020 Harald Wolff-Thobaben + http server + 0.0.1.0 + 0.0.1.0 + + + + + + + + + + + diff --git a/message/Header.cs b/message/Header.cs new file mode 100644 index 0000000..cf7a026 --- /dev/null +++ b/message/Header.cs @@ -0,0 +1,159 @@ +// /** +// * File: Header.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Linq; +using System.Net.Sockets; +namespace ln.http.message +{ + public class Header + { + public string Name { get; } + + string rawvalue; + public string RawValue { + get => rawvalue; + set => SetValue(value); + } + + string comments; + public string Comments => comments; + + string value; + public string Value + { + get => value; + set => SetValue(value); + } + + Dictionary parameters; + + public Header(string headerLine) + { + int colon = headerLine.IndexOf(':'); + if (colon == -1) + throw new FormatException("expected to find :"); + + Name = headerLine.Substring(0, colon).ToUpper(); + SetValue(headerLine.Substring(colon + 1)); + } + public Header(string name, string value) + { + Name = name.ToUpper(); + SetValue(value); + } + + public void SetValue(string newValue) + { + rawvalue = newValue; + value = ParseValue(new StringReader(newValue.Trim()),out comments); + + // at least MIME Content-* header follow the parameter syntax... + if (Name.StartsWith("CONTENT-", StringComparison.InvariantCulture)) + { + ParseParameters(); + } + } + + public bool ContainsParameter(string parameterName) => parameters.ContainsKey(parameterName.ToUpper()); + public string GetParameter(string parameterName) => parameters[parameterName.ToUpper()]; + public string GetParameter(string parameterName,string defaultValue) => parameters[parameterName.ToUpper()]; + + string ParseComment(TextReader reader) + { + StringBuilder commentBuilder = new StringBuilder(); + ParseComment(reader, commentBuilder); + return commentBuilder.ToString(); + } + void ParseComment(TextReader reader,StringBuilder commentBuilder) + { + int ch; + while (((ch = reader.Read()) != -1) && (ch != ')')) + commentBuilder.Append((char)ch); + } + public virtual string ParseValue(TextReader reader,out string parsedComments) + { + StringBuilder stringBuilder = new StringBuilder(); + StringBuilder commentBuilder = new StringBuilder(); + + int ch; + while (((ch = reader.Read())!=-1)) + { + if (ch == '(') + { + commentBuilder.Append(ParseComment(reader)); + } else + { + stringBuilder.Append((char)ch); + } + } + parsedComments = commentBuilder.ToString().Trim(); + return stringBuilder.ToString().Trim(); + } + + public void ParseParameters() + { + if (parameters != null) + return; + + parameters = new Dictionary(); + + int semicolon = value.IndexOf(';'); + if (semicolon > 0) + { + TokenReader tokenReader = new TokenReader(new StringReader(value.Substring(semicolon))); + while (tokenReader.Peek() != -1) + { + if (tokenReader.Read() != ';') + throw new FormatException(); + + string pName = tokenReader.ReadToken().ToUpper(); + if (tokenReader.Read() != '=') + throw new FormatException("expected ="); + + string pValue = (tokenReader.Peek() == '"') ? tokenReader.ReadQuotedString() : tokenReader.ReadToken(); + parameters.Add(pName, pValue); + } + + value = value.Substring(0, semicolon).Trim(); + } + } + + //void parseValue(string v) + //{ + // rawValue = v; + // TokenReader tokenReader = new TokenReader(parseComments(new StringReader(v))); + // StringBuilder stringBuilder = new StringBuilder(); + + // int ch; + // while (((ch = tokenReader.Read()) != -1) && (ch != ';')) + // stringBuilder.Append((char)ch); + + // Value = stringBuilder.ToString(); + + // while (tokenReader.Peek() != -1) + // { + // string pName = tokenReader.ReadToken(); + // if (tokenReader.Read() != '=') + // throw new FormatException("expected ="); + + // string pValue = (tokenReader.Peek() == '"') ? tokenReader.ReadQuotedString() : tokenReader.ReadToken(); + // parameters.Add(pName, pValue); + // } + + + //} + + public override int GetHashCode() => Name.GetHashCode(); + public override bool Equals(object obj) => (obj is Header you) && Name.Equals(you.Name); + } +} diff --git a/message/HeaderContainer.cs b/message/HeaderContainer.cs new file mode 100644 index 0000000..b4d0638 --- /dev/null +++ b/message/HeaderContainer.cs @@ -0,0 +1,76 @@ +// /** +// * File: HeaderContainer.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using ln.http.io; +namespace ln.http.message +{ + public class HeaderContainer + { + Dictionary headers = new Dictionary(); + + public HeaderContainer() + { + } + public HeaderContainer(Stream stream):this(new UnbufferedStreamReader(stream)) + { + } + public HeaderContainer(TextReader reader) + { + List headerLines = new List(); + string currentline = reader.ReadLine(); + while (!currentline.Equals(string.Empty)) + { + if (char.IsWhiteSpace(currentline[0])) + { + headerLines[headerLines.Count - 1] = headerLines[headerLines.Count - 1] + currentline; + } + else + { + headerLines.Add(currentline); + } + currentline = reader.ReadLine(); + } + + foreach (string headerLine in headerLines) + { + Header header = new Header(headerLine); + headers.Add(header.Name, header); + } + } + + public Header this[string name] + { + get => headers[name.ToUpper()]; + } + + public void Add(Header header)=> headers.Add(header.Name, header); + + public bool ContainsKey(string name) => headers.ContainsKey(name.ToUpper()); + public bool Contains(string name) => headers.ContainsKey(name.ToUpper()); + public string Get(string name) => this[name].Value; + public void Set(string name,string value) + { + name = name.ToUpper(); + if (!headers.TryGetValue(name,out Header header)) + { + header = new Header(name); + headers.Add(name, header); + } + header.Value = value; + } + public void Remove(string name) => headers.Remove(name.ToUpper()); + + public IEnumerable Keys => headers.Keys; + + } +} diff --git a/message/Message.cs b/message/Message.cs new file mode 100644 index 0000000..f34bc3a --- /dev/null +++ b/message/Message.cs @@ -0,0 +1,153 @@ +// /** +// * File: Message.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Globalization; +using ln.http.io; +using ln.type; +using ln.http.message.parser; +namespace ln.http.message +{ + public class Message + { + public HeaderContainer Headers { get; private set; } + + byte[] bodyData; + int bodyOffset; + int bodyLength; + + List parts; + + bool isMultipart; + public bool IsMultipart => isMultipart; + + public Message() + { + Setup(new HeaderContainer(), new byte[0], 0, 0); + } + + public Message(HeaderContainer headers, byte[] body) + : this(headers, body, 0, body.Length) { } + + public Message(HeaderContainer headers, byte[] body, int offset, int length) + { + Setup(headers, body, offset, length); + } + + public Message(byte[] body, int offset, int length) + { + MemoryStream memoryStream = new MemoryStream(body, offset, length); + HeaderContainer headers = MIME.ReadHeader(new UnbufferedStreamReader(memoryStream)); + + if (memoryStream.Position >= length) + throw new FormatException("MIME header section too long"); + + Setup(headers, body, offset + (int)memoryStream.Position, length - (int)memoryStream.Position); + } + + public Message(Stream stream) + { + HeaderContainer headers = MIME.ReadHeader(new UnbufferedStreamReader(stream)); + + byte[] data = stream.ReadToEnd(); + + Setup(headers, data, 0, data.Length); + } + + private void Setup(HeaderContainer headers, byte[] body, int offset, int length) + { + Headers = headers; + + bodyData = body; + bodyOffset = offset; + bodyLength = length; + + string ct = Headers["Content-Type"].Value; + isMultipart = ct.StartsWith("multipart/", StringComparison.InvariantCulture) || ct.StartsWith("message/", StringComparison.InvariantCulture); + } + + public void ReadParts() + { + parts = new List(); + + if (isMultipart) + { + string boundary = Headers["Content-Type"].GetParameter("boundary"); + string delimiter = "--" + boundary; + int[] indeces = FindIndeces(bodyData, bodyOffset, bodyLength, Encoding.ASCII.GetBytes(delimiter)); + + for (int n = 1; n < indeces.Length; n++) + { + Message part = new Message(bodyData, indeces[n - 1], indeces[n] - indeces[n - 1]); + parts.Add(part); + } + + } + } + + int[] FindIndeces(byte[] data,int offset,int length,byte[] pattern) + { + List offsets = new List(); + List validated = new List(); + + for (int n = offset; n < (length - pattern.Length); n++) + { + int p = 0; + while ((p < pattern.Length) && (data[n + p] == pattern[p])) + p++; + + if (p == pattern.Length) + { + if ((n == offset) || ((n >= (offset + 2)) && (data[offset + n - 2] == '\r') && (data[offset + n - 1] == '\n'))) + { + n += pattern.Length; + + while ((n < (offset + length)) && (data[n - 2] != '\r') && (data[n - 1] != '\n')) + n++; + + validated.Add(n); + + + if (((offset + length) > (n + 1)) && (data[n] == '-') && (data[n + 1] == '-')) + break; + } + } + } + + return validated.ToArray(); + } + + + public Stream OpenBodyStream() => new MemoryStream(bodyData, bodyOffset, bodyLength); + + public IEnumerable Parts + { + get + { + if (parts == null) + ReadParts(); + return parts; + } + } + + public bool HasHeader(string name) => Headers.Contains(name); + public Header GetHeader(string name) => Headers[name]; + public void SetHeader(string name, string value) => Headers.Set(name, value); + public void RemoveHeader(String name) => Headers.Remove(name); + + public override string ToString() + { + return base.ToString(); + } + + } +} diff --git a/message/MimeTypeMap.cs b/message/MimeTypeMap.cs new file mode 100644 index 0000000..47766a4 --- /dev/null +++ b/message/MimeTypeMap.cs @@ -0,0 +1,767 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ln.http.message +{ + public static class MimeTypeMap + { + private static readonly Lazy> _mappings = new Lazy>(BuildMappings); + + private static IDictionary BuildMappings() + { + var mappings = new Dictionary(StringComparer.OrdinalIgnoreCase) { + + #region Big freaking list of mime types + + // maps both ways, + // extension -> mime type + // and + // mime type -> extension + // + // any mime types on left side not pre-loaded on right side, are added automatically + // some mime types can map to multiple extensions, so to get a deterministic mapping, + // add those to the dictionary specifcially + // + // combination of values from Windows 7 Registry and + // from C:\Windows\System32\inetsrv\config\applicationHost.config + // some added, including .7z and .dat + // + // Some added based on http://www.iana.org/assignments/media-types/media-types.xhtml + // which lists mime types, but not extensions + // + {".323", "text/h323"}, + {".3g2", "video/3gpp2"}, + {".3gp", "video/3gpp"}, + {".3gp2", "video/3gpp2"}, + {".3gpp", "video/3gpp"}, + {".7z", "application/x-7z-compressed"}, + {".aa", "audio/audible"}, + {".AAC", "audio/aac"}, + {".aaf", "application/octet-stream"}, + {".aax", "audio/vnd.audible.aax"}, + {".ac3", "audio/ac3"}, + {".aca", "application/octet-stream"}, + {".accda", "application/msaccess.addin"}, + {".accdb", "application/msaccess"}, + {".accdc", "application/msaccess.cab"}, + {".accde", "application/msaccess"}, + {".accdr", "application/msaccess.runtime"}, + {".accdt", "application/msaccess"}, + {".accdw", "application/msaccess.webapplication"}, + {".accft", "application/msaccess.ftemplate"}, + {".acx", "application/internet-property-stream"}, + {".AddIn", "text/xml"}, + {".ade", "application/msaccess"}, + {".adobebridge", "application/x-bridge-url"}, + {".adp", "application/msaccess"}, + {".ADT", "audio/vnd.dlna.adts"}, + {".ADTS", "audio/aac"}, + {".afm", "application/octet-stream"}, + {".ai", "application/postscript"}, + {".aif", "audio/aiff"}, + {".aifc", "audio/aiff"}, + {".aiff", "audio/aiff"}, + {".air", "application/vnd.adobe.air-application-installer-package+zip"}, + {".amc", "application/mpeg"}, + {".anx", "application/annodex"}, + {".apk", "application/vnd.android.package-archive" }, + {".application", "application/x-ms-application"}, + {".art", "image/x-jg"}, + {".asa", "application/xml"}, + {".asax", "application/xml"}, + {".ascx", "application/xml"}, + {".asd", "application/octet-stream"}, + {".asf", "video/x-ms-asf"}, + {".ashx", "application/xml"}, + {".asi", "application/octet-stream"}, + {".asm", "text/plain"}, + {".asmx", "application/xml"}, + {".aspx", "application/xml"}, + {".asr", "video/x-ms-asf"}, + {".asx", "video/x-ms-asf"}, + {".atom", "application/atom+xml"}, + {".au", "audio/basic"}, + {".avi", "video/x-msvideo"}, + {".axa", "audio/annodex"}, + {".axs", "application/olescript"}, + {".axv", "video/annodex"}, + {".bas", "text/plain"}, + {".bcpio", "application/x-bcpio"}, + {".bin", "application/octet-stream"}, + {".bmp", "image/bmp"}, + {".c", "text/plain"}, + {".cab", "application/octet-stream"}, + {".caf", "audio/x-caf"}, + {".calx", "application/vnd.ms-office.calx"}, + {".cat", "application/vnd.ms-pki.seccat"}, + {".cc", "text/plain"}, + {".cd", "text/plain"}, + {".cdda", "audio/aiff"}, + {".cdf", "application/x-cdf"}, + {".cer", "application/x-x509-ca-cert"}, + {".cfg", "text/plain"}, + {".chm", "application/octet-stream"}, + {".class", "application/x-java-applet"}, + {".clp", "application/x-msclip"}, + {".cmd", "text/plain"}, + {".cmx", "image/x-cmx"}, + {".cnf", "text/plain"}, + {".cod", "image/cis-cod"}, + {".config", "application/xml"}, + {".contact", "text/x-ms-contact"}, + {".coverage", "application/xml"}, + {".cpio", "application/x-cpio"}, + {".cpp", "text/plain"}, + {".crd", "application/x-mscardfile"}, + {".crl", "application/pkix-crl"}, + {".crt", "application/x-x509-ca-cert"}, + {".cs", "text/plain"}, + {".csdproj", "text/plain"}, + {".csh", "application/x-csh"}, + {".csproj", "text/plain"}, + {".css", "text/css"}, + {".csv", "text/csv"}, + {".cur", "application/octet-stream"}, + {".cxx", "text/plain"}, + {".dat", "application/octet-stream"}, + {".datasource", "application/xml"}, + {".dbproj", "text/plain"}, + {".dcr", "application/x-director"}, + {".def", "text/plain"}, + {".deploy", "application/octet-stream"}, + {".der", "application/x-x509-ca-cert"}, + {".dgml", "application/xml"}, + {".dib", "image/bmp"}, + {".dif", "video/x-dv"}, + {".dir", "application/x-director"}, + {".disco", "text/xml"}, + {".divx", "video/divx"}, + {".dll", "application/x-msdownload"}, + {".dll.config", "text/xml"}, + {".dlm", "text/dlm"}, + {".doc", "application/msword"}, + {".docm", "application/vnd.ms-word.document.macroEnabled.12"}, + {".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, + {".dot", "application/msword"}, + {".dotm", "application/vnd.ms-word.template.macroEnabled.12"}, + {".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template"}, + {".dsp", "application/octet-stream"}, + {".dsw", "text/plain"}, + {".dtd", "text/xml"}, + {".dtsConfig", "text/xml"}, + {".dv", "video/x-dv"}, + {".dvi", "application/x-dvi"}, + {".dwf", "drawing/x-dwf"}, + {".dwg", "application/acad"}, + {".dwp", "application/octet-stream"}, + {".dxf", "application/x-dxf" }, + {".dxr", "application/x-director"}, + {".eml", "message/rfc822"}, + {".emz", "application/octet-stream"}, + {".eot", "application/vnd.ms-fontobject"}, + {".eps", "application/postscript"}, + {".es", "application/ecmascript"}, + {".etl", "application/etl"}, + {".etx", "text/x-setext"}, + {".evy", "application/envoy"}, + {".exe", "application/vnd.microsoft.portable-executable"}, + {".exe.config", "text/xml"}, + {".f4v", "video/mp4"}, + {".fdf", "application/vnd.fdf"}, + {".fif", "application/fractals"}, + {".filters", "application/xml"}, + {".fla", "application/octet-stream"}, + {".flac", "audio/flac"}, + {".flr", "x-world/x-vrml"}, + {".flv", "video/x-flv"}, + {".fsscript", "application/fsharp-script"}, + {".fsx", "application/fsharp-script"}, + {".generictest", "application/xml"}, + {".gif", "image/gif"}, + {".gpx", "application/gpx+xml"}, + {".group", "text/x-ms-group"}, + {".gsm", "audio/x-gsm"}, + {".gtar", "application/x-gtar"}, + {".gz", "application/x-gzip"}, + {".h", "text/plain"}, + {".hdf", "application/x-hdf"}, + {".hdml", "text/x-hdml"}, + {".hhc", "application/x-oleobject"}, + {".hhk", "application/octet-stream"}, + {".hhp", "application/octet-stream"}, + {".hlp", "application/winhlp"}, + {".hpp", "text/plain"}, + {".hqx", "application/mac-binhex40"}, + {".hta", "application/hta"}, + {".htc", "text/x-component"}, + {".htm", "text/html"}, + {".html", "text/html"}, + {".htt", "text/webviewhtml"}, + {".hxa", "application/xml"}, + {".hxc", "application/xml"}, + {".hxd", "application/octet-stream"}, + {".hxe", "application/xml"}, + {".hxf", "application/xml"}, + {".hxh", "application/octet-stream"}, + {".hxi", "application/octet-stream"}, + {".hxk", "application/xml"}, + {".hxq", "application/octet-stream"}, + {".hxr", "application/octet-stream"}, + {".hxs", "application/octet-stream"}, + {".hxt", "text/html"}, + {".hxv", "application/xml"}, + {".hxw", "application/octet-stream"}, + {".hxx", "text/plain"}, + {".i", "text/plain"}, + {".ico", "image/x-icon"}, + {".ics", "application/octet-stream"}, + {".idl", "text/plain"}, + {".ief", "image/ief"}, + {".iii", "application/x-iphone"}, + {".inc", "text/plain"}, + {".inf", "application/octet-stream"}, + {".ini", "text/plain"}, + {".inl", "text/plain"}, + {".ins", "application/x-internet-signup"}, + {".ipa", "application/x-itunes-ipa"}, + {".ipg", "application/x-itunes-ipg"}, + {".ipproj", "text/plain"}, + {".ipsw", "application/x-itunes-ipsw"}, + {".iqy", "text/x-ms-iqy"}, + {".isp", "application/x-internet-signup"}, + {".isma", "application/octet-stream"}, + {".ismv", "application/octet-stream"}, + {".ite", "application/x-itunes-ite"}, + {".itlp", "application/x-itunes-itlp"}, + {".itms", "application/x-itunes-itms"}, + {".itpc", "application/x-itunes-itpc"}, + {".IVF", "video/x-ivf"}, + {".jar", "application/java-archive"}, + {".java", "application/octet-stream"}, + {".jck", "application/liquidmotion"}, + {".jcz", "application/liquidmotion"}, + {".jfif", "image/pjpeg"}, + {".jnlp", "application/x-java-jnlp-file"}, + {".jpb", "application/octet-stream"}, + {".jpe", "image/jpeg"}, + {".jpeg", "image/jpeg"}, + {".jpg", "image/jpeg"}, + {".js", "application/javascript"}, + {".json", "application/json"}, + {".jsx", "text/jscript"}, + {".jsxbin", "text/plain"}, + {".latex", "application/x-latex"}, + {".library-ms", "application/windows-library+xml"}, + {".lit", "application/x-ms-reader"}, + {".loadtest", "application/xml"}, + {".lpk", "application/octet-stream"}, + {".lsf", "video/x-la-asf"}, + {".lst", "text/plain"}, + {".lsx", "video/x-la-asf"}, + {".lzh", "application/octet-stream"}, + {".m13", "application/x-msmediaview"}, + {".m14", "application/x-msmediaview"}, + {".m1v", "video/mpeg"}, + {".m2t", "video/vnd.dlna.mpeg-tts"}, + {".m2ts", "video/vnd.dlna.mpeg-tts"}, + {".m2v", "video/mpeg"}, + {".m3u", "audio/x-mpegurl"}, + {".m3u8", "audio/x-mpegurl"}, + {".m4a", "audio/m4a"}, + {".m4b", "audio/m4b"}, + {".m4p", "audio/m4p"}, + {".m4r", "audio/x-m4r"}, + {".m4v", "video/x-m4v"}, + {".mac", "image/x-macpaint"}, + {".mak", "text/plain"}, + {".man", "application/x-troff-man"}, + {".manifest", "application/x-ms-manifest"}, + {".map", "text/plain"}, + {".master", "application/xml"}, + {".mbox", "application/mbox"}, + {".mda", "application/msaccess"}, + {".mdb", "application/x-msaccess"}, + {".mde", "application/msaccess"}, + {".mdp", "application/octet-stream"}, + {".me", "application/x-troff-me"}, + {".mfp", "application/x-shockwave-flash"}, + {".mht", "message/rfc822"}, + {".mhtml", "message/rfc822"}, + {".mid", "audio/mid"}, + {".midi", "audio/mid"}, + {".mix", "application/octet-stream"}, + {".mk", "text/plain"}, + {".mk3d", "video/x-matroska-3d"}, + {".mka", "audio/x-matroska"}, + {".mkv", "video/x-matroska"}, + {".mmf", "application/x-smaf"}, + {".mno", "text/xml"}, + {".mny", "application/x-msmoney"}, + {".mod", "video/mpeg"}, + {".mov", "video/quicktime"}, + {".movie", "video/x-sgi-movie"}, + {".mp2", "video/mpeg"}, + {".mp2v", "video/mpeg"}, + {".mp3", "audio/mpeg"}, + {".mp4", "video/mp4"}, + {".mp4v", "video/mp4"}, + {".mpa", "video/mpeg"}, + {".mpe", "video/mpeg"}, + {".mpeg", "video/mpeg"}, + {".mpf", "application/vnd.ms-mediapackage"}, + {".mpg", "video/mpeg"}, + {".mpp", "application/vnd.ms-project"}, + {".mpv2", "video/mpeg"}, + {".mqv", "video/quicktime"}, + {".ms", "application/x-troff-ms"}, + {".msg", "application/vnd.ms-outlook"}, + {".msi", "application/octet-stream"}, + {".mso", "application/octet-stream"}, + {".mts", "video/vnd.dlna.mpeg-tts"}, + {".mtx", "application/xml"}, + {".mvb", "application/x-msmediaview"}, + {".mvc", "application/x-miva-compiled"}, + {".mxf", "application/mxf"}, + {".mxp", "application/x-mmxp"}, + {".nc", "application/x-netcdf"}, + {".nsc", "video/x-ms-asf"}, + {".nws", "message/rfc822"}, + {".ocx", "application/octet-stream"}, + {".oda", "application/oda"}, + {".odb", "application/vnd.oasis.opendocument.database"}, + {".odc", "application/vnd.oasis.opendocument.chart"}, + {".odf", "application/vnd.oasis.opendocument.formula"}, + {".odg", "application/vnd.oasis.opendocument.graphics"}, + {".odh", "text/plain"}, + {".odi", "application/vnd.oasis.opendocument.image"}, + {".odl", "text/plain"}, + {".odm", "application/vnd.oasis.opendocument.text-master"}, + {".odp", "application/vnd.oasis.opendocument.presentation"}, + {".ods", "application/vnd.oasis.opendocument.spreadsheet"}, + {".odt", "application/vnd.oasis.opendocument.text"}, + {".oga", "audio/ogg"}, + {".ogg", "audio/ogg"}, + {".ogv", "video/ogg"}, + {".ogx", "application/ogg"}, + {".one", "application/onenote"}, + {".onea", "application/onenote"}, + {".onepkg", "application/onenote"}, + {".onetmp", "application/onenote"}, + {".onetoc", "application/onenote"}, + {".onetoc2", "application/onenote"}, + {".opus", "audio/ogg"}, + {".orderedtest", "application/xml"}, + {".osdx", "application/opensearchdescription+xml"}, + {".otf", "application/font-sfnt"}, + {".otg", "application/vnd.oasis.opendocument.graphics-template"}, + {".oth", "application/vnd.oasis.opendocument.text-web"}, + {".otp", "application/vnd.oasis.opendocument.presentation-template"}, + {".ots", "application/vnd.oasis.opendocument.spreadsheet-template"}, + {".ott", "application/vnd.oasis.opendocument.text-template"}, + {".oxt", "application/vnd.openofficeorg.extension"}, + {".p10", "application/pkcs10"}, + {".p12", "application/x-pkcs12"}, + {".p7b", "application/x-pkcs7-certificates"}, + {".p7c", "application/pkcs7-mime"}, + {".p7m", "application/pkcs7-mime"}, + {".p7r", "application/x-pkcs7-certreqresp"}, + {".p7s", "application/pkcs7-signature"}, + {".pbm", "image/x-portable-bitmap"}, + {".pcast", "application/x-podcast"}, + {".pct", "image/pict"}, + {".pcx", "application/octet-stream"}, + {".pcz", "application/octet-stream"}, + {".pdf", "application/pdf"}, + {".pfb", "application/octet-stream"}, + {".pfm", "application/octet-stream"}, + {".pfx", "application/x-pkcs12"}, + {".pgm", "image/x-portable-graymap"}, + {".pic", "image/pict"}, + {".pict", "image/pict"}, + {".pkgdef", "text/plain"}, + {".pkgundef", "text/plain"}, + {".pko", "application/vnd.ms-pki.pko"}, + {".pls", "audio/scpls"}, + {".pma", "application/x-perfmon"}, + {".pmc", "application/x-perfmon"}, + {".pml", "application/x-perfmon"}, + {".pmr", "application/x-perfmon"}, + {".pmw", "application/x-perfmon"}, + {".png", "image/png"}, + {".pnm", "image/x-portable-anymap"}, + {".pnt", "image/x-macpaint"}, + {".pntg", "image/x-macpaint"}, + {".pnz", "image/png"}, + {".pot", "application/vnd.ms-powerpoint"}, + {".potm", "application/vnd.ms-powerpoint.template.macroEnabled.12"}, + {".potx", "application/vnd.openxmlformats-officedocument.presentationml.template"}, + {".ppa", "application/vnd.ms-powerpoint"}, + {".ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12"}, + {".ppm", "image/x-portable-pixmap"}, + {".pps", "application/vnd.ms-powerpoint"}, + {".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12"}, + {".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow"}, + {".ppt", "application/vnd.ms-powerpoint"}, + {".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12"}, + {".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, + {".prf", "application/pics-rules"}, + {".prm", "application/octet-stream"}, + {".prx", "application/octet-stream"}, + {".ps", "application/postscript"}, + {".psc1", "application/PowerShell"}, + {".psd", "application/octet-stream"}, + {".psess", "application/xml"}, + {".psm", "application/octet-stream"}, + {".psp", "application/octet-stream"}, + {".pst", "application/vnd.ms-outlook"}, + {".pub", "application/x-mspublisher"}, + {".pwz", "application/vnd.ms-powerpoint"}, + {".qht", "text/x-html-insertion"}, + {".qhtm", "text/x-html-insertion"}, + {".qt", "video/quicktime"}, + {".qti", "image/x-quicktime"}, + {".qtif", "image/x-quicktime"}, + {".qtl", "application/x-quicktimeplayer"}, + {".qxd", "application/octet-stream"}, + {".ra", "audio/x-pn-realaudio"}, + {".ram", "audio/x-pn-realaudio"}, + {".rar", "application/x-rar-compressed"}, + {".ras", "image/x-cmu-raster"}, + {".rat", "application/rat-file"}, + {".rc", "text/plain"}, + {".rc2", "text/plain"}, + {".rct", "text/plain"}, + {".rdlc", "application/xml"}, + {".reg", "text/plain"}, + {".resx", "application/xml"}, + {".rf", "image/vnd.rn-realflash"}, + {".rgb", "image/x-rgb"}, + {".rgs", "text/plain"}, + {".rm", "application/vnd.rn-realmedia"}, + {".rmi", "audio/mid"}, + {".rmp", "application/vnd.rn-rn_music_package"}, + {".roff", "application/x-troff"}, + {".rpm", "audio/x-pn-realaudio-plugin"}, + {".rqy", "text/x-ms-rqy"}, + {".rtf", "application/rtf"}, + {".rtx", "text/richtext"}, + {".rvt", "application/octet-stream" }, + {".ruleset", "application/xml"}, + {".s", "text/plain"}, + {".safariextz", "application/x-safari-safariextz"}, + {".scd", "application/x-msschedule"}, + {".scr", "text/plain"}, + {".sct", "text/scriptlet"}, + {".sd2", "audio/x-sd2"}, + {".sdp", "application/sdp"}, + {".sea", "application/octet-stream"}, + {".searchConnector-ms", "application/windows-search-connector+xml"}, + {".setpay", "application/set-payment-initiation"}, + {".setreg", "application/set-registration-initiation"}, + {".settings", "application/xml"}, + {".sgimb", "application/x-sgimb"}, + {".sgml", "text/sgml"}, + {".sh", "application/x-sh"}, + {".shar", "application/x-shar"}, + {".shtml", "text/html"}, + {".sit", "application/x-stuffit"}, + {".sitemap", "application/xml"}, + {".skin", "application/xml"}, + {".skp", "application/x-koan" }, + {".sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12"}, + {".sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide"}, + {".slk", "application/vnd.ms-excel"}, + {".sln", "text/plain"}, + {".slupkg-ms", "application/x-ms-license"}, + {".smd", "audio/x-smd"}, + {".smi", "application/octet-stream"}, + {".smx", "audio/x-smd"}, + {".smz", "audio/x-smd"}, + {".snd", "audio/basic"}, + {".snippet", "application/xml"}, + {".snp", "application/octet-stream"}, + {".sol", "text/plain"}, + {".sor", "text/plain"}, + {".spc", "application/x-pkcs7-certificates"}, + {".spl", "application/futuresplash"}, + {".spx", "audio/ogg"}, + {".src", "application/x-wais-source"}, + {".srf", "text/plain"}, + {".SSISDeploymentManifest", "text/xml"}, + {".ssm", "application/streamingmedia"}, + {".sst", "application/vnd.ms-pki.certstore"}, + {".stl", "application/vnd.ms-pki.stl"}, + {".sv4cpio", "application/x-sv4cpio"}, + {".sv4crc", "application/x-sv4crc"}, + {".svc", "application/xml"}, + {".svg", "image/svg+xml"}, + {".swf", "application/x-shockwave-flash"}, + {".step", "application/step"}, + {".stp", "application/step"}, + {".t", "application/x-troff"}, + {".tar", "application/x-tar"}, + {".tcl", "application/x-tcl"}, + {".testrunconfig", "application/xml"}, + {".testsettings", "application/xml"}, + {".tex", "application/x-tex"}, + {".texi", "application/x-texinfo"}, + {".texinfo", "application/x-texinfo"}, + {".tgz", "application/x-compressed"}, + {".thmx", "application/vnd.ms-officetheme"}, + {".thn", "application/octet-stream"}, + {".tif", "image/tiff"}, + {".tiff", "image/tiff"}, + {".tlh", "text/plain"}, + {".tli", "text/plain"}, + {".toc", "application/octet-stream"}, + {".tr", "application/x-troff"}, + {".trm", "application/x-msterminal"}, + {".trx", "application/xml"}, + {".ts", "video/vnd.dlna.mpeg-tts"}, + {".tsv", "text/tab-separated-values"}, + {".ttf", "application/font-sfnt"}, + {".tts", "video/vnd.dlna.mpeg-tts"}, + {".txt", "text/plain"}, + {".u32", "application/octet-stream"}, + {".uls", "text/iuls"}, + {".user", "text/plain"}, + {".ustar", "application/x-ustar"}, + {".vb", "text/plain"}, + {".vbdproj", "text/plain"}, + {".vbk", "video/mpeg"}, + {".vbproj", "text/plain"}, + {".vbs", "text/vbscript"}, + {".vcf", "text/x-vcard"}, + {".vcproj", "application/xml"}, + {".vcs", "text/plain"}, + {".vcxproj", "application/xml"}, + {".vddproj", "text/plain"}, + {".vdp", "text/plain"}, + {".vdproj", "text/plain"}, + {".vdx", "application/vnd.ms-visio.viewer"}, + {".vml", "text/xml"}, + {".vscontent", "application/xml"}, + {".vsct", "text/xml"}, + {".vsd", "application/vnd.visio"}, + {".vsi", "application/ms-vsi"}, + {".vsix", "application/vsix"}, + {".vsixlangpack", "text/xml"}, + {".vsixmanifest", "text/xml"}, + {".vsmdi", "application/xml"}, + {".vspscc", "text/plain"}, + {".vss", "application/vnd.visio"}, + {".vsscc", "text/plain"}, + {".vssettings", "text/xml"}, + {".vssscc", "text/plain"}, + {".vst", "application/vnd.visio"}, + {".vstemplate", "text/xml"}, + {".vsto", "application/x-ms-vsto"}, + {".vsw", "application/vnd.visio"}, + {".vsx", "application/vnd.visio"}, + {".vtt", "text/vtt"}, + {".vtx", "application/vnd.visio"}, + {".wasm", "application/wasm"}, + {".wav", "audio/wav"}, + {".wave", "audio/wav"}, + {".wax", "audio/x-ms-wax"}, + {".wbk", "application/msword"}, + {".wbmp", "image/vnd.wap.wbmp"}, + {".wcm", "application/vnd.ms-works"}, + {".wdb", "application/vnd.ms-works"}, + {".wdp", "image/vnd.ms-photo"}, + {".webarchive", "application/x-safari-webarchive"}, + {".webm", "video/webm"}, + {".webp", "image/webp"}, /* https://en.wikipedia.org/wiki/WebP */ + {".webtest", "application/xml"}, + {".wiq", "application/xml"}, + {".wiz", "application/msword"}, + {".wks", "application/vnd.ms-works"}, + {".WLMP", "application/wlmoviemaker"}, + {".wlpginstall", "application/x-wlpg-detect"}, + {".wlpginstall3", "application/x-wlpg3-detect"}, + {".wm", "video/x-ms-wm"}, + {".wma", "audio/x-ms-wma"}, + {".wmd", "application/x-ms-wmd"}, + {".wmf", "application/x-msmetafile"}, + {".wml", "text/vnd.wap.wml"}, + {".wmlc", "application/vnd.wap.wmlc"}, + {".wmls", "text/vnd.wap.wmlscript"}, + {".wmlsc", "application/vnd.wap.wmlscriptc"}, + {".wmp", "video/x-ms-wmp"}, + {".wmv", "video/x-ms-wmv"}, + {".wmx", "video/x-ms-wmx"}, + {".wmz", "application/x-ms-wmz"}, + {".woff", "application/font-woff"}, + {".woff2", "application/font-woff2"}, + {".wpl", "application/vnd.ms-wpl"}, + {".wps", "application/vnd.ms-works"}, + {".wri", "application/x-mswrite"}, + {".wrl", "x-world/x-vrml"}, + {".wrz", "x-world/x-vrml"}, + {".wsc", "text/scriptlet"}, + {".wsdl", "text/xml"}, + {".wvx", "video/x-ms-wvx"}, + {".x", "application/directx"}, + {".xaf", "x-world/x-vrml"}, + {".xaml", "application/xaml+xml"}, + {".xap", "application/x-silverlight-app"}, + {".xbap", "application/x-ms-xbap"}, + {".xbm", "image/x-xbitmap"}, + {".xdr", "text/plain"}, + {".xht", "application/xhtml+xml"}, + {".xhtml", "application/xhtml+xml"}, + {".xla", "application/vnd.ms-excel"}, + {".xlam", "application/vnd.ms-excel.addin.macroEnabled.12"}, + {".xlc", "application/vnd.ms-excel"}, + {".xld", "application/vnd.ms-excel"}, + {".xlk", "application/vnd.ms-excel"}, + {".xll", "application/vnd.ms-excel"}, + {".xlm", "application/vnd.ms-excel"}, + {".xls", "application/vnd.ms-excel"}, + {".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12"}, + {".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12"}, + {".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, + {".xlt", "application/vnd.ms-excel"}, + {".xltm", "application/vnd.ms-excel.template.macroEnabled.12"}, + {".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template"}, + {".xlw", "application/vnd.ms-excel"}, + {".xml", "text/xml"}, + {".xmp", "application/octet-stream" }, + {".xmta", "application/xml"}, + {".xof", "x-world/x-vrml"}, + {".XOML", "text/plain"}, + {".xpm", "image/x-xpixmap"}, + {".xps", "application/vnd.ms-xpsdocument"}, + {".xrm-ms", "text/xml"}, + {".xsc", "application/xml"}, + {".xsd", "text/xml"}, + {".xsf", "text/xml"}, + {".xsl", "text/xml"}, + {".xslt", "text/xml"}, + {".xsn", "application/octet-stream"}, + {".xss", "application/xml"}, + {".xspf", "application/xspf+xml"}, + {".xtp", "application/octet-stream"}, + {".xwd", "image/x-xwindowdump"}, + {".z", "application/x-compress"}, + {".zip", "application/zip"}, + + {"application/fsharp-script", ".fsx"}, + {"application/msaccess", ".adp"}, + {"application/msword", ".doc"}, + {"application/octet-stream", ".bin"}, + {"application/onenote", ".one"}, + {"application/postscript", ".eps"}, + {"application/step", ".step"}, + {"application/vnd.ms-excel", ".xls"}, + {"application/vnd.ms-powerpoint", ".ppt"}, + {"application/vnd.ms-works", ".wks"}, + {"application/vnd.visio", ".vsd"}, + {"application/x-director", ".dir"}, + {"application/x-shockwave-flash", ".swf"}, + {"application/x-x509-ca-cert", ".cer"}, + {"application/x-zip-compressed", ".zip"}, + {"application/xhtml+xml", ".xhtml"}, + {"application/xml", ".xml"}, // anomoly, .xml -> text/xml, but application/xml -> many thingss, but all are xml, so safest is .xml + {"audio/aac", ".AAC"}, + {"audio/aiff", ".aiff"}, + {"audio/basic", ".snd"}, + {"audio/mid", ".midi"}, + {"audio/wav", ".wav"}, + {"audio/x-m4a", ".m4a"}, + {"audio/x-mpegurl", ".m3u"}, + {"audio/x-pn-realaudio", ".ra"}, + {"audio/x-smd", ".smd"}, + {"image/bmp", ".bmp"}, + {"image/jpeg", ".jpg"}, + {"image/pict", ".pic"}, + {"image/png", ".png"}, //Defined in [RFC-2045], [RFC-2048] + {"image/x-png", ".png"}, //See https://www.w3.org/TR/PNG/#A-Media-type :"It is recommended that implementations also recognize the media type "image/x-png"." + {"image/tiff", ".tiff"}, + {"image/x-macpaint", ".mac"}, + {"image/x-quicktime", ".qti"}, + {"message/rfc822", ".eml"}, + {"text/html", ".html"}, + {"text/plain", ".txt"}, + {"text/scriptlet", ".wsc"}, + {"text/xml", ".xml"}, + {"video/3gpp", ".3gp"}, + {"video/3gpp2", ".3gp2"}, + {"video/mp4", ".mp4"}, + {"video/mpeg", ".mpg"}, + {"video/quicktime", ".mov"}, + {"video/vnd.dlna.mpeg-tts", ".m2t"}, + {"video/x-dv", ".dv"}, + {"video/x-la-asf", ".lsf"}, + {"video/x-ms-asf", ".asf"}, + {"x-world/x-vrml", ".xof"}, + + #endregion + + }; + + var cache = mappings.ToList(); // need ToList() to avoid modifying while still enumerating + + foreach (var mapping in cache) + { + if (!mappings.ContainsKey(mapping.Value)) + { + mappings.Add(mapping.Value, mapping.Key); + } + } + + return mappings; + } + + public static string GetMimeType(string extension) + { + if (extension == null) + { + throw new ArgumentNullException("extension"); + } + + if (!extension.StartsWith(".")) + { + extension = "." + extension; + } + + string mime; + + return _mappings.Value.TryGetValue(extension, out mime) ? mime : "application/octet-stream"; + } + + public static string GetExtension(string mimeType) + { + return GetExtension(mimeType, true); + } + + public static string GetExtension(string mimeType, bool throwErrorIfNotFound) + { + if (mimeType == null) + { + throw new ArgumentNullException("mimeType"); + } + + if (mimeType.StartsWith(".")) + { + throw new ArgumentException("Requested mime type is not valid: " + mimeType); + } + + string extension; + + if (_mappings.Value.TryGetValue(mimeType, out extension)) + { + return extension; + } + if (throwErrorIfNotFound) + { + throw new ArgumentException("Requested mime type is not registered: " + mimeType); + } + else + { + return string.Empty; + } + } + } +} diff --git a/message/TokenReader.cs b/message/TokenReader.cs new file mode 100644 index 0000000..ad5cefe --- /dev/null +++ b/message/TokenReader.cs @@ -0,0 +1,68 @@ +// /** +// * File: TokenReader.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.IO; +using System.Text; +using System.Linq; +namespace ln.http.message +{ + public class TokenReader : TextReader + { + public static char[] specialChars = new char[] { '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=' }; + + public TextReader BaseReader { get; } + + public TokenReader(String text) + :this(new StringReader(text)) + {} + public TokenReader(TextReader baseReader) + { + BaseReader = baseReader; + } + + public override int Read() => BaseReader.Read(); + public override int Peek() => BaseReader.Peek(); + + public string ReadToken() + { + while ((BaseReader.Peek() != -1) && char.IsWhiteSpace((char)BaseReader.Peek())) + BaseReader.Read(); + + StringBuilder stringBuilder = new StringBuilder(); + int ch; + do + { + ch = BaseReader.Peek(); + + if ((ch <= ' ') || specialChars.Contains((char)ch)) + return stringBuilder.ToString(); + + stringBuilder.Append((char)BaseReader.Read()); + } while (ch != -1); + + return stringBuilder.ToString(); + } + + public string ReadQuotedString() + { + StringBuilder stringBuilder = new StringBuilder(); + + if (BaseReader.Read() != '"') + throw new FormatException("quoted string must start with \""); + + int ch; + while (((ch = BaseReader.Read()) != -1) && (ch != '"')) + stringBuilder.Append((char)ch); + + return stringBuilder.ToString(); + } + + } +} diff --git a/message/parser/HTTP.cs b/message/parser/HTTP.cs new file mode 100644 index 0000000..167ca49 --- /dev/null +++ b/message/parser/HTTP.cs @@ -0,0 +1,40 @@ +// /** +// * File: HTTP.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.IO; +using System.Collections.Generic; +using ln.http.exceptions; +namespace ln.http.message.parser +{ + public static class HTTP + { + public static HeaderContainer ReadHeader(TextReader reader) + { + List headerLines = new List(); + string currentline = reader.ReadLine(); + while (!currentline.Equals(string.Empty)) + { + if (char.IsWhiteSpace(currentline[0])) + throw new BadRequestException(); + + headerLines.Add(currentline.Trim()); + + currentline = reader.ReadLine(); + } + + HeaderContainer headerContainer = new HeaderContainer(); + + foreach (string headerLine in headerLines) + headerContainer.Add(new Header(headerLine)); + + return headerContainer; + } + } +} diff --git a/message/parser/MIME.cs b/message/parser/MIME.cs new file mode 100644 index 0000000..19d314c --- /dev/null +++ b/message/parser/MIME.cs @@ -0,0 +1,43 @@ +// /** +// * File: MIME.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.IO; +using System.Collections.Generic; +namespace ln.http.message.parser +{ + public static class MIME + { + public static HeaderContainer ReadHeader(TextReader reader) + { + List headerLines = new List(); + string currentline = reader.ReadLine(); + while (!currentline.Equals(string.Empty)) + { + if (char.IsWhiteSpace(currentline[0])) + { + headerLines[headerLines.Count - 1] = headerLines[headerLines.Count - 1] + currentline; + } + else + { + headerLines.Add(currentline); + } + currentline = reader.ReadLine(); + } + + HeaderContainer headerContainer = new HeaderContainer(); + + foreach (string headerLine in headerLines) + headerContainer.Add(new Header(headerLine)); + + return headerContainer; + } + + } +} diff --git a/mime/MimeTypeMap.cs b/mime/MimeTypeMap.cs new file mode 100644 index 0000000..7bddbfc --- /dev/null +++ b/mime/MimeTypeMap.cs @@ -0,0 +1,767 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ln.http.mime +{ + public static class MimeTypeMap + { + private static readonly Lazy> _mappings = new Lazy>(BuildMappings); + + private static IDictionary BuildMappings() + { + var mappings = new Dictionary(StringComparer.OrdinalIgnoreCase) { + + #region Big freaking list of mime types + + // maps both ways, + // extension -> mime type + // and + // mime type -> extension + // + // any mime types on left side not pre-loaded on right side, are added automatically + // some mime types can map to multiple extensions, so to get a deterministic mapping, + // add those to the dictionary specifcially + // + // combination of values from Windows 7 Registry and + // from C:\Windows\System32\inetsrv\config\applicationHost.config + // some added, including .7z and .dat + // + // Some added based on http://www.iana.org/assignments/media-types/media-types.xhtml + // which lists mime types, but not extensions + // + {".323", "text/h323"}, + {".3g2", "video/3gpp2"}, + {".3gp", "video/3gpp"}, + {".3gp2", "video/3gpp2"}, + {".3gpp", "video/3gpp"}, + {".7z", "application/x-7z-compressed"}, + {".aa", "audio/audible"}, + {".AAC", "audio/aac"}, + {".aaf", "application/octet-stream"}, + {".aax", "audio/vnd.audible.aax"}, + {".ac3", "audio/ac3"}, + {".aca", "application/octet-stream"}, + {".accda", "application/msaccess.addin"}, + {".accdb", "application/msaccess"}, + {".accdc", "application/msaccess.cab"}, + {".accde", "application/msaccess"}, + {".accdr", "application/msaccess.runtime"}, + {".accdt", "application/msaccess"}, + {".accdw", "application/msaccess.webapplication"}, + {".accft", "application/msaccess.ftemplate"}, + {".acx", "application/internet-property-stream"}, + {".AddIn", "text/xml"}, + {".ade", "application/msaccess"}, + {".adobebridge", "application/x-bridge-url"}, + {".adp", "application/msaccess"}, + {".ADT", "audio/vnd.dlna.adts"}, + {".ADTS", "audio/aac"}, + {".afm", "application/octet-stream"}, + {".ai", "application/postscript"}, + {".aif", "audio/aiff"}, + {".aifc", "audio/aiff"}, + {".aiff", "audio/aiff"}, + {".air", "application/vnd.adobe.air-application-installer-package+zip"}, + {".amc", "application/mpeg"}, + {".anx", "application/annodex"}, + {".apk", "application/vnd.android.package-archive" }, + {".application", "application/x-ms-application"}, + {".art", "image/x-jg"}, + {".asa", "application/xml"}, + {".asax", "application/xml"}, + {".ascx", "application/xml"}, + {".asd", "application/octet-stream"}, + {".asf", "video/x-ms-asf"}, + {".ashx", "application/xml"}, + {".asi", "application/octet-stream"}, + {".asm", "text/plain"}, + {".asmx", "application/xml"}, + {".aspx", "application/xml"}, + {".asr", "video/x-ms-asf"}, + {".asx", "video/x-ms-asf"}, + {".atom", "application/atom+xml"}, + {".au", "audio/basic"}, + {".avi", "video/x-msvideo"}, + {".axa", "audio/annodex"}, + {".axs", "application/olescript"}, + {".axv", "video/annodex"}, + {".bas", "text/plain"}, + {".bcpio", "application/x-bcpio"}, + {".bin", "application/octet-stream"}, + {".bmp", "image/bmp"}, + {".c", "text/plain"}, + {".cab", "application/octet-stream"}, + {".caf", "audio/x-caf"}, + {".calx", "application/vnd.ms-office.calx"}, + {".cat", "application/vnd.ms-pki.seccat"}, + {".cc", "text/plain"}, + {".cd", "text/plain"}, + {".cdda", "audio/aiff"}, + {".cdf", "application/x-cdf"}, + {".cer", "application/x-x509-ca-cert"}, + {".cfg", "text/plain"}, + {".chm", "application/octet-stream"}, + {".class", "application/x-java-applet"}, + {".clp", "application/x-msclip"}, + {".cmd", "text/plain"}, + {".cmx", "image/x-cmx"}, + {".cnf", "text/plain"}, + {".cod", "image/cis-cod"}, + {".config", "application/xml"}, + {".contact", "text/x-ms-contact"}, + {".coverage", "application/xml"}, + {".cpio", "application/x-cpio"}, + {".cpp", "text/plain"}, + {".crd", "application/x-mscardfile"}, + {".crl", "application/pkix-crl"}, + {".crt", "application/x-x509-ca-cert"}, + {".cs", "text/plain"}, + {".csdproj", "text/plain"}, + {".csh", "application/x-csh"}, + {".csproj", "text/plain"}, + {".css", "text/css"}, + {".csv", "text/csv"}, + {".cur", "application/octet-stream"}, + {".cxx", "text/plain"}, + {".dat", "application/octet-stream"}, + {".datasource", "application/xml"}, + {".dbproj", "text/plain"}, + {".dcr", "application/x-director"}, + {".def", "text/plain"}, + {".deploy", "application/octet-stream"}, + {".der", "application/x-x509-ca-cert"}, + {".dgml", "application/xml"}, + {".dib", "image/bmp"}, + {".dif", "video/x-dv"}, + {".dir", "application/x-director"}, + {".disco", "text/xml"}, + {".divx", "video/divx"}, + {".dll", "application/x-msdownload"}, + {".dll.config", "text/xml"}, + {".dlm", "text/dlm"}, + {".doc", "application/msword"}, + {".docm", "application/vnd.ms-word.document.macroEnabled.12"}, + {".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, + {".dot", "application/msword"}, + {".dotm", "application/vnd.ms-word.template.macroEnabled.12"}, + {".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template"}, + {".dsp", "application/octet-stream"}, + {".dsw", "text/plain"}, + {".dtd", "text/xml"}, + {".dtsConfig", "text/xml"}, + {".dv", "video/x-dv"}, + {".dvi", "application/x-dvi"}, + {".dwf", "drawing/x-dwf"}, + {".dwg", "application/acad"}, + {".dwp", "application/octet-stream"}, + {".dxf", "application/x-dxf" }, + {".dxr", "application/x-director"}, + {".eml", "message/rfc822"}, + {".emz", "application/octet-stream"}, + {".eot", "application/vnd.ms-fontobject"}, + {".eps", "application/postscript"}, + {".es", "application/ecmascript"}, + {".etl", "application/etl"}, + {".etx", "text/x-setext"}, + {".evy", "application/envoy"}, + {".exe", "application/vnd.microsoft.portable-executable"}, + {".exe.config", "text/xml"}, + {".f4v", "video/mp4"}, + {".fdf", "application/vnd.fdf"}, + {".fif", "application/fractals"}, + {".filters", "application/xml"}, + {".fla", "application/octet-stream"}, + {".flac", "audio/flac"}, + {".flr", "x-world/x-vrml"}, + {".flv", "video/x-flv"}, + {".fsscript", "application/fsharp-script"}, + {".fsx", "application/fsharp-script"}, + {".generictest", "application/xml"}, + {".gif", "image/gif"}, + {".gpx", "application/gpx+xml"}, + {".group", "text/x-ms-group"}, + {".gsm", "audio/x-gsm"}, + {".gtar", "application/x-gtar"}, + {".gz", "application/x-gzip"}, + {".h", "text/plain"}, + {".hdf", "application/x-hdf"}, + {".hdml", "text/x-hdml"}, + {".hhc", "application/x-oleobject"}, + {".hhk", "application/octet-stream"}, + {".hhp", "application/octet-stream"}, + {".hlp", "application/winhlp"}, + {".hpp", "text/plain"}, + {".hqx", "application/mac-binhex40"}, + {".hta", "application/hta"}, + {".htc", "text/x-component"}, + {".htm", "text/html"}, + {".html", "text/html"}, + {".htt", "text/webviewhtml"}, + {".hxa", "application/xml"}, + {".hxc", "application/xml"}, + {".hxd", "application/octet-stream"}, + {".hxe", "application/xml"}, + {".hxf", "application/xml"}, + {".hxh", "application/octet-stream"}, + {".hxi", "application/octet-stream"}, + {".hxk", "application/xml"}, + {".hxq", "application/octet-stream"}, + {".hxr", "application/octet-stream"}, + {".hxs", "application/octet-stream"}, + {".hxt", "text/html"}, + {".hxv", "application/xml"}, + {".hxw", "application/octet-stream"}, + {".hxx", "text/plain"}, + {".i", "text/plain"}, + {".ico", "image/x-icon"}, + {".ics", "application/octet-stream"}, + {".idl", "text/plain"}, + {".ief", "image/ief"}, + {".iii", "application/x-iphone"}, + {".inc", "text/plain"}, + {".inf", "application/octet-stream"}, + {".ini", "text/plain"}, + {".inl", "text/plain"}, + {".ins", "application/x-internet-signup"}, + {".ipa", "application/x-itunes-ipa"}, + {".ipg", "application/x-itunes-ipg"}, + {".ipproj", "text/plain"}, + {".ipsw", "application/x-itunes-ipsw"}, + {".iqy", "text/x-ms-iqy"}, + {".isp", "application/x-internet-signup"}, + {".isma", "application/octet-stream"}, + {".ismv", "application/octet-stream"}, + {".ite", "application/x-itunes-ite"}, + {".itlp", "application/x-itunes-itlp"}, + {".itms", "application/x-itunes-itms"}, + {".itpc", "application/x-itunes-itpc"}, + {".IVF", "video/x-ivf"}, + {".jar", "application/java-archive"}, + {".java", "application/octet-stream"}, + {".jck", "application/liquidmotion"}, + {".jcz", "application/liquidmotion"}, + {".jfif", "image/pjpeg"}, + {".jnlp", "application/x-java-jnlp-file"}, + {".jpb", "application/octet-stream"}, + {".jpe", "image/jpeg"}, + {".jpeg", "image/jpeg"}, + {".jpg", "image/jpeg"}, + {".js", "application/javascript"}, + {".json", "application/json"}, + {".jsx", "text/jscript"}, + {".jsxbin", "text/plain"}, + {".latex", "application/x-latex"}, + {".library-ms", "application/windows-library+xml"}, + {".lit", "application/x-ms-reader"}, + {".loadtest", "application/xml"}, + {".lpk", "application/octet-stream"}, + {".lsf", "video/x-la-asf"}, + {".lst", "text/plain"}, + {".lsx", "video/x-la-asf"}, + {".lzh", "application/octet-stream"}, + {".m13", "application/x-msmediaview"}, + {".m14", "application/x-msmediaview"}, + {".m1v", "video/mpeg"}, + {".m2t", "video/vnd.dlna.mpeg-tts"}, + {".m2ts", "video/vnd.dlna.mpeg-tts"}, + {".m2v", "video/mpeg"}, + {".m3u", "audio/x-mpegurl"}, + {".m3u8", "audio/x-mpegurl"}, + {".m4a", "audio/m4a"}, + {".m4b", "audio/m4b"}, + {".m4p", "audio/m4p"}, + {".m4r", "audio/x-m4r"}, + {".m4v", "video/x-m4v"}, + {".mac", "image/x-macpaint"}, + {".mak", "text/plain"}, + {".man", "application/x-troff-man"}, + {".manifest", "application/x-ms-manifest"}, + {".map", "text/plain"}, + {".master", "application/xml"}, + {".mbox", "application/mbox"}, + {".mda", "application/msaccess"}, + {".mdb", "application/x-msaccess"}, + {".mde", "application/msaccess"}, + {".mdp", "application/octet-stream"}, + {".me", "application/x-troff-me"}, + {".mfp", "application/x-shockwave-flash"}, + {".mht", "message/rfc822"}, + {".mhtml", "message/rfc822"}, + {".mid", "audio/mid"}, + {".midi", "audio/mid"}, + {".mix", "application/octet-stream"}, + {".mk", "text/plain"}, + {".mk3d", "video/x-matroska-3d"}, + {".mka", "audio/x-matroska"}, + {".mkv", "video/x-matroska"}, + {".mmf", "application/x-smaf"}, + {".mno", "text/xml"}, + {".mny", "application/x-msmoney"}, + {".mod", "video/mpeg"}, + {".mov", "video/quicktime"}, + {".movie", "video/x-sgi-movie"}, + {".mp2", "video/mpeg"}, + {".mp2v", "video/mpeg"}, + {".mp3", "audio/mpeg"}, + {".mp4", "video/mp4"}, + {".mp4v", "video/mp4"}, + {".mpa", "video/mpeg"}, + {".mpe", "video/mpeg"}, + {".mpeg", "video/mpeg"}, + {".mpf", "application/vnd.ms-mediapackage"}, + {".mpg", "video/mpeg"}, + {".mpp", "application/vnd.ms-project"}, + {".mpv2", "video/mpeg"}, + {".mqv", "video/quicktime"}, + {".ms", "application/x-troff-ms"}, + {".msg", "application/vnd.ms-outlook"}, + {".msi", "application/octet-stream"}, + {".mso", "application/octet-stream"}, + {".mts", "video/vnd.dlna.mpeg-tts"}, + {".mtx", "application/xml"}, + {".mvb", "application/x-msmediaview"}, + {".mvc", "application/x-miva-compiled"}, + {".mxf", "application/mxf"}, + {".mxp", "application/x-mmxp"}, + {".nc", "application/x-netcdf"}, + {".nsc", "video/x-ms-asf"}, + {".nws", "message/rfc822"}, + {".ocx", "application/octet-stream"}, + {".oda", "application/oda"}, + {".odb", "application/vnd.oasis.opendocument.database"}, + {".odc", "application/vnd.oasis.opendocument.chart"}, + {".odf", "application/vnd.oasis.opendocument.formula"}, + {".odg", "application/vnd.oasis.opendocument.graphics"}, + {".odh", "text/plain"}, + {".odi", "application/vnd.oasis.opendocument.image"}, + {".odl", "text/plain"}, + {".odm", "application/vnd.oasis.opendocument.text-master"}, + {".odp", "application/vnd.oasis.opendocument.presentation"}, + {".ods", "application/vnd.oasis.opendocument.spreadsheet"}, + {".odt", "application/vnd.oasis.opendocument.text"}, + {".oga", "audio/ogg"}, + {".ogg", "audio/ogg"}, + {".ogv", "video/ogg"}, + {".ogx", "application/ogg"}, + {".one", "application/onenote"}, + {".onea", "application/onenote"}, + {".onepkg", "application/onenote"}, + {".onetmp", "application/onenote"}, + {".onetoc", "application/onenote"}, + {".onetoc2", "application/onenote"}, + {".opus", "audio/ogg"}, + {".orderedtest", "application/xml"}, + {".osdx", "application/opensearchdescription+xml"}, + {".otf", "application/font-sfnt"}, + {".otg", "application/vnd.oasis.opendocument.graphics-template"}, + {".oth", "application/vnd.oasis.opendocument.text-web"}, + {".otp", "application/vnd.oasis.opendocument.presentation-template"}, + {".ots", "application/vnd.oasis.opendocument.spreadsheet-template"}, + {".ott", "application/vnd.oasis.opendocument.text-template"}, + {".oxt", "application/vnd.openofficeorg.extension"}, + {".p10", "application/pkcs10"}, + {".p12", "application/x-pkcs12"}, + {".p7b", "application/x-pkcs7-certificates"}, + {".p7c", "application/pkcs7-mime"}, + {".p7m", "application/pkcs7-mime"}, + {".p7r", "application/x-pkcs7-certreqresp"}, + {".p7s", "application/pkcs7-signature"}, + {".pbm", "image/x-portable-bitmap"}, + {".pcast", "application/x-podcast"}, + {".pct", "image/pict"}, + {".pcx", "application/octet-stream"}, + {".pcz", "application/octet-stream"}, + {".pdf", "application/pdf"}, + {".pfb", "application/octet-stream"}, + {".pfm", "application/octet-stream"}, + {".pfx", "application/x-pkcs12"}, + {".pgm", "image/x-portable-graymap"}, + {".pic", "image/pict"}, + {".pict", "image/pict"}, + {".pkgdef", "text/plain"}, + {".pkgundef", "text/plain"}, + {".pko", "application/vnd.ms-pki.pko"}, + {".pls", "audio/scpls"}, + {".pma", "application/x-perfmon"}, + {".pmc", "application/x-perfmon"}, + {".pml", "application/x-perfmon"}, + {".pmr", "application/x-perfmon"}, + {".pmw", "application/x-perfmon"}, + {".png", "image/png"}, + {".pnm", "image/x-portable-anymap"}, + {".pnt", "image/x-macpaint"}, + {".pntg", "image/x-macpaint"}, + {".pnz", "image/png"}, + {".pot", "application/vnd.ms-powerpoint"}, + {".potm", "application/vnd.ms-powerpoint.template.macroEnabled.12"}, + {".potx", "application/vnd.openxmlformats-officedocument.presentationml.template"}, + {".ppa", "application/vnd.ms-powerpoint"}, + {".ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12"}, + {".ppm", "image/x-portable-pixmap"}, + {".pps", "application/vnd.ms-powerpoint"}, + {".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12"}, + {".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow"}, + {".ppt", "application/vnd.ms-powerpoint"}, + {".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12"}, + {".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, + {".prf", "application/pics-rules"}, + {".prm", "application/octet-stream"}, + {".prx", "application/octet-stream"}, + {".ps", "application/postscript"}, + {".psc1", "application/PowerShell"}, + {".psd", "application/octet-stream"}, + {".psess", "application/xml"}, + {".psm", "application/octet-stream"}, + {".psp", "application/octet-stream"}, + {".pst", "application/vnd.ms-outlook"}, + {".pub", "application/x-mspublisher"}, + {".pwz", "application/vnd.ms-powerpoint"}, + {".qht", "text/x-html-insertion"}, + {".qhtm", "text/x-html-insertion"}, + {".qt", "video/quicktime"}, + {".qti", "image/x-quicktime"}, + {".qtif", "image/x-quicktime"}, + {".qtl", "application/x-quicktimeplayer"}, + {".qxd", "application/octet-stream"}, + {".ra", "audio/x-pn-realaudio"}, + {".ram", "audio/x-pn-realaudio"}, + {".rar", "application/x-rar-compressed"}, + {".ras", "image/x-cmu-raster"}, + {".rat", "application/rat-file"}, + {".rc", "text/plain"}, + {".rc2", "text/plain"}, + {".rct", "text/plain"}, + {".rdlc", "application/xml"}, + {".reg", "text/plain"}, + {".resx", "application/xml"}, + {".rf", "image/vnd.rn-realflash"}, + {".rgb", "image/x-rgb"}, + {".rgs", "text/plain"}, + {".rm", "application/vnd.rn-realmedia"}, + {".rmi", "audio/mid"}, + {".rmp", "application/vnd.rn-rn_music_package"}, + {".roff", "application/x-troff"}, + {".rpm", "audio/x-pn-realaudio-plugin"}, + {".rqy", "text/x-ms-rqy"}, + {".rtf", "application/rtf"}, + {".rtx", "text/richtext"}, + {".rvt", "application/octet-stream" }, + {".ruleset", "application/xml"}, + {".s", "text/plain"}, + {".safariextz", "application/x-safari-safariextz"}, + {".scd", "application/x-msschedule"}, + {".scr", "text/plain"}, + {".sct", "text/scriptlet"}, + {".sd2", "audio/x-sd2"}, + {".sdp", "application/sdp"}, + {".sea", "application/octet-stream"}, + {".searchConnector-ms", "application/windows-search-connector+xml"}, + {".setpay", "application/set-payment-initiation"}, + {".setreg", "application/set-registration-initiation"}, + {".settings", "application/xml"}, + {".sgimb", "application/x-sgimb"}, + {".sgml", "text/sgml"}, + {".sh", "application/x-sh"}, + {".shar", "application/x-shar"}, + {".shtml", "text/html"}, + {".sit", "application/x-stuffit"}, + {".sitemap", "application/xml"}, + {".skin", "application/xml"}, + {".skp", "application/x-koan" }, + {".sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12"}, + {".sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide"}, + {".slk", "application/vnd.ms-excel"}, + {".sln", "text/plain"}, + {".slupkg-ms", "application/x-ms-license"}, + {".smd", "audio/x-smd"}, + {".smi", "application/octet-stream"}, + {".smx", "audio/x-smd"}, + {".smz", "audio/x-smd"}, + {".snd", "audio/basic"}, + {".snippet", "application/xml"}, + {".snp", "application/octet-stream"}, + {".sol", "text/plain"}, + {".sor", "text/plain"}, + {".spc", "application/x-pkcs7-certificates"}, + {".spl", "application/futuresplash"}, + {".spx", "audio/ogg"}, + {".src", "application/x-wais-source"}, + {".srf", "text/plain"}, + {".SSISDeploymentManifest", "text/xml"}, + {".ssm", "application/streamingmedia"}, + {".sst", "application/vnd.ms-pki.certstore"}, + {".stl", "application/vnd.ms-pki.stl"}, + {".sv4cpio", "application/x-sv4cpio"}, + {".sv4crc", "application/x-sv4crc"}, + {".svc", "application/xml"}, + {".svg", "image/svg+xml"}, + {".swf", "application/x-shockwave-flash"}, + {".step", "application/step"}, + {".stp", "application/step"}, + {".t", "application/x-troff"}, + {".tar", "application/x-tar"}, + {".tcl", "application/x-tcl"}, + {".testrunconfig", "application/xml"}, + {".testsettings", "application/xml"}, + {".tex", "application/x-tex"}, + {".texi", "application/x-texinfo"}, + {".texinfo", "application/x-texinfo"}, + {".tgz", "application/x-compressed"}, + {".thmx", "application/vnd.ms-officetheme"}, + {".thn", "application/octet-stream"}, + {".tif", "image/tiff"}, + {".tiff", "image/tiff"}, + {".tlh", "text/plain"}, + {".tli", "text/plain"}, + {".toc", "application/octet-stream"}, + {".tr", "application/x-troff"}, + {".trm", "application/x-msterminal"}, + {".trx", "application/xml"}, + {".ts", "video/vnd.dlna.mpeg-tts"}, + {".tsv", "text/tab-separated-values"}, + {".ttf", "application/font-sfnt"}, + {".tts", "video/vnd.dlna.mpeg-tts"}, + {".txt", "text/plain"}, + {".u32", "application/octet-stream"}, + {".uls", "text/iuls"}, + {".user", "text/plain"}, + {".ustar", "application/x-ustar"}, + {".vb", "text/plain"}, + {".vbdproj", "text/plain"}, + {".vbk", "video/mpeg"}, + {".vbproj", "text/plain"}, + {".vbs", "text/vbscript"}, + {".vcf", "text/x-vcard"}, + {".vcproj", "application/xml"}, + {".vcs", "text/plain"}, + {".vcxproj", "application/xml"}, + {".vddproj", "text/plain"}, + {".vdp", "text/plain"}, + {".vdproj", "text/plain"}, + {".vdx", "application/vnd.ms-visio.viewer"}, + {".vml", "text/xml"}, + {".vscontent", "application/xml"}, + {".vsct", "text/xml"}, + {".vsd", "application/vnd.visio"}, + {".vsi", "application/ms-vsi"}, + {".vsix", "application/vsix"}, + {".vsixlangpack", "text/xml"}, + {".vsixmanifest", "text/xml"}, + {".vsmdi", "application/xml"}, + {".vspscc", "text/plain"}, + {".vss", "application/vnd.visio"}, + {".vsscc", "text/plain"}, + {".vssettings", "text/xml"}, + {".vssscc", "text/plain"}, + {".vst", "application/vnd.visio"}, + {".vstemplate", "text/xml"}, + {".vsto", "application/x-ms-vsto"}, + {".vsw", "application/vnd.visio"}, + {".vsx", "application/vnd.visio"}, + {".vtt", "text/vtt"}, + {".vtx", "application/vnd.visio"}, + {".wasm", "application/wasm"}, + {".wav", "audio/wav"}, + {".wave", "audio/wav"}, + {".wax", "audio/x-ms-wax"}, + {".wbk", "application/msword"}, + {".wbmp", "image/vnd.wap.wbmp"}, + {".wcm", "application/vnd.ms-works"}, + {".wdb", "application/vnd.ms-works"}, + {".wdp", "image/vnd.ms-photo"}, + {".webarchive", "application/x-safari-webarchive"}, + {".webm", "video/webm"}, + {".webp", "image/webp"}, /* https://en.wikipedia.org/wiki/WebP */ + {".webtest", "application/xml"}, + {".wiq", "application/xml"}, + {".wiz", "application/msword"}, + {".wks", "application/vnd.ms-works"}, + {".WLMP", "application/wlmoviemaker"}, + {".wlpginstall", "application/x-wlpg-detect"}, + {".wlpginstall3", "application/x-wlpg3-detect"}, + {".wm", "video/x-ms-wm"}, + {".wma", "audio/x-ms-wma"}, + {".wmd", "application/x-ms-wmd"}, + {".wmf", "application/x-msmetafile"}, + {".wml", "text/vnd.wap.wml"}, + {".wmlc", "application/vnd.wap.wmlc"}, + {".wmls", "text/vnd.wap.wmlscript"}, + {".wmlsc", "application/vnd.wap.wmlscriptc"}, + {".wmp", "video/x-ms-wmp"}, + {".wmv", "video/x-ms-wmv"}, + {".wmx", "video/x-ms-wmx"}, + {".wmz", "application/x-ms-wmz"}, + {".woff", "application/font-woff"}, + {".woff2", "application/font-woff2"}, + {".wpl", "application/vnd.ms-wpl"}, + {".wps", "application/vnd.ms-works"}, + {".wri", "application/x-mswrite"}, + {".wrl", "x-world/x-vrml"}, + {".wrz", "x-world/x-vrml"}, + {".wsc", "text/scriptlet"}, + {".wsdl", "text/xml"}, + {".wvx", "video/x-ms-wvx"}, + {".x", "application/directx"}, + {".xaf", "x-world/x-vrml"}, + {".xaml", "application/xaml+xml"}, + {".xap", "application/x-silverlight-app"}, + {".xbap", "application/x-ms-xbap"}, + {".xbm", "image/x-xbitmap"}, + {".xdr", "text/plain"}, + {".xht", "application/xhtml+xml"}, + {".xhtml", "application/xhtml+xml"}, + {".xla", "application/vnd.ms-excel"}, + {".xlam", "application/vnd.ms-excel.addin.macroEnabled.12"}, + {".xlc", "application/vnd.ms-excel"}, + {".xld", "application/vnd.ms-excel"}, + {".xlk", "application/vnd.ms-excel"}, + {".xll", "application/vnd.ms-excel"}, + {".xlm", "application/vnd.ms-excel"}, + {".xls", "application/vnd.ms-excel"}, + {".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12"}, + {".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12"}, + {".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, + {".xlt", "application/vnd.ms-excel"}, + {".xltm", "application/vnd.ms-excel.template.macroEnabled.12"}, + {".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template"}, + {".xlw", "application/vnd.ms-excel"}, + {".xml", "text/xml"}, + {".xmp", "application/octet-stream" }, + {".xmta", "application/xml"}, + {".xof", "x-world/x-vrml"}, + {".XOML", "text/plain"}, + {".xpm", "image/x-xpixmap"}, + {".xps", "application/vnd.ms-xpsdocument"}, + {".xrm-ms", "text/xml"}, + {".xsc", "application/xml"}, + {".xsd", "text/xml"}, + {".xsf", "text/xml"}, + {".xsl", "text/xml"}, + {".xslt", "text/xml"}, + {".xsn", "application/octet-stream"}, + {".xss", "application/xml"}, + {".xspf", "application/xspf+xml"}, + {".xtp", "application/octet-stream"}, + {".xwd", "image/x-xwindowdump"}, + {".z", "application/x-compress"}, + {".zip", "application/zip"}, + + {"application/fsharp-script", ".fsx"}, + {"application/msaccess", ".adp"}, + {"application/msword", ".doc"}, + {"application/octet-stream", ".bin"}, + {"application/onenote", ".one"}, + {"application/postscript", ".eps"}, + {"application/step", ".step"}, + {"application/vnd.ms-excel", ".xls"}, + {"application/vnd.ms-powerpoint", ".ppt"}, + {"application/vnd.ms-works", ".wks"}, + {"application/vnd.visio", ".vsd"}, + {"application/x-director", ".dir"}, + {"application/x-shockwave-flash", ".swf"}, + {"application/x-x509-ca-cert", ".cer"}, + {"application/x-zip-compressed", ".zip"}, + {"application/xhtml+xml", ".xhtml"}, + {"application/xml", ".xml"}, // anomoly, .xml -> text/xml, but application/xml -> many thingss, but all are xml, so safest is .xml + {"audio/aac", ".AAC"}, + {"audio/aiff", ".aiff"}, + {"audio/basic", ".snd"}, + {"audio/mid", ".midi"}, + {"audio/wav", ".wav"}, + {"audio/x-m4a", ".m4a"}, + {"audio/x-mpegurl", ".m3u"}, + {"audio/x-pn-realaudio", ".ra"}, + {"audio/x-smd", ".smd"}, + {"image/bmp", ".bmp"}, + {"image/jpeg", ".jpg"}, + {"image/pict", ".pic"}, + {"image/png", ".png"}, //Defined in [RFC-2045], [RFC-2048] + {"image/x-png", ".png"}, //See https://www.w3.org/TR/PNG/#A-Media-type :"It is recommended that implementations also recognize the media type "image/x-png"." + {"image/tiff", ".tiff"}, + {"image/x-macpaint", ".mac"}, + {"image/x-quicktime", ".qti"}, + {"message/rfc822", ".eml"}, + {"text/html", ".html"}, + {"text/plain", ".txt"}, + {"text/scriptlet", ".wsc"}, + {"text/xml", ".xml"}, + {"video/3gpp", ".3gp"}, + {"video/3gpp2", ".3gp2"}, + {"video/mp4", ".mp4"}, + {"video/mpeg", ".mpg"}, + {"video/quicktime", ".mov"}, + {"video/vnd.dlna.mpeg-tts", ".m2t"}, + {"video/x-dv", ".dv"}, + {"video/x-la-asf", ".lsf"}, + {"video/x-ms-asf", ".asf"}, + {"x-world/x-vrml", ".xof"}, + + #endregion + + }; + + var cache = mappings.ToList(); // need ToList() to avoid modifying while still enumerating + + foreach (var mapping in cache) + { + if (!mappings.ContainsKey(mapping.Value)) + { + mappings.Add(mapping.Value, mapping.Key); + } + } + + return mappings; + } + + public static string GetMimeType(string extension) + { + if (extension == null) + { + throw new ArgumentNullException("extension"); + } + + if (!extension.StartsWith(".")) + { + extension = "." + extension; + } + + string mime; + + return _mappings.Value.TryGetValue(extension, out mime) ? mime : "application/octet-stream"; + } + + public static string GetExtension(string mimeType) + { + return GetExtension(mimeType, true); + } + + public static string GetExtension(string mimeType, bool throwErrorIfNotFound) + { + if (mimeType == null) + { + throw new ArgumentNullException("mimeType"); + } + + if (mimeType.StartsWith(".")) + { + throw new ArgumentException("Requested mime type is not valid: " + mimeType); + } + + string extension; + + if (_mappings.Value.TryGetValue(mimeType, out extension)) + { + return extension; + } + if (throwErrorIfNotFound) + { + throw new ArgumentException("Requested mime type is not registered: " + mimeType); + } + else + { + return string.Empty; + } + } + } +} diff --git a/rest/CRUDObjectContainer.RegisteredType.cs b/rest/CRUDObjectContainer.RegisteredType.cs new file mode 100644 index 0000000..3137ecf --- /dev/null +++ b/rest/CRUDObjectContainer.RegisteredType.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; + +namespace ln.http.rest +{ + + public abstract partial class CRUDObjectContainer + { + class RegisteredType + { + public CRUDObjectContainer ObjectContainer { get; } + public Type ObjectType { get; } + + Func _idGetter; + Dictionary objectCache = new Dictionary(); + + public IEnumerable FieldDescriptors => fieldDescriptors; + List fieldDescriptors = new List(); + + public RegisteredType(CRUDObjectContainer objectContainer,Type objectType,Func idGetter) + { + ObjectContainer = objectContainer; + ObjectType = objectType; + _idGetter = idGetter; + } + + public bool TryGetObjectID(object objectInstance,out object objectIdentifier) + { + try + { + objectIdentifier = _idGetter(objectInstance); + } catch (Exception e) + { + objectIdentifier = null; + return false; + } + return true; + } + + public void AddCachedObject(object objectInstance) + { + Type objectType = objectInstance.GetType(); + if (!ObjectType.Equals(objectType)) + throw new ArgumentException(); + + if (!TryGetObjectID(objectInstance, out object objectIdentifier)) + throw new ArgumentOutOfRangeException(); + + objectCache.Add(objectIdentifier, objectInstance); + } + + public void RemoveCachedObject(object objectInstance) + { + Type objectType = objectInstance.GetType(); + if (!ObjectType.Equals(objectType)) + throw new ArgumentException(); + + if (!TryGetObjectID(objectInstance, out object objectIdentifier)) + throw new ArgumentOutOfRangeException(); + + objectCache.Remove(objectIdentifier); + } + + } + } +} + + + + + diff --git a/rest/CRUDObjectContainer.cs b/rest/CRUDObjectContainer.cs new file mode 100644 index 0000000..6b825d2 --- /dev/null +++ b/rest/CRUDObjectContainer.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ln.http.rest +{ + public delegate void CRUDEvent(CRUDObjectContainer sender, CRUDEventArgs args); + public enum CRUDEventType { CREATE, READ, UPDATE, DELETE } + + public class CRUDEventArgs : EventArgs + { + public CRUDEventType EventType { get; } + public Type ObjectType { get; } + public object ObjectIdentity { get; } + public object ObjectInstance { get; } + public FieldValueList FieldValues { get; } + + public CRUDEventArgs(CRUDEventType eventType, Type objectType, object objectIdentity, object objectInstance, FieldValueList fieldValues) + { + EventType = eventType; + ObjectType = objectType; + ObjectIdentity = objectIdentity; + ObjectInstance = objectInstance; + FieldValues = fieldValues; + } + } + + public abstract partial class CRUDObjectContainer + { + public event CRUDEvent OnCRUDEvent; + + + Dictionary registeredTypes = new Dictionary(); + + public CRUDObjectContainer() + { } + + public abstract bool TryGetObjectIdentifier(object objectInstance, out object objectIdentifier); + + public abstract bool TryCreateObject(Type objectType, FieldValueList fieldValues, out object objectInstance); + public abstract bool TryLookupObject(Type objectType, object objectIdentifier, out object objectInstance); + public abstract bool TryUpdateObject(Type objectType, object objectIdentifier, FieldValueList fieldValues); + public abstract bool TryDeleteObject(Type objectType, object objectIdentifier); + + public virtual bool TryUpdateObject(object objectInstance, FieldValueList fieldValues) + { + if (TryGetObjectIdentifier(objectInstance, out object objectIdentifier)) + return TryUpdateObject(objectInstance.GetType(), objectIdentifier, fieldValues); + return false; + } + public virtual bool TryDeleteObject(object objectInstance) + { + if (TryGetObjectIdentifier(objectInstance, out object objectIdentifier)) + return TryDeleteObject(objectInstance.GetType(), objectIdentifier); + return false; + } + + protected void FireCRUDEvent(CRUDEventArgs args) => OnCRUDEvent?.Invoke(this, args); + protected void FireCRUDEVent(CRUDEventType EventType, Type ObjectType, object ObjectIdentity, object ObjectInstance, FieldValueList FieldValues) + => FireCRUDEvent(new CRUDEventArgs(EventType, ObjectType, ObjectIdentity, ObjectInstance, FieldValues)); + + public virtual IEnumerable RegisteredTypes => registeredTypes.Keys; + public virtual bool ContainsRegisteredType(Type objectType) => registeredTypes.ContainsKey(objectType); + + public virtual bool TryRegisterType(Type objectType) => TryRegisterType(objectType, null); + public virtual bool TryRegisterType(Type objectType, Func idGetter) + { + if (ContainsRegisteredType(objectType)) + throw new ArgumentException("objectType already registered", nameof(objectType)); + + RegisteredType registeredType = new RegisteredType(this, objectType, idGetter); + + registeredTypes.Add(objectType, registeredType); + + return true; + } + public virtual bool TryGetFieldDescriptors(Type objectType, out FieldDescriptor[] fieldDescriptors) + { + if (registeredTypes.TryGetValue(objectType, out RegisteredType registeredType)) + { + fieldDescriptors = registeredType.FieldDescriptors.ToArray(); + return true; + } + fieldDescriptors = null; + return false; + } + + public virtual IEnumerable EnumerateObjectInstances(Type objectType) => EnumerateObjectInstances(objectType, null); + public abstract IEnumerable EnumerateObjectInstances(Type objectType, FieldValueList conditionalFieldValues); + + public abstract bool AddForeignObjectInstance(object objectInstance); + } +} + + + + + diff --git a/rest/FieldDescriptor.cs b/rest/FieldDescriptor.cs new file mode 100644 index 0000000..6ddd6d8 --- /dev/null +++ b/rest/FieldDescriptor.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; + +namespace ln.http.rest +{ + public class FieldDescriptor + { + Action setter; + Func getter; + + public Type OwningType { get; } + + public string FieldName { get; } + public Type FieldType { get; } + + public bool CanWrite { get; } + + public FieldDescriptor(FieldInfo fieldInfo) + { + OwningType = fieldInfo.DeclaringType; + FieldName = fieldInfo.Name; + FieldType = fieldInfo.FieldType; + + CanWrite = !fieldInfo.IsInitOnly; + + getter = fieldInfo.GetValue; + setter = fieldInfo.SetValue; + } + + public FieldDescriptor(PropertyInfo propertyInfo) + { + OwningType = propertyInfo.DeclaringType; + FieldName = propertyInfo.Name; + FieldType = propertyInfo.PropertyType; + + CanWrite = propertyInfo.CanWrite; + + getter = propertyInfo.GetValue; + setter = propertyInfo.SetValue; + } + + public void SetFieldValue(object objectInstance, object fieldValue) => setter(objectInstance, fieldValue); + public object GetFieldValue(object objectInstance) => getter(objectInstance); + } +} diff --git a/rest/FieldValueList.cs b/rest/FieldValueList.cs new file mode 100644 index 0000000..71cf1ab --- /dev/null +++ b/rest/FieldValueList.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace ln.http.rest +{ + public class FieldValueList + { + public Type ObjectType { get; } + + Dictionary fieldValues = new Dictionary(); + + public FieldValueList(Type objectType) + { + ObjectType = objectType; + } + + public IEnumerable FieldNames => fieldValues.Keys; + public object GetFieldValue(string fieldName) => fieldValues[fieldName]; + public void SetFieldValue(string fieldName, object fieldValue) => fieldValues[fieldName] = fieldValue; + public void RemoveFieldValue(string fieldName) => fieldValues.Remove(fieldName); + + } +} diff --git a/rest/RestAPIAdapter.cs b/rest/RestAPIAdapter.cs new file mode 100644 index 0000000..49f05af --- /dev/null +++ b/rest/RestAPIAdapter.cs @@ -0,0 +1,22 @@ +using ln.http.router; +using System; +using System.Collections.Generic; +using System.Text; + +namespace ln.http.rest +{ + public class RestAPIAdapter : IHttpRouter + { + public CRUDObjectContainer ObjectContainer { get; } + + public RestAPIAdapter(CRUDObjectContainer objectContainer) + { + ObjectContainer = objectContainer; + } + + public HttpResponse Route(HttpRoutingContext routingContext, HttpRequest httpRequest) + { + throw new NotImplementedException(); + } + } +} diff --git a/router/FileRouter.cs b/router/FileRouter.cs new file mode 100644 index 0000000..135ad8d --- /dev/null +++ b/router/FileRouter.cs @@ -0,0 +1,35 @@ +// /** +// * File: FileSystemRouter.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System.IO; +using ln.http.message; +namespace ln.http.router +{ + public class FileRouter : IHttpRouter + { + public string FileName { get; set; } + + public FileRouter(string filename) + { + if (!File.Exists(filename)) + throw new FileNotFoundException(); + + FileName = filename; + } + + public HttpResponse Route(HttpRoutingContext routingContext, HttpRequest httpRequest) + { + HttpResponse httpResponse = new HttpResponse(httpRequest, new FileStream(FileName, FileMode.Open)); + httpResponse.SetHeader("content-type", MimeTypeMap.GetMimeType(Path.GetExtension(FileName))); + + return httpResponse; + } + } + +} diff --git a/router/HttpRoutingContext.cs b/router/HttpRoutingContext.cs new file mode 100644 index 0000000..3250104 --- /dev/null +++ b/router/HttpRoutingContext.cs @@ -0,0 +1,53 @@ +using System; +namespace ln.http.router +{ + public class HttpRoutingContext + { + public HttpRequest HttpRequest { get; } + + public string Path { get; set; } + public string RoutedPath { get; set; } + + public HttpRoutingContext(HttpRequest httpRequest) : this(httpRequest, httpRequest.URI.AbsolutePath) { } + public HttpRoutingContext(HttpRequest httpRequest, string path) + { + HttpRequest = httpRequest; + Path = path; + RoutedPath = ""; + } + HttpRoutingContext(HttpRequest httpRequest,string path,string routedPath) + { + HttpRequest = httpRequest; + Path = path; + RoutedPath = routedPath; + } + + public HttpRoutingContext Routed(string residual) + { + return new HttpRoutingContext(HttpRequest, residual, RoutedPath + Path.Substring(0,Path.Length - residual.Length)); + } + + public bool PopNext(out string next, out HttpRoutingContext nextRoutingContext) + { + next = Path; + + if (String.Empty.Equals(next) || "/".Equals(next)) + { + nextRoutingContext = null; + return false; + } + + int indSlash = next.IndexOf('/',1); + if (indSlash > 0) + next = next.Substring(0, indSlash-1); + + nextRoutingContext = Routed(Path.Substring(next.Length)); + + if (next[0] == '/') + next = next.Substring(1); + + return true; + } + + } +} diff --git a/router/LoggingRouter.cs b/router/LoggingRouter.cs new file mode 100644 index 0000000..a8e13ea --- /dev/null +++ b/router/LoggingRouter.cs @@ -0,0 +1,59 @@ +// /** +// * File: LoggingRouter.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using ln.logging; +using System.Diagnostics; +namespace ln.http.router +{ + public class LoggingRouter : IHttpRouter + { + IHttpRouter Next { get; } + Logger Logger { get; } + + public LoggingRouter(IHttpRouter next) + :this(next, Logger.Default) + {} + public LoggingRouter(IHttpRouter next,Logger logger) + { + Next = next; + Logger = logger; + } + + public HttpResponse Route(HttpRoutingContext routingContext, HttpRequest httpRequest) + { + DateTime start = DateTime.Now; + HttpResponse response = null; + + try + { + response = Next.Route(routingContext, httpRequest); + } + catch (Exception e) + { + throw; + } + finally + { + DateTime end = DateTime.Now; + TimeSpan duration = end - start; + + Logger.Log(LogLevel.INFO, "{0} {1} {2} {3} {4}", + start, + duration, + response?.StatusCode.ToString() ?? "-", + httpRequest.Method, + httpRequest.RequestURL + ); + } + + return response; + } + } +} diff --git a/router/RouterTarget.cs b/router/RouterTarget.cs new file mode 100644 index 0000000..1622a09 --- /dev/null +++ b/router/RouterTarget.cs @@ -0,0 +1,61 @@ +using System; +using ln.http.exceptions; +namespace ln.http.router +{ + public class RouterTarget :IHttpRouter + { + public Func Target { get; } + + public RouterTarget(Func target) + { + Target = (HttpRoutingContext routingContext, HttpRequest httpRequest) => target(routingContext.Path, httpRequest); + } + public RouterTarget(Func target) + { + Target = target; + } + public RouterTarget(Func target) + { + Target = (context,request) => target(request); + } + protected RouterTarget() + { + Target = (path,request) => Dispatch(request); + } + + public virtual HttpResponse Dispatch(HttpRequest request) + { + switch (request.Method.ToUpper()) + { + case "HEAD": + return HEAD(request); + case "GET": + return GET(request); + case "PROPFIND": + return PROPFIND(request); + case "POST": + return POST(request); + case "PUT": + return PUT(request); + case "DELETE": + return DELETE(request); + default: + throw new MethodNotAllowedException(); + } + } + + public HttpResponse Route(HttpRoutingContext routingContext, HttpRequest httpRequest) + { + return Target(routingContext, httpRequest); + } + + public virtual HttpResponse HEAD(HttpRequest request) => throw new MethodNotAllowedException(); + public virtual HttpResponse GET(HttpRequest request) => throw new MethodNotAllowedException(); + public virtual HttpResponse PROPFIND(HttpRequest request) => throw new MethodNotAllowedException(); + public virtual HttpResponse POST(HttpRequest request) => throw new MethodNotAllowedException(); + public virtual HttpResponse PUT(HttpRequest request) => throw new MethodNotAllowedException(); + public virtual HttpResponse DELETE(HttpRequest request) => throw new MethodNotAllowedException(); + + + } +} diff --git a/router/SimpleRouter.cs b/router/SimpleRouter.cs new file mode 100644 index 0000000..127a2a8 --- /dev/null +++ b/router/SimpleRouter.cs @@ -0,0 +1,97 @@ +using System; +using System.Text.RegularExpressions; +using System.Collections.Generic; +using System.Text; +using System.Linq; +namespace ln.http.router +{ + public class SimpleRouter : IHttpRouter + { + List routes = new List(); + public SimpleRoute[] Routes => routes.ToArray(); + + public SimpleRouter() + { + } + + public void AddSimpleRoute(string simpleRoute, Func target) => AddSimpleRoute(simpleRoute, new RouterTarget(target)); + public void AddSimpleRoute(string simpleRoute, Func target) => AddSimpleRoute(simpleRoute, new RouterTarget(target)); + public void AddSimpleRoute(string simpleRoute, Func target) => AddSimpleRoute(simpleRoute, new RouterTarget(target)); + public void AddSimpleRoute(string simpleRoute, IHttpRouter target) => AddSimpleRoute(simpleRoute, target, simpleRoute.Split('/').Length); + public void AddSimpleRoute(string simpleRoute, IHttpRouter target, int priority) + { + string[] parts = simpleRoute.Split(new char[] { '/' }); + string[] reparts = parts.Select((part) => + { + if (part.StartsWith(":", StringComparison.InvariantCulture)) + if (part.EndsWith("*", StringComparison.InvariantCulture)) + return string.Format("(?<{0}>[^/]+)(?<_>/.*)?", part.Substring(1, part.Length - 2)); + else + return string.Format("(?<{0}>[^/]+)", part.Substring(1)); + else if (part.Equals("*")) + return string.Format("(?<_>.*)"); + else + return string.Format("{0}", part); + }).ToArray(); + + string reroute = string.Join("/", reparts); + + AddRoute(reroute, target, priority); + } + + public void AddRoute(String route, IHttpRouter target) => AddRoute(route, target, 0); + public void AddRoute(String route, IHttpRouter target,int priority) + { + lock (this) + { + routes.Add(new SimpleRoute(route, target, priority)); + routes.Sort((SimpleRoute a, SimpleRoute b) => b.Priority - a.Priority); + } + } + public void Remove(SimpleRoute simpleRoute) => routes.Remove(simpleRoute); + + public HttpResponse Route(HttpRoutingContext routingContext, HttpRequest httpRequest) + { + foreach (SimpleRoute simpleRoute in routes.ToArray()) + { + Match match = simpleRoute.Route.Match(routingContext.Path); + if (match.Success) + { + string residual = ""; + + foreach (Group group in match.Groups) + { + httpRequest?.SetParameter(group.Name, group.Value); + if (group.Name.Equals("_")) + if (group.Value.StartsWith("/", StringComparison.InvariantCulture)) + residual = group.Value; + else + residual = "/" + group.Value; + } + + HttpResponse response = simpleRoute.Target.Route(routingContext.Routed(residual), httpRequest); + if (response != null) + return response; + } + } + return null; + } + + + public class SimpleRoute + { + public int Priority { get; } + + public Regex Route { get; } + public IHttpRouter Target { get; } + + public SimpleRoute(string regex, IHttpRouter target) : this(regex, target, 0) { } + public SimpleRoute(string regex, IHttpRouter target,int priority) + { + Route = new Regex(regex); + Target = target; + Priority = priority; + } + } + } +} diff --git a/router/StaticRouter.cs b/router/StaticRouter.cs new file mode 100644 index 0000000..d2b1b4a --- /dev/null +++ b/router/StaticRouter.cs @@ -0,0 +1,67 @@ +// /** +// * File: FileSystemRouter.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.IO; +using ln.http.message; +using System.Collections.Generic; +namespace ln.http.router +{ + public class StaticRouter : IHttpRouter + { + public String RootPath { get; } + + List indexNames = new List(); + public String[] IndexNames => indexNames.ToArray(); + + public StaticRouter(string path) + { + if (!Directory.Exists(path)) + throw new FileNotFoundException(); + + RootPath = Path.GetFullPath(path); + + AddIndex("index.html"); + AddIndex("index.htm"); + } + + public void AddIndex(string indexName) => indexNames.Add(indexName); + public void RemoveIndex(string indexName) => indexNames.Remove(indexName); + + public HttpResponse Route(HttpRoutingContext routingContext, HttpRequest httpRequest) + { + string finalPath = Path.Combine(RootPath, routingContext.Path.Substring(1)); + + if (Directory.Exists(finalPath)) + { + foreach (string indexName in indexNames) + { + string indexFileName = Path.Combine(finalPath, indexName); + if (File.Exists(indexFileName)) + { + finalPath = indexFileName; + break; + } + } + } + + if (File.Exists(finalPath)) + { + lock (this) + { + HttpResponse httpResponse = new HttpResponse(httpRequest, new FileStream(finalPath, FileMode.Open)); + httpResponse.SetHeader("content-type", MimeTypeMap.GetMimeType(Path.GetExtension(finalPath))); + return httpResponse; + } + } + return null; + } + } + +} diff --git a/router/VirtualHostRouter.cs b/router/VirtualHostRouter.cs new file mode 100644 index 0000000..127f69a --- /dev/null +++ b/router/VirtualHostRouter.cs @@ -0,0 +1,48 @@ +using ln.http.exceptions; +using System.Collections.Generic; + +namespace ln.http.router +{ + public class VirtualHostRouter : IHttpRouter + { + public IHttpRouter DefaultRoute { get; set; } + + Dictionary virtualHosts = new Dictionary(); + + public VirtualHostRouter() + { + } + public VirtualHostRouter(IHttpRouter defaultRoute) + { + DefaultRoute = defaultRoute; + } + public VirtualHostRouter(IEnumerable> routes) + { + foreach (KeyValuePair route in routes) + virtualHosts.Add(route.Key, route.Value); + } + public VirtualHostRouter(IHttpRouter defaultRoute,IEnumerable> routes) + :this(routes) + { + DefaultRoute = defaultRoute; + } + public VirtualHostRouter(VirtualHostRouter source) + : this(source.virtualHosts) { } + + public void Add(string hostname,IHttpRouter router) => virtualHosts.Add(hostname, router); + public void Remove(string hostname) => virtualHosts.Remove(hostname); + public bool Contains(string hostname) => virtualHosts.ContainsKey(hostname); + + public bool TryGetValue(string hostname, out IHttpRouter router) => virtualHosts.TryGetValue(hostname, out router); + + public HttpResponse Route(HttpRoutingContext routingContext, HttpRequest httpRequest) + { + if (virtualHosts.TryGetValue(httpRequest.Hostname, out IHttpRouter virtualHost)) + return virtualHost.Route(routingContext, httpRequest); + if (DefaultRoute != null) + return DefaultRoute.Route(routingContext, httpRequest); + + throw new HttpException(410, string.Format("Gone. Hostname {0} not found on this server.", httpRequest.Hostname)); + } + } +} diff --git a/router/WebsocketRouter.cs b/router/WebsocketRouter.cs new file mode 100644 index 0000000..5b3015b --- /dev/null +++ b/router/WebsocketRouter.cs @@ -0,0 +1,32 @@ +using System; +using ln.http.websocket; +using ln.http.exceptions; +using ln.logging; +namespace ln.http.router +{ + public class WebsocketRouter : IHttpRouter + { + Func createWebsocket; + + public WebsocketRouter(Func createWebsocketDelegate) + { + createWebsocket = createWebsocketDelegate; + } + + public WebSocket CreateWebSocket(HttpRequest request) => createWebsocket(request); + + public HttpResponse Route(HttpRoutingContext routingContext, HttpRequest httpRequest) + { + WebSocket websocket = CreateWebSocket(httpRequest); + try + { + websocket.Run(); + } + catch (Exception e) + { + Logging.Log(e); + } + throw new DisposeConnectionException(); + } + } +} diff --git a/session/Session.cs b/session/Session.cs new file mode 100644 index 0000000..90bdfe1 --- /dev/null +++ b/session/Session.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ln.http.session +{ + public class Session : IDisposable + { + public Guid SessionID { get; private set; } + public long SessionAge => DateTimeOffset.Now.ToUnixTimeSeconds() - LastTouch; + public long LastTouch { get; private set; } + + public HttpUser CurrentUser { get; set; } + + + private readonly Dictionary elements = new Dictionary(); + + public Session() + { + SessionID = Guid.NewGuid(); + CurrentUser = new HttpUser(); + } + + public object this[string name] + { + get => this.elements[name]; + set => this.elements[name] = value; + } + + public T Get(string name) where T:class + { + if (elements.ContainsKey(name)) + return elements[name] as T; + return null; + } + + public String[] ElementNames => this.elements.Keys.ToArray(); + + public void Touch() + { + LastTouch = DateTimeOffset.Now.ToUnixTimeSeconds(); + } + + public virtual void Authenticate(HttpRequest httpRequest) + { + + } + + public void Dispose() + { + foreach (object o in elements.Values) + { +// if (o is IDisposable disposable) +// { +// try +// { +// disposable.Dispose(); +// } +//#pragma warning disable RECS0022 // catch-Klausel, die System.Exception abfängt und keinen Text aufweist +// catch (Exception) +//#pragma warning restore RECS0022 // catch-Klausel, die System.Exception abfängt und keinen Text aufweist + // { + + // } + //} + } + } + } +} diff --git a/session/SessionCache.cs b/session/SessionCache.cs new file mode 100644 index 0000000..79cdca6 --- /dev/null +++ b/session/SessionCache.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +namespace ln.http.session +{ + public class SessionCache + { + Dictionary sessions = new Dictionary(); + + public SessionCache() + { + } + + public bool Contains(Guid sessionID) + { + return this.sessions.ContainsKey(sessionID); + } + + /* + * Create a new Session instance + * + */ + public virtual Session CreateSession() + { + return new Session(); + } + + /* + * Find and return SessionID from given HttpRequest + * + */ + public virtual Guid FindSessionID(HttpRequest httpRequest) + { + if (httpRequest.ContainsCookie("SID_LN")) + { + return Guid.Parse(httpRequest.GetCookie("SID_LN")); + } + return Guid.Empty; + } + + /* + * Apply SessionID of the given Session to the given Response + * + */ + public virtual void ApplySessionID(HttpResponse httpResponse,Session session) + { + HttpCookie sessionCookie = new HttpCookie("SID_LN",session.SessionID.ToString()); + sessionCookie.Path = "/"; + httpResponse.AddCookie(sessionCookie); + } + + public Session GetSession(HttpRequest httpRequest) + { + Guid sessionID = FindSessionID(httpRequest); + if (!Guid.Empty.Equals(sessionID) && Contains(sessionID)) + { + lock (sessions) + { + Session session = this.sessions[sessionID]; + session.Touch(); + return session; + } + } + else + { + Session session = CreateSession(); + lock (this.sessions) + { + this.sessions.Add(session.SessionID, session); + } + return session; + } + } + + } +} diff --git a/websocket/WebSocket.cs b/websocket/WebSocket.cs new file mode 100644 index 0000000..e570e2a --- /dev/null +++ b/websocket/WebSocket.cs @@ -0,0 +1,201 @@ +using System; +using System.IO; +using System.Threading; +using ln.logging; +using ln.http.exceptions; +using System.Security.Cryptography; +using System.Text; +using ln.type; +using ln.http.connections; + +namespace ln.http.websocket +{ + public enum WebSocketOpcode : int + { + CONTINUATION = 0x00, + TEXT = 0x01, + BINARY = 0x02, + CLOSE = 0x08, + PING = 0x09, + PONG = 0x0A, + + INVALIDOPCODE = -1 + } + + public enum WebSocketState + { + HANDSHAKE, + OPEN, + CLOSING, + CLOSED, + ERROR + } + + public delegate void WebSocketEventDelegate(WebSocket sender,WebSocketEventArgs e); + + public abstract class WebSocket + { + public HTTPServer HTTPServer => HttpRequest.HTTPServer; + public HttpRequest HttpRequest { get; } + public Stream Stream { get; } + + public WebSocketState State { get; private set; } = WebSocketState.HANDSHAKE; + + public WebSocket(HttpRequest httpRequest) + { + HttpRequest = httpRequest; + Stream = httpRequest.GetConnectionStream(); + + if ((!httpRequest.GetRequestHeader("upgrade", "").Contains("websocket")) && (!httpRequest.GetRequestHeader("connection", "").Contains("Upgrade"))) + throw new HttpException(400, "This resource is a websocket endpoint only"); + + if (!httpRequest.GetRequestHeader("Sec-WebSocket-Version", "").Equals("13")) + throw new HttpException(400, "Unsupported Protocol Version (WebSocket)"); + + String wsKey = httpRequest.GetRequestHeader("Sec-WebSocket-Key"); + + HttpResponse httpResponse = new HttpResponse(httpRequest); + httpResponse.StatusCode = 101; + httpResponse.StatusMessage = "Switching Protocols"; + httpResponse.AddHeader("upgrade", "websocket"); + httpResponse.AddHeader("connection", "Upgrade"); + httpResponse.AddHeader("Sec-WebSocket-Version", "13"); + + string acceptKey = String.Format("{0}258EAFA5-E914-47DA-95CA-C5AB0DC85B11", httpRequest.GetRequestHeader("Sec-WebSocket-Key")); + + httpResponse.AddHeader( + "Sec-Websocket-Accept", + Convert.ToBase64String(SHA1.Create().ComputeHash(Encoding.ASCII.GetBytes(acceptKey))) + ); + + Connection.SendResponse(Stream, httpResponse); + //HTTPServerConnection.Current.Value.AbortRequested += (connection) => Close(); + State = WebSocketState.OPEN; + } + + public bool IsAlive => false; + + public void Close() + { + switch (State) + { + case WebSocketState.HANDSHAKE: + case WebSocketState.ERROR: + case WebSocketState.CLOSING: + State = WebSocketState.CLOSED; + Stream.Close(); + break; + case WebSocketState.CLOSED: + break; + case WebSocketState.OPEN: + WebSocketFrame closeFrame = new WebSocketFrame(WebSocketOpcode.CLOSE); + lock (Stream) + { + Send(closeFrame); + State = WebSocketState.CLOSING; + } + break; + } + } + + public void Run() + { + try + { + while (State != WebSocketState.CLOSED) + { + WebSocketFrame webSocketFrame = new WebSocketFrame(Stream); + switch (webSocketFrame.Opcode) + { + case WebSocketOpcode.TEXT: + Received(Encoding.UTF8.GetString(webSocketFrame.ApplicationData)); + break; + case WebSocketOpcode.BINARY: + Received(webSocketFrame.ApplicationData); + break; + case WebSocketOpcode.CLOSE: + if (State == WebSocketState.OPEN) + { + WebSocketFrame closeFrame = new WebSocketFrame(WebSocketOpcode.CLOSE); + closeFrame.FIN = true; + lock (Stream) + { + Send(closeFrame); + State = WebSocketState.CLOSING; + } + } + + State = WebSocketState.CLOSED; + Stream.Close(); + break; + case WebSocketOpcode.PING: + WebSocketFrame pong = new WebSocketFrame(WebSocketOpcode.PONG); + pong.ApplicationData = webSocketFrame.ApplicationData; + Send(pong); + break; + } + } + } catch (IOException) + { + State = WebSocketState.ERROR; + Logging.Log(LogLevel.DEBUG, "WebSocket connection was dropped unexpected"); + Close(); + } catch (Exception e) + { + Logging.Log(LogLevel.ERROR, "WebSocket caught Exception: {0}", e.ToString()); + Logging.Log(e); + } finally + { + } + } + + public virtual bool Received(string textMessage) + { + Logging.Log(LogLevel.WARNING, "WebSocket received unexpected text message:\n{0}", textMessage); + return false; + } + public virtual bool Received(byte[] binaryMessage) + { + Logging.Log(LogLevel.WARNING, "WebSocket received unexpected binary message:\n{0}",binaryMessage.ToHexString()); + return false; + } + + public void Send(WebSocketFrame frame) + { + lock (Stream) + { + if (State == WebSocketState.OPEN) + { + try + { + frame.WriteTo(Stream); + } catch (IOException) + { + if (State != WebSocketState.ERROR) + { + Logging.Log(LogLevel.ERROR, "WebSocket.Send(): Websocket connection was dropped unexpected"); + State = WebSocketState.ERROR; + Close(); + } + } + } + else + throw new IOException("WebSocket is not open"); + } + } + + public void Send(string textMessage) + { + WebSocketFrame webSocketFrame = new WebSocketFrame(textMessage); + Send(webSocketFrame); + } + public void Send(byte[] binaryMessage) + { + WebSocketFrame webSocketFrame = new WebSocketFrame(binaryMessage); + Send(webSocketFrame); + } + + + + } +} diff --git a/websocket/WebSocketEventArgs.cs b/websocket/WebSocketEventArgs.cs new file mode 100644 index 0000000..8cd452c --- /dev/null +++ b/websocket/WebSocketEventArgs.cs @@ -0,0 +1,36 @@ +using System; +using System.Text; +namespace ln.http.websocket +{ + public enum WebSocketEventType { OPEN, CLOSE, MESSAGE, ERROR } + + public class WebSocketEventArgs + { + public WebSocketFrame Frame { get; } + + public WebSocketEventType EventType { get; } + public byte[] BinaryMessage { get; } + public String TextMessage => Encoding.UTF8.GetString(BinaryMessage); + + public bool IsBinary { get; } + + public String ErrorMessage { get; } + + public WebSocketEventArgs(WebSocketFrame frame) + { + Frame = frame; + + switch (frame.Opcode) + { + case WebSocketOpcode.BINARY: + case WebSocketOpcode.TEXT: + IsBinary = (frame.Opcode == WebSocketOpcode.BINARY); + EventType = WebSocketEventType.MESSAGE; + BinaryMessage = frame.ApplicationData; + ErrorMessage = null; + + break; + } + } + } +} diff --git a/websocket/WebSocketFrame.cs b/websocket/WebSocketFrame.cs new file mode 100644 index 0000000..d72fc3a --- /dev/null +++ b/websocket/WebSocketFrame.cs @@ -0,0 +1,171 @@ +using System; +using System.IO; +using System.Text; +using ln.type; +using ln.logging; +namespace ln.http.websocket +{ + public class WebSocketFrame + { + public bool FIN; + public bool RSV1; + public bool RSV2; + public bool RSV3; + + public WebSocketOpcode Opcode = WebSocketOpcode.INVALIDOPCODE; + + public bool Mask; + + public int MaskingKey; + + public byte[] ExtensionData; + public byte[] ApplicationData; + + public byte[] Payload => ExtensionData.Concat(ApplicationData); + + public WebSocketFrame() + { + ExtensionData = new byte[0]; + ApplicationData = new byte[0]; + } + + public WebSocketFrame(WebSocketOpcode opcode) + :this(opcode,new byte[0]) + { + } + public WebSocketFrame(WebSocketOpcode opcode,byte[] applicationData) + { + Opcode = opcode; + ExtensionData = new byte[0]; + ApplicationData = applicationData; + } + + public WebSocketFrame(string applicationData) + :this(Encoding.UTF8.GetBytes(applicationData),new byte[0]) + { + Opcode = WebSocketOpcode.TEXT; + } + public WebSocketFrame(byte[] applicationData) + : this(applicationData, new byte[0]) { } + public WebSocketFrame(byte[] applicationData,byte[] extensionData) + { + FIN = true; + Opcode = WebSocketOpcode.BINARY; + ExtensionData = extensionData; + ApplicationData = applicationData; + } + + public WebSocketFrame(Stream stream) + { + ReadFrom(stream); + } + + public void ReadFrom(Stream stream) + { + int firstByte = stream.ReadByte(); + if (firstByte == -1) + throw new IOException(); + + FIN = (firstByte & 0x80) != 0; + RSV1 = (firstByte & 0x40) != 0; + RSV3 = (firstByte & 0x20) != 0; + RSV1 = (firstByte & 0x10) != 0; + + Opcode = (WebSocketOpcode)(firstByte & 0x0F); + + int secondByte = stream.ReadByte(); + if (secondByte == -1) + throw new IOException(); + + Mask = (secondByte & 0x80) != 0; + + int pLength = (secondByte) & 0x7F; + if (pLength == 126) + { + pLength = stream.ReadUShort(true); + } + else if (pLength == 127) + { + ulong ulpLength = stream.ReadULong(true); + if (ulpLength > int.MaxValue) + throw new NotSupportedException(String.Format("Maximum supported frame size is: {0} bytes", int.MaxValue)); + + pLength = (int)ulpLength; + } + + if (Mask) + { + MaskingKey = stream.ReadInteger(); + } + + ExtensionData = new byte[0]; + ApplicationData = stream.ReadBytes(pLength); + + if (Mask) + { + MaskPayload(ApplicationData, MaskingKey); + } + } + + public void WriteTo(Stream stream) + { + stream.WriteByte( + (byte)( + (FIN ? 0x80 : 0x00) | + (RSV1 ? 0x40 : 0x00) | + (RSV2 ? 0x20 : 0x00) | + (RSV3 ? 0x10 : 0x00) | + (((int)Opcode) & 0x0F) + ) + ); + + int dLength = ExtensionData.Length + ApplicationData.Length; + int pLength = 0; + + if (dLength < 126) + { + pLength = (dLength) | (Mask ? 0x80 : 0x00); + + stream.WriteByte((byte)pLength); + + } else if (dLength < (1U<<16)) + { + pLength = (126) | (Mask ? 0x80 : 0x00); + + stream.WriteByte((byte)pLength); + stream.WriteBytes(((ushort)dLength).GetBytes(true)); + } + else + { + pLength = (127) | (Mask ? 0x80 : 0x00); + + stream.WriteByte((byte)pLength); + stream.WriteBytes(((ulong)dLength).GetBytes(true)); + } + + byte[] payload = Payload; + + if (Mask) + { + stream.WriteBytes(MaskingKey.GetBytes()); + MaskPayload(payload, MaskingKey); + } + + stream.WriteBytes(payload); + + stream.Flush(); + } + + public static void MaskPayload(byte[] data,int maskingKey) + { + byte[] mk = BitConverter.GetBytes(maskingKey); + for (int n = 0; n < data.Length; n++) + { + data[n] = (byte)(data[n] ^ mk[n % 4]); + } + } + + + static Random random = new Random(); + } +}