tytd-server/TYTD.Api/SimpleHttpExtensions.cs

432 lines
15 KiB
C#

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<string, List<string>> args);
public delegate Task HttpActionQueryAsync(HttpListenerRequest req, HttpListenerResponse resp, Dictionary<string, List<string>> 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<T>(this HttpListenerRequest req)
{
return JsonConvert.DeserializeObject<T>(req.BodyAsString());
}
public static object ParseRawBodyJson(this HttpListenerRequest req)
{
return JsonConvert.DeserializeObject(req.BodyAsString());
}
public static void AddToList<T, T2>(this Dictionary<T, List<T2>> dict, T key, T2 value)
{
if (dict.ContainsKey(key))
{
dict[key].Add(value);
}
else
{
List<T2> items = new List<T2>();
items.Add(value);
dict.Add(key, items);
}
}
public static Dictionary<string, List<string>> GetQueryParams(this string parm)
{
Dictionary<string, List<string>> ls = new Dictionary<string, List<string>>();
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;
}
/// <summary>
/// Parses body of the request including form and multi-part form data.
/// </summary>
/// <param name="request">HTTP request.</param>
/// <param name="args">Key-value pairs populated by the form data by this function.</param>
/// <returns>Name-file pair collection.</returns>
public static Dictionary<string, HttpFileQuery> ParseBody(this HttpListenerRequest request, Dictionary<string, List<string>> args)
{
return request.ParseBody(args, (n, fn, ct) => new MemoryStream());
}
/// <summary>
/// Parses body of the request including form and multi-part form data.
/// </summary>
/// <param name="request">HTTP request.</param>
/// <param name="args">Key-value pairs populated by the form data by this function.</param>
/// <param name="onFile">
/// Function called if a file is about to be parsed. The stream is attached to a corresponding <see cref="HttpFile"/>.
/// <para>By default, <see cref="MemoryStream"/> is used, but for large files, it is recommended to open <see cref="FileStream"/> directly.</para>
/// </param>
/// <returns>Name-file pair collection.</returns>
public static Dictionary<string, HttpFileQuery> ParseBody(this HttpListenerRequest request, Dictionary<string, List<string>> 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<string, HttpFileQuery>();
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<string, List<string>> 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<string, HttpFileQuery> ParseMultipartForm(HttpListenerRequest request, Dictionary<string, List<string>> 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<string, HttpFileQuery>();
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=""?(?<n>[^\""]*)").Groups["n"].Value;
var f = Regex.Match(l, @"filename\*?=""?(?<f>[^\"";]*)").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: (?<cType>.+)").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();
}
}
/// <summary>
/// HTTP file data container.
/// </summary>
public class HttpFileQuery : IDisposable
{
/// <summary>
/// Creates new HTTP file data container.
/// </summary>
/// <param name="fileName">File name.</param>
/// <param name="value">Data.</param>
/// <param name="contentType">Content type.</param>
internal HttpFileQuery(string fileName, Stream value, string contentType)
{
Value = value;
FileName = fileName;
ContentType = contentType;
}
/// <summary>
/// Gets the name of the file.
/// </summary>
public string FileName { get; private set; }
/// <summary>
/// Gets the data.
/// <para>If a stream is created <see cref="OnFile"/> it will be closed when this HttpFile object is disposed.</para>
/// </summary>
public Stream Value { get; private set; }
/// <summary>
/// Content type.
/// </summary>
public string ContentType { get; private set; }
/// <summary>
/// Saves the data into a file.
/// <para>Directory path will be auto created if does not exists.</para>
/// </summary>
/// <param name="fileName">File path with name.</param>
/// <param name="overwrite">True to overwrite the existing file, false otherwise.</param>
/// <returns>True if the file is saved/overwritten, false otherwise.</returns>
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;
}
/// <summary>
/// Disposes the current instance.
/// </summary>
public void Dispose()
{
if (Value != null)
{
Value?.Dispose();
Value = null;
}
}
/// <summary>
/// Disposes the current instance.
/// </summary>
~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<string, List<string>> str = new Dictionary<string, List<string>>();
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<string, List<string>> str = new Dictionary<string, List<string>>();
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);
}
}
}