diff --git a/DTLib.Demystifier b/DTLib.Demystifier index b19c39b..bb96774 160000 --- a/DTLib.Demystifier +++ b/DTLib.Demystifier @@ -1 +1 @@ -Subproject commit b19c39b68463ae15c1f63051583da9f74d97baac +Subproject commit bb96774c37ef7b636139872b972e5252076bc913 diff --git a/DTLib.Logging.Microsoft/DTLib.Logging.Microsoft.csproj b/DTLib.Logging.Microsoft/DTLib.Logging.Microsoft.csproj index 2a0e14b..f35617a 100644 --- a/DTLib.Logging.Microsoft/DTLib.Logging.Microsoft.csproj +++ b/DTLib.Logging.Microsoft/DTLib.Logging.Microsoft.csproj @@ -13,7 +13,7 @@ netstandard2.0;netstandard2.1 - 12 + latest disable disable diff --git a/DTLib.Web/DTLib.Web.csproj b/DTLib.Web/DTLib.Web.csproj new file mode 100644 index 0000000..688783f --- /dev/null +++ b/DTLib.Web/DTLib.Web.csproj @@ -0,0 +1,30 @@ + + + + DTLib.Web + 1.0.0 + Timerix + HTTP Server with simple routing + GIT + https://timerix.ddns.net:3322/Timerix/DTLib + https://timerix.ddns.net:3322/Timerix/DTLib + Release + MIT + + netstandard2.0;netstandard2.1 + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + latest + disable + enable + False + + + + + + + + + + diff --git a/DTLib.Web/Routes/DelegateRoute.cs b/DTLib.Web/Routes/DelegateRoute.cs new file mode 100644 index 0000000..f4ff3e0 --- /dev/null +++ b/DTLib.Web/Routes/DelegateRoute.cs @@ -0,0 +1,6 @@ +namespace DTLib.Web.Routes; + +public class DelegateRoute(Func> routeHandler) : Route +{ + public override Task HandleRequest(HttpListenerContext ctx) => routeHandler(ctx); +} \ No newline at end of file diff --git a/DTLib.Web/Routes/Route.cs b/DTLib.Web/Routes/Route.cs new file mode 100644 index 0000000..c72a3e2 --- /dev/null +++ b/DTLib.Web/Routes/Route.cs @@ -0,0 +1,6 @@ +namespace DTLib.Web.Routes; + +public abstract class Route +{ + public abstract Task HandleRequest(HttpListenerContext ctx); +} \ No newline at end of file diff --git a/DTLib.Web/Routes/ServeFilesRoute.cs b/DTLib.Web/Routes/ServeFilesRoute.cs new file mode 100644 index 0000000..2f68f9f --- /dev/null +++ b/DTLib.Web/Routes/ServeFilesRoute.cs @@ -0,0 +1,33 @@ +namespace DTLib.Web.Routes; + +public class ServeFilesRoute(IOPath _publicDir, string _homePageUrl = "index.html") : Route +{ + public override async Task HandleRequest(HttpListenerContext ctx) + { + if (ctx.Request.HttpMethod != "GET") + return HttpStatusCode.BadRequest; + + string requestPath = ctx.Request.Url?.AbsolutePath ?? "/"; + if (requestPath == "/") + requestPath = _homePageUrl; + string ext = Path.Extension(requestPath).Str; + IOPath filePath = Path.Concat(_publicDir, requestPath); + if (!File.Exists(filePath)) + return HttpStatusCode.NotFound; + + string contentType = ext switch + { + "html" => "text/html", + "css" => "text/css", + "js" or "jsx" or "ts" or "tsx" or "map" => "text/javascript", + _ => "binary/octet-stream" + }; + ctx.Response.Headers.Set("Content-Type", contentType); + ctx.Response.Headers.Set("Content-Disposition", "attachment filename=" + filePath.LastName()); + + var fileStream = File.OpenRead(filePath); + ctx.Response.ContentLength64 = fileStream.Length; + await fileStream.CopyToAsync(ctx.Response.OutputStream); + return HttpStatusCode.OK; + } +} \ No newline at end of file diff --git a/DTLib.Web/WebApp.cs b/DTLib.Web/WebApp.cs new file mode 100644 index 0000000..9bdc947 --- /dev/null +++ b/DTLib.Web/WebApp.cs @@ -0,0 +1,99 @@ +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; + } +} \ No newline at end of file diff --git a/DTLib.XXHash b/DTLib.XXHash index 895d53d..9360dfe 160000 --- a/DTLib.XXHash +++ b/DTLib.XXHash @@ -1 +1 @@ -Subproject commit 895d53d362c83b114c9ca06467d2e24d407190c3 +Subproject commit 9360dfe30549732bb95d16268218a6458d2229b3 diff --git a/DTLib.sln b/DTLib.sln index 6ec6891..aef04a8 100644 --- a/DTLib.sln +++ b/DTLib.sln @@ -22,6 +22,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DTLib.Logging.Microsoft", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DTLib.XXHash", "DTLib.XXHash\DTLib.XXHash\DTLib.XXHash.csproj", "{C7029741-816D-41B2-A2C4-E20565B1739D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DTLib.Web", "DTLib.Web\DTLib.Web.csproj", "{9A3220EB-CCED-4172-9BD4-C3700FF36539}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -44,6 +46,10 @@ Global {C7029741-816D-41B2-A2C4-E20565B1739D}.Debug|Any CPU.Build.0 = Debug|Any CPU {C7029741-816D-41B2-A2C4-E20565B1739D}.Release|Any CPU.ActiveCfg = Release|Any CPU {C7029741-816D-41B2-A2C4-E20565B1739D}.Release|Any CPU.Build.0 = Release|Any CPU + {9A3220EB-CCED-4172-9BD4-C3700FF36539}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A3220EB-CCED-4172-9BD4-C3700FF36539}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A3220EB-CCED-4172-9BD4-C3700FF36539}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A3220EB-CCED-4172-9BD4-C3700FF36539}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/DTLib/DTLib.csproj b/DTLib/DTLib.csproj index 57ee493..0a8f4bc 100644 --- a/DTLib/DTLib.csproj +++ b/DTLib/DTLib.csproj @@ -14,7 +14,7 @@ netstandard2.0;netstandard2.1 $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb - 12 + latest disable enable False diff --git a/pack.sh b/pack.sh index f809178..56e54f5 100755 --- a/pack.sh +++ b/pack.sh @@ -16,7 +16,7 @@ function build_package() { packages_to_build="$@" if [ -z "$packages_to_build" ]; then - packages_to_build='DTLib.Demystifier DTLib.XXHash DTLib DTLib.Logging.Microsoft' + packages_to_build='DTLib.Demystifier DTLib.XXHash DTLib DTLib.Logging.Microsoft DTLib.Web' fi echo "building packages $packages_to_build" for p in $packages_to_build; do