Managed implementation of HttpListener. (#1460)

This commit is contained in:
Pieter-Jan Briers
2020-12-21 02:51:04 +01:00
committed by GitHub
parent d94f702601
commit 8f870403d2
9 changed files with 155 additions and 102 deletions

3
.gitmodules vendored
View File

@@ -10,3 +10,6 @@
[submodule "Robust.LoaderApi"]
path = Robust.LoaderApi
url = https://github.com/space-wizards/Robust.LoaderApi.git
[submodule "ManagedHttpListener"]
path = ManagedHttpListener
url = https://github.com/space-wizards/ManagedHttpListener.git

1
ManagedHttpListener Submodule

Submodule ManagedHttpListener added at f2aa590fec

View File

@@ -1,14 +1,15 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json.Linq;
namespace Robust.Server.Interfaces.ServerStatus
{
public delegate bool StatusHostHandler(
HttpMethod method,
HttpListenerRequest request,
HttpListenerResponse response);
IStatusHandlerContext context);
public interface IStatusHost
{
@@ -32,4 +33,30 @@ namespace Robust.Server.Interfaces.ServerStatus
/// </summary>
event Action<JObject> OnInfoRequest;
}
public interface IStatusHandlerContext
{
HttpMethod RequestMethod { get; }
IPEndPoint RemoteEndPoint { get; }
Uri Url { get; }
bool IsGetLike { get; }
IReadOnlyDictionary<string, StringValues> RequestHeaders { get; }
[return: MaybeNull]
public T RequestBodyJson<T>();
void Respond(
string text,
HttpStatusCode code = HttpStatusCode.OK,
string contentType = "text/plain");
void Respond(
string text,
int code = 200,
string contentType = "text/plain");
void RespondError(HttpStatusCode code);
void RespondJson(object jsonData, HttpStatusCode code = HttpStatusCode.OK);
}
}

View File

@@ -14,13 +14,16 @@
<PackageReference Include="Microsoft.Data.Sqlite" Version="5.0.0" />
<PackageReference Include="prometheus-net" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.Loki" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.Primitives" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Lidgren.Network\Lidgren.Network.csproj" />
<ProjectReference Include="..\ManagedHttpListener\src\System.Net.HttpListener.csproj" />
<ProjectReference Include="..\Robust.Physics\Robust.Physics.csproj" />
<ProjectReference Include="..\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
<ProjectReference Include="..\Robust.Shared.Scripting\Robust.Shared.Scripting.csproj" />
<ProjectReference Include="..\Robust.Shared\Robust.Shared.csproj" />
<ProjectReference Include="..\System.Net.HttpListener\src\System.Net.HttpListener.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="server_config.toml">

View File

