commit 259c1275a5844935e5edb0a0067986b95100011d Author: U-WALDRENNACH\haraldwolff Date: Tue Nov 17 23:46:07 2020 +0100 Initial Commit 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(); + } +}