using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using HeyRed.Mime; using Newtonsoft.Json; 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 { /* Thanks to you we fixed this public static void Print(string text,[CallerLineNumber] int lineNumber = 0) { Console.WriteLine($"[LINE {lineNumber}] {text}"); }*/ 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) { //Print("Enter 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; //Print("Before BodyAsString"); var str = ctx.BodyAsString(); //Print("After BodyAsString"); if (str == null) return false; //Print("Before For Loop"); foreach (var pair in str.Split('&')) { var nameValue = pair.Split('='); if (nameValue.Length != (1 + 1)) continue; //Print($"Before Add: {nameValue[0]}: {nameValue[1]}"); args.Add(nameValue[0], WebUtility.UrlDecode(nameValue[1])); //Print($"After Add: {nameValue[0]}: {nameValue[1]}"); } return true; } private static string BodyAsString(this ServerContext ctx) { string str = null; using (var reader = new StreamReader(ctx.GetRequestStream())) { //Print("Before ReadToEnd"); str = reader.ReadToEnd(); //Print("After ReadToEnd"); } return str; } public static void SendNonSeekableStream(this ServerContext ctx,Stream strm,long readFor=-1,string contentType = "application/octet-stream") { try { var strm2=new ChunkedStream(ctx.NetworkStream,false); long tread=0; byte[] buffer=new byte[8*1024*1024]; int read=0; do { if(readFor > -1){ read=(int)Math.Min(buffer.Length,readFor-tread); }else{ read=buffer.Length; } if(read == 0) break; read = strm.Read(buffer,0,read); strm2.Write(buffer,0,read); }while(read > 0); } finally { strm.Close(); ctx.NetworkStream.Close(); } } public static async Task SendNonSeekableStreamAsync(this ServerContext ctx,Stream strm,long readFor=-1,string contentType="application/octet-stream") { try { var strm2=new ChunkedStream(ctx.NetworkStream,false); long tread=0; byte[] buffer=new byte[8*1024*1024]; int read=0; do { if(readFor > -1){ read=(int)Math.Min(buffer.Length,readFor-tread); }else{ read=buffer.Length; } if(read == 0) break; read = await strm.ReadAsync(buffer,0,read); await strm2.WriteAsync(buffer,0,read); }while(read > 0); } finally { strm.Close(); ctx.NetworkStream.Close(); } } public static void SendStream(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) && strm.CanSeek) { 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.WithMimeType(contentType); ctx.WriteHeaders(); if (!ctx.Method.Equals("HEAD", StringComparison.Ordinal)) { try { if(strm.CanSeek) strm.Position = start; strm.CopyTo(ctx.NetworkStream, Math.Min(8 * 1024 * 1024, end - start + 1)); } finally { strm.Close(); ctx.NetworkStream.Close(); } } } 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) && strm.CanSeek) { 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.WithMimeType(contentType); await ctx.WriteHeadersAsync(); if (!ctx.Method.Equals("HEAD", StringComparison.Ordinal)) { try { if(strm.CanSeek) strm.Position = start; await strm.CopyToAsync(ctx.NetworkStream, Math.Min(8 * 1024 * 1024, end - start + 1)); } finally { strm.Close(); ctx.NetworkStream.Close(); } } } /// /// 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)); } 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"); 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.GetRequestStream()); //Print("Before ParseUntillBoundaryEnd"); parseUntillBoundaryEnd(inputStream, new MemoryStream(), boundary); //Print("After ParseUntillBoundaryEnd"); while (true) { //Print("Before ParseSection"); var (n, v, fn, ct) = parseSection(inputStream, "\r\n" + boundary, onFile); //Print("After ParseSection"); 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) { //Print("Before ReadContentDisposition"); var (n, fn, ct) = readContentDisposition(source); //Print("After ReadContentDisposition"); 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."); //Print("Before ParseUntillBodyEnd"); parseUntillBoundaryEnd(source, dst, boundary); //Print("Before ParseUntillBodyEnd"); 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, allowing multiple file with same key. /// /// 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> ParseBodyMultiple(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)); 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)) { 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; } public static HttpFileResponse ParseBodyWithTempDirectory(this ServerContext request) { DateTime dt=DateTime.Now; return request.ParseBodyWithTempDirectory(Path.Combine(Path.GetTempPath(),$"TWSUPLOAD_{dt.ToString("yyyyMMdd_HHmmss")}_{ServerContext.UniqueNumber()}")); } /// /// 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;} [JsonIgnore] public FileInfo FileInfo => new FileInfo(Path); [JsonIgnore] public object PrivateData {get;set;}=null; 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() { if(System.IO.Directory.Exists(Directory)) System.IO.Directory.Delete(Directory,true); } ~HttpFileResponse() { Dispose(); } } /// /// 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(); } } /// /// 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 RouteServer() : this(new NotFoundServer()) { } public RouteServer(IServer otherServer) { this.otherServer=otherServer; } public List<(ShouldProcessFunc ShouldProcessFunc, HttpActionAsync Action)> Methods = new List<(ShouldProcessFunc ShouldProcessFunc, HttpActionAsync Action)>(); private IServer otherServer; public override async Task GetAsync(ServerContext ctx) { if(!await Process(ctx)) { await Guaranteed(otherServer).GetAsync(ctx); } } public override async Task PostAsync(ServerContext ctx) { if(!await Process(ctx)) { await Guaranteed(otherServer).PostAsync(ctx); } } public override async Task OtherAsync(ServerContext ctx) { if(!await Process(ctx)) { await Guaranteed(otherServer).OtherAsync(ctx); } } public override async Task OptionsAsync(ServerContext ctx) { if(!await Process(ctx)) { await Guaranteed(otherServer).OptionsAsync(ctx); } } private async Task Process(ServerContext ctx) { foreach(var (shouldProcessFunc,action) in Methods) { if(!shouldProcessFunc(ctx)) { ctx.ResetQueryParms(); continue; } await action(ctx); return true; } return false; } /// /// 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); } )); } } }