route redesign again

This commit is contained in:
uwaa 2024-10-28 06:48:41 +00:00
parent bc8ce1a19a
commit 2dacb80d49
8 changed files with 208 additions and 168 deletions

View file

@ -59,6 +59,11 @@ public sealed class HttpRequest
/// </summary>
public bool Connected => Client.Connected;
/// <summary>
/// If true, the connection is requesting to be upgrading to a websocket.
/// </summary>
public bool IsWebsocket => Headers.TryGetValue("Upgrade", out string? connection) && connection.Equals("websocket", StringComparison.OrdinalIgnoreCase);
internal HttpRequest(HttpServer server, TcpClient client, Stream stream)
{
Server = server;
@ -187,7 +192,9 @@ public sealed class HttpRequest
public Task Flush()
{
return Buffer.FlushAsync();
Buffer.Flush();
Stream.Flush();
return Stream.FlushAsync();
}
/// <summary>
@ -223,7 +230,6 @@ public sealed class HttpRequest
}
catch
{
//Who cares if it does not arrive. Fuck off remote endpoint.
}
}
Client.Close();

View file

@ -113,12 +113,13 @@ public sealed class HttpServer
{
HttpRequest req = new HttpRequest(this, client, stream);
HttpResponse? response;
bool keepAlive = true;
try
{
await req.ReadAllHeaders();
//Parse path
ArraySegment<string> pathSpl = req.Path.Split('/');
ArraySegment<string> pathSpl = req.Path.Split('/', StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < pathSpl.Count; i++)
pathSpl[i] = WebUtility.UrlDecode(pathSpl[i]);
@ -127,11 +128,17 @@ public sealed class HttpServer
pathSpl = pathSpl.Slice(1);
//Execute
response = await Router.Handle(req, pathSpl, null);
response = await Router.Handle(req, pathSpl);
if (response != null)
{
await req.Write(response);
keepAlive = (response.StatusCode is >= 200 and < 300) && !(req.Headers.TryGetValue("connection", out string? connectionValue) && connectionValue == "close");
}
else
await req.Write(new HttpResponse(500, "Server produced no response"));
{
await req.Write(new HttpResponse(404, "Router produced no response"));
keepAlive = false;
}
}
catch (RequestParseException e)
{
@ -148,8 +155,7 @@ public sealed class HttpServer
throw;
}
if ((response != null && response.StatusCode is >= 300 and < 400) ||
req.Headers.TryGetValue("connection", out string? connectionValue) && connectionValue == "close")
if (!keepAlive)
{
client.Close();
break;

View file

@ -11,12 +11,12 @@ public class CORS : IRouter
{
}
public Task<HttpResponse?> Handle(HttpRequest client, ArraySegment<string> path, VariableCollection? baseParameters)
public Task<HttpResponse?> Handle(HttpRequest client, ArraySegment<string> path)
{
if (client.Method != HttpMethod.OPTIONS)
return Task.FromResult<HttpResponse?>(null);
if (client.Method == HttpMethod.OPTIONS)
return Task.FromResult<HttpResponse?>(new PreflightResponse());
return Task.FromResult<HttpResponse?>(null);
}
}

View file

@ -6,14 +6,12 @@ namespace MiniHTTP.Routing;
/// An asynchronous endpoint handler.
/// </summary>
/// <param name="request">The request being served.</param>
/// <param name="variables">Variables provided in the path.</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, VariableCollection variables);
public delegate Task<HttpResponse?> EndpointHandlerAsync(HttpRequest request, RouteMatch route);
/// <summary>
/// An synchronous endpoint handler.
/// </summary>
/// <param name="request">The request being served.</param>
/// <param name="variables">Variables provided in the path.</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, VariableCollection variables);
public delegate HttpResponse? EndpointHandler(HttpRequest request, RouteMatch route);

View file

@ -7,5 +7,5 @@ namespace MiniHTTP.Routing;
/// </summary>
public interface IRouter
{
Task<HttpResponse?> Handle(HttpRequest client, ArraySegment<string> path, VariableCollection? baseParameters);
Task<HttpResponse?> Handle(HttpRequest client, ArraySegment<string> path);
}

View file

@ -7,112 +7,197 @@ namespace MiniHTTP.Routing;
/// </summary>
public class Router : IRouter
{
struct Route
{
public string Path;
public IRouter Router;
public Route(string path, IRouter router)
{
Path = path;
Router = router;
}
public override string ToString() => Path;
}
/// <summary>
/// Endpoints supported by this router.
/// </summary>
public readonly List<Endpoint> Endpoints = new List<Endpoint>();
/// <summary>
/// The default handler to use if no other handlers are hit.
/// The default endpoint handler to use if no suitable endpoint was found.
/// </summary>
public EndpointHandlerAsync? Endpoint;
public EndpointHandlerAsync? Default { get; private set; } = null;
/// <summary>
/// Which method this router serves.
/// </summary>
public HttpMethod Method = HttpMethod.Any;
readonly List<Route> Routes = new List<Route>();
public Router(EndpointHandlerAsync endpoint, HttpMethod method = HttpMethod.Any)
public async Task<HttpResponse?> Handle(HttpRequest request, ArraySegment<string> path)
{
Endpoint = endpoint;
//Must be fast
foreach (Endpoint endpoint in Endpoints)
{
if (!endpoint.CheckMatch(path))
continue;
var result = await endpoint.Handler(request, new RouteMatch(path, endpoint.Pattern));
if (result != null)
return result;
}
public Router(EndpointHandler endpoint, HttpMethod method = HttpMethod.Any)
{
Endpoint = (client, parameters) => Task.FromResult(endpoint(client, parameters));
}
internal async Task<HttpResponse?> Handle(HttpRequest client, ArraySegment<string> path, VariableCollection? baseParameters)
{
if (Method != HttpMethod.Any && client.Method != Method)
return null;
if (Routes.Count > 0)
{
VariableCollection parameters = new VariableCollection(baseParameters);
foreach (var route in Routes)
{
if (route.Path != "*")
{
string key = route.Path;
if (key.Length > 0 && key[0] == ':')
{
parameters[route.Path[1..]] = path[0];
}
else
{
if (!key.Equals(path[0], StringComparison.OrdinalIgnoreCase))
goto skip;
}
}
if (path.Count > 0)
{
HttpResponse? resp = await route.Router.Handle(client, path.Slice(1), parameters);
if (resp != null)
return resp;
}
skip:
parameters.Clear();
}
}
if (Endpoint == null)
if (Default == null)
return null;
else
return await Endpoint(client, baseParameters ?? VariableCollection.Empty);
}
Task<HttpResponse?> IRouter.Handle(HttpRequest client, ArraySegment<string> path, VariableCollection? baseParameters)
=> Handle(client, path, baseParameters);
/// <summary>
/// Adds a new route to the router.
/// </summary>
/// <param name="pathSegment">The path segment to the route.</param>
/// <param name="route">The router to use.</param>
public void Use(string pathSegment, IRouter route)
{
Routes.Add(new Route(pathSegment, route));
return await Default(request, new RouteMatch(path, Array.Empty<PathSegment>()));
}
/// <summary>
/// Adds an endpoint to the router.
/// </summary>
/// <param name="method">The HTTP method to respond to.</param>
/// <param name="pathSegment">The subpath to the endpoint.</param>
/// <param name="endpoint">The endpoint handler which serves the request.</param>
public void Add(HttpMethod method, string pathSegment, EndpointHandlerAsync endpoint) => Use(pathSegment, new Router(endpoint, method));
/// <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(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(':'))
{
name = name[1..];
type = PathSegmentType.Parameter;
}
pattern[i] = new PathSegment(name, type);
}
Endpoints.Add(new Endpoint(pattern, PathMatchMode.AllowTrailing, handler));
}
/// <inheritdoc cref="Add(string, EndpointHandlerAsync)" />
public void Add(HttpMethod method, string pathSegment, EndpointHandler endpoint) => Use(pathSegment, new Router(endpoint, method));
public void Add(string path, EndpointHandler handler)
{
Add(path, (req, route) => Task.FromResult(handler(req, route)));
}
/// <summary>
/// Adds a static file route to the router.
/// Adds a sub-router.
/// </summary>
public void Static(string directory) => Routes.Add(new Route(":asset", new Static(directory, "asset")));
public void Add(IRouter router)
{
Endpoints.Add(new Endpoint(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 PathSegment[] Pattern;
public readonly PathMatchMode MatchMode;
public readonly EndpointHandlerAsync Handler;
public Endpoint(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.
/// </summary>
AllowTrailing,
}

View file

@ -6,8 +6,14 @@ namespace MiniHTTP.Routing;
/// <summary>
/// Special router which serves static files from the filesystem.
/// </summary>
public partial class Static : IRouter
public partial class Static
{
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
@ -70,12 +76,9 @@ public partial class Static : IRouter
AssetParameter = assetParameter;
}
async Task<HttpResponse?> IRouter.Handle(HttpRequest client, ArraySegment<string> path, VariableCollection? baseParameters)
public async Task<HttpResponse?> Handle(HttpRequest req, RouteMatch route)
{
if (baseParameters == null)
return null;
string? asset = baseParameters[AssetParameter];
string? asset = route.GetVariable(AssetParameter);
if (string.IsNullOrWhiteSpace(asset))
return null;

View file

@ -1,58 +0,0 @@
namespace MiniHTTP.Routing;
/// <summary>
/// A map of arguments passed as variables in a HTTP path.
/// </summary>
/// <remarks>
/// <para>
/// Routes may contain variables, denoted by a colon at the beginning. When a request is made, whatever is provided in that space becomes available in the variable collection as an argument.
/// If you have a route such as <c>/far/:example/bar</c>, and a client requests the path <c>/foo/quux/bar</c>, then the argument passed to the "example" variable will be "quux".
/// </para>
/// <para>
/// Arguments can be obtained from the collection like a <see cref="Dictionary{TKey, TValue}"/>. For example:
/// <code>
/// string id = variables["id"];
/// </code>
/// </para>
/// </remarks>
public class VariableCollection
{
public static readonly VariableCollection Empty = new VariableCollection();
/// <summary>
/// A variable collection to read from if this instance fails to fulfill a lookup.
/// </summary>
public readonly VariableCollection? Fallback;
readonly Dictionary<string, string> Values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public string? this[string key]
{
get
{
if (Values.TryGetValue(key, out var value))
return value;
else if (Fallback == null)
return null;
else
return Fallback[key];
}
set
{
if (value == null)
Values.Remove(key);
else
Values[key] = value;
}
}
public VariableCollection(VariableCollection? fallback = null)
{
Fallback = fallback;
}
public void Clear()
{
Values.Clear();
}
}