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 scheduled; 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; import startup_requests; const AGENT_KEY_HEADER = "X-Agent-Key"; const CLIENT_KEY_HEADER = "X-Client-Key"; __gshared ServerStatus[] serverStatuses; __gshared SysTime lastServerStatusTimestamp; __gshared Mutex serversMutex; __gshared ApiConfig config; struct ApiConfig { string agentKey; string clientKey; string discordWebhookUrl; } void main() { config = readConfig(); auto provider = new DefaultProvider(false, Levels.INFO); configureLoggingProvider(provider); serversMutex = new Mutex(); lastServerStatusTimestamp = Clock.currTime(); JobScheduler scheduler = JobScheduler.getDefault(); scheduler.addJob( &removeOldStartupRequests, new FixedIntervalSchedule(minutes(5)) ); scheduler.addJob( &checkForOutOfDateServerStatus, new FixedIntervalSchedule(minutes(1)) ); scheduler.start(); 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 serverConfig; serverConfig.connectionQueueSize = 20; serverConfig.receiveBufferSize = 4096; serverConfig.workerPoolSize = 3; serverConfig.port = 8105; HttpServer server = new HttpServer(handler, serverConfig); server.start(); } /// Called when the agent posts the server status to us. void postServerStatus(ref HttpRequestContext ctx) { checkKey(ctx, AGENT_KEY_HEADER, config.agentKey); JSONValue jsonBody = ctx.request.readBodyAsJson(); serversMutex.lock(); scope(exit) serversMutex.unlock(); serverStatuses = deserializeServerStatuses(jsonBody); lastServerStatusTimestamp = Clock.currTime(); // Remove startup requests for any servers that are now online. foreach (server; serverStatuses) { if (server.online && isStartupRequested(server.identifier)) { removeStartupRequest(server.identifier); } } } /// 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) { checkKey(ctx, CLIENT_KEY_HEADER, config.clientKey); string identifier = ctx.request.getPathParamAs!string("id"); serversMutex.lock(); scope(exit) serversMutex.unlock(); foreach (server; serverStatuses) { if (server.identifier == identifier) { createServerStartupRequest(server.identifier); sendDiscordMessage( config.discordWebhookUrl, 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"); } void checkKey(ref HttpRequestContext ctx, string keyHeaderName, string expectedValue) { Optional!string key = ctx.request.headers.getFirst(keyHeaderName); if (!key || key.value != expectedValue) { throw new HttpStatusException(HttpStatus.UNAUTHORIZED, "Invalid or missing key."); } } ApiConfig readConfig() { import properd; import std.file : exists; if (!exists("api-config.properties")) throw new Exception("Missing api-config.properties"); auto props = readProperties("api-config.properties"); return ApiConfig( props["agentKey"], props["clientKey"], props["discordWebhookUrl"] ); } void checkForOutOfDateServerStatus() { Duration statusAge = Clock.currTime() - lastServerStatusTimestamp; serversMutex.lock(); scope(exit) serversMutex.unlock(); if (statusAge > minutes(15) && serverStatuses.length > 0) { serverStatuses = []; } }