implemented check of HttpMethod as binary flags

This commit is contained in:
Timerix 2025-05-22 21:24:05 +05:00
parent e4ee03364c
commit 8d55c1c533
5 changed files with 63 additions and 37 deletions

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<!--package info--> <!--package info-->
<PackageId>DTLib.Web</PackageId> <PackageId>DTLib.Web</PackageId>
<Version>1.3.1</Version> <Version>1.4.0</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>

View File

@ -1,15 +1,18 @@
namespace DTLib.Web; namespace DTLib.Web;
/// <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods"/> /// <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods"/>
public enum HttpMethod [Flags]
public enum HttpMethod : ushort
{ {
GET, NONE = 0,
POST, GET = 1,
PUT, POST = 2,
DELETE, PUT = 4,
PATCH, DELETE = 8,
HEAD, PATCH = 16,
OPTIONS, HEAD = 32,
TRACE, OPTIONS = 64,
CONNECT TRACE = 128,
CONNECT = 256,
ANY = 65535
} }

View File

@ -2,5 +2,5 @@ namespace DTLib.Web.Routes;
public interface IRouter public interface IRouter
{ {
Task<HttpStatusCode> Resolve(HttpListenerContext ctx, ContextLogger requestLogger); Task Resolve(HttpListenerContext ctx, ContextLogger requestLogger);
} }

View File

@ -3,42 +3,63 @@ namespace DTLib.Web.Routes;
public class SimpleRouter : IRouter public class SimpleRouter : IRouter
{ {
/// route for any url that doesn't have its own handler /// route for any url that doesn't have its own handler
public IRouteHandler? DefaultRoute { get; set; } public record RouteWithMethod(HttpMethod method, IRouteHandler routeHandler)
{
private readonly Dictionary<string, IRouteHandler> _routes = new(); public bool CheckMethod(HttpMethod requestMethod) => (requestMethod & method) != 0;
public bool CheckMethod(string requestMethodStr)
=> Enum.TryParse<HttpMethod>(requestMethodStr, out var requestMethod)
&& CheckMethod(requestMethod);
}
private readonly Dictionary<string, RouteWithMethod> _routes = new();
private readonly ILogger _logger; private readonly ILogger _logger;
public RouteWithMethod? DefaultRoute { get; set; }
public SimpleRouter(ILogger logger) public SimpleRouter(ILogger logger)
{ {
_logger = new ContextLogger(nameof(SimpleRouter), logger); _logger = new ContextLogger(nameof(SimpleRouter), logger);
} }
public void MapRoute(string url, HttpMethod method, IRouteHandler route) => _routes.Add($"{url}:{method}", route); public void MapRoute(string url, HttpMethod method, IRouteHandler route)
=> _routes.Add(url, new RouteWithMethod(method, route));
public void MapRoute(string url, HttpMethod method, public void MapRoute(string url, HttpMethod method,
Func<HttpListenerContext, ContextLogger, Task<HttpStatusCode>> route) Func<HttpListenerContext, ContextLogger, Task<HttpStatusCode>> route)
=> MapRoute(url, method, new DelegateRouteHandler(route)); => MapRoute(url, method, new DelegateRouteHandler(route));
public async Task<HttpStatusCode> Resolve(HttpListenerContext ctx, ContextLogger requestLogger) public async Task Resolve(HttpListenerContext ctx, ContextLogger requestLogger)
{ {
HttpStatusCode status = HttpStatusCode.InternalServerError;
string? requestPath = ctx.Request.Url?.AbsolutePath; try
if (string.IsNullOrEmpty(requestPath))
requestPath = "/";
if (!_routes.TryGetValue($"{requestPath}:{ctx.Request.HttpMethod}", out var route))
route = DefaultRoute;
HttpStatusCode status;
if (route == null)
{ {
_logger.LogWarn(nameof(SimpleRouter), $"couldn't resolve request path {requestPath}"); string? requestPath = ctx.Request.Url?.AbsolutePath;
status = HttpStatusCode.NotFound; if (string.IsNullOrEmpty(requestPath))
} requestPath = "/";
else status = await route.HandleRequest(ctx, requestLogger);
if(!_routes.TryGetValue(requestPath!, out var routeWithMethod))
routeWithMethod = DefaultRoute;
ctx.Response.StatusCode = (int)status; if (routeWithMethod is null)
await ctx.Response.OutputStream.FlushAsync(); {
ctx.Response.OutputStream.Close(); _logger.LogWarn(nameof(SimpleRouter),
return status; $"couldn't resolve request path {ctx.Request.HttpMethod} {requestPath}");
status = HttpStatusCode.NotFound;
}
else if (!routeWithMethod.CheckMethod(ctx.Request.HttpMethod))
{
_logger.LogWarn(nameof(SimpleRouter),
$"received request with invalid method {ctx.Request.HttpMethod} {requestPath}");
status = HttpStatusCode.MethodNotAllowed;
}
else status = await routeWithMethod.routeHandler.HandleRequest(ctx, requestLogger);
}
finally
{
ctx.Response.StatusCode = (int)status;
await ctx.Response.OutputStream.FlushAsync();
ctx.Response.OutputStream.Close();
}
} }
} }

View File

@ -58,9 +58,11 @@ public class WebApp
requestLogger.LogInfo($"{ctx.Request.HttpMethod} {ctx.Request.RawUrl} from {ctx.Request.RemoteEndPoint}..."); requestLogger.LogInfo($"{ctx.Request.HttpMethod} {ctx.Request.RawUrl} from {ctx.Request.RemoteEndPoint}...");
var stopwatch = new Stopwatch(); var stopwatch = new Stopwatch();
stopwatch.Start(); stopwatch.Start();
var status = await _router.Resolve(ctx, requestLogger); await _router.Resolve(ctx, requestLogger);
stopwatch.Stop(); stopwatch.Stop();
requestLogger.LogInfo($"responded {(int)status} ({status}) in {stopwatch.ElapsedMilliseconds}ms"); requestLogger.LogInfo($"responded {ctx.Response.StatusCode}" +
$" ({(HttpStatusCode)ctx.Response.StatusCode})" +
$" in {stopwatch.ElapsedMilliseconds}ms");
} }
catch (Exception ex) catch (Exception ex)
{ {