router redesign again again

This commit is contained in:
uwaa 2024-10-28 10:59:27 +00:00
parent 3dd2c79b21
commit d948ab6191
9 changed files with 147 additions and 245 deletions

View file

@ -15,7 +15,7 @@ public sealed class HttpServer
/// <summary>
/// The router to be used to find and serve content to clients.
/// </summary>
public readonly IRouter Router;
public readonly RouterBase Router;
/// <summary>
/// If non-null, all connections will be secured via SSL using this certificate.
@ -41,7 +41,7 @@ public sealed class HttpServer
readonly TcpListener listener;
public HttpServer(int port, X509Certificate? certificate, IRouter router)
public HttpServer(int port, X509Certificate? certificate, RouterBase router)
{
listener = new TcpListener(IPAddress.Any, port);
Certificate = certificate;
@ -119,7 +119,9 @@ public sealed class HttpServer
await req.ReadAllHeaders();
//Parse path
ArraySegment<string> pathSpl = req.Path.Split('/', StringSplitOptions.RemoveEmptyEntries);
ArraySegment<string> pathSpl = req.Path.Split('/');
if (pathSpl.Count == 0)
pathSpl = new string[] { string.Empty };
for (int i = 0; i < pathSpl.Count; i++)
pathSpl[i] = WebUtility.UrlDecode(pathSpl[i]);
@ -128,7 +130,7 @@ public sealed class HttpServer
pathSpl = pathSpl.Slice(1);
//Execute
response = await Router.Handle(req, pathSpl);
response = await Router.GetResponse(req, pathSpl);
if (response != null)
{
await req.Write(response);

View file

@ -5,18 +5,15 @@ namespace MiniHTTP.Routing;
/// <summary>
/// Handles CORS preflight requests.
/// </summary>
public class CORS : IRouter
public class CORS : RouterBase
{
public CORS()
{
}
public override HttpMethod Method => HttpMethod.OPTIONS;
public Task<HttpResponse?> Handle(HttpRequest client, ArraySegment<string> path)
{
if (client.Method == HttpMethod.OPTIONS)
return Task.FromResult<HttpResponse?>(new PreflightResponse());
public override int Arguments => 0;
return Task.FromResult<HttpResponse?>(null);
protected override Task<HttpResponse?> GetResponseInner(HttpRequest client, ArraySegment<string> path)
{
return Task.FromResult<HttpResponse?>(new PreflightResponse());
}
}

View file

@ -1,17 +0,0 @@
using MiniHTTP.Responses;
namespace MiniHTTP.Routing;
/// <summary>
/// An asynchronous endpoint handler.
/// </summary>
/// <param name="request">The request being served.</param>
/// <returns>Returns a HTTP response if the request successfully hit. Returns null if the request misses the handler.</returns>
public delegate Task<HttpResponse?> EndpointHandlerAsync(HttpRequest request, RouteMatch route);
/// <summary>
/// An synchronous endpoint handler.
/// </summary>
/// <param name="request">The request being served.</param>
/// <returns>Returns a HTTP response if the request successfully hit. Returns null if the request misses the handler.</returns>
public delegate HttpResponse? EndpointHandler(HttpRequest request, RouteMatch route);

View file

@ -6,14 +6,8 @@ namespace MiniHTTP.Routing;
/// <summary>
/// Special router which serves static files from the filesystem.
/// </summary>
public partial class Static
public partial class FileEndpoint : RouterBase
{
public static EndpointHandlerAsync Create(string directory, string assetParameter)
{
Static s = new Static(directory, assetParameter);
return s.Handle;
}
static string GuessMIMEType(string extension)
{
return extension switch
@ -65,20 +59,18 @@ public partial class Static
/// </summary>
public string Directory;
/// <summary>
/// The name of the asset parameter in the route.
/// </summary>
public string AssetParameter;
public override HttpMethod Method => HttpMethod.GET;
public Static(string directory, string assetParameter)
public override int Arguments => 1;
public FileEndpoint(string directory)
{
Directory = directory;
AssetParameter = assetParameter;
}
public async Task<HttpResponse?> Handle(HttpRequest req, RouteMatch route)
protected override async Task<HttpResponse?> GetResponseInner(HttpRequest req, ArraySegment<string> path)
{
string? asset = route.GetVariable(AssetParameter);
string? asset = path[0];
if (string.IsNullOrWhiteSpace(asset))
return null;

View file

@ -0,0 +1,24 @@
using MiniHTTP.Responses;
namespace MiniHTTP.Routing;
public delegate Task<HttpResponse?> FuncEndpointDelegate(HttpRequest req);
public class FuncEndpoint : RouterBase
{
public FuncEndpointDelegate Function;
public FuncEndpoint(FuncEndpointDelegate function)
{
Function = function;
}
public override HttpMethod Method => HttpMethod.GET;
public override int Arguments => 0;
protected override Task<HttpResponse?> GetResponseInner(HttpRequest req, ArraySegment<string> path)
{
return Function(req);
}
}

View file

@ -1,11 +0,0 @@
using MiniHTTP.Responses;
namespace MiniHTTP.Routing;
/// <summary>
/// Parses a path and serves content to clients.
/// </summary>
public interface IRouter
{
Task<HttpResponse?> Handle(HttpRequest client, ArraySegment<string> path);
}

View file

@ -3,206 +3,60 @@
namespace MiniHTTP.Routing;
/// <summary>
/// Standard implementation for <see cref="IRouter"/>.
/// Selects one of multiple possible routers.
/// </summary>
public class Router : IRouter
public class Router : RouterBase
{
/// <summary>
/// Endpoints supported by this router.
/// </summary>
public readonly List<Endpoint> Endpoints = new List<Endpoint>();
public record Route(string? Key, RouterBase Router);
/// <summary>
/// The default endpoint handler to use if no suitable endpoint was found.
/// </summary>
public EndpointHandlerAsync? Default { get; private set; } = null;
public readonly List<Route> Routes = new List<Route>();
public async Task<HttpResponse?> Handle(HttpRequest request, ArraySegment<string> path)
public override HttpMethod Method => HttpMethod.Any;
public override int Arguments => 1;
protected override async Task<HttpResponse?> GetResponseInner(HttpRequest req, ArraySegment<string> path)
{
//Must be fast
foreach (Endpoint endpoint in Endpoints)
string next = path[0];
foreach (Route route in Routes)
{
if (endpoint.Method != HttpMethod.Any && request.Method != endpoint.Method)
continue;
if (!endpoint.CheckMatch(path))
continue;
var result = await endpoint.Handler(request, new RouteMatch(path, endpoint.Pattern));
if (result != null)
return result;
}
if (Default == null)
return null;
else
return await Default(request, new RouteMatch(path, Array.Empty<PathSegment>()));
}
/// <summary>
/// Adds an endpoint to the router.
/// </summary>
/// <param name="path">The full path to the endpoint.</param>
/// <param name="handler">The function to call to produce a response.</param>
public void Add(HttpMethod method, string path, EndpointHandlerAsync handler)
{
//Runs at startup
string[] spl = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
PathSegment[] pattern = new PathSegment[spl.Length];
for (int i = 0; i < spl.Length; i++)
{
string name = spl[i];
PathSegmentType type = PathSegmentType.Constant;
if (name.StartsWith(':'))
if (route.Key == next || route.Key == null)
{
name = name[1..];
type = PathSegmentType.Parameter;
HttpResponse? resp = await route.Router.GetResponse(req, route.Key == null ? path : path[1..]);
if (resp != null)
return resp;
}
pattern[i] = new PathSegment(name, type);
}
Endpoints.Add(new Endpoint(method, pattern, PathMatchMode.AllowTrailing, handler));
}
/// <inheritdoc cref="Add(string, EndpointHandlerAsync)" />
public void Add(HttpMethod method, string path, EndpointHandler handler)
{
Add(method, path, (req, route) => Task.FromResult(handler(req, route)));
}
/// <summary>
/// Adds a sub-router.
/// </summary>
public void Add(IRouter router)
{
Endpoints.Add(new Endpoint(HttpMethod.Any, Array.Empty<PathSegment>(), PathMatchMode.AllowTrailing, (req, route) => router.Handle(req, route.Request)));
}
/// <summary>
/// Sets the fallback endpoint handler to use if a request is not handled by any other endpoint. This usually serves HTTP 404 statuses.
/// </summary>
public void SetDefault(EndpointHandlerAsync? handler)
{
Default = handler;
}
/// <inheritdoc cref="SetDefault(EndpointHandlerAsync)"/>
public void SetDefault(EndpointHandler handler)
{
SetDefault((req, route) => Task.FromResult(handler(req, route)));
}
}
public readonly struct RouteMatch
{
/// <summary>
/// The requested path.
/// </summary>
public readonly ArraySegment<string> Request;
/// <summary>
/// The route matching the requested path.
/// </summary>
public readonly PathSegment[] Pattern;
public RouteMatch(ArraySegment<string> request, PathSegment[] pattern)
{
Request = request;
Pattern = pattern;
}
public readonly string? GetVariable(string name)
{
if (Pattern.Length != Request.Count)
return null;
for (int i = 0; i < Pattern.Length; i++)
{
PathSegment seg = Pattern[i];
if (seg.Type == PathSegmentType.Parameter && seg.Name == name)
return Request[i];
}
return null;
}
}
public readonly struct Endpoint
{
public readonly HttpMethod Method;
public readonly PathSegment[] Pattern;
public readonly PathMatchMode MatchMode;
public readonly EndpointHandlerAsync Handler;
public Endpoint(HttpMethod method, PathSegment[] path, PathMatchMode matchMode, EndpointHandlerAsync handler)
{
Pattern = path;
MatchMode = matchMode;
Handler = handler;
}
public bool CheckMatch(ArraySegment<string> path)
{
if (path.Count != Pattern.Length)
{
switch (MatchMode)
{
case PathMatchMode.Exact:
return false;
case PathMatchMode.AllowTrailing:
if (path.Count < Pattern.Length)
return false;
break;
}
}
for (int i = 0; i < Pattern.Length; i++)
{
PathSegment pseg = Pattern[i];
if (pseg.Type == PathSegmentType.Constant)
if (!pseg.Name.Equals(path[i], StringComparison.OrdinalIgnoreCase))
return false;
}
return true;
}
public override string ToString() => string.Join("/", Pattern);
}
public readonly struct PathSegment
{
public readonly string Name;
public readonly PathSegmentType Type;
public PathSegment(string name, PathSegmentType type)
{
Name = name;
Type = type;
}
public override string ToString() => (Type == PathSegmentType.Parameter ? ":" : null) + Name;
}
public enum PathSegmentType
{
Constant,
Parameter
}
public enum PathMatchMode
{
/// <summary>
/// Request path must match the endpoint path exactly.
/// </summary>
Exact,
/// <summary>
/// Request path can have additional path segments.
/// Adds a route to the router on the given sub path.
/// </summary>
AllowTrailing,
}
public void Add(string? key, RouterBase router) => Routes.Add(new Route(key, router));
/// <summary>
/// Adds a route to the router.
/// </summary>
public void Add(RouterBase router) => Add(null, router);
/// <summary>
/// Adds an endpoint to the router on the given sub path.
/// </summary>
public void Add(string? key, FuncEndpointDelegate func) => Routes.Add(new Route(key, new FuncEndpoint(func)));
/// <summary>
/// Adds a wildcard endpoint to the router.
/// </summary>
public void Add(FuncEndpointDelegate func) => Add(null, func);
/// <summary>
/// Adds a static response to the router on the given sub path.
/// </summary>
public void Add(string? key, HttpResponse response) => Routes.Add(new Route(key, new StaticEndpoint(response)));
/// <summary>
/// Adds a wildcard static response to the router.
/// </summary>
public void Add(HttpResponse response) => Add(null, response);
}

View file

@ -0,0 +1,39 @@
using MiniHTTP.Responses;
namespace MiniHTTP.Routing;
/// <summary>
/// Parses a path and generates or acquires a response.
/// </summary>
public abstract class RouterBase
{
/// <summary>
/// Which HTTP method the router will respond to.
/// </summary>
public abstract HttpMethod Method { get; }
/// <summary>
/// The minimum number of path segments required to produce a response.
/// </summary>
public abstract int Arguments { get; }
public Task<HttpResponse?> GetResponse(HttpRequest req, ArraySegment<string> path)
{
HttpMethod method = Method;
if (method != HttpMethod.Any && req.Method != method)
return Task.FromResult<HttpResponse?>(null);
int arguments = Arguments;
if (path.Count < arguments)
return Task.FromResult<HttpResponse?>(null);
return GetResponseInner(req, path);
}
/// <summary>
/// Inline method for routing.
/// </summary>
/// <param name="req">The request for which the response is being generated.</param>
/// <param name="path">The path segments relevant to the router.</param>
protected abstract Task<HttpResponse?> GetResponseInner(HttpRequest req, ArraySegment<string> path);
}

View file

@ -0,0 +1,22 @@
using MiniHTTP.Responses;
namespace MiniHTTP.Routing;
public class StaticEndpoint : RouterBase
{
public HttpResponse Response;
public StaticEndpoint(HttpResponse response)
{
Response = response;
}
public override HttpMethod Method => HttpMethod.GET;
public override int Arguments => 0;
protected override Task<HttpResponse?> GetResponseInner(HttpRequest req, ArraySegment<string> path)
{
return Task.FromResult(Response)!;
}
}