wip static serving
This commit is contained in:
parent
4fad2d648e
commit
8545ed80e9
|
|
@ -4,6 +4,7 @@ using SimpleHttpServer.Types.ParameterConverters;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace SimpleHttpServer;
|
namespace SimpleHttpServer;
|
||||||
|
|
||||||
|
|
@ -13,7 +14,8 @@ public sealed class HttpServer {
|
||||||
|
|
||||||
private readonly HttpListener listener;
|
private readonly HttpListener listener;
|
||||||
private Task? listenerTask;
|
private Task? listenerTask;
|
||||||
private readonly Logger logger;
|
private readonly Logger mainLogger;
|
||||||
|
private readonly Logger requestLogger;
|
||||||
private readonly SimpleHttpServerConfiguration conf;
|
private readonly SimpleHttpServerConfiguration conf;
|
||||||
private bool shutdown = false;
|
private bool shutdown = false;
|
||||||
|
|
||||||
|
|
@ -22,19 +24,20 @@ public sealed class HttpServer {
|
||||||
conf = configuration;
|
conf = configuration;
|
||||||
listener = new HttpListener();
|
listener = new HttpListener();
|
||||||
listener.Prefixes.Add($"http://localhost:{port}/");
|
listener.Prefixes.Add($"http://localhost:{port}/");
|
||||||
logger = new(LogOutputTopic.Main, conf);
|
mainLogger = new(LogOutputTopic.Main, conf);
|
||||||
|
requestLogger = new(LogOutputTopic.Request, conf);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Start() {
|
public void Start() {
|
||||||
logger.Information($"Starting on port {Port}...");
|
mainLogger.Information($"Starting on port {Port}...");
|
||||||
Assert(listenerTask == null, "Server was already started!");
|
Assert(listenerTask == null, "Server was already started!");
|
||||||
listener.Start();
|
listener.Start();
|
||||||
listenerTask = Task.Run(GetContextLoopAsync);
|
listenerTask = Task.Run(GetContextLoopAsync);
|
||||||
logger.Information($"Ready to handle requests!");
|
mainLogger.Information($"Ready to handle requests!");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StopAsync(CancellationToken ctok) {
|
public async Task StopAsync(CancellationToken ctok) {
|
||||||
logger.Information("Stopping server...");
|
mainLogger.Information("Stopping server...");
|
||||||
Assert(listenerTask != null, "Server was not started!");
|
Assert(listenerTask != null, "Server was not started!");
|
||||||
shutdown = true;
|
shutdown = true;
|
||||||
listener.Stop();
|
listener.Stop();
|
||||||
|
|
@ -48,7 +51,7 @@ public sealed class HttpServer {
|
||||||
_ = ProcessRequestAsync(ctx);
|
_ = ProcessRequestAsync(ctx);
|
||||||
} catch (HttpListenerException ex) when (ex.ErrorCode == 995) { //The I/O operation has been aborted because of either a thread exit or an application request
|
} catch (HttpListenerException ex) when (ex.ErrorCode == 995) { //The I/O operation has been aborted because of either a thread exit or an application request
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
logger.Fatal($"Caught otherwise uncaught exception in GetContextLoop:\n{ex}");
|
mainLogger.Fatal($"Caught otherwise uncaught exception in GetContextLoop:\n{ex}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -127,24 +130,70 @@ public sealed class HttpServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
var reqMethod = Enum.GetName(attrib.RequestMethod) ?? throw new ArgumentException("Request method was undefined");
|
var reqMethod = Enum.GetName(attrib.RequestMethod) ?? throw new ArgumentException("Request method was undefined");
|
||||||
|
mainLogger.Information($"Registered endpoint: '{reqMethod} {normLocation}'");
|
||||||
simpleEndpointMethodInfos.Add((normLocation, reqMethod), new EndpointInvocationInfo(mi, qparams));
|
simpleEndpointMethodInfos.Add((normLocation, reqMethod), new EndpointInvocationInfo(mi, qparams));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serves all files located in <paramref name="filesystemDirectory"/> on a website path that is relative to <paramref name="requestPath"/>,
|
||||||
|
/// while restricting requests to inside the local filesystem directory. Static serving has a lower priority than registering an endpoint.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="requestPath"></param>
|
||||||
|
/// <param name="filesystemDirectory"></param>
|
||||||
|
public void RegisterStaticServePath(string requestPath, string filesystemDirectory) {
|
||||||
|
var absPath = Path.GetFullPath(filesystemDirectory);
|
||||||
|
string npath = NormalizeUrlPath(requestPath);
|
||||||
|
mainLogger.Information($"Registered static serve path: '{npath}' --> '{absPath}'");
|
||||||
|
staticServePaths.Add(npath, absPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Dictionary<string, string> staticServePaths = new Dictionary<string, string>();
|
||||||
|
|
||||||
private readonly Dictionary<Type, IParameterConverter> stringToTypeParameterConverters = new();
|
private readonly Dictionary<Type, IParameterConverter> stringToTypeParameterConverters = new();
|
||||||
|
|
||||||
private string NormalizeUrlPath(string url) => '/' + url.TrimStart('/');
|
private static string NormalizeUrlPath(string url) {
|
||||||
|
var fwdSlashUrl = url.Replace('\\', '/');
|
||||||
|
|
||||||
|
var segments = fwdSlashUrl.Trim('/').Split('/', StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||||
|
List<string> simplifiedSegmentsReversed = new List<string>();
|
||||||
|
int doubleDotsEncountered = 0;
|
||||||
|
for (int i = segments.Count - 1; i >= 0; i--) {
|
||||||
|
var segment = segments[i];
|
||||||
|
if (segment == ".") {
|
||||||
|
continue; // remove single dot segments
|
||||||
|
}
|
||||||
|
if (segment == "..") {
|
||||||
|
doubleDotsEncountered++; // if we encounter a doubledot, keep track of that and dont add it to the output yet
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// otherwise only keep the segment if doubleDotsEncountered > 0
|
||||||
|
if (doubleDotsEncountered > 0) {
|
||||||
|
doubleDotsEncountered--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
simplifiedSegmentsReversed.Add(segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
var rv = new StringBuilder();
|
||||||
|
for (int i = 0; i < doubleDotsEncountered; i++) {
|
||||||
|
rv.Append("../");
|
||||||
|
}
|
||||||
|
rv.AppendJoin('/', simplifiedSegmentsReversed.Reverse<string>());
|
||||||
|
|
||||||
|
return '/' + (rv.ToString().TrimEnd('/') + (fwdSlashUrl.EndsWith('/') ? "/" : "")).TrimStart('/');
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ProcessRequestAsync(HttpListenerContext ctx) {
|
private async Task ProcessRequestAsync(HttpListenerContext ctx) {
|
||||||
using RequestContext rc = new RequestContext(ctx);
|
using RequestContext rc = new RequestContext(ctx);
|
||||||
try {
|
try {
|
||||||
var decUri = WebUtility.UrlDecode(ctx.Request.RawUrl)!; // TODO add path escape countermeasures+unittests
|
var decUri = WebUtility.UrlDecode(ctx.Request.RawUrl)!; // TODO add path escape countermeasures+unittests
|
||||||
var splitted = NormalizeUrlPath(decUri).Split('?', 2, StringSplitOptions.None);
|
var splitted = decUri.Split('?', 2, StringSplitOptions.None);
|
||||||
var path = WebUtility.UrlDecode(splitted.First());
|
var reqPath = NormalizeUrlPath(WebUtility.UrlDecode(splitted.First()));
|
||||||
|
|
||||||
|
|
||||||
if (simpleEndpointMethodInfos.TryGetValue((path, ctx.Request.HttpMethod.ToUpperInvariant()), out var endpointInvocationInfo)) {
|
if (simpleEndpointMethodInfos.TryGetValue((reqPath, ctx.Request.HttpMethod.ToUpperInvariant()), out var endpointInvocationInfo)) {
|
||||||
var mi = endpointInvocationInfo.methodInfo;
|
var mi = endpointInvocationInfo.methodInfo;
|
||||||
var qparams = endpointInvocationInfo.queryParameters;
|
var qparams = endpointInvocationInfo.queryParameters;
|
||||||
var args = splitted.Length == 2 ? splitted[1] : null;
|
var args = splitted.Length == 2 ? splitted[1] : null;
|
||||||
|
|
@ -193,13 +242,23 @@ public sealed class HttpServer {
|
||||||
|
|
||||||
await (Task) (mi.Invoke(null, convertedQParamValues) ?? throw new NullReferenceException("Website func returned null unexpectedly"));
|
await (Task) (mi.Invoke(null, convertedQParamValues) ?? throw new NullReferenceException("Website func returned null unexpectedly"));
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
foreach (var (k, v) in staticServePaths) {
|
||||||
|
if (k.StartsWith(reqPath)) { // do a static serve
|
||||||
|
// TODO finish and add path traversal checking
|
||||||
|
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// invoke 404
|
// invoke 404
|
||||||
await HandleDefaultErrorPageAsync(rc, 404);
|
await HandleDefaultErrorPageAsync(rc, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
await HandleDefaultErrorPageAsync(rc, 500);
|
await HandleDefaultErrorPageAsync(rc, 500);
|
||||||
logger.Fatal($"Caught otherwise uncaught exception while ProcessingRequest:\n{ex}");
|
mainLogger.Fatal($"Caught otherwise uncaught exception while ProcessingRequest:\n{ex}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user