Compare commits
3 commits
c302da3713
...
bc8ce1a19a
Author | SHA1 | Date | |
---|---|---|---|
|
bc8ce1a19a | ||
|
e042d64e5e | ||
|
2a62fb2c7b |
8 changed files with 106 additions and 112 deletions
|
@ -112,6 +112,7 @@ public sealed class HttpServer
|
||||||
while (client.Connected)
|
while (client.Connected)
|
||||||
{
|
{
|
||||||
HttpRequest req = new HttpRequest(this, client, stream);
|
HttpRequest req = new HttpRequest(this, client, stream);
|
||||||
|
HttpResponse? response;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await req.ReadAllHeaders();
|
await req.ReadAllHeaders();
|
||||||
|
@ -126,7 +127,7 @@ public sealed class HttpServer
|
||||||
pathSpl = pathSpl.Slice(1);
|
pathSpl = pathSpl.Slice(1);
|
||||||
|
|
||||||
//Execute
|
//Execute
|
||||||
HttpResponse? response = await Router.Handle(req, pathSpl, null);
|
response = await Router.Handle(req, pathSpl, null);
|
||||||
if (response != null)
|
if (response != null)
|
||||||
await req.Write(response);
|
await req.Write(response);
|
||||||
else
|
else
|
||||||
|
@ -147,7 +148,8 @@ public sealed class HttpServer
|
||||||
throw;
|
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();
|
client.Close();
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -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)
|
if (client.Method != HttpMethod.OPTIONS)
|
||||||
return Task.FromResult<HttpResponse?>(null);
|
return Task.FromResult<HttpResponse?>(null);
|
||||||
|
|
|
@ -6,14 +6,14 @@ namespace MiniHTTP.Routing;
|
||||||
/// An asynchronous endpoint handler.
|
/// An asynchronous endpoint handler.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">The request being served.</param>
|
/// <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>
|
/// <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>
|
/// <summary>
|
||||||
/// An synchronous endpoint handler.
|
/// An synchronous endpoint handler.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">The request being served.</param>
|
/// <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>
|
/// <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);
|
|
@ -7,5 +7,5 @@ namespace MiniHTTP.Routing;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IRouter
|
public interface IRouter
|
||||||
{
|
{
|
||||||
Task<HttpResponse?> Handle(HttpRequest client, ArraySegment<string> path, ParameterCollection? baseParameters);
|
Task<HttpResponse?> Handle(HttpRequest client, ArraySegment<string> path, VariableCollection? baseParameters);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -9,15 +9,17 @@ public class Router : IRouter
|
||||||
{
|
{
|
||||||
struct Route
|
struct Route
|
||||||
{
|
{
|
||||||
public string[] Paths;
|
public string Path;
|
||||||
|
|
||||||
public IRouter Router;
|
public IRouter Router;
|
||||||
|
|
||||||
public Route(string[] paths, IRouter router)
|
public Route(string path, IRouter router)
|
||||||
{
|
{
|
||||||
Paths = paths;
|
Path = path;
|
||||||
Router = router;
|
Router = router;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override string ToString() => Path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -42,85 +44,75 @@ public class Router : IRouter
|
||||||
Endpoint = (client, parameters) => Task.FromResult(endpoint(client, parameters));
|
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)
|
if (Method != HttpMethod.Any && client.Method != Method)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
ParameterCollection parameters = new ParameterCollection(baseParameters);
|
if (Routes.Count > 0)
|
||||||
foreach (var route in Routes)
|
|
||||||
{
|
{
|
||||||
if (route.Paths.Length != 1 || route.Paths[0] != "*")
|
VariableCollection parameters = new VariableCollection(baseParameters);
|
||||||
|
foreach (var route in Routes)
|
||||||
{
|
{
|
||||||
if (route.Paths.Length > path.Count)
|
if (route.Path != "*")
|
||||||
continue;
|
|
||||||
|
|
||||||
for (int i = 0; i < route.Paths.Length; i++)
|
|
||||||
{
|
{
|
||||||
string key = route.Paths[i];
|
string key = route.Path;
|
||||||
if (key.Length == 0)
|
|
||||||
|
if (key.Length > 0 && key[0] == ':')
|
||||||
{
|
{
|
||||||
if (path[i].Length == 0)
|
parameters[route.Path[1..]] = path[0];
|
||||||
continue;
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
if (!key.Equals(path[0], StringComparison.OrdinalIgnoreCase))
|
||||||
goto skip;
|
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)
|
if (Endpoint == null)
|
||||||
return null;
|
return null;
|
||||||
else
|
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);
|
=> Handle(client, path, baseParameters);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a new route to the router.
|
/// Adds a new route to the router.
|
||||||
/// </summary>
|
/// </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>
|
/// <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(pathSegment, route));
|
||||||
Routes.Add(new Route(pathParams, route));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds an endpoint to the router.
|
/// Adds an endpoint to the router.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="method">The HTTP method to respond to.</param>
|
/// <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>
|
/// <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)"/>
|
/// <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>
|
/// <summary>
|
||||||
/// Adds a static file route to the router.
|
/// Adds a static file route to the router.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Static(string directory) => Routes.Add(new Route([":asset"], new Static(directory, "asset")));
|
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));
|
|
||||||
}
|
}
|
|
@ -70,7 +70,7 @@ public partial class Static : IRouter
|
||||||
AssetParameter = assetParameter;
|
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)
|
if (baseParameters == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
58
MiniHTTP/Routing/VariableCollection.cs
Normal file
58
MiniHTTP/Routing/VariableCollection.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue