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 found404 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);