route redesign again
This commit is contained in:
parent
bc8ce1a19a
commit
2dacb80d49
8 changed files with 208 additions and 168 deletions
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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?>(new PreflightResponse());
|
||||
return Task.FromResult<HttpResponse?>(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
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)
|
||||
//Must be fast
|
||||
foreach (Endpoint endpoint in Endpoints)
|
||||
{
|
||||
VariableCollection parameters = new VariableCollection(baseParameters);
|
||||
foreach (var route in Routes)
|
||||
{
|
||||
if (route.Path != "*")
|
||||
{
|
||||
string key = route.Path;
|
||||
if (!endpoint.CheckMatch(path))
|
||||
continue;
|
||||
|
||||
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();
|
||||
}
|
||||
var result = await endpoint.Handler(request, new RouteMatch(path, endpoint.Pattern));
|
||||
if (result != null)
|
||||
return result;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
/// <inheritdoc cref="Add(string, EndpointHandlerAsync)"/>
|
||||
public void Add(HttpMethod method, string pathSegment, EndpointHandler endpoint) => Use(pathSegment, new Router(endpoint, method));
|
||||
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(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,
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue