From d9e66a1427544a5251029cf56f59440cb2af9000 Mon Sep 17 00:00:00 2001 From: Michael Nolan Date: Sat, 2 Apr 2022 16:15:20 -0500 Subject: [PATCH] First version --- .vs/Tesses.WebServer/xs/UserPrefs.xml | 18 +- README.md | 22 ++ Tesses.WebServer.Console/Program.cs | 14 +- Tesses.WebServer.Console/Server.cs | 85 ++++ .../Tesses.WebServer.Console.csproj | 1 + Tesses.WebServer/ServerContext.cs | 9 +- Tesses.WebServer/SimpleHttpCode.cs | 364 ++++++++++++++++++ Tesses.WebServer/Tesses.WebServer.csproj | 3 +- .../{MyClass.cs => TessesServer.cs} | 217 ++++++++--- 9 files changed, 654 insertions(+), 79 deletions(-) create mode 100644 README.md create mode 100644 Tesses.WebServer.Console/Server.cs create mode 100644 Tesses.WebServer/SimpleHttpCode.cs rename Tesses.WebServer/{MyClass.cs => TessesServer.cs} (63%) diff --git a/.vs/Tesses.WebServer/xs/UserPrefs.xml b/.vs/Tesses.WebServer/xs/UserPrefs.xml index b722524..c325784 100644 --- a/.vs/Tesses.WebServer/xs/UserPrefs.xml +++ b/.vs/Tesses.WebServer/xs/UserPrefs.xml @@ -1,19 +1,23 @@  - + - - - - + + + + + + - + + + + - diff --git a/README.md b/README.md new file mode 100644 index 0000000..42aafeb --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# Tesses.WebServer + +A TcpListener HTTP Server + +Currently Supports +- GET+HEAD+POST Requests +- Seekable Video Files (Using Range) +- Can Send Json To Client with helper function (uses Newtonsoft.Json) +- Cors Header + +# Classes To Make It Easier +- Static Website Class (Can pass in other class (instead of 404 when file doesnt exist) can choose other names other than index.html, index.htm, default.html, default.htm) +- 404 Not Found Class +- Mount class (So you could use Api) + +# Comming Soon Hopefully +- Basic Auth Class + +# Might Happen But not sure +- WebDav Class + +> Note: Range code and POST code is not mine its a modified version of the code from ( [dajuric/simple-http](https://github.com/dajuric/simple-http/blob/master/Source/SimpleHTTP/Extensions/Response/ResponseExtensions.PartialStream.cs "dajuric/simple-http")) diff --git a/Tesses.WebServer.Console/Program.cs b/Tesses.WebServer.Console/Program.cs index c347068..9c72f5d 100644 --- a/Tesses.WebServer.Console/Program.cs +++ b/Tesses.WebServer.Console/Program.cs @@ -1,4 +1,6 @@ -namespace Tesses.WebServer.ConsoleApp +using Tesses; +using Tesses.WebServer; +namespace Tesses.WebServer.ConsoleApp { class MainClass @@ -6,8 +8,14 @@ public static void Main(string[] args) { var ip=System.Net.IPAddress.Any; - StaticServer server = new StaticServer("/home/ddlovato/Videos/"); - HttpServerListener s = new HttpServerListener(new System.Net.IPEndPoint(ip, 24240),server); + StaticServer static_server = new StaticServer(System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyVideos)); + MountableServer mountable = new MountableServer(static_server); + + mountable.Mount("/api/",new DynamicServer()); + + + HttpServerListener s = new HttpServerListener(new System.Net.IPEndPoint(ip, 24240),mountable); + s.ListenAsync(System.Threading.CancellationToken.None).Wait(); } } diff --git a/Tesses.WebServer.Console/Server.cs b/Tesses.WebServer.Console/Server.cs new file mode 100644 index 0000000..30be29d --- /dev/null +++ b/Tesses.WebServer.Console/Server.cs @@ -0,0 +1,85 @@ +using System; +using System.Text; +using System.Threading.Tasks; + +namespace Tesses.WebServer.ConsoleApp +{ + public class DynamicServer : Server + { + public DynamicServer() + { + + } + Random rand = new Random(); + int count = 0; + public override async Task GetAsync(ServerContext ctx) + { + //Console.WriteLine("HANDLE"); + if(ctx.UrlPath=="/count") + { + count++; + await ctx.SendTextAsync($"This page has been viewed {count} times"); + } + + if(ctx.UrlPath=="/rand") + { + int min = 0; + int max = 65536; + int times = 5; + bool dont_show_hint = false; + if(ctx.QueryParams.ContainsKey("min")) + { + if(!int.TryParse(ctx.QueryParams.GetFirst("min"),out min)) + { + min = 0; + } + else + { + dont_show_hint = true; + } + } + if (ctx.QueryParams.ContainsKey("max")) + { + if (!int.TryParse(ctx.QueryParams.GetFirst("max"), out max)) + { + max = 65536; + } + else + { + dont_show_hint = true; + } + } + if (ctx.QueryParams.ContainsKey("times")) + { + if (!int.TryParse(ctx.QueryParams.GetFirst("times"), out times)) + { + times = 5; + } + else + { + dont_show_hint = true; + } + } + max++; + StringBuilder html = new StringBuilder(); + html.Append("Random Numbers

