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}"); } }