Compare commits
4 Commits
9082d7a4d0
...
2b5d6b6a54
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b5d6b6a54 | |||
| 386d71260c | |||
| dc35725b64 | |||
| 7d814ee4cb |
@ -2,7 +2,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!--package info-->
|
<!--package info-->
|
||||||
<PackageId>DTLib.Web</PackageId>
|
<PackageId>DTLib.Web</PackageId>
|
||||||
<Version>1.0.0</Version>
|
<Version>1.1.2</Version>
|
||||||
<Authors>Timerix</Authors>
|
<Authors>Timerix</Authors>
|
||||||
<Description>HTTP Server with simple routing</Description>
|
<Description>HTTP Server with simple routing</Description>
|
||||||
<RepositoryType>GIT</RepositoryType>
|
<RepositoryType>GIT</RepositoryType>
|
||||||
@ -25,6 +25,6 @@
|
|||||||
<ProjectReference Include="..\DTLib\DTLib.csproj" />
|
<ProjectReference Include="..\DTLib\DTLib.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Condition=" '$(Configuration)' != 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' != 'Debug' ">
|
||||||
<PackageReference Include="DTLib" Version="1.6.*" />
|
<PackageReference Include="DTLib" Version="1.6.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
namespace DTLib.Web.Routes;
|
namespace DTLib.Web.Routes;
|
||||||
|
|
||||||
public class DelegateRoute(Func<HttpListenerContext, Task<HttpStatusCode>> routeHandler) : Route
|
public class DelegateRouteHandler(Func<HttpListenerContext, Task<HttpStatusCode>> routeHandler) : RouteHandler
|
||||||
{
|
{
|
||||||
public override Task<HttpStatusCode> HandleRequest(HttpListenerContext ctx) => routeHandler(ctx);
|
public override Task<HttpStatusCode> HandleRequest(HttpListenerContext ctx) => routeHandler(ctx);
|
||||||
}
|
}
|
||||||
6
DTLib.Web/Routes/IRouter.cs
Normal file
6
DTLib.Web/Routes/IRouter.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace DTLib.Web.Routes;
|
||||||
|
|
||||||
|
public interface IRouter
|
||||||
|
{
|
||||||
|
Task<HttpStatusCode> Resolve(HttpListenerContext ctx);
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
namespace DTLib.Web.Routes;
|
namespace DTLib.Web.Routes;
|
||||||
|
|
||||||
public abstract class Route
|
public abstract class RouteHandler
|
||||||
{
|
{
|
||||||
public abstract Task<HttpStatusCode> HandleRequest(HttpListenerContext ctx);
|
public abstract Task<HttpStatusCode> HandleRequest(HttpListenerContext ctx);
|
||||||
}
|
}
|
||||||
@ -1,33 +0,0 @@
|
|||||||
namespace DTLib.Web.Routes;
|
|
||||||
|
|
||||||
public class ServeFilesRoute(IOPath _publicDir, string _homePageUrl = "index.html") : Route
|
|
||||||
{
|
|
||||||
public override async Task<HttpStatusCode> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
40
DTLib.Web/Routes/ServeFilesRouteHandler.cs
Normal file
40
DTLib.Web/Routes/ServeFilesRouteHandler.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
namespace DTLib.Web.Routes;
|
||||||
|
|
||||||
|
public class ServeFilesRouteHandler(IOPath _publicDir, string _homePageUrl = "index.html") : RouteHandler
|
||||||
|
{
|
||||||
|
public override async Task<HttpStatusCode> HandleRequest(HttpListenerContext ctx)
|
||||||
|
{
|
||||||
|
if (ctx.Request.HttpMethod != "GET")
|
||||||
|
return HttpStatusCode.BadRequest;
|
||||||
|
|
||||||
|
string requestPath = ctx.Request.Url?.AbsolutePath!;
|
||||||
|
if (string.IsNullOrEmpty(requestPath) || requestPath == "/")
|
||||||
|
{
|
||||||
|
requestPath = _homePageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
string ext = Path.Extension(requestPath).Str;
|
||||||
|
IOPath filePath = Path.Concat(_publicDir, requestPath);
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
return HttpStatusCode.NotFound;
|
||||||
|
|
||||||
|
List<(string key, string val)> headers = ext switch
|
||||||
|
{
|
||||||
|
"html" => [("Content-Type", "text/html")],
|
||||||
|
"css" => [("Content-Type", "text/css")],
|
||||||
|
"js" or "jsx" or "ts" or "tsx" or "map" => [("Content-Type", "text/javascript")],
|
||||||
|
_ =>
|
||||||
|
[
|
||||||
|
("Content-Type", "binary/octet-stream"),
|
||||||
|
("Content-Disposition", $"attachment filename={filePath.LastName()}")
|
||||||
|
]
|
||||||
|
};
|
||||||
|
foreach (var header in headers)
|
||||||
|
ctx.Response.Headers.Set(header.key, header.val);
|
||||||
|
|
||||||
|
var fileStream = File.OpenRead(filePath);
|
||||||
|
ctx.Response.ContentLength64 = fileStream.Length;
|
||||||
|
await fileStream.CopyToAsync(ctx.Response.OutputStream);
|
||||||
|
return HttpStatusCode.OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
48
DTLib.Web/Routes/SimpleRouter.cs
Normal file
48
DTLib.Web/Routes/SimpleRouter.cs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
namespace DTLib.Web.Routes;
|
||||||
|
|
||||||
|
public class SimpleRouter : IRouter
|
||||||
|
{
|
||||||
|
/// route for base url
|
||||||
|
public RouteHandler? HomePageRoute = null;
|
||||||
|
/// route for any url that doesn't have its own handler
|
||||||
|
public RouteHandler? DefaultRoute = null;
|
||||||
|
|
||||||
|
|
||||||
|
private readonly Dictionary<string, RouteHandler> _routes = new();
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
public SimpleRouter(ILogger logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void MapRoute(string url, Func<HttpListenerContext, Task<HttpStatusCode>> route)
|
||||||
|
=> MapRoute(url, new DelegateRouteHandler(route));
|
||||||
|
|
||||||
|
public void MapRoute(string url, RouteHandler route) => _routes.Add(url, route);
|
||||||
|
|
||||||
|
public async Task<HttpStatusCode> Resolve(HttpListenerContext ctx)
|
||||||
|
{
|
||||||
|
string requestPath = ctx.Request.Url?.AbsolutePath ?? "/";
|
||||||
|
RouteHandler? 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(nameof(SimpleRouter), $"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();
|
||||||
|
ctx.Response.OutputStream.Close();
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,32 +3,27 @@ global using System.Collections.Generic;
|
|||||||
global using System.Text;
|
global using System.Text;
|
||||||
global using System.Threading;
|
global using System.Threading;
|
||||||
global using System.Threading.Tasks;
|
global using System.Threading.Tasks;
|
||||||
global using DTLib;
|
|
||||||
global using DTLib.Demystifier;
|
|
||||||
global using DTLib.Filesystem;
|
global using DTLib.Filesystem;
|
||||||
global using DTLib.Logging;
|
global using DTLib.Logging;
|
||||||
global using System.Net;
|
global using System.Net;
|
||||||
|
using DTLib.Extensions;
|
||||||
using DTLib.Web.Routes;
|
using DTLib.Web.Routes;
|
||||||
|
|
||||||
namespace DTLib.Web;
|
namespace DTLib.Web;
|
||||||
|
|
||||||
internal class WebApp
|
public class WebApp
|
||||||
{
|
{
|
||||||
/// route for base url
|
private readonly string _baseUrl;
|
||||||
public Route? HomePageRoute = null;
|
private readonly ContextLogger _logger;
|
||||||
/// route for any url that doesn't have its own handler
|
private readonly IRouter _router;
|
||||||
public Route? DefaultRoute = null;
|
private readonly CancellationToken _stopToken;
|
||||||
|
|
||||||
private ContextLogger _logger;
|
public WebApp(string baseUrl, ILogger logger, IRouter router, CancellationToken stopToken = default)
|
||||||
private string _baseUrl;
|
|
||||||
private CancellationToken _stopToken;
|
|
||||||
private Dictionary<string, Route> routes = new();
|
|
||||||
|
|
||||||
public WebApp(ILogger logger, string baseUrl, CancellationToken stopToken)
|
|
||||||
{
|
{
|
||||||
_logger = new ContextLogger(nameof(WebApp), logger);
|
_logger = new ContextLogger(nameof(WebApp), logger);
|
||||||
_baseUrl = baseUrl;
|
_baseUrl = baseUrl;
|
||||||
_stopToken = stopToken;
|
_stopToken = stopToken;
|
||||||
|
_router = router;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Run()
|
public async Task Run()
|
||||||
@ -38,10 +33,10 @@ internal class WebApp
|
|||||||
server.Prefixes.Add(_baseUrl);
|
server.Prefixes.Add(_baseUrl);
|
||||||
server.Start();
|
server.Start();
|
||||||
_logger.LogInfo("server started");
|
_logger.LogInfo("server started");
|
||||||
long requestId = 0;
|
long requestId = 1;
|
||||||
while (!_stopToken.IsCancellationRequested)
|
while (!_stopToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
var ctx = await server.GetContextAsync();
|
var ctx = await server.GetContextAsync().AsCancellable(_stopToken);
|
||||||
HandleRequestAsync(ctx, requestId);
|
HandleRequestAsync(ctx, requestId);
|
||||||
requestId++;
|
requestId++;
|
||||||
}
|
}
|
||||||
@ -58,8 +53,8 @@ internal class WebApp
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInfo(logContext, $"[{ctx.Request.HttpMethod}] {ctx.Request.RawUrl} from {ctx.Request.RemoteEndPoint} ...");
|
_logger.LogInfo(logContext, $"[{ctx.Request.HttpMethod}] {ctx.Request.RawUrl} from {ctx.Request.RemoteEndPoint} ...");
|
||||||
var status = await Resolve(ctx);
|
var status = await _router.Resolve(ctx);
|
||||||
_logger.LogInfo(logContext, status);
|
_logger.LogInfo(logContext, $"{(int)status} ({status})");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -67,33 +62,4 @@ internal class WebApp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void MapRoute(string url, Func<HttpListenerContext, Task<HttpStatusCode>> route)
|
|
||||||
=> MapRoute(url, new DelegateRoute(route));
|
|
||||||
|
|
||||||
public void MapRoute(string url, Route route) => routes.Add(url, route);
|
|
||||||
|
|
||||||
public async Task<HttpStatusCode> 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -2,7 +2,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!--package info-->
|
<!--package info-->
|
||||||
<PackageId>DTLib</PackageId>
|
<PackageId>DTLib</PackageId>
|
||||||
<Version>1.6.3</Version>
|
<Version>1.6.5</Version>
|
||||||
<Authors>Timerix</Authors>
|
<Authors>Timerix</Authors>
|
||||||
<Description>Library for all my C# projects</Description>
|
<Description>Library for all my C# projects</Description>
|
||||||
<RepositoryType>GIT</RepositoryType>
|
<RepositoryType>GIT</RepositoryType>
|
||||||
|
|||||||
37
DTLib/Extensions/TaskExtensions.cs
Normal file
37
DTLib/Extensions/TaskExtensions.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
namespace DTLib.Extensions;
|
||||||
|
|
||||||
|
public static class TaskExtensions
|
||||||
|
{
|
||||||
|
// https://stackoverflow.com/a/69861689
|
||||||
|
public static Task<T> AsCancellable<T>(this Task<T> task, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (!token.CanBeCanceled)
|
||||||
|
{
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tcs = new TaskCompletionSource<T>();
|
||||||
|
// This cancels the returned task:
|
||||||
|
// 1. If the token has been canceled, it cancels the TCS straightaway
|
||||||
|
// 2. Otherwise, it attempts to cancel the TCS whenever
|
||||||
|
// the token indicates cancelled
|
||||||
|
token.Register(() => tcs.TrySetCanceled(token),
|
||||||
|
useSynchronizationContext: false);
|
||||||
|
|
||||||
|
task.ContinueWith(t =>
|
||||||
|
{
|
||||||
|
// Complete the TCS per task status
|
||||||
|
// If the TCS has been cancelled, this continuation does nothing
|
||||||
|
if (task.IsCanceled)
|
||||||
|
tcs.TrySetCanceled();
|
||||||
|
else if (task.IsFaulted)
|
||||||
|
tcs.TrySetException(t.Exception!);
|
||||||
|
else tcs.TrySetResult(t.Result);
|
||||||
|
},
|
||||||
|
CancellationToken.None,
|
||||||
|
TaskContinuationOptions.ExecuteSynchronously,
|
||||||
|
TaskScheduler.Default);
|
||||||
|
|
||||||
|
return tcs.Task;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,29 +15,40 @@ public readonly struct IOPath
|
|||||||
|
|
||||||
public IOPath(char[] path, bool separatorsFixed=false)
|
public IOPath(char[] path, bool separatorsFixed=false)
|
||||||
{
|
{
|
||||||
|
if (path.Length == 0)
|
||||||
|
throw new Exception("path is null or empty");
|
||||||
Str = separatorsFixed ? new string(path) : FixSeparators(path);
|
Str = separatorsFixed ? new string(path) : FixSeparators(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IOPath(string path, bool separatorsFixed=false)
|
public IOPath(string path, bool separatorsFixed=false) : this(path.ToCharArray(), separatorsFixed)
|
||||||
{
|
{
|
||||||
if (path.IsNullOrEmpty())
|
|
||||||
throw new Exception("path is null or empty");
|
|
||||||
Str = separatorsFixed ? path : FixSeparators(path.ToCharArray());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static string FixSeparators(char[] path)
|
static string FixSeparators(char[] path)
|
||||||
{
|
{
|
||||||
int length = path.Length;
|
int length = path.Length;
|
||||||
if (path[length-1] == Path.Sep || path[length-1] == Path.NotSep)
|
char lastChar = path[path.Length - 1];
|
||||||
|
if (lastChar == Path.Sep || lastChar == Path.NotSep)
|
||||||
length--; // removing trailing sep
|
length--; // removing trailing sep
|
||||||
char[] fixed_path = new char[length];
|
StringBuilder sb = new StringBuilder();
|
||||||
|
bool prevWasSeparator = false;
|
||||||
for (int i = 0; i < length; i++)
|
for (int i = 0; i < length; i++)
|
||||||
{
|
{
|
||||||
if (path[i] == Path.NotSep)
|
if (path[i] == Path.NotSep)
|
||||||
fixed_path[i] = Path.Sep;
|
{
|
||||||
else fixed_path[i] = path[i];
|
// prevent double separators like this "a//b"
|
||||||
|
if(!prevWasSeparator)
|
||||||
|
sb.Append(Path.Sep);
|
||||||
|
prevWasSeparator = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Append(path[i]);
|
||||||
|
prevWasSeparator = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return new string(fixed_path);
|
|
||||||
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IOPath[] ArrayCast(string[] a, bool separatorsFixed=false)
|
public static IOPath[] ArrayCast(string[] a, bool separatorsFixed=false)
|
||||||
|
|||||||
@ -96,10 +96,13 @@ public static class Path
|
|||||||
/// returns just dir name or file name with extension
|
/// returns just dir name or file name with extension
|
||||||
public static IOPath LastName(this IOPath path)
|
public static IOPath LastName(this IOPath path)
|
||||||
{
|
{
|
||||||
|
if(path.Length < 2)
|
||||||
|
return path;
|
||||||
int i = path.LastIndexOf(Sep);
|
int i = path.LastIndexOf(Sep);
|
||||||
if (i == path.Length - 1) // ends with separator
|
if (i == path.Length - 1) // ends with separator
|
||||||
i = path.LastIndexOf(Sep, i - 1);
|
i = path.LastIndexOf(Sep, i - 1);
|
||||||
if (i == -1) return path;
|
if (i == -1)
|
||||||
|
return path;
|
||||||
return path.Substring(i + 1);
|
return path.Substring(i + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user