diff --git a/.vs/Tesses.WebServer/xs/UserPrefs.xml b/.vs/Tesses.WebServer/xs/UserPrefs.xml index c325784..0e52cc4 100644 --- a/.vs/Tesses.WebServer/xs/UserPrefs.xml +++ b/.vs/Tesses.WebServer/xs/UserPrefs.xml @@ -1,22 +1,21 @@  - + - - - + + + - - + + + - - - + - + diff --git a/README.md b/README.md index 42aafeb..b09410f 100644 --- a/README.md +++ b/README.md @@ -7,16 +7,17 @@ Currently Supports - Seekable Video Files (Using Range) - Can Send Json To Client with helper function (uses Newtonsoft.Json) - Cors Header +- HTTPS Support (At least I think It will work) # 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 +- Mount class (So you could use Multiple Apis, And Static Sites If you want) - Basic Auth Class +- Route Class (Just like dajuric/simple-http) +- Host Name Class (like Mount Class but is used for hostnames/ip addresses like tesses.cf, 192.168.0.142, demilovato.com, ebay.com) # 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")) +> Note: Range code, POST code and Route Class 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 9c72f5d..80756ef 100644 --- a/Tesses.WebServer.Console/Program.cs +++ b/Tesses.WebServer.Console/Program.cs @@ -7,16 +7,50 @@ namespace Tesses.WebServer.ConsoleApp { public static void Main(string[] args) { + TestObject some_object = new TestObject(); + RouteServer rserver = new RouteServer(); + rserver.Add("/", async(ctx) => { + await ctx.SendJsonAsync(some_object); + }); + rserver.Add("/page", async(ctx) => { + await ctx.SendTextAsync("Demetria Devonne Lovato 8/20/1992"); + }); + var ip=System.Net.IPAddress.Any; StaticServer static_server = new StaticServer(System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyVideos)); MountableServer mountable = new MountableServer(static_server); mountable.Mount("/api/",new DynamicServer()); - + BasicAuthServer basicAuth = new BasicAuthServer((user, pass) => { return user == "demi" && pass == "password123"; }, rserver, "RouteServer"); //bad pasword I know, This is a sample + mountable.Mount("/api/route/",basicAuth); HttpServerListener s = new HttpServerListener(new System.Net.IPEndPoint(ip, 24240),mountable); + /* + So this sample application + Route Server (Like dajuric/simple-http's routes (uses modified code from that project)) + (In this example It is password protected, Username: "demi", Password: "password123") + I know password123 is a bad password (but its ok for this sample project) + + /api/route/page: shows authors favorite artist and the birthday + /api/route/: shows authors name, birthday, gender + Dynamic Server (native api) + /api/rand: shows how you can use query params + /api/count: counts up every time you go to it + + everything else is files in My Videos + */ + s.ListenAsync(System.Threading.CancellationToken.None).Wait(); } + + public class TestObject + { + public string name => "Mike Nolan"; + public int month => 12; + public int day => 2; + public int year => 2000; + public string gender => "Male"; //duh + } } } diff --git a/Tesses.WebServer/ServerContext.cs b/Tesses.WebServer/ServerContext.cs index 08e6955..feada67 100644 --- a/Tesses.WebServer/ServerContext.cs +++ b/Tesses.WebServer/ServerContext.cs @@ -14,8 +14,7 @@ namespace Tesses.WebServer NetworkStream = strm; RequestHeaders = headers; ResponseHeaders = new Dictionary>(); - var qp = new Dictionary>(); - + QueryParams = new Dictionary>(); StatusCode = 200; // /joel/path/luigi?local=jim&john_surname=connor&demi_surname=lovato&local=tim @@ -29,24 +28,35 @@ 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[] { '&'},StringSplitOptions.RemoveEmptyEntries)) + q_parm = splitUrl[1]; + } + else + { + q_parm = ""; + } + ResetQueryParms(); + } + + } + public void ResetQueryParms() + { + QueryParams.Clear(); + foreach (var item in q_parm.Split(new char[] { '&' }, StringSplitOptions.RemoveEmptyEntries)) + { + //Console.WriteLine(item); + var itemSplit = item.Split(new char[] { '=' }, 2); + if (itemSplit.Length > 0) + { + string key = itemSplit[0]; + string value = ""; + if (itemSplit.Length == 2) { - //Console.WriteLine(item); - var itemSplit = item.Split(new char[] { '=' }, 2); - if(itemSplit.Length > 0) - { - string key = itemSplit[0]; - string value = ""; - if(itemSplit.Length ==2) - { - value = itemSplit[1]; - } - qp.Add(key, value); - } + value = itemSplit[1]; } + QueryParams.Add(key, value); } } - QueryParams = qp; + } private string get_host() { @@ -54,8 +64,11 @@ namespace Tesses.WebServer { return RequestHeaders.GetFirst("Host"); } - return ""; + return Server.Address.ToString(); } + string q_parm; + public IPEndPoint Server { get; set; } + public IPEndPoint Client { get; set; } public string Host { get { return get_host(); } } public string UrlPath { get; set; } public Dictionary> QueryParams { get; set; } diff --git a/Tesses.WebServer/SimpleHttpCode.cs b/Tesses.WebServer/SimpleHttpCode.cs index 5890e84..eed81e4 100644 --- a/Tesses.WebServer/SimpleHttpCode.cs +++ b/Tesses.WebServer/SimpleHttpCode.cs @@ -22,6 +22,11 @@ namespace Tesses.WebServer public static class DajuricSimpleHttpExtensions { + static void Deconstruct(this KeyValuePair tuple, out T1 key, out T2 value) + { + key = tuple.Key; + value = tuple.Value; + } const string BYTES_RANGE_HEADER = "Range"; static bool ParseForm(this ServerContext ctx) @@ -360,5 +365,111 @@ namespace Tesses.WebServer } } + /// + /// Route server, Based on SimpleHTTP (Used most of the Route Source) + /// + public delegate bool ShouldProcessFunc(ServerContext ctx); + public delegate Task HttpActionAsync(ServerContext ctx); + public delegate void HttpAction(ServerContext ctx); + public class RouteServer : Server + { + public List<(ShouldProcessFunc ShouldProcessFunc, HttpActionAsync Action)> Methods = new List<(ShouldProcessFunc ShouldProcessFunc, HttpActionAsync Action)>(); + public override async Task GetAsync(ServerContext ctx) + { + await Process(ctx); + } + public override async Task PostAsync(ServerContext ctx) + { + await Process(ctx); + } + public override async Task OtherAsync(ServerContext ctx) + { + await Process(ctx); + } + + public override async Task OptionsAsync(ServerContext ctx) + { + await Process(ctx); + } + private async Task Process(ServerContext ctx) + { + foreach(var (shouldProcessFunc,action) in Methods) + { + if(!shouldProcessFunc(ctx)) + { + ctx.ResetQueryParms(); + continue; + } + + await action(ctx); + return; + } + } + /// + /// Adds the specified action to the route collection. + /// The order of actions defines the priority. + /// + /// Function defining whether the specified action should be executed or not. + /// Action executed if the specified pattern matches the URL path. + public void Add(ShouldProcessFunc shouldProcess, HttpActionAsync action) + { + Methods.Add((shouldProcess, action)); + } + /// + /// Adds the specified action to the route collection. + /// The order of actions defines the priority. + /// + /// + /// String url + /// + /// Action executed if the specified pattern matches the URL path. + /// HTTP method (GET, POST, DELETE, HEAD). + public void Add(string url,HttpActionAsync action,string method="GET") + { + Add((e) => + { + if (!e.Method.Equals( method,StringComparison.Ordinal)) + return false; + + return e.UrlPath.Equals(url,StringComparison.Ordinal); + }, + action); + } + /// + /// Adds the specified action to the route collection. + /// The order of actions defines the priority. + /// + /// + /// String url + /// + /// Action executed if the specified pattern matches the URL path. + /// HTTP method (GET, POST, DELETE, HEAD). + public void Add(string url, HttpAction action, string method = "GET") + { + Add((e) => + { + if (!e.Method.Equals(method, StringComparison.Ordinal)) + return false; + + return e.UrlPath.Equals(url, StringComparison.Ordinal); + }, + action); + } + /// + /// Adds the specified action to the route collection. + /// The order of actions defines the priority. + /// + /// Function defining whether the specified action should be executed or not. + /// Action executed if the specified pattern matches the URL path. + public void Add(ShouldProcessFunc shouldProcess,HttpAction action) + { + Methods.Add((shouldProcess, (e) => + { + action(e); + return Task.FromResult(true); + } + )); + } + } } diff --git a/Tesses.WebServer/TessesServer.cs b/Tesses.WebServer/TessesServer.cs index 7b8a605..cdab2e4 100644 --- a/Tesses.WebServer/TessesServer.cs +++ b/Tesses.WebServer/TessesServer.cs @@ -9,6 +9,8 @@ using System.Collections.Generic; using System.Linq; using HeyRed.Mime; using Newtonsoft.Json; +using System.Security.Cryptography.X509Certificates; +using System.Net.Security; namespace Tesses.WebServer { @@ -64,6 +66,20 @@ namespace Tesses.WebServer { return args[key][0]; } + public static bool TryGetFirst(this Dictionary> args,T1 key,out T2 value) + { + List ls; + if (args.TryGetValue(key,out ls)) + { + if(ls.Count > 0) + { + value = ls[0]; + return true; + } + } + value = default(T2); + return false; + } public static void Add(this Dictionary> list,T1 key,T2 item) { if (list.ContainsKey(key)) @@ -178,6 +194,16 @@ namespace Tesses.WebServer } public abstract class Server : IServer { + public static readonly NotFoundServer ServerNull = new NotFoundServer(); + public IServer Guaranteed(IServer svr) + { + if(svr != null) + { + return svr; + } + return ServerNull; + } + public bool CorsHeader = true; public abstract Task GetAsync(ServerContext ctx); public virtual async Task PostAsync(ServerContext ctx) @@ -199,17 +225,21 @@ namespace Tesses.WebServer } 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 void AddCors(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"); + + } + } } public sealed class MountableServer : Server @@ -220,21 +250,23 @@ namespace Tesses.WebServer _root = root; } IServer _root; - private KeyValuePair GetFromPath(ServerContext ctx) + private (string Key,IServer Value) GetFromPath(ServerContext ctx) { //bool j = false; foreach(var item in _servers.Reverse()) { if(ctx.UrlPath.StartsWith(item.Key,StringComparison.Ordinal)) { - - - return item; + return (item.Key,Guaranteed(item.Value)); + } + if (ctx.UrlPath == item.Key.TrimEnd('/')) + { + ctx.UrlPath += "/"; + return (item.Key,Guaranteed(item.Value)); } - } - Console.WriteLine("HERE WE ARE"); - return new KeyValuePair("/",_root); + //Console.WriteLine("HERE WE ARE"); + return ("/",Guaranteed(_root)); } /// /// Mount the specified url and server. @@ -305,9 +337,150 @@ namespace Tesses.WebServer await v.Value.OtherAsync(ctx); } } + public delegate bool Authenticate(string username, string password); + public class BasicAuthServer : Server + { + public BasicAuthServer(Authenticate auth,IServer inner,string realm="SampleRealm") + { + Authenticate = auth; + InnerServer = inner; + Realm = realm; + } + public override async Task BeforeAsync(ServerContext ctx) + { + if(await Authorize(ctx)) + { + + return await Guaranteed(InnerServer).BeforeAsync(ctx); + } + return true; + } + public override async Task GetAsync(ServerContext ctx) + { + + await Guaranteed(InnerServer).GetAsync(ctx); + + } + public override async Task PostAsync(ServerContext ctx) + { + + await Guaranteed(InnerServer).PostAsync(ctx); + + } + + public override async Task OtherAsync(ServerContext ctx) + { + + await Guaranteed(InnerServer).OtherAsync(ctx); + + } + public override async Task OptionsAsync(ServerContext ctx) + { + + await Guaranteed(InnerServer).OptionsAsync(ctx); + + } + public IServer InnerServer { get; set; } + public Authenticate Authenticate { get; set; } + public string Realm { get; set; } + + private bool ValidAuth(ServerContext ctx) + { + string auth; + if (ctx.RequestHeaders.TryGetFirst("Authorization", out auth)) + { + string[] authorization = auth.Split(' '); + //authorization_basic + + if (authorization[0] == "Basic") + { + 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]); + } + } + return false; + } + private async Task Authorize(ServerContext ctx) + { + if (Authenticate == null) + return true; + + if (ValidAuth(ctx)) + return true; + + ctx.ResponseHeaders.Add("WWW-Authenticate", $"Basic realm=\"{Realm}\""); + ctx.StatusCode = 401; + await UnauthorizedPage(ctx); + return false; + } + protected virtual async Task UnauthorizedPage(ServerContext ctx) + { + await ctx.SendTextAsync("Unauthorized"); + } + } + public class HostDomainServer : Server + { + public HostDomainServer(IServer alt) + { + Default = alt; + Servers = new Dictionary(); + } + public void Clear() + { + Servers.Clear(); + } + public void Remove(string fqdn_or_ip) + { + Servers.Remove(fqdn_or_ip); + } + public IServer Default { get; set; } + Dictionary Servers; + + public void AddDomain(string fqdn_or_ip,IServer svr) + { + Servers.Add(fqdn_or_ip, svr); + } + public override async Task BeforeAsync(ServerContext ctx) + { + return await GetDomain(ctx).BeforeAsync(ctx); + + } + public override async Task PostAsync(ServerContext ctx) + { + await GetDomain(ctx).PostAsync(ctx); + } + public override async Task OtherAsync(ServerContext ctx) + { + await GetDomain(ctx).OtherAsync(ctx); + } + public override async Task OptionsAsync(ServerContext ctx) + { + await GetDomain(ctx).OptionsAsync(ctx); + } + public override async Task GetAsync(ServerContext ctx) + { + await GetDomain(ctx).GetAsync(ctx); + } + private IServer GetDomain(ServerContext ctx) + { + string fqdn_or_ip = ctx.Host; + foreach(var item in Servers) + { + if(item.Key.Equals(fqdn_or_ip,StringComparison.Ordinal)) + { + return Guaranteed(item.Value); + } + } + return Guaranteed(Default); + + } + + } public interface IServer { + void AddCors(ServerContext ctx); Task BeforeAsync(ServerContext ctx); Task GetAsync(ServerContext ctx); Task PostAsync(ServerContext ctx); @@ -317,14 +490,29 @@ namespace Tesses.WebServer public sealed class HttpServerListener { + bool https; + X509Certificate cert; IServer _server; TcpListener _listener; public HttpServerListener(IPEndPoint endPoint,IServer server) { _listener = new TcpListener(endPoint); _server = server; + https = false; + } + public HttpServerListener(IServer server) + { + _listener = new TcpListener(new IPEndPoint(IPAddress.Any, 3251)); + _server = server; + https = false; + } + public HttpServerListener(IPEndPoint endpoint,IServer server,X509Certificate cert) + { + _listener = new TcpListener(endpoint); + _server = server; + https = cert != null; + this.cert = cert; } - public async Task ListenAsync(CancellationToken token) { _listener.Start(); @@ -332,12 +520,12 @@ namespace Tesses.WebServer while (!token.IsCancellationRequested) { var socket=await _listener.AcceptTcpClientAsync(); - await CommunicateHostAsync(socket); + await CommunicateHostAsync(socket).ConfigureAwait(false); } } } - private string ReadHeaders(NetworkStream strm) + private string ReadHeaders(Stream strm) { StringBuilder s = new StringBuilder(); @@ -375,6 +563,25 @@ namespace Tesses.WebServer } return items; } + public Stream GetStream(TcpClient clt) + { + if(https) + { + SslStream sslStream = new SslStream( + clt.GetStream(), false); + try + { + sslStream.AuthenticateAsServer(cert, clientCertificateRequired: false, checkCertificateRevocation: true); + + } + catch (Exception ex) + { + _ = ex; + } + return sslStream; + } + return clt.GetStream(); + } private async Task CommunicateHostAsync(TcpClient clt) { // HTTP/1.1\r\n @@ -387,7 +594,7 @@ namespace Tesses.WebServer //RESPONSE - using (NetworkStream strm = clt.GetStream()) + using (Stream strm = GetStream(clt)) { string request_line = ""; @@ -403,6 +610,9 @@ namespace Tesses.WebServer string path = request[1]; string ver = request[2]; ctx = new ServerContext(method, strm, path, headers); + ctx.Server = clt.Client.LocalEndPoint as IPEndPoint; + ctx.Client = clt.Client.RemoteEndPoint as IPEndPoint; + _server.AddCors(ctx); if (!await _server.BeforeAsync(ctx)) { switch (method)