@@ -1,11 +1,8 @@
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Robust.Server.Interfaces.ServerStatus;
using Robust.Shared;
using Robust.Shared.Utility;
namespace Robust.Server.ServerStatus
{
@@ -20,32 +17,24 @@ namespace Robust.Server.ServerStatus
AddHandler(HandleInfo);
}
private static bool HandleTeapot(HttpMethod method, HttpListenerRequest request, HttpListenerResponse response)
private static bool HandleTeapot(IStatusHandlerContext context)
{
if (!method.IsGetLike() || request.Url!.AbsolutePath != "/teapot")
if (!context.IsGetLike || context.Url!.AbsolutePath != "/teapot")
{
return false;
}
response.Respond(method, "I am a teapot.", (HttpStatusCode) 418);
context.Respond("I am a teapot.", (HttpStatusCode) 418);
return true;
}
private bool HandleStatus(HttpMethod method, HttpListenerRequest request, HttpListenerResponse response)
private bool HandleStatus(IStatusHandlerContext context)
{
if (!method.IsGetLike() || request.Url!.AbsolutePath != "/status")
if (!context.IsGetLike || context.Url!.AbsolutePath != "/status")
{
return false;
}
response.StatusCode = (int) HttpStatusCode.OK;
response.ContentType = "application/json";
if (method == HttpMethod.Head)
{
return true;
}
var jObject = new JObject
{
// We need to send at LEAST name and player count to have the launcher work with us.
@@ -56,32 +45,18 @@ namespace Robust.Server.ServerStatus
OnStatusRequest?.Invoke(jObject);
using var streamWriter = new StreamWriter(response.OutputStream, EncodingHelpers.UTF8);
using var jsonWriter = new JsonTextWriter(streamWriter);
JsonSerializer.Serialize(jsonWriter, jObject);
jsonWriter.Flush();
context.RespondJson(jObject);
return true;
}
private bool HandleInfo(HttpMethod method, HttpListenerRequest request, HttpListenerResponse response)
private bool HandleInfo(IStatusHandlerContext context)
{
if (!method.IsGetLike() || request.Url!.AbsolutePath != "/info")
if (!context.IsGetLike || context.Url!.AbsolutePath != "/info")
{
return false;
}
response.StatusCode = (int) HttpStatusCode.OK;
response.ContentType = "application/json";
if (method == HttpMethod.Head)
{
return true;
}
var downloadUrl = _configurationManager.GetCVar(CVars.BuildDownloadUrl);
JObject? buildInfo;
@@ -125,13 +100,7 @@ namespace Robust.Server.ServerStatus
OnInfoRequest?.Invoke(jObject);
using var streamWriter = new StreamWriter(response.OutputStream, EncodingHelpers.UTF8);
using var jsonWriter = new JsonTextWriter(streamWriter);
JsonSerializer.Serialize(jsonWriter, jObject);
jsonWriter.Flush();
context.RespondJson(jObject);
return true;
}

View File

@@ -1,13 +1,14 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Robust.Server.Interfaces;
using Robust.Server.Interfaces.Player;
using Robust.Server.Interfaces.ServerStatus;
using Robust.Shared;
@@ -18,6 +19,9 @@ using Robust.Shared.Interfaces.Log;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Utility;
using HttpListener = ManagedHttpListener.HttpListener;
using HttpListenerContext = ManagedHttpListener.HttpListenerContext;
// This entire file is NIHing a REST server because pulling in libraries is effort.
// Also it was fun to write.
@@ -32,7 +36,6 @@ namespace Robust.Server.ServerStatus
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IServerNetManager _netManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IBaseServer _baseServer = default!;
private static readonly JsonSerializer JsonSerializer = new();
private readonly List<StatusHostHandler> _handlers = new();
@@ -44,17 +47,16 @@ namespace Robust.Server.ServerStatus
public Task ProcessRequestAsync(HttpListenerContext context)
{
var response = context.Response;
var request = context.Request;
var method = new HttpMethod(request.HttpMethod);
var apiContext = (IStatusHandlerContext) new ContextImpl(context);
_httpSawmill.Info($"{method} {context.Request.Url?.PathAndQuery} from {request.RemoteEndPoint}");
_httpSawmill.Info(
$"{apiContext.RequestMethod} {apiContext.Url.PathAndQuery} from {apiContext.RemoteEndPoint}");
try
{
foreach (var handler in _handlers)
{
if (handler(method, request, response))
if (handler(apiContext))
{
return Task.CompletedTask;
}
@@ -62,11 +64,11 @@ namespace Robust.Server.ServerStatus
// No handler returned true, assume no handlers care about this.
// 404.
response.Respond(method, "Not Found", HttpStatusCode.NotFound);
apiContext.Respond("Not Found", HttpStatusCode.NotFound);
}
catch (Exception e)
{
response.Respond(method, "Internal Server Error", HttpStatusCode.InternalServerError);
apiContext.Respond("Internal Server Error", HttpStatusCode.InternalServerError);
_httpSawmill.Error($"Exception in StatusHost: {e}");
}
@@ -195,5 +197,78 @@ namespace Robust.Server.ServerStatus
[JsonProperty("fork_id")] public string ForkId = default!;
[JsonProperty("version")] public string Version = default!;
}
private sealed class ContextImpl : IStatusHandlerContext
{
private readonly HttpListenerContext _context;
public HttpMethod RequestMethod { get; }
public IPEndPoint RemoteEndPoint => _context.Request.RemoteEndPoint!;
public Uri Url => _context.Request.Url!;
public bool IsGetLike => RequestMethod == HttpMethod.Head || RequestMethod == HttpMethod.Get;
public IReadOnlyDictionary<string, StringValues> RequestHeaders { get; }
public ContextImpl(HttpListenerContext context)
{
_context = context;
RequestMethod = new HttpMethod(context.Request.HttpMethod!);
var headers = new Dictionary<string, StringValues>();
foreach (string? key in context.Request.Headers.Keys)
{
if (key == null)
continue;
headers.Add(key, context.Request.Headers.GetValues(key));
}
RequestHeaders = headers;
}
[return: MaybeNull]
public T RequestBodyJson<T>()
{
using var streamReader = new StreamReader(_context.Request.InputStream, EncodingHelpers.UTF8);
using var jsonReader = new JsonTextReader(streamReader);
var serializer = new JsonSerializer();
return serializer.Deserialize<T>(jsonReader);
}
public void Respond(string text, HttpStatusCode code = HttpStatusCode.OK, string contentType = "text/plain")
{
Respond(text, (int) code, contentType);
}
public void Respond(string text, int code = 200, string contentType = "text/plain")
{
_context.Response.StatusCode = code;
_context.Response.ContentType = contentType;
if (RequestMethod == HttpMethod.Head)
{
return;
}
using var writer = new StreamWriter(_context.Response.OutputStream, EncodingHelpers.UTF8);
writer.Write(text);
}
public void RespondError(HttpStatusCode code)
{
Respond(code.ToString(), code);
}
public void RespondJson(object jsonData, HttpStatusCode code = HttpStatusCode.OK)
{
using var streamWriter = new StreamWriter(_context.Response.OutputStream, EncodingHelpers.UTF8);
using var jsonWriter = new JsonTextWriter(streamWriter);
JsonSerializer.Serialize(jsonWriter, jsonData);
jsonWriter.Flush();
}
}
}
}

