router redesign again again
This commit is contained in:
parent
3dd2c79b21
commit
d948ab6191
9 changed files with 147 additions and 245 deletions
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
|
@ -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;
|
||||
|
24
MiniHTTP/Routing/FuncEndpoint.cs
Normal file
24
MiniHTTP/Routing/FuncEndpoint.cs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
39
MiniHTTP/Routing/RouterBase.cs
Normal file
39
MiniHTTP/Routing/RouterBase.cs
Normal 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);
|
||||
}
|
22
MiniHTTP/Routing/StaticEndpoint.cs
Normal file
22
MiniHTTP/Routing/StaticEndpoint.cs
Normal 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)!;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue