diff --git a/ln.http/CertificateStore.cs b/ln.http/CertificateStore.cs new file mode 100644 index 0000000..f57b632 --- /dev/null +++ b/ln.http/CertificateStore.cs @@ -0,0 +1,19 @@ +using System.Security.Cryptography.X509Certificates; +using ln.collections; + +namespace ln.http; + +public class CertificateStore +{ + private Cache _cache = new Cache(); + + public CertificateStore() + { + } + + public void AddCertificate(X509Certificate certificate) => _cache.Add(certificate.Subject, certificate); + public bool TryGetCertificate(string hostname, out X509Certificate certificate) => + _cache.TryGetValue(hostname, out certificate); + + public void RemoveCertificate(string hostname) => _cache.Remove(hostname); +} \ No newline at end of file diff --git a/ln.http/FileRouter.cs b/ln.http/FileRouter.cs new file mode 100644 index 0000000..b5dbcf4 --- /dev/null +++ b/ln.http/FileRouter.cs @@ -0,0 +1,55 @@ +// /** +// * 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.mime; +using ln.http.route; +using ln.mime; + +namespace ln.http +{ + public class FileRouter : HttpRoute + { + public string FileName { get; } + public string ContentType { get; } + + public FileRouter(string filename) + :this(null, filename) + { + } + + public FileRouter(string filename, MimeTypes mimeTypes) + :this(null, filename) + { + ContentType = mimeTypes.GetMimeTypeByExtension(Path.GetExtension(FileName)).ToString(); + } + public FileRouter(string filename, string contentType) + :base(HttpMethod.GET, null) + { + ContentType = contentType; + + if (!File.Exists(filename)) + throw new FileNotFoundException(); + + FileName = filename; + _routerDelegate = RouteToFile; + } + + public bool RouteToFile(HttpRequestContext requestContext, string routePath) + { + requestContext.Response = + HttpResponse + .OK() + .Content(new FileStream(FileName, FileMode.Open, FileAccess.Read)) + .ContentType(ContentType); + return true; + } + } + +} diff --git a/ln.http/FileSystemRouter.cs b/ln.http/FileSystemRouter.cs index 4a05b34..ec89626 100644 --- a/ln.http/FileSystemRouter.cs +++ b/ln.http/FileSystemRouter.cs @@ -10,11 +10,12 @@ using System; using System.IO; using System.Collections.Generic; -using ln.http.mime; +using ln.http.route; +using ln.mime; namespace ln.http { - public class FileSystemRouter : IDisposable + public class FileSystemRouter : HttpRoute { private string _rootPath; public String RootPath @@ -28,75 +29,89 @@ namespace ln.http List indexNames = new List(); public String[] IndexNames => indexNames.ToArray(); - - private HttpServer _httpServer; + private HttpRouter _parentRouter; - private HttpRouter.HttpMapping _parentMapping; + private MimeTypes _mimeTypes; - public FileSystemRouter(HttpServer httpServer, string path) + public FileSystemRouter(HttpRouter parentRouter, string mapPath, MimeTypes mimeTypes, string fileSystemPath) + :base(HttpMethod.GET, mapPath) { - _httpServer = httpServer; - httpServer?.AddRouter(this.Route); + _parentRouter = parentRouter; + _parentRouter?.Map(this); + + _mimeTypes = mimeTypes; - if (!Directory.Exists(path)) + if (!Directory.Exists(fileSystemPath)) throw new FileNotFoundException(); - RootPath = path; + RootPath = fileSystemPath; Console.Error.WriteLine("FileSystemRouter created ({0})", RootPath); AddIndex("index.html"); AddIndex("index.htm"); + + _routerDelegate = RouteToFileSystem; } - public FileSystemRouter(string path) - :this(null, path) + + public FileSystemRouter(string mapPath, MimeTypes mimeTypes, string fileSystemPath) + :this(null, mapPath, mimeTypes, fileSystemPath){} + + public FileSystemRouter(string mapPath, string fileSystemPath) + :this(null, mapPath, null, fileSystemPath){} + + public FileSystemRouter(MimeTypes mimeTypes, string fileSystemPath) + :this(null, null, mimeTypes, fileSystemPath) { } - - public FileSystemRouter(HttpRouter parentRouter, string mappingPath, string path) - :this(null, path) + public FileSystemRouter(string fileSystemPath) + :this(null, null, null, fileSystemPath) { - _parentRouter = parentRouter; - _parentMapping = _parentRouter.Map(HttpMethod.ANY, mappingPath, this.Route); } - + public void AddIndex(string indexName) => indexNames.Add(indexName); public void RemoveIndex(string indexName) => indexNames.Remove(indexName); - public bool Route(HttpContext httpContext) + public bool RouteToFileSystem(HttpRequestContext requestContext, string routePath) { - string finalPath = httpContext.RoutableUri.Length > 0 ? Path.Combine(RootPath, httpContext.RoutableUri.Substring(1)) : "."; + string filename = ChooseFile(routePath); + if (filename is null) + return false; + + requestContext.Response = + HttpResponse + .OK() + .Content(new FileStream(filename, FileMode.Open, FileAccess.Read)); + + if (_mimeTypes?.TryGetMimeTypeByExtension(Path.GetExtension(filename), out MimeType mimeType) ?? false) + requestContext.Response.ContentType(mimeType.ToString()); + + return true; + } + + string ChooseFile(string path) + { + string finalPath = Path.Join(_rootPath, path); + + if (File.Exists(finalPath)) + return finalPath; + if (Directory.Exists(finalPath)) { foreach (string indexName in indexNames) { string indexFileName = Path.Combine(finalPath, indexName); if (File.Exists(indexFileName)) - { - finalPath = indexFileName; - break; - } + return indexFileName; } } - if (File.Exists(finalPath)) - { - lock (this) - { - httpContext.Response = new HttpResponse(httpContext.Request, new FileStream(finalPath, FileMode.Open, FileAccess.Read)); - httpContext.Response.SetHeader("content-type", MimeTypeMap.GetMimeType(Path.GetExtension(finalPath))); - return true; - } - } - return false; + return null; } public void Dispose() { - _httpServer?.RemoveRouter(this.Route); - _httpServer = null; - _parentRouter?.RemoveHttpMapping(_parentMapping); - _parentMapping = null; + _parentRouter?.UnMap(this); _parentRouter = null; } } diff --git a/ln.http/HeaderContainer.cs b/ln.http/HeaderContainer.cs new file mode 100644 index 0000000..24781ae --- /dev/null +++ b/ln.http/HeaderContainer.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; + +namespace ln.http +{ + public class HeaderContainer : IEnumerable
+ { + private Dictionary _headers = new Dictionary(); + + public HeaderContainer() + { + } + + public HeaderContainer(Stream stream) + { + Read(stream); + } + + public string this[string headerName] + { + get => Get(headerName); + set => Set(headerName, value); + } + + public bool TryGetHeader(string headerName, out Header header) => + _headers.TryGetValue(headerName.ToLower(), out header); + + public bool TryGetValue(string headerName, out string headerValue) + { + if (_headers.TryGetValue(headerName.ToLower(), out Header header)) + { + headerValue = header.Value; + return true; + } + + headerValue = null; + return false; + } + + public bool Contains(string headerName) => _headers.ContainsKey(headerName.ToLower()); + + public void Add(string headerName, string headerValue) + { + string lowerHeaderName = headerName.ToLower(); + if (_headers.TryGetValue(lowerHeaderName, out Header header)) + header.Value += " " + headerValue; + else + _headers.Add(lowerHeaderName, new Header(headerName, headerValue)); + } + + public void Remove(string headerName) => _headers.Remove(headerName.ToLower()); + public void Clear() => _headers.Clear(); + + public void Set(string headerName, String headerValue) + { + string lowerHeaderName = headerName.ToLower(); + if (!_headers.TryGetValue(lowerHeaderName, out Header header)) + { + header = new Header(headerName, ""); + _headers.Add(lowerHeaderName, header); + } + + header.Value = headerValue; + } + + public string Get(string headerName) => Get(headerName, null); + public string Get(string headerName, string defaultValue) + { + if (TryGetHeader(headerName, out Header header)) + return header.Value; + return defaultValue; + } + + public int GetInteger(string headerName) + { + return int.Parse(Get(headerName)); + } + + public int GetInteger(string headerName, int defaultValue) + { + if (TryGetValue(headerName, out string headerValue)) + return int.Parse(headerValue); + return defaultValue; + } + + public bool TryGetInteger(string headerName, out int value) + { + value = 0; + return TryGetValue(headerName, out string headerValue) && int.TryParse(headerValue, out value); + } + + public float GetFloat(string headerName) + { + return float.Parse(Get(headerName)); + } + + public float GetFloat(string headerName, float defaultValue) + { + if (TryGetValue(headerName, out string headerValue)) + return float.Parse(headerValue); + return defaultValue; + } + + public bool TryGetFloat(string headerName, out float value) + { + value = 0; + return TryGetValue(headerName, out string headerValue) && float.TryParse(headerValue, out value); + } + + + public double GetDouble(string headerName) + { + return double.Parse(Get(headerName)); + } + + public double GetDouble(string headerName, double defaultValue) + { + if (TryGetValue(headerName, out string headerValue)) + return double.Parse(headerValue); + return defaultValue; + } + + public bool TryGetDouble(string headerName, out double value) + { + value = 0; + return TryGetValue(headerName, out string headerValue) && double.TryParse(headerValue, out value); + } + + public IEnumerator
GetEnumerator() => _headers.Values.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + foreach (var header in _headers) + sb.AppendFormat("{0}: {1}\r\n", header.Value.Name, header.Value.Value); + return sb.ToString(); + } + + public void Read(Stream stream) + { + string headerName = null; + string headerLine; + while (!String.Empty.Equals(headerLine = HttpConnection.ReadLine(stream))) + { + int colon = headerLine.IndexOf(':', StringComparison.InvariantCulture); + if (char.IsWhiteSpace(headerLine[0])) + { + if (headerName is string) + Add(headerName, " " + headerLine.Substring(1).Trim()); + else + throw new FormatException("expected header name"); + } + else if (colon > 0) + { + headerName = headerLine.Substring(0, colon).Trim(); + Add(headerName, headerLine.Substring(colon + 1).Trim()); + } + } + } + + public void CopyTo(Stream stream) + { + foreach (var header in _headers.Values) + { + byte[] bytes = Encoding.ASCII.GetBytes($"{header.Name}: {header.Value}\r\n"); + stream.Write(bytes, 0, bytes.Length); + } + stream.Write(new byte[] { 0x0d, 0x0a }, 0, 2); + } + + } + + public class Header + { + private string _name; + private Dictionary _parameters; + + public string Name + { + get => _name; + set + { + _name = value; + UpperName = value.ToUpper(); + } + } + + public string UpperName { get; private set; } + public string Value { get; set; } + + + public Header(string headerName) + { + Name = headerName; + Value = String.Empty; + } + + public Header(string headerName, string headerValue) + { + Name = headerName; + Value = headerValue; + } + + public bool TryGetParameter(string parameterName, out string parameterValue) + { + if (_parameters is null) + ParseParameters(); + + return _parameters.TryGetValue(parameterName, out parameterValue); + } + public bool ContainsParameter(string parameterName) + { + if (_parameters is null) + ParseParameters(); + + return _parameters.ContainsKey(parameterName); + } + + public Header ParseParameters() + { + _parameters = new Dictionary(); + + int idxSemicolon = Value.IndexOf(';'); + if (idxSemicolon != -1) + { + string[] ptoks = Value.Split(';'); + + foreach (string ptok in ptoks) + { + int idxEqual = ptok.IndexOf('='); + string pn, pv; + if (idxEqual != -1) + { + pn = ptok.Substring(0, idxEqual).Trim(); + pv = ptok.Substring(idxEqual + 1); + } + else + { + pn = ptok.Trim(); + pv = ""; + } + + _parameters.Add(pn, pv); + } + } + return this; + } + + } +} \ No newline at end of file diff --git a/ln.http/Http1XConnection.cs b/ln.http/Http1XConnection.cs new file mode 100644 index 0000000..63b2ea3 --- /dev/null +++ b/ln.http/Http1XConnection.cs @@ -0,0 +1,92 @@ +using System; +using System.IO; +using System.Net; +using System.Text; + +namespace ln.http; + +public class Http1XConnection : HttpConnection +{ + bool _keepAlive; + + public Http1XConnection(Listener listener, IPEndPoint remoteEndpoint, Stream connectionStream, string method, Uri uri, HttpVersion httpVersion) + : base(listener, remoteEndpoint, connectionStream, method, uri, httpVersion) + { + if (httpVersion == HttpVersion.HTTP11) + _keepAlive = true; + } + + public override void Run() + { + string _method = Method; + Uri _requestUri = RequestUri; + HttpVersion _httpVersion = HttpVersion; + HeaderContainer headerContainer = new HeaderContainer(); + + while (ConnectionStream.CanRead && ConnectionStream.CanWrite) + { + headerContainer.Clear(); + headerContainer.Read(ConnectionStream); + + if ( + headerContainer.TryGetHeader("connection", out Header connectionHeader) + ) + { + switch (connectionHeader.Value) + { + case "close": + _keepAlive = false; + break; + case "keep-alive": + _keepAlive = true; + break; + } + } + + HttpRequestStream requestStream = null; + if (headerContainer.TryGetInteger("content-length", out int contentLength)) + requestStream = new HttpRequestStream(ConnectionStream, contentLength); + + HttpRequestContext requestContext = new HttpRequestContext(Listener, this, ConnectionStream, new HttpRequest(_method, _requestUri, _httpVersion, false, headerContainer, requestStream)); + Listener.Dispatch(requestContext); + + if (!ConnectionStream.CanWrite && !ConnectionStream.CanRead) + break; + + SendResponse(requestContext); + ConnectionStream.Flush(); + + string responseConnectionHeader = requestContext.Response.GetHeader("connection"); + _keepAlive = responseConnectionHeader switch + { + "close" => false, + "keep-alive" => true, + _ => _keepAlive + }; + + requestContext.Dispose(); + + if (!_keepAlive) + break; + + if (!HttpConnection.ReadRequestLine(ConnectionStream, out _method, out _requestUri, out _httpVersion)) + break; + } + } + + public override void SendResponse(HttpRequestContext requestContext) + { + foreach (var httpCookie in requestContext.Response.Cookies) + requestContext.Response.Headers.Add("Set-Cookie", httpCookie.ToString()); + + string statusLine = $"{HttpVersionSupport.ToString(requestContext.Request.HttpVersion)} {(int)requestContext.Response.HttpStatusCode} {requestContext.Response.HttpStatusCode.ToString()}\r\n"; + byte[] statusLineBytes = Encoding.ASCII.GetBytes(statusLine); + requestContext.ConnectionStream.Write(statusLineBytes, 0, statusLineBytes.Length); + if (!requestContext.Response.Headers.Contains("content-type")) + requestContext.Response.Headers.Set("content-type", requestContext.Response.HttpContent?.ContentType); + requestContext.Response.Headers.Set("content-length", requestContext.Response.HttpContent?.Length.ToString() ?? "0"); + requestContext.Response.Headers.CopyTo(requestContext.ConnectionStream); + requestContext.Response.HttpContent?.CopyTo(requestContext.ConnectionStream); + } + +} \ No newline at end of file diff --git a/ln.http/Http2Connection.cs b/ln.http/Http2Connection.cs new file mode 100644 index 0000000..a68811b --- /dev/null +++ b/ln.http/Http2Connection.cs @@ -0,0 +1,24 @@ +using System; +using System.IO; +using System.Net; + +namespace ln.http; + +public class Http2Connection : HttpConnection +{ + public Http2Connection(Listener listener, IPEndPoint remoteEndpoint, Stream connectionStream, string method, Uri uri, HttpVersion httpVersion) + : base(listener, remoteEndpoint, connectionStream, method, uri, httpVersion) + { + throw new NotImplementedException(); + } + + public override void Run() + { + throw new NotImplementedException(); + } + + public override void SendResponse(HttpRequestContext requestContext) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/ln.http/HttpConnection.cs b/ln.http/HttpConnection.cs index 937d8ed..e0e7156 100644 --- a/ln.http/HttpConnection.cs +++ b/ln.http/HttpConnection.cs @@ -1,26 +1,105 @@ +using System; +using System.Collections.Generic; using System.IO; using System.Net; +using System.Text; namespace ln.http { - public class HttpConnection + public abstract class HttpConnection { - public Stream ClientStream { get; } - public IPEndPoint RemoteEndPoint { get; } - public IPEndPoint LocalEndPoint { get; } - - public bool IsEncrypted { get; } + public Listener Listener { get; } + public IPEndPoint LocalEndpoint => Listener.LocalEndpoint; + public IPEndPoint RemoteEndpoint { get; } + public Stream ConnectionStream { get; } + public string Method { get; } + public Uri RequestUri { get; } + public HttpVersion HttpVersion { get; } - public HttpConnection(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, Stream clientStream) - :this(localEndPoint, remoteEndPoint, clientStream, false) - {} - - public HttpConnection(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, Stream clientStream, bool isEncrypted) + public HttpConnection(Listener listener, IPEndPoint remoteEndpoint, Stream connectionStream, string method, + Uri uri, HttpVersion httpVersion) { - ClientStream = clientStream; - LocalEndPoint = localEndPoint; - RemoteEndPoint = remoteEndPoint; - IsEncrypted = isEncrypted; + Listener = listener; + RemoteEndpoint = remoteEndpoint; + ConnectionStream = connectionStream; + + Method = method; + RequestUri = uri; + HttpVersion = httpVersion; } + + public abstract void Run(); + + public static string ReadLine(Stream stream) => ReadLine(stream, Encoding.ASCII); + + public static string ReadLine(Stream stream, Encoding encoding) + { + if (!stream.CanRead) + return null; + + byte[] line = new byte[1024]; + int n = 0; + int ch; + while ((ch = stream.ReadByte()) != -1) + { + switch (ch) + { + case '\r': + break; + case '\n': + return encoding.GetString(line, 0, n); + default: + line[n++] = (byte)ch; + break; + } + } + + if (n > 0) + return encoding.GetString(line, 0, n); + + return null; + } + + public static bool ReadRequestLine(Stream stream, out string method, out Uri requestUri, + out HttpVersion httpVersion) + { + string requestLine = ReadLine(stream); + if (requestLine is null) + { + method = null; + requestUri = null; + httpVersion = HttpVersion.None; + return false; + } + + int idxSpace1 = requestLine.IndexOf(' '); + int idxSpace2 = requestLine.IndexOf(' ', idxSpace1 + 1); + + if ((idxSpace1 > 0) && (idxSpace2 > 0)) + { + method = requestLine.Substring(0, idxSpace1); + requestUri = new Uri(requestLine.Substring(idxSpace1 + 1, (idxSpace2 - idxSpace1 - 1))); + string protocol = requestLine.Substring(idxSpace2 + 1); + httpVersion = HttpVersion.None; + + if ("PRI".Equals(method) && Http2PrefaceUri.Equals(requestUri) && "HTTP/2.0".Equals(protocol)) + httpVersion = HttpVersion.HTTP2; + else if ("HTTP/1.0".Equals(protocol)) + httpVersion = HttpVersion.HTTP10; + else if ("HTTP/1.1".Equals(protocol)) + httpVersion = HttpVersion.HTTP11; + + return true; + } + + method = null; + requestUri = null; + httpVersion = HttpVersion.None; + return false; + } + + public abstract void SendResponse(HttpRequestContext requestContext); + + static Uri Http2PrefaceUri = new Uri("*"); } -} \ No newline at end of file +} diff --git a/ln.http/HttpConnectionFlags.cs b/ln.http/HttpConnectionFlags.cs new file mode 100644 index 0000000..16e3770 --- /dev/null +++ b/ln.http/HttpConnectionFlags.cs @@ -0,0 +1,9 @@ +using System; + +namespace ln.http; + +[Flags] +public enum HttpConnectionFlags +{ + TLS, +} \ No newline at end of file diff --git a/ln.http/HttpContent.cs b/ln.http/HttpContent.cs new file mode 100644 index 0000000..353eef0 --- /dev/null +++ b/ln.http/HttpContent.cs @@ -0,0 +1,166 @@ +using System; +using System.IO; +using System.Text; +using ln.json; +using ln.mime; + +namespace ln.http; + +public abstract class HttpContent : IDisposable +{ + public HttpContent(string contentType) + { + ContentType = contentType; + } + + public string ContentType { get; } + public abstract long Length { get; } + public abstract void CopyTo(Stream targetStream); + + public virtual void Dispose() + { + } +} + +public class StringContent : HttpContent +{ + public string Text { get; set; } = String.Empty; + public Encoding Encoding { get; set; } + + public StringContent(string text, string contentType) + :base(contentType) + { + Encoding = Encoding.UTF8; + Text = text; + } + public StringContent(string text) + :this(text, "text/plain; charset=utf-8") + { + } + + public override long Length => Encoding.GetBytes(Text ).Length; + public override void CopyTo(Stream targetStream) + { + byte[] bytes = Encoding.GetBytes(Text); + targetStream.Write(bytes, 0, bytes.Length); + } +} + +public class FileContent : HttpContent +{ + public string FileName { get; set; } + + public FileContent(string filename, string contentType) + : base(contentType) + { + FileName = filename; + } + public FileContent(string filename) + :this(filename, new MimeTypes().GetMimeTypeByExtension(Path.GetExtension(filename)).ToString()) + { + } + + public override long Length => new FileInfo(FileName).Length; + public override void CopyTo(Stream targetStream) + { + using FileStream fs = new FileStream(FileName, FileMode.Open, FileAccess.Read); + fs.CopyTo(targetStream); + } +} + +public class JsonContent : HttpContent +{ + public JSONValue Value { get; set; } + + private byte[] _bytes; + + public JsonContent(JSONValue json) + :base("application/json") + { + Value = json; + } + + public override long Length + { + get + { + if (_bytes is null) + Serialize(); + + return _bytes.Length; + } + } + + public override void CopyTo(Stream targetStream) + { + if (_bytes is null) + Serialize(); + + targetStream.Write(_bytes, 0, _bytes.Length); + } + + public void Serialize() + { + string jsonText = Value.ToString(); + _bytes = Encoding.UTF8.GetBytes(jsonText); + } +} + +public class StreamContent : HttpContent +{ + public Stream Stream { get; protected set; } + public bool DisposeStream { get; } + + public StreamContent(Stream stream) + : this(stream, true, "application/octet-stream") + { + } + + public StreamContent(Stream stream, bool disposeStream) + :this(stream, disposeStream, "application/octet-stream") + {} + + public StreamContent(Stream stream, string contentType) + :this(stream, true, contentType) + { + } + public StreamContent(Stream stream, bool disposeStream, string contentType) + :base(contentType) + { + Stream = stream; + DisposeStream = disposeStream; + } + + public override long Length => Stream.CanSeek ? Stream.Length - Stream.Position : Stream.Length; + public override void CopyTo(Stream targetStream) => Stream.CopyTo(targetStream); + + public override void Dispose() + { + if (DisposeStream) + Stream.Dispose(); + } +} + +public class StreamedContent : HttpContent +{ + public MemoryStream ContentStream { get; } + + public StreamedContent(string contentType) + :base(contentType) + { + ContentStream = new MemoryStream(); + } + + public override long Length => ContentStream.Length; + + public override void CopyTo(Stream targetStream) + { + ContentStream.Position = 0; + ContentStream.CopyTo(targetStream); + } + + public override void Dispose() + { + ContentStream?.Dispose(); + } +} diff --git a/ln.http/HttpContext.cs b/ln.http/HttpContext.cs deleted file mode 100644 index 4ad93e5..0000000 --- a/ln.http/HttpContext.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using ln.http.exceptions; -using ln.http.router; - -namespace ln.http -{ - public class HttpContext - { - public HttpServer HttpServer { get; } - public HttpContext SourceContext { get; } - public HttpException HttpException { get; } - public HttpRequest Request { get; set; } - public HttpResponse Response { get; set; } - public HttpPrincipal AuthenticatedPrincipal { get; private set; } - - public HttpContext(HttpServer httpServer) - { - HttpServer = httpServer; - } - - public HttpContext(HttpServer httpServer, HttpRequest httpRequest) : this(httpServer) - { - Request = httpRequest; - RoutableUri = httpRequest.RequestUri.AbsolutePath; - } - - public HttpContext(HttpContext sourceContext, HttpException httpException) - { - SourceContext = sourceContext; - HttpException = httpException; - - HttpServer = sourceContext.HttpServer; - RoutableUri = String.Format("/_err/{0:3}.html", (int)httpException.HttpStatusCode); - AuthenticatedPrincipal = SourceContext.AuthenticatedPrincipal; - } - - public string RoutableUri { get; set; } - - public bool Authenticate() - { - foreach (var authenticationSource in HttpServer.AuthenticationSources) - { - if (authenticationSource.AuthenticatePrincipal(this, out HttpPrincipal httpPrincipal)) - { - AuthenticatedPrincipal = httpPrincipal; - return true; - } - } - return false; - } - public void DeAuthenticate() => AuthenticatedPrincipal = null; - } -} \ No newline at end of file diff --git a/ln.http/HttpEndpointController.cs b/ln.http/HttpEndpointController.cs index c31742a..b33bdac 100644 --- a/ln.http/HttpEndpointController.cs +++ b/ln.http/HttpEndpointController.cs @@ -3,18 +3,31 @@ using System.ComponentModel; using System.IO; using System.Linq; using System.Reflection; +using System.Text.RegularExpressions; using ln.json; using ln.json.mapping; -using ln.protocols.helper; using ln.type; namespace ln.http { - public abstract class HttpEndpointController : HttpRouter + public abstract class HttpEndpointController : HttpRouter, IDisposable { - public HttpEndpointController() + private HttpRouter _httpRouter; + + public HttpEndpointController():this(null){} + public HttpEndpointController(HttpRouter httpRouter) { + _httpRouter = httpRouter; + MapAttribute mapAttribute = GetType().GetCustomAttribute(); + if (mapAttribute is not null) + { + HttpMethod = mapAttribute.Method != HttpMethod.NONE ? mapAttribute.Method : HttpMethod.ANY; + Route = mapAttribute.Path is not null ? new Regex(mapAttribute.Path) : null; + } + Initialize(); + + _httpRouter?.Map(this); } void Initialize() @@ -60,9 +73,9 @@ namespace ln.http .Select((pi) => pi.GetCustomAttribute()).ToArray(); } - public abstract bool Route(HttpContext httpContext); + public abstract bool Route(HttpRequestContext httpContext, string routePath); - bool TryApplyParameters(HttpContext httpContext, out object[] parameters) + bool TryApplyParameters(HttpRequestContext requestContext, out object[] parameters) { parameters = new object[_parameterInfos.Length]; @@ -70,14 +83,14 @@ namespace ln.http { ParameterInfo parameterInfo = _parameterInfos[n]; - if (parameterInfo.ParameterType.Equals(typeof(HttpContext))) - parameters[n] = httpContext; + if (parameterInfo.ParameterType.Equals(typeof(HttpRequestContext))) + parameters[n] = requestContext; else if (parameterInfo.ParameterType.Equals(typeof(HttpRequest))) - parameters[n] = httpContext.Request; + parameters[n] = requestContext.Request; else if (parameterInfo.ParameterType.Equals(typeof(HttpPrincipal))) - parameters[n] = httpContext.AuthenticatedPrincipal; + parameters[n] = requestContext.AuthenticatedPrincipal; else if (TryFindArgumentByName( - httpContext, + requestContext, _argumentSourceAttributes[n]?.ArgumentSource ?? HttpArgumentSource.AUTO, _argumentSourceAttributes[n]?.ArgumentName ?? _parameterInfos[n].Name, out string parameterValue @@ -94,14 +107,14 @@ namespace ln.http { if (_parameterInfos[n].ParameterType.Equals(typeof(Stream))) { - parameters[n] = httpContext.Request.ContentStream; + parameters[n] = requestContext.Request.ContentStream; } else if (_parameterInfos[n].ParameterType.Equals(typeof(TextReader))) { - parameters[n] = new StreamReader(httpContext.Request.ContentStream); - } else if (httpContext.Request.Headers.TryGetValue("Content-Type", out string contentType) && + parameters[n] = new StreamReader(requestContext.Request.ContentStream); + } else if (requestContext.Request.Headers.TryGetValue("Content-Type", out string contentType) && contentType.Equals("application/json")) { - using (TextReader reader = httpContext.Request.ContentStream.TextReader()) + using (TextReader reader = new StreamReader(requestContext.Request.ContentStream)) { JSONValue jsonValue = JSONParser.Parse(reader); @@ -124,9 +137,9 @@ namespace ln.http return true; } - bool TryFindArgumentByName(HttpContext httpContext, HttpArgumentSource argumentSource, string parameterName, out string parameterValue) + bool TryFindArgumentByName(HttpRequestContext httpContext, HttpArgumentSource argumentSource, string parameterName, out string parameterValue) { - if (((argumentSource & HttpArgumentSource.PARAMETER) == HttpArgumentSource.PARAMETER) && httpContext.Request.TryGetParameter(parameterName, out parameterValue)) + if (((argumentSource & HttpArgumentSource.PARAMETER) == HttpArgumentSource.PARAMETER) && httpContext.TryGetParameter(parameterName, out parameterValue)) return true; else if (((argumentSource & HttpArgumentSource.HEADER) == HttpArgumentSource.HEADER) && (httpContext.Request.Headers.TryGetValue(parameterName, out parameterValue))) @@ -139,7 +152,7 @@ namespace ln.http return false; } - private object InvokeMethod(HttpContext httpContext) + private object InvokeMethod(HttpRequestContext httpContext) { object[] parameters = null; try @@ -156,8 +169,6 @@ namespace ln.http DisposeParameters(parameters); } } - - public class NonVoidEndpoint : MappedEndpoint { @@ -166,7 +177,7 @@ namespace ln.http { } - public override bool Route(HttpContext httpContext) + public override bool Route(HttpRequestContext httpContext, string routePath) { object returnedValue = InvokeMethod(httpContext); @@ -189,7 +200,7 @@ namespace ln.http { } - public override bool Route(HttpContext httpContext) + public override bool Route(HttpRequestContext httpContext, string routePath) { object returnedValue = InvokeMethod(httpContext); if (returnedValue is HttpResponse httpResponse) @@ -213,6 +224,11 @@ namespace ln.http } } } - + + public void Dispose() + { + _httpRouter?.Dispose(); + base.Dispose(); + } } } \ No newline at end of file diff --git a/ln.http/HttpListener.cs b/ln.http/HttpListener.cs deleted file mode 100644 index f716bf9..0000000 --- a/ln.http/HttpListener.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Net; -using System.Net.Sockets; -using System.Threading; - -namespace ln.http -{ - public class HttpListener : IDisposable - { - public static int DefaultPort = 80; - - - private IPEndPoint _localEndPoint; - private Socket _socket; - private HttpServer _httpServer; - - public IPEndPoint LocalEndpoint => _localEndPoint; - - public HttpListener(HttpServer httpServer) : this(httpServer, DefaultPort) - { - } - - public HttpListener(HttpServer httpServer, int port) - { - _httpServer = httpServer; - _localEndPoint = new IPEndPoint(IPAddress.IPv6Any, port); - - Initialize(); - } - - private void Initialize() - { - _socket = new Socket(SocketType.Stream, ProtocolType.Tcp); - _socket.ExclusiveAddressUse = false; - _socket.Bind(_localEndPoint); - _localEndPoint = (IPEndPoint)_socket.LocalEndPoint; - _socket.Listen(); - - ThreadPool.QueueUserWorkItem((state )=> ListenerThread()); - } - - private void ListenerThread() - { - while (_socket?.IsBound ?? false) - { - try - { - Socket clientSocket = _socket.Accept(); - _httpServer.Connection( - new HttpConnection( - _localEndPoint, - (IPEndPoint)clientSocket.RemoteEndPoint, - new NetworkStream(clientSocket) - ) - ); - } - catch - { - throw; - } - } - } - - public void Dispose() - { - _socket?.Close(); - _socket?.Dispose(); - _socket = null; - } - } -} \ No newline at end of file diff --git a/ln.http/HttpRequest.cs b/ln.http/HttpRequest.cs index 1e14078..1c64bd6 100644 --- a/ln.http/HttpRequest.cs +++ b/ln.http/HttpRequest.cs @@ -2,49 +2,70 @@ using System.IO; using System.Collections.Generic; using System.Linq; -using System.Net; -using ln.protocols.helper; -using ln.rtp; +using System.Text.RegularExpressions; +using ln.json; namespace ln.http -{ public class HttpRequest : IDisposable +{ + public class HttpRequest : IDisposable { - public Request BaseRequest { get; } - public HttpServer Server { get; } - - public String Protocol => BaseRequest.Protocol; - public HttpMethod Method { get; } - - public IPAddress ClientAddress { get; } - public String Host { get; } - public int Port { get; } - public bool TLS { get; } - - public HeaderContainer Headers => BaseRequest.Headers; + public string RequestMethodName { get; } + public Uri RequestUri { get; private set; } + public HttpVersion HttpVersion { get; } + public HeaderContainer Headers { get; } + + public HttpMethod Method { get; private set; } + public String Host { get; private set; } + public int Port { get; private set; } + public bool ViaTLS { get; private set; } + public QueryStringParameters Query { get; private set; } - - public Stream ContentStream => BaseRequest.ContentStream; - public Stream ConnectionStream => BaseRequest.ContentStream.BaseStream; - - public Uri BaseUri { get; } - public Uri RequestUri { get; } + public Uri BaseUri { get; private set; } + Dictionary requestCookies; - Dictionary requestParameters; - - - public HttpRequest(HttpServer httpServer, Request baseRequest) + public HttpRequestStream ContentStream { get; } + + public HttpRequest(string requestMethodName, Uri requestUri, HttpVersion httpVersion, bool viaTLS, HeaderContainer headerContainer, HttpRequestStream requestStream) { - Server = httpServer; - BaseRequest = baseRequest; + RequestMethodName = requestMethodName; + RequestUri = requestUri; + HttpVersion = httpVersion; + Headers = headerContainer; + ViaTLS = viaTLS; + ContentStream = requestStream; - if (Enum.TryParse(BaseRequest.Method, out HttpMethod httpMethod)) + Initialize(); + } + + //private static Regex reHostPort = new Regex("(?(\\w[^:]*|\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|\\[\\]]+))(:(?\\d+))?"); + + void Initialize() + { + if (Enum.TryParse(RequestMethodName, out HttpMethod httpMethod)) Method = httpMethod; else Method = HttpMethod.NONE; - Headers.TryGetValue("Host", out string host); + if (Headers.TryGetValue("Host", out string host)) + { + int colon = host.LastIndexOf(':'); + if (colon != -1) + { + Host = host.Substring(0, colon).Trim(); + Port = int.Parse(host.Substring(colon + 1).Trim()); + } + else + { + Host = host.Trim(); + } + } else + { + Host = RequestUri.Host; + Port = RequestUri.Port; + } +/* if (Headers.TryGetValue("X-Forwarded-Host", out string forwardedHost)) host = forwardedHost; if (Headers.TryGetValue("X-Forwarded-For", out string forwardedFor)) @@ -56,31 +77,17 @@ namespace ln.http { // ToDo: Implement parser } - - int colon = host.IndexOf(':'); - if (colon != -1) - { - Host = host.Substring(0, colon).Trim(); - Port = int.Parse(host.Substring(colon + 1).Trim()); - } - else - { - Host = host.Trim(); - } - - BaseUri = new UriBuilder(TLS ? "https:" : "http:", Host, Port).Uri; - RequestUri = new Uri(BaseUri, BaseRequest.RequestUri); +*/ + BaseUri = new UriBuilder(ViaTLS ? "https:" : "http:", Host, Port).Uri; + RequestUri = new Uri(BaseUri, RequestUri); Query = new QueryStringParameters(RequestUri.Query); - - requestParameters = new Dictionary(); + requestCookies = new Dictionary(); if (Headers.TryGetValue("Cookie", out string cookies)) SetupCookies(cookies); - - } - + private void SetupCookies(string cookies) { foreach (String cookie in cookies.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) @@ -98,11 +105,8 @@ namespace ln.http } } - 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 override string ToString() => + $"[HttpRequest: Host={Host} Port={Port} HttpVersion={HttpVersion} Method={Method} URI={RequestUri} Query={Query}]"; public String GetRequestHeader(String name) { @@ -122,36 +126,8 @@ namespace ln.http public bool TryGetCookie(string cookieName, out string cookieValue) => requestCookies.TryGetValue(cookieName, out cookieValue); - - 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 bool TryGetParameter(String parameterName, out string parameterValue) => - requestParameters.TryGetValue(parameterName, out parameterValue); - public void SetParameter(String parameterName, String parameterValue) => - requestParameters[parameterName] = parameterValue; - - public IEnumerable ParameterNames => requestParameters.Keys; - - 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 HttpResponse Redirect(string location, params object[] p) => Redirect(HttpStatusCode.SeeOther, location, p); public HttpResponse Redirect(HttpStatusCode statusCode, string location, params object[] p) { @@ -163,9 +139,21 @@ namespace ln.http return response; } - public void Dispose() + public JSONValue ToJsonValue() { - ContentStream?.Dispose(); + long p = ContentStream.Position; + try + { + using (TextReader reader = new StreamReader(ContentStream)) + return JSONParser.Parse(reader); + } + finally + { + ContentStream.Position = p; + } } + + + public void Dispose() => ContentStream?.Dispose(); } } \ No newline at end of file diff --git a/ln.http/HttpRequestContext.cs b/ln.http/HttpRequestContext.cs new file mode 100644 index 0000000..ef4eda9 --- /dev/null +++ b/ln.http/HttpRequestContext.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using ln.collections; + +namespace ln.http; + +public sealed class HttpRequestContext : IDisposable +{ + public Listener Listener { get; } + public IPEndPoint LocalEndpoint => Listener.LocalEndpoint; + public HttpConnection HttpConnection { get; } + public Stream ConnectionStream { get; } + public HttpRequest Request { get; protected set; } + public HttpResponse Response { get; set; } + public HttpRequestContext ParentContext { get; } + public HttpPrincipal AuthenticatedPrincipal { get; set; } + + + Dictionary _parameters = new Dictionary(); + + public HttpRequestContext(HttpRequestContext parentContext, HttpRequest request) + : this(parentContext.Listener, parentContext.HttpConnection, parentContext.ConnectionStream, request) + { + ParentContext = parentContext; + } + + public HttpRequestContext(Listener listener, HttpConnection httpConnection, Stream connectionStream, HttpRequest request) + { + Listener = listener; + Request = request; + HttpConnection = httpConnection; + ConnectionStream = connectionStream; + } + + protected HttpRequestContext(Listener listener, HttpConnection httpConnection, Stream connectionStream) + { + Listener = listener; + HttpConnection = httpConnection; + ConnectionStream = connectionStream; + } + + public void Dispose() + { + Request?.Dispose(); + Response?.Dispose(); + } + + + public bool ContainsParameter(string parameterName) => _parameters.ContainsKey(parameterName); + public String GetParameter(String parameterName) => GetParameter(parameterName, null); + + public String GetParameter(String parameterName, String defaultValue) + { + if (!_parameters.TryGetValue(parameterName, out string value)) + value = defaultValue; + return value; + } + + public bool TryGetParameter(String parameterName, out string parameterValue) => + _parameters.TryGetValue(parameterName, out parameterValue); + public void SetParameter(String parameterName, String parameterValue) => + _parameters[parameterName] = parameterValue; + + public IEnumerable ParameterNames => _parameters.Keys; +} \ No newline at end of file diff --git a/ln.http/HttpRequestStream.cs b/ln.http/HttpRequestStream.cs new file mode 100644 index 0000000..4dc903e --- /dev/null +++ b/ln.http/HttpRequestStream.cs @@ -0,0 +1,63 @@ +using System; +using System.IO; + +namespace ln.http; + +public class HttpRequestStream : Stream +{ + public int MemoryLimit { get; set; } = 1024 * 1024 * 10; + + private Stream _stream; + private string _tempFileName; + + public HttpRequestStream(Stream connectionStream, int length) + { + byte[] transferBuffer; + if (length <= MemoryLimit) + { + transferBuffer = new byte[length]; + if (connectionStream.Read(transferBuffer, 0, transferBuffer.Length) != length) + throw new IOException(); + + _stream = new MemoryStream(transferBuffer); + } + else + { + _tempFileName = Path.GetTempFileName(); + _stream = new FileStream(_tempFileName, FileMode.Open, FileAccess.ReadWrite); + + transferBuffer = new byte[MemoryLimit]; + while (length > 0) + { + int size = length > transferBuffer.Length ? transferBuffer.Length : length; + int nread = connectionStream.Read(transferBuffer, 0, size); + _stream.Write(transferBuffer, 0, nread); + length -= nread; + } + + _stream.Position = 0; + } + } + + public override int Read(byte[] buffer, int offset, int count) => _stream.Read(buffer, offset, count); + public override long Seek(long offset, SeekOrigin origin) => _stream.Seek(offset, origin); + public override void SetLength(long value) => throw new System.NotImplementedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new System.NotImplementedException(); + public override void Flush() => throw new System.NotImplementedException(); + + public override bool CanRead { get; } = true; + public override bool CanSeek { get; } = true; + public override bool CanWrite { get; } = false; + public override long Length => _stream.Length; + public override long Position { get => _stream.Position; set => _stream.Position = value; } + + public override int Read(Span buffer) => _stream.Read(buffer); + public override void Close() => _stream.Close(); + + protected override void Dispose(bool disposing) + { + _stream.Dispose(); + if (_tempFileName is not null) + File.Delete(_tempFileName); + } +} \ No newline at end of file diff --git a/ln.http/HttpResponse.cs b/ln.http/HttpResponse.cs index 0e52b20..0e5cb89 100644 --- a/ln.http/HttpResponse.cs +++ b/ln.http/HttpResponse.cs @@ -1,117 +1,52 @@ using System; using System.IO; using System.Collections.Generic; -using System.Linq; using ln.json; -using ln.json.mapping; namespace ln.http { - public class HttpResponse + public class HttpResponse : IDisposable { - public Stream ContentStream { get; private set; } - public TextWriter ContentWriter { get; private set; } - public bool HasCustomContentStream { get; private set; } + List _cookies = new List(); - Dictionary> headers = new Dictionary>(); - List cookies = new List(); - - public HttpResponse() : this(HttpStatusCode.OK) - { - } public HttpResponse(HttpStatusCode statusCode) { HttpStatusCode = statusCode; - ContentStream = new MemoryStream(); - ContentWriter = new StreamWriter(ContentStream); - - SetHeader("content-type", "text/html"); + Headers = new HeaderContainer(); + } + public HttpResponse() : this(HttpStatusCode.OK) + { } - - [Obsolete] - public HttpResponse(HttpRequest httpRequest) : this() { } - [Obsolete] - public HttpResponse(HttpRequest httpRequest,string contentType) : this(contentType) { } public HttpResponse(string contentType) :this() { SetHeader("content-type", contentType); } + + public HeaderContainer Headers { get; } + public HttpContent HttpContent { get; set; } - [Obsolete] - public HttpResponse(HttpRequest httpRequest, Stream contentStream) : this(contentStream) { } - public HttpResponse(Stream contentStream) - { - Content(contentStream); + public String GetHeader(string name) => Headers.Get(name); + public String GetHeader(string name, string defValue) => Headers.Get(name, defValue); - HttpStatusCode = HttpStatusCode.OK; - SetHeader("content-type", "text/html"); - } + public void SetHeader(String name, String value) => Headers.Set(name, value); - [Obsolete] - public HttpResponse(HttpRequest httpRequest, Stream contentStream,string contentType) : this(contentStream, contentType){ } - public HttpResponse(Stream contentStream,string contentType) - :this(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) => headers.ContainsKey(headerName.ToUpper()); + public void AddHeader(String name, String value) => Headers.Add(name, value); + public void RemoveHeader(String name) => Headers.Remove(name); + public bool ContainsHeader(string headerName) => Headers.Contains(headerName); 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 void AddCookie(HttpCookie httpCookie) => _cookies.Add(httpCookie); + public void RemoveCookie(HttpCookie httpCookie) => _cookies.Remove(httpCookie); + public HttpCookie[] Cookies => _cookies.ToArray(); public HttpStatusCode HttpStatusCode { get; set; } - [Obsolete] - public int StatusCode - { - get => (int)HttpStatusCode; - set => HttpStatusCode = (HttpStatusCode)value; - } - [Obsolete] - public String StatusMessage - { - get => HttpStatusCode.ToString(); - } + public HttpResponse Header(string name,string value) { SetHeader(name, value); @@ -171,55 +106,36 @@ namespace ln.http public HttpResponse Content(Exception exception) { - SetHeader("content-type", "text/plain"); - ContentWriter.WriteLine("{0}", exception); - ContentWriter.Flush(); - + HttpContent = new StringContent(exception.ToString()); return this; } public HttpResponse Content(string text) { - ContentWriter.Write(text); - ContentWriter.Flush(); - + HttpContent = new StringContent(text); return this; } - public HttpResponse Content(JSONValue json) => ContentType("application/json").Content(json.ToString()); + public HttpResponse Content(JSONValue json) + { + HttpContent = new JsonContent(json); + return this; + } public HttpResponse Content(Stream contentStream) { - ContentStream = contentStream; - ContentWriter = null; - HasCustomContentStream = true; + HttpContent = new StreamContent(contentStream); return this; } - public virtual void WriteTo(Stream stream) + public HttpResponse Content(HttpContent httpContent) { - SetHeader("Content-Length", ContentStream.Length.ToString()); + HttpContent = httpContent; + return this; + } - StreamWriter streamWriter = new StreamWriter(stream) {NewLine = "\r\n"}; - streamWriter.WriteLine("{0} {1} {2}", "HTTP/1.1", (int)HttpStatusCode, HttpStatusCode.ToString()); - foreach (String headerName in GetHeaderNames()) - { - streamWriter.WriteLine("{0}: {1}", headerName, GetHeader(headerName)); - } - - foreach (HttpCookie httpCookie in Cookies) - { - streamWriter.WriteLine("Set-Cookie: {0}", httpCookie.ToString()); - } - - streamWriter.WriteLine(); - streamWriter.Flush(); - - ContentStream.Position = 0; - ContentStream.CopyTo(stream); - ContentStream.Close(); - ContentStream.Dispose(); - - stream.Flush(); + public void Dispose() + { + HttpContent?.Dispose(); } } } diff --git a/ln.http/HttpRouter.cs b/ln.http/HttpRouter.cs index 214b7ff..6246367 100644 --- a/ln.http/HttpRouter.cs +++ b/ln.http/HttpRouter.cs @@ -2,108 +2,74 @@ using System.Text.RegularExpressions; using System.Collections.Generic; using System.Linq; +using ln.http.route; namespace ln.http { - public delegate bool HttpRouterDelegate(HttpContext context); - public delegate bool HttpAuthorizationDelegate(HttpContext httpContext); - public delegate void HttpFilterDelegate(HttpContext httpContext); + public delegate bool HttpRouterDelegate(HttpRequestContext context, string routedPath); + public delegate bool HttpAuthorizationDelegate(HttpRequestContext httpRequestContext); + public delegate void HttpFilterDelegate(HttpRequestContext httpRequestContext); public enum HttpRoutePriority : int { HIGHEST = 0, HIGH = 1, NORMAL = 2, LOW = 3, LOWEST = 4 } - public class HttpRouter : IDisposable + public class HttpRouter : HttpRoute, IDisposable { public event HttpFilterDelegate HttpFilters; - - private List[] _mappings = new List[5]; - private HttpServer _httpServer; + public event HttpFilterDelegate HttpFixups; + private HttpRouter _parentRouter; - private HttpMapping _parentMapping; + private List _routes = new List(); public HttpRouter() + :this(HttpMethod.ANY, null) { - for (int n = 0; n < 5; n++) - _mappings[n] = new List(); + _routerDelegate = RouteRequest; } - public HttpRouter(HttpServer httpServer) - : this() + public HttpRouter(HttpMethod httpMethod, string mapPath) + : base(httpMethod, mapPath) { - _httpServer = httpServer; - httpServer.AddRouter(this); } - - public HttpRouter(HttpRouter parentRouter, string mappingPath) + + public HttpRouter(HttpRouter parentRouter, string mapPath) + :base(HttpMethod.ANY, mapPath) { _parentRouter = parentRouter; - _parentMapping = _parentRouter.Map(HttpMethod.ANY, mappingPath, this.Route); + _parentRouter.Map(this); } - public HttpMapping Map(HttpMethod httpMethod, string uri, HttpRouterDelegate routerDelegate) => - Map(httpMethod, uri, HttpRoutePriority.NORMAL, routerDelegate); - - public HttpMapping Map(HttpMethod httpMethod, string uri, HttpRoutePriority priority, HttpRouterDelegate routerDelegate) - { - HttpMapping httpMapping = new HttpMapping(httpMethod, RegexFromRoute(uri), routerDelegate); - _mappings[(int)priority].Add(httpMapping); - return httpMapping; - } - - public bool RemoveHttpMapping(HttpMapping httpMapping) - { - foreach (var mappings in _mappings) - { - if (mappings.Contains(httpMapping)) - return mappings.Remove(httpMapping); - } - - return false; - } - - public bool Route(HttpContext httpContext) - { - string residual = ""; - - HttpFilters?.Invoke(httpContext); - - for (int n = 0; n < _mappings.Length; n++) - { - foreach (HttpMapping httpMapping in _mappings[n]) - { - if ((httpMapping.Method == httpContext.Request?.Method) || (httpMapping.Method == HttpMethod.ANY)) - { - Match match = httpMapping.UriRegex.Match(httpContext.RoutableUri); - if (match.Success) - { - foreach (Group group in match.Groups) - { - httpContext.Request?.SetParameter(group.Name, group.Value); - if (group.Name.Equals("_")) - if (group.Value.StartsWith("/", StringComparison.InvariantCulture)) - residual = group.Value; - else - residual = "/" + group.Value; - } - - string saveRoutableUri = httpContext.RoutableUri; - httpContext.RoutableUri = residual; - if (httpMapping.Route(httpContext)) - return true; - - httpContext.RoutableUri = saveRoutableUri; - } - } - } - - if (httpContext.Response is not null) - break; - } - return false; - } + public void Map(HttpRoute httpRoute) => _routes.Add(httpRoute); + public void UnMap(HttpRoute httpRoute) => _routes.Remove(httpRoute); + public void Map(HttpMethod httpMethod, string route, HttpRouterDelegate routerDelegate) + => Map(new HttpRoute(httpMethod, route, routerDelegate)); - private string RegexFromRoute(string route) + + public bool RouteRequest(HttpRequestContext httpRequestContext, string routePath) + { + HttpFilters?.Invoke(httpRequestContext); + + foreach (var httpRoute in _routes) + { + if (httpRoute.TryRoute(httpRequestContext, routePath)) + { + HttpFixups?.Invoke(httpRequestContext); + return true; + } + } + + return false; + } + + public void Dispose() + { + _parentRouter?.Dispose(); + } + + public override string ToString() => $"[HttpRouter]"; + + /*private string RegexFromRoute(string route) { string[] parts = route.Split(new char[] { '/' }); string[] reparts = parts.Select((part) => @@ -115,84 +81,14 @@ namespace ln.http return string.Format("(?<{0}>[^/]+)", part.Substring(1)); else if (part.Equals("*")) return string.Format("(?<_>.*)"); + else if (part.EndsWith("*", StringComparison.InvariantCulture)) + return string.Format("{0}[^/]+(?<_>/.*)?", part.Substring(0, part.Length-1)); else return string.Format("{0}", part); }).ToArray(); return string.Format("^{0}/?$", string.Join("/", reparts)); - } + }*/ - public class HttpMapping - { - public HttpMethod Method { get; set; } - public Regex UriRegex { get; set; } - public HttpRouterDelegate RouterDelegate { get; set; } - - public bool AuthenticationRequired { get; set; } - public HttpAuthorizationDelegate AuthorizationDelegate { get; set; } - - public event HttpFilterDelegate HttpFilters; - - public HttpMapping(HttpMethod httpMethod, string reUri, HttpRouterDelegate routerDelegate) - { - Method = httpMethod; - UriRegex = new Regex(reUri); - RouterDelegate = routerDelegate; - AuthenticationRequired = HttpServer.DefaultRouteAuthentication; - } - - public HttpMapping UseFilter(HttpFilterDelegate filterDelegate) - { - HttpFilters += filterDelegate; - return this; - } - - public HttpMapping Authenticate() - { - AuthenticationRequired = true; - return this; - } - public HttpMapping Anonymous() - { - AuthenticationRequired = false; - return this; - } - - public HttpMapping Authorize(HttpAuthorizationDelegate authorizationDelegate) - { - AuthorizationDelegate = authorizationDelegate; - return this; - } - - - - - public bool Route(HttpContext httpContext) - { - if (AuthenticationRequired && httpContext.AuthenticatedPrincipal is null) - { - httpContext.Response = HttpResponse.Unauthorized().Header("WWW-Authenticate", "Basic"); - return true; - } - - if ((AuthorizationDelegate is not null) && (!AuthorizationDelegate(httpContext))) - return false; - - HttpFilters?.Invoke(httpContext); - - return RouterDelegate(httpContext); - } - } - - public void Dispose() - { - _httpServer?.RemoveRouter(this); - _httpServer = null; - - _parentRouter?.RemoveHttpMapping(_parentMapping); - _parentMapping = null; - _parentRouter = null; - - } } } diff --git a/ln.http/HttpServer.cs b/ln.http/HttpServer.cs index 8f779c6..6ea748d 100644 --- a/ln.http/HttpServer.cs +++ b/ln.http/HttpServer.cs @@ -3,13 +3,13 @@ using System.Collections.Generic; using ln.logging; using ln.http.exceptions; using System.IO; -using System.Net.Http; using System.Text; using System.Threading; -using ln.protocols.helper; namespace ln.http { + + /* public class HttpServer { public static bool DefaultRouteAuthentication { get; set; } = false; @@ -17,19 +17,22 @@ namespace ln.http private HashSet _routerDelegates = new HashSet(); public IEnumerable Routers => _routerDelegates; - public TextWriter LoggingWriter { get; set; } + public TextWriter AccessLogWriter { get; set; } + private ILogWriter _logWriter; private HashSet _authenticationSources = new HashSet(); public IEnumerable AuthenticationSources => _authenticationSources; - public HttpServer() : this(Console.Out) + public HttpServer() : this(Console.Out, new LogWriter(new ConsoleLogSink())) { } - public HttpServer(TextWriter loggingWriter) + + public HttpServer(TextWriter accessLogWriter, ILogWriter logWriter) { - LoggingWriter = loggingWriter; + AccessLogWriter = accessLogWriter; + _logWriter = logWriter; } public HttpServer(HttpRouterDelegate router) : this() @@ -42,10 +45,10 @@ namespace ln.http public void UnregisterAuthenticationSource(IHttpAuthenticationSource authenticationSource) => _authenticationSources.Remove(authenticationSource); - public void AddRouter(HttpRouter httpRouter) => AddRouter(httpRouter.Route); + public void AddRouter(HttpRouter httpRouter) => AddRouter(httpRouter.RouteRequest); public void AddRouter(HttpRouterDelegate routerDelegate) => _routerDelegates.Add(routerDelegate); - public void RemoveRouter(HttpRouter httpRouter) => RemoveRouter(httpRouter.Route); + public void RemoveRouter(HttpRouter httpRouter) => RemoveRouter(httpRouter.RouteRequest); public void RemoveRouter(HttpRouterDelegate routerDelegate) => _routerDelegates.Remove(routerDelegate); public void Connection(HttpConnection httpConnection) => @@ -74,7 +77,7 @@ namespace ln.http } catch (Exception exception) { - Logging.Log(exception); + _logWriter?.Log(exception); if (exception is HttpException httpException) { @@ -83,8 +86,8 @@ namespace ln.http HttpContext errorContext = new HttpContext(httpContext, httpException); if (!RouteRequest(errorContext)) { - errorContext.RoutableUri = String.Format("/_err.html", - (int)httpException.HttpStatusCode); + errorContext.ResetRoutingStack(String.Format("/_err.html", + (int)httpException.HttpStatusCode)); RouteRequest(errorContext); } httpContext.Response = errorContext.Response; @@ -118,15 +121,17 @@ namespace ln.http DateTime end = DateTime.Now; TimeSpan duration = end - start; - LoggingWriter.WriteLine("{0} {1} {2} {3} {4} {5} {6}", + AccessLogWriter?.WriteLine("{0} {1} {2} {3} {4} {5} {6} {7}", start, end, duration, + httpConnection.RemoteEndPoint?.ToString(), httpContext.Response?.StatusCode.ToString() ?? "-", httpContext.AuthenticatedPrincipal?.ToString() ?? "-", httpContext.Request.Method, httpContext.Request.RequestUri ); + AccessLogWriter?.Flush(); keepalive = httpContext.Response.GetHeader("connection", "keep-alive").Equals("keep-alive") && httpRequest @@ -170,9 +175,10 @@ namespace ln.http return null; } catch (Exception e) { - Logging.Log(e); + _logWriter?.Log(e); return null; } } } +*/ } diff --git a/ln.http/HttpVersion.cs b/ln.http/HttpVersion.cs new file mode 100644 index 0000000..1fdcb4b --- /dev/null +++ b/ln.http/HttpVersion.cs @@ -0,0 +1,37 @@ +using System; + +namespace ln.http; + +[Flags] +public enum HttpVersion +{ + None = 0, + HTTP10 = (1<<0), // HTTP/1.0 + HTTP11 = (1<<1), // HTTP/1.1 + HTTP2 = (1<<2), // HTTP/2.0 + + ALL = HTTP10 | HTTP11 | HTTP2 +} + +public static class HttpVersionSupport +{ + + public static string ToString(HttpVersion httpVersion) + { + switch (httpVersion) + { + case HttpVersion.None: + return "None"; + case HttpVersion.ALL: + return "ALL"; + case HttpVersion.HTTP2: + return "HTTP/2"; + case HttpVersion.HTTP10: + return "HTTP/1.0"; + case HttpVersion.HTTP11: + return "HTTP/1.1"; + default: + throw new NotSupportedException(); + } + } +} \ No newline at end of file diff --git a/ln.http/HttpsListener.cs b/ln.http/HttpsListener.cs deleted file mode 100644 index 11d6b82..0000000 --- a/ln.http/HttpsListener.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Security; -using System.Net.Sockets; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using System.Threading; - -namespace ln.http -{ - public class HttpsListener : IDisposable - { - public static int DefaultPort = 443; - public string CertificateStore = Path.Combine(AppContext.BaseDirectory, "certs"); - - - private IPEndPoint _localEndPoint; - private Socket _socket; - private HttpServer _httpServer; - - private X509Certificate _defaultCertificate; - private Dictionary _certificateCache = new Dictionary(); - - public HttpsListener(HttpServer httpServer) : this(httpServer, DefaultPort) - { - } - - public HttpsListener(HttpServer httpServer, int port) - { - _httpServer = httpServer; - _localEndPoint = new IPEndPoint(IPAddress.IPv6Any, port); - - Initialize(); - } - - private void Initialize() - { - _socket = new Socket(SocketType.Stream, ProtocolType.Tcp); - _socket.ExclusiveAddressUse = false; - _socket.Bind(_localEndPoint); - _socket.Listen(); - - if (File.Exists("localhost.crt")) - _defaultCertificate = X509Certificate.CreateFromCertFile("localhost.crt"); - else - _defaultCertificate = buildSelfSignedServerCertificate(); - - ThreadPool.QueueUserWorkItem((state )=> ListenerThread()); - } - - private void ListenerThread() - { - while (_socket?.IsBound ?? false) - { - try - { - Socket clientSocket = _socket.Accept(); - InitializeTLS(clientSocket); - } - catch - { - throw; - } - } - } - - private void InitializeTLS(Socket clientSocket) - { - SslStream sslStream = new SslStream(new NetworkStream(clientSocket), false, null, CertificateSelectionCallback); - sslStream.AuthenticateAsServer(_defaultCertificate, false, false); - - _httpServer.Connection( - new HttpConnection( - _localEndPoint, - (IPEndPoint)clientSocket.RemoteEndPoint, - sslStream - ) - ); - - } - - private X509Certificate CertificateSelectionCallback(object sender, string targethost, - X509CertificateCollection localcertificates, X509Certificate? remotecertificate, string[] acceptableissuers) - { - Console.Error.WriteLine("Certificate Selection for: {0}", targethost); - if (_certificateCache.TryGetValue(targethost, out X509Certificate localCertificate)) - { - return localCertificate; - } - else if (File.Exists(Path.Combine(CertificateStore ?? ".", String.Format("{0}.crt", targethost)))) - { - localCertificate = X509Certificate.CreateFromCertFile( - Path.Combine(CertificateStore ?? ".", String.Format("{0}.crt", targethost)) - ); - _certificateCache.Add(targethost, localCertificate); - return localCertificate; - } - - return _defaultCertificate; - } - - private X509Certificate2 buildSelfSignedServerCertificate() - { - SubjectAlternativeNameBuilder sanBuilder = new SubjectAlternativeNameBuilder(); - sanBuilder.AddIpAddress(IPAddress.Loopback); - sanBuilder.AddIpAddress(IPAddress.IPv6Loopback); - sanBuilder.AddDnsName("localhost"); - sanBuilder.AddDnsName(Environment.MachineName); - - X500DistinguishedName distinguishedName = new X500DistinguishedName($"CN=localhost"); - - using (RSA rsa = RSA.Create(4096)) - { - var request = new CertificateRequest(distinguishedName, rsa, HashAlgorithmName.SHA256,RSASignaturePadding.Pkcs1); - request.CertificateExtensions.Add( - new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature , false)); - request.CertificateExtensions.Add( - new X509EnhancedKeyUsageExtension( - new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false)); - request.CertificateExtensions.Add(sanBuilder.Build()); - - var certificate= request.CreateSelfSigned(new DateTimeOffset(DateTime.UtcNow.AddDays(-1)), new DateTimeOffset(DateTime.UtcNow.AddDays(3650))); - //certificate.FriendlyName = "localhost"; - using (FileStream fs = new FileStream("localhost.crt", FileMode.Create, FileAccess.Write)) - fs.Write(certificate.Export(X509ContentType.Pfx)); - - return certificate; - } - } - - public void Dispose() - { - _socket?.Close(); - _socket?.Dispose(); - _socket = null; - } - } -} \ No newline at end of file diff --git a/ln.http/IHttpAuthenticationSource.cs b/ln.http/IHttpAuthenticationSource.cs index 99bb6c9..f5ede16 100644 --- a/ln.http/IHttpAuthenticationSource.cs +++ b/ln.http/IHttpAuthenticationSource.cs @@ -2,6 +2,6 @@ namespace ln.http { public interface IHttpAuthenticationSource { - bool AuthenticatePrincipal(HttpContext httpContext, out HttpPrincipal httpPrincipal); + bool AuthenticatePrincipal(HttpRequestContext requestContext, out HttpPrincipal httpPrincipal); } } \ No newline at end of file diff --git a/ln.http/Listener.cs b/ln.http/Listener.cs new file mode 100644 index 0000000..e091991 --- /dev/null +++ b/ln.http/Listener.cs @@ -0,0 +1,127 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using ln.http.exceptions; +using ln.threading; + +namespace ln.http; + +public class Listener : IDisposable +{ + static string _http2_preface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"; + static string _http2_preface1 = "PRI * HTTP/2.0"; + + protected Socket Socket; + + public IPEndPoint LocalEndpoint => (IPEndPoint)Socket.LocalEndPoint; + public HttpVersion AcceptedHttpVersion { get; set; } = HttpVersion.ALL; + + public HttpRouter HttpRouter { get; set; } + + public Listener(HttpRouter httpRouter) + : this(httpRouter, new IPEndPoint(IPAddress.IPv6Any, 0)) + { + } + public Listener(HttpRouter httpRouter, int port) + : this(httpRouter, new IPEndPoint(IPAddress.IPv6Any, port)) + { + } + + public Listener(HttpRouter httpRouter, IPAddress bind, int port) + : this(httpRouter, new IPEndPoint(bind, port)) + { + } + + public Listener(HttpRouter httpRouter, IPEndPoint bind) + { + HttpRouter = httpRouter; + + Socket = new Socket(bind?.AddressFamily ?? AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp); + Socket.ExclusiveAddressUse = false; + + if (bind is null) + bind = new IPEndPoint(IPAddress.IPv6Any, 0); + + Socket.Bind(bind); + Socket.Listen(); + Socket.ReceiveTimeout = 10000; + + DynamicThreadPool.DefaultPool.Enqueue(ConnectionHandler); + } + + private void ConnectionHandler() + { + Socket clientSocket = Socket.Accept(); + if (Socket.IsBound) + DynamicThreadPool.DefaultPool.Enqueue(ConnectionHandler); + + clientSocket.ReceiveTimeout = 10000; + + try + { + using (NetworkStream networkStream = new NetworkStream(clientSocket)) + Accepted(clientSocket, networkStream); + } + finally + { + clientSocket.Close(); + clientSocket.Dispose(); + } + } + + protected virtual void Accepted(Socket connectedSocket, Stream connectionStream) + { + if (HttpConnection.ReadRequestLine(connectionStream, out string method, out Uri requestUri, out HttpVersion httpVersion)) + { + if ((AcceptedHttpVersion & httpVersion) == HttpVersion.None) + return; + + HttpConnection httpConnection; + + switch (httpVersion) + { + case HttpVersion.HTTP2: + httpConnection = new Http2Connection(this, (IPEndPoint) connectedSocket.RemoteEndPoint, connectionStream, method, requestUri, httpVersion); + break; + case HttpVersion.HTTP10: + case HttpVersion.HTTP11: + default: + httpConnection = new Http1XConnection(this, (IPEndPoint) connectedSocket.RemoteEndPoint, connectionStream, method, requestUri, httpVersion); + break; + } + + Accepted(httpConnection); + } + + + } + + protected virtual void Accepted(HttpConnection httpConnection) + { + httpConnection.Run(); + } + + public void Dispatch(HttpRequestContext requestContext) + { + try + { + if (!HttpRouter.RouteRequest(requestContext, requestContext.Request.RequestUri.AbsolutePath)) + throw new NotFoundException(); + } + catch (HttpException httpException) + { + requestContext.Response = new HttpResponse(httpException.HttpStatusCode); + } + catch (Exception e) + { + requestContext.Response = HttpResponse.InternalServerError().Content(e); + } + } + + public virtual void Dispose() + { + Socket?.Dispose(); + } + +} \ No newline at end of file diff --git a/ln.http/MapAttribute.cs b/ln.http/MapAttribute.cs index d4a74a0..70df232 100644 --- a/ln.http/MapAttribute.cs +++ b/ln.http/MapAttribute.cs @@ -2,7 +2,7 @@ using System; namespace ln.http { - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)] public class MapAttribute : Attribute { public string Path { get; set; } diff --git a/ln.http/TlsListener.cs b/ln.http/TlsListener.cs new file mode 100644 index 0000000..87f8aa2 --- /dev/null +++ b/ln.http/TlsListener.cs @@ -0,0 +1,79 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Threading; + +namespace ln.http; + +public class TlsListener : Listener +{ + private CertificateStore _certificateStore; + public X509Certificate DefaultCertificate { get; set; } + + public TlsListener(HttpRouter httpRouter, CertificateStore certificateStore) + :this(httpRouter, null, certificateStore){} + public TlsListener(HttpRouter httpRouter, IPAddress bind, int port, CertificateStore certificateStore) + :this(httpRouter, new IPEndPoint(bind, port), certificateStore){} + + public TlsListener(HttpRouter httpRouter, IPEndPoint bind, CertificateStore certificateStore) + : this(httpRouter, bind, certificateStore, null) + { + } + public TlsListener(HttpRouter httpRouter, IPEndPoint bind, CertificateStore certificateStore, X509Certificate defaultCertificate) :base(httpRouter, bind) + { + _certificateStore = certificateStore; + DefaultCertificate = defaultCertificate; + + if (DefaultCertificate is null) + DefaultCertificate = buildSelfSignedServerCertificate(); + + } + + protected override void Accepted(Socket connectedSocket, Stream connectionStream) + { + SslStream sslStream = new SslStream(connectionStream, false, null, CertificateSelectionCallback); + sslStream.AuthenticateAsServer(DefaultCertificate); + + // ToDo: Check for correct ALPN protocol identifier + + base.Accepted(connectedSocket, sslStream); + } + + + private X509Certificate CertificateSelectionCallback(object sender, string targethost, + X509CertificateCollection localcertificates, X509Certificate? remotecertificate, string[] acceptableissuers) + { + if (!_certificateStore.TryGetCertificate(targethost, out X509Certificate certificate)) + return null; + + return certificate; + } + + private X509Certificate2 buildSelfSignedServerCertificate() + { + SubjectAlternativeNameBuilder sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddIpAddress(LocalEndpoint.Address); + sanBuilder.AddDnsName(Environment.MachineName); + + X500DistinguishedName distinguishedName = new X500DistinguishedName($"CN={LocalEndpoint.Address.ToString()}"); + + using (RSA rsa = RSA.Create(4096)) + { + var request = new CertificateRequest(distinguishedName, rsa, HashAlgorithmName.SHA256,RSASignaturePadding.Pkcs1); + request.CertificateExtensions.Add( + new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature , false)); + request.CertificateExtensions.Add( + new X509EnhancedKeyUsageExtension( + new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false)); + request.CertificateExtensions.Add(sanBuilder.Build()); + + var certificate= request.CreateSelfSigned(new DateTimeOffset(DateTime.UtcNow.AddDays(-1)), new DateTimeOffset(DateTime.UtcNow.AddDays(3650))); + return certificate; + } + } + +} \ No newline at end of file diff --git a/ln.http/VirtualHostRouter.cs b/ln.http/VirtualHostRouter.cs new file mode 100644 index 0000000..171f76e --- /dev/null +++ b/ln.http/VirtualHostRouter.cs @@ -0,0 +1,49 @@ +using ln.http.exceptions; +using System.Collections.Generic; +using System.Net.Http; + +namespace ln.http.router +{ + public class VirtualHostRouter : HttpRouter + { + public HttpRouter DefaultRoute { get; set; } + + Dictionary virtualHosts = new Dictionary(); + + public VirtualHostRouter() + { + } + public VirtualHostRouter(HttpRouter defaultRoute) + { + DefaultRoute = defaultRoute; + } + public VirtualHostRouter(IEnumerable> routes) + { + foreach (KeyValuePair route in routes) + virtualHosts.Add(route.Key, route.Value); + } + public VirtualHostRouter(HttpRouter defaultRoute,IEnumerable> routes) + :this(routes) + { + DefaultRoute = defaultRoute; + } + public VirtualHostRouter(VirtualHostRouter source) + : this(source.virtualHosts) { } + + public void Add(string hostname,HttpRouter 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 HttpRouter router) => virtualHosts.TryGetValue(hostname, out router); + + public bool Route(HttpRequestContext requestContext, string routePath) + { + if (virtualHosts.TryGetValue(requestContext.Request.Host, out HttpRouter virtualHost)) + return virtualHost.RouteRequest(requestContext, routePath); + if (DefaultRoute != null) + return DefaultRoute.RouteRequest(requestContext, routePath); + + throw new HttpException(HttpStatusCode.Gone, string.Format("Gone. Hostname {0} not found on this server.", requestContext.Request.Host)); + } + } +} diff --git a/ln.http/client/HttpClientRequest.cs b/ln.http/client/HttpClientRequest.cs index 71903ed..b50be1b 100644 --- a/ln.http/client/HttpClientRequest.cs +++ b/ln.http/client/HttpClientRequest.cs @@ -3,7 +3,6 @@ using ln.type; using System.IO; using System.Net.Sockets; using System.Net.Security; -using ln.protocols.helper; namespace ln.http.client { diff --git a/ln.http/ln.http.csproj b/ln.http/ln.http.csproj index 40723ed..7dd5243 100644 --- a/ln.http/ln.http.csproj +++ b/ln.http/ln.http.csproj @@ -9,21 +9,17 @@ (c) 2020 Harald Wolff-Thobaben http server default - 0.6.5 + 0.9.5 0.6.2.0 net5.0;net6.0 - - - - - - - - - + + + + + diff --git a/ln.http/mime/MimeTypeMap.cs b/ln.http/mime/MimeTypeMap.cs index 7bddbfc..c75f732 100644 --- a/ln.http/mime/MimeTypeMap.cs +++ b/ln.http/mime/MimeTypeMap.cs @@ -1,767 +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; - } - } - } -} +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/ln.http/route/HttpRoute.cs b/ln.http/route/HttpRoute.cs new file mode 100644 index 0000000..e9ae64b --- /dev/null +++ b/ln.http/route/HttpRoute.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text.RegularExpressions; + +namespace ln.http.route; + +public class HttpRoute +{ + protected HttpRouterDelegate _routerDelegate; + + public HttpMethod HttpMethod { get; protected set; } + public Regex Route { get; protected set; } + + public HttpRoute(HttpMethod httpMethod, string route, HttpRouterDelegate routerDelegate) + :this(httpMethod, route) + { + _routerDelegate = routerDelegate; + } + + protected HttpRoute(HttpMethod httpMethod, string route) + { + HttpMethod = httpMethod; + Route = route is string ? new Regex(route) : null; + } + + public bool TryRoute(HttpRequestContext httpRequestContext, string routePath) + { + bool matched = false; + + if ( + (HttpMethod == HttpMethod.ANY) || + (HttpMethod == httpRequestContext.Request.Method) + ) + { + if (Route is null) + { + matched = true; + } + else + { + Match match = Route.Match(routePath); + if (match.Success) + { + foreach (Group group in match.Groups.Values) + if (group.Success) + httpRequestContext.SetParameter(group.Name, group.Value); + + routePath = routePath.Substring(match.Length); + matched = true; + } + } + } + + if (matched) + return _routerDelegate(httpRequestContext, routePath) && (httpRequestContext.Response is not null); + + return false; + } + + public override string ToString() => $"[HttpRoute HttpMethod={HttpMethod} Route={Route}]"; +} \ No newline at end of file diff --git a/ln.http/router/FileRequestRouter.cs b/ln.http/router/FileRequestRouter.cs deleted file mode 100644 index cfedbb5..0000000 --- a/ln.http/router/FileRequestRouter.cs +++ /dev/null @@ -1,35 +0,0 @@ -// /** -// * 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.mime; - -namespace ln.http.router -{ - public class FileRouter - { - public string FileName { get; set; } - - public FileRouter(string filename) - { - if (!File.Exists(filename)) - throw new FileNotFoundException(); - - FileName = filename; - } - - public bool Route(HttpContext httpContext) - { - httpContext.Response = new HttpResponse(httpContext.Request, new FileStream(FileName, FileMode.Open)); - httpContext.Response.SetHeader("content-type", MimeTypeMap.GetMimeType(Path.GetExtension(FileName))); - return true; - } - } - -} diff --git a/ln.http/router/LoggingRequestRouter.cs b/ln.http/router/LoggingRequestRouter.cs deleted file mode 100644 index 7b9726e..0000000 --- a/ln.http/router/LoggingRequestRouter.cs +++ /dev/null @@ -1,60 +0,0 @@ -// /** -// * 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 - { - HttpRouterDelegate Next { get; set; } - Logger Logger { get; } - - public LoggingRouter(HttpRouterDelegate next) - :this(next, Logger.Default) - {} - public LoggingRouter(HttpRouterDelegate next,Logger logger) - { - Next = next; - Logger = logger; - } - - public bool Route(HttpContext httpContext) - { - DateTime start = DateTime.Now; - bool success = false; - - try - { - success = Next(httpContext); - } - catch (Exception e) - { - throw; - } - finally - { - DateTime end = DateTime.Now; - TimeSpan duration = end - start; - - Logger.Log(LogLevel.INFO, "{0} {1} {2} {3} {4} {5} {6}", - start, - end, - duration, - httpContext.Response?.StatusCode.ToString() ?? "-", - httpContext.AuthenticatedPrincipal?.ToString() ?? "-", - httpContext.Request.Method, - httpContext.Request.RequestUri - ); - } - return success; - } - } -} diff --git a/ln.http/router/StaticRouter.cs b/ln.http/router/StaticRouter.cs deleted file mode 100644 index 532344b..0000000 --- a/ln.http/router/StaticRouter.cs +++ /dev/null @@ -1,97 +0,0 @@ -// /** -// * 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 System.Collections.Generic; -using ln.http.mime; - -namespace ln.http.router -{ - public class StaticRouter : IDisposable - { - private string _rootPath; - public String RootPath - { - get => _rootPath; - private set - { - _rootPath = Path.GetFullPath(value); - } - } - - List indexNames = new List(); - public String[] IndexNames => indexNames.ToArray(); - - private HttpServer _httpServer; - - - public StaticRouter(HttpServer httpServer) - { - _httpServer = httpServer; - httpServer.AddRouter(this.Route); - } - - public StaticRouter(HttpServer httpServer, string path) - :this(path) - { - _httpServer = httpServer; - httpServer.AddRouter(this.Route); - } - public StaticRouter(string path) - { - if (!Directory.Exists(path)) - throw new FileNotFoundException(); - - RootPath = path; - - AddIndex("index.html"); - AddIndex("index.htm"); - } - - public void AddIndex(string indexName) => indexNames.Add(indexName); - public void RemoveIndex(string indexName) => indexNames.Remove(indexName); - - public bool Route(HttpContext httpContext) - { - string finalPath = httpContext.RoutableUri.Length > 0 ? Path.Combine(RootPath, httpContext.RoutableUri.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) - { - httpContext.Response = new HttpResponse(httpContext.Request, new FileStream(finalPath, FileMode.Open, FileAccess.Read)); - httpContext.Response.SetHeader("content-type", MimeTypeMap.GetMimeType(Path.GetExtension(finalPath))); - return true; - } - } - return false; - } - - public void Dispose() - { - _httpServer?.RemoveRouter(this.Route); - _httpServer = null; - } - } - -} diff --git a/ln.http/router/VirtualHostRouter.cs b/ln.http/router/VirtualHostRouter.cs deleted file mode 100644 index 689cecc..0000000 --- a/ln.http/router/VirtualHostRouter.cs +++ /dev/null @@ -1,49 +0,0 @@ -using ln.http.exceptions; -using System.Collections.Generic; -using System.Net.Http; - -namespace ln.http.router -{ - public class VirtualHostRouter - { - public HttpRouterDelegate DefaultRoute { get; set; } - - Dictionary virtualHosts = new Dictionary(); - - public VirtualHostRouter() - { - } - public VirtualHostRouter(HttpRouterDelegate defaultRoute) - { - DefaultRoute = defaultRoute; - } - public VirtualHostRouter(IEnumerable> routes) - { - foreach (KeyValuePair route in routes) - virtualHosts.Add(route.Key, route.Value); - } - public VirtualHostRouter(HttpRouterDelegate defaultRoute,IEnumerable> routes) - :this(routes) - { - DefaultRoute = defaultRoute; - } - public VirtualHostRouter(VirtualHostRouter source) - : this(source.virtualHosts) { } - - public void Add(string hostname,HttpRouterDelegate 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 HttpRouterDelegate router) => virtualHosts.TryGetValue(hostname, out router); - - public bool Route(HttpContext httpContext) - { - if (virtualHosts.TryGetValue(httpContext.Request.Host, out HttpRouterDelegate virtualHost)) - return virtualHost(httpContext); - if (DefaultRoute != null) - return DefaultRoute(httpContext); - - throw new HttpException(HttpStatusCode.Gone, string.Format("Gone. Hostname {0} not found on this server.", httpContext.Request.Host)); - } - } -} diff --git a/ln.http/router/WebsocketRequestRouter.cs b/ln.http/router/WebsocketRequestRouter.cs index 820b4dd..78fcd20 100644 --- a/ln.http/router/WebsocketRequestRouter.cs +++ b/ln.http/router/WebsocketRequestRouter.cs @@ -4,6 +4,7 @@ using ln.http.exceptions; using ln.logging; namespace ln.http.router { + /* public class WebsocketRouter { Func createWebsocket; @@ -24,9 +25,10 @@ namespace ln.http.router } catch (Exception e) { - Logging.Log(e); + Console.Error.WriteLine(e); } throw new DisposeConnectionException(); } } +*/ } diff --git a/ln.http/websocket/WebSocket.cs b/ln.http/websocket/WebSocket.cs index fa3b754..7928d8d 100644 --- a/ln.http/websocket/WebSocket.cs +++ b/ln.http/websocket/WebSocket.cs @@ -29,47 +29,61 @@ namespace ln.http.websocket ERROR } - public delegate void WebSocketEventDelegate(WebSocket sender,WebSocketEventArgs e); + public delegate void WebSocketStateChanged(WebSocket webSocket,WebSocketState newState); + public delegate void WebSocketTextReceived(WebSocket webSocket, string text); + public delegate void WebSocketBytesReceived(WebSocket webSocket, byte[] bytes); - public abstract class WebSocket + public class WebSocket { - public HttpServer HTTPServer => HttpRequest.Server; - public HttpRequest HttpRequest { get; } + public event WebSocketTextReceived TextReceived; + public event WebSocketBytesReceived BytesReceived; + public event WebSocketStateChanged StateChanged; + + public HttpRequestContext RequestContext { get; } public Stream Stream { get; } - public WebSocketState State { get; private set; } = WebSocketState.HANDSHAKE; - - public WebSocket(HttpRequest httpRequest) + private WebSocketState _state = WebSocketState.HANDSHAKE; + public WebSocketState State { - HttpRequest = httpRequest; - Stream = httpRequest.ConnectionStream; + get => _state; + private set + { + _state = value; + StateChanged?.Invoke(this, _state); + } + } - if ((!httpRequest.GetRequestHeader("upgrade", "").Contains("websocket")) && (!httpRequest.GetRequestHeader("connection", "").Contains("Upgrade"))) + public WebSocket(HttpRequestContext requestContext) + { + RequestContext = requestContext; + Stream = requestContext.ConnectionStream; + + if ((!requestContext.Request.GetRequestHeader("upgrade", "").Contains("websocket")) && (!requestContext.Request.GetRequestHeader("connection", "").Contains("Upgrade"))) throw new HttpException(HttpStatusCode.BadRequest, "This resource is a websocket endpoint only"); - if (!httpRequest.GetRequestHeader("Sec-WebSocket-Version", "").Equals("13")) + if (!requestContext.Request.GetRequestHeader("Sec-WebSocket-Version", "").Equals("13")) throw new HttpException(HttpStatusCode.BadRequest, "Unsupported Protocol Version (WebSocket)"); - String wsKey = httpRequest.GetRequestHeader("Sec-WebSocket-Key"); + String wsKey = requestContext.Request.GetRequestHeader("Sec-WebSocket-Key"); HttpResponse httpResponse = new HttpResponse(HttpStatusCode.SwitchingProtocols); 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")); + string acceptKey = String.Format("{0}258EAFA5-E914-47DA-95CA-C5AB0DC85B11", requestContext.Request.GetRequestHeader("Sec-WebSocket-Key")); httpResponse.AddHeader( "Sec-Websocket-Accept", Convert.ToBase64String(SHA1.Create().ComputeHash(Encoding.ASCII.GetBytes(acceptKey))) ); - httpResponse.WriteTo(Stream); + requestContext.Response = httpResponse; + + requestContext.HttpConnection.SendResponse(requestContext); State = WebSocketState.OPEN; } - public bool IsAlive => false; - public void Close() { switch (State) @@ -103,10 +117,10 @@ namespace ln.http.websocket switch (webSocketFrame.Opcode) { case WebSocketOpcode.TEXT: - Received(Encoding.UTF8.GetString(webSocketFrame.ApplicationData)); + _received(Encoding.UTF8.GetString(webSocketFrame.ApplicationData)); break; case WebSocketOpcode.BINARY: - Received(webSocketFrame.ApplicationData); + _received(webSocketFrame.ApplicationData); break; case WebSocketOpcode.CLOSE: if (State == WebSocketState.OPEN) @@ -133,29 +147,18 @@ namespace ln.http.websocket } 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); + Console.Error.WriteLine("WebSocket: Exception: {0}", e.ToString()); + Console.Error.WriteLine(" {0}", e.StackTrace.ToString()); } finally { + Stream.Close(); } } - 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) + private void Send(WebSocketFrame frame) { lock (Stream) { @@ -168,7 +171,6 @@ namespace ln.http.websocket { if (State != WebSocketState.ERROR) { - Logging.Log(LogLevel.ERROR, "WebSocket.Send(): Websocket connection was dropped unexpected"); State = WebSocketState.ERROR; Close(); } @@ -178,19 +180,23 @@ namespace ln.http.websocket throw new IOException("WebSocket is not open"); } } - - public void Send(string textMessage) + + private void _received(string textMessage) { - WebSocketFrame webSocketFrame = new WebSocketFrame(textMessage); - Send(webSocketFrame); + TextReceived?.Invoke(this, textMessage); + Received(textMessage); } - public void Send(byte[] binaryMessage) + private void _received(byte[] bytes) { - WebSocketFrame webSocketFrame = new WebSocketFrame(binaryMessage); - Send(webSocketFrame); + BytesReceived?.Invoke(this, bytes); + Received(bytes); } - + public virtual void Received(string text){} + public virtual void Received(byte[] bytes){} + + public void Send(string textMessage) => Send(new WebSocketFrame(textMessage)); + public void Send(byte[] binaryMessage) => Send(new WebSocketFrame(binaryMessage)); } } diff --git a/ln.http/websocket/WebSocketEventArgs.cs b/ln.http/websocket/WebSocketEventArgs.cs index 8cd452c..ab31e80 100644 --- a/ln.http/websocket/WebSocketEventArgs.cs +++ b/ln.http/websocket/WebSocketEventArgs.cs @@ -10,16 +10,16 @@ namespace ln.http.websocket public WebSocketEventType EventType { get; } public byte[] BinaryMessage { get; } - public String TextMessage => Encoding.UTF8.GetString(BinaryMessage); + + private string _textMessage; + public String TextMessage => _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: @@ -28,7 +28,6 @@ namespace ln.http.websocket EventType = WebSocketEventType.MESSAGE; BinaryMessage = frame.ApplicationData; ErrorMessage = null; - break; } } diff --git a/ln.http/websocket/WebSocketResponse.cs b/ln.http/websocket/WebSocketResponse.cs index c19c859..da5b577 100644 --- a/ln.http/websocket/WebSocketResponse.cs +++ b/ln.http/websocket/WebSocketResponse.cs @@ -8,6 +8,8 @@ using ln.type; namespace ln.http.websocket { + + /* public delegate void WebSocketStateChanged(WebSocketResponse sender, WebSocketState newState); public delegate void WebSocketReceivedText(WebSocketResponse sender, string text); public delegate void WebSocketReceivedBytes(WebSocketResponse sender, byte[] bytes); @@ -19,6 +21,7 @@ namespace ln.http.websocket public event WebSocketReceivedBytes OnWebSocketReceivedBytes; public Stream Stream { get; private set; } + public ILogWriter _logWriter; WebSocketState state = WebSocketState.HANDSHAKE; public WebSocketState State { @@ -66,7 +69,7 @@ namespace ln.http.websocket WebSocketFrame webSocketFrame = new WebSocketFrame(Stream); if (webSocketFrame.Opcode != WebSocketOpcode.TEXT) - Logging.Log(LogLevel.DEBUG, "ws.opcode: {0}", webSocketFrame.Opcode); + _logWriter.Log(LogLevel.DEBUG, "ws.opcode: {0}", webSocketFrame.Opcode); switch (webSocketFrame.Opcode) { @@ -100,12 +103,12 @@ namespace ln.http.websocket } catch (IOException) { State = WebSocketState.ERROR; - Logging.Log(LogLevel.DEBUG, "WebSocket connection was dropped unexpected"); + _logWriter.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); + _logWriter.Log(LogLevel.ERROR, "WebSocket caught Exception: {0}", e.ToString()); + _logWriter.Log(e); } finally { State = WebSocketState.CLOSED; @@ -131,7 +134,7 @@ namespace ln.http.websocket { if (State != WebSocketState.ERROR) { - Logging.Log(LogLevel.ERROR, "WebSocket.Send(): Websocket connection was dropped unexpected"); + _logWriter.Log(LogLevel.ERROR, "WebSocket.Send(): Websocket connection was dropped unexpected"); State = WebSocketState.ERROR; Close(); } @@ -180,10 +183,11 @@ namespace ln.http.websocket Convert.ToBase64String(SHA1.Create().ComputeHash(Encoding.ASCII.GetBytes(acceptKey))) ); - base.WriteTo(Stream); + State = WebSocketState.OPEN; } Run(); } } +*/ }