initial commit
This commit is contained in:
commit
05b722e513
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.vs/
|
||||||
|
*/obj/
|
||||||
|
*/bin/
|
||||||
31
SimpleHttpServer.sln
Normal file
31
SimpleHttpServer.sln
Normal 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
|
||||||
8
SimpleHttpServer/DefaultAuthorizer.cs
Normal file
8
SimpleHttpServer/DefaultAuthorizer.cs
Normal 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);
|
||||||
|
}
|
||||||
23
SimpleHttpServer/HttpEndpoint.cs
Normal file
23
SimpleHttpServer/HttpEndpoint.cs
Normal 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) { }
|
||||||
|
}
|
||||||
76
SimpleHttpServer/HttpEndpointHandler.cs
Normal file
76
SimpleHttpServer/HttpEndpointHandler.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
SimpleHttpServer/HttpHandlingException.cs
Normal file
14
SimpleHttpServer/HttpHandlingException.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
SimpleHttpServer/HttpRequestType.cs
Normal file
13
SimpleHttpServer/HttpRequestType.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
namespace SimpleHttpServer;
|
||||||
|
|
||||||
|
public enum HttpRequestType {
|
||||||
|
GET,
|
||||||
|
HEAD,
|
||||||
|
POST,
|
||||||
|
PUT,
|
||||||
|
DELETE,
|
||||||
|
CONNECT,
|
||||||
|
OPTIONS,
|
||||||
|
TRACE,
|
||||||
|
PATCH,
|
||||||
|
}
|
||||||
14
SimpleHttpServer/HttpResponseBuilder.cs
Normal file
14
SimpleHttpServer/HttpResponseBuilder.cs
Normal 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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
130
SimpleHttpServer/HttpServer.cs
Normal file
130
SimpleHttpServer/HttpServer.cs
Normal 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;
|
||||||
|
}
|
||||||
7
SimpleHttpServer/IAuthorizer.cs
Normal file
7
SimpleHttpServer/IAuthorizer.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace SimpleHttpServer;
|
||||||
|
|
||||||
|
public interface IAuthorizer {
|
||||||
|
public abstract (bool auth, object? data) IsAuthenticated(HttpListenerContext contect);
|
||||||
|
}
|
||||||
13
SimpleHttpServer/SimpleHttpServer.csproj
Normal file
13
SimpleHttpServer/SimpleHttpServer.csproj
Normal 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>
|
||||||
1
SimpleHttpServer/SketchBoard.txt
Normal file
1
SimpleHttpServer/SketchBoard.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
18
SimpleHttpServerTest/SimpleHttpServerTest.csproj
Normal file
18
SimpleHttpServerTest/SimpleHttpServerTest.csproj
Normal 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>
|
||||||
8
SimpleHttpServerTest/UnitTest1.cs
Normal file
8
SimpleHttpServerTest/UnitTest1.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace SimpleHttpServerTest;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class UnitTest1 {
|
||||||
|
[TestMethod]
|
||||||
|
public void TestMethod1() {
|
||||||
|
}
|
||||||
|
}
|
||||||
1
SimpleHttpServerTest/Usings.cs
Normal file
1
SimpleHttpServerTest/Usings.cs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
global using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
Loading…
Reference in New Issue
Block a user