initial commit

This commit is contained in:
00asdf 2023-11-30 17:31:19 +01:00
commit 05b722e513
15 changed files with 360 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.vs/
*/obj/
*/bin/

31
SimpleHttpServer.sln Normal file
View File

@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33213.308
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleHttpServer", "SimpleHttpServer\SimpleHttpServer.csproj", "{36394C2C-AA78-4009-A0EB-13DD78556F56}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleHttpServerTest", "SimpleHttpServerTest\SimpleHttpServerTest.csproj", "{B5D81497-B28F-4EE6-9988-B6F4CA82FFE3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{36394C2C-AA78-4009-A0EB-13DD78556F56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{36394C2C-AA78-4009-A0EB-13DD78556F56}.Debug|Any CPU.Build.0 = Debug|Any CPU
{36394C2C-AA78-4009-A0EB-13DD78556F56}.Release|Any CPU.ActiveCfg = Release|Any CPU
{36394C2C-AA78-4009-A0EB-13DD78556F56}.Release|Any CPU.Build.0 = Release|Any CPU
{B5D81497-B28F-4EE6-9988-B6F4CA82FFE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B5D81497-B28F-4EE6-9988-B6F4CA82FFE3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B5D81497-B28F-4EE6-9988-B6F4CA82FFE3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B5D81497-B28F-4EE6-9988-B6F4CA82FFE3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {EEC27661-6BF4-459C-AAEB-E0DE61D96E6B}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,8 @@
using System.Net;
namespace SimpleHttpServer;
public sealed class DefaultAuthorizer : IAuthorizer
{
public (bool auth, object? data) IsAuthenticated(HttpListenerContext contect) => (true, null);
}

View File

@ -0,0 +1,23 @@
namespace SimpleHttpServer;
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class HttpEndpoint<T> : Attribute where T : IAuthorizer
{
public HttpRequestType Type { get; private set; }
public string Location { get; private set; }
public Type Authorizer { get; private set; }
public HttpEndpoint(HttpRequestType type, string location)
{
Type = type;
Location = location;
Authorizer = typeof(T);
}
}
[AttributeUsage(AttributeTargets.Method)]
public class HttpEndpoint : HttpEndpoint<DefaultAuthorizer>
{
public HttpEndpoint(HttpRequestType type, string location) : base(type, location) { }
}

View File

@ -0,0 +1,76 @@
using Newtonsoft.Json;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace SimpleHttpServer;
internal class HttpEndpointHandler {
private static readonly DefaultAuthorizer defaultAuth = new();
private readonly IAuthorizer _auth;
private readonly MethodInfo _handler;
private readonly Dictionary<string, (int pindex, Type type, int pparamIdx)> _params;
private readonly Func<Exception, HttpResponseBuilder> _errorPageBuilder;
public HttpEndpointHandler() {
_auth = defaultAuth;
}
public HttpEndpointHandler(IAuthorizer auth) {
}
public virtual void Handle(HttpListenerContext ctx) {
try {
var (isAuth, authData) = _auth.IsAuthenticated(ctx);
if (!isAuth) {
throw new HttpHandlingException(401, "Authorization required!");
}
// collect parameters
var invokeParams = new object?[_params.Count + 1];
var set = new BitArray(_params.Count);
invokeParams[0] = ctx;
// read pparams
// read qparams
var qst = ctx.Request.QueryString;
foreach (var qelem in ctx.Request.QueryString.AllKeys) {
if (_params.ContainsKey(qelem!)) {
var (pindex, type, isPParam) = _params[qelem!];
if (type == typeof(string)) {
invokeParams[pindex] = ctx.Request.QueryString[qelem!];
set.Set(pindex - 1, true);
} else {
var elem = JsonConvert.DeserializeObject(ctx.Request.QueryString[qelem!]!, type);
if (elem != null) {
invokeParams[pindex] = elem;
set.Set(pindex - 1, true);
}
}
}
}
// fill with defaults
foreach (var p in _params) {
if (!set.Get(p.Value.pindex)) {
invokeParams[p.Value.pindex] = p.Value.type.IsValueType ? Activator.CreateInstance(p.Value.type) : null;
}
}
var builder = _handler.Invoke(null, invokeParams) as HttpResponseBuilder;
builder!.SendResponse(ctx.Response);
} catch (Exception e) {
if (e is TargetInvocationException tex) {
e = tex.InnerException!;
}
_errorPageBuilder(e).SendResponse(ctx.Response);
}
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SimpleHttpServer;
internal class HttpHandlingException : Exception {
public readonly int status;
public HttpHandlingException(int status, string msg) : base(msg) {
this.status = status;
}
}

View File

@ -0,0 +1,13 @@
namespace SimpleHttpServer;
public enum HttpRequestType {
GET,
HEAD,
POST,
PUT,
DELETE,
CONNECT,
OPTIONS,
TRACE,
PATCH,
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace SimpleHttpServer;
public class HttpResponseBuilder {
public void SendResponse(HttpListenerResponse response) {
}
}

View File

@ -0,0 +1,130 @@
using System.Net;
using System.Reflection;
namespace SimpleHttpServer;
public sealed class HttpServer {
private Thread? _listenerThread;
private HttpListener _listener;
private readonly Dictionary<(string path, HttpRequestType rType), HttpEndpointHandler> _plainEndpoints = new();
private readonly Dictionary<(string path, HttpRequestType rType), HttpEndpointHandler> _pparamEndpoints = new();
public string Url { get; private set; }
public Func<HttpListenerContext, HttpResponseBuilder> Default404 { get; private set; }
public static HttpServer Create(int port, string url, params Type[] apiDefinitions) => Create(Console.Error, port, url, false, apiDefinitions);
public static HttpServer Create(TextWriter error, int port, string url, bool throwOnInvalidEndpoint, params Type[] apiDefinitions) {
var epDict = new Dictionary<(string, HttpRequestType), HttpEndpointHandler>();
foreach (var definition in apiDefinitions) {
foreach (var endpoint in definition.GetMethods()) {
var attrib = endpoint.GetCustomAttributes()
.Where(x => x.GetType().IsAssignableTo(typeof(HttpEndpoint<>)))
.Select(x => (HttpEndpoint<IAuthorizer>) x)
.SingleOrDefault();
if (attrib == null) {
continue;
}
// sanity checks
if (!endpoint.IsStatic) {
PrintErrorOrThrow(error, endpoint, throwOnInvalidEndpoint, "HttpEndpointAttribute is only valid on static methods!");
continue;
}
var myParams = endpoint.GetParameters();
if (myParams.Length <= 0 || !myParams[0].GetType().IsAssignableFrom(typeof(HttpListenerContext))) {
PrintErrorOrThrow(error, endpoint, throwOnInvalidEndpoint, $"{GetFancyMethodName(endpoint)} needs to have a HttpListenerContext as its first argument!");
continue;
}
if (!endpoint.ReturnParameter.ParameterType.IsAssignableTo(typeof(HttpResponseBuilder))) {
PrintErrorOrThrow(error, endpoint, throwOnInvalidEndpoint, $"{GetFancyMethodName(endpoint)} needs to have a HttpResponseBuilder as the return type!");
}
var path = attrib.Location;
int idx = path.IndexOf('{');
if (idx >= 0) {
// this path contains path parameters
throw new NotImplementedException("Implement path parameters!");
}
var qparams = new List<(string, Type)>();
}
}
return null!;
}
public void Shutdown() {
Shutdown(-1);
}
public bool Shutdown(int timeout) {
if (_listenerThread == null) {
throw new InvalidOperationException("Cannot shutdown HttpServer that has not been started");
}
_listenerThread.Interrupt();
bool exited = true;
if (timeout < 0) {
_listenerThread.Join();
} else {
exited = _listenerThread.Join(timeout);
}
_listenerThread = null;
_listener.Stop();
return exited;
}
public void Start() {
_listenerThread = new Thread(RunServer);
_listener.Start();
_listenerThread.Start();
}
private void RunServer() {
try {
for (; ; ) {
var ctx = _listener.GetContext();
ThreadPool.QueueUserWorkItem((localCtx) => {
HttpRequestType type;
if (!Enum.TryParse(localCtx.Request.HttpMethod, out type)) {
Default404(localCtx).SendResponse(localCtx.Response);
return;
}
var path = localCtx.Request.Url!.LocalPath.Replace('\\', '/');
HttpEndpointHandler? ep = null;
if (!_plainEndpoints.TryGetValue((path, type), out ep)) {
// not found among plain endpoints
foreach (var epk in _pparamEndpoints.Keys) {
if (epk.rType == type && path.StartsWith(epk.path)) {
ep = _pparamEndpoints[epk];
break;
}
}
if (ep == null) {
Default404(localCtx).SendResponse(localCtx.Response);
return;
}
}
ep.Handle(localCtx);
}, ctx, false);
}
} catch (ThreadInterruptedException) {
// this can only be reached when listener.GetContext is interrupted
// safely exit main loop
}
}
private static void PrintErrorOrThrow(TextWriter error, MethodInfo method, bool forceThrow, string msg) {
if (forceThrow) {
throw new Exception(msg);
} else {
error.WriteLine($"> {msg}\n skipping {GetFancyMethodName(method)} ...");
}
}
private static string GetFancyMethodName(MethodInfo method) => method.DeclaringType!.Name + "#" + method.Name;
}

View File

@ -0,0 +1,7 @@
using System.Net;
namespace SimpleHttpServer;
public interface IAuthorizer {
public abstract (bool auth, object? data) IsAuthenticated(HttpListenerContext contect);
}

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>

View File

@ -0,0 +1 @@


View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,8 @@
namespace SimpleHttpServerTest;
[TestClass]
public class UnitTest1 {
[TestMethod]
public void TestMethod1() {
}
}

View File

@ -0,0 +1 @@
global using Microsoft.VisualStudio.TestTools.UnitTesting;