mirror of
https://github.com/AndrioCelos/TableturfBattleApp.git
synced 2026-03-22 01:44:12 -05:00
151 lines
6.4 KiB
C#
151 lines
6.4 KiB
C#
using System.Net;
|
|
using System.Reflection;
|
|
using System.Web;
|
|
using WebSocketSharp.Server;
|
|
using HttpListenerRequest = WebSocketSharp.Net.HttpListenerRequest;
|
|
using HttpListenerResponse = WebSocketSharp.Net.HttpListenerResponse;
|
|
|
|
namespace TableturfBattleServer;
|
|
|
|
internal delegate void ApiEndpointGlobalHandler(HttpListenerRequest request, HttpListenerResponse response);
|
|
internal delegate void ApiEndpointGameHandler(Game game, HttpListenerRequest request, HttpListenerResponse response);
|
|
|
|
internal partial class Program {
|
|
internal static HttpServer? httpServer;
|
|
|
|
private static readonly Dictionary<string, (ApiEndpointAttribute attribute, ApiEndpointGlobalHandler handler)> apiGlobalHandlers = [];
|
|
private static readonly Dictionary<string, (ApiEndpointAttribute attribute, ApiEndpointGameHandler handler)> apiGameHandlers = [];
|
|
private static readonly HashSet<string> spaPaths = [ "/", "/deckeditor", "/cardlist", "/game" , "/replay" ];
|
|
|
|
private static string? GetClientRootPath() {
|
|
var directory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
|
while (true) {
|
|
if (directory == null) return null;
|
|
var directory2 = Path.Combine(directory, "TableturfBattleClient");
|
|
if (Directory.Exists(directory2)) return directory2;
|
|
directory = Path.GetDirectoryName(directory);
|
|
}
|
|
}
|
|
|
|
internal static void Main(string[] args) {
|
|
foreach (var method in typeof(ApiEndpoints).GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)) {
|
|
var attribute = method.GetCustomAttribute<ApiEndpointAttribute>();
|
|
if (attribute == null) continue;
|
|
if (attribute.Namespace == ApiEndpointNamespace.ApiRoot)
|
|
apiGlobalHandlers[attribute.Path] = (attribute, method.CreateDelegate<ApiEndpointGlobalHandler>());
|
|
else
|
|
apiGameHandlers[attribute.Path] = (attribute, method.CreateDelegate<ApiEndpointGameHandler>());
|
|
}
|
|
|
|
httpServer = new(args.Contains("--open") ? IPAddress.Any : IPAddress.Loopback, 3333) { DocumentRootPath = GetClientRootPath() };
|
|
|
|
httpServer.AddWebSocketService<TableturfWebSocketBehaviour>("/api/websocket");
|
|
httpServer.OnGet += HttpServer_OnRequest;
|
|
httpServer.OnPost += HttpServer_OnRequest;
|
|
httpServer.Start();
|
|
Console.WriteLine($"Listening on http://{httpServer.Address}:{httpServer.Port}");
|
|
if (httpServer.DocumentRootPath != null)
|
|
Console.WriteLine($"Serving client files from {httpServer.DocumentRootPath}");
|
|
else
|
|
Console.WriteLine($"Client files were not found.");
|
|
|
|
while (true) {
|
|
var s = Console.ReadLine();
|
|
if (s == null)
|
|
Thread.Sleep(Timeout.Infinite);
|
|
else {
|
|
s = s.Trim().ToLower();
|
|
if (s == "update") {
|
|
if (Server.Instance.games.Count == 0)
|
|
Environment.Exit(2);
|
|
Server.Instance.Lockdown = true;
|
|
Console.WriteLine("Locking server for update.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void HttpServer_OnRequest(object? sender, HttpRequestEventArgs e) {
|
|
e.Response.AppendHeader("Access-Control-Allow-Origin", "*");
|
|
if (!e.Request.RawUrl.StartsWith('/')) {
|
|
e.Response.SetErrorResponse(new(HttpStatusCode.BadRequest, "InvalidRequestUrl", "Invalid request URL."));
|
|
return;
|
|
}
|
|
|
|
if (!e.Request.RawUrl.StartsWith("/api/")) {
|
|
// Static files
|
|
if (e.Request.HttpMethod is not ("GET" or "HEAD")) {
|
|
e.Response.AddHeader("Allow", "GET, HEAD");
|
|
e.Response.SetErrorResponse(new(HttpStatusCode.MethodNotAllowed, "MethodNotAllowed", "Invalid request method for this endpoint."));
|
|
return;
|
|
}
|
|
var pos = e.Request.RawUrl.IndexOf('/', 1);
|
|
var topLevelFileName = pos < 0 ? e.Request.RawUrl : e.Request.RawUrl[..pos];
|
|
var path = spaPaths.Contains(topLevelFileName) ? "index.html" : HttpUtility.UrlDecode(e.Request.RawUrl[1..]);
|
|
if (e.TryReadFile(path, out var bytes))
|
|
e.Response.SetResponse(HttpStatusCode.OK, Path.GetExtension(path) switch {
|
|
".html" or ".htm" => "text/html",
|
|
".css" => "text/css",
|
|
".js" => "text/javascript",
|
|
".png" => "image/png",
|
|
".tar" => "application/x-tar",
|
|
".webp" => "image/webp",
|
|
".woff" or ".woff2" => "font/woff",
|
|
_ => "application/octet-stream"
|
|
}, bytes);
|
|
else
|
|
e.Response.SetErrorResponse(new(HttpStatusCode.NotFound, "NotFound", "File not found."));
|
|
} else {
|
|
var pos = e.Request.RawUrl.IndexOf('?', 5);
|
|
var path = pos < 0 ? e.Request.RawUrl[4..] : e.Request.RawUrl[4..pos];
|
|
if (apiGlobalHandlers.TryGetValue(path, out var entry)) {
|
|
if ((e.Request.HttpMethod == "HEAD" ? "GET" : e.Request.HttpMethod) != entry.attribute.AllowedMethod) {
|
|
e.Response.AddHeader("Allow", entry.attribute.AllowedMethod == "GET" ? "GET, HEAD" : entry.attribute.AllowedMethod);
|
|
e.Response.SetErrorResponse(new(HttpStatusCode.MethodNotAllowed, "MethodNotAllowed", "Invalid request method for this endpoint."));
|
|
return;
|
|
}
|
|
if (e.Request.ContentLength64 >= 65536) {
|
|
e.Response.SetErrorResponse(new(HttpStatusCode.RequestEntityTooLarge, "ContentTooLarge", "Request content is too large."));
|
|
return;
|
|
}
|
|
entry.handler(e.Request, e.Response);
|
|
} else {
|
|
if (!path.StartsWith("/games/")) {
|
|
e.Response.SetErrorResponse(new(HttpStatusCode.NotFound, "NotFound", "Endpoint not found."));
|
|
return;
|
|
}
|
|
pos = path.IndexOf('/', 7);
|
|
var gameIdString = path[7..(pos < 0 ? ^0 : pos)];
|
|
path = pos < 0 ? "/" : path[pos..];
|
|
if (!Guid.TryParse(gameIdString, out var gameID)) {
|
|
e.Response.SetErrorResponse(new(HttpStatusCode.BadRequest, "InvalidGameID", "Invalid game ID."));
|
|
return;
|
|
}
|
|
|
|
lock (Server.Instance.games) {
|
|
if (!Server.Instance.TryGetGame(gameID, out var game)) {
|
|
e.Response.SetErrorResponse(new(HttpStatusCode.NotFound, "GameNotFound", "Game not found."));
|
|
return;
|
|
}
|
|
lock (game.Players) {
|
|
if (!apiGameHandlers.TryGetValue(path, out var entry2)) {
|
|
e.Response.SetErrorResponse(new(HttpStatusCode.NotFound, "NotFound", "Endpoint not found."));
|
|
return;
|
|
}
|
|
if ((e.Request.HttpMethod == "HEAD" ? "GET" : e.Request.HttpMethod) != entry2.attribute.AllowedMethod) {
|
|
e.Response.AddHeader("Allow", entry2.attribute.AllowedMethod == "GET" ? "GET, HEAD" : entry2.attribute.AllowedMethod);
|
|
e.Response.SetErrorResponse(new(HttpStatusCode.MethodNotAllowed, "MethodNotAllowed", "Invalid request method for this endpoint."));
|
|
return;
|
|
}
|
|
if (e.Request.ContentLength64 >= 65536) {
|
|
e.Response.SetErrorResponse(new(HttpStatusCode.RequestEntityTooLarge, "ContentTooLarge", "Request content is too large."));
|
|
return;
|
|
}
|
|
entry2.handler(game, e.Request, e.Response);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|