M .gitignore => .gitignore +1 -1
@@ 3,4 3,4 @@ obj/
/packages/
riderModule.iml
/_ReSharper.Caches/
-.idea>
\ No newline at end of file
+.idea
M Connections/ServerLogConnection.cs => Connections/ServerLogConnection.cs +18 -18
@@ 1,18 1,18 @@
-using WebSocketSharp;
-using WebSocketSharp.Server;
-
-namespace Mamoru.Connections;
-
-public class ServerLogConnection : WebSocketBehavior {
- public static string Path = "/serverlog";
-
- protected override void OnMessage(MessageEventArgs e) {
- Send(e.Data);
- Log.Info(e.Data);
- }
-
- protected override void OnOpen() {
- Log.Info("New client connected.");
- base.OnOpen();
- }
-}>
\ No newline at end of file
+// using WebSocketSharp;
+// using WebSocketSharp.Server;
+//
+// namespace Mamoru.Connections;
+//
+// public class ServerLogConnection : WebSocketBehavior {
+// public static string Path = "/serverlog";
+//
+// protected override void OnMessage(MessageEventArgs e) {
+// Send(e.Data);
+// Log.Info(e.Data);
+// }
+//
+// protected override void OnOpen() {
+// Log.Info("New client connected.");
+// base.OnOpen();
+// }
+// }<
\ No newline at end of file
M Mamoru.csproj => Mamoru.csproj +3 -4
@@ 1,7 1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <LangVersion>preview</LangVersion>
+ <LangVersion>13</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UnityVersion>2022.3.43f1</UnityVersion>
@@ 9,12 9,11 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="EXILED" Version="9.0.0-beta.4"/>
- <PackageReference Include="WebSocketSharp" Version="1.0.3-rc11"/>
+ <PackageReference Include="EXILED" Version="9.0.0-beta.5" />
+ <PackageReference Include="Watson" Version="6.2.2" />
</ItemGroup>
<ItemGroup>
<Folder Include="Models.EXILED\"/>
- <Folder Include="Router\"/>
</ItemGroup>
</Project>
M Mamoru.sln.DotSettings.user => Mamoru.sln.DotSettings.user +3 -2
@@ 1,6 1,7 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue"><AssemblyExplorer>
- <Assembly Path="/home/nora/.steam/steam/steamapps/common/SCP Secret Laboratory Dedicated Server/SCPSL_Data/Managed/Assembly-CSharp.dll" />
+ <Assembly Path="/home/nora/.nuget/packages/exiled/9.0.0-beta.5/lib/net48/Exiled.API.dll" />
</AssemblyExplorer></s:String>
<s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue">/home/nora/.dotnet/sdk/9.0.100-preview.7.24407.12/MSBuild.dll</s:String>
- <s:Int64 x:Key="/Default/Environment/Hierarchy/Build/BuildTool/MsbuildVersion/@EntryValue">1114112</s:Int64></wpf:ResourceDictionary>>
\ No newline at end of file
+ <s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/DotNetCliExePath/@EntryValue">/home/nora/.dotnet/dotnet</s:String>
+ </wpf:ResourceDictionary><
\ No newline at end of file
M Managers/RoutesManager.cs => Managers/RoutesManager.cs +29 -56
@@ 1,78 1,51 @@
using System.Net;
using System.Reflection;
+using System.Text.RegularExpressions;
using Exiled.API.Features;
-using Mamoru.Connections;
-using Mamoru.Routes;
+using Mamoru.Resources;
using Mamoru.Utils;
-using WebSocketSharp.Server;
-using HttpStatusCode = WebSocketSharp.Net.HttpStatusCode;
+using WatsonWebserver.Core;
+using WatsonWebserver;
namespace Mamoru.Managers;
public class RoutesManager {
- private readonly HttpServer _httpServer = new(IPAddress.Loopback, 4649);
- private readonly Node _router = new("/");
+ private readonly Webserver _httpServer = new(
+ new WebserverSettings(IPAddress.Loopback.ToString(), 4649),
+ MiscResource.DefaultRoute
+ );
public RoutesManager() {
- Log.Info("Setting up HTTP server...");
LoadRoutes();
- _router.Traverse(n => Log.Info(n.Path));
-
- Log.Info("Registering HandleRequest events...");
- RegisterEvents();
-
Log.Info("Starting HTTP server...");
_httpServer.Start();
- Log.Info($"HTTP server listening on port {_httpServer.Port}");
- }
-
- private void HandleRequest(object sender, HttpRequestEventArgs ev) {
- var req = ev.Request;
- var res = ev.Response;
- var path = req.RawUrl;
- var ctx = new Context(new Dictionary<string, string>(), new Dictionary<string, string>());
- var route = _router.MatchRoute(path, ref ctx);
- Log.Info($"{req.HttpMethod} {path} {route} {string.Join(", ", ctx.QueryParams)}");
- if (route is null) {
- EmptyResponse.Create(ref res, HttpStatusCode.NotFound);
- return;
- }
- var methodName = req.HttpMethod[0] + req.HttpMethod.ToLower().Substring(1);
- var method = route.GetType().GetMethod(methodName);
- method!.Invoke(route, [req, res, ctx]);
+ Log.Info($"HTTP server listening on {_httpServer.Settings.Hostname}:{_httpServer.Settings.Port}");
}
private void LoadRoutes() {
- Log.Info("Loading routes...");
- Assembly.GetExecutingAssembly()
- .GetTypes()
- .Where(type => type.IsClass && type is { IsAbstract: false, Namespace: "Mamoru.Routes" })
- .ToArray()
- .ForEach(type => {
- try {
- var clazz = (AbstractRoute)Activator.CreateInstance(type);
- _router.AddRoute(ref clazz, clazz.Path.Trim('/'));
- Log.Info($"Loaded http route {type.Name} at {clazz.Path}");
+ var assembly = Assembly.GetExecutingAssembly();
+ var typeClasses = assembly.GetTypes().Where(type => type.IsClass && type.Namespace == "Mamoru.Resources" && !type.Name.Contains("<"));
+ foreach (var classType in typeClasses) {
+ Log.Info($"Loading resource {classType.Name}...");
+ var methods = classType.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly);
+ foreach (var methodType in methods) {
+ if (methodType.GetCustomAttribute<DoNotLoad>() is not null) {
+ Log.Info($"Skipping method {methodType.Name}...");
+ continue;
}
- catch {
- Log.Warn($"Failed to load http route {type.Name}");
- }
- });
-
- _httpServer.AddWebSocketService<ServerLogConnection>(ServerLogConnection.Path);
- }
-
- private void RegisterEvents() {
- var methodInfo = GetType().GetMethod("HandleRequest", BindingFlags.Instance | BindingFlags.NonPublic);
- _httpServer.GetType().GetEvents().ForEach(eventInfo => {
- eventInfo.AddEventHandler(_httpServer, Delegate.CreateDelegate(
- eventInfo.EventHandlerType,
- this,
- methodInfo!
- ));
- });
+ Log.Info($"Loading method {methodType.Name}...");
+ var attributes = methodType.GetCustomAttribute<Route>();
+ if (attributes is null) break;
+ var method = (Func<HttpContextBase, Task>)Delegate.CreateDelegate(typeof(Func<HttpContextBase, Task>), methodType);
+ if (attributes.IsDynamic) _httpServer.Routes.PreAuthentication.Dynamic.Add(attributes.Method, new Regex(attributes.Path), method);
+ if (attributes.IsParameter) _httpServer.Routes.PreAuthentication.Parameter.Add(attributes.Method, attributes.Path, method);
+ if (attributes.IsStatic) _httpServer.Routes.PreAuthentication.Static.Add(attributes.Method, attributes.Path, method);
+ Log.Info($"Loaded method {methodType.Name} at {attributes.Path}.");
+ }
+ Log.Info($"Finished loading resource {classType.Name}.");
+ }
}
public void StopHttpServer() {
R Routes/StatusRoute.cs => Resources/MiscResource.cs +19 -13
@@ 2,21 2,27 @@ using System.Reflection;
using Exiled.Loader;
using Mamoru.Models.Mamoru;
using Mamoru.Utils;
-using WebSocketSharp.Net;
+using WatsonWebserver.Core;
-namespace Mamoru.Routes;
+namespace Mamoru.Resources;
-public class StatusRoute : AbstractRoute {
- public override string Path => "/.well-known/mamoru/status";
- public override bool Cors => true;
+public static class MiscResource {
+ [DoNotLoad]
+ public static async Task DefaultRoute(HttpContextBase ctx) {
+ ctx.Response.StatusCode = 404;
+ await ctx.Response.Send("not found");
+ }
+
+ [Route(HttpMethod.GET, "/.well-known/mamoru/status")]
+ public static async Task GetServerStatus(HttpContextBase ctx) {
+ var status = new GetServerStatusResponse(
+ true,
+ true,
+ Assembly.GetAssembly(typeof(Loader)).GetName().Version.ToString(),
+ Assembly.GetExecutingAssembly().GetName().Version.ToString()
+ );
- public override void Get(ref HttpListenerRequest req, ref HttpListenerResponse res, ref Context ctx) {
- var status = new GetServerStatusResponse(
- true,
- true,
- Assembly.GetAssembly(typeof(Loader)).GetName().Version.ToString(),
- Assembly.GetExecutingAssembly().GetName().Version.ToString()
- );
- YamlResponse.Create(ref res, status);
+ ctx.Response.ContentType = "application/yaml";
+ await ctx.Response.Send(Serializers.YamlSerializer.Serialize(status));
}
}=
\ No newline at end of file
R Routes/PlayersRoute.cs => Resources/PlayersResource.cs +9 -9
@@ 1,20 1,20 @@
using Exiled.API.Features;
using Mamoru.Models.Mamoru;
using Mamoru.Utils;
-using WebSocketSharp.Net;
+using WatsonWebserver.Core;
-namespace Mamoru.Routes;
+namespace Mamoru.Resources;
-public class PlayersRoute : AbstractRoute {
- public override string Path => "/players";
- public override bool Cors => true;
-
- public override void Get(ref HttpListenerRequest req, ref HttpListenerResponse res, ref Context ctx) {
+public static class PlayersResource {
+ [Route(HttpMethod.GET, "/players/list")]
+ public static async Task ListPlayers(HttpContextBase ctx) {
List<ListPlayersResponse> players = [];
+
Player.List.ToList().ForEach(player => {
players.Add(new ListPlayersResponse((ushort)player.Id, player.UserId, player.Nickname, player.RemoteAdminAccess));
});
-
- YamlResponse.Create(ref res, players);
+
+ ctx.Response.ContentType = "application/yaml";
+ await ctx.Response.Send(Serializers.YamlSerializer.Serialize(players));
}
}=
\ No newline at end of file
D Routes/AbstractRoute.cs => Routes/AbstractRoute.cs +0 -54
@@ 1,54 0,0 @@
-using Mamoru.Utils;
-using WebSocketSharp.Net;
-
-namespace Mamoru.Routes;
-
-public abstract class AbstractRoute {
- public abstract string Path { get; }
- public virtual bool Cors => false;
-
- public virtual void Connect(ref HttpListenerRequest req, ref HttpListenerResponse res, ref Context ctx) {
- EmptyResponse.Create(ref res, HttpStatusCode.MethodNotAllowed);
- }
-
- public virtual void Delete(ref HttpListenerRequest req, ref HttpListenerResponse res, ref Context ctx) {
- EmptyResponse.Create(ref res, HttpStatusCode.MethodNotAllowed);
- }
-
- public virtual void Get(ref HttpListenerRequest req, ref HttpListenerResponse res, ref Context ctx) {
- EmptyResponse.Create(ref res, HttpStatusCode.MethodNotAllowed);
- }
-
- public virtual void Head(ref HttpListenerRequest req, ref HttpListenerResponse res, ref Context ctx) {
- EmptyResponse.Create(ref res, HttpStatusCode.MethodNotAllowed);
- }
-
- public virtual void Options(ref HttpListenerRequest req, ref HttpListenerResponse res, ref Context ctx) {
- if (Cors) {
- res.Headers.Add("Access-Control-Allow-Origin", "*");
- res.Headers.Add("Access-Control-Allow-Headers", "*");
- res.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
- res.Headers.Add("Access-Control-Max-Age", "86400");
- EmptyResponse.Create(ref res);
- return;
- }
-
- EmptyResponse.Create(ref res, HttpStatusCode.MethodNotAllowed);
- }
-
- public virtual void Post(ref HttpListenerRequest req, ref HttpListenerResponse res, ref Context ctx) {
- EmptyResponse.Create(ref res, HttpStatusCode.MethodNotAllowed);
- }
-
- public virtual void Patch(ref HttpListenerRequest req, ref HttpListenerResponse res, ref Context ctx) {
- EmptyResponse.Create(ref res, HttpStatusCode.MethodNotAllowed);
- }
-
- public virtual void Put(ref HttpListenerRequest req, ref HttpListenerResponse res, ref Context ctx) {
- EmptyResponse.Create(ref res, HttpStatusCode.MethodNotAllowed);
- }
-
- public virtual void Trace(ref HttpListenerRequest req, ref HttpListenerResponse res, ref Context ctx) {
- EmptyResponse.Create(ref res, HttpStatusCode.MethodNotAllowed);
- }
-}>
\ No newline at end of file
A Utils/Attributes.cs => Utils/Attributes.cs +16 -0
@@ 0,0 1,16 @@
+using System.Text.RegularExpressions;
+using WatsonWebserver.Core;
+
+namespace Mamoru.Utils;
+
+[AttributeUsage(AttributeTargets.Method)]
+public class Route(HttpMethod method, string path, bool isParameter = false, bool isDynamic = false) : Attribute {
+ public HttpMethod Method => method;
+ public string Path => path;
+ public bool IsStatic => !isParameter && !isDynamic;
+ public bool IsParameter => isParameter;
+ public bool IsDynamic => isDynamic;
+}
+
+[AttributeUsage(AttributeTargets.Method)]
+public class DoNotLoad : Attribute {}<
\ No newline at end of file
D Utils/Response.cs => Utils/Response.cs +0 -30
@@ 1,30 0,0 @@
-using System.Text;
-using WebSocketSharp.Net;
-using YamlDotNet.Serialization;
-using YamlDotNet.Serialization.NamingConventions;
-
-namespace Mamoru.Utils;
-
-public static class YamlResponse {
- private static readonly ISerializer Serializer = new SerializerBuilder()
- .WithNamingConvention(UnderscoredNamingConvention.Instance)
- .Build();
-
- public static void Create(ref HttpListenerResponse res, object value, HttpStatusCode statusCode = HttpStatusCode.OK) {
- var serialized = Serializer.Serialize(value);
- var data = Utf8.GetBytes(serialized);
- res.StatusCode = (int)statusCode;
- res.ContentType = "application/yaml";
- res.ContentEncoding = Encoding.UTF8;
- res.ContentLength64 = data.LongLength;
- res.Close(data, false);
- }
-}
-
-public static class EmptyResponse {
- public static void Create(ref HttpListenerResponse res, HttpStatusCode statusCode = HttpStatusCode.NoContent) {
- res.StatusCode = (int)statusCode;
- res.ContentLength64 = 0;
- res.Close();
- }
-}>
\ No newline at end of file
D Utils/Router.cs => Utils/Router.cs +0 -79
@@ 1,79 0,0 @@
-using System.Text.RegularExpressions;
-using Exiled.API.Features;
-using Mamoru.Routes;
-
-namespace Mamoru.Utils;
-
-public record Context(Dictionary<string, string> QueryParams, Dictionary<string, string> RouteParams);
-
-public class Node(string path, AbstractRoute? route = null) {
- public readonly List<Node> Children = [];
- public readonly string Path = path;
- public readonly AbstractRoute? Route = route;
-
- public void Add(Node child) {
- Children.Add(child);
- }
-
- public void Remove(Node child) {
- Children.Remove(child);
- }
-
- public Node? Find(string index) {
- return Children.FirstOrDefault(node => node.Path == index);
- }
-
- public void Traverse(Action<Node> action) {
- action(this);
- Children.ForEach(node => node.Traverse(action));
- }
-
- public void AddRoute(ref AbstractRoute route, string path, Node? parent = null) {
- path = path.Trim('/');
- parent ??= this;
-
- var sep = path.IndexOf('/');
- var seg = sep == -1 ? path : path.Substring(0, sep);
- var curr = parent.Find(seg);
- var n = new Node(seg, sep == -1 ? route : null);
-
- parent.Add(n);
- if (n.Route is not null) return;
-
- AddRoute(ref route, path.Substring(sep + 1), curr ?? parent.Find(seg));
- }
-
- public AbstractRoute? MatchRoute(string path, ref Context ctx, Node? parent = null) {
- path = path.Trim('/');
- string? query = null;
- if (path.Contains("?")) {
- var p = path.Split('?');
- path = p[0];
- query = "?" + p[1];
- }
-
- parent ??= this;
-
- var sep = path.IndexOf('/');
- var seg = sep == -1 ? path.Split('?')[0] : path.Substring(0, sep);
- if (string.IsNullOrEmpty(seg)) return MatchRoute(path.Substring(sep + 1), ref ctx, parent);
- var curr = parent.Find(seg);
- var routeParam = curr is null ? parent.Children.FirstOrDefault(node => node.Path.StartsWith(":")) : null;
- if (routeParam is not null) {
- ctx.RouteParams.Add(routeParam.Path.Substring(1), seg);
- curr ??= routeParam;
- }
-
- var queryParams = sep == -1 && query is not null
- ? new Regex(@"[\\?&](?<key>([^&=]+))=(?<value>[^&=]+)").Matches(query).Cast<Match>().ToArray()
- : null;
- if (queryParams is not null) {
- foreach (var match in queryParams) {
- ctx.QueryParams.Add(match.Groups["key"].Value, match.Groups["value"].Value);
- }
- }
-
- if (curr is null) return null;
- return sep == -1 ? curr.Route : MatchRoute(path.Substring(sep + 1), ref ctx, curr);
- }
-}>
\ No newline at end of file
A Utils/Serializers.cs => Utils/Serializers.cs +11 -0
@@ 0,0 1,11 @@
+using YamlDotNet.Serialization;
+using YamlDotNet.Serialization.NamingConventions;
+
+namespace Mamoru.Utils;
+
+public static class Serializers {
+ public static readonly ISerializer YamlSerializer = new SerializerBuilder()
+ .WithNamingConvention(UnderscoredNamingConvention.Instance)
+ .Build();
+
+}<
\ No newline at end of file