Compare commits
No commits in common. "2b5d6b6a544c16d24930d0ded504b1762e58bf5a" and "9082d7a4d054cb048d423d278bc503cd048a748b" have entirely different histories.
2b5d6b6a54
...
9082d7a4d0
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<!--package info-->
|
||||
<PackageId>DTLib.Web</PackageId>
|
||||
<Version>1.1.2</Version>
|
||||
<Version>1.0.0</Version>
|
||||
<Authors>Timerix</Authors>
|
||||
<Description>HTTP Server with simple routing</Description>
|
||||
<RepositoryType>GIT</RepositoryType>
|
||||
@ -25,6 +25,6 @@
|
||||
<ProjectReference Include="..\DTLib\DTLib.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition=" '$(Configuration)' != 'Debug' ">
|
||||
<PackageReference Include="DTLib" Version="1.6.5" />
|
||||
<PackageReference Include="DTLib" Version="1.6.*" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
namespace DTLib.Web.Routes;
|
||||
|
||||
public class DelegateRouteHandler(Func<HttpListenerContext, Task<HttpStatusCode>> routeHandler) : RouteHandler
|
||||
public class DelegateRoute(Func<HttpListenerContext, Task<HttpStatusCode>> routeHandler) : Route
|
||||
{
|
||||
public override Task<HttpStatusCode> HandleRequest(HttpListenerContext ctx) => routeHandler(ctx);
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
namespace DTLib.Web.Routes;
|
||||
|
||||
public interface IRouter
|
||||
{
|
||||
Task<HttpStatusCode> Resolve(HttpListenerContext ctx);
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
namespace DTLib.Web.Routes;
|
||||
|
||||
public abstract class RouteHandler
|
||||
public abstract class Route
|
||||
{
|
||||
public abstract Task<HttpStatusCode> HandleRequest(HttpListenerContext ctx);
|
||||
}
|
||||
33
DTLib.Web/Routes/ServeFilesRoute.cs
Normal file
33
DTLib.Web/Routes/ServeFilesRoute.cs
Normal file
@ -0,0 +1,33 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
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,27 +3,32 @@ 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.Extensions;
|
||||
using DTLib.Web.Routes;
|
||||
|
||||
namespace DTLib.Web;
|
||||
|
||||
public class WebApp
|
||||
internal class WebApp
|
||||
{
|
||||
private readonly string _baseUrl;
|
||||
private readonly ContextLogger _logger;
|
||||
private readonly IRouter _router;
|
||||
private readonly CancellationToken _stopToken;
|
||||
/// route for base url
|
||||
public Route? HomePageRoute = null;
|
||||
/// route for any url that doesn't have its own handler
|
||||
public Route? DefaultRoute = null;
|
||||
|
||||
public WebApp(string baseUrl, ILogger logger, IRouter router, CancellationToken stopToken = default)
|
||||
private ContextLogger _logger;
|
||||
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);
|
||||
_baseUrl = baseUrl;
|
||||
_stopToken = stopToken;
|
||||
_router = router;
|
||||
}
|
||||
|
||||
public async Task Run()
|
||||
@ -33,10 +38,10 @@ public class WebApp
|
||||
server.Prefixes.Add(_baseUrl);
|
||||
server.Start();
|
||||
_logger.LogInfo("server started");
|
||||
long requestId = 1;
|
||||
long requestId = 0;
|
||||
while (!_stopToken.IsCancellationRequested)
|
||||
{
|
||||
var ctx = await server.GetContextAsync().AsCancellable(_stopToken);
|
||||
var ctx = await server.GetContextAsync();
|
||||
HandleRequestAsync(ctx, requestId);
|
||||
requestId++;
|
||||
}
|
||||
@ -53,8 +58,8 @@ public class WebApp
|
||||
try
|
||||
{
|
||||
_logger.LogInfo(logContext, $"[{ctx.Request.HttpMethod}] {ctx.Request.RawUrl} from {ctx.Request.RemoteEndPoint} ...");
|
||||
var status = await _router.Resolve(ctx);
|
||||
_logger.LogInfo(logContext, $"{(int)status} ({status})");
|
||||
var status = await Resolve(ctx);
|
||||
_logger.LogInfo(logContext, status);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -62,4 +67,33 @@ public 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>
|
||||
<!--package info-->
|
||||
<PackageId>DTLib</PackageId>
|
||||
<Version>1.6.5</Version>
|
||||
<Version>1.6.3</Version>
|
||||
<Authors>Timerix</Authors>
|
||||
<Description>Library for all my C# projects</Description>
|
||||
<RepositoryType>GIT</RepositoryType>
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
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,40 +15,29 @@ public readonly struct IOPath
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public IOPath(string path, bool separatorsFixed=false) : this(path.ToCharArray(), separatorsFixed)
|
||||
public IOPath(string path, bool separatorsFixed=false)
|
||||
{
|
||||
if (path.IsNullOrEmpty())
|
||||
throw new Exception("path is null or empty");
|
||||
Str = separatorsFixed ? path : FixSeparators(path.ToCharArray());
|
||||
}
|
||||
|
||||
static string FixSeparators(char[] path)
|
||||
{
|
||||
int length = path.Length;
|
||||
char lastChar = path[path.Length - 1];
|
||||
if (lastChar == Path.Sep || lastChar == Path.NotSep)
|
||||
if (path[length-1] == Path.Sep || path[length-1] == Path.NotSep)
|
||||
length--; // removing trailing sep
|
||||
StringBuilder sb = new StringBuilder();
|
||||
bool prevWasSeparator = false;
|
||||
char[] fixed_path = new char[length];
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
if (path[i] == Path.NotSep)
|
||||
{
|
||||
// prevent double separators like this "a//b"
|
||||
if(!prevWasSeparator)
|
||||
sb.Append(Path.Sep);
|
||||
prevWasSeparator = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(path[i]);
|
||||
prevWasSeparator = false;
|
||||
}
|
||||
fixed_path[i] = Path.Sep;
|
||||
else fixed_path[i] = path[i];
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
return new string(fixed_path);
|
||||
}
|
||||
|
||||
public static IOPath[] ArrayCast(string[] a, bool separatorsFixed=false)
|
||||
|
||||
@ -96,13 +96,10 @@ public static class Path
|
||||
/// returns just dir name or file name with extension
|
||||
public static IOPath LastName(this IOPath path)
|
||||
{
|
||||
if(path.Length < 2)
|
||||
return path;
|
||||
int i = path.LastIndexOf(Sep);
|
||||
if (i == path.Length - 1) // ends with separator
|
||||
i = path.LastIndexOf(Sep, i - 1);
|
||||
if (i == -1)
|
||||
return path;
|
||||
if (i == -1) return path;
|
||||
return path.Substring(i + 1);
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user