using System; using System.Net; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using System.IO; using System.Text.RegularExpressions; using Newtonsoft.Json; using Hyperlinq; //Uses nuget packages HyperLinq, SimpleHttp namespace SimpleHttp { public delegate void HttpActionQuery(HttpListenerRequest req, HttpListenerResponse resp, Dictionary> args); public delegate Task HttpActionQueryAsync(HttpListenerRequest req, HttpListenerResponse resp, Dictionary> args); public static class RequestExtensionsQuery { public static void AsJson(this HttpListenerResponse resp, object respValue, string mime = "application/json") { resp.AsText(JsonConvert.SerializeObject(respValue), mime); } public static T ParseRawBodyJson(this HttpListenerRequest req) { return JsonConvert.DeserializeObject(req.BodyAsString()); } public static object ParseRawBodyJson(this HttpListenerRequest req) { return JsonConvert.DeserializeObject(req.BodyAsString()); } public static void AddToList(this Dictionary> dict, T key, T2 value) { if (dict.ContainsKey(key)) { dict[key].Add(value); } else { List items = new List(); items.Add(value); dict.Add(key, items); } } public static Dictionary> GetQueryParams(this string parm) { Dictionary> ls = new Dictionary>(); string[] args = parm.Split(new char[] { '&' }, StringSplitOptions.RemoveEmptyEntries); foreach (var arg in args) { //name=value string[] nvp = arg.Split(new char[] { '=' }, 2, StringSplitOptions.RemoveEmptyEntries); if (nvp.Length == 2) { ls.AddToList(nvp[0], nvp[1]); } else if (nvp.Length == 1) { ls.AddToList(nvp[0], ""); } } return ls; } /// /// 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 HttpListenerRequest request, Dictionary> args) { return request.ParseBody(args, (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 HttpListenerRequest request, Dictionary> args, OnFile onFile) { if (request == null) throw new ArgumentNullException(nameof(request)); if (args == null) throw new ArgumentNullException(nameof(args)); if (onFile == null) throw new ArgumentNullException(nameof(onFile)); var files = new Dictionary(); if (request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.Ordinal)) { ParseForm(request, args); } else if (request.ContentType.StartsWith("multipart/form-data", StringComparison.Ordinal)) { files = ParseMultipartForm(request, args, onFile); } else throw new NotSupportedException("The body content-type is not supported."); return files; } static bool ParseForm(HttpListenerRequest request, Dictionary> args) { if (request.ContentType != "application/x-www-form-urlencoded") return false; var str = request.BodyAsString(); if (str == null) return false; foreach (var pair in str.Split('&')) { var nameValue = pair.Split('='); if (nameValue.Length != (1 + 1)) continue; args.AddToList(nameValue[0], WebUtility.UrlDecode(nameValue[1])); } return true; } static string BodyAsString(this HttpListenerRequest request) { if (!request.HasEntityBody) return null; string str = null; using (var reader = new StreamReader(request.InputStream, request.ContentEncoding)) { str = reader.ReadToEnd(); } return str; } static Dictionary ParseMultipartForm(HttpListenerRequest request, Dictionary> args, OnFile onFile) { if (request.ContentType.StartsWith("multipart/form-data") == false) throw new InvalidDataException("Not 'multipart/form-data'."); var boundary = Regex.Match(request.ContentType, "boundary=(.+)").Groups[1].Value; boundary = "--" + boundary; var files = new Dictionary(); var inputStream = new BufferedStream(request.InputStream); 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 HttpFileQuery(fn, v, ct)); else args.AddToList(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(); } } /// /// HTTP file data container. /// public class HttpFileQuery : IDisposable { /// /// Creates new HTTP file data container. /// /// File name. /// Data. /// Content type. internal HttpFileQuery(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. /// ~HttpFileQuery() { Dispose(); } } public static class RouteQuery { public static void AsHtml(this HttpListenerResponse resp, HElement element) { resp.AsText(element.ToString()); } public static void AddQuery(string url, HttpActionQueryAsync httpActionQueryAsync, string method = "GET", string queryText = "query_parms") { Route.Add($"{url}{{{queryText}}}", async (req, resp, args) => { string queryparms = args[queryText]; Dictionary> str = new Dictionary>(); foreach (var arg in args) { if (arg.Key != queryText) { str.AddToList(arg.Key, arg.Value); } } foreach (var queryParm in queryparms.GetQueryParams()) { foreach (var value in queryParm.Value) { str.AddToList(queryParm.Key, value); } } await httpActionQueryAsync(req, resp, str); }); } public static void AddQuery(string url, HttpActionQuery httpActionQuery, string method = "GET", string queryText = "query_parms") { Route.Add($"{url}{{{queryText}}}", (req, resp, args) => { string queryparms = args[queryText]; Dictionary> str = new Dictionary>(); foreach (var arg in args) { if (arg.Key != queryText) { str.AddToList(arg.Key, arg.Value); } } foreach (var queryParm in queryparms.GetQueryParams()) { foreach (var value in queryParm.Value) { str.AddToList(queryParm.Key, value); } } httpActionQuery(req, resp, str); }); } public static void AddQuery(string url, HttpActionAsync httpActionAsync, string method = "GET", string queryText = "query_parms") { Route.Add($"{url}{{{queryText}}}", async (req, resp, args) => { string queryparms = args[queryText]; foreach (var queryParm in queryparms.GetQueryParams()) { args.Add(queryParm.Key, queryParm.Value[0]); } await httpActionAsync(req, resp, args); }); } public static void AddQuery(string url, HttpAction action, string method = "GET", string queryText = "query_parms") { Route.Add($"{url}{{{queryText}}}", (req, resp, args) => { string queryparms = args[queryText]; foreach (var queryParm in queryparms.GetQueryParams()) { args.Add(queryParm.Key, queryParm.Value[0]); } action(req, resp, args); }, method); Route.Add(url, action, method); } } }