M Mamoru.csproj => Mamoru.csproj +1 -0
@@ 15,5 15,6 @@
<ItemGroup>
<Folder Include="Models.EXILED\" />
+ <Folder Include="Router\" />
</ItemGroup>
</Project>
M Managers/RoutesManager.cs => Managers/RoutesManager.cs +17 -19
@@ 11,18 11,28 @@ namespace Mamoru.Managers;
public class RoutesManager {
private readonly HttpServer _httpServer = new(IPAddress.Loopback, 4649);
- public readonly Dictionary<string, AbstractRoute> HttpRoutes = new();
+ private readonly Node _router = new("/");
public RoutesManager() {
- StartHttpServer();
+ 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 route = HttpRoutes.TryGetValue(path, out var r) ? r : null;
- Log.Info($"{req.HttpMethod} {path}");
+ var route = _router.MatchRoute(path);
+ Log.Info($"{req.HttpMethod} {path} {route}");
if (route is null) {
EmptyResponse.Create(ref res, HttpStatusCode.NotFound);
return;
@@ 30,7 40,7 @@ public class RoutesManager {
var methodName = req.HttpMethod[0] + req.HttpMethod.ToLower().Substring(1);
var method = route.GetType().GetMethod(methodName);
- method!.Invoke(route, [req, res]);
+ method!.Invoke(route, [req, res, new Context(null, null)]);
}
private void LoadRoutes() {
@@ 42,14 52,14 @@ public class RoutesManager {
.ForEach((type) => {
try {
var clazz = (AbstractRoute)Activator.CreateInstance(type);
- HttpRoutes[clazz.Path] = clazz;
+ _router.AddRoute(ref clazz, clazz.Path.Trim('/'));
Log.Info($"Loaded http route {type.Name} at {clazz.Path}");
}
catch {
Log.Warn($"Failed to load http route {type.Name}");
}
});
-
+
_httpServer.AddWebSocketService<ServerLogConnection>(ServerLogConnection.Path);
}
@@ 64,18 74,6 @@ public class RoutesManager {
});
}
- private void StartHttpServer() {
- Log.Info("Setting up HTTP server...");
- LoadRoutes();
-
- Log.Info("Registering HandleRequest events...");
- RegisterEvents();
-
- Log.Info("Starting HTTP server...");
- _httpServer.Start();
- Log.Info($"HTTP server listening on port {_httpServer.Port}");
- }
-
public void StopHttpServer() {
Log.Info("Stopping HTTP server...");
_httpServer.Stop();
M Routes/AbstractRoute.cs => Routes/AbstractRoute.cs +10 -10
@@ 6,24 6,24 @@ 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) {
+
+ 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) {
+ 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) {
+ 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) {
+ 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) {
+ 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", "*");
@@ 35,19 35,19 @@ public abstract class AbstractRoute {
EmptyResponse.Create(ref res, HttpStatusCode.MethodNotAllowed);
}
- public virtual void Post(ref HttpListenerRequest req, ref HttpListenerResponse res) {
+ 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) {
+ 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) {
+ 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) {
+ 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
M Routes/StatusRoute.cs => Routes/StatusRoute.cs +1 -1
@@ 10,7 10,7 @@ public class StatusRoute : AbstractRoute {
public override string Path => "/.well-known/mamoru/status";
public override bool Cors => true;
- public override void Get(ref HttpListenerRequest req, ref HttpListenerResponse res) {
+ public override void Get(ref HttpListenerRequest req, ref HttpListenerResponse res, ref Context ctx) {
var status = new Status(
true,
true,
A Utils/Router.cs => Utils/Router.cs +57 -0
@@ 0,0 1,57 @@
+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 string Path = path;
+ public readonly List<Node> Children = [];
+ 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, Node? parent = null) {
+ path = path.Trim('/');
+ parent ??= this;
+ var sep = path.IndexOf('/');
+ var seg = sep == -1 ? path : path.Substring(0, sep);
+ if (string.IsNullOrEmpty(seg)) {
+ return MatchRoute(path.Substring(sep + 1), parent);
+ }
+ var curr = parent.Find(seg);
+
+ if (curr is null) return null;
+ return sep == -1 ? curr.Route : MatchRoute(path.Substring(sep + 1), curr);
+ }
+}<
\ No newline at end of file
A global.json => global.json +7 -0
@@ 0,0 1,7 @@
+{
+ "sdk": {
+ "version": "9.0.0",
+ "rollForward": "latestMajor",
+ "allowPrerelease": true
+ }
+}<
\ No newline at end of file