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