From ccdcf0b8f6670c8ab22ed7e50eb49dc60fb667a0 Mon Sep 17 00:00:00 2001 From: Michael Nolan Date: Sat, 23 Apr 2022 13:25:45 -0500 Subject: [PATCH] updated --- Tesses.WebServer.NetStandard/ServerContext.cs | 75 ++++- .../Tesses.WebServer.NetStandard.csproj | 9 +- Tesses.WebServer.NetStandard/TessesServer.cs | 294 +++++++++++++++++- 3 files changed, 363 insertions(+), 15 deletions(-) diff --git a/Tesses.WebServer.NetStandard/ServerContext.cs b/Tesses.WebServer.NetStandard/ServerContext.cs index feada67..dc8899d 100644 --- a/Tesses.WebServer.NetStandard/ServerContext.cs +++ b/Tesses.WebServer.NetStandard/ServerContext.cs @@ -7,6 +7,9 @@ namespace Tesses.WebServer { public class ServerContext { + /// + /// Method (ex GET, POST, HEAD) + /// public string Method { get; set; } public ServerContext(string method,Stream strm,string path,Dictionary> headers) { @@ -15,6 +18,7 @@ namespace Tesses.WebServer RequestHeaders = headers; ResponseHeaders = new Dictionary>(); QueryParams = new Dictionary>(); + RawUrl=path; StatusCode = 200; // /joel/path/luigi?local=jim&john_surname=connor&demi_surname=lovato&local=tim @@ -24,6 +28,7 @@ namespace Tesses.WebServer if (splitUrl.Length > 0) { UrlPath = splitUrl[0]; + OriginalUrlPath=splitUrl[0]; if (splitUrl.Length == 2) { //local=jim&john_surname=connor&demi_surname=lovato&local=tim @@ -38,6 +43,9 @@ namespace Tesses.WebServer } } + /// + /// Reset query parms (If api sets them) + /// public void ResetQueryParms() { QueryParams.Clear(); @@ -67,14 +75,77 @@ namespace Tesses.WebServer return Server.Address.ToString(); } string q_parm; + /// + /// the /somepath/file?s=42&joel=file relative to Mount + /// + + public string UrlAndQuery {get { + if(!string.IsNullOrWhiteSpace(q_parm)) + { + return UrlPath + "?" + q_parm; + } + return UrlPath; + }} + /// + /// Original Url Path + /// + /// + public string OriginalUrlPath {get; private set;} + /// + /// Original Url path (includes query) + /// + /// + public string RawUrl {get;private set;} + + /// + /// Query parms string only + /// + + public string QueryParamsString {get {return q_parm;}} + /// + /// Server ip + /// + public IPEndPoint Server { get; set; } + /// + /// Client ip + /// + public IPEndPoint Client { get; set; } + /// + /// Host name + /// + /// public string Host { get { return get_host(); } } + /// + /// Url path (can be eet by Moutable) + /// + public string UrlPath { get; set; } + /// + /// Query Params + /// + public Dictionary> QueryParams { get; set; } + /// + /// Request headers + /// + public Dictionary> RequestHeaders { get; set; } + /// + /// Response headers + /// + public Dictionary> ResponseHeaders { get; set; } + /// + /// TCP Stream for http server + /// + public Stream NetworkStream { get; set; } - public int StatusCode { get; internal set; } + /// + /// Status code for resource + /// + + public int StatusCode { get; set; } } -} \ No newline at end of file +} diff --git a/Tesses.WebServer.NetStandard/Tesses.WebServer.NetStandard.csproj b/Tesses.WebServer.NetStandard/Tesses.WebServer.NetStandard.csproj index 795363e..ab2f410 100644 --- a/Tesses.WebServer.NetStandard/Tesses.WebServer.NetStandard.csproj +++ b/Tesses.WebServer.NetStandard/Tesses.WebServer.NetStandard.csproj @@ -5,15 +5,18 @@ Tesses.WebServer Mike Nolan Tesses + 1.0.2.0 + 1.0.2.0 + 1.0.2.0 A TCP Listener HTTP(s) Server MIT HTTP, WebServer, Website https://gitlab.tesses.cf/tesses50/tesses.webserver - - - + + + diff --git a/Tesses.WebServer.NetStandard/TessesServer.cs b/Tesses.WebServer.NetStandard/TessesServer.cs index 86deee9..758a263 100644 --- a/Tesses.WebServer.NetStandard/TessesServer.cs +++ b/Tesses.WebServer.NetStandard/TessesServer.cs @@ -18,6 +18,11 @@ namespace Tesses.WebServer public static class Extensions { + /// + /// Write headers to stream + /// + /// ServerContext + public static async Task WriteHeadersAsync(this ServerContext ctx) { string status_line = $"HTTP/1.1 {ctx.StatusCode} {StatusCodeMap.GetStatusString(ctx.StatusCode)}\r\n"; @@ -33,6 +38,12 @@ namespace Tesses.WebServer var data = Encoding.UTF8.GetBytes(b.ToString()); await ctx.NetworkStream.WriteAsync(data, 0, data.Length); } + /// + /// Send file to client (supports range partial content) + /// + /// ServerContext + /// the file to serve + public static async Task SendFileAsync(this ServerContext ctx, string file) { using (var strm = File.OpenRead(file)) @@ -40,6 +51,12 @@ namespace Tesses.WebServer await ctx.SendStreamAsync( strm, MimeTypesMap.GetMimeType(file)); } } + /// + /// Send exception to client + /// + /// ServerContext + /// the Exception + public static async Task SendExceptionAsync(this ServerContext ctx, Exception ex) { string name = ex.GetType().FullName; @@ -47,14 +64,44 @@ namespace Tesses.WebServer ctx.StatusCode = 500; await ctx.SendTextAsync(j); } + /// + /// Send object as json to client + /// + /// ServerContext + /// an object to serialize with newtonsoft.json public static async Task SendJsonAsync(this ServerContext ctx,object value) { await ctx.SendTextAsync(JsonConvert.SerializeObject(value), "application/json"); } + /// + /// Send text to client + /// + /// ServerContext + /// some text + /// mime type + public static async Task SendTextAsync(this ServerContext ctx, string data, string contentType = "text/html") { await ctx.SendBytesAsync(Encoding.UTF8.GetBytes(data), contentType); } + /// + /// Send redirect + /// + /// ServerContext + /// Url to redirect to + public static async Task SendRedirectAsync(this ServerContext ctx,string url) + { + ctx.StatusCode = 301; + ctx.ResponseHeaders.Add("Cache-Control","no-cache"); + ctx.ResponseHeaders.Add("Location",url); + await ctx.WriteHeadersAsync(); + } + /// + /// Send byte[] to client + /// + /// ServerContext + /// a byte[] array + /// mime type public static async Task SendBytesAsync(this ServerContext ctx, byte[] array, string contentType = "application/octet-stream") { using (var ms = new MemoryStream(array)) @@ -62,11 +109,27 @@ namespace Tesses.WebServer await ctx.SendStreamAsync( ms, contentType); } } - + /// + /// Get first item in Dictionary> based on key + /// + /// the dictionary with list value + /// some key + /// key type + /// value type + /// public static T2 GetFirst(this Dictionary> args,T1 key) { return args[key][0]; } + /// + /// Try to get first item in Dictionary> based on key + /// + /// the dictionary with list value + /// the key to check + /// the value returned + /// key type + /// value type + /// true if found else false if not found public static bool TryGetFirst(this Dictionary> args,T1 key,out T2 value) { List ls; @@ -81,6 +144,14 @@ namespace Tesses.WebServer value = default(T2); return false; } + /// + /// Add item to the Dictionary> with specified key (will create key in dictionary if not exist) + /// + /// the dictionary with list value + /// the key to add or to add to + /// a item + /// key type + /// value type public static void Add(this Dictionary> list,T1 key,T2 item) { if (list.ContainsKey(key)) @@ -94,7 +165,14 @@ namespace Tesses.WebServer list.Add(key, items); } } - + /// + /// Add multiple items to the Dictionary> with specified key (will create key in dictionary if not exist) + /// + /// the dictionary with list value + /// the key to add or to add to + /// IEnumerable + /// key type + /// value type public static void AddRange(this Dictionary> list,T1 key,IEnumerable items) { if (list.ContainsKey(key)) @@ -109,7 +187,13 @@ namespace Tesses.WebServer } } - + /// + /// StringBuilder ends with + /// + /// string builder + /// text to check + /// comparison type + /// true if sb ends with test, false if it does not public static bool EndsWith(this StringBuilder sb, string test, StringComparison comparison) { @@ -120,12 +204,22 @@ namespace Tesses.WebServer return end.Equals(test, comparison); } } + /// + /// returns 404 not found page + /// public class NotFoundServer : Server { + /// + /// 404 not found custom html use "{url}" in your html as url + /// + /// the custom html public NotFoundServer(string html) { _html = html; } + /// + ///404 not found default html + /// public NotFoundServer() { _html = "File {url} not found

404 Not Found

{url}

"; @@ -134,14 +228,20 @@ namespace Tesses.WebServer public override async Task GetAsync(ServerContext ctx) { ctx.StatusCode = 404; - await ctx.SendTextAsync( _html.Replace("{url}", WebUtility.HtmlEncode(ctx.UrlPath))); + await ctx.SendTextAsync( _html.Replace("{url}", WebUtility.HtmlEncode(ctx.OriginalUrlPath))); } } - + /// + /// Serve static files (doesnt allow listing files) + /// public class StaticServer : Server { string _path; IServer _server; + /// + /// construct with path + /// + /// directory for server public StaticServer(string path) { _path = path; @@ -149,6 +249,12 @@ namespace Tesses.WebServer _defaultFileNames = new string[] {"index.html","index.htm","default.html","default.htm" }; } string[] _defaultFileNames; + /// + /// construct with path, custom filenames, and server for not found + /// + /// directory for server + /// like index.html, index.htm, default.html, default.htm + /// 404 not found server public StaticServer(string path,string[] defaultFileNames,IServer notfoundserver) { _path = path; @@ -193,9 +299,72 @@ namespace Tesses.WebServer } } + /// + /// Server where you can change inner server + /// + public class ChangeableServer : Server + { + /// + /// The inner server to change + /// + + public IServer Server {get;set;} + /// + /// Construct with default value + /// + public ChangeableServer() + { + Server=null; + } + /// + /// Construct with server + /// + /// the inner server + public ChangeableServer(IServer svr) + { + Server=svr; + } + + public override async Task BeforeAsync(ServerContext ctx) + { + return await Guaranteed(Server).BeforeAsync(ctx); + } + + public override async Task GetAsync(ServerContext ctx) + { + await Guaranteed(Server).GetAsync(ctx); + } + + public override async Task OptionsAsync(ServerContext ctx) + { + await Guaranteed(Server).OptionsAsync(ctx); + } + + public override async Task OtherAsync(ServerContext ctx) + { + await Guaranteed(Server).OtherAsync(ctx); + } + + public override async Task PostAsync(ServerContext ctx) + { + await Guaranteed(Server).PostAsync(ctx); + } + } + /// + /// Abstract class for server + /// public abstract class Server : IServer { + /// + /// Returns 404 Not found + /// + public static readonly NotFoundServer ServerNull = new NotFoundServer(); + /// + /// You are guarenteed to have a server + /// + /// any server object + /// if null return ServerNull otherwise return svr public IServer Guaranteed(IServer svr) { if(svr != null) @@ -204,31 +373,59 @@ namespace Tesses.WebServer } return ServerNull; } - + /// + /// Put cors header + /// public bool CorsHeader = true; + /// + /// Called on GET Request + /// + /// ServerContext public abstract Task GetAsync(ServerContext ctx); + /// + /// Called on POST Request + /// + /// ServerContext public virtual async Task PostAsync(ServerContext ctx) { ctx.StatusCode = (int)HttpStatusCode.MethodNotAllowed; await ctx.SendTextAsync("Method Not Supported"); } + /// + /// Called on OPTIONS Request + /// + /// ServerContext + public virtual async Task OptionsAsync(ServerContext ctx) { await ctx.WriteHeadersAsync(); ctx.NetworkStream.Close(); } + /// + /// Called on any other Request method + /// + /// ServerContext public virtual async Task OtherAsync(ServerContext ctx) { ctx.StatusCode = (int)HttpStatusCode.MethodNotAllowed; await ctx.SendTextAsync("Method Not Supported"); } + /// + /// Called before request was made + /// + /// ServerContext + /// true to cancel request, false to continue request public virtual async Task BeforeAsync(ServerContext ctx) { return await Task.FromResult(false); } + /// + /// Add cors header + /// + /// Server Context public void AddCors(ServerContext ctx) { if (CorsHeader) @@ -242,7 +439,9 @@ namespace Tesses.WebServer } } } - + /// + /// mount multiple servers at different url paths + /// public sealed class MountableServer : Server { Dictionary _servers = new Dictionary(); @@ -286,10 +485,17 @@ namespace Tesses.WebServer { _servers.Add(url, server); } + /// + /// Unmount a server + /// + /// Url public void Unmount(string url) { _servers.Remove(url); } + /// + /// Unmount all servers + /// public void UnmountAll() { _servers.Clear(); @@ -338,14 +544,49 @@ namespace Tesses.WebServer await v.Value.OtherAsync(ctx); } } + /// + /// Check username and password are correct or if request can be anonymous + /// + /// Username, can and will be "" on first request for resource + /// Password, can and will be "" on first request for resource + /// true for authorized, false for unauthorized public delegate bool Authenticate(string username, string password); + /// + /// Check username and password are correct or if request can be anonymous + /// + /// Server Context + /// Username, can and will be "" on first request for resource + /// Password, can and will be "" on first request for resource + /// true for authorized, false for unauthorized + public delegate bool AuthenticateWithContext(ServerContext context,string username,string password); + /// + /// Protect server with password + /// public class BasicAuthServer : Server { + /// + /// Construct server for user authorization + /// + /// callback for authorization + /// server to protect + /// realm parameter in WWW-Auhenticate Header public BasicAuthServer(Authenticate auth,IServer inner,string realm="SampleRealm") { Authenticate = auth; InnerServer = inner; Realm = realm; + } + /// + /// Construct server for user authorization (With ServerContext in callback) + /// + /// callback for authorization + /// server to protect + /// realm parameter in WWW-Auhenticate Header + public BasicAuthServer(AuthenticateWithContext auth,IServer inner,string realm = "SampleRealm") + { + AuthenticateWithContext=auth; + InnerServer=inner; + Realm = realm; } public override async Task BeforeAsync(ServerContext ctx) { @@ -382,13 +623,29 @@ namespace Tesses.WebServer await Guaranteed(InnerServer).OptionsAsync(ctx); } + /// + /// Server to protect + /// + public IServer InnerServer { get; set; } + /// + /// Authentication callback without ServerContext + /// public Authenticate Authenticate { get; set; } + /// + /// Authentication callback with ServerContext + /// + + public AuthenticateWithContext AuthenticateWithContext {get;set;} + /// + /// Realm parameter in WWW-Authenticate header + /// public string Realm { get; set; } private bool ValidAuth(ServerContext ctx) { string auth; + if(Authenticate == null && AuthenticateWithContext == null) return true; if (ctx.RequestHeaders.TryGetFirst("Authorization", out auth)) { string[] authorization = auth.Split(' '); @@ -398,14 +655,27 @@ namespace Tesses.WebServer { string[] userPass = Encoding.UTF8.GetString(Convert.FromBase64String(authorization[1])).Split(new char[] { ':' },2); //return userPass.Equals($"{config.UserName}:{config.Password}", StringComparison.Ordinal); - return Authenticate(userPass[0], userPass[1]); + if(Authenticate != null) + return Authenticate(userPass[0], userPass[1]); + + if(AuthenticateWithContext != null) + return AuthenticateWithContext(ctx,userPass[0],userPass[2]); + + } + }else{ + if(Authenticate != null) + return Authenticate("", ""); + + if(AuthenticateWithContext != null) + return AuthenticateWithContext(ctx,"",""); + } return false; } private async Task Authorize(ServerContext ctx) { - if (Authenticate == null) + if (Authenticate == null && AuthenticateWithContext == null) return true; if (ValidAuth(ctx)) @@ -491,13 +761,17 @@ namespace Tesses.WebServer public sealed class HttpServerListener { + /// + /// Print urls when running + /// + /// true if verbose, false if not public bool PrintUrls {get;set;} bool https; X509Certificate cert; IServer _server; TcpListener _listener; SslProtocols protocols; - // + public HttpServerListener(IPEndPoint endPoint,IServer server) { _listener = new TcpListener(endPoint);