mc-server-manager/api/source/app.d

163 lines
4.7 KiB
D

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 = [];
}
}