Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdab5151be | ||
|
|
b645d4d654 | ||
|
|
f20ba933dc | ||
|
|
c91714a6af | ||
|
|
a9436bfda8 | ||
|
|
f14294387e | ||
|
|
0bfe34ab6b | ||
|
|
6cc849bf01 | ||
|
|
8cdff9268a | ||
|
|
94b23cadc5 | ||
|
|
0814bc6b2d | ||
|
|
8545ed80e9 | ||
|
|
4fad2d648e | ||
|
|
ea74cb899c | ||
|
|
92e472d526 | ||
|
|
14ab546d4d | ||
|
|
e1e1596e54 | ||
|
|
6d74d659f6 | ||
|
|
0b4975e74b | ||
|
|
d6190b024d | ||
|
|
aa06679742 | ||
|
|
020075ad54 | ||
|
|
f0c9754fb2 | ||
|
|
7ad2b5185b | ||
|
|
08003d1fc3 | ||
|
|
a2a70e8339 | ||
|
|
09fa3b8734 |
@@ -1,4 +1,5 @@
|
|||||||
using SimpleHttpServer.Internal;
|
using SimpleHttpServer.Internal;
|
||||||
|
using SimpleHttpServer.Types;
|
||||||
|
|
||||||
namespace SimpleHttpServer;
|
namespace SimpleHttpServer;
|
||||||
|
|
||||||
|
|||||||
+125
-23
@@ -4,6 +4,7 @@ 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;
|
||||||
|
|
||||||
namespace SimpleHttpServer;
|
namespace SimpleHttpServer;
|
||||||
|
|
||||||
@@ -13,7 +14,8 @@ public sealed class HttpServer {
|
|||||||
|
|
||||||
private readonly HttpListener listener;
|
private readonly HttpListener listener;
|
||||||
private Task? listenerTask;
|
private Task? listenerTask;
|
||||||
private readonly Logger logger;
|
private readonly Logger mainLogger;
|
||||||
|
private readonly Logger requestLogger;
|
||||||
private readonly SimpleHttpServerConfiguration conf;
|
private readonly SimpleHttpServerConfiguration conf;
|
||||||
private bool shutdown = false;
|
private bool shutdown = false;
|
||||||
|
|
||||||
@@ -22,19 +24,20 @@ 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}/");
|
||||||
logger = new(LogOutputTopic.Main, conf);
|
mainLogger = new(LogOutputTopic.Main, conf);
|
||||||
|
requestLogger = new(LogOutputTopic.Request, conf);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Start() {
|
public void Start() {
|
||||||
logger.Information($"Starting on port {Port}...");
|
mainLogger.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);
|
||||||
logger.Information($"Ready to handle requests!");
|
mainLogger.Information($"Ready to handle requests!");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StopAsync(CancellationToken ctok) {
|
public async Task StopAsync(CancellationToken ctok) {
|
||||||
logger.Information("Stopping server...");
|
mainLogger.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();
|
||||||
@@ -46,8 +49,9 @@ 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) {
|
||||||
logger.Fatal($"Caught otherwise uncaught exception in GetContextLoop:\n{ex}");
|
mainLogger.Fatal($"Caught otherwise uncaught exception in GetContextLoop:\n{ex}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,6 +60,7 @@ 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>();
|
||||||
@@ -109,7 +114,7 @@ public sealed class HttpServer {
|
|||||||
var par = methodParams[i];
|
var par = methodParams[i];
|
||||||
var attr = par.GetCustomAttribute<ParameterAttribute>(false);
|
var attr = par.GetCustomAttribute<ParameterAttribute>(false);
|
||||||
qparams.Add((attr?.Name ?? par.Name ?? throw new ArgumentException($"C# variable name of parameter at index {i} of method {GetFancyMethodName()} is null!"),
|
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)));
|
(par.ParameterType, attr?.IsOptional ?? false)));
|
||||||
|
|
||||||
if (!stringToTypeParameterConverters.ContainsKey(par.ParameterType)) {
|
if (!stringToTypeParameterConverters.ContainsKey(par.ParameterType)) {
|
||||||
throw new MissingParameterConverterException($"Parameter converter for type {par.ParameterType} has not been registered (yet)!");
|
throw new MissingParameterConverterException($"Parameter converter for type {par.ParameterType} has not been registered (yet)!");
|
||||||
@@ -117,30 +122,84 @@ public sealed class HttpServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach (var location in attrib.Locations) {
|
foreach (var location in attrib.Locations) {
|
||||||
int idx = location.IndexOf('{');
|
var normLocation = NormalizeUrlPath(location);
|
||||||
|
int idx = normLocation.IndexOf('{');
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
// this path contains path parameters
|
// this path contains path parameters
|
||||||
throw new NotImplementedException("Path parameters are not yet implemented!");
|
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));
|
mainLogger.Information($"Registered endpoint: '{reqMethod} {normLocation}'");
|
||||||
|
simpleEndpointMethodInfos.Add((normLocation, reqMethod), new EndpointInvocationInfo(mi, qparams));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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 Dictionary<string, string>();
|
||||||
|
|
||||||
private readonly Dictionary<Type, IParameterConverter> stringToTypeParameterConverters = new();
|
private readonly Dictionary<Type, IParameterConverter> stringToTypeParameterConverters = new();
|
||||||
|
|
||||||
|
private static 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>());
|
||||||
|
|
||||||
|
return '/' + (rv.ToString().TrimEnd('/') + (fwdSlashUrl.EndsWith('/') ? "/" : "")).TrimStart('/');
|
||||||
|
}
|
||||||
|
|
||||||
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());
|
|
||||||
|
|
||||||
|
if (simpleEndpointMethodInfos.TryGetValue((reqPath, requestMethod), out var endpointInvocationInfo)) {
|
||||||
using var rc = new RequestContext(ctx);
|
|
||||||
if (simpleEndpointMethodInfos.TryGetValue((decUri, ctx.Request.HttpMethod.ToUpperInvariant()), out var endpointInvocationInfo)) {
|
|
||||||
var mi = endpointInvocationInfo.methodInfo;
|
var mi = endpointInvocationInfo.methodInfo;
|
||||||
var qparams = endpointInvocationInfo.queryParameters;
|
var qparams = endpointInvocationInfo.queryParameters;
|
||||||
var args = splitted.Length == 2 ? splitted[1] : null;
|
var args = splitted.Length == 2 ? splitted[1] : null;
|
||||||
@@ -155,11 +214,11 @@ public sealed class HttpServer {
|
|||||||
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) {
|
||||||
rc.SetStatusCodeAndDispose(HttpStatusCode.BadRequest, "Malformed request URL parameters");
|
await HandleDefaultErrorPageAsync(rc, 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]))) {
|
||||||
rc.SetStatusCodeAndDispose(HttpStatusCode.BadRequest, "Duplicate request URL parameters");
|
await HandleDefaultErrorPageAsync(rc, HttpStatusCode.BadRequest, "Duplicate request URL parameters");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,39 +231,82 @@ public sealed class HttpServer {
|
|||||||
if (stringToTypeParameterConverters[qparamInfo.type].TryConvertFromString(qparamValue, out object objRes)) {
|
if (stringToTypeParameterConverters[qparamInfo.type].TryConvertFromString(qparamValue, out object objRes)) {
|
||||||
convertedQParamValues[i] = objRes;
|
convertedQParamValues[i] = objRes;
|
||||||
} else {
|
} else {
|
||||||
rc.SetStatusCodeAndDispose(HttpStatusCode.BadRequest);
|
await HandleDefaultErrorPageAsync(rc, HttpStatusCode.BadRequest);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (qparamInfo.isOptional) {
|
if (qparamInfo.isOptional) {
|
||||||
convertedQParamValues[i] = null!;
|
convertedQParamValues[i] = null!;
|
||||||
} else {
|
} else {
|
||||||
rc.SetStatusCodeAndDispose(HttpStatusCode.BadRequest, $"Missing required query parameter {qparamName}");
|
await HandleDefaultErrorPageAsync(rc, HttpStatusCode.BadRequest, $"Missing required query parameter {qparamName}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
convertedQParamValues[0] = rc;
|
convertedQParamValues[0] = rc;
|
||||||
|
rc.ParsedParameters = parsedQParams.AsReadOnly();
|
||||||
|
|
||||||
await (Task) (mi.Invoke(null, convertedQParamValues) ?? throw new NullReferenceException("Website func returned null unexpectedly"));
|
await (Task) (mi.Invoke(null, convertedQParamValues) ?? throw new NullReferenceException("Website func returned null unexpectedly"));
|
||||||
} else {
|
} else {
|
||||||
|
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 {
|
||||||
|
await HandleDefaultErrorPageAsync(rc, HttpStatusCode.NotFound);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// invoke 404
|
// invoke 404
|
||||||
await HandleDefaultErrorPageAsync(rc, 404);
|
await HandleDefaultErrorPageAsync(rc, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
logger.Fatal($"Caught otherwise uncaught exception while ProcessingRequest:\n{ex}");
|
await HandleDefaultErrorPageAsync(rc, 500);
|
||||||
|
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) {
|
private static async Task HandleDefaultErrorPageAsync(RequestContext ctx, int errorCode, string? statusDescription = null) {
|
||||||
|
ctx.SetStatusCode(errorCode);
|
||||||
|
string desc = statusDescription != null ? $"\r\n{statusDescription}" : "";
|
||||||
await ctx.WriteLineToRespAsync($"""
|
await ctx.WriteLineToRespAsync($"""
|
||||||
<body>
|
<body>
|
||||||
<h1>Oh no, and error occurred!</h1>
|
<h1>Oh no, an error occurred!</h1>
|
||||||
<p>Code: {errorCode}</p>
|
<p>Code: {errorCode}</p>{desc}
|
||||||
</body>
|
</body>
|
||||||
""");
|
""");
|
||||||
|
try {
|
||||||
|
if (statusDescription == null) {
|
||||||
|
await ctx.SetStatusCodeAndDisposeAsync(errorCode);
|
||||||
|
} else {
|
||||||
|
await ctx.SetStatusCodeAndDisposeAsync(errorCode, statusDescription);
|
||||||
|
}
|
||||||
|
} catch (ObjectDisposedException) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,73 +1,81 @@
|
|||||||
using Konscious.Security.Cryptography;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
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 salt;
|
public string passwordSalt;
|
||||||
|
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 {
|
||||||
salt = Convert.FromBase64String(salt),
|
passwordSalt = Convert.FromBase64String(passwordSalt),
|
||||||
password = Convert.FromBase64String(pwd)
|
extraDataSalt = Convert.FromBase64String(extraDataSalt)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal struct LoginData {
|
internal struct LoginData {
|
||||||
public byte[] salt;
|
public byte[] passwordSalt;
|
||||||
public byte[] password;
|
public byte[] extraDataSalt;
|
||||||
public byte[] encryptedData;
|
public byte[] passwordHash;
|
||||||
|
public byte[] encryptedExtraData;
|
||||||
|
|
||||||
public SerialLoginData toSerial() {
|
public SerialLoginData ToSerial() {
|
||||||
return new SerialLoginData {
|
return new SerialLoginData {
|
||||||
salt = Convert.ToBase64String(salt),
|
passwordSalt = Convert.ToBase64String(passwordSalt),
|
||||||
pwd = Convert.ToBase64String(password),
|
extraDataSalt = Convert.ToBase64String(extraDataSalt),
|
||||||
additionalData = Convert.ToBase64String(encryptedData)
|
pwd = Convert.ToBase64String(passwordHash),
|
||||||
|
extraData = Convert.ToBase64String(encryptedExtraData)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal struct LoginDataProviderConfig {
|
internal struct LoginDataProviderConfig {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Size of the password salt and the extradata salt. So each salt will be of size <see cref="SALT_SIZE"/>.
|
||||||
|
/// </summary>
|
||||||
public int SALT_SIZE = 32;
|
public int SALT_SIZE = 32;
|
||||||
public int KEY_LENGTH = 256 / 8;
|
public int KEY_LENGTH = 256 / 8;
|
||||||
public int A2_ITERATIONS = 5;
|
|
||||||
public int A2_MEMORY_SIZE = 500_000;
|
|
||||||
public int A2_PARALLELISM = 8;
|
|
||||||
public int A2_HASH_LENGTH = 256 / 8;
|
|
||||||
public int A2_MAX_CONCURRENT = 4;
|
|
||||||
public int PBKDF2_ITERATIONS = 600_000;
|
public int PBKDF2_ITERATIONS = 600_000;
|
||||||
|
|
||||||
public LoginDataProviderConfig() { }
|
public LoginDataProviderConfig() { }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class LoginProvider<T> {
|
public class LoginProvider<TExtraData> {
|
||||||
|
|
||||||
private static readonly Func<T, byte[]> JsonSerialize = t => Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(t));
|
private static readonly Func<TExtraData, byte[]> JsonSerialize = t => Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(t));
|
||||||
private static readonly Func<byte[], T> JsonDeserialize = b => JsonConvert.DeserializeObject<T>(Encoding.UTF8.GetString(b))!;
|
private static readonly Func<byte[], TExtraData> JsonDeserialize = b => JsonConvert.DeserializeObject<TExtraData>(Encoding.UTF8.GetString(b))!;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static SHA256? _sha256PerThread;
|
||||||
|
private static SHA256 Sha256PerThread { get => _sha256PerThread ??= SHA256.Create(); }
|
||||||
|
|
||||||
private readonly LoginDataProviderConfig config;
|
private readonly LoginDataProviderConfig config;
|
||||||
private readonly ReaderWriterLockSlim ldLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
|
private readonly ReaderWriterLockSlim ldLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
|
||||||
private readonly string ldPath;
|
private readonly string ldPath;
|
||||||
private readonly Dictionary<string, LoginData> loginData;
|
private readonly Dictionary<string, LoginData> loginDatas;
|
||||||
private readonly SemaphoreSlim argon2Limit;
|
|
||||||
|
private Func<TExtraData, byte[]> DataSerializer = JsonSerialize;
|
||||||
|
private Func<byte[], TExtraData> DataDeserializer = JsonDeserialize;
|
||||||
|
public void SetDataSerializers(Func<TExtraData, byte[]> serializer, Func<byte[], TExtraData> deserializer) {
|
||||||
|
DataSerializer = serializer ?? JsonSerialize;
|
||||||
|
DataDeserializer = deserializer ?? JsonDeserialize;
|
||||||
|
}
|
||||||
|
|
||||||
private Func<T, byte[]> DataSerializer = JsonSerialize;
|
|
||||||
private Func<byte[], T> DataDeserializer = JsonDeserialize;
|
|
||||||
|
|
||||||
public LoginProvider(string ldPath, string confPath) {
|
public LoginProvider(string ldPath, string confPath) {
|
||||||
this.ldPath = ldPath;
|
this.ldPath = ldPath;
|
||||||
loginData = LoadLoginData(ldPath);
|
loginDatas = LoadLoginDatas(ldPath);
|
||||||
config = LoadArgon2Config(confPath);
|
config = LoadLoginProviderConfig(confPath);
|
||||||
argon2Limit = new SemaphoreSlim(config.A2_MAX_CONCURRENT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Dictionary<string, LoginData> LoadLoginData(string path) {
|
private static Dictionary<string, LoginData> LoadLoginDatas(string path) {
|
||||||
Dictionary<string, SerialLoginData> tempData;
|
Dictionary<string, SerialLoginData> tempData;
|
||||||
if (!File.Exists(path)) {
|
if (!File.Exists(path)) {
|
||||||
File.WriteAllText(path, "{}", Encoding.UTF8);
|
File.WriteAllText(path, "{}", Encoding.UTF8);
|
||||||
@@ -79,13 +87,26 @@ public class LoginProvider<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var ld = new Dictionary<string, LoginData>();
|
var ld = new Dictionary<string, LoginData>();
|
||||||
foreach (var pair in tempData!) {
|
foreach (var pair in tempData) {
|
||||||
ld.Add(pair.Key, pair.Value.toPlainData());
|
ld.Add(pair.Key, pair.Value.ToPlainData());
|
||||||
}
|
}
|
||||||
return ld;
|
return ld;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LoginDataProviderConfig LoadArgon2Config(string path) {
|
private void SaveLoginData() {
|
||||||
|
var serial = new Dictionary<string, SerialLoginData>();
|
||||||
|
ldLock.EnterWriteLock();
|
||||||
|
try {
|
||||||
|
foreach (var pair in loginDatas) {
|
||||||
|
serial.Add(pair.Key, pair.Value.ToSerial());
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
ldLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
File.WriteAllText(ldPath, JsonConvert.SerializeObject(serial));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LoginDataProviderConfig LoadLoginProviderConfig(string path) {
|
||||||
if (!File.Exists(path)) {
|
if (!File.Exists(path)) {
|
||||||
var conf = new LoginDataProviderConfig();
|
var conf = new LoginDataProviderConfig();
|
||||||
File.WriteAllText(path, JsonConvert.SerializeObject(conf));
|
File.WriteAllText(path, JsonConvert.SerializeObject(conf));
|
||||||
@@ -94,39 +115,22 @@ public class LoginProvider<T> {
|
|||||||
return JsonConvert.DeserializeObject<LoginDataProviderConfig>(File.ReadAllText(path));
|
return JsonConvert.DeserializeObject<LoginDataProviderConfig>(File.ReadAllText(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetDataSerialization(Func<T, byte[]> serializer, Func<byte[], T> deserializer) {
|
public bool AddUser(string username, string password, TExtraData additional) {
|
||||||
DataSerializer = serializer ?? JsonSerialize;
|
|
||||||
DataDeserializer = deserializer ?? JsonDeserialize;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StoreLoginData() {
|
|
||||||
var serial = new Dictionary<string, SerialLoginData>();
|
|
||||||
ldLock.EnterWriteLock();
|
ldLock.EnterWriteLock();
|
||||||
try {
|
try {
|
||||||
foreach (var pair in loginData!) {
|
if (loginDatas.ContainsKey(username)) {
|
||||||
serial.Add(pair.Key, pair.Value.toSerial());
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
ldLock.ExitWriteLock();
|
|
||||||
}
|
|
||||||
File.WriteAllText(ldPath, JsonConvert.SerializeObject(serial));
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool AddUser(string username, string password, T additional) {
|
|
||||||
ldLock.EnterWriteLock();
|
|
||||||
try {
|
|
||||||
if (loginData.ContainsKey(username)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
var salt = RandomNumberGenerator.GetBytes(config.SALT_SIZE);
|
var passwordSalt = RandomNumberGenerator.GetBytes(config.SALT_SIZE);
|
||||||
var pwdHash = HashPwd(password, salt);
|
var extraDataSalt = RandomNumberGenerator.GetBytes(config.SALT_SIZE);
|
||||||
LoginData ld = new LoginData() {
|
LoginData ld = new LoginData() {
|
||||||
salt = salt,
|
passwordSalt = passwordSalt,
|
||||||
password = pwdHash,
|
extraDataSalt = extraDataSalt,
|
||||||
encryptedData = EncryptAdditionalData(password, salt, additional)
|
passwordHash = ComputeSaltedSha256Hash(password, passwordSalt),
|
||||||
|
encryptedExtraData = EncryptExtraData(password, extraDataSalt, additional),
|
||||||
};
|
};
|
||||||
loginData.Add(username, ld);
|
loginDatas.Add(username, ld);
|
||||||
StoreLoginData();
|
SaveLoginData();
|
||||||
} finally {
|
} finally {
|
||||||
ldLock.ExitWriteLock();
|
ldLock.ExitWriteLock();
|
||||||
}
|
}
|
||||||
@@ -136,9 +140,9 @@ public class LoginProvider<T> {
|
|||||||
public bool RemoveUser(string username) {
|
public bool RemoveUser(string username) {
|
||||||
ldLock.EnterWriteLock();
|
ldLock.EnterWriteLock();
|
||||||
try {
|
try {
|
||||||
var removed = loginData.Remove(username);
|
var removed = loginDatas.Remove(username);
|
||||||
if (removed) {
|
if (removed) {
|
||||||
StoreLoginData();
|
SaveLoginData();
|
||||||
}
|
}
|
||||||
return removed;
|
return removed;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -146,64 +150,62 @@ public class LoginProvider<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ModifyUser(string username, string newPassword, T newAdditional) {
|
public bool ModifyUser(string username, string newPassword, TExtraData newExtraData) {
|
||||||
ldLock.EnterWriteLock();
|
ldLock.EnterWriteLock();
|
||||||
try {
|
try {
|
||||||
if (!loginData.ContainsKey(username)) {
|
if (!loginDatas.ContainsKey(username)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
loginData.Remove(username, out var data);
|
loginDatas.Remove(username, out var data);
|
||||||
data.password = HashPwd(newPassword, data.salt);
|
data.passwordHash = ComputeSaltedSha256Hash(newPassword, data.passwordSalt);
|
||||||
data.encryptedData = EncryptAdditionalData(newPassword, data.salt, newAdditional);
|
data.encryptedExtraData = EncryptExtraData(newPassword, data.extraDataSalt, newExtraData);
|
||||||
loginData.Add(username, data);
|
loginDatas.Add(username, data);
|
||||||
StoreLoginData();
|
SaveLoginData();
|
||||||
} finally {
|
} finally {
|
||||||
ldLock.ExitWriteLock();
|
ldLock.ExitWriteLock();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public (bool, T) Authenticate(string username, string password) {
|
public bool TryAuthenticate(string username, string password, [MaybeNullWhen(false)] out TExtraData extraData) {
|
||||||
LoginData data;
|
LoginData data;
|
||||||
ldLock.EnterReadLock();
|
ldLock.EnterReadLock();
|
||||||
try {
|
try {
|
||||||
if (!loginData.TryGetValue(username, out data)) {
|
if (!loginDatas.TryGetValue(username, out data)) {
|
||||||
return (false, default(T)!);
|
extraData = default;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
ldLock.ExitReadLock();
|
ldLock.ExitReadLock();
|
||||||
}
|
}
|
||||||
var hash = HashPwd(password, data.salt);
|
var hash = ComputeSaltedSha256Hash(password, data.passwordSalt);
|
||||||
if (!hash.SequenceEqual(data.password)) {
|
if (!hash.SequenceEqual(data.passwordHash)) {
|
||||||
return (false, default(T)!);
|
extraData = default;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return (true, DecryptAdditionalData(password, data.salt, data.encryptedData));
|
extraData = DecryptExtraData(password, data.extraDataSalt, data.encryptedExtraData);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] HashPwd(string pwd, byte[] salt) {
|
/// <summary>
|
||||||
byte[] hash;
|
/// Threadsafe as the SHA256 instance (<see cref="Sha256PerThread"/>) is per thread.
|
||||||
argon2Limit.Wait();
|
/// </summary>
|
||||||
try {
|
/// <param name="data"></param>
|
||||||
using (var argon2 = new Argon2id(Encoding.UTF8.GetBytes(pwd))) {
|
/// <param name="salt"></param>
|
||||||
argon2.Iterations = config.A2_ITERATIONS;
|
/// <returns></returns>
|
||||||
argon2.MemorySize = config.A2_MEMORY_SIZE;
|
private static byte[] ComputeSaltedSha256Hash(string data, byte[] salt) {
|
||||||
argon2.DegreeOfParallelism = config.A2_PARALLELISM;
|
var dataBytes = Encoding.UTF8.GetBytes(data);
|
||||||
argon2.Salt = salt;
|
var buf = new byte[data.Length + salt.Length];
|
||||||
hash = argon2.GetBytes(config.A2_HASH_LENGTH);
|
Buffer.BlockCopy(dataBytes, 0, buf, 0, dataBytes.Length);
|
||||||
}
|
Buffer.BlockCopy(salt, 0, buf, dataBytes.Length, salt.Length);
|
||||||
// force collection to reduce sustained memory usage if many hashes are done in close time proximity to each other
|
return Sha256PerThread.ComputeHash(buf);
|
||||||
GC.Collect();
|
|
||||||
} finally {
|
|
||||||
argon2Limit.Release();
|
|
||||||
}
|
|
||||||
return hash;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] EncryptAdditionalData(string pwd, byte[] salt, T data) {
|
private byte[] EncryptExtraData(string pwd, byte[] salt, TExtraData extraData) {
|
||||||
var pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(pwd), salt, config.PBKDF2_ITERATIONS, HashAlgorithmName.SHA256);
|
var pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(pwd), salt, config.PBKDF2_ITERATIONS, HashAlgorithmName.SHA256);
|
||||||
var key = pbkdf2.GetBytes(config.KEY_LENGTH / 8);
|
var key = pbkdf2.GetBytes(config.KEY_LENGTH / 8);
|
||||||
|
|
||||||
var plainBytes = DataSerializer(data);
|
var plainBytes = DataSerializer(extraData);
|
||||||
using var aes = Aes.Create();
|
using var aes = Aes.Create();
|
||||||
aes.KeySize = config.KEY_LENGTH;
|
aes.KeySize = config.KEY_LENGTH;
|
||||||
aes.Key = key;
|
aes.Key = key;
|
||||||
@@ -219,7 +221,7 @@ public class LoginProvider<T> {
|
|||||||
return encryptedBytes;
|
return encryptedBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private T DecryptAdditionalData(string pwd, byte[] salt, byte[] encryptedData) {
|
private TExtraData DecryptExtraData(string pwd, byte[] salt, byte[] encryptedData) {
|
||||||
var pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(pwd), salt, config.PBKDF2_ITERATIONS, HashAlgorithmName.SHA256);
|
var pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(pwd), salt, config.PBKDF2_ITERATIONS, HashAlgorithmName.SHA256);
|
||||||
var key = pbkdf2.GetBytes(config.KEY_LENGTH / 8);
|
var key = pbkdf2.GetBytes(config.KEY_LENGTH / 8);
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
using System.Net;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
namespace SimpleHttpServer;
|
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 StreamReader? reqReader;
|
private TextReader? reqReader;
|
||||||
public StreamReader ReqReader => reqReader ??= new(ListenerContext.Request.InputStream);
|
/// <summary>
|
||||||
|
/// THREADSAFE
|
||||||
|
/// </summary>
|
||||||
|
public TextReader ReqReader => reqReader ??= TextReader.Synchronized(new StreamReader(ListenerContext.Request.InputStream));
|
||||||
|
|
||||||
private StreamWriter? respWriter;
|
private TextWriter? respWriter;
|
||||||
public StreamWriter RespWriter => respWriter ??= new(ListenerContext.Response.OutputStream) { NewLine = "\n" };
|
/// <summary>
|
||||||
|
/// THREADSAFE
|
||||||
|
/// </summary>
|
||||||
|
public TextWriter RespWriter => respWriter ??= TextWriter.Synchronized(new StreamWriter(ListenerContext.Response.OutputStream) { NewLine = "\n" });
|
||||||
|
|
||||||
public RequestContext(HttpListenerContext listenerContext) {
|
public RequestContext(HttpListenerContext listenerContext) {
|
||||||
ListenerContext = listenerContext;
|
ListenerContext = listenerContext;
|
||||||
@@ -25,27 +33,40 @@ public class RequestContext : IDisposable {
|
|||||||
|
|
||||||
public void SetStatusCode(HttpStatusCode status) => SetStatusCode((int) status);
|
public void SetStatusCode(HttpStatusCode status) => SetStatusCode((int) status);
|
||||||
|
|
||||||
public void SetStatusCodeAndDispose(int status) {
|
public async Task SetStatusCodeAndDisposeAsync(int status) {
|
||||||
using (this)
|
using (this) {
|
||||||
SetStatusCode(status);
|
SetStatusCode(status);
|
||||||
|
await WriteToRespAsync("\n\n");
|
||||||
|
await RespWriter.FlushAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetStatusCodeAndDispose(HttpStatusCode status) {
|
public async Task SetStatusCodeAndDisposeAsync(HttpStatusCode status) {
|
||||||
using (this)
|
using (this) {
|
||||||
SetStatusCode((int) status);
|
SetStatusCode((int) status);
|
||||||
|
await WriteToRespAsync("\n\n");
|
||||||
|
await RespWriter.FlushAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void SetStatusCodeAndDispose(int status, string description) {
|
public async Task SetStatusCodeAndDisposeAsync(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 void SetStatusCodeAndDispose(HttpStatusCode status, string description) => SetStatusCodeAndDispose((int) status, description);
|
public async Task SetStatusCodeAndDisposeAsync(HttpStatusCode status, string description) => await SetStatusCodeAndDisposeAsync((int) status, description);
|
||||||
|
|
||||||
|
|
||||||
void IDisposable.Dispose() {
|
public async Task WriteRedirect302AndDisposeAsync(string url) {
|
||||||
|
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,7 +7,6 @@
|
|||||||
</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>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace SimpleHttpServer;
|
namespace SimpleHttpServer.Types;
|
||||||
|
|
||||||
public enum HttpRequestType {
|
public enum HttpRequestType {
|
||||||
GET,
|
GET,
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace SimpleHttpServer.Types.ParameterConverters;
|
||||||
|
internal class StringParameterConverter : IParameterConverter {
|
||||||
|
public bool TryConvertFromString(string value, out object result) {
|
||||||
|
result = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
using SimpleHttpServer;
|
using SimpleHttpServer;
|
||||||
|
using SimpleHttpServer.Types;
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
namespace SimpleHttpServerTest;
|
namespace SimpleHttpServerTest;
|
||||||
|
|
||||||
@@ -8,19 +10,36 @@ 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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,20 +52,87 @@ 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() {
|
||||||
using var hc = new HttpClient();
|
var resp = await AssertGetStatusCodeAsync("/", HttpStatusCode.OK);
|
||||||
await hc.GetStringAsync(GetRequestPath("/"));
|
var str = await resp.Content.ReadAsStringAsync();
|
||||||
|
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.WriteLineAsync("It works!");
|
await req.RespWriter.WriteAsync("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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user