First version
This commit is contained in:
@@ -0,0 +1,364 @@
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Delegate executed when a file is about to be read from a body stream.
|
||||
/// </summary>
|
||||
/// <param name="fieldName">Field name.</param>
|
||||
/// <param name="fileName">name of the file.</param>
|
||||
/// <param name="contentType">Content type.</param>
|
||||
/// <returns>Stream to be populated.</returns>
|
||||
public delegate Stream OnFile(string fieldName, string fileName, string contentType);
|
||||
|
||||
public static class DajuricSimpleHttpExtensions
|
||||
{
|
||||
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<string, HttpFile> 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<string, HttpFile>();
|
||||
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=""?(?<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>
|
||||
/// 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, HttpFile> ParseBody(this ServerContext ctx)
|
||||
{
|
||||
return ctx.ParseBody( (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, HttpFile> 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, HttpFile>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTTP file data container.
|
||||
/// </summary>
|
||||
public class HttpFile : 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 HttpFile(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>
|
||||
~HttpFile()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user