using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; 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 { 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) { var args = ctx.QueryParams; string content_type = ctx.RequestHeaders.GetFirst("Content-Type"); if (content_type != "application/x-www-form-urlencoded") return false; var str = ctx.BodyAsString(); if (str == null) return false; foreach (var pair in str.Split('&')) { var nameValue = pair.Split('='); if (nameValue.Length != (1 + 1)) continue; args.Add(nameValue[0], WebUtility.UrlDecode(nameValue[1])); } return true; } static string BodyAsString(this ServerContext ctx) { string str = null; using (var reader = new StreamReader(ctx.NetworkStream)) { str = reader.ReadToEnd(); } return str; } 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)) { 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.ResponseHeaders.Add("Content-Type", contentType); await ctx.WriteHeadersAsync(); if (!ctx.Method.Equals("HEAD", StringComparison.Ordinal)) { try { strm.Position = start; strm.CopyTo(ctx.NetworkStream, Math.Min(8 * 1024 * 1024, end - start + 1)); } finally { strm.Close(); ctx.NetworkStream.Close(); } } } 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.NetworkStream); parseUntillBoundaryEnd(inputStream, new MemoryStream(), boundary); while (true) { var (n, v, fn, ct) = parseSection(inputStream, "\r\n" + boundary, onFile); 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) { var (n, fn, ct) = readContentDisposition(source); 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."); parseUntillBoundaryEnd(source, dst, boundary); 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. /// /// 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) { 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)); 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); } else if (content_type.StartsWith("multipart/form-data",StringComparison.Ordinal)) { files = ParseMultipartForm(request, onFile); } else throw new NotSupportedException("The body content-type is not supported."); return files; } } /// /// 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 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); } )); } } }