diff --git a/README.md b/README.md index d9ee975..dd5761f 100644 --- a/README.md +++ b/README.md @@ -5,25 +5,48 @@ # License Starting with 1.0.3.9 this library will use GPL-3.0 If you can not use GPL either use 1.0.3.8 or use another library -
+ A TcpListener HTTP Server +WARNING: use at least version 1.0.4.2 because of security issue with paths + +To make your life easier, install [Tesses.WebServer.EasyServer](https://www.nuget.org/packages/Tesses.WebServer.EasyServer) alongside [Tesses.WebServer](https://www.nuget.org/packages/Tesses.WebServer) and use this code: + +```csharp +using Tesses.WebServer; +using Tesses.WebServer.EasyServer; + +... + +StaticServer server=new StaticServer(""); //or any server + +... + +server.StartServer(9500); //or any port number +//and it will print your ips to console with +//the message Almost ready to Listen +``` # Currently Supports - GET+HEAD+POST Requests -- Seekable Video Files (Using Range) -- Can Send Json To Client with helper function (uses Newtonsoft.Json) +- Seekable Video Files (or any file) (Using Range) +- Can Send/Receive Json To/From Client with helper functions (uses Newtonsoft.Json) - Cors Header +- Chunked encoding +- Tesses.IVirtualFileSystem support (Work in progress and buggy) +- wii-linux-ngx using this copy of [mono](https://tesses.net/apps/tytd/2022/wii.php) # 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 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) +- Route Class (Just like dajuric/simple-http, except it uses query parameters) +- Host Name Class (like Mount Class but is used for hostnames/ip addresses like tesses.net, 192.168.0.142, demilovato.com, ebay.com, tessesstudios.com, godworldwide.org) +- Path Value Class (can handle paths like this /user/Jehovah/files where Jehovah is the path element) # Might Happen But not sure -- WebDav Class +- WebDav Class (and may be used in a seperate library) +- Reverse Proxy (in a seperate library) > 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.ClientTest/Program.cs b/Tesses.WebServer.ClientTest/Program.cs new file mode 100644 index 0000000..624995b --- /dev/null +++ b/Tesses.WebServer.ClientTest/Program.cs @@ -0,0 +1,19 @@ + +using System.Net.Http.Json; +using System.Text; + +HttpClient client = new HttpClient(); + +HttpRequestMessage requestMessage=new HttpRequestMessage(HttpMethod.Get,"http://localhost:24240/api/route/jsonEndpoint"); +requestMessage.Headers.Add("Authorization",$"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes("demi:password123"))}"); +requestMessage.Content = JsonContent.Create(new Obj()); +var resp=await client.SendAsync(requestMessage); +Console.WriteLine(await resp.Content.ReadAsStringAsync()); + + +public class Obj +{ + public string Name {get;set;}="Demetria Devonne Lovato"; + + public DateTime Birthday {get;set;}=new DateTime(1992,8,20); +} \ No newline at end of file diff --git a/Tesses.WebServer.ClientTest/Tesses.WebServer.ClientTest.csproj b/Tesses.WebServer.ClientTest/Tesses.WebServer.ClientTest.csproj new file mode 100644 index 0000000..206b89a --- /dev/null +++ b/Tesses.WebServer.ClientTest/Tesses.WebServer.ClientTest.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + enable + enable + + + diff --git a/Tesses.WebServer.Console/Program.cs b/Tesses.WebServer.Console/Program.cs index 84f10c5..9419fb4 100644 --- a/Tesses.WebServer.Console/Program.cs +++ b/Tesses.WebServer.Console/Program.cs @@ -2,7 +2,12 @@ using Tesses.WebServer; namespace Tesses.WebServer.ConsoleApp { + class JsonObj + { + public string Name {get;set;}=""; + public DateTime Birthday {get;set;}=DateTime.Now; + } class MainClass { public static void Main(string[] args) @@ -16,6 +21,27 @@ namespace Tesses.WebServer.ConsoleApp await ctx.SendTextAsync("Demetria Devonne Lovato 8/20/1992"); }); + rserver.Add("/jsonEndpoint",async(ctx)=>{ + var res=await ctx.ReadJsonAsync(); + if(res !=null) + { + Console.WriteLine($"Name: {res.Name}"); + Console.WriteLine($"Birthday: {res.Birthday.ToShortDateString()}"); + await ctx.SendTextAsync("The meaning of life is 42","text/plain"); + } + }); + rserver.Add("/typewriter",(ctx)=>{ + using(var sw=ctx.GetResponseStreamWriter("text/plain")) + { + foreach(var c in "This is a typewriter\nwell this is cool\nThis is thanks to the chunked stream.") + { + sw.Write(c); + sw.Flush(); + System.Threading.Thread.Sleep(50); + } + } + }); + var ip=System.Net.IPAddress.Any; StaticServer static_server = new StaticServer(System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyVideos)); MountableServer mountable = new MountableServer(static_server); diff --git a/Tesses.WebServer.Console/Tesses.WebServer.Console.csproj b/Tesses.WebServer.Console/Tesses.WebServer.Console.csproj index aee68de..263c165 100644 --- a/Tesses.WebServer.Console/Tesses.WebServer.Console.csproj +++ b/Tesses.WebServer.Console/Tesses.WebServer.Console.csproj @@ -1,12 +1,12 @@ - - + + Exe - net6.0 + net8.0 enable enable diff --git a/Tesses.WebServer.FileServer/Program.cs b/Tesses.WebServer.FileServer/Program.cs index 32f7e71..4b780d7 100644 --- a/Tesses.WebServer.FileServer/Program.cs +++ b/Tesses.WebServer.FileServer/Program.cs @@ -19,7 +19,7 @@ namespace Tesses.WebServer.ConsoleApp HttpServerListener s = new HttpServerListener(new System.Net.IPEndPoint(ip, 24240),static_server); - + s.ListenAsync(System.Threading.CancellationToken.None).Wait(); } diff --git a/Tesses.WebServer.NetStandard/ServerContext.cs b/Tesses.WebServer.NetStandard/ServerContext.cs index 67f746e..b6bd0d8 100644 --- a/Tesses.WebServer.NetStandard/ServerContext.cs +++ b/Tesses.WebServer.NetStandard/ServerContext.cs @@ -2,6 +2,7 @@ using System.IO; using System; using System.Net; +using System.Text; namespace Tesses.WebServer { @@ -70,6 +71,20 @@ internal class SizedStream : Stream } public class ServerContext { + const string bad_chars = "<>?/\\\"*|:"; + public static string FixFileName(string filename,bool requireAscii=false) + { + StringBuilder builder=new StringBuilder(); + foreach(var c in filename) + { + if(char.IsControl(c)) continue; + if(requireAscii && c > 127) continue; + if(bad_chars.Contains(c.ToString())) continue; + builder.Append(c); + } + + return builder.ToString(); + } /// /// Some user data /// @@ -277,6 +292,10 @@ internal class SizedStream : Stream return new SizedStream(NetworkStream,len); } } + else if(RequestHeaders.TryGetFirst("Transfer-Encoding",out var res) && res == "chunked") + { + return new ChunkedStream(NetworkStream,true); + } //DajuricSimpleHttpExtensions.Print("Returns NetworkStream"); return NetworkStream; } diff --git a/Tesses.WebServer.NetStandard/SimpleHttpCode.cs b/Tesses.WebServer.NetStandard/SimpleHttpCode.cs index da0d5c2..f6199c6 100644 --- a/Tesses.WebServer.NetStandard/SimpleHttpCode.cs +++ b/Tesses.WebServer.NetStandard/SimpleHttpCode.cs @@ -7,6 +7,7 @@ using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; +using HeyRed.Mime; namespace Tesses.WebServer { @@ -82,6 +83,7 @@ namespace Tesses.WebServer { try { + var strm2=new ChunkedStream(ctx.NetworkStream,false); long tread=0; byte[] buffer=new byte[8*1024*1024]; int read=0; @@ -94,7 +96,7 @@ namespace Tesses.WebServer } if(read == 0) break; read = strm.Read(buffer,0,read); - strm.Write(buffer,0,read); + strm2.Write(buffer,0,read); }while(read > 0); } finally { strm.Close(); @@ -105,6 +107,7 @@ namespace Tesses.WebServer { try { + var strm2=new ChunkedStream(ctx.NetworkStream,false); long tread=0; byte[] buffer=new byte[8*1024*1024]; int read=0; @@ -117,7 +120,7 @@ namespace Tesses.WebServer } if(read == 0) break; read = await strm.ReadAsync(buffer,0,read); - await strm.WriteAsync(buffer,0,read); + await strm2.WriteAsync(buffer,0,read); }while(read > 0); } finally { strm.Close(); @@ -151,7 +154,8 @@ namespace Tesses.WebServer } ctx.ResponseHeaders.Add("Content-Length", (end - start + 1).ToString()); - ctx.ResponseHeaders.Add("Content-Type", contentType); + + ctx.WithMimeType(contentType); ctx.WriteHeaders(); if (!ctx.Method.Equals("HEAD", StringComparison.Ordinal)) @@ -214,8 +218,118 @@ namespace Tesses.WebServer } } } + /// + /// Send file to client (supports range partial content) + /// + /// ServerContext + /// the file to serve + + public static async Task SendFileAsync(this ServerContext ctx, string file) + { + if(!File.Exists(file)) + { + await ctx.SendNotFoundAsync(); + return; + } + if(!ctx.RequestHeaders.ContainsKey(BYTES_RANGE_HEADER) && handleIfCached()) + return; + + using(var f = File.OpenRead(file)) + { + await ctx.SendStreamAsync(f,MimeTypesMap.GetMimeType(file)); + } - static Dictionary ParseMultipartForm(ServerContext serverCtx, OnFile onFile) + bool handleIfCached() + { + var lastModified = File.GetLastWriteTimeUtc(file); + string etag=lastModified.Ticks.ToString("x"); + ctx.ResponseHeaders.Add("ETag",etag); + ctx.ResponseHeaders.Add("Last-Modified",lastModified.ToString("R")); + + if(ctx.RequestHeaders.TryGetFirst("If-None-Match",out var ifNoneMatch)) + { + var eTags = ifNoneMatch.Split(',').Select(x => x.Trim()).ToArray(); + if (eTags.Contains(etag)) + { + ctx.StatusCode = 304; + ctx.WriteHeaders(); + + return true; + } + } + + + if(ctx.RequestHeaders.TryGetFirst("If-Modified-Since",out var iMs) && DateTime.TryParse(iMs,out DateTime ifModifiedSince)) + { + if (lastModified <= ifModifiedSince) + { + ctx.StatusCode = 304; + ctx.WriteHeaders(); + + return true; + } + } + + return false; + } + } + /// + /// Send file to client (supports range partial content) + /// + /// ServerContext + /// the file to serve + + public static void SendFile(this ServerContext ctx, string file) + { + if(!File.Exists(file)) + { + ctx.SendNotFound(); + return; + } + if(!ctx.RequestHeaders.ContainsKey(BYTES_RANGE_HEADER) && handleIfCached()) + return; + + using(var f = File.OpenRead(file)) + { + ctx.SendStream(f,MimeTypesMap.GetMimeType(file)); + } + + bool handleIfCached() + { + var lastModified = File.GetLastWriteTimeUtc(file); + string etag=lastModified.Ticks.ToString("x"); + ctx.ResponseHeaders.Add("ETag",etag); + ctx.ResponseHeaders.Add("Last-Modified",lastModified.ToString("R")); + + if(ctx.RequestHeaders.TryGetFirst("If-None-Match",out var ifNoneMatch)) + { + var eTags = ifNoneMatch.Split(',').Select(x => x.Trim()).ToArray(); + if (eTags.Contains(etag)) + { + ctx.StatusCode = 304; + ctx.WriteHeaders(); + + return true; + } + } + + + if(ctx.RequestHeaders.TryGetFirst("If-Modified-Since",out var iMs) && DateTime.TryParse(iMs,out DateTime ifModifiedSince)) + { + if (lastModified <= ifModifiedSince) + { + ctx.StatusCode = 304; + ctx.WriteHeaders(); + + return true; + } + } + + return false; + } + } + + static Dictionary> ParseMultipartForm(ServerContext serverCtx, OnFile onFile) { var args = serverCtx.QueryParams; string content_type=serverCtx.RequestHeaders.GetFirst("Content-Type"); @@ -226,7 +340,7 @@ namespace Tesses.WebServer boundary = "--" + boundary; - var files = new Dictionary(); + var files = new Dictionary>(); var inputStream = new BufferedStream(serverCtx.GetRequestStream()); //Print("Before ParseUntillBoundaryEnd"); @@ -362,7 +476,7 @@ namespace Tesses.WebServer } /// - /// Parses body of the request including form and multi-part form data. + /// Parses body of the request including form and multi-part form data, allowing multiple file with same key. /// /// HTTP request. /// Key-value pairs populated by the form data by this function. @@ -371,7 +485,7 @@ namespace Tesses.WebServer /// 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) + public static Dictionary> ParseBodyMultiple(this ServerContext request, OnFile onFile) { if (request == null) throw new ArgumentNullException(nameof(request)); @@ -383,21 +497,119 @@ namespace Tesses.WebServer 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); + var files = new Dictionary>(); + return files; } else if (content_type.StartsWith("multipart/form-data",StringComparison.Ordinal)) { - files = ParseMultipartForm(request, onFile); + return ParseMultipartForm(request, onFile); } else throw new NotSupportedException("The body content-type is not supported."); + + } + /// + /// 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) + { + var res=ParseBodyMultiple(request,onFile); + Dictionary files = new Dictionary(); + foreach(var item in res) + { + if(item.Value.Count > 0) + { + files.Add(item.Key,item.Value[0]); + } + } return files; } + /// + /// Parses body of the request including form and multi-part form data, allowing multiple file with same key and storing the files in a temp directory specified by the user. + /// + /// HTTP request. + /// The root directory to store all the uploads in + /// A HttpFileResponse Containing Paths to files, Dispose only deletes the files, so if you want to keep the files don't dispose it. + public static HttpFileResponse ParseBodyWithTempDirectory(this ServerContext request, string tempDir) + { + Directory.CreateDirectory(tempDir); + + + Stream Open(string field, string filename, string contentType) + { + string dir=Path.Combine(tempDir,ServerContext.FixFileName(field)); + Directory.CreateDirectory(dir); + string filename2 = Path.Combine(dir,ServerContext.FixFileName(filename)); + return File.Create(filename2); + } + List responseEntries=new List(); + foreach(var item in ParseBodyMultiple(request,Open)) + { + foreach(var i2 in item.Value) + { + responseEntries.Add(new HttpFileResponseEntry(Path.Combine(tempDir,ServerContext.FixFileName(item.Key),ServerContext.FixFileName(i2.FileName)),i2.FileName,item.Key,i2.ContentType)); + i2.Dispose(); + } + } + return new HttpFileResponse(tempDir,responseEntries); + } + } + + public sealed class HttpFileResponseEntry + { + public HttpFileResponseEntry(string path, string filename, string fieldname, string contype) + { + Path = path; + FileName = filename; + ContentType = contype; + FieldName = fieldname; + } + public string FileName {get;} + + public string ContentType {get;} + + public string Path {get;} + + public string FieldName {get;} + + public FileInfo FileInfo => new FileInfo(Path); + + public Stream OpenRead() + { + return File.OpenRead(Path); + } + public void MoveTo(string dest) + { + File.Move(Path,dest); + } + } + public sealed class HttpFileResponse : IDisposable + { + public HttpFileResponse(string dir, IReadOnlyList entries) + { + Directory = dir; + Files = entries; + } + public IReadOnlyList Files {get;} + public string Directory {get;} + + public void Dispose() + { + System.IO.Directory.Delete(Directory,true); + } } /// diff --git a/Tesses.WebServer.NetStandard/Tesses.WebServer.NetStandard.csproj b/Tesses.WebServer.NetStandard/Tesses.WebServer.NetStandard.csproj index 4642ce6..61b9c2e 100644 --- a/Tesses.WebServer.NetStandard/Tesses.WebServer.NetStandard.csproj +++ b/Tesses.WebServer.NetStandard/Tesses.WebServer.NetStandard.csproj @@ -5,14 +5,14 @@ Tesses.WebServer Mike Nolan Tesses - 1.0.4.1 - 1.0.4.1 - 1.0.4.1 + 1.0.4.2 + 1.0.4.2 + 1.0.4.2 A TCP Listener HTTP(s) Server GPL-3.0-only true HTTP, WebServer, Website - https://gitlab.tesses.cf/tesses50/tesses.webserver + https://gitlab.tesses.net/tesses50/tesses.webserver diff --git a/Tesses.WebServer.NetStandard/TessesServer.cs b/Tesses.WebServer.NetStandard/TessesServer.cs index 7120e23..277831c 100644 --- a/Tesses.WebServer.NetStandard/TessesServer.cs +++ b/Tesses.WebServer.NetStandard/TessesServer.cs @@ -14,6 +14,7 @@ using System.Net.Security; using System.Security.Authentication; using System.Web; using Tesses.VirtualFilesystem; +using System.Net.Mime; namespace Tesses.WebServer { @@ -42,6 +43,58 @@ namespace Tesses.WebServer public static class Extensions { + public static ServerContext WithStatusCode(this ServerContext ctx, HttpStatusCode statusCode) + { + ctx.StatusCode = (int)statusCode; + return ctx; + } + public static void SendNotFound(this ServerContext ctx) + { + ctx.StatusCode=404; + string url=WebUtility.HtmlEncode(ctx.OriginalUrlPath); + ctx.SendText($"File {url} not found

404 Not Found

{url}

"); + } + public static async Task SendNotFoundAsync(this ServerContext ctx) + { + ctx.StatusCode=404; + string url=WebUtility.HtmlEncode(ctx.OriginalUrlPath); + await ctx.SendTextAsync($"File {url} not found

404 Not Found

{url}

"); + } + public static ServerContext WithDate(this ServerContext ctx, DateTime dateTime) + { + ctx.ResponseHeaders.Add("Date",dateTime.ToString("R")); + return ctx; + } + public static ServerContext WithLastModified(this ServerContext ctx, DateTime dateTime) + { + ctx.ResponseHeaders.Add("Last-Modified",dateTime.ToString("R")); + return ctx; + } + public static ServerContext WithMimeType(this ServerContext ctx, string mimeType) + { + if(ctx.ResponseHeaders.ContainsKey("Content-Type")) + { + ctx.ResponseHeaders["Content-Type"].Clear(); + ctx.ResponseHeaders["Content-Type"].Add(mimeType); + } + else + { + ctx.ResponseHeaders.Add("Content-Type",mimeType); + } + return ctx; + } + public static ServerContext WithMimeTypeFromFileName(this ServerContext ctx, string filename) + { + return ctx.WithMimeType(HeyRed.Mime.MimeTypesMap.GetMimeType(filename)); + } + public static ServerContext WithFileName(this ServerContext ctx, string filename, bool inline) + { + ContentDisposition disposition=new ContentDisposition(); + disposition.FileName = filename; + disposition.Inline = inline; + ctx.ResponseHeaders.Add("Content-Disposition",disposition.ToString()); + return ctx; + } public static async Task WriteAsync(this Stream strm,string text) { var data=Encoding.UTF8.GetBytes(text); @@ -82,13 +135,36 @@ namespace Tesses.WebServer public static async Task ReadStringAsync(this ServerContext ctx) { string str = null; - using (var reader = new StreamReader(ctx.GetRequestStream())) + using (var reader = ctx.GetRequestStreamReader()) { str = await reader.ReadToEndAsync(); } return str; } + public static StreamReader GetRequestStreamReader(this ServerContext ctx) + { + return new StreamReader(ctx.GetRequestStream(),Encoding.UTF8,false,4096,false); + } + public static StreamWriter GetResponseStreamWriter(this ServerContext ctx,string mimetype) + { + return ctx.WithMimeType(mimetype).GetResponseStreamWriter(); + } + public static StreamWriter GetResponseStreamWriter(this ServerContext ctx) + { + return new StreamWriter(ctx.GetResponseStream(),Encoding.UTF8,4096,false); + } + public static Stream GetResponseStream(this ServerContext ctx) + { + ctx.ResponseHeaders.Add("Transfer-Encoding","chunked"); + ctx.WriteHeaders(); + return new ChunkedStream(ctx.NetworkStream,false); + } + public static Stream GetResponseStream(this ServerContext ctx, string mimetype) + { + return ctx.WithMimeType(mimetype).GetResponseStream(); + + } /// /// Read string from request body /// @@ -97,7 +173,7 @@ namespace Tesses.WebServer public static string ReadString(this ServerContext ctx) { string str = null; - using (var reader = new StreamReader(ctx.GetRequestStream())) + using (var reader = ctx.GetRequestStreamReader()) { str = reader.ReadToEnd(); } @@ -214,7 +290,7 @@ namespace Tesses.WebServer } return filename; } - + /// /// Write headers to stream /// @@ -255,32 +331,7 @@ namespace Tesses.WebServer var data = Encoding.UTF8.GetBytes(b.ToString()); ctx.NetworkStream.Write(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)) - { - await ctx.SendStreamAsync( strm, MimeTypesMap.GetMimeType(file)); - } - } - /// - /// Send file to client (supports range partial content) - /// - /// ServerContext - /// the file to serve - - public static void SendFile(this ServerContext ctx, string file) - { - using (var strm = File.OpenRead(file)) - { - ctx.SendStream( strm, MimeTypesMap.GetMimeType(file)); - } - } + /// /// Send exception to client /// @@ -290,7 +341,7 @@ namespace Tesses.WebServer public static async Task SendExceptionAsync(this ServerContext ctx, Exception ex) { string name = ex.GetType().FullName; - string j = $"{WebUtility.HtmlEncode(name)} thrown

{WebUtility.HtmlEncode(name)} thrown

Description: {WebUtility.HtmlEncode(ex.Message)}

"; + string j = $"{WebUtility.HtmlEncode(name)} thrown

{WebUtility.HtmlEncode(name)} thrown

Description: {WebUtility.HtmlEncode(ex.ToString())}

"; ctx.StatusCode = 500; await ctx.SendTextAsync(j); } @@ -303,7 +354,7 @@ namespace Tesses.WebServer public static void SendException(this ServerContext ctx, Exception ex) { string name = ex.GetType().FullName; - string j = $"{WebUtility.HtmlEncode(name)} thrown

{WebUtility.HtmlEncode(name)} thrown

Description: {WebUtility.HtmlEncode(ex.Message)}

"; + string j = $"{WebUtility.HtmlEncode(name)} thrown

{WebUtility.HtmlEncode(name)} thrown

Description: {WebUtility.HtmlEncode(ex.ToString())}

"; ctx.StatusCode = 500; ctx.SendText(j); } @@ -512,6 +563,8 @@ namespace Tesses.WebServer { } + + } public class SameServer : Server @@ -534,6 +587,7 @@ namespace Tesses.WebServer ctx.StatusCode = _statusCode; await ctx.SendTextAsync( _html.Replace("{url}", WebUtility.HtmlEncode(ctx.OriginalUrlPath))); } + } public enum WebServerPathType @@ -693,9 +747,23 @@ namespace Tesses.WebServer name = ""; return false; } + //fix a nasty security risk, that allows people to access parent files in filesystem + private string FixDotPath(string url) + { + List strs=new List(); + foreach(var item in url.Split(new char[]{'/'}, StringSplitOptions.RemoveEmptyEntries)) + { + if(item != ".." && item != ".") + { + strs.Add(item); + } + } + return string.Join("/",strs); + } public WebServerPathEntry GetPath(string url) { - string someUrl = Path.Combine(_path,WebUtility.UrlDecode(url.Substring(1)).Replace('/', Path.DirectorySeparatorChar)); + + string someUrl = Path.Combine(_path,WebUtility.UrlDecode(FixDotPath(url)).Replace('/', Path.DirectorySeparatorChar)); //Console.WriteLine(someUrl); if (Directory.Exists(someUrl)) { @@ -967,12 +1035,13 @@ namespace Tesses.WebServer _forbidden = new SameServer("File {url} not accessable

403 Forbidden

{url}

",403); } + public bool RedirectToRootInsteadOfNotFound {get;set;}=false; /// /// construct with path and with option to allow/deny listing directories /// /// directory for server /// whether to allow listing directory or not (overridable by environment variable with TESSES_WEBSERVER_ALLOW_LISTING=true|false) - public bool RedirectToRootInsteadOfNotFound {get;set;}=false; + public StaticServer(string path,bool allowListing) { _server = new NotFoundServer(); @@ -1240,6 +1309,124 @@ namespace Tesses.WebServer } } /// + /// use values inside path like this /hello/YOUR_VALUE/somepage + /// + public class PathValueServer : Server + { + public IServer Server {get;set;} + int paths; + /// + /// Construct a PathValueServer with NotFoundServer + /// + /// How many path parts do you want, eg if your path is /jim/john/joel, 1: jim 2: jim/john 3: jim/john/joel + public PathValueServer(int paths=1) + { + Server = new NotFoundServer(); + this.paths=paths; + } + /// + /// Construct a PathValueServer with your server + /// + /// Your server + /// How many path parts do you want, eg if your path is /jim/john/joel, 1: jim 2: jim/john 3: jim/john/joel + public PathValueServer(IServer inner,int paths=1) + { + + Server = inner; + this.paths = paths; + } + /// + /// Use this inside your inner server to get string value + /// + /// the ServerContext + /// the string value from the url part + public string GetValue(ServerContext ctx) + { + lock(kvps) + { + if(kvps.ContainsKey(ctx)) + { + return kvps[ctx]; + } + } + return ""; + } + private void SetValue(ServerContext ctx,string val) + { + lock(kvps) + { + if(kvps.ContainsKey(ctx)) + { + kvps[ctx] = val; + } + else + { + kvps.Add(ctx,val); + } + } + } + + private void RemoveValue(ServerContext ctx) + { + lock(kvps) + { + kvps.Remove(ctx); + } + } + + Dictionary kvps=new Dictionary(); + public override async Task GetAsync(ServerContext ctx) + { + string[] path=ctx.UrlPath.Split(new char[]{'/'},System.StringSplitOptions.RemoveEmptyEntries); + SetValue(ctx,string.Join("/",path.Take(paths))); + ctx.UrlPath = $"/{string.Join("/",path.Skip(paths))}"; + + await Server.GetAsync(ctx); + RemoveValue(ctx); + } + public override async Task OptionsAsync(ServerContext ctx) + { + string[] path=ctx.UrlPath.Split(new char[]{'/'},System.StringSplitOptions.RemoveEmptyEntries); + SetValue(ctx,string.Join("/",path.Take(paths))); + ctx.UrlPath = $"/{string.Join("/",path.Skip(paths))}"; + + await Server.OptionsAsync(ctx); + RemoveValue(ctx); + + } + public override async Task OtherAsync(ServerContext ctx) + { + string[] path=ctx.UrlPath.Split(new char[]{'/'},System.StringSplitOptions.RemoveEmptyEntries); + SetValue(ctx,string.Join("/",path.Take(paths))); + ctx.UrlPath = $"/{string.Join("/",path.Skip(paths))}"; + + await Server.OtherAsync(ctx); + RemoveValue(ctx); + + } + public override async Task PostAsync(ServerContext ctx) + { + string[] path=ctx.UrlPath.Split(new char[]{'/'},System.StringSplitOptions.RemoveEmptyEntries); + SetValue(ctx,string.Join("/",path.Take(paths))); + ctx.UrlPath = $"/{string.Join("/",path.Skip(paths))}"; + + await Server.PostAsync(ctx); + RemoveValue(ctx); + + } + public override async Task BeforeAsync(ServerContext ctx) + { + var urlPath = ctx.UrlPath; + string[] path=ctx.UrlPath.Split(new char[]{'/'},System.StringSplitOptions.RemoveEmptyEntries); + SetValue(ctx,string.Join("/",path.Take(paths))); + ctx.UrlPath = $"/{string.Join("/",path.Skip(paths))}"; + + var r= await Server.BeforeAsync(ctx); + ctx.UrlPath = urlPath; + return r; + } + } + /// /// Abstract class for server /// public abstract class Server : IServer @@ -1327,6 +1514,36 @@ namespace Tesses.WebServer } } + + public static Server FromCallback(HttpActionAsync cb) + { + return new CallbackServer(cb); + } + private class CallbackServer : Server + { + HttpActionAsync cb; + public CallbackServer(HttpActionAsync cb) + { + this.cb = cb; + } + + public override async Task GetAsync(ServerContext ctx) + { + await cb(ctx); + } + public override async Task OptionsAsync(ServerContext ctx) + { + await cb(ctx); + } + public override async Task OtherAsync(ServerContext ctx) + { + await cb(ctx); + } + public override async Task PostAsync(ServerContext ctx) + { + await cb(ctx); + } + } } /// /// mount multiple servers at different url paths @@ -1652,6 +1869,140 @@ namespace Tesses.WebServer Task OtherAsync(ServerContext ctx); } + public sealed class ChunkedStream : Stream + { + int offset=0; + byte[] buffer=new byte[0]; + Stream strm; + bool receive; + public ChunkedStream(Stream strm, bool receive) + { + this.receive = receive; + this.strm=strm; + } + public override bool CanRead => receive; + + public override bool CanSeek => false; + + public override bool CanWrite => !receive; + + public override long Length => throw new NotImplementedException(); + + public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public override void Flush() + { + strm.Flush(); + } + + + + public override int Read(byte[] buffer, int offset, int count) + { + if(!receive) throw new IOException("Cannot read from a writeonly stream"); + if(EOF) throw new EndOfStreamException(); + if(this.offset>=this.buffer.Length) + { + + //we need to read a new one + StringBuilder b=new StringBuilder(); + int read = 0; + while((read=strm.ReadByte()) != '\r') + { + b.Append((char)read); + } + if(read == '\r') + { + read=strm.ReadByte(); + if(read != '\n') + { + throw new IOException("Must end with \r\n"); + } + } + else + { + throw new IOException("Must end with \r\n"); + } + + if(int.TryParse(b.ToString(),System.Globalization.NumberStyles.HexNumber,null,out int val)) + { + if(val == 0) + { + EOF=true; + if(strm.ReadByte()!='\r' || strm.ReadByte() != '\n') + { + throw new IOException("Must end with \r\n"); + } + return 0; + } + this.offset = 0; + this.buffer=new byte[val]; + strm.Read(this.buffer,0,this.buffer.Length); + + if(strm.ReadByte()!='\r' || strm.ReadByte() != '\n') + { + throw new IOException("Must end with \r\n"); + } + + } + } + count = Math.Min(count,this.buffer.Length-this.offset); + Array.Copy(this.buffer,this.offset,buffer,offset,count); + + this.offset+=count; + return count; + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + if(receive) throw new IOException("Cannot write to readonly stream"); + if(count == 0 && EOF) return; + var len = System.Text.Encoding.ASCII.GetBytes($"{count.ToString("X")}\r\n"); + + strm.Write(len,0,len.Length); + strm.Write(buffer,offset,count); + strm.Write(crlf,0,crlf.Length); + + if(count==0) EOF=true; + } + private static readonly byte[] crlf = new byte[]{(byte)'\r',(byte)'\n'}; + public bool EOF {get;private set;}=false; + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if(receive) throw new IOException("Cannot write to readonly stream"); + if(count == 0 && EOF) return; + var len = System.Text.Encoding.ASCII.GetBytes($"{count.ToString("X")}\r\n"); + + await strm.WriteAsync(len,0,len.Length,cancellationToken); + await strm.WriteAsync(buffer,offset,count,cancellationToken); + await strm.WriteAsync(crlf,0,crlf.Length,cancellationToken); + + if(count==0) EOF=true; + } + protected override void Dispose(bool disposing) + { + if(disposing) + { + if(!EOF) + { + if(!receive) + Write(crlf,0,0); + } + + } + } + } + public sealed class HttpServerListener { /// @@ -1861,8 +2212,7 @@ namespace Tesses.WebServer { StringBuilder s = new StringBuilder(); - var decoder = Encoding.UTF8.GetDecoder(); - var nextChar = new char[1]; + while (!s.EndsWith("\r\n\r\n",StringComparison.Ordinal)) { int data = strm.ReadByte(); @@ -1870,9 +2220,8 @@ namespace Tesses.WebServer { break; } - int charCount=decoder.GetChars(new byte[] { (byte)data }, 0, 1, nextChar, 0); - if (charCount == 0) continue; - s.Append(nextChar); + if(data > 127) continue; + s.Append((char)data); //its ascii now } return s.ToString();