~furry/mamoru-server

fbe157ed05f037a29962847229228cf556f91210 — nora 2 months ago 1dee0da
tree router
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