diff --git a/SimpleHttpServer/HttpServer.cs b/SimpleHttpServer/HttpServer.cs
index d736b76..a1b136d 100644
--- a/SimpleHttpServer/HttpServer.cs
+++ b/SimpleHttpServer/HttpServer.cs
@@ -4,6 +4,7 @@ using SimpleHttpServer.Types.ParameterConverters;
using System.Net;
using System.Numerics;
using System.Reflection;
+using System.Text;
namespace SimpleHttpServer;
@@ -13,7 +14,8 @@ public sealed class HttpServer {
private readonly HttpListener listener;
private Task? listenerTask;
- private readonly Logger logger;
+ private readonly Logger mainLogger;
+ private readonly Logger requestLogger;
private readonly SimpleHttpServerConfiguration conf;
private bool shutdown = false;
@@ -22,19 +24,20 @@ public sealed class HttpServer {
conf = configuration;
listener = new HttpListener();
listener.Prefixes.Add($"http://localhost:{port}/");
- logger = new(LogOutputTopic.Main, conf);
+ mainLogger = new(LogOutputTopic.Main, conf);
+ requestLogger = new(LogOutputTopic.Request, conf);
}
public void Start() {
- logger.Information($"Starting on port {Port}...");
+ mainLogger.Information($"Starting on port {Port}...");
Assert(listenerTask == null, "Server was already started!");
listener.Start();
listenerTask = Task.Run(GetContextLoopAsync);
- logger.Information($"Ready to handle requests!");
+ mainLogger.Information($"Ready to handle requests!");
}
public async Task StopAsync(CancellationToken ctok) {
- logger.Information("Stopping server...");
+ mainLogger.Information("Stopping server...");
Assert(listenerTask != null, "Server was not started!");
shutdown = true;
listener.Stop();
@@ -48,7 +51,7 @@ public sealed class HttpServer {
_ = 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 (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");
+ mainLogger.Information($"Registered endpoint: '{reqMethod} {normLocation}'");
simpleEndpointMethodInfos.Add((normLocation, reqMethod), new EndpointInvocationInfo(mi, qparams));
}
}
}
+ ///
+ /// Serves all files located in on a website path that is relative to ,
+ /// while restricting requests to inside the local filesystem directory. Static serving has a lower priority than registering an endpoint.
+ ///
+ ///
+ ///
+ 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 staticServePaths = new Dictionary();
+
private readonly Dictionary 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 simplifiedSegmentsReversed = new List();
+ 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());
+
+ return '/' + (rv.ToString().TrimEnd('/') + (fwdSlashUrl.EndsWith('/') ? "/" : "")).TrimStart('/');
+ }
private async Task ProcessRequestAsync(HttpListenerContext ctx) {
using RequestContext rc = new RequestContext(ctx);
try {
var decUri = WebUtility.UrlDecode(ctx.Request.RawUrl)!; // TODO add path escape countermeasures+unittests
- var splitted = NormalizeUrlPath(decUri).Split('?', 2, StringSplitOptions.None);
- var path = WebUtility.UrlDecode(splitted.First());
+ var splitted = decUri.Split('?', 2, StringSplitOptions.None);
+ 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 qparams = endpointInvocationInfo.queryParameters;
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"));
} 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
await HandleDefaultErrorPageAsync(rc, 404);
}
} catch (Exception ex) {
await HandleDefaultErrorPageAsync(rc, 500);
- logger.Fatal($"Caught otherwise uncaught exception while ProcessingRequest:\n{ex}");
+ mainLogger.Fatal($"Caught otherwise uncaught exception while ProcessingRequest:\n{ex}");
}
}