First version
This commit is contained in:
parent
c144565f70
commit
d9e66a1427
|
@ -1,19 +1,23 @@
|
||||||
<Properties StartupConfiguration="{3E464D71-CC54-4E71-9C8F-60B0ADF11EC1}|Default">
|
<Properties StartupConfiguration="{3E464D71-CC54-4E71-9C8F-60B0ADF11EC1}|Default">
|
||||||
<MonoDevelop.Ide.Workbench ActiveDocument="Tesses.WebServer/StatusCodeMap.cs">
|
<MonoDevelop.Ide.Workbench ActiveDocument="Tesses.WebServer.Console/Server.cs">
|
||||||
<Files>
|
<Files>
|
||||||
<File FileName="Tesses.WebServer.Console/Program.cs" Line="20" Column="6" />
|
<File FileName="Tesses.WebServer.Console/Program.cs" Line="11" Column="13" />
|
||||||
<File FileName="Tesses.WebServer/MyClass.cs" Line="142" Column="26" />
|
<File FileName="Tesses.WebServer/TessesServer.cs" Line="345" Column="26" />
|
||||||
<File FileName="Tesses.WebServer/ServerContext.cs" Line="15" Column="29" />
|
<File FileName="Tesses.WebServer/ServerContext.cs" Line="66" Column="2" />
|
||||||
<File FileName="Tesses.WebServer/StatusCodeMap.cs" Line="2" Column="1" />
|
<File FileName="Tesses.WebServer/StatusCodeMap.cs" Line="28" Column="9" />
|
||||||
|
<File FileName="Tesses.WebServer/SimpleHttpCode.cs" Line="261" Column="13" />
|
||||||
|
<File FileName="Tesses.WebServer.Console/Server.cs" Line="6" Column="18" />
|
||||||
</Files>
|
</Files>
|
||||||
<Pads>
|
<Pads>
|
||||||
<Pad Id="ProjectPad">
|
<Pad Id="ProjectPad">
|
||||||
<State name="__root__">
|
<State name="__root__">
|
||||||
<Node name="Tesses.WebServer" expanded="True">
|
<Node name="Tesses.WebServer" expanded="True">
|
||||||
<Node name="Tesses.WebServer" expanded="True">
|
<Node name="Tesses.WebServer" expanded="True">
|
||||||
<Node name="StatusCodeMap.cs" selected="True" />
|
<Node name="Packages" expanded="True" />
|
||||||
|
</Node>
|
||||||
|
<Node name="Tesses.WebServer.Console" expanded="True">
|
||||||
|
<Node name="Server.cs" selected="True" />
|
||||||
</Node>
|
</Node>
|
||||||
<Node name="Tesses.WebServer.Console" expanded="True" />
|
|
||||||
</Node>
|
</Node>
|
||||||
</State>
|
</State>
|
||||||
</Pad>
|
</Pad>
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Tesses.WebServer
|
||||||
|
|
||||||
|
A TcpListener HTTP Server
|
||||||
|
|
||||||
|
Currently Supports
|
||||||
|
- GET+HEAD+POST Requests
|
||||||
|
- Seekable Video Files (Using Range)
|
||||||
|
- Can Send Json To Client with helper function (uses Newtonsoft.Json)
|
||||||
|
- Cors Header
|
||||||
|
|
||||||
|
# Classes To Make It Easier
|
||||||
|
- Static Website Class (Can pass in other class (instead of 404 when file doesnt exist) can choose other names other than index.html, index.htm, default.html, default.htm)
|
||||||
|
- 404 Not Found Class
|
||||||
|
- Mount class (So you could use Api)
|
||||||
|
|
||||||
|
# Comming Soon Hopefully
|
||||||
|
- Basic Auth Class
|
||||||
|
|
||||||
|
# Might Happen But not sure
|
||||||
|
- WebDav Class
|
||||||
|
|
||||||
|
> Note: Range code and POST code is not mine its a modified version of the code from ( [dajuric/simple-http](https://github.com/dajuric/simple-http/blob/master/Source/SimpleHTTP/Extensions/Response/ResponseExtensions.PartialStream.cs "dajuric/simple-http"))
|
|
@ -1,4 +1,6 @@
|
||||||
namespace Tesses.WebServer.ConsoleApp
|
using Tesses;
|
||||||
|
using Tesses.WebServer;
|
||||||
|
namespace Tesses.WebServer.ConsoleApp
|
||||||
{
|
{
|
||||||
|
|
||||||
class MainClass
|
class MainClass
|
||||||
|
@ -6,8 +8,14 @@
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
var ip=System.Net.IPAddress.Any;
|
var ip=System.Net.IPAddress.Any;
|
||||||
StaticServer server = new StaticServer("/home/ddlovato/Videos/");
|
StaticServer static_server = new StaticServer(System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyVideos));
|
||||||
HttpServerListener s = new HttpServerListener(new System.Net.IPEndPoint(ip, 24240),server);
|
MountableServer mountable = new MountableServer(static_server);
|
||||||
|
|
||||||
|
mountable.Mount("/api/",new DynamicServer());
|
||||||
|
|
||||||
|
|
||||||
|
HttpServerListener s = new HttpServerListener(new System.Net.IPEndPoint(ip, 24240),mountable);
|
||||||
|
|
||||||
s.ListenAsync(System.Threading.CancellationToken.None).Wait();
|
s.ListenAsync(System.Threading.CancellationToken.None).Wait();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Tesses.WebServer.ConsoleApp
|
||||||
|
{
|
||||||
|
public class DynamicServer : Server
|
||||||
|
{
|
||||||
|
public DynamicServer()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
Random rand = new Random();
|
||||||
|
int count = 0;
|
||||||
|
public override async Task GetAsync(ServerContext ctx)
|
||||||
|
{
|
||||||
|
//Console.WriteLine("HANDLE");
|
||||||
|
if(ctx.UrlPath=="/count")
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
await ctx.SendTextAsync($"This page has been viewed {count} times");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ctx.UrlPath=="/rand")
|
||||||
|
{
|
||||||
|
int min = 0;
|
||||||
|
int max = 65536;
|
||||||
|
int times = 5;
|
||||||
|
bool dont_show_hint = false;
|
||||||
|
if(ctx.QueryParams.ContainsKey("min"))
|
||||||
|
{
|
||||||
|
if(!int.TryParse(ctx.QueryParams.GetFirst("min"),out min))
|
||||||
|
{
|
||||||
|
min = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dont_show_hint = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ctx.QueryParams.ContainsKey("max"))
|
||||||
|
{
|
||||||
|
if (!int.TryParse(ctx.QueryParams.GetFirst("max"), out max))
|
||||||
|
{
|
||||||
|
max = 65536;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dont_show_hint = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ctx.QueryParams.ContainsKey("times"))
|
||||||
|
{
|
||||||
|
if (!int.TryParse(ctx.QueryParams.GetFirst("times"), out times))
|
||||||
|
{
|
||||||
|
times = 5;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dont_show_hint = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
max++;
|
||||||
|
StringBuilder html = new StringBuilder();
|
||||||
|
html.Append("<html><head><title>Random Numbers</title></head><body><h1>Random Numbers</h1>");
|
||||||
|
|
||||||
|
if(!dont_show_hint)
|
||||||
|
{
|
||||||
|
string hint = "Hint: <a href=\"./rand?min=41&max=1992×=42\">./rand?min=41&max=1992×=42</a><br>";
|
||||||
|
|
||||||
|
html.Append(hint);
|
||||||
|
}
|
||||||
|
html.Append(rand.Next(min, max));
|
||||||
|
for(int i = 1;i<times;i++)
|
||||||
|
{
|
||||||
|
html.Append($", {rand.Next(min, max)}");
|
||||||
|
}
|
||||||
|
html.Append("</body></html>");
|
||||||
|
await ctx.SendTextAsync(html.ToString());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,6 +40,7 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Program.cs" />
|
<Compile Include="Program.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
<Compile Include="Server.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Tesses.WebServer\Tesses.WebServer.csproj">
|
<ProjectReference Include="..\Tesses.WebServer\Tesses.WebServer.csproj">
|
||||||
|
|
|
@ -15,7 +15,7 @@ namespace Tesses.WebServer
|
||||||
RequestHeaders = headers;
|
RequestHeaders = headers;
|
||||||
ResponseHeaders = new Dictionary<string, List<string>>();
|
ResponseHeaders = new Dictionary<string, List<string>>();
|
||||||
var qp = new Dictionary<string, List<string>>();
|
var qp = new Dictionary<string, List<string>>();
|
||||||
QueryParams = qp;
|
|
||||||
StatusCode = 200;
|
StatusCode = 200;
|
||||||
|
|
||||||
// /joel/path/luigi?local=jim&john_surname=connor&demi_surname=lovato&local=tim
|
// /joel/path/luigi?local=jim&john_surname=connor&demi_surname=lovato&local=tim
|
||||||
|
@ -29,8 +29,9 @@ namespace Tesses.WebServer
|
||||||
{
|
{
|
||||||
//local=jim&john_surname=connor&demi_surname=lovato&local=tim
|
//local=jim&john_surname=connor&demi_surname=lovato&local=tim
|
||||||
//we want to split on &
|
//we want to split on &
|
||||||
foreach(var item in splitUrl[1].Split(new char[] { '&'},2))
|
foreach(var item in splitUrl[1].Split(new char[] { '&'},StringSplitOptions.RemoveEmptyEntries))
|
||||||
{
|
{
|
||||||
|
//Console.WriteLine(item);
|
||||||
var itemSplit = item.Split(new char[] { '=' }, 2);
|
var itemSplit = item.Split(new char[] { '=' }, 2);
|
||||||
if(itemSplit.Length > 0)
|
if(itemSplit.Length > 0)
|
||||||
{
|
{
|
||||||
|
@ -40,12 +41,12 @@ namespace Tesses.WebServer
|
||||||
{
|
{
|
||||||
value = itemSplit[1];
|
value = itemSplit[1];
|
||||||
}
|
}
|
||||||
qp.Add(key, value); //hince qp is reference to QueryParams
|
qp.Add(key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
QueryParams = qp;
|
||||||
}
|
}
|
||||||
private string get_host()
|
private string get_host()
|
||||||
{
|
{
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -36,10 +36,11 @@
|
||||||
</Reference>
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="MyClass.cs" />
|
<Compile Include="TessesServer.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="ServerContext.cs" />
|
<Compile Include="ServerContext.cs" />
|
||||||
<Compile Include="StatusCodeMap.cs" />
|
<Compile Include="StatusCodeMap.cs" />
|
||||||
|
<Compile Include="SimpleHttpCode.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="packages.config" />
|
<None Include="packages.config" />
|
||||||
|
|
|
@ -12,11 +12,10 @@ using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Tesses.WebServer
|
namespace Tesses.WebServer
|
||||||
{
|
{
|
||||||
|
|
||||||
public static class Extensions
|
public static class Extensions
|
||||||
{
|
{
|
||||||
const string BYTES_RANGE_HEADER = "Range";
|
public static async Task WriteHeadersAsync(this ServerContext ctx)
|
||||||
|
|
||||||
private static async Task WriteHeadersAsync(this ServerContext ctx)
|
|
||||||
{
|
{
|
||||||
string status_line = $"HTTP/1.1 {ctx.StatusCode} {StatusCodeMap.GetStatusString(ctx.StatusCode)}\r\n";
|
string status_line = $"HTTP/1.1 {ctx.StatusCode} {StatusCodeMap.GetStatusString(ctx.StatusCode)}\r\n";
|
||||||
StringBuilder b = new StringBuilder(status_line);
|
StringBuilder b = new StringBuilder(status_line);
|
||||||
|
@ -49,59 +48,15 @@ public static class Extensions
|
||||||
{
|
{
|
||||||
await ctx.SendTextAsync(JsonConvert.SerializeObject(value), "application/json");
|
await ctx.SendTextAsync(JsonConvert.SerializeObject(value), "application/json");
|
||||||
}
|
}
|
||||||
public static async Task SendTextAsync(this ServerContext ctx, string data, string content_type = "text/html")
|
public static async Task SendTextAsync(this ServerContext ctx, string data, string contentType = "text/html")
|
||||||
{
|
{
|
||||||
await ctx.SendBytesAsync(Encoding.UTF8.GetBytes(data), content_type);
|
await ctx.SendBytesAsync(Encoding.UTF8.GetBytes(data), contentType);
|
||||||
}
|
}
|
||||||
public static async Task SendBytesAsync(this ServerContext ctx, byte[] array, string content_type = "application/octet-stream")
|
public static async Task SendBytesAsync(this ServerContext ctx, byte[] array, string contentType = "application/octet-stream")
|
||||||
{
|
{
|
||||||
using (var ms = new MemoryStream(array))
|
using (var ms = new MemoryStream(array))
|
||||||
{
|
{
|
||||||
await ctx.SendStreamAsync( ms, content_type);
|
await ctx.SendStreamAsync( ms, contentType);
|
||||||
}
|
|
||||||
}
|
|
||||||
public static async Task SendStreamAsync(this ServerContext ctx, Stream strm, string content_type = "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", content_type);
|
|
||||||
|
|
||||||
await WriteHeadersAsync(ctx);
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,18 +178,141 @@ public static class Extensions
|
||||||
}
|
}
|
||||||
public abstract class Server : IServer
|
public abstract class Server : IServer
|
||||||
{
|
{
|
||||||
|
public bool CorsHeader = true;
|
||||||
public abstract Task GetAsync(ServerContext ctx);
|
public abstract Task GetAsync(ServerContext ctx);
|
||||||
public virtual async Task PostAsync(ServerContext ctx)
|
public virtual async Task PostAsync(ServerContext ctx)
|
||||||
{
|
{
|
||||||
ctx.StatusCode = (int)HttpStatusCode.MethodNotAllowed;
|
ctx.StatusCode = (int)HttpStatusCode.MethodNotAllowed;
|
||||||
await ctx.SendTextAsync("Method Not Supported");
|
await ctx.SendTextAsync("Method Not Supported");
|
||||||
|
|
||||||
|
}
|
||||||
|
public virtual async Task OptionsAsync(ServerContext ctx)
|
||||||
|
{
|
||||||
|
await ctx.WriteHeadersAsync();
|
||||||
|
ctx.NetworkStream.Close();
|
||||||
|
}
|
||||||
|
public virtual async Task OtherAsync(ServerContext ctx)
|
||||||
|
{
|
||||||
|
ctx.StatusCode = (int)HttpStatusCode.MethodNotAllowed;
|
||||||
|
await ctx.SendTextAsync("Method Not Supported");
|
||||||
|
|
||||||
|
}
|
||||||
|
public virtual async Task<bool> BeforeAsync(ServerContext ctx)
|
||||||
|
{
|
||||||
|
if(CorsHeader)
|
||||||
|
{
|
||||||
|
|
||||||
|
ctx.ResponseHeaders.Add("Access-Control-Allow-Origin", "*");
|
||||||
|
ctx.ResponseHeaders.Add("Access-Control-Allow-Headers", "Cache-Control, Pragma, Accept, Origin, Authorization, Content-Type, X-Requested-With");
|
||||||
|
ctx.ResponseHeaders.Add("Access-Control-Allow-Methods", "GET, POST");
|
||||||
|
ctx.ResponseHeaders.Add("Access-Control-Allow-Credentials", "true");
|
||||||
|
|
||||||
|
}
|
||||||
|
return await Task.FromResult(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class MountableServer : Server
|
||||||
|
{
|
||||||
|
Dictionary<string, IServer> _servers = new Dictionary<string, IServer>();
|
||||||
|
public MountableServer(IServer root)
|
||||||
|
{
|
||||||
|
_root = root;
|
||||||
|
}
|
||||||
|
IServer _root;
|
||||||
|
private KeyValuePair<string,IServer> GetFromPath(ServerContext ctx)
|
||||||
|
{
|
||||||
|
//bool j = false;
|
||||||
|
foreach(var item in _servers.Reverse())
|
||||||
|
{
|
||||||
|
if(ctx.UrlPath.StartsWith(item.Key,StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
Console.WriteLine("HERE WE ARE");
|
||||||
|
return new KeyValuePair<string, IServer>("/",_root);
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Mount the specified url and server.
|
||||||
|
/// Must mount like this
|
||||||
|
/// /somePath0
|
||||||
|
/// /somePath0/someSubPath0
|
||||||
|
/// /somePath0/someSubPath0/someSubSubPath0
|
||||||
|
/// /somePath0/someSubPath0/someSubSubPath1
|
||||||
|
/// /somePath0/someSubPath1
|
||||||
|
/// /somePath0/someSubPath1/someSubSubPath0
|
||||||
|
/// /somePath0/someSubPath1/someSubSubPath1
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">URL.</param>
|
||||||
|
/// <param name="server">Server.</param>
|
||||||
|
public void Mount(string url,IServer server)
|
||||||
|
{
|
||||||
|
_servers.Add(url, server);
|
||||||
|
}
|
||||||
|
public void Unmount(string url)
|
||||||
|
{
|
||||||
|
_servers.Remove(url);
|
||||||
|
}
|
||||||
|
public void UnmountAll()
|
||||||
|
{
|
||||||
|
_servers.Clear();
|
||||||
|
}
|
||||||
|
public override async Task GetAsync(ServerContext ctx)
|
||||||
|
{
|
||||||
|
var v = GetFromPath(ctx);
|
||||||
|
string url = '/' + ctx.UrlPath.Substring(v.Key.Length).TrimStart('/');
|
||||||
|
ctx.UrlPath = url;
|
||||||
|
|
||||||
|
await v.Value.GetAsync(ctx);
|
||||||
|
}
|
||||||
|
public override async Task PostAsync(ServerContext ctx)
|
||||||
|
{
|
||||||
|
var v = GetFromPath(ctx);
|
||||||
|
string url = '/' + ctx.UrlPath.Substring(v.Key.Length).TrimStart('/');
|
||||||
|
ctx.UrlPath = url;
|
||||||
|
|
||||||
|
await v.Value.PostAsync(ctx);
|
||||||
|
}
|
||||||
|
public override async Task<bool> BeforeAsync(ServerContext ctx)
|
||||||
|
{
|
||||||
|
var v = GetFromPath(ctx);
|
||||||
|
string old=ctx.UrlPath;
|
||||||
|
string url = '/' + ctx.UrlPath.Substring(v.Key.Length).TrimStart('/');
|
||||||
|
ctx.UrlPath = url;
|
||||||
|
|
||||||
|
var res=await v.Value.BeforeAsync(ctx);
|
||||||
|
ctx.UrlPath = old;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
public override async Task OptionsAsync(ServerContext ctx)
|
||||||
|
{
|
||||||
|
var v = GetFromPath(ctx);
|
||||||
|
string url = '/' + ctx.UrlPath.Substring(v.Key.Length).TrimStart('/');
|
||||||
|
ctx.UrlPath = url;
|
||||||
|
|
||||||
|
await v.Value.OptionsAsync(ctx);
|
||||||
|
}
|
||||||
|
public override async Task OtherAsync(ServerContext ctx)
|
||||||
|
{
|
||||||
|
var v = GetFromPath(ctx);
|
||||||
|
string url = '/' + ctx.UrlPath.Substring(v.Key.Length).TrimStart('/');
|
||||||
|
ctx.UrlPath = url;
|
||||||
|
|
||||||
|
await v.Value.OtherAsync(ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IServer
|
public interface IServer
|
||||||
{
|
{
|
||||||
|
Task<bool> BeforeAsync(ServerContext ctx);
|
||||||
Task GetAsync(ServerContext ctx);
|
Task GetAsync(ServerContext ctx);
|
||||||
Task PostAsync(ServerContext ctx);
|
Task PostAsync(ServerContext ctx);
|
||||||
|
Task OptionsAsync(ServerContext ctx);
|
||||||
|
Task OtherAsync(ServerContext ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class HttpServerListener
|
public sealed class HttpServerListener
|
||||||
|
@ -321,18 +399,29 @@ public static class Extensions
|
||||||
string[] request=request_line.Split(new char[] { ' ' }, 3);
|
string[] request=request_line.Split(new char[] { ' ' }, 3);
|
||||||
string method = request[0];
|
string method = request[0];
|
||||||
try
|
try
|
||||||
|
{
|
||||||
|
string path = request[1];
|
||||||
|
string ver = request[2];
|
||||||
|
ctx = new ServerContext(method, strm, path, headers);
|
||||||
|
if (!await _server.BeforeAsync(ctx))
|
||||||
{
|
{
|
||||||
switch (method)
|
switch (method)
|
||||||
{
|
{
|
||||||
case "HEAD":
|
case "HEAD":
|
||||||
case "GET":
|
case "GET":
|
||||||
string path = request[1];
|
|
||||||
string ver = request[2];
|
|
||||||
ctx = new ServerContext(method,strm, path, headers);
|
|
||||||
await _server.GetAsync(ctx);
|
await _server.GetAsync(ctx);
|
||||||
break;
|
break;
|
||||||
case "POST":
|
case "POST":
|
||||||
|
await _server.PostAsync(ctx);
|
||||||
break;
|
break;
|
||||||
|
case "OPTIONS":
|
||||||
|
await _server.OptionsAsync(ctx);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
await _server.OtherAsync(ctx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}catch(Exception ex)
|
}catch(Exception ex)
|
||||||
{
|
{
|
Loading…
Reference in New Issue