import handy_httpd; import handy_httpd.handlers.path_handler; import handy_httpd.handlers.file_resolving_handler; import handy_httpd.components.optional; import slf4d; import slf4d.default_provider; import std.json; import std.stdio; import std.file; import std.algorithm; import std.string; import std.array; import std.format; import std.datetime; import core.sync.mutex; import shared_utils.server_status; import shared_utils.discord; __gshared ServerStatus[] serverStatuses; __gshared Mutex serversMutex; __gshared string agentKey = "abc"; __gshared string clientKey = "abc"; void main() { auto provider = new DefaultProvider(false, Levels.INFO); configureLoggingProvider(provider); serversMutex = new Mutex(); PathHandler handler = new PathHandler(); handler.addMapping(Method.POST, "/api/servers", &postServerStatus); handler.addMapping(Method.GET, "/api/servers", &listServers); handler.addMapping(Method.POST, "/api/servers/:id/requests", &requestServerStartup); handler.addMapping(Method.GET, "/api/server-requests", &getServerRequests); handler.addMapping(Method.GET, "/**", new FileResolvingHandler( "app", DirectoryResolutionStrategies.serveIndexFiles )); ServerConfig config; config.connectionQueueSize = 20; config.receiveBufferSize = 4096; config.workerPoolSize = 3; config.port = 8105; HttpServer server = new HttpServer(handler, config); server.start(); } /// Called when the agent posts the server status to us. void postServerStatus(ref HttpRequestContext ctx) { Optional!string key = ctx.request.headers.getFirst("X-Agent-Key"); if (!key || key.value != agentKey) { ctx.response.status = HttpStatus.UNAUTHORIZED; return; } JSONValue jsonBody = ctx.request.readBodyAsJson(); serversMutex.lock(); scope(exit) serversMutex.unlock(); serverStatuses = deserializeServerStatuses(jsonBody); // Remove startup requests for any servers that are now online. foreach (server; serverStatuses) { if (server.online && isStartupRequested(server.identifier)) { std.file.remove("request_" ~ server.identifier ~ ".txt"); } } } /// Called by the web app when a user is refreshing the list of servers. void listServers(ref HttpRequestContext ctx) { serversMutex.lock(); scope(exit) serversMutex.unlock(); JSONValue payload = serializeServerStatuses(serverStatuses); ctx.response.writeBodyString(payload.toJSON, "application/json"); } /// Called by a user when they request to start a server. void requestServerStartup(ref HttpRequestContext ctx) { Optional!string key = ctx.request.headers.getFirst("X-Client-Key"); if (!key || key.value != clientKey) { ctx.response.status = HttpStatus.UNAUTHORIZED; return; } string identifier = ctx.request.getPathParamAs!string("id"); serversMutex.lock(); scope(exit) serversMutex.unlock(); foreach (server; serverStatuses) { if (server.identifier == identifier) { File f = File("request_" ~ identifier ~ ".txt", "w"); f.writeln(Clock.currTime().toISOExtString()); f.close(); sendDiscordMessage(format!"User requested to start server %s."(server.identifier)); return; } } ctx.response.status = HttpStatus.NOT_FOUND; } /// Called by the agent to get the list of servers to start. void getServerRequests(ref HttpRequestContext ctx) { JSONValue result = JSONValue.emptyArray; serversMutex.lock(); scope(exit) serversMutex.unlock(); foreach (server; serverStatuses) { if (isStartupRequested(server.identifier)) { result.array ~= JSONValue(server.identifier); } } ctx.response.writeBodyString(result.toJSON, "application/json"); } bool isStartupRequested(string id) { return std.file.exists("request_" ~ id ~ ".txt"); }