View File

@@ -1,7 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net;
using System.Net.Http;
using Newtonsoft.Json;
using Robust.Shared.Utility;
@@ -9,41 +8,6 @@ namespace Robust.Server.ServerStatus
{
public static class StatusHostHelpers
{
public static bool IsGetLike(this HttpMethod method)
{
return method == HttpMethod.Get || method == HttpMethod.Head;
}
public static void Respond(
this HttpListenerResponse response,
HttpMethod method,
string text,
HttpStatusCode code = HttpStatusCode.OK,
string contentType = "text/plain")
{
response.Respond(method, text, (int) code, contentType);
}
public static void Respond(
this HttpListenerResponse response,
HttpMethod method,
string text,
int code = 200,
string contentType = "text/plain")
{
response.StatusCode = code;
response.ContentType = contentType;
if (method == HttpMethod.Head)
{
return;
}
using var writer = new StreamWriter(response.OutputStream, EncodingHelpers.UTF8);
writer.Write(text);
}
[return: MaybeNull]
public static T GetFromJson<T>(this HttpListenerRequest request)
{

View File

@@ -45,9 +45,9 @@ namespace Robust.Server.ServerStatus
_statusHost.AddHandler(ShutdownHandler);
}
private bool UpdateHandler(HttpMethod method, HttpListenerRequest request, HttpListenerResponse response)
private bool UpdateHandler(IStatusHandlerContext context)
{
if (method != HttpMethod.Post || request.Url!.AbsolutePath != "/update")
if (context.RequestMethod != HttpMethod.Post || context.Url!.AbsolutePath != "/update")
{
return false;
}
@@ -58,26 +58,26 @@ namespace Robust.Server.ServerStatus
return false;
}
var auth = request.Headers["WatchdogToken"];
var auth = context.RequestHeaders["WatchdogToken"];
if (auth != _watchdogToken)
{
// Holy shit nobody read these logs please.
_sawmill.Info(@"Failed auth: ""{0}"" vs ""{1}""", auth, _watchdogToken);
response.StatusCode = (int) HttpStatusCode.Unauthorized;
context.RespondError(HttpStatusCode.Unauthorized);
return true;
}
_taskManager.RunOnMainThread(() => UpdateReceived?.Invoke());
response.StatusCode = (int) HttpStatusCode.OK;
context.Respond("Success", HttpStatusCode.OK);
return true;
}
private bool ShutdownHandler(HttpMethod method, HttpListenerRequest request, HttpListenerResponse response)
private bool ShutdownHandler(IStatusHandlerContext context)
{
if (method != HttpMethod.Post || request.Url!.AbsolutePath != "/shutdown")
if (context.RequestMethod != HttpMethod.Post || context.Url!.AbsolutePath != "/shutdown")
{
return false;
}
@@ -88,22 +88,21 @@ namespace Robust.Server.ServerStatus
return false;
}
var auth = request.Headers["WatchdogToken"];
var auth = context.RequestHeaders["WatchdogToken"];
if (auth != _watchdogToken)
{
_sawmill.Warning(
"received POST /shutdown with invalid authentication token. Ignoring {0}, {1}", auth,
_watchdogToken);
response.StatusCode = (int) HttpStatusCode.Unauthorized;
context.RespondError(HttpStatusCode.Unauthorized);
return true;
}
ShutdownParameters? parameters = null;
try
{
parameters = request.GetFromJson<ShutdownParameters>();
parameters = context.RequestBodyJson<ShutdownParameters>();
}
catch (JsonSerializationException)
{
@@ -112,14 +111,14 @@ namespace Robust.Server.ServerStatus
if (parameters == null)
{
response.StatusCode = (int) HttpStatusCode.BadRequest;
context.RespondError(HttpStatusCode.BadRequest);
return true;
}
_taskManager.RunOnMainThread(() => _baseServer.Shutdown(parameters.Reason));
response.StatusCode = (int) HttpStatusCode.OK;
context.Respond("Success", HttpStatusCode.OK);
return true;
}

View File

@@ -29,6 +29,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Robust.LoaderApi", "Robust.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robust.LoaderApi", "Robust.LoaderApi\Robust.LoaderApi\Robust.LoaderApi.csproj", "{4FC5049F-AEEC-4DC0-9F4D-EB927AAB4F15}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ManagedHttpListener", "ManagedHttpListener", "{15D28C35-25F6-4EA8-8D53-29DA7C8A24A7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Net.HttpListener", "ManagedHttpListener\src\System.Net.HttpListener.csproj", "{C3EB43AF-31FD-48F5-A4FB-552D0F13948B}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robust.Client.Injectors", "Robust.Client.Injectors\Robust.Client.Injectors.csproj", "{EEF2C805-5E03-41EA-A916-49C1DD15EF41}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robust.Client.NameGenerator", "Robust.Client.NameGenerator\Robust.Client.NameGenerator.csproj", "{EFB7A05D-71D0-47D1-B7B4-35D4FF661F13}"
@@ -137,6 +140,14 @@ Global
{4FC5049F-AEEC-4DC0-9F4D-EB927AAB4F15}.Release|Any CPU.Build.0 = Release|Any CPU
{4FC5049F-AEEC-4DC0-9F4D-EB927AAB4F15}.Release|x64.ActiveCfg = Release|Any CPU
{4FC5049F-AEEC-4DC0-9F4D-EB927AAB4F15}.Release|x64.Build.0 = Release|Any CPU
{C3EB43AF-31FD-48F5-A4FB-552D0F13948B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C3EB43AF-31FD-48F5-A4FB-552D0F13948B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C3EB43AF-31FD-48F5-A4FB-552D0F13948B}.Debug|x64.ActiveCfg = Debug|Any CPU
{C3EB43AF-31FD-48F5-A4FB-552D0F13948B}.Debug|x64.Build.0 = Debug|Any CPU
{C3EB43AF-31FD-48F5-A4FB-552D0F13948B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C3EB43AF-31FD-48F5-A4FB-552D0F13948B}.Release|Any CPU.Build.0 = Release|Any CPU
{C3EB43AF-31FD-48F5-A4FB-552D0F13948B}.Release|x64.ActiveCfg = Release|Any CPU
{C3EB43AF-31FD-48F5-A4FB-552D0F13948B}.Release|x64.Build.0 = Release|Any CPU
{EEF2C805-5E03-41EA-A916-49C1DD15EF41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EEF2C805-5E03-41EA-A916-49C1DD15EF41}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EEF2C805-5E03-41EA-A916-49C1DD15EF41}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -184,6 +195,7 @@ Global
GlobalSection(NestedProjects) = preSolution
{ECBCE1D8-05C2-4881-9446-197C4C8E1C14} = {9143C8DD-A989-4089-9149-C50D12189FE4}
{4FC5049F-AEEC-4DC0-9F4D-EB927AAB4F15} = {805C8FD2-0C32-4DA8-BC4B-143BA5D48FF4}
{C3EB43AF-31FD-48F5-A4FB-552D0F13948B} = {15D28C35-25F6-4EA8-8D53-29DA7C8A24A7}
{D73768A2-BFCD-4916-8F52-4034C28F345C} = {1B1FC7C4-0212-4B3E-90D4-C7B58759E4B0}
{1CDC9C4F-668E-47A3-8A44-216E95644BEB} = {1B1FC7C4-0212-4B3E-90D4-C7B58759E4B0}
{B05EFB71-AEC7-4C6E-984A-A1BCC58F9AD1} = {1B1FC7C4-0212-4B3E-90D4-C7B58759E4B0}