Compare commits
2 Commits
master
...
ghxx/refac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bc6086509 | ||
|
|
a03fafebcf |
21
LICENSE
21
LICENSE
|
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2024 00asdf, GHXX
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
global using static SimpleHttpServer.GlobalUsings;
|
global using static SimpleHttpServer.GlobalUsings;
|
||||||
using SimpleHttpServer.Types.Exceptions;
|
using SimpleHttpServer.Types.Exceptions;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
namespace SimpleHttpServer;
|
namespace SimpleHttpServer;
|
||||||
internal static class GlobalUsings {
|
internal static class GlobalUsings {
|
||||||
|
|
||||||
[DebuggerHidden]
|
|
||||||
internal static void Assert([DoesNotReturnIf(false)] bool b, string? message = null) {
|
internal static void Assert([DoesNotReturnIf(false)] bool b, string? message = null) {
|
||||||
if (!b) {
|
if (!b) {
|
||||||
if (message == null)
|
if (message == null)
|
||||||
|
|
@ -16,6 +13,5 @@ internal static class GlobalUsings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[DebuggerHidden]
|
|
||||||
internal static void AssertImplies(bool x, bool y, string? message = null) => Assert(!x || y, message);
|
internal static void AssertImplies(bool x, bool y, string? message = null) => Assert(!x || y, message);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,22 @@
|
||||||
using SimpleHttpServer.Types;
|
using SimpleHttpServer.Internal;
|
||||||
|
|
||||||
namespace SimpleHttpServer;
|
namespace SimpleHttpServer;
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||||
public class HttpEndpointAttribute : Attribute {
|
public class HttpEndpointAttribute<T> : Attribute where T : IAuthorizer {
|
||||||
|
|
||||||
public HttpRequestType RequestMethod { get; private set; }
|
public HttpRequestType RequestMethod { get; private set; }
|
||||||
public string[] Locations { get; private set; }
|
public string[] Locations { get; private set; }
|
||||||
|
public Type Authorizer { get; private set; }
|
||||||
|
|
||||||
public HttpEndpointAttribute(HttpRequestType requestMethod, params string[] locations) {
|
public HttpEndpointAttribute(HttpRequestType requestMethod, params string[] locations) {
|
||||||
RequestMethod = requestMethod;
|
RequestMethod = requestMethod;
|
||||||
Locations = locations;
|
Locations = locations;
|
||||||
|
Authorizer = typeof(T);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method)]
|
||||||
|
public class HttpEndpointAttribute : HttpEndpointAttribute<DefaultAuthorizer> {
|
||||||
|
public HttpEndpointAttribute(HttpRequestType type, params string[] locations) : base(type, locations) { }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
namespace SimpleHttpServer.Types;
|
namespace SimpleHttpServer;
|
||||||
|
|
||||||
public enum HttpRequestType {
|
public enum HttpRequestType {
|
||||||
GET,
|
GET,
|
||||||
|
|
@ -4,8 +4,6 @@ using SimpleHttpServer.Types.ParameterConverters;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
|
||||||
using static SimpleHttpServer.Types.EndpointInvocationInfo;
|
|
||||||
|
|
||||||
namespace SimpleHttpServer;
|
namespace SimpleHttpServer;
|
||||||
|
|
||||||
|
|
@ -15,8 +13,7 @@ public sealed class HttpServer {
|
||||||
|
|
||||||
private readonly HttpListener listener;
|
private readonly HttpListener listener;
|
||||||
private Task? listenerTask;
|
private Task? listenerTask;
|
||||||
private readonly Logger mainLogger;
|
private readonly Logger logger;
|
||||||
private readonly Logger requestLogger;
|
|
||||||
private readonly SimpleHttpServerConfiguration conf;
|
private readonly SimpleHttpServerConfiguration conf;
|
||||||
private bool shutdown = false;
|
private bool shutdown = false;
|
||||||
|
|
||||||
|
|
@ -25,20 +22,19 @@ public sealed class HttpServer {
|
||||||
conf = configuration;
|
conf = configuration;
|
||||||
listener = new HttpListener();
|
listener = new HttpListener();
|
||||||
listener.Prefixes.Add($"http://localhost:{port}/");
|
listener.Prefixes.Add($"http://localhost:{port}/");
|
||||||
mainLogger = new(LogOutputTopic.Main, conf);
|
logger = new(LogOutputTopic.Main, conf);
|
||||||
requestLogger = new(LogOutputTopic.Request, conf);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Start() {
|
public void Start() {
|
||||||
mainLogger.Information($"Starting on port {Port}...");
|
logger.Information($"Starting on port {Port}...");
|
||||||
Assert(listenerTask == null, "Server was already started!");
|
Assert(listenerTask == null, "Server was already started!");
|
||||||
listener.Start();
|
listener.Start();
|
||||||
listenerTask = Task.Run(GetContextLoopAsync);
|
listenerTask = Task.Run(GetContextLoopAsync);
|
||||||
mainLogger.Information($"Ready to handle requests!");
|
logger.Information($"Ready to handle requests!");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StopAsync(CancellationToken ctok) {
|
public async Task StopAsync(CancellationToken ctok) {
|
||||||
mainLogger.Information("Stopping server...");
|
logger.Information("Stopping server...");
|
||||||
Assert(listenerTask != null, "Server was not started!");
|
Assert(listenerTask != null, "Server was not started!");
|
||||||
shutdown = true;
|
shutdown = true;
|
||||||
listener.Stop();
|
listener.Stop();
|
||||||
|
|
@ -50,9 +46,8 @@ public sealed class HttpServer {
|
||||||
try {
|
try {
|
||||||
var ctx = await listener.GetContextAsync();
|
var ctx = await listener.GetContextAsync();
|
||||||
_ = ProcessRequestAsync(ctx);
|
_ = ProcessRequestAsync(ctx);
|
||||||
} catch (HttpListenerException ex) when (ex.ErrorCode == 995) { //The I/O operation has been aborted because of either a thread exit or an application request
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
mainLogger.Fatal($"Caught otherwise uncaught exception in GetContextLoop:\n{ex}");
|
logger.Fatal($"Caught otherwise uncaught exception in GetContextLoop:\n{ex}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -61,7 +56,6 @@ public sealed class HttpServer {
|
||||||
void RegisterConverter<T>() where T : IParsable<T> {
|
void RegisterConverter<T>() where T : IParsable<T> {
|
||||||
stringToTypeParameterConverters.Add(typeof(T), new ParsableParameterConverter<T>());
|
stringToTypeParameterConverters.Add(typeof(T), new ParsableParameterConverter<T>());
|
||||||
}
|
}
|
||||||
stringToTypeParameterConverters.Add(typeof(string), new StringParameterConverter());
|
|
||||||
|
|
||||||
stringToTypeParameterConverters.Add(typeof(bool), new BoolParsableParameterConverter());
|
stringToTypeParameterConverters.Add(typeof(bool), new BoolParsableParameterConverter());
|
||||||
RegisterConverter<char>();
|
RegisterConverter<char>();
|
||||||
|
|
@ -84,335 +78,133 @@ public sealed class HttpServer {
|
||||||
RegisterConverter<decimal>();
|
RegisterConverter<decimal>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly MultiKeyDictionary<string, string, EndpointInvocationInfo> simpleEndpointMethodInfos = new(); // requestmethod, path
|
private readonly Dictionary<(string path, string rType), EndpointInvocationInfo> simpleEndpointMethodInfos = new();
|
||||||
private readonly MultiKeyDictionary<string, string, EndpointInvocationInfo> pathEndpointMethodInfos = new(); // requestmethod, path
|
|
||||||
private readonly Dictionary<string, PathTree<EndpointInvocationInfo>> pathEndpointMethodInfosTrees = new(); // reqmethod : pathtree
|
|
||||||
private static readonly Type[] expectedEndpointParameterTypes = new[] { typeof(RequestContext) };
|
private static readonly Type[] expectedEndpointParameterTypes = new[] { typeof(RequestContext) };
|
||||||
internal static readonly int expectedEndpointParameterPrefixCount = expectedEndpointParameterTypes.Length;
|
public void RegisterEndpointsFromType<T>() {
|
||||||
|
if (simpleEndpointMethodInfos.Count == 0)
|
||||||
public void RegisterEndpointsFromType<T>(Func<T>? instanceFactory = null) where T : class { // T cannot be static, as generic args must be nonstatic
|
|
||||||
if (stringToTypeParameterConverters.Count == 0)
|
|
||||||
RegisterDefaultConverters();
|
RegisterDefaultConverters();
|
||||||
|
|
||||||
var t = typeof(T);
|
var t = typeof(T);
|
||||||
var mis = t.GetMethods()
|
foreach (var (mi, attrib) in t.GetMethods()
|
||||||
.ToDictionary(x => x, x => x.GetCustomAttributes<HttpEndpointAttribute>())
|
.ToDictionary(x => x, x => x.GetCustomAttributes(typeof(HttpEndpointAttribute<>)))
|
||||||
.Where(x => x.Value.Any()).ToDictionary(x => x.Key, x => x.Value.Single());
|
.Where(x => x.Value.Any()).ToDictionary(x => x.Key, x => (HttpEndpointAttribute) x.Value.Single())) {
|
||||||
|
|
||||||
var isStatic = mis.All(x => x.Key.IsStatic); // if all are static then there is no point in having a constructor as no instance data is accessible, but we allow passing a factory anyway
|
|
||||||
Assert(isStatic || (instanceFactory != null), $"You must provide an instance factory if any methods of the given type ({typeof(T).FullName}) are non-static");
|
|
||||||
T? classInstance = instanceFactory?.Invoke();
|
|
||||||
foreach (var (mi, attrib) in mis) {
|
|
||||||
|
|
||||||
string GetFancyMethodName() => mi.DeclaringType!.FullName + "#" + mi.Name;
|
string GetFancyMethodName() => mi.DeclaringType!.FullName + "#" + mi.Name;
|
||||||
|
|
||||||
//Assert(mi.IsStatic, $"Method tagged with HttpEndpointAttribute must be static! ({GetFancyMethodName()})");
|
Assert(mi.IsStatic, $"Method tagged with HttpEndpointAttribute must be static! ({GetFancyMethodName()})");
|
||||||
Assert(mi.IsPublic, $"Method tagged with HttpEndpointAttribute must be public! ({GetFancyMethodName()})");
|
Assert(mi.IsPublic, $"Method tagged with HttpEndpointAttribute must be public! ({GetFancyMethodName()})");
|
||||||
|
|
||||||
var methodParams = mi.GetParameters();
|
var methodParams = mi.GetParameters();
|
||||||
// check the mandatory prefix parameters
|
|
||||||
Assert(methodParams.Length >= expectedEndpointParameterTypes.Length);
|
Assert(methodParams.Length >= expectedEndpointParameterTypes.Length);
|
||||||
for (int i = 0; i < expectedEndpointParameterTypes.Length; i++) {
|
for (int i = 0; i < expectedEndpointParameterTypes.Length; i++) {
|
||||||
Assert(methodParams[i].ParameterType.IsAssignableFrom(expectedEndpointParameterTypes[i]),
|
Assert(methodParams[i].ParameterType.IsAssignableFrom(expectedEndpointParameterTypes[i]),
|
||||||
$"Parameter at index {i} of {GetFancyMethodName()} is of a type that cannot contain the expected type {expectedEndpointParameterTypes[i].FullName}.");
|
$"Parameter at index {i} of {GetFancyMethodName()} is of a type that cannot contain the expected type {expectedEndpointParameterTypes[i].FullName}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// check return type
|
|
||||||
Assert(mi.ReturnType == typeof(Task), $"Return type of {GetFancyMethodName()} is not {typeof(Task)}!");
|
Assert(mi.ReturnType == typeof(Task), $"Return type of {GetFancyMethodName()} is not {typeof(Task)}!");
|
||||||
|
|
||||||
// check the rest of the method parameters
|
|
||||||
var qparams = new List<QueryParameterInfo>();
|
var qparams = new List<(string, (Type type, bool isOptional))>();
|
||||||
var pparams = new List<PathParameterInfo>();
|
|
||||||
int mParamIndex = expectedEndpointParameterTypes.Length;
|
|
||||||
for (int i = expectedEndpointParameterTypes.Length; i < methodParams.Length; i++) {
|
for (int i = expectedEndpointParameterTypes.Length; i < methodParams.Length; i++) {
|
||||||
var par = methodParams[i];
|
var par = methodParams[i];
|
||||||
var attr = par.GetCustomAttribute<ParameterAttribute>(false);
|
var attr = par.GetCustomAttribute<ParameterAttribute>(false);
|
||||||
var pathAttr = par.GetCustomAttribute<PathParameterAttribute>(false);
|
qparams.Add((attr?.Name ?? par.Name ?? throw new ArgumentException($"C# variable name of parameter at index {i} of method {GetFancyMethodName()} is null!"),
|
||||||
|
(par.GetType(), attr?.IsOptional ?? false)));
|
||||||
if (attr != null && pathAttr != null) {
|
|
||||||
throw new ArgumentException($"A method argument cannot be tagged with both {nameof(ParameterAttribute)} and {nameof(PathParameterAttribute)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stringToTypeParameterConverters.ContainsKey(par.ParameterType)) {
|
if (!stringToTypeParameterConverters.ContainsKey(par.ParameterType)) {
|
||||||
throw new MissingParameterConverterException($"Parameter converter for type {par.ParameterType} for parameter at index {i} of method {GetFancyMethodName()} has not been registered (yet)!");
|
throw new MissingParameterConverterException($"Parameter converter for type {par.ParameterType} has not been registered (yet)!");
|
||||||
}
|
|
||||||
|
|
||||||
if (pathAttr != null) { // parameter is a path param
|
|
||||||
|
|
||||||
pparams.Add(new(
|
|
||||||
pathAttr?.Name ?? throw new ArgumentException($"C# variable name of path parameter at index {i} of method {GetFancyMethodName()} is null!"),
|
|
||||||
par.ParameterType,
|
|
||||||
mParamIndex++
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else { // parameter is a normal query param
|
|
||||||
qparams.Add(new(
|
|
||||||
attr?.Name ?? par.Name ?? throw new ArgumentException($"C# variable name of query parameter at index {i} of method {GetFancyMethodName()} is null!"),
|
|
||||||
par.ParameterType,
|
|
||||||
mParamIndex++,
|
|
||||||
attr?.IsOptional ?? false)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// stores the check attributes that are defined on the method and on the containing class
|
|
||||||
InternalEndpointCheckAttribute[] requiredChecks = mi.GetCustomAttributes<InternalEndpointCheckAttribute>(true)
|
|
||||||
.Concat(mi.DeclaringType?.GetCustomAttributes<InternalEndpointCheckAttribute>(true) ?? Enumerable.Empty<Attribute>())
|
|
||||||
.Where(a => a.GetType().IsAssignableTo(typeof(InternalEndpointCheckAttribute)))
|
|
||||||
.Cast<InternalEndpointCheckAttribute>().ToArray();
|
|
||||||
|
|
||||||
InternalEndpointCheckAttribute.Initialize(classInstance, requiredChecks);
|
|
||||||
|
|
||||||
foreach (var location in attrib.Locations) {
|
foreach (var location in attrib.Locations) {
|
||||||
var normLocation = NormalizeUrlPath(location);
|
int idx = location.IndexOf('{');
|
||||||
|
if (idx >= 0) {
|
||||||
|
// this path contains path parameters
|
||||||
|
throw new NotImplementedException("Path parameters are not yet implemented!");
|
||||||
|
}
|
||||||
|
|
||||||
var reqMethod = Enum.GetName(attrib.RequestMethod) ?? throw new ArgumentException("Request method was undefined");
|
var reqMethod = Enum.GetName(attrib.RequestMethod) ?? throw new ArgumentException("Request method was undefined");
|
||||||
|
simpleEndpointMethodInfos.Add((location, reqMethod), new EndpointInvocationInfo(mi, qparams));
|
||||||
var pparamsCopy = new List<PathParameterInfo>(pparams);
|
|
||||||
var splittedLocation = location[1..].Split('/');
|
|
||||||
for (int i = 0; i < pparamsCopy.Count; i++) {
|
|
||||||
var pp = pparamsCopy[i];
|
|
||||||
var idx = Array.IndexOf(splittedLocation, pp.Name);
|
|
||||||
Assert(idx != -1, "Path parameter name was incorrect?");
|
|
||||||
pp.SegmentStartPos = idx;
|
|
||||||
pparamsCopy[i] = pp;
|
|
||||||
}
|
|
||||||
|
|
||||||
var epInvocInfo = new EndpointInvocationInfo(mi, pparamsCopy, qparams, requiredChecks, classInstance);
|
|
||||||
if (pparams.Any()) {
|
|
||||||
mainLogger.Information($"Registered path endpoint: '{reqMethod} {normLocation}'");
|
|
||||||
Assert(normLocation[0] == '/');
|
|
||||||
pathEndpointMethodInfos.Add(reqMethod, normLocation[1..], epInvocInfo);
|
|
||||||
} else {
|
|
||||||
mainLogger.Information($"Registered simple endpoint: '{reqMethod} {normLocation}'");
|
|
||||||
simpleEndpointMethodInfos.Add(reqMethod, normLocation, epInvocInfo);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// rebuild path trees
|
|
||||||
pathEndpointMethodInfosTrees.Clear();
|
|
||||||
foreach (var (reqMethod, d2) in pathEndpointMethodInfos.backingDict)
|
|
||||||
pathEndpointMethodInfosTrees.Add(reqMethod, new(d2));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Serves all files located in <paramref name="filesystemDirectory"/> on a website path that is relative to <paramref name="requestPath"/>,
|
|
||||||
/// while restricting requests to inside the local filesystem directory. Static serving has a lower priority than registering an endpoint.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="requestPath"></param>
|
|
||||||
/// <param name="filesystemDirectory"></param>
|
|
||||||
public void RegisterStaticServePath(string requestPath, string filesystemDirectory) {
|
|
||||||
var absPath = Path.GetFullPath(filesystemDirectory);
|
|
||||||
string npath = NormalizeUrlPath(requestPath);
|
|
||||||
mainLogger.Information($"Registered static serve path: '{npath}' --> '{absPath}'");
|
|
||||||
staticServePaths.Add(npath, absPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly Dictionary<string, string> staticServePaths = new();
|
|
||||||
|
|
||||||
private readonly Dictionary<Type, IParameterConverter> stringToTypeParameterConverters = new();
|
private readonly Dictionary<Type, IParameterConverter> stringToTypeParameterConverters = new();
|
||||||
|
|
||||||
private string NormalizeUrlPath(string url) {
|
|
||||||
var fwdSlashUrl = url.Replace('\\', '/');
|
|
||||||
|
|
||||||
var segments = fwdSlashUrl.Trim('/').Split('/', StringSplitOptions.RemoveEmptyEntries).ToList();
|
|
||||||
List<string> simplifiedSegmentsReversed = new List<string>();
|
|
||||||
int doubleDotsEncountered = 0;
|
|
||||||
for (int i = segments.Count - 1; i >= 0; i--) {
|
|
||||||
var segment = segments[i];
|
|
||||||
if (segment == ".") {
|
|
||||||
continue; // remove single dot segments
|
|
||||||
}
|
|
||||||
if (segment == "..") {
|
|
||||||
doubleDotsEncountered++; // if we encounter a doubledot, keep track of that and dont add it to the output yet
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// otherwise only keep the segment if doubleDotsEncountered > 0
|
|
||||||
if (doubleDotsEncountered > 0) {
|
|
||||||
doubleDotsEncountered--;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
simplifiedSegmentsReversed.Add(segment);
|
|
||||||
}
|
|
||||||
|
|
||||||
var rv = new StringBuilder();
|
|
||||||
for (int i = 0; i < doubleDotsEncountered; i++) {
|
|
||||||
rv.Append("../");
|
|
||||||
}
|
|
||||||
rv.AppendJoin('/', simplifiedSegmentsReversed.Reverse<string>());
|
|
||||||
|
|
||||||
var suffix = (rv.ToString().TrimEnd('/') + (fwdSlashUrl.EndsWith('/') ? "/" : "")).TrimStart('/');
|
|
||||||
if (conf.TrimTrailingSlash) {
|
|
||||||
suffix = suffix.TrimEnd('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
return '/' + suffix;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ProcessRequestAsync(HttpListenerContext ctx) {
|
private async Task ProcessRequestAsync(HttpListenerContext ctx) {
|
||||||
using RequestContext rc = new RequestContext(ctx);
|
|
||||||
|
|
||||||
// TODO add path escape countermeasure-unittests
|
|
||||||
var splitted = (ctx.Request.RawUrl ?? "").Split('?', 2, StringSplitOptions.None);
|
|
||||||
var reqPath = NormalizeUrlPath(WebUtility.UrlDecode(splitted.First()));
|
|
||||||
string requestMethod = ctx.Request.HttpMethod.ToUpperInvariant();
|
|
||||||
bool wasStaticlyServed = false;
|
|
||||||
|
|
||||||
void LogRequest() {
|
|
||||||
requestLogger.Information($"{rc.ListenerContext.Response.StatusCode} {(wasStaticlyServed ? "static" : "endpnt")} {requestMethod} {ctx.Request.Url}");
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
|
var decUri = WebUtility.UrlDecode(ctx.Request.RawUrl)!; // TODO add path escape countermeasures+unittests
|
||||||
|
var splitted = decUri.Split('?', 2, StringSplitOptions.None);
|
||||||
|
var path = WebUtility.UrlDecode(splitted.First());
|
||||||
|
|
||||||
/* Finding the endpoint that should process the request:
|
|
||||||
* 1. Try to see if there is a simple endpoint where request method and path match
|
|
||||||
* 2. Otherwise, try to see if a path-parameter-endpoint matches (duplicates throw an error on startup)
|
|
||||||
* 3. Otherwise, check if it is inside a static serve path
|
|
||||||
* 4. Otherwise, show 404 page */
|
|
||||||
|
|
||||||
EndpointInvocationInfo? pathEndpointInvocationInfo = null;
|
using var rc = new RequestContext(ctx);
|
||||||
if (simpleEndpointMethodInfos.TryGetValue(requestMethod, reqPath, out var simpleEndpointInvocationInfo) ||
|
if (simpleEndpointMethodInfos.TryGetValue((decUri, ctx.Request.HttpMethod.ToUpperInvariant()), out var endpointInvocationInfo)) {
|
||||||
pathEndpointMethodInfosTrees.TryGetValue(requestMethod, out var pt) && pt.TryGetPath(reqPath, out pathEndpointInvocationInfo)) { // try to find simple or pathparam-endpoint
|
|
||||||
var endpointInvocationInfo = simpleEndpointInvocationInfo ?? pathEndpointInvocationInfo ?? throw new Exception("retrieved endpoint is somehow null");
|
|
||||||
var mi = endpointInvocationInfo.methodInfo;
|
var mi = endpointInvocationInfo.methodInfo;
|
||||||
var qparams = endpointInvocationInfo.queryParameters;
|
var qparams = endpointInvocationInfo.queryParameters;
|
||||||
var pparams = endpointInvocationInfo.pathParameters;
|
|
||||||
var args = splitted.Length == 2 ? splitted[1] : null;
|
var args = splitted.Length == 2 ? splitted[1] : null;
|
||||||
|
|
||||||
var parsedQParams = new Dictionary<string, string>();
|
var parsedQParams = new Dictionary<string, string>();
|
||||||
var convertedMParamValues = new object[expectedEndpointParameterTypes.Length + pparams.Count + qparams.Count];
|
var convertedQParamValues = new object[qparams.Count + 1];
|
||||||
|
|
||||||
// run the checks to see if the client is allowed to make this request
|
// TODO add authcheck here
|
||||||
if (!endpointInvocationInfo.CheckAll(rc.ListenerContext.Request)) { // if any check failed return Forbidden
|
|
||||||
await HandleDefaultErrorPageAsync(rc, HttpStatusCode.Forbidden, "Client is not allowed to access this resource");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args != null) {
|
if (args != null) {
|
||||||
var queryStringArgs = args.Split('&', StringSplitOptions.None);
|
var queryStringArgs = args.Split('&', StringSplitOptions.None);
|
||||||
foreach (var queryKV in queryStringArgs) {
|
foreach (var queryKV in queryStringArgs) {
|
||||||
var queryKVSplitted = queryKV.Split('=');
|
var queryKVSplitted = queryKV.Split('=');
|
||||||
if (queryKVSplitted.Length != 2) {
|
if (queryKVSplitted.Length != 2) {
|
||||||
await HandleDefaultErrorPageAsync(rc, HttpStatusCode.BadRequest, "Malformed request URL parameters");
|
rc.SetStatusCodeAndDispose(HttpStatusCode.BadRequest, "Malformed request URL parameters");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!parsedQParams.TryAdd(WebUtility.UrlDecode(queryKVSplitted[0]), WebUtility.UrlDecode(queryKVSplitted[1]))) {
|
if (!parsedQParams.TryAdd(WebUtility.UrlDecode(queryKVSplitted[0]), WebUtility.UrlDecode(queryKVSplitted[1]))) {
|
||||||
await HandleDefaultErrorPageAsync(rc, HttpStatusCode.BadRequest, "Duplicate request URL parameters");
|
rc.SetStatusCodeAndDispose(HttpStatusCode.BadRequest, "Duplicate request URL parameters");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < qparams.Count;) {
|
for (int i = 0; i < qparams.Count;) {
|
||||||
var qparam = qparams[i];
|
var (qparamName, qparamInfo) = qparams[i];
|
||||||
i++;
|
i++;
|
||||||
|
|
||||||
if (parsedQParams.TryGetValue(qparam.Name, out var qparamValue)) {
|
if (parsedQParams.TryGetValue(qparamName, out var qparamValue)) {
|
||||||
if (stringToTypeParameterConverters[qparam.Type].TryConvertFromString(qparamValue, out object objRes)) {
|
if (stringToTypeParameterConverters[qparamInfo.type].TryConvertFromString(qparamValue, out object objRes)) {
|
||||||
convertedMParamValues[qparam.ArgPos] = objRes;
|
convertedQParamValues[i] = objRes;
|
||||||
} else {
|
} else {
|
||||||
await HandleDefaultErrorPageAsync(rc, HttpStatusCode.BadRequest);
|
rc.SetStatusCodeAndDispose(HttpStatusCode.BadRequest);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (qparam.IsOptional) {
|
if (qparamInfo.isOptional) {
|
||||||
convertedMParamValues[qparam.ArgPos] = null!;
|
convertedQParamValues[i] = null!;
|
||||||
} else {
|
} else {
|
||||||
await HandleDefaultErrorPageAsync(rc, HttpStatusCode.BadRequest, $"Missing required query parameter {qparam.Name}");
|
rc.SetStatusCodeAndDispose(HttpStatusCode.BadRequest, $"Missing required query parameter {qparamName}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else { // check for missing query parameters
|
|
||||||
var requiredParams = qparams.Where(x => !x.IsOptional).Select(x => $"'{x.Name}'").ToList();
|
|
||||||
if (requiredParams.Any()) {
|
|
||||||
await HandleDefaultErrorPageAsync(rc, HttpStatusCode.BadRequest, $"Missing required query parameter(s): {string.Join(",", requiredParams)}");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
convertedQParamValues[0] = rc;
|
||||||
if (pparams.Count != 0) {
|
|
||||||
var splittedReqPath = reqPath[1..].Split('/');
|
|
||||||
for (int i = 0; i < pparams.Count; i++) {
|
|
||||||
var pparam = pparams[i];
|
|
||||||
string paramValue;
|
|
||||||
if (pparam.IsCatchAll)
|
|
||||||
paramValue = string.Join('/', splittedReqPath[pparam.SegmentStartPos..]);
|
|
||||||
else
|
|
||||||
paramValue = splittedReqPath[pparam.SegmentStartPos];
|
|
||||||
|
|
||||||
if (stringToTypeParameterConverters[pparam.Type].TryConvertFromString(paramValue, out var res))
|
await (Task) (mi.Invoke(null, convertedQParamValues) ?? throw new NullReferenceException("Website func returned null unexpectedly"));
|
||||||
convertedMParamValues[pparam.ArgPos] = res;
|
|
||||||
else {
|
|
||||||
await HandleDefaultErrorPageAsync(rc, HttpStatusCode.BadRequest);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
convertedMParamValues[0] = rc;
|
|
||||||
rc.ParsedParameters = parsedQParams.AsReadOnly();
|
|
||||||
|
|
||||||
// todo read and convert pathparams
|
|
||||||
|
|
||||||
await (Task) (mi.Invoke(endpointInvocationInfo.typeInstanceReference, convertedMParamValues) ?? throw new NullReferenceException("Website func returned null unexpectedly"));
|
|
||||||
} else { // try to find suitable static serve path
|
|
||||||
if (requestMethod == "GET")
|
|
||||||
foreach (var (k, v) in staticServePaths) {
|
|
||||||
if (reqPath.StartsWith(k)) { // do a static serve
|
|
||||||
wasStaticlyServed = true;
|
|
||||||
var relativeStaticReqPath = reqPath[k.Length..];
|
|
||||||
var staticResponsePath = Path.GetFullPath(Path.Join(v, relativeStaticReqPath.TrimStart('/')));
|
|
||||||
|
|
||||||
if (Path.GetRelativePath(v, staticResponsePath).Contains("..")) {
|
|
||||||
requestLogger.Warning($"Blocked GET request to {reqPath} as somehow the target file does not lie inside the static serve folder? Are you using symlinks?");
|
|
||||||
await HandleDefaultErrorPageAsync(rc, HttpStatusCode.NotFound);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (File.Exists(staticResponsePath)) {
|
|
||||||
rc.SetStatusCode(HttpStatusCode.OK);
|
|
||||||
if (staticResponsePath.EndsWith(".svg")) {
|
|
||||||
rc.ListenerContext.Response.AddHeader("Content-Type", "image/svg+xml");
|
|
||||||
}
|
|
||||||
using var f = File.OpenRead(staticResponsePath);
|
|
||||||
await f.CopyToAsync(rc.ListenerContext.Response.OutputStream);
|
|
||||||
} else {
|
} else {
|
||||||
await HandleDefaultErrorPageAsync(rc, HttpStatusCode.NotFound);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// invoke 404
|
// invoke 404
|
||||||
await HandleDefaultErrorPageAsync(rc, 404);
|
await HandleDefaultErrorPageAsync(rc, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
await HandleDefaultErrorPageAsync(rc, 500);
|
logger.Fatal($"Caught otherwise uncaught exception while ProcessingRequest:\n{ex}");
|
||||||
mainLogger.Fatal($"Caught otherwise uncaught exception while ProcessingRequest:\n{ex}");
|
|
||||||
} finally {
|
|
||||||
try { await rc.RespWriter.FlushAsync(); } catch (ObjectDisposedException) { }
|
|
||||||
rc.ListenerContext.Response.Close();
|
|
||||||
LogRequest();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task HandleDefaultErrorPageAsync(RequestContext ctx, HttpStatusCode errorCode, string? statusDescription = null) => await HandleDefaultErrorPageAsync(ctx, (int) errorCode, statusDescription);
|
|
||||||
|
|
||||||
private static async Task HandleDefaultErrorPageAsync(RequestContext ctx, int errorCode, string? statusDescription = null) {
|
private static async Task HandleDefaultErrorPageAsync(RequestContext ctx, int errorCode) {
|
||||||
ctx.SetStatusCode(errorCode);
|
|
||||||
string desc = statusDescription != null ? $"\r\n{statusDescription}" : "";
|
|
||||||
await ctx.WriteLineToRespAsync($"""
|
await ctx.WriteLineToRespAsync($"""
|
||||||
<body>
|
<body>
|
||||||
<h1>Oh no, an error occurred!</h1>
|
<h1>Oh no, and error occurred!</h1>
|
||||||
<p>Code: {errorCode}</p>{desc}
|
<p>Code: {errorCode}</p>
|
||||||
</body>
|
</body>
|
||||||
""");
|
""");
|
||||||
try {
|
|
||||||
if (statusDescription == null) {
|
|
||||||
await ctx.SetStatusCodeAndDisposeAsync(errorCode);
|
|
||||||
} else {
|
|
||||||
await ctx.SetStatusCodeAndDisposeAsync(errorCode, statusDescription);
|
|
||||||
}
|
|
||||||
} catch (ObjectDisposedException) { }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
7
SimpleHttpServer/IAuthorizer.cs
Normal file
7
SimpleHttpServer/IAuthorizer.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace SimpleHttpServer;
|
||||||
|
|
||||||
|
public interface IAuthorizer {
|
||||||
|
public abstract (bool auth, object? data) IsAuthenticated(HttpListenerContext contect);
|
||||||
|
}
|
||||||
7
SimpleHttpServer/Internal/DefaultAuthorizer.cs
Normal file
7
SimpleHttpServer/Internal/DefaultAuthorizer.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace SimpleHttpServer.Internal;
|
||||||
|
|
||||||
|
public sealed class DefaultAuthorizer : IAuthorizer {
|
||||||
|
public (bool auth, object? data) IsAuthenticated(HttpListenerContext contect) => (true, null);
|
||||||
|
}
|
||||||
|
|
@ -1,73 +1,73 @@
|
||||||
//using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
//using System.Collections;
|
using System.Collections;
|
||||||
//using System.Net;
|
using System.Net;
|
||||||
//using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
//namespace SimpleHttpServer.Internal;
|
namespace SimpleHttpServer.Internal;
|
||||||
|
|
||||||
//internal class HttpEndpointHandler {
|
internal class HttpEndpointHandler {
|
||||||
// private static readonly DefaultAuthorizer defaultAuth = new();
|
private static readonly DefaultAuthorizer defaultAuth = new();
|
||||||
|
|
||||||
// private readonly IAuthorizer auth;
|
private readonly IAuthorizer auth;
|
||||||
// private readonly MethodInfo handler;
|
private readonly MethodInfo handler;
|
||||||
// private readonly Dictionary<string, (int pindex, Type type, int pparamIdx)> @params;
|
private readonly Dictionary<string, (int pindex, Type type, int pparamIdx)> @params;
|
||||||
// private readonly Func<Exception, HttpResponseBuilder> errorPageBuilder;
|
private readonly Func<Exception, HttpResponseBuilder> errorPageBuilder;
|
||||||
|
|
||||||
// public HttpEndpointHandler() {
|
public HttpEndpointHandler() {
|
||||||
// auth = defaultAuth;
|
auth = defaultAuth;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// public HttpEndpointHandler(IAuthorizer auth) {
|
public HttpEndpointHandler(IAuthorizer auth) {
|
||||||
|
|
||||||
// }
|
}
|
||||||
|
|
||||||
// public virtual void Handle(HttpListenerContext ctx) {
|
public virtual void Handle(HttpListenerContext ctx) {
|
||||||
// try {
|
try {
|
||||||
// var (isAuth, authData) = auth.IsAuthenticated(ctx);
|
var (isAuth, authData) = auth.IsAuthenticated(ctx);
|
||||||
// if (!isAuth) {
|
if (!isAuth) {
|
||||||
// throw new HttpHandlingException(401, "Authorization required!");
|
throw new HttpHandlingException(401, "Authorization required!");
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // collect parameters
|
// collect parameters
|
||||||
// var invokeParams = new object?[@params.Count + 1];
|
var invokeParams = new object?[@params.Count + 1];
|
||||||
// var set = new BitArray(@params.Count);
|
var set = new BitArray(@params.Count);
|
||||||
// invokeParams[0] = ctx;
|
invokeParams[0] = ctx;
|
||||||
|
|
||||||
// // read pparams
|
// read pparams
|
||||||
|
|
||||||
// // read qparams
|
// read qparams
|
||||||
// var qst = ctx.Request.QueryString;
|
var qst = ctx.Request.QueryString;
|
||||||
// foreach (var qelem in ctx.Request.QueryString.AllKeys) {
|
foreach (var qelem in ctx.Request.QueryString.AllKeys) {
|
||||||
// if (@params.ContainsKey(qelem!)) {
|
if (@params.ContainsKey(qelem!)) {
|
||||||
// var (pindex, type, isPParam) = @params[qelem!];
|
var (pindex, type, isPParam) = @params[qelem!];
|
||||||
// if (type == typeof(string)) {
|
if (type == typeof(string)) {
|
||||||
// invokeParams[pindex] = ctx.Request.QueryString[qelem!];
|
invokeParams[pindex] = ctx.Request.QueryString[qelem!];
|
||||||
// set.Set(pindex - 1, true);
|
set.Set(pindex - 1, true);
|
||||||
// } else {
|
} else {
|
||||||
// var elem = JsonConvert.DeserializeObject(ctx.Request.QueryString[qelem!]!, type);
|
var elem = JsonConvert.DeserializeObject(ctx.Request.QueryString[qelem!]!, type);
|
||||||
// if (elem != null) {
|
if (elem != null) {
|
||||||
// invokeParams[pindex] = elem;
|
invokeParams[pindex] = elem;
|
||||||
// set.Set(pindex - 1, true);
|
set.Set(pindex - 1, true);
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // fill with defaults
|
// fill with defaults
|
||||||
// foreach (var p in @params) {
|
foreach (var p in @params) {
|
||||||
// if (!set.Get(p.Value.pindex)) {
|
if (!set.Get(p.Value.pindex)) {
|
||||||
// invokeParams[p.Value.pindex] = p.Value.type.IsValueType ? Activator.CreateInstance(p.Value.type) : null;
|
invokeParams[p.Value.pindex] = p.Value.type.IsValueType ? Activator.CreateInstance(p.Value.type) : null;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
|
||||||
// var builder = handler.Invoke(null, invokeParams) as HttpResponseBuilder;
|
var builder = handler.Invoke(null, invokeParams) as HttpResponseBuilder;
|
||||||
// builder!.SendResponse(ctx.Response);
|
builder!.SendResponse(ctx.Response);
|
||||||
// } catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// if (e is TargetInvocationException tex) {
|
if (e is TargetInvocationException tex) {
|
||||||
// e = tex.InnerException!;
|
e = tex.InnerException!;
|
||||||
// }
|
}
|
||||||
// errorPageBuilder(e).SendResponse(ctx.Response);
|
errorPageBuilder(e).SendResponse(ctx.Response);
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
//}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,245 +1,243 @@
|
||||||
//using Newtonsoft.Json;
|
using Konscious.Security.Cryptography;
|
||||||
//using System.Diagnostics.CodeAnalysis;
|
using Newtonsoft.Json;
|
||||||
//using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
//using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
//namespace SimpleHttpServer.Login;
|
namespace SimpleHttpServer.Login;
|
||||||
|
|
||||||
//internal struct SerialLoginData {
|
internal struct SerialLoginData {
|
||||||
// public string passwordSalt;
|
public string salt;
|
||||||
// public string extraDataSalt;
|
public string pwd;
|
||||||
// public string pwd;
|
public string additionalData;
|
||||||
// public string extraData;
|
|
||||||
|
|
||||||
// public LoginData ToPlainData() {
|
public LoginData toPlainData() {
|
||||||
// return new LoginData {
|
return new LoginData {
|
||||||
// passwordSalt = Convert.FromBase64String(passwordSalt),
|
salt = Convert.FromBase64String(salt),
|
||||||
// extraDataSalt = Convert.FromBase64String(extraDataSalt)
|
password = Convert.FromBase64String(pwd)
|
||||||
// };
|
};
|
||||||
// }
|
}
|
||||||
//}
|
}
|
||||||
|
|
||||||
//internal struct LoginData {
|
internal struct LoginData {
|
||||||
// public byte[] passwordSalt;
|
public byte[] salt;
|
||||||
// public byte[] extraDataSalt;
|
public byte[] password;
|
||||||
// public byte[] passwordHash;
|
public byte[] encryptedData;
|
||||||
// public byte[] encryptedExtraData;
|
|
||||||
|
|
||||||
// public SerialLoginData ToSerial() {
|
public SerialLoginData toSerial() {
|
||||||
// return new SerialLoginData {
|
return new SerialLoginData {
|
||||||
// passwordSalt = Convert.ToBase64String(passwordSalt),
|
salt = Convert.ToBase64String(salt),
|
||||||
// extraDataSalt = Convert.ToBase64String(extraDataSalt),
|
pwd = Convert.ToBase64String(password),
|
||||||
// pwd = Convert.ToBase64String(passwordHash),
|
additionalData = Convert.ToBase64String(encryptedData)
|
||||||
// extraData = Convert.ToBase64String(encryptedExtraData)
|
};
|
||||||
// };
|
}
|
||||||
// }
|
}
|
||||||
//}
|
|
||||||
|
|
||||||
//internal struct LoginDataProviderConfig {
|
internal struct LoginDataProviderConfig {
|
||||||
|
|
||||||
// /// <summary>
|
public int SALT_SIZE = 32;
|
||||||
// /// Size of the password salt and the extradata salt. So each salt will be of size <see cref="SALT_SIZE"/>.
|
public int KEY_LENGTH = 256 / 8;
|
||||||
// /// </summary>
|
public int A2_ITERATIONS = 5;
|
||||||
// public int SALT_SIZE = 32;
|
public int A2_MEMORY_SIZE = 500_000;
|
||||||
// public int KEY_LENGTH = 256 / 8;
|
public int A2_PARALLELISM = 8;
|
||||||
// public int PBKDF2_ITERATIONS = 600_000;
|
public int A2_HASH_LENGTH = 256 / 8;
|
||||||
|
public int A2_MAX_CONCURRENT = 4;
|
||||||
|
public int PBKDF2_ITERATIONS = 600_000;
|
||||||
|
|
||||||
// public LoginDataProviderConfig() { }
|
public LoginDataProviderConfig() { }
|
||||||
//}
|
}
|
||||||
|
|
||||||
//public class LoginProvider<TExtraData> {
|
public class LoginProvider<T> {
|
||||||
|
|
||||||
// private static readonly Func<TExtraData, byte[]> JsonSerialize = t => Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(t));
|
private static readonly Func<T, byte[]> JsonSerialize = t => Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(t));
|
||||||
// private static readonly Func<byte[], TExtraData> JsonDeserialize = b => JsonConvert.DeserializeObject<TExtraData>(Encoding.UTF8.GetString(b))!;
|
private static readonly Func<byte[], T> JsonDeserialize = b => JsonConvert.DeserializeObject<T>(Encoding.UTF8.GetString(b))!;
|
||||||
|
|
||||||
// [ThreadStatic]
|
private readonly LoginDataProviderConfig config;
|
||||||
// private static SHA256? _sha256PerThread;
|
private readonly ReaderWriterLockSlim ldLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
|
||||||
// private static SHA256 Sha256PerThread { get => _sha256PerThread ??= SHA256.Create(); }
|
private readonly string ldPath;
|
||||||
|
private readonly Dictionary<string, LoginData> loginData;
|
||||||
|
private readonly SemaphoreSlim argon2Limit;
|
||||||
|
|
||||||
// private readonly LoginDataProviderConfig config;
|
private Func<T, byte[]> DataSerializer = JsonSerialize;
|
||||||
// private readonly ReaderWriterLockSlim ldLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
|
private Func<byte[], T> DataDeserializer = JsonDeserialize;
|
||||||
// private readonly string ldPath;
|
|
||||||
// private readonly Dictionary<string, LoginData> loginDatas;
|
|
||||||
|
|
||||||
// private Func<TExtraData, byte[]> DataSerializer = JsonSerialize;
|
public LoginProvider(string ldPath, string confPath) {
|
||||||
// private Func<byte[], TExtraData> DataDeserializer = JsonDeserialize;
|
this.ldPath = ldPath;
|
||||||
// public void SetDataSerializers(Func<TExtraData, byte[]> serializer, Func<byte[], TExtraData> deserializer) {
|
loginData = LoadLoginData(ldPath);
|
||||||
// DataSerializer = serializer ?? JsonSerialize;
|
config = LoadArgon2Config(confPath);
|
||||||
// DataDeserializer = deserializer ?? JsonDeserialize;
|
argon2Limit = new SemaphoreSlim(config.A2_MAX_CONCURRENT);
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, LoginData> LoadLoginData(string path) {
|
||||||
|
Dictionary<string, SerialLoginData> tempData;
|
||||||
|
if (!File.Exists(path)) {
|
||||||
|
File.WriteAllText(path, "{}", Encoding.UTF8);
|
||||||
|
tempData = new();
|
||||||
|
} else {
|
||||||
|
tempData = JsonConvert.DeserializeObject<Dictionary<string, SerialLoginData>>(File.ReadAllText(path))!;
|
||||||
|
if (tempData == null) {
|
||||||
|
throw new InvalidDataException($"could not read login data from file {path}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var ld = new Dictionary<string, LoginData>();
|
||||||
|
foreach (var pair in tempData!) {
|
||||||
|
ld.Add(pair.Key, pair.Value.toPlainData());
|
||||||
|
}
|
||||||
|
return ld;
|
||||||
|
}
|
||||||
|
|
||||||
// public LoginProvider(string ldPath, string confPath) {
|
private static LoginDataProviderConfig LoadArgon2Config(string path) {
|
||||||
// this.ldPath = ldPath;
|
if (!File.Exists(path)) {
|
||||||
// loginDatas = LoadLoginDatas(ldPath);
|
var conf = new LoginDataProviderConfig();
|
||||||
// config = LoadLoginProviderConfig(confPath);
|
File.WriteAllText(path, JsonConvert.SerializeObject(conf));
|
||||||
// }
|
return conf;
|
||||||
|
}
|
||||||
|
return JsonConvert.DeserializeObject<LoginDataProviderConfig>(File.ReadAllText(path));
|
||||||
|
}
|
||||||
|
|
||||||
// private static Dictionary<string, LoginData> LoadLoginDatas(string path) {
|
public void SetDataSerialization(Func<T, byte[]> serializer, Func<byte[], T> deserializer) {
|
||||||
// Dictionary<string, SerialLoginData> tempData;
|
DataSerializer = serializer ?? JsonSerialize;
|
||||||
// if (!File.Exists(path)) {
|
DataDeserializer = deserializer ?? JsonDeserialize;
|
||||||
// File.WriteAllText(path, "{}", Encoding.UTF8);
|
}
|
||||||
// tempData = new();
|
|
||||||
// } else {
|
|
||||||
// tempData = JsonConvert.DeserializeObject<Dictionary<string, SerialLoginData>>(File.ReadAllText(path))!;
|
|
||||||
// if (tempData == null) {
|
|
||||||
// throw new InvalidDataException($"could not read login data from file {path}");
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// var ld = new Dictionary<string, LoginData>();
|
|
||||||
// foreach (var pair in tempData) {
|
|
||||||
// ld.Add(pair.Key, pair.Value.ToPlainData());
|
|
||||||
// }
|
|
||||||
// return ld;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// private void SaveLoginData() {
|
private void StoreLoginData() {
|
||||||
// var serial = new Dictionary<string, SerialLoginData>();
|
var serial = new Dictionary<string, SerialLoginData>();
|
||||||
// ldLock.EnterWriteLock();
|
ldLock.EnterWriteLock();
|
||||||
// try {
|
try {
|
||||||
// foreach (var pair in loginDatas) {
|
foreach (var pair in loginData!) {
|
||||||
// serial.Add(pair.Key, pair.Value.ToSerial());
|
serial.Add(pair.Key, pair.Value.toSerial());
|
||||||
// }
|
}
|
||||||
// } finally {
|
} finally {
|
||||||
// ldLock.ExitWriteLock();
|
ldLock.ExitWriteLock();
|
||||||
// }
|
}
|
||||||
// File.WriteAllText(ldPath, JsonConvert.SerializeObject(serial));
|
File.WriteAllText(ldPath, JsonConvert.SerializeObject(serial));
|
||||||
// }
|
}
|
||||||
|
|
||||||
// private static LoginDataProviderConfig LoadLoginProviderConfig(string path) {
|
public bool AddUser(string username, string password, T additional) {
|
||||||
// if (!File.Exists(path)) {
|
ldLock.EnterWriteLock();
|
||||||
// var conf = new LoginDataProviderConfig();
|
try {
|
||||||
// File.WriteAllText(path, JsonConvert.SerializeObject(conf));
|
if (loginData.ContainsKey(username)) {
|
||||||
// return conf;
|
return false;
|
||||||
// }
|
}
|
||||||
// return JsonConvert.DeserializeObject<LoginDataProviderConfig>(File.ReadAllText(path));
|
var salt = RandomNumberGenerator.GetBytes(config.SALT_SIZE);
|
||||||
// }
|
var pwdHash = HashPwd(password, salt);
|
||||||
|
LoginData ld = new LoginData() {
|
||||||
|
salt = salt,
|
||||||
|
password = pwdHash,
|
||||||
|
encryptedData = EncryptAdditionalData(password, salt, additional)
|
||||||
|
};
|
||||||
|
loginData.Add(username, ld);
|
||||||
|
StoreLoginData();
|
||||||
|
} finally {
|
||||||
|
ldLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// public bool AddUser(string username, string password, TExtraData additional) {
|
public bool RemoveUser(string username) {
|
||||||
// ldLock.EnterWriteLock();
|
ldLock.EnterWriteLock();
|
||||||
// try {
|
try {
|
||||||
// if (loginDatas.ContainsKey(username)) {
|
var removed = loginData.Remove(username);
|
||||||
// return false;
|
if (removed) {
|
||||||
// }
|
StoreLoginData();
|
||||||
// var passwordSalt = RandomNumberGenerator.GetBytes(config.SALT_SIZE);
|
}
|
||||||
// var extraDataSalt = RandomNumberGenerator.GetBytes(config.SALT_SIZE);
|
return removed;
|
||||||
// LoginData ld = new LoginData() {
|
} finally {
|
||||||
// passwordSalt = passwordSalt,
|
ldLock.ExitWriteLock();
|
||||||
// extraDataSalt = extraDataSalt,
|
}
|
||||||
// passwordHash = ComputeSaltedSha256Hash(password, passwordSalt),
|
}
|
||||||
// encryptedExtraData = EncryptExtraData(password, extraDataSalt, additional),
|
|
||||||
// };
|
|
||||||
// loginDatas.Add(username, ld);
|
|
||||||
// SaveLoginData();
|
|
||||||
// } finally {
|
|
||||||
// ldLock.ExitWriteLock();
|
|
||||||
// }
|
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public bool RemoveUser(string username) {
|
public bool ModifyUser(string username, string newPassword, T newAdditional) {
|
||||||
// ldLock.EnterWriteLock();
|
ldLock.EnterWriteLock();
|
||||||
// try {
|
try {
|
||||||
// var removed = loginDatas.Remove(username);
|
if (!loginData.ContainsKey(username)) {
|
||||||
// if (removed) {
|
return false;
|
||||||
// SaveLoginData();
|
}
|
||||||
// }
|
loginData.Remove(username, out var data);
|
||||||
// return removed;
|
data.password = HashPwd(newPassword, data.salt);
|
||||||
// } finally {
|
data.encryptedData = EncryptAdditionalData(newPassword, data.salt, newAdditional);
|
||||||
// ldLock.ExitWriteLock();
|
loginData.Add(username, data);
|
||||||
// }
|
StoreLoginData();
|
||||||
// }
|
} finally {
|
||||||
|
ldLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// public bool ModifyUser(string username, string newPassword, TExtraData newExtraData) {
|
public (bool, T) Authenticate(string username, string password) {
|
||||||
// ldLock.EnterWriteLock();
|
LoginData data;
|
||||||
// try {
|
ldLock.EnterReadLock();
|
||||||
// if (!loginDatas.ContainsKey(username)) {
|
try {
|
||||||
// return false;
|
if (!loginData.TryGetValue(username, out data)) {
|
||||||
// }
|
return (false, default(T)!);
|
||||||
// loginDatas.Remove(username, out var data);
|
}
|
||||||
// data.passwordHash = ComputeSaltedSha256Hash(newPassword, data.passwordSalt);
|
} finally {
|
||||||
// data.encryptedExtraData = EncryptExtraData(newPassword, data.extraDataSalt, newExtraData);
|
ldLock.ExitReadLock();
|
||||||
// loginDatas.Add(username, data);
|
}
|
||||||
// SaveLoginData();
|
var hash = HashPwd(password, data.salt);
|
||||||
// } finally {
|
if (!hash.SequenceEqual(data.password)) {
|
||||||
// ldLock.ExitWriteLock();
|
return (false, default(T)!);
|
||||||
// }
|
}
|
||||||
// return true;
|
return (true, DecryptAdditionalData(password, data.salt, data.encryptedData));
|
||||||
// }
|
}
|
||||||
|
|
||||||
// public bool TryAuthenticate(string username, string password, [MaybeNullWhen(false)] out TExtraData extraData) {
|
private byte[] HashPwd(string pwd, byte[] salt) {
|
||||||
// LoginData data;
|
byte[] hash;
|
||||||
// ldLock.EnterReadLock();
|
argon2Limit.Wait();
|
||||||
// try {
|
try {
|
||||||
// if (!loginDatas.TryGetValue(username, out data)) {
|
using (var argon2 = new Argon2id(Encoding.UTF8.GetBytes(pwd))) {
|
||||||
// extraData = default;
|
argon2.Iterations = config.A2_ITERATIONS;
|
||||||
// return false;
|
argon2.MemorySize = config.A2_MEMORY_SIZE;
|
||||||
// }
|
argon2.DegreeOfParallelism = config.A2_PARALLELISM;
|
||||||
// } finally {
|
argon2.Salt = salt;
|
||||||
// ldLock.ExitReadLock();
|
hash = argon2.GetBytes(config.A2_HASH_LENGTH);
|
||||||
// }
|
}
|
||||||
// var hash = ComputeSaltedSha256Hash(password, data.passwordSalt);
|
// force collection to reduce sustained memory usage if many hashes are done in close time proximity to each other
|
||||||
// if (!hash.SequenceEqual(data.passwordHash)) {
|
GC.Collect();
|
||||||
// extraData = default;
|
} finally {
|
||||||
// return false;
|
argon2Limit.Release();
|
||||||
// }
|
}
|
||||||
// extraData = DecryptExtraData(password, data.extraDataSalt, data.encryptedExtraData);
|
return hash;
|
||||||
// return true;
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// /// <summary>
|
private byte[] EncryptAdditionalData(string pwd, byte[] salt, T data) {
|
||||||
// /// Threadsafe as the SHA256 instance (<see cref="Sha256PerThread"/>) is per thread.
|
var pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(pwd), salt, config.PBKDF2_ITERATIONS, HashAlgorithmName.SHA256);
|
||||||
// /// </summary>
|
var key = pbkdf2.GetBytes(config.KEY_LENGTH / 8);
|
||||||
// /// <param name="data"></param>
|
|
||||||
// /// <param name="salt"></param>
|
|
||||||
// /// <returns></returns>
|
|
||||||
// private static byte[] ComputeSaltedSha256Hash(string data, byte[] salt) {
|
|
||||||
// var dataBytes = Encoding.UTF8.GetBytes(data);
|
|
||||||
// var buf = new byte[data.Length + salt.Length];
|
|
||||||
// Buffer.BlockCopy(dataBytes, 0, buf, 0, dataBytes.Length);
|
|
||||||
// Buffer.BlockCopy(salt, 0, buf, dataBytes.Length, salt.Length);
|
|
||||||
// return Sha256PerThread.ComputeHash(buf);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// private byte[] EncryptExtraData(string pwd, byte[] salt, TExtraData extraData) {
|
var plainBytes = DataSerializer(data);
|
||||||
// var pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(pwd), salt, config.PBKDF2_ITERATIONS, HashAlgorithmName.SHA256);
|
using var aes = Aes.Create();
|
||||||
// var key = pbkdf2.GetBytes(config.KEY_LENGTH / 8);
|
aes.KeySize = config.KEY_LENGTH;
|
||||||
|
aes.Key = key;
|
||||||
|
aes.Mode = CipherMode.CBC;
|
||||||
|
aes.Padding = PaddingMode.PKCS7;
|
||||||
|
ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
|
||||||
|
byte[] cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
|
||||||
|
|
||||||
// var plainBytes = DataSerializer(extraData);
|
var encryptedBytes = new byte[aes.IV.Length + cipherBytes.Length];
|
||||||
// using var aes = Aes.Create();
|
Array.Copy(aes.IV, 0, encryptedBytes, 0, aes.IV.Length);
|
||||||
// aes.KeySize = config.KEY_LENGTH;
|
Array.Copy(cipherBytes, 0, encryptedBytes, aes.IV.Length, cipherBytes.Length);
|
||||||
// aes.Key = key;
|
|
||||||
// aes.Mode = CipherMode.CBC;
|
|
||||||
// aes.Padding = PaddingMode.PKCS7;
|
|
||||||
// ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
|
|
||||||
// byte[] cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
|
|
||||||
|
|
||||||
// var encryptedBytes = new byte[aes.IV.Length + cipherBytes.Length];
|
return encryptedBytes;
|
||||||
// Array.Copy(aes.IV, 0, encryptedBytes, 0, aes.IV.Length);
|
}
|
||||||
// Array.Copy(cipherBytes, 0, encryptedBytes, aes.IV.Length, cipherBytes.Length);
|
|
||||||
|
|
||||||
// return encryptedBytes;
|
private T DecryptAdditionalData(string pwd, byte[] salt, byte[] encryptedData) {
|
||||||
// }
|
var pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(pwd), salt, config.PBKDF2_ITERATIONS, HashAlgorithmName.SHA256);
|
||||||
|
var key = pbkdf2.GetBytes(config.KEY_LENGTH / 8);
|
||||||
|
|
||||||
// private TExtraData DecryptExtraData(string pwd, byte[] salt, byte[] encryptedData) {
|
using var aes = Aes.Create();
|
||||||
// var pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(pwd), salt, config.PBKDF2_ITERATIONS, HashAlgorithmName.SHA256);
|
aes.KeySize = config.KEY_LENGTH;
|
||||||
// var key = pbkdf2.GetBytes(config.KEY_LENGTH / 8);
|
aes.Key = key;
|
||||||
|
aes.Mode = CipherMode.CBC;
|
||||||
|
aes.Padding = PaddingMode.PKCS7;
|
||||||
|
var iv = new byte[aes.BlockSize / 8];
|
||||||
|
var cipherBytes = new byte[encryptedData.Length - iv.Length];
|
||||||
|
|
||||||
// using var aes = Aes.Create();
|
Array.Copy(encryptedData, 0, iv, 0, iv.Length);
|
||||||
// aes.KeySize = config.KEY_LENGTH;
|
Array.Copy(encryptedData, iv.Length, cipherBytes, 0, cipherBytes.Length);
|
||||||
// aes.Key = key;
|
|
||||||
// aes.Mode = CipherMode.CBC;
|
|
||||||
// aes.Padding = PaddingMode.PKCS7;
|
|
||||||
// var iv = new byte[aes.BlockSize / 8];
|
|
||||||
// var cipherBytes = new byte[encryptedData.Length - iv.Length];
|
|
||||||
|
|
||||||
// Array.Copy(encryptedData, 0, iv, 0, iv.Length);
|
aes.IV = iv;
|
||||||
// Array.Copy(encryptedData, iv.Length, cipherBytes, 0, cipherBytes.Length);
|
ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
|
||||||
|
byte[] plainBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
|
||||||
|
|
||||||
// aes.IV = iv;
|
return DataDeserializer(plainBytes);
|
||||||
// ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
|
}
|
||||||
// byte[] plainBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
|
}
|
||||||
|
|
||||||
// return DataDeserializer(plainBytes);
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,19 @@
|
||||||
using System.Collections.ObjectModel;
|
using System.Net;
|
||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace SimpleHttpServer.Types;
|
namespace SimpleHttpServer;
|
||||||
public class RequestContext : IDisposable {
|
public class RequestContext : IDisposable {
|
||||||
|
|
||||||
public HttpListenerContext ListenerContext { get; }
|
public HttpListenerContext ListenerContext { get; }
|
||||||
public ReadOnlyDictionary<string, string> ParsedParameters { get; internal set; }
|
|
||||||
|
|
||||||
private TextReader? reqReader;
|
private StreamReader? reqReader;
|
||||||
/// <summary>
|
public StreamReader ReqReader => reqReader ??= new(ListenerContext.Request.InputStream);
|
||||||
/// THREADSAFE
|
|
||||||
/// </summary>
|
|
||||||
public TextReader ReqReader => reqReader ??= TextReader.Synchronized(new StreamReader(ListenerContext.Request.InputStream));
|
|
||||||
|
|
||||||
private TextWriter? respWriter;
|
private StreamWriter? respWriter;
|
||||||
/// <summary>
|
public StreamWriter RespWriter => respWriter ??= new(ListenerContext.Response.OutputStream) { NewLine = "\n" };
|
||||||
/// THREADSAFE
|
|
||||||
/// </summary>
|
|
||||||
public TextWriter RespWriter => respWriter ??= TextWriter.Synchronized(new StreamWriter(ListenerContext.Response.OutputStream) { NewLine = "\n" });
|
|
||||||
|
|
||||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
|
||||||
public RequestContext(HttpListenerContext listenerContext) {
|
public RequestContext(HttpListenerContext listenerContext) {
|
||||||
ListenerContext = listenerContext;
|
ListenerContext = listenerContext;
|
||||||
}
|
}
|
||||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
|
||||||
|
|
||||||
public async Task WriteLineToRespAsync(string resp) => await RespWriter.WriteLineAsync(resp);
|
public async Task WriteLineToRespAsync(string resp) => await RespWriter.WriteLineAsync(resp);
|
||||||
public async Task WriteToRespAsync(string resp) => await RespWriter.WriteAsync(resp);
|
public async Task WriteToRespAsync(string resp) => await RespWriter.WriteAsync(resp);
|
||||||
|
|
@ -35,46 +25,27 @@ public class RequestContext : IDisposable {
|
||||||
|
|
||||||
public void SetStatusCode(HttpStatusCode status) => SetStatusCode((int) status);
|
public void SetStatusCode(HttpStatusCode status) => SetStatusCode((int) status);
|
||||||
|
|
||||||
public async Task SetStatusCodeWriteLineDisposeAsync(HttpStatusCode status, string message) {
|
public void SetStatusCodeAndDispose(int status) {
|
||||||
|
using (this)
|
||||||
SetStatusCode(status);
|
SetStatusCode(status);
|
||||||
await WriteLineToRespAsync(message);
|
|
||||||
await RespWriter.FlushAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetStatusCodeAndDisposeAsync(int status) {
|
public void SetStatusCodeAndDispose(HttpStatusCode status) {
|
||||||
using (this) {
|
using (this)
|
||||||
SetStatusCode(status);
|
|
||||||
await WriteToRespAsync("\n\n");
|
|
||||||
await RespWriter.FlushAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SetStatusCodeAndDisposeAsync(HttpStatusCode status) {
|
|
||||||
using (this) {
|
|
||||||
SetStatusCode((int) status);
|
SetStatusCode((int) status);
|
||||||
await WriteToRespAsync("\n\n");
|
|
||||||
await RespWriter.FlushAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task SetStatusCodeAndDisposeAsync(int status, string description) {
|
public void SetStatusCodeAndDispose(int status, string description) {
|
||||||
using (this) {
|
using (this) {
|
||||||
ListenerContext.Response.StatusCode = status;
|
ListenerContext.Response.StatusCode = status;
|
||||||
ListenerContext.Response.StatusDescription = description;
|
ListenerContext.Response.StatusDescription = description;
|
||||||
await WriteToRespAsync("\n\n");
|
|
||||||
await RespWriter.FlushAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public async Task SetStatusCodeAndDisposeAsync(HttpStatusCode status, string description) => await SetStatusCodeAndDisposeAsync((int) status, description);
|
public void SetStatusCodeAndDispose(HttpStatusCode status, string description) => SetStatusCodeAndDispose((int) status, description);
|
||||||
|
|
||||||
|
|
||||||
public async Task WriteRedirect302AndDisposeAsync(string url) {
|
void IDisposable.Dispose() {
|
||||||
ListenerContext.Response.AddHeader("Location", url);
|
|
||||||
await SetStatusCodeAndDisposeAsync(HttpStatusCode.Redirect);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose() {
|
|
||||||
reqReader?.Dispose();
|
reqReader?.Dispose();
|
||||||
respWriter?.Dispose();
|
respWriter?.Dispose();
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,6 @@ public class SimpleHttpServerConfiguration {
|
||||||
/// See description of <see cref="DisableLogMessagePrinting"/>
|
/// See description of <see cref="DisableLogMessagePrinting"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CustomLogMessageHandler? LogMessageHandler { get; init; } = null;
|
public CustomLogMessageHandler? LogMessageHandler { get; init; } = null;
|
||||||
/// <summary>
|
|
||||||
/// If set to true, paths ending with / are identical to paths without said trailing slash. E.g. /index is then the same as /index/
|
|
||||||
/// </summary>
|
|
||||||
public bool TrimTrailingSlash { get; init; } = true;
|
|
||||||
|
|
||||||
public SimpleHttpServerConfiguration() { }
|
public SimpleHttpServerConfiguration() { }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
using System.Net;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace SimpleHttpServer.Types;
|
|
||||||
|
|
||||||
public abstract class InternalEndpointCheckAttribute : Attribute {
|
|
||||||
public InternalEndpointCheckAttribute() {
|
|
||||||
CheckSharedVariables();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CheckSharedVariables() {
|
|
||||||
foreach (var f in GetType().GetRuntimeFields()) {
|
|
||||||
if (f.FieldType.IsAssignableTo(typeof(SharedVariable))) {
|
|
||||||
if (!f.IsInitOnly) {
|
|
||||||
throw new Exception($"Found non-readonly global field {f}!");
|
|
||||||
}
|
|
||||||
if (f.GetValue(this) == null) {
|
|
||||||
throw new Exception("Global fields must be assigned in the CCTOR!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Initialize(object? instance, Dictionary<FieldInfo, List<(InternalEndpointCheckAttribute, SharedVariable)>> globals) {
|
|
||||||
SetInstance(instance);
|
|
||||||
foreach (var f in GetType().GetRuntimeFields()) {
|
|
||||||
if (f.FieldType.IsAssignableTo(typeof(SharedVariable))) {
|
|
||||||
SharedVariable origVal = (SharedVariable) f.GetValue(this)!;
|
|
||||||
if (globals.TryGetValue(f, out var options)) {
|
|
||||||
bool foundMatch = false;
|
|
||||||
foreach ((var checker, var gv) in options) {
|
|
||||||
if (Match(checker)) {
|
|
||||||
foundMatch = true;
|
|
||||||
// we need to unify their global variables
|
|
||||||
f.SetValue(this, gv);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!foundMatch) {
|
|
||||||
options.Add((this, origVal));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
globals.Add(f, new List<(InternalEndpointCheckAttribute, SharedVariable)>() { (this, origVal) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Initialize(object? instance, IEnumerable<InternalEndpointCheckAttribute> endPointChecks) {
|
|
||||||
Dictionary<FieldInfo, List<(InternalEndpointCheckAttribute, SharedVariable)>> globals = new();
|
|
||||||
foreach (var check in endPointChecks) {
|
|
||||||
check.Initialize(instance, globals);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private interface SharedVariable {
|
|
||||||
// Tagging interface
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a Mutable Shared Variable. Fields of this type need to be initialized in the CCtor.
|
|
||||||
/// </summary>
|
|
||||||
protected sealed class MSV<V> : SharedVariable {
|
|
||||||
private readonly V __default;
|
|
||||||
|
|
||||||
public V Val { get; set; } = default!;
|
|
||||||
|
|
||||||
public MSV() : this(default!) { }
|
|
||||||
|
|
||||||
public MSV(V _default) {
|
|
||||||
__default = _default;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static implicit operator V(MSV<V> v) => v.Val;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents an Immutable Shared Variable. Fields of this type need to be initialized in the CCtor.
|
|
||||||
/// </summary>
|
|
||||||
protected sealed class ISV<V> : SharedVariable {
|
|
||||||
private readonly V __default;
|
|
||||||
|
|
||||||
public V Val { get; } = default!;
|
|
||||||
|
|
||||||
public ISV() : this(default!) { }
|
|
||||||
|
|
||||||
public ISV(V _default) {
|
|
||||||
__default = _default;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static implicit operator V(ISV<V> v) => v.Val;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Executed when the endpoint is invoked. The endpoint invocation is skipped if any of the checks fail.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>True to allow invocation, false to prevent.</returns>
|
|
||||||
public abstract bool Check(HttpListenerRequest req);
|
|
||||||
|
|
||||||
protected virtual bool Match(InternalEndpointCheckAttribute other) => true;
|
|
||||||
|
|
||||||
internal abstract void SetInstance(object? instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
|
|
||||||
public abstract class BaseEndpointCheckAttribute<T> : InternalEndpointCheckAttribute {
|
|
||||||
/// <summary>
|
|
||||||
/// A reference to the instance of the class that this attribute is attached to.
|
|
||||||
/// Will be null iff an class factory was passed in <see cref="HttpServer.RegisterEndpointsFromType{T}(Func{T}?)"/>.
|
|
||||||
/// </summary>
|
|
||||||
protected internal T? EndpointClassInstance { get; internal set; } = default;
|
|
||||||
|
|
||||||
public BaseEndpointCheckAttribute() : base() { }
|
|
||||||
|
|
||||||
internal override void SetInstance(object? instance) {
|
|
||||||
if (instance != null)
|
|
||||||
EndpointClassInstance = (T?) instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,47 +1,12 @@
|
||||||
using System.Net;
|
using System.Reflection;
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace SimpleHttpServer.Types;
|
namespace SimpleHttpServer.Types;
|
||||||
internal record EndpointInvocationInfo {
|
internal struct EndpointInvocationInfo {
|
||||||
//internal record struct QueryParameterInfo(string Name, Type Type, bool isPathParam, bool Path_isCatchAll, bool Query_IsOptional) {
|
|
||||||
// public static QueryParameterInfo CreatePathParam(string name, Type type) => new(name, type, false, name == "$*", false);
|
|
||||||
// public static QueryParameterInfo CreateQueryParam(string name, Type type, bool isOptional) => new(name, type, false, false, isOptional);
|
|
||||||
//}
|
|
||||||
internal record struct PathParameterInfo(string Name, Type Type, int ArgPos, int SegmentStartPos, bool IsCatchAll) {
|
|
||||||
public PathParameterInfo(string name, Type type, int argPos) : this(name, type, argPos, -1, name == "$*") { }
|
|
||||||
}
|
|
||||||
|
|
||||||
internal record struct QueryParameterInfo(string Name, Type Type, int ArgPos, bool IsOptional);
|
|
||||||
|
|
||||||
internal readonly MethodInfo methodInfo;
|
internal readonly MethodInfo methodInfo;
|
||||||
internal readonly List<QueryParameterInfo> queryParameters;
|
internal readonly List<(string, (Type type, bool isOptional))> queryParameters;
|
||||||
internal readonly List<PathParameterInfo> pathParameters;
|
|
||||||
internal readonly InternalEndpointCheckAttribute[] requiredChecks;
|
|
||||||
/// <summary>
|
|
||||||
/// a reference to the object in which this method is defined (or null if the class is static)
|
|
||||||
/// </summary>
|
|
||||||
internal readonly object? typeInstanceReference;
|
|
||||||
|
|
||||||
public EndpointInvocationInfo(MethodInfo methodInfo, List<PathParameterInfo> pathParameters, List<QueryParameterInfo> queryParameters, InternalEndpointCheckAttribute[] requiredChecks,
|
|
||||||
object? typeInstanceReference) {
|
|
||||||
|
|
||||||
|
public EndpointInvocationInfo(MethodInfo methodInfo, List<(string, (Type type, bool isOptional))> queryParameters) {
|
||||||
this.methodInfo = methodInfo ?? throw new ArgumentNullException(nameof(methodInfo));
|
this.methodInfo = methodInfo ?? throw new ArgumentNullException(nameof(methodInfo));
|
||||||
this.queryParameters = queryParameters ?? throw new ArgumentNullException(nameof(queryParameters));
|
this.queryParameters = queryParameters ?? throw new ArgumentNullException(nameof(queryParameters));
|
||||||
this.pathParameters = pathParameters ?? throw new ArgumentNullException(nameof(pathParameters));
|
|
||||||
this.requiredChecks = requiredChecks;
|
|
||||||
this.typeInstanceReference = typeInstanceReference;
|
|
||||||
|
|
||||||
if (pathParameters.Any()) {
|
|
||||||
Assert(pathParameters.Count(x => x.IsCatchAll) <= 1); // at most one catchall parameter
|
|
||||||
var argPoses = pathParameters.Select(x => x.ArgPos).Concat(queryParameters.Select(x => x.ArgPos)).ToArray();
|
|
||||||
var argCnt = pathParameters.Count + queryParameters.Count;
|
|
||||||
Assert(argPoses.Distinct().Count() == argCnt); // ArgPoses must be unique
|
|
||||||
Assert(argPoses.Min() == HttpServer.expectedEndpointParameterPrefixCount); // ArgPoses must start from just after the prefix
|
|
||||||
Assert(argPoses.Max() == HttpServer.expectedEndpointParameterPrefixCount + argCnt - 1); // ArgPoses must be contiguous
|
|
||||||
|
|
||||||
Assert(pathParameters.All(x => x.SegmentStartPos != -1));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public bool CheckAll(HttpListenerRequest req) => requiredChecks.All(x => x.Check(req));
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace SimpleHttpServer.Types;
|
|
||||||
internal class MultiKeyDictionary<K1, K2, V> where K1 : notnull where K2 : notnull {
|
|
||||||
internal readonly Dictionary<K1, Dictionary<K2, V>> backingDict = new();
|
|
||||||
public MultiKeyDictionary() { }
|
|
||||||
|
|
||||||
public void Add(K1 k1, K2 k2, V value) {
|
|
||||||
if (!backingDict.TryGetValue(k1, out var d2))
|
|
||||||
d2 = new();
|
|
||||||
d2.Add(k2, value);
|
|
||||||
backingDict[k1] = d2;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryGetValue(K1 k1, K2 k2, [MaybeNullWhen(false)] out V value) {
|
|
||||||
if (backingDict.TryGetValue(k1, out var d2) && d2.TryGetValue(k2, out value))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
value = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -5,6 +5,9 @@
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)]
|
[AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)]
|
||||||
public sealed class ParameterAttribute : Attribute {
|
public sealed class ParameterAttribute : Attribute {
|
||||||
|
// See the attribute guidelines at
|
||||||
|
// http://go.microsoft.com/fwlink/?LinkId=85236
|
||||||
|
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
public bool IsOptional { get; }
|
public bool IsOptional { get; }
|
||||||
public ParameterAttribute(string name, bool isOptional = false) {
|
public ParameterAttribute(string name, bool isOptional = false) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
namespace SimpleHttpServer.Types.ParameterConverters;
|
|
||||||
internal class StringParameterConverter : IParameterConverter {
|
|
||||||
public bool TryConvertFromString(string value, out object result) {
|
|
||||||
result = value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
namespace SimpleHttpServer.Types;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Specifies the name of a http endpoint path parameter. Path parameter names must be in the format $1, $2, $3, ..., and the end of the path may be $*
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class PathParameterAttribute : Attribute {
|
|
||||||
public string Name { get; }
|
|
||||||
public PathParameterAttribute(string name) {
|
|
||||||
if (string.IsNullOrWhiteSpace(name)) {
|
|
||||||
throw new ArgumentException($"'{nameof(name)}' cannot be null or whitespace.", nameof(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!name.StartsWith('$')) {
|
|
||||||
throw new ArgumentException($"'{nameof(name)}' must start with $.", nameof(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.Contains(' ')) {
|
|
||||||
throw new ArgumentException($"'{nameof(name)}' must not contain spaces.", nameof(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!uint.TryParse(name[1..], out _) && name != "$*") {
|
|
||||||
throw new ArgumentException($"'{nameof(name)}' must only consist of spaces or be exactly '$*'.", nameof(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
Name = name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
using System.Data;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace SimpleHttpServer.Types;
|
|
||||||
|
|
||||||
internal class PathTree<T> where T : class {
|
|
||||||
private readonly Node? rootNode = null;
|
|
||||||
|
|
||||||
public PathTree() : this(new()) { }
|
|
||||||
public PathTree(Dictionary<string, T> dict) {
|
|
||||||
if (dict == null || dict.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
rootNode = new();
|
|
||||||
var currNode = rootNode;
|
|
||||||
var unpackedPaths = dict.Keys.Select(p => p.Split('/').ToArray()).ToArray();
|
|
||||||
var unpackedLeafData = dict.Values.ToArray();
|
|
||||||
for (int i = 0; i < unpackedPaths.Length; i++) {
|
|
||||||
var path = unpackedPaths[i];
|
|
||||||
var catchallidx = Array.IndexOf(path, "$*");
|
|
||||||
if (catchallidx != -1 && catchallidx != path.Length - 1) {
|
|
||||||
throw new Exception($"Found illegal catchall-wildcard in path: '{string.Join('/', path)}'");
|
|
||||||
}
|
|
||||||
|
|
||||||
var leafdata = unpackedLeafData[i] ?? throw new ArgumentNullException("Leafdata must not be null!");
|
|
||||||
rootNode.AddSuccessor(path, leafdata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal bool TryGetPath(string reqPath, [MaybeNullWhen(false)] out T endpoint) {
|
|
||||||
if (rootNode == null) {
|
|
||||||
endpoint = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to find path-match
|
|
||||||
Node currNode = rootNode;
|
|
||||||
Assert(reqPath[0] == '/');
|
|
||||||
var splittedPath = reqPath[1..].Split("/");
|
|
||||||
Node? lastCatchallNode = null;
|
|
||||||
for (int i = 0; i < splittedPath.Length; i++) {
|
|
||||||
|
|
||||||
// keep track of the current best catchallNode
|
|
||||||
if (currNode.catchAllNext != null) {
|
|
||||||
lastCatchallNode = currNode.catchAllNext;
|
|
||||||
}
|
|
||||||
|
|
||||||
var seg = splittedPath[i];
|
|
||||||
if (currNode.next?.TryGetValue(seg, out var next) == true) { // look for an explicit path to follow greedily
|
|
||||||
currNode = next;
|
|
||||||
} else if (currNode.pathWildcardNext != null) { // otherwise look for a single-wildcard to follow
|
|
||||||
currNode = currNode.pathWildcardNext;
|
|
||||||
} else { // otherwise we are done, there is no valid path --> fall back to the most specific catchall
|
|
||||||
endpoint = lastCatchallNode?.leafData;
|
|
||||||
return lastCatchallNode != null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// return found path
|
|
||||||
endpoint = currNode.leafData;
|
|
||||||
return endpoint != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Node {
|
|
||||||
public T? leafData = null; // null means that this is a node without a value (e.g. when it is just part of a path)
|
|
||||||
public Dictionary<string, Node>? next = null;
|
|
||||||
public Node? pathWildcardNext = null; // path wildcard
|
|
||||||
public Node? catchAllNext = null; // trailing-catchall wildcard
|
|
||||||
|
|
||||||
public void AddSuccessor(string[] segments, T newLeafData) {
|
|
||||||
if (segments.Length == 0) { // actually add the data to this node
|
|
||||||
Assert(leafData == null);
|
|
||||||
leafData = newLeafData;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var seg = segments[0];
|
|
||||||
bool newIsWildcard = seg.Length > 1 && seg[0] == '$';
|
|
||||||
if (newIsWildcard) {
|
|
||||||
bool newIsCatchallWildcard = newIsWildcard && seg.Length == 2 && seg[1] == '*';
|
|
||||||
if (newIsCatchallWildcard) { // this is a catchall wildcard
|
|
||||||
Assert(catchAllNext == null);
|
|
||||||
catchAllNext = new();
|
|
||||||
catchAllNext.AddSuccessor(segments[1..], newLeafData);
|
|
||||||
return;
|
|
||||||
} else { // must be single wildcard otherwise
|
|
||||||
pathWildcardNext ??= new();
|
|
||||||
pathWildcardNext.AddSuccessor(segments[1..], newLeafData);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise we want to add a new constant path successor
|
|
||||||
next ??= new();
|
|
||||||
|
|
||||||
if (next.TryGetValue(seg, out var existingNode)) {
|
|
||||||
existingNode.AddSuccessor(segments[1..], newLeafData);
|
|
||||||
} else {
|
|
||||||
var newNode = next[seg] = new();
|
|
||||||
newNode.AddSuccessor(segments[1..], newLeafData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
using SimpleHttpServer;
|
using SimpleHttpServer;
|
||||||
using SimpleHttpServer.Types;
|
|
||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace SimpleHttpServerTest;
|
namespace SimpleHttpServerTest;
|
||||||
|
|
||||||
|
|
@ -10,36 +8,19 @@ public class SimpleServerTest {
|
||||||
const int PORT = 8833;
|
const int PORT = 8833;
|
||||||
|
|
||||||
private HttpServer? activeServer = null;
|
private HttpServer? activeServer = null;
|
||||||
private HttpClient? activeHttpClient = null;
|
|
||||||
private bool failOnLogError = true;
|
|
||||||
private static string GetRequestPath(string url) => $"http://localhost:{PORT}/{url.TrimStart('/')}";
|
private static string GetRequestPath(string url) => $"http://localhost:{PORT}/{url.TrimStart('/')}";
|
||||||
private async Task RequestGetStringAsync(string path) => await activeHttpClient!.GetStringAsync(GetRequestPath(path));
|
|
||||||
private async Task<HttpResponseMessage> AssertGetStatusCodeAsync(string path, HttpStatusCode statusCode) {
|
|
||||||
var resp = await activeHttpClient!.GetAsync(GetRequestPath(path));
|
|
||||||
Assert.AreEqual(statusCode, resp.StatusCode);
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestInitialize]
|
[TestInitialize]
|
||||||
public void Init() {
|
public void Init() {
|
||||||
var conf = new SimpleHttpServerConfiguration() {
|
var conf = new SimpleHttpServerConfiguration();
|
||||||
DisableLogMessagePrinting = false,
|
|
||||||
LogMessageHandler = (LogOutputTopic topic, string message, LogOutputLevel logLevel) => {
|
|
||||||
if (failOnLogError && logLevel is LogOutputLevel.Error or LogOutputLevel.Fatal)
|
|
||||||
Assert.Fail($"An error was thrown in the log output:\n{topic} {message}");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (activeServer != null)
|
if (activeServer != null)
|
||||||
throw new InvalidOperationException("Tried to create another httpserver instance when an existing one was already running.");
|
throw new InvalidOperationException("Tried to create another httpserver instance when an existing one was already running.");
|
||||||
|
|
||||||
Console.WriteLine("Starting server...");
|
Console.WriteLine("Starting server...");
|
||||||
failOnLogError = true;
|
|
||||||
activeServer = new HttpServer(PORT, conf);
|
activeServer = new HttpServer(PORT, conf);
|
||||||
activeServer.RegisterEndpointsFromType<TestEndpoints>();
|
activeServer.RegisterEndpointsFromType<TestEndpoints>();
|
||||||
activeServer.Start();
|
activeServer.Start();
|
||||||
|
|
||||||
activeHttpClient = new HttpClient();
|
|
||||||
|
|
||||||
Console.WriteLine("Server started.");
|
Console.WriteLine("Server started.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,87 +33,20 @@ public class SimpleServerTest {
|
||||||
}
|
}
|
||||||
await Console.Out.WriteLineAsync("Shutting down server...");
|
await Console.Out.WriteLineAsync("Shutting down server...");
|
||||||
await activeServer.StopAsync(ctokSrc.Token);
|
await activeServer.StopAsync(ctokSrc.Token);
|
||||||
activeHttpClient?.Dispose();
|
|
||||||
activeHttpClient = null;
|
|
||||||
await Console.Out.WriteLineAsync("Shutdown finished.");
|
await Console.Out.WriteLineAsync("Shutdown finished.");
|
||||||
}
|
}
|
||||||
|
|
||||||
static string GetHttpPageContentFromPrefix(string page)
|
|
||||||
=> $"It works!!!!!!56sg5sdf46a4sd65a412f31sdfgdf89h74g9f8h4as56d4f56as2as1f3d24f87g9d87{page}";
|
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public async Task CheckSimpleServe() {
|
public async Task CheckSimpleServe() {
|
||||||
var resp = await AssertGetStatusCodeAsync("/", HttpStatusCode.OK);
|
using var hc = new HttpClient();
|
||||||
var str = await resp.Content.ReadAsStringAsync();
|
await hc.GetStringAsync(GetRequestPath("/"));
|
||||||
Assert.AreEqual("It works!", str);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
public async Task CheckMultiServe() {
|
|
||||||
|
|
||||||
foreach (var item in "index2.html;testpage;testpage2;testpage3".Split(';')) {
|
|
||||||
await Console.Out.WriteLineAsync($"Checking page: /{item}");
|
|
||||||
var resp = await AssertGetStatusCodeAsync(item, HttpStatusCode.OK);
|
|
||||||
var str = await resp.Content.ReadAsStringAsync();
|
|
||||||
Assert.AreEqual(GetHttpPageContentFromPrefix(item), str);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
public async Task CheckQueryArgs() {
|
|
||||||
foreach (var a1 in "test1;longstring2;something else with a space".Split(';')) {
|
|
||||||
foreach (var a2 in new[] { -10, 2, -2, 5, 0, 4 }) {
|
|
||||||
foreach (var a3 in new[] { -1, 9, 2, -20, 0 }) {
|
|
||||||
foreach (var a4 in new[] { -1, 9, 0 }) {
|
|
||||||
foreach (var page in "returnqueries;returnqueries2".Split(';')) {
|
|
||||||
var resp = await AssertGetStatusCodeAsync($"{page}?arg1={a1}&arg2={a2}&arg3={a3}&arg4={a4}", HttpStatusCode.OK);
|
|
||||||
var str = await resp.Content.ReadAsStringAsync();
|
|
||||||
Assert.AreEqual(TestEndpoints.GetReturnQueryPageResult(a1, a2, page == "returnqueries2" ? (a3 + a4) : a3), str);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestEndpoints {
|
public class TestEndpoints {
|
||||||
[HttpEndpoint(HttpRequestType.GET, "/", "index.html")]
|
|
||||||
|
[HttpEndpoint(HttpRequestType.GET, "/", "index.html", "amogus.html")]
|
||||||
public static async Task Index(RequestContext req) {
|
public static async Task Index(RequestContext req) {
|
||||||
await req.RespWriter.WriteAsync("It works!");
|
await req.RespWriter.WriteLineAsync("It works!");
|
||||||
}
|
|
||||||
|
|
||||||
[HttpEndpoint(HttpRequestType.GET, "index2.html")]
|
|
||||||
public static async Task Index2(RequestContext req) {
|
|
||||||
await req.RespWriter.WriteAsync(GetHttpPageContentFromPrefix("index2.html"));
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpEndpoint(HttpRequestType.GET, "/testpage")]
|
|
||||||
public static async Task TestPage(RequestContext req) {
|
|
||||||
await req.RespWriter.WriteAsync(GetHttpPageContentFromPrefix("testpage"));
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpEndpoint(HttpRequestType.GET, "testpage2")]
|
|
||||||
public static async Task TestPage2(RequestContext req) {
|
|
||||||
await req.RespWriter.WriteAsync(GetHttpPageContentFromPrefix("testpage2"));
|
|
||||||
}
|
|
||||||
[HttpEndpoint(HttpRequestType.GET, "/testpage3")]
|
|
||||||
public static async Task TestPage3(RequestContext req) {
|
|
||||||
await req.RespWriter.WriteAsync(GetHttpPageContentFromPrefix("testpage3"));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static string GetReturnQueryPageResult(string arg1, int arg2, int arg3) => $"{arg1};{arg2 * 2 - arg3 * 5}";
|
|
||||||
|
|
||||||
[HttpEndpoint(HttpRequestType.GET, "/returnqueries")]
|
|
||||||
public static async Task ReturnQueriesPage(RequestContext req, string arg1, int arg2, int arg3) {
|
|
||||||
await req.RespWriter.WriteAsync(GetReturnQueryPageResult(arg1, arg2, arg3));
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpEndpoint(HttpRequestType.GET, "/returnqueries2")]
|
|
||||||
public static async Task ReturnQueriesPage2(RequestContext req,
|
|
||||||
[Parameter("arg2")] int arg1, [Parameter("arg1")] string arg2, int arg3, [Parameter("arg4", true)] int arg4) {
|
|
||||||
// arg4 should be equal to zero as it should get the deafult value because it is not passed to the server
|
|
||||||
await req.RespWriter.WriteAsync(GetReturnQueryPageResult(arg2, arg1, arg3 + arg4));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user