Compare commits

...

3 commits

Author SHA1 Message Date
uwaa
bc8ce1a19a simplify router a lil 2024-10-27 23:49:37 +00:00
uwaa
e042d64e5e fix redirect hanging 2024-10-27 23:49:33 +00:00
uwaa
2a62fb2c7b refactor 2024-10-27 21:56:48 +00:00
8 changed files with 106 additions and 112 deletions

View file

@ -112,6 +112,7 @@ public sealed class HttpServer
while (client.Connected)
{
HttpRequest req = new HttpRequest(this, client, stream);
HttpResponse? response;
try
{
await req.ReadAllHeaders();
@ -126,7 +127,7 @@ public sealed class HttpServer
pathSpl = pathSpl.Slice(1);
//Execute
HttpResponse? response = await Router.Handle(req, pathSpl, null);
response = await Router.Handle(req, pathSpl, null);
if (response != null)
await req.Write(response);
else
@ -147,7 +148,8 @@ public sealed class HttpServer
throw;
}
if (req.Headers.TryGetValue("connection", out string? connectionValue) && connectionValue == "close")
if ((response != null && response.StatusCode is >= 300 and < 400) ||
req.Headers.TryGetValue("connection", out string? connectionValue) && connectionValue == "close")
{
client.Close();
break;

View file

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

View file

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

View file

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

View file

@ -1,58 +0,0 @@
namespace MiniHTTP.Routing;
/// <summary>
/// A map of arguments passed as parameters in a HTTP path.
/// </summary>
/// <remarks>
/// <para>
/// Routes may contain parameters, denoted by a colon at the beginning. When a request is made, whatever is provided in that space becomes available in the parameter 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" parameter will be "quux".
/// </para>
/// <para>
/// Arguments can be obtained from the collection like a <see cref="Dictionary{TKey, TValue}"/>. For example:
/// <code>
/// string id = parameters["id"];
/// </code>
/// </para>
/// </remarks>
public class ParameterCollection
{
public static readonly ParameterCollection Empty = new ParameterCollection();
/// <summary>
/// A parameter collection to read from if this instance fails to fulfill a lookup.
/// </summary>
public readonly ParameterCollection? Fallback;
readonly Dictionary<string, string> Parameters = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public string? this[string key]
{
get
{
if (Parameters.TryGetValue(key, out var value))
return value;
else if (Fallback == null)
return null;
else
return Fallback[key];
}
set
{
if (value == null)
Parameters.Remove(key);
else
Parameters[key] = value;
}
}
public ParameterCollection(ParameterCollection? fallback = null)
{
Fallback = fallback;
}
public void Clear()
{
Parameters.Clear();
}
}

View file

@ -9,15 +9,17 @@ public class Router : IRouter
{
struct Route
{
public string[] Paths;
public string Path;
public IRouter Router;
public Route(string[] paths, IRouter router)
public Route(string path, IRouter router)
{
Paths = paths;
Path = path;
Router = router;
}
public override string ToString() => Path;
}
/// <summary>
@ -42,85 +44,75 @@ public class Router : IRouter
Endpoint = (client, parameters) => Task.FromResult(endpoint(client, parameters));
}
internal async Task<HttpResponse?> Handle(HttpRequest client, ArraySegment<string> path, ParameterCollection? baseParameters)
internal async Task<HttpResponse?> Handle(HttpRequest client, ArraySegment<string> path, VariableCollection? baseParameters)
{
if (Method != HttpMethod.Any && client.Method != Method)
return null;
ParameterCollection parameters = new ParameterCollection(baseParameters);
foreach (var route in Routes)
if (Routes.Count > 0)
{
if (route.Paths.Length != 1 || route.Paths[0] != "*")
VariableCollection parameters = new VariableCollection(baseParameters);
foreach (var route in Routes)
{
if (route.Paths.Length > path.Count)
continue;
for (int i = 0; i < route.Paths.Length; i++)
if (route.Path != "*")
{
string key = route.Paths[i];
if (key.Length == 0)
string key = route.Path;
if (key.Length > 0 && key[0] == ':')
{
if (path[i].Length == 0)
continue;
else
parameters[route.Path[1..]] = path[0];
}
else
{
if (!key.Equals(path[0], StringComparison.OrdinalIgnoreCase))
goto skip;
}
if (key[0] == ':')
{
parameters[route.Paths[i][1..]] = path[i];
continue;
}
if (!key.Equals(path[i], 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();
}
HttpResponse? resp = await route.Router.Handle(client, path.Slice(route.Paths.Length), parameters);
if (resp != null)
return resp;
skip:
parameters.Clear();
}
if (Endpoint == null)
return null;
else
return await Endpoint(client, baseParameters ?? ParameterCollection.Empty);
return await Endpoint(client, baseParameters ?? VariableCollection.Empty);
}
Task<HttpResponse?> IRouter.Handle(HttpRequest client, ArraySegment<string> path, ParameterCollection? baseParameters)
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="path">The subpath to the route.</param>
/// <param name="pathSegment">The path segment to the route.</param>
/// <param name="route">The router to use.</param>
public void Use(string path, IRouter route)
public void Use(string pathSegment, IRouter route)
{
string[] pathParams = path.Split('/', StringSplitOptions.TrimEntries);
Routes.Add(new Route(pathParams, route));
Routes.Add(new Route(pathSegment, route));
}
/// <summary>
/// Adds an endpoint to the router.
/// </summary>
/// <param name="method">The HTTP method to respond to.</param>
/// <param name="path">The subpath to the endpoint.</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 path, EndpointHandlerAsync endpoint) => Use(path, new Router(endpoint, method));
public void Add(HttpMethod method, string pathSegment, EndpointHandlerAsync endpoint) => Use(pathSegment, new Router(endpoint, method));
/// <inheritdoc cref="Add(string, EndpointHandlerAsync)"/>
public void Add(HttpMethod method, string path, EndpointHandler endpoint) => Use(path, new Router(endpoint, method));
public void Add(HttpMethod method, string pathSegment, EndpointHandler endpoint) => Use(pathSegment, new Router(endpoint, method));
/// <summary>
/// Adds a static file route to the router.
/// </summary>
public void Static(string directory) => Routes.Add(new Route([":asset"], new Static(directory, "asset")));
/// <inheritdoc cref="Static(string)"/>
public void Static(string directory, string path, string assetParam) => Use(path, new Static(directory, assetParam));
public void Static(string directory) => Routes.Add(new Route(":asset", new Static(directory, "asset")));
}

View file

@ -70,7 +70,7 @@ public partial class Static : IRouter
AssetParameter = assetParameter;
}
async Task<HttpResponse?> IRouter.Handle(HttpRequest client, ArraySegment<string> path, ParameterCollection? baseParameters)
async Task<HttpResponse?> IRouter.Handle(HttpRequest client, ArraySegment<string> path, VariableCollection? baseParameters)
{
if (baseParameters == null)
return null;

View file

@ -0,0 +1,58 @@
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();
}
}