Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e4570a560 | ||
|
|
30daf382ba | ||
|
|
2cf6cd4a7d | ||
|
|
29eecc7887 | ||
|
|
a4ae359df0 | ||
|
|
176c5e7197 | ||
|
|
d7a934e25c | ||
|
|
c75d29a1ba | ||
|
|
fa79134d02 |
@@ -1,23 +1,15 @@
|
|||||||
using SimpleHttpServer.Internal;
|
using SimpleHttpServer.Types;
|
||||||
using SimpleHttpServer.Types;
|
|
||||||
|
|
||||||
namespace SimpleHttpServer;
|
namespace SimpleHttpServer;
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||||
public class HttpEndpointAttribute<T> : Attribute where T : IAuthorizer {
|
public class HttpEndpointAttribute : Attribute {
|
||||||
|
|
||||||
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) { }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using System.Net;
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using static SimpleHttpServer.Types.EndpointInvocationInfo;
|
||||||
|
|
||||||
namespace SimpleHttpServer;
|
namespace SimpleHttpServer;
|
||||||
|
|
||||||
@@ -85,18 +86,24 @@ public sealed class HttpServer {
|
|||||||
|
|
||||||
private readonly Dictionary<(string path, string rType), EndpointInvocationInfo> simpleEndpointMethodInfos = new();
|
private readonly Dictionary<(string path, string rType), EndpointInvocationInfo> simpleEndpointMethodInfos = new();
|
||||||
private static readonly Type[] expectedEndpointParameterTypes = new[] { typeof(RequestContext) };
|
private static readonly Type[] expectedEndpointParameterTypes = new[] { typeof(RequestContext) };
|
||||||
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);
|
||||||
foreach (var (mi, attrib) in t.GetMethods()
|
var mis = t.GetMethods()
|
||||||
.ToDictionary(x => x, x => x.GetCustomAttributes(typeof(HttpEndpointAttribute<>)))
|
.ToDictionary(x => x, x => x.GetCustomAttributes<HttpEndpointAttribute>())
|
||||||
.Where(x => x.Value.Any()).ToDictionary(x => x.Key, x => (HttpEndpointAttribute) x.Value.Single())) {
|
.Where(x => x.Value.Any()).ToDictionary(x => x.Key, x => 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();
|
||||||
@@ -109,18 +116,29 @@ public sealed class HttpServer {
|
|||||||
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)}!");
|
||||||
|
|
||||||
|
|
||||||
var qparams = new List<(string, (Type type, bool isOptional))>();
|
var qparams = new List<QueryParameterInfo>();
|
||||||
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);
|
||||||
qparams.Add((attr?.Name ?? par.Name ?? throw new ArgumentException($"C# variable name of parameter at index {i} of method {GetFancyMethodName()} is null!"),
|
qparams.Add(new(
|
||||||
(par.ParameterType, attr?.IsOptional ?? false)));
|
attr?.Name ?? par.Name ?? throw new ArgumentException($"C# variable name of parameter at index {i} of method {GetFancyMethodName()} is null!"),
|
||||||
|
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)!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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);
|
var normLocation = NormalizeUrlPath(location);
|
||||||
int idx = normLocation.IndexOf('{');
|
int idx = normLocation.IndexOf('{');
|
||||||
@@ -131,7 +149,7 @@ public sealed class HttpServer {
|
|||||||
|
|
||||||
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");
|
||||||
mainLogger.Information($"Registered endpoint: '{reqMethod} {normLocation}'");
|
mainLogger.Information($"Registered endpoint: '{reqMethod} {normLocation}'");
|
||||||
simpleEndpointMethodInfos.Add((normLocation, reqMethod), new EndpointInvocationInfo(mi, qparams));
|
simpleEndpointMethodInfos.Add((normLocation, reqMethod), new EndpointInvocationInfo(mi, qparams, requiredChecks, classInstance));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,7 +167,7 @@ public sealed class HttpServer {
|
|||||||
staticServePaths.Add(npath, absPath);
|
staticServePaths.Add(npath, absPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly Dictionary<string, string> staticServePaths = new Dictionary<string, string>();
|
private readonly Dictionary<string, string> staticServePaths = new();
|
||||||
|
|
||||||
private readonly Dictionary<Type, IParameterConverter> stringToTypeParameterConverters = new();
|
private readonly Dictionary<Type, IParameterConverter> stringToTypeParameterConverters = new();
|
||||||
|
|
||||||
@@ -207,7 +225,11 @@ public sealed class HttpServer {
|
|||||||
var parsedQParams = new Dictionary<string, string>();
|
var parsedQParams = new Dictionary<string, string>();
|
||||||
var convertedQParamValues = new object[qparams.Count + 1];
|
var convertedQParamValues = new object[qparams.Count + 1];
|
||||||
|
|
||||||
// TODO add authcheck here
|
// run the checks to see if the client is allowed to make this request
|
||||||
|
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);
|
||||||
@@ -224,30 +246,36 @@ public sealed class HttpServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < qparams.Count;) {
|
for (int i = 0; i < qparams.Count;) {
|
||||||
var (qparamName, qparamInfo) = qparams[i];
|
var qparam = qparams[i];
|
||||||
i++;
|
i++;
|
||||||
|
|
||||||
if (parsedQParams.TryGetValue(qparamName, out var qparamValue)) {
|
if (parsedQParams.TryGetValue(qparam.Name, out var qparamValue)) {
|
||||||
if (stringToTypeParameterConverters[qparamInfo.type].TryConvertFromString(qparamValue, out object objRes)) {
|
if (stringToTypeParameterConverters[qparam.Type].TryConvertFromString(qparamValue, out object objRes)) {
|
||||||
convertedQParamValues[i] = objRes;
|
convertedQParamValues[i] = objRes;
|
||||||
} else {
|
} else {
|
||||||
await HandleDefaultErrorPageAsync(rc, HttpStatusCode.BadRequest);
|
await HandleDefaultErrorPageAsync(rc, HttpStatusCode.BadRequest);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (qparamInfo.isOptional) {
|
if (qparam.IsOptional) {
|
||||||
convertedQParamValues[i] = null!;
|
convertedQParamValues[i] = null!;
|
||||||
} else {
|
} else {
|
||||||
await HandleDefaultErrorPageAsync(rc, HttpStatusCode.BadRequest, $"Missing required query parameter {qparamName}");
|
await HandleDefaultErrorPageAsync(rc, HttpStatusCode.BadRequest, $"Missing required query parameter {qparam.Name}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
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;
|
convertedQParamValues[0] = rc;
|
||||||
rc.ParsedParameters = parsedQParams.AsReadOnly();
|
rc.ParsedParameters = parsedQParams.AsReadOnly();
|
||||||
|
|
||||||
await (Task) (mi.Invoke(null, convertedQParamValues) ?? throw new NullReferenceException("Website func returned null unexpectedly"));
|
await (Task) (mi.Invoke(endpointInvocationInfo.typeInstanceReference, convertedQParamValues) ?? throw new NullReferenceException("Website func returned null unexpectedly"));
|
||||||
} else {
|
} else {
|
||||||
if (requestMethod == "GET")
|
if (requestMethod == "GET")
|
||||||
foreach (var (k, v) in staticServePaths) {
|
foreach (var (k, v) in staticServePaths) {
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace SimpleHttpServer;
|
|
||||||
|
|
||||||
public interface IAuthorizer {
|
|
||||||
public abstract (bool auth, object? data) IsAuthenticated(HttpListenerContext contect);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
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,245 @@
|
|||||||
using Newtonsoft.Json;
|
//using Newtonsoft.Json;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
//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 passwordSalt;
|
// public string passwordSalt;
|
||||||
public string extraDataSalt;
|
// public string extraDataSalt;
|
||||||
public string pwd;
|
// public string pwd;
|
||||||
public string extraData;
|
// public string extraData;
|
||||||
|
|
||||||
public LoginData ToPlainData() {
|
// public LoginData ToPlainData() {
|
||||||
return new LoginData {
|
// return new LoginData {
|
||||||
passwordSalt = Convert.FromBase64String(passwordSalt),
|
// passwordSalt = Convert.FromBase64String(passwordSalt),
|
||||||
extraDataSalt = Convert.FromBase64String(extraDataSalt)
|
// extraDataSalt = Convert.FromBase64String(extraDataSalt)
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
||||||
internal struct LoginData {
|
//internal struct LoginData {
|
||||||
public byte[] passwordSalt;
|
// public byte[] passwordSalt;
|
||||||
public byte[] extraDataSalt;
|
// public byte[] extraDataSalt;
|
||||||
public byte[] passwordHash;
|
// public byte[] passwordHash;
|
||||||
public byte[] encryptedExtraData;
|
// public byte[] encryptedExtraData;
|
||||||
|
|
||||||
public SerialLoginData ToSerial() {
|
// public SerialLoginData ToSerial() {
|
||||||
return new SerialLoginData {
|
// return new SerialLoginData {
|
||||||
passwordSalt = Convert.ToBase64String(passwordSalt),
|
// passwordSalt = Convert.ToBase64String(passwordSalt),
|
||||||
extraDataSalt = Convert.ToBase64String(extraDataSalt),
|
// extraDataSalt = Convert.ToBase64String(extraDataSalt),
|
||||||
pwd = Convert.ToBase64String(passwordHash),
|
// pwd = Convert.ToBase64String(passwordHash),
|
||||||
extraData = Convert.ToBase64String(encryptedExtraData)
|
// extraData = Convert.ToBase64String(encryptedExtraData)
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
||||||
internal struct LoginDataProviderConfig {
|
//internal struct LoginDataProviderConfig {
|
||||||
|
|
||||||
/// <summary>
|
// /// <summary>
|
||||||
/// Size of the password salt and the extradata salt. So each salt will be of size <see cref="SALT_SIZE"/>.
|
// /// Size of the password salt and the extradata salt. So each salt will be of size <see cref="SALT_SIZE"/>.
|
||||||
/// </summary>
|
// /// </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 PBKDF2_ITERATIONS = 600_000;
|
// public int PBKDF2_ITERATIONS = 600_000;
|
||||||
|
|
||||||
public LoginDataProviderConfig() { }
|
// public LoginDataProviderConfig() { }
|
||||||
}
|
//}
|
||||||
|
|
||||||
public class LoginProvider<TExtraData> {
|
//public class LoginProvider<TExtraData> {
|
||||||
|
|
||||||
private static readonly Func<TExtraData, 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[], TExtraData> JsonDeserialize = b => JsonConvert.DeserializeObject<TExtraData>(Encoding.UTF8.GetString(b))!;
|
// private static readonly Func<byte[], TExtraData> JsonDeserialize = b => JsonConvert.DeserializeObject<TExtraData>(Encoding.UTF8.GetString(b))!;
|
||||||
|
|
||||||
[ThreadStatic]
|
// [ThreadStatic]
|
||||||
private static SHA256? _sha256PerThread;
|
// private static SHA256? _sha256PerThread;
|
||||||
private static SHA256 Sha256PerThread { get => _sha256PerThread ??= SHA256.Create(); }
|
// 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> loginDatas;
|
// private readonly Dictionary<string, LoginData> loginDatas;
|
||||||
|
|
||||||
private Func<TExtraData, byte[]> DataSerializer = JsonSerialize;
|
// private Func<TExtraData, byte[]> DataSerializer = JsonSerialize;
|
||||||
private Func<byte[], TExtraData> DataDeserializer = JsonDeserialize;
|
// private Func<byte[], TExtraData> DataDeserializer = JsonDeserialize;
|
||||||
public void SetDataSerializers(Func<TExtraData, byte[]> serializer, Func<byte[], TExtraData> deserializer) {
|
// public void SetDataSerializers(Func<TExtraData, byte[]> serializer, Func<byte[], TExtraData> deserializer) {
|
||||||
DataSerializer = serializer ?? JsonSerialize;
|
// DataSerializer = serializer ?? JsonSerialize;
|
||||||
DataDeserializer = deserializer ?? JsonDeserialize;
|
// DataDeserializer = deserializer ?? JsonDeserialize;
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|
||||||
public LoginProvider(string ldPath, string confPath) {
|
// public LoginProvider(string ldPath, string confPath) {
|
||||||
this.ldPath = ldPath;
|
// this.ldPath = ldPath;
|
||||||
loginDatas = LoadLoginDatas(ldPath);
|
// loginDatas = LoadLoginDatas(ldPath);
|
||||||
config = LoadLoginProviderConfig(confPath);
|
// config = LoadLoginProviderConfig(confPath);
|
||||||
}
|
// }
|
||||||
|
|
||||||
private static Dictionary<string, LoginData> LoadLoginDatas(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);
|
||||||
tempData = new();
|
// tempData = new();
|
||||||
} else {
|
// } else {
|
||||||
tempData = JsonConvert.DeserializeObject<Dictionary<string, SerialLoginData>>(File.ReadAllText(path))!;
|
// tempData = JsonConvert.DeserializeObject<Dictionary<string, SerialLoginData>>(File.ReadAllText(path))!;
|
||||||
if (tempData == null) {
|
// if (tempData == null) {
|
||||||
throw new InvalidDataException($"could not read login data from file {path}");
|
// throw new InvalidDataException($"could not read login data from file {path}");
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
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 void SaveLoginData() {
|
// private void SaveLoginData() {
|
||||||
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 loginDatas) {
|
||||||
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) {
|
// 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));
|
||||||
return conf;
|
// return conf;
|
||||||
}
|
// }
|
||||||
return JsonConvert.DeserializeObject<LoginDataProviderConfig>(File.ReadAllText(path));
|
// return JsonConvert.DeserializeObject<LoginDataProviderConfig>(File.ReadAllText(path));
|
||||||
}
|
// }
|
||||||
|
|
||||||
public bool AddUser(string username, string password, TExtraData additional) {
|
// public bool AddUser(string username, string password, TExtraData additional) {
|
||||||
ldLock.EnterWriteLock();
|
// ldLock.EnterWriteLock();
|
||||||
try {
|
// try {
|
||||||
if (loginDatas.ContainsKey(username)) {
|
// if (loginDatas.ContainsKey(username)) {
|
||||||
return false;
|
// return false;
|
||||||
}
|
// }
|
||||||
var passwordSalt = RandomNumberGenerator.GetBytes(config.SALT_SIZE);
|
// var passwordSalt = RandomNumberGenerator.GetBytes(config.SALT_SIZE);
|
||||||
var extraDataSalt = RandomNumberGenerator.GetBytes(config.SALT_SIZE);
|
// var extraDataSalt = RandomNumberGenerator.GetBytes(config.SALT_SIZE);
|
||||||
LoginData ld = new LoginData() {
|
// LoginData ld = new LoginData() {
|
||||||
passwordSalt = passwordSalt,
|
// passwordSalt = passwordSalt,
|
||||||
extraDataSalt = extraDataSalt,
|
// extraDataSalt = extraDataSalt,
|
||||||
passwordHash = ComputeSaltedSha256Hash(password, passwordSalt),
|
// passwordHash = ComputeSaltedSha256Hash(password, passwordSalt),
|
||||||
encryptedExtraData = EncryptExtraData(password, extraDataSalt, additional),
|
// encryptedExtraData = EncryptExtraData(password, extraDataSalt, additional),
|
||||||
};
|
// };
|
||||||
loginDatas.Add(username, ld);
|
// loginDatas.Add(username, ld);
|
||||||
SaveLoginData();
|
// SaveLoginData();
|
||||||
} finally {
|
// } finally {
|
||||||
ldLock.ExitWriteLock();
|
// ldLock.ExitWriteLock();
|
||||||
}
|
// }
|
||||||
return true;
|
// return true;
|
||||||
}
|
// }
|
||||||
|
|
||||||
public bool RemoveUser(string username) {
|
// public bool RemoveUser(string username) {
|
||||||
ldLock.EnterWriteLock();
|
// ldLock.EnterWriteLock();
|
||||||
try {
|
// try {
|
||||||
var removed = loginDatas.Remove(username);
|
// var removed = loginDatas.Remove(username);
|
||||||
if (removed) {
|
// if (removed) {
|
||||||
SaveLoginData();
|
// SaveLoginData();
|
||||||
}
|
// }
|
||||||
return removed;
|
// return removed;
|
||||||
} finally {
|
// } finally {
|
||||||
ldLock.ExitWriteLock();
|
// ldLock.ExitWriteLock();
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
public bool ModifyUser(string username, string newPassword, TExtraData newExtraData) {
|
// public bool ModifyUser(string username, string newPassword, TExtraData newExtraData) {
|
||||||
ldLock.EnterWriteLock();
|
// ldLock.EnterWriteLock();
|
||||||
try {
|
// try {
|
||||||
if (!loginDatas.ContainsKey(username)) {
|
// if (!loginDatas.ContainsKey(username)) {
|
||||||
return false;
|
// return false;
|
||||||
}
|
// }
|
||||||
loginDatas.Remove(username, out var data);
|
// loginDatas.Remove(username, out var data);
|
||||||
data.passwordHash = ComputeSaltedSha256Hash(newPassword, data.passwordSalt);
|
// data.passwordHash = ComputeSaltedSha256Hash(newPassword, data.passwordSalt);
|
||||||
data.encryptedExtraData = EncryptExtraData(newPassword, data.extraDataSalt, newExtraData);
|
// data.encryptedExtraData = EncryptExtraData(newPassword, data.extraDataSalt, newExtraData);
|
||||||
loginDatas.Add(username, data);
|
// loginDatas.Add(username, data);
|
||||||
SaveLoginData();
|
// SaveLoginData();
|
||||||
} finally {
|
// } finally {
|
||||||
ldLock.ExitWriteLock();
|
// ldLock.ExitWriteLock();
|
||||||
}
|
// }
|
||||||
return true;
|
// return true;
|
||||||
}
|
// }
|
||||||
|
|
||||||
public bool TryAuthenticate(string username, string password, [MaybeNullWhen(false)] out TExtraData extraData) {
|
// public bool TryAuthenticate(string username, string password, [MaybeNullWhen(false)] out TExtraData extraData) {
|
||||||
LoginData data;
|
// LoginData data;
|
||||||
ldLock.EnterReadLock();
|
// ldLock.EnterReadLock();
|
||||||
try {
|
// try {
|
||||||
if (!loginDatas.TryGetValue(username, out data)) {
|
// if (!loginDatas.TryGetValue(username, out data)) {
|
||||||
extraData = default;
|
// extraData = default;
|
||||||
return false;
|
// return false;
|
||||||
}
|
// }
|
||||||
} finally {
|
// } finally {
|
||||||
ldLock.ExitReadLock();
|
// ldLock.ExitReadLock();
|
||||||
}
|
// }
|
||||||
var hash = ComputeSaltedSha256Hash(password, data.passwordSalt);
|
// var hash = ComputeSaltedSha256Hash(password, data.passwordSalt);
|
||||||
if (!hash.SequenceEqual(data.passwordHash)) {
|
// if (!hash.SequenceEqual(data.passwordHash)) {
|
||||||
extraData = default;
|
// extraData = default;
|
||||||
return false;
|
// return false;
|
||||||
}
|
// }
|
||||||
extraData = DecryptExtraData(password, data.extraDataSalt, data.encryptedExtraData);
|
// extraData = DecryptExtraData(password, data.extraDataSalt, data.encryptedExtraData);
|
||||||
return true;
|
// return true;
|
||||||
}
|
// }
|
||||||
|
|
||||||
/// <summary>
|
// /// <summary>
|
||||||
/// Threadsafe as the SHA256 instance (<see cref="Sha256PerThread"/>) is per thread.
|
// /// Threadsafe as the SHA256 instance (<see cref="Sha256PerThread"/>) is per thread.
|
||||||
/// </summary>
|
// /// </summary>
|
||||||
/// <param name="data"></param>
|
// /// <param name="data"></param>
|
||||||
/// <param name="salt"></param>
|
// /// <param name="salt"></param>
|
||||||
/// <returns></returns>
|
// /// <returns></returns>
|
||||||
private static byte[] ComputeSaltedSha256Hash(string data, byte[] salt) {
|
// private static byte[] ComputeSaltedSha256Hash(string data, byte[] salt) {
|
||||||
var dataBytes = Encoding.UTF8.GetBytes(data);
|
// var dataBytes = Encoding.UTF8.GetBytes(data);
|
||||||
var buf = new byte[data.Length + salt.Length];
|
// var buf = new byte[data.Length + salt.Length];
|
||||||
Buffer.BlockCopy(dataBytes, 0, buf, 0, dataBytes.Length);
|
// Buffer.BlockCopy(dataBytes, 0, buf, 0, dataBytes.Length);
|
||||||
Buffer.BlockCopy(salt, 0, buf, dataBytes.Length, salt.Length);
|
// Buffer.BlockCopy(salt, 0, buf, dataBytes.Length, salt.Length);
|
||||||
return Sha256PerThread.ComputeHash(buf);
|
// return Sha256PerThread.ComputeHash(buf);
|
||||||
}
|
// }
|
||||||
|
|
||||||
private byte[] EncryptExtraData(string pwd, byte[] salt, TExtraData extraData) {
|
// 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(extraData);
|
// 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;
|
||||||
aes.Mode = CipherMode.CBC;
|
// aes.Mode = CipherMode.CBC;
|
||||||
aes.Padding = PaddingMode.PKCS7;
|
// aes.Padding = PaddingMode.PKCS7;
|
||||||
ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
|
// ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
|
||||||
byte[] cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
|
// byte[] cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
|
||||||
|
|
||||||
var encryptedBytes = new byte[aes.IV.Length + cipherBytes.Length];
|
// var encryptedBytes = new byte[aes.IV.Length + cipherBytes.Length];
|
||||||
Array.Copy(aes.IV, 0, encryptedBytes, 0, aes.IV.Length);
|
// Array.Copy(aes.IV, 0, encryptedBytes, 0, aes.IV.Length);
|
||||||
Array.Copy(cipherBytes, 0, encryptedBytes, aes.IV.Length, cipherBytes.Length);
|
// Array.Copy(cipherBytes, 0, encryptedBytes, aes.IV.Length, cipherBytes.Length);
|
||||||
|
|
||||||
return encryptedBytes;
|
// return encryptedBytes;
|
||||||
}
|
// }
|
||||||
|
|
||||||
private TExtraData DecryptExtraData(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);
|
||||||
|
|
||||||
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;
|
||||||
aes.Mode = CipherMode.CBC;
|
// aes.Mode = CipherMode.CBC;
|
||||||
aes.Padding = PaddingMode.PKCS7;
|
// aes.Padding = PaddingMode.PKCS7;
|
||||||
var iv = new byte[aes.BlockSize / 8];
|
// var iv = new byte[aes.BlockSize / 8];
|
||||||
var cipherBytes = new byte[encryptedData.Length - iv.Length];
|
// var cipherBytes = new byte[encryptedData.Length - iv.Length];
|
||||||
|
|
||||||
Array.Copy(encryptedData, 0, iv, 0, iv.Length);
|
// Array.Copy(encryptedData, 0, iv, 0, iv.Length);
|
||||||
Array.Copy(encryptedData, iv.Length, cipherBytes, 0, cipherBytes.Length);
|
// Array.Copy(encryptedData, iv.Length, cipherBytes, 0, cipherBytes.Length);
|
||||||
|
|
||||||
aes.IV = iv;
|
// aes.IV = iv;
|
||||||
ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
|
// ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
|
||||||
byte[] plainBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
|
// byte[] plainBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
|
||||||
|
|
||||||
return DataDeserializer(plainBytes);
|
// return DataDeserializer(plainBytes);
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
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,12 +1,24 @@
|
|||||||
using System.Reflection;
|
using System.Net;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
namespace SimpleHttpServer.Types;
|
namespace SimpleHttpServer.Types;
|
||||||
internal struct EndpointInvocationInfo {
|
internal readonly struct EndpointInvocationInfo {
|
||||||
internal readonly MethodInfo methodInfo;
|
internal record struct QueryParameterInfo(string Name, Type Type, bool IsOptional);
|
||||||
internal readonly List<(string, (Type type, bool isOptional))> queryParameters;
|
|
||||||
|
|
||||||
public EndpointInvocationInfo(MethodInfo methodInfo, List<(string, (Type type, bool isOptional))> queryParameters) {
|
internal readonly MethodInfo methodInfo;
|
||||||
|
internal readonly List<QueryParameterInfo> queryParameters;
|
||||||
|
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<QueryParameterInfo> queryParameters, InternalEndpointCheckAttribute[] requiredChecks, object? typeInstanceReference) {
|
||||||
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.requiredChecks = requiredChecks;
|
||||||
|
this.typeInstanceReference = typeInstanceReference;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public readonly bool CheckAll(HttpListenerRequest req) => requiredChecks.All(x => x.Check(req));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
namespace SimpleHttpServer;
|
namespace SimpleHttpServer.Types;
|
||||||
public class RequestContext : IDisposable {
|
public class RequestContext : IDisposable {
|
||||||
|
|
||||||
public HttpListenerContext ListenerContext { get; }
|
public HttpListenerContext ListenerContext { get; }
|
||||||
@@ -19,9 +19,11 @@ public class RequestContext : IDisposable {
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TextWriter RespWriter => respWriter ??= TextWriter.Synchronized(new StreamWriter(ListenerContext.Response.OutputStream) { NewLine = "\n" });
|
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);
|
||||||
Reference in New Issue
Block a user