global using System; global using System.Collections.Generic; global using System.Text; global using System.Threading; global using System.Threading.Tasks; global using DTLib; global using DTLib.Demystifier; global using DTLib.Filesystem; global using DTLib.Logging; global using System.Net; using DTLib.Web.Routes; namespace DTLib.Web; internal class WebApp { /// route for base url public Route? HomePageRoute = null; /// route for any url that doesn't have its own handler public Route? DefaultRoute = null; private ContextLogger _logger; private string _baseUrl; private CancellationToken _stopToken; private Dictionary routes = new(); public WebApp(ILogger logger, string baseUrl, CancellationToken stopToken) { _logger = new ContextLogger(nameof(WebApp), logger); _baseUrl = baseUrl; _stopToken = stopToken; } public async Task Run() { _logger.LogInfo($"starting webserver at {_baseUrl} ..."); HttpListener server = new HttpListener(); server.Prefixes.Add(_baseUrl); server.Start(); _logger.LogInfo("server started"); long requestId = 0; while (!_stopToken.IsCancellationRequested) { var ctx = await server.GetContextAsync(); HandleRequestAsync(ctx, requestId); requestId++; } // stop server.Stop(); _logger.LogInfo("server stopped"); } // ReSharper disable once AsyncVoidMethod private async void HandleRequestAsync(HttpListenerContext ctx, long requestId) { string logContext = $"Request {requestId}"; try { _logger.LogInfo(logContext, $"[{ctx.Request.HttpMethod}] {ctx.Request.RawUrl} from {ctx.Request.RemoteEndPoint} ..."); var status = await Resolve(ctx); _logger.LogInfo(logContext, status); } catch (Exception ex) { _logger.LogWarn(logContext, ex); } } public void MapRoute(string url, Func> route) => MapRoute(url, new DelegateRoute(route)); public void MapRoute(string url, Route route) => routes.Add(url, route); public async Task Resolve(HttpListenerContext ctx) { string requestPath = ctx.Request.Url?.AbsolutePath ?? "/"; Route? route; if(HomePageRoute != null && requestPath == "/") route = HomePageRoute; else if (routes.TryGetValue(requestPath, out var routeDelegate)) route = routeDelegate; else route = DefaultRoute; HttpStatusCode status; if (route == null) { _logger.LogWarn("couldn't resolve request path {requestPath}"); status = HttpStatusCode.NotFound; } else status = await route.HandleRequest(ctx); ctx.Response.StatusCode = (int)status; await ctx.Response.OutputStream.FlushAsync(_stopToken); ctx.Response.OutputStream.Close(); return status; } }