Random Numbers

"); + + if(!dont_show_hint) + { + string hint = "Hint: ./rand?min=41&max=1992×=42
"; + + html.Append(hint); + } + html.Append(rand.Next(min, max)); + for(int i = 1;i"); + await ctx.SendTextAsync(html.ToString()); + + } + + } + } +} diff --git a/Tesses.WebServer.Console/Tesses.WebServer.Console.csproj b/Tesses.WebServer.Console/Tesses.WebServer.Console.csproj index 95c4aeb..8da4f74 100644 --- a/Tesses.WebServer.Console/Tesses.WebServer.Console.csproj +++ b/Tesses.WebServer.Console/Tesses.WebServer.Console.csproj @@ -40,6 +40,7 @@ + diff --git a/Tesses.WebServer/ServerContext.cs b/Tesses.WebServer/ServerContext.cs index 3847802..08e6955 100644 --- a/Tesses.WebServer/ServerContext.cs +++ b/Tesses.WebServer/ServerContext.cs @@ -15,7 +15,7 @@ namespace Tesses.WebServer RequestHeaders = headers; ResponseHeaders = new Dictionary>(); var qp = new Dictionary>(); - QueryParams = qp; + StatusCode = 200; // /joel/path/luigi?local=jim&john_surname=connor&demi_surname=lovato&local=tim @@ -29,8 +29,9 @@ namespace Tesses.WebServer { //local=jim&john_surname=connor&demi_surname=lovato&local=tim //we want to split on & - foreach(var item in splitUrl[1].Split(new char[] { '&'},2)) + foreach(var item in splitUrl[1].Split(new char[] { '&'},StringSplitOptions.RemoveEmptyEntries)) { + //Console.WriteLine(item); var itemSplit = item.Split(new char[] { '=' }, 2); if(itemSplit.Length > 0) { @@ -40,12 +41,12 @@ namespace Tesses.WebServer { value = itemSplit[1]; } - qp.Add(key, value); //hince qp is reference to QueryParams + qp.Add(key, value); } } } } - + QueryParams = qp; } private string get_host() { diff --git a/Tesses.WebServer/SimpleHttpCode.cs b/Tesses.WebServer/SimpleHttpCode.cs new file mode 100644 index 0000000..5890e84 --- /dev/null +++ b/Tesses.WebServer/SimpleHttpCode.cs @@ -0,0 +1,364 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Tesses.WebServer +{ + //This file contains modified code from https://github.com/dajuric/simple-http + + /// + /// Delegate executed when a file is about to be read from a body stream. + /// + /// Field name. + /// name of the file. + /// Content type. + /// Stream to be populated. + public delegate Stream OnFile(string fieldName, string fileName, string contentType); + + public static class DajuricSimpleHttpExtensions + { + const string BYTES_RANGE_HEADER = "Range"; + + static bool ParseForm(this ServerContext ctx) + { + var args = ctx.QueryParams; + string content_type = ctx.RequestHeaders.GetFirst("Content-Type"); + if (content_type != "application/x-www-form-urlencoded") + return false; + + var str = ctx.BodyAsString(); + if (str == null) + return false; + + foreach (var pair in str.Split('&')) + { + var nameValue = pair.Split('='); + if (nameValue.Length != (1 + 1)) + continue; + + args.Add(nameValue[0], WebUtility.UrlDecode(nameValue[1])); + } + + return true; + } + + static string BodyAsString(this ServerContext ctx) + { + + + string str = null; + using (var reader = new StreamReader(ctx.NetworkStream)) + { + str = reader.ReadToEnd(); + } + + return str; + } + + public static async Task SendStreamAsync(this ServerContext ctx, Stream strm, string contentType = "application/octet-stream") + { + //ctx.StatusCode = 200; + int start = 0, end = (int)strm.Length - 1; + if (ctx.RequestHeaders.ContainsKey(BYTES_RANGE_HEADER)) + { + if (ctx.RequestHeaders[BYTES_RANGE_HEADER].Count > 1) + { + throw new NotSupportedException("Multiple 'Range' headers are not supported."); + } + var range = ctx.RequestHeaders[BYTES_RANGE_HEADER][0].Replace("bytes=", String.Empty) + .Split(new string[] { "-" }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => Int32.Parse(x)) + .ToArray(); + + start = (range.Length > 0) ? range[0] : 0; + end = (range.Length > 1) ? range[1] : (int)(strm.Length - 1); + + + var hdrs = ctx.ResponseHeaders; + hdrs.Add("Accept-Ranges", "bytes"); + hdrs.Add("Content-Range", "bytes " + start + "-" + end + "/" + strm.Length); + ctx.StatusCode = 206; + + + } + ctx.ResponseHeaders.Add("Content-Length", (end - start + 1).ToString()); + ctx.ResponseHeaders.Add("Content-Type", contentType); + + await ctx.WriteHeadersAsync(); + if (!ctx.Method.Equals("HEAD", StringComparison.Ordinal)) + { + try + { + strm.Position = start; + strm.CopyTo(ctx.NetworkStream, Math.Min(8 * 1024 * 1024, end - start + 1)); + } + finally + { + strm.Close(); + ctx.NetworkStream.Close(); + } + } + } + + static Dictionary ParseMultipartForm(ServerContext serverCtx, OnFile onFile) + { + var args = serverCtx.QueryParams; + string content_type=serverCtx.RequestHeaders.GetFirst("Content-Type"); + if (content_type.StartsWith("multipart/form-data",StringComparison.Ordinal) == false) + throw new InvalidDataException("Not 'multipart/form-data'."); + + var boundary = Regex.Match(content_type, "boundary=(.+)").Groups[1].Value; + boundary = "--" + boundary; + + + var files = new Dictionary(); + var inputStream = new BufferedStream(serverCtx.NetworkStream); + + parseUntillBoundaryEnd(inputStream, new MemoryStream(), boundary); + while (true) + { + var (n, v, fn, ct) = parseSection(inputStream, "\r\n" + boundary, onFile); + if (String.IsNullOrEmpty(n)) break; + + v.Position = 0; + if (!String.IsNullOrEmpty(fn)) + files.Add(n, new HttpFile(fn, v, ct)); + else + args.Add(n, readAsString(v)); + } + + return files; + } + + private static (string Name, Stream Value, string FileName, string ContentType) parseSection(Stream source, string boundary, OnFile onFile) + { + var (n, fn, ct) = readContentDisposition(source); + source.ReadByte(); source.ReadByte(); //\r\n (empty row) + + var dst = String.IsNullOrEmpty(fn) ? new MemoryStream() : onFile(n, fn, ct); + if (dst == null) + throw new ArgumentException(nameof(onFile), "The on-file callback must return a stream."); + + parseUntillBoundaryEnd(source, dst, boundary); + + return (n, dst, fn, ct); + } + + private static (string Name, string FileName, string ContentType) readContentDisposition(Stream stream) + { + const string UTF_FNAME = "utf-8''"; + + var l = readLine(stream); + if (String.IsNullOrEmpty(l)) + return (null, null, null); + + //(regex matches are taken from NancyFX) and modified + var n = Regex.Match(l, @"name=""?(?[^\""]*)").Groups["n"].Value; + var f = Regex.Match(l, @"filename\*?=""?(?[^\"";]*)").Groups["f"]?.Value; + + string cType = null; + if (!String.IsNullOrEmpty(f)) + { + if (f.StartsWith(UTF_FNAME)) + f = Uri.UnescapeDataString(f.Substring(UTF_FNAME.Length)); + + l = readLine(stream); + cType = Regex.Match(l, "Content-Type: (?.+)").Groups["cType"].Value; + } + + return (n, f, cType); + } + + private static void parseUntillBoundaryEnd(Stream source, Stream destination, string boundary) + { + var checkBuffer = new byte[boundary.Length]; //for boundary checking + + int b, i = 0; + while ((b = source.ReadByte()) != -1) + { + if (i == boundary.Length) //boundary found -> go to the end of line + { + if (b == '\n') break; + continue; + } + + if (b == boundary[i]) //start filling the check buffer + { + checkBuffer[i] = (byte)b; + i++; + } + else + { + var idx = 0; + while (idx < i) //write the buffer data to stream + { + destination.WriteByte(checkBuffer[idx]); + idx++; + } + + i = 0; + destination.WriteByte((byte)b); //write the current byte + } + } + } + + private static string readLine(Stream stream) + { + var sb = new StringBuilder(); + + int b; + while ((b = stream.ReadByte()) != -1 && b != '\n') + sb.Append((char)b); + + if (sb.Length > 0 && sb[sb.Length - 1] == '\r') + sb.Remove(sb.Length - 1, 1); + + return sb.ToString(); + } + + private static string readAsString(Stream stream) + { + var sb = new StringBuilder(); + + int b; + while ((b = stream.ReadByte()) != -1) + sb.Append((char)b); + + return sb.ToString(); + + } + + /// + /// Parses body of the request including form and multi-part form data. + /// + /// HTTP request. + /// Key-value pairs populated by the form data by this function. + /// Name-file pair collection. + public static Dictionary ParseBody(this ServerContext ctx) + { + return ctx.ParseBody( (n, fn, ct) => new MemoryStream()); + } + + /// + /// Parses body of the request including form and multi-part form data. + /// + /// HTTP request. + /// Key-value pairs populated by the form data by this function. + /// + /// Function called if a file is about to be parsed. The stream is attached to a corresponding . + /// By default, is used, but for large files, it is recommended to open directly. + /// + /// Name-file pair collection. + public static Dictionary ParseBody(this ServerContext request, OnFile onFile) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + + if (!request.RequestHeaders.ContainsKey("Content-Type")) + throw new ArgumentNullException("request.RequestHeaders[\"Content-Type\"]"); + + if (onFile == null) + throw new ArgumentNullException(nameof(onFile)); + + + var files = new Dictionary(); + string content_type = request.RequestHeaders.GetFirst("Content-Type"); + if (content_type.StartsWith("application/x-www-form-urlencoded",StringComparison.Ordinal)) + { + ParseForm(request); + } + else if (content_type.StartsWith("multipart/form-data",StringComparison.Ordinal)) + { + files = ParseMultipartForm(request, onFile); + } + else + throw new NotSupportedException("The body content-type is not supported."); + + return files; + } + } + + /// + /// HTTP file data container. + /// + public class HttpFile : IDisposable + { + /// + /// Creates new HTTP file data container. + /// + /// File name. + /// Data. + /// Content type. + internal HttpFile(string fileName, Stream value, string contentType) + { + Value = value; + FileName = fileName; + ContentType = contentType; + } + + /// + /// Gets the name of the file. + /// + public string FileName { get; private set; } + + /// + /// Gets the data. + /// If a stream is created it will be closed when this HttpFile object is disposed. + /// + public Stream Value { get; private set; } + + /// + /// Content type. + /// + public string ContentType { get; private set; } + + /// + /// Saves the data into a file. + /// Directory path will be auto created if does not exists. + /// + /// File path with name. + /// True to overwrite the existing file, false otherwise. + /// True if the file is saved/overwritten, false otherwise. + public bool Save(string fileName, bool overwrite = false) + { + if (File.Exists(Path.GetFullPath(fileName))) + return false; + + var dir = Path.GetDirectoryName(Path.GetFullPath(fileName)); + Directory.CreateDirectory(dir); + + Value.Position = 0; + using (var outStream = File.OpenWrite(fileName)) + Value.CopyTo(outStream); + + return true; + } + + /// + /// Disposes the current instance. + /// + public void Dispose() + { + if (Value != null) + { + Value?.Dispose(); + Value = null; + } + } + + /// + /// Disposes the current instance. + /// + ~HttpFile() + { + Dispose(); + } + } + +} + diff --git a/Tesses.WebServer/Tesses.WebServer.csproj b/Tesses.WebServer/Tesses.WebServer.csproj index f1b9374..f9afa76 100644 --- a/Tesses.WebServer/Tesses.WebServer.csproj +++ b/Tesses.WebServer/Tesses.WebServer.csproj @@ -36,10 +36,11 @@ - + + diff --git a/Tesses.WebServer/MyClass.cs b/Tesses.WebServer/TessesServer.cs similarity index 63% rename from Tesses.WebServer/MyClass.cs rename to Tesses.WebServer/TessesServer.cs index f6efc12..7b8a605 100644 --- a/Tesses.WebServer/MyClass.cs +++ b/Tesses.WebServer/TessesServer.cs @@ -12,11 +12,10 @@ using Newtonsoft.Json; namespace Tesses.WebServer { -public static class Extensions - { - const string BYTES_RANGE_HEADER = "Range"; - - private static async Task WriteHeadersAsync(this ServerContext ctx) + + public static class Extensions + { + public static async Task WriteHeadersAsync(this ServerContext ctx) { string status_line = $"HTTP/1.1 {ctx.StatusCode} {StatusCodeMap.GetStatusString(ctx.StatusCode)}\r\n"; StringBuilder b = new StringBuilder(status_line); @@ -49,62 +48,18 @@ public static class Extensions { await ctx.SendTextAsync(JsonConvert.SerializeObject(value), "application/json"); } - public static async Task SendTextAsync(this ServerContext ctx, string data, string content_type = "text/html") + public static async Task SendTextAsync(this ServerContext ctx, string data, string contentType = "text/html") { - await ctx.SendBytesAsync(Encoding.UTF8.GetBytes(data), content_type); + await ctx.SendBytesAsync(Encoding.UTF8.GetBytes(data), contentType); } - public static async Task SendBytesAsync(this ServerContext ctx, byte[] array, string content_type = "application/octet-stream") + public static async Task SendBytesAsync(this ServerContext ctx, byte[] array, string contentType = "application/octet-stream") { using (var ms = new MemoryStream(array)) { - await ctx.SendStreamAsync( ms, content_type); + await ctx.SendStreamAsync( ms, contentType); } } - public static async Task SendStreamAsync(this ServerContext ctx, Stream strm, string content_type = "application/octet-stream") - { - //ctx.StatusCode = 200; - int start = 0, end = (int)strm.Length - 1; - if (ctx.RequestHeaders.ContainsKey(BYTES_RANGE_HEADER)) - { - if (ctx.RequestHeaders[BYTES_RANGE_HEADER].Count > 1) - { - throw new NotSupportedException("Multiple 'Range' headers are not supported."); - } - var range = ctx.RequestHeaders[BYTES_RANGE_HEADER][0].Replace("bytes=", String.Empty) - .Split(new string[] { "-" }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => Int32.Parse(x)) - .ToArray(); - - start = (range.Length > 0) ? range[0] : 0; - end = (range.Length > 1) ? range[1] : (int)(strm.Length - 1); - - - var hdrs = ctx.ResponseHeaders; - hdrs.Add("Accept-Ranges", "bytes"); - hdrs.Add("Content-Range", "bytes " + start + "-" + end + "/" + strm.Length); - ctx.StatusCode = 206; - - - } - ctx.ResponseHeaders.Add("Content-Length", (end - start + 1).ToString()); - ctx.ResponseHeaders.Add("Content-Type", content_type); - - await WriteHeadersAsync(ctx); - if (!ctx.Method.Equals("HEAD",StringComparison.Ordinal)) - { - try - { - strm.Position = start; - strm.CopyTo(ctx.NetworkStream, Math.Min(8 * 1024 * 1024, end - start + 1)); - } - finally - { - strm.Close(); - ctx.NetworkStream.Close(); - } - } - } - + public static T2 GetFirst(this Dictionary> args,T1 key) { return args[key][0]; @@ -223,18 +178,141 @@ public static class Extensions } public abstract class Server : IServer { + public bool CorsHeader = true; public abstract Task GetAsync(ServerContext ctx); public virtual async Task PostAsync(ServerContext ctx) { ctx.StatusCode = (int)HttpStatusCode.MethodNotAllowed; await ctx.SendTextAsync("Method Not Supported"); + + } + public virtual async Task OptionsAsync(ServerContext ctx) + { + await ctx.WriteHeadersAsync(); + ctx.NetworkStream.Close(); + } + public virtual async Task OtherAsync(ServerContext ctx) + { + ctx.StatusCode = (int)HttpStatusCode.MethodNotAllowed; + await ctx.SendTextAsync("Method Not Supported"); + + } + public virtual async Task BeforeAsync(ServerContext ctx) + { + if(CorsHeader) + { + + ctx.ResponseHeaders.Add("Access-Control-Allow-Origin", "*"); + ctx.ResponseHeaders.Add("Access-Control-Allow-Headers", "Cache-Control, Pragma, Accept, Origin, Authorization, Content-Type, X-Requested-With"); + ctx.ResponseHeaders.Add("Access-Control-Allow-Methods", "GET, POST"); + ctx.ResponseHeaders.Add("Access-Control-Allow-Credentials", "true"); + + } + return await Task.FromResult(false); + } + } + + public sealed class MountableServer : Server + { + Dictionary _servers = new Dictionary(); + public MountableServer(IServer root) + { + _root = root; + } + IServer _root; + private KeyValuePair GetFromPath(ServerContext ctx) + { + //bool j = false; + foreach(var item in _servers.Reverse()) + { + if(ctx.UrlPath.StartsWith(item.Key,StringComparison.Ordinal)) + { + + + return item; + } + + } + Console.WriteLine("HERE WE ARE"); + return new KeyValuePair("/",_root); + } + /// + /// Mount the specified url and server. + /// Must mount like this + /// /somePath0 + /// /somePath0/someSubPath0 + /// /somePath0/someSubPath0/someSubSubPath0 + /// /somePath0/someSubPath0/someSubSubPath1 + /// /somePath0/someSubPath1 + /// /somePath0/someSubPath1/someSubSubPath0 + /// /somePath0/someSubPath1/someSubSubPath1 + /// + /// URL. + /// Server. + public void Mount(string url,IServer server) + { + _servers.Add(url, server); + } + public void Unmount(string url) + { + _servers.Remove(url); + } + public void UnmountAll() + { + _servers.Clear(); + } + public override async Task GetAsync(ServerContext ctx) + { + var v = GetFromPath(ctx); + string url = '/' + ctx.UrlPath.Substring(v.Key.Length).TrimStart('/'); + ctx.UrlPath = url; + + await v.Value.GetAsync(ctx); + } + public override async Task PostAsync(ServerContext ctx) + { + var v = GetFromPath(ctx); + string url = '/' + ctx.UrlPath.Substring(v.Key.Length).TrimStart('/'); + ctx.UrlPath = url; + + await v.Value.PostAsync(ctx); + } + public override async Task BeforeAsync(ServerContext ctx) + { + var v = GetFromPath(ctx); + string old=ctx.UrlPath; + string url = '/' + ctx.UrlPath.Substring(v.Key.Length).TrimStart('/'); + ctx.UrlPath = url; + + var res=await v.Value.BeforeAsync(ctx); + ctx.UrlPath = old; + return res; + } + public override async Task OptionsAsync(ServerContext ctx) + { + var v = GetFromPath(ctx); + string url = '/' + ctx.UrlPath.Substring(v.Key.Length).TrimStart('/'); + ctx.UrlPath = url; + + await v.Value.OptionsAsync(ctx); + } + public override async Task OtherAsync(ServerContext ctx) + { + var v = GetFromPath(ctx); + string url = '/' + ctx.UrlPath.Substring(v.Key.Length).TrimStart('/'); + ctx.UrlPath = url; + + await v.Value.OtherAsync(ctx); } } public interface IServer { + Task BeforeAsync(ServerContext ctx); Task GetAsync(ServerContext ctx); Task PostAsync(ServerContext ctx); + Task OptionsAsync(ServerContext ctx); + Task OtherAsync(ServerContext ctx); } public sealed class HttpServerListener @@ -322,17 +400,28 @@ public static class Extensions string method = request[0]; try { - switch (method) + string path = request[1]; + string ver = request[2]; + ctx = new ServerContext(method, strm, path, headers); + if (!await _server.BeforeAsync(ctx)) { - case "HEAD": - case "GET": - string path = request[1]; - string ver = request[2]; - ctx = new ServerContext(method,strm, path, headers); - await _server.GetAsync(ctx); - break; - case "POST": - break; + switch (method) + { + case "HEAD": + case "GET": + + await _server.GetAsync(ctx); + break; + case "POST": + await _server.PostAsync(ctx); + break; + case "OPTIONS": + await _server.OptionsAsync(ctx); + break; + default: + await _server.OtherAsync(ctx); + break; + } } }catch(Exception ex) {