2024-06-27 15:58:40 +00:00
|
|
|
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;
|
|
|
|
|
2024-08-04 23:06:24 +00:00
|
|
|
import startup_requests;
|
|
|
|
|
2024-08-03 18:15:41 +00:00
|
|
|
const AGENT_KEY_HEADER = "X-Agent-Key";
|
|
|
|
const CLIENT_KEY_HEADER = "X-Client-Key";
|
|
|
|
|
2024-06-27 15:58:40 +00:00
|
|
|
__gshared ServerStatus[] serverStatuses;
|
2024-08-04 23:06:24 +00:00
|
|
|
__gshared SysTime lastServerStatusTimestamp;
|
2024-06-27 15:58:40 +00:00
|
|
|
__gshared Mutex serversMutex;
|
2024-08-03 18:15:41 +00:00
|
|
|
__gshared ApiConfig config;
|
|
|
|
|
|
|
|
struct ApiConfig {
|
|
|
|
string agentKey;
|
|
|
|
string clientKey;
|
|
|
|
string discordWebhookUrl;
|
|
|
|
}
|
2024-06-27 15:58:40 +00:00
|
|
|
|
|
|
|
void main() {
|
2024-08-03 18:15:41 +00:00
|
|
|
config = readConfig();
|
|
|
|
|
2024-06-27 15:58:40 +00:00
|
|
|
auto provider = new DefaultProvider(false, Levels.INFO);
|
|
|
|
configureLoggingProvider(provider);
|
|
|
|
|
|
|
|
serversMutex = new Mutex();
|
2024-08-04 23:06:24 +00:00
|
|
|
lastServerStatusTimestamp = Clock.currTime();
|
|
|
|
|
|
|
|
// JobScheduler scheduler = JobScheduler.getDefault();
|
|
|
|
// scheduler.addJob(
|
|
|
|
// &removeOldStartupRequests,
|
|
|
|
// new FixedIntervalSchedule(minutes(5))
|
|
|
|
// );
|
|
|
|
// scheduler.addJob(
|
|
|
|
// &checkForOutOfDateServerStatus,
|
|
|
|
// new FixedIntervalSchedule(minutes(1))
|
|
|
|
// );
|
|
|
|
// scheduler.start();
|
2024-06-27 15:58:40 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
));
|
|
|
|
|
2024-08-03 18:15:41 +00:00
|
|
|
ServerConfig serverConfig;
|
|
|
|
serverConfig.connectionQueueSize = 20;
|
|
|
|
serverConfig.receiveBufferSize = 4096;
|
|
|
|
serverConfig.workerPoolSize = 3;
|
|
|
|
serverConfig.port = 8105;
|
|
|
|
HttpServer server = new HttpServer(handler, serverConfig);
|
2024-06-27 15:58:40 +00:00
|
|
|
server.start();
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Called when the agent posts the server status to us.
|
|
|
|
void postServerStatus(ref HttpRequestContext ctx) {
|
2024-08-03 18:15:41 +00:00
|
|
|
checkKey(ctx, AGENT_KEY_HEADER, config.agentKey);
|
|
|
|
|
2024-06-27 15:58:40 +00:00
|
|
|
JSONValue jsonBody = ctx.request.readBodyAsJson();
|
|
|
|
serversMutex.lock();
|
|
|
|
scope(exit) serversMutex.unlock();
|
|
|
|
serverStatuses = deserializeServerStatuses(jsonBody);
|
2024-08-04 23:06:24 +00:00
|
|
|
lastServerStatusTimestamp = Clock.currTime();
|
2024-06-27 15:58:40 +00:00
|
|
|
|
|
|
|
// Remove startup requests for any servers that are now online.
|
|
|
|
foreach (server; serverStatuses) {
|
|
|
|
if (server.online && isStartupRequested(server.identifier)) {
|
2024-08-04 23:06:24 +00:00
|
|
|
removeStartupRequest(server.identifier);
|
2024-06-27 15:58:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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) {
|
2024-08-03 18:15:41 +00:00
|
|
|
checkKey(ctx, CLIENT_KEY_HEADER, config.clientKey);
|
2024-06-27 15:58:40 +00:00
|
|
|
|
|
|
|
string identifier = ctx.request.getPathParamAs!string("id");
|
|
|
|
serversMutex.lock();
|
|
|
|
scope(exit) serversMutex.unlock();
|
|
|
|
foreach (server; serverStatuses) {
|
|
|
|
if (server.identifier == identifier) {
|
2024-08-04 23:06:24 +00:00
|
|
|
createServerStartupRequest(server.identifier);
|
2024-08-03 18:15:41 +00:00
|
|
|
sendDiscordMessage(
|
|
|
|
config.discordWebhookUrl,
|
|
|
|
format!"User requested to start server %s."(server.identifier)
|
|
|
|
);
|
2024-06-27 15:58:40 +00:00
|
|
|
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");
|
|
|
|
}
|
|
|
|
|
2024-08-03 18:15:41 +00:00
|
|
|
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"]
|
|
|
|
);
|
|
|
|
}
|
2024-08-04 23:06:24 +00:00
|
|
|
|
|
|
|
void checkForOutOfDateServerStatus() {
|
|
|
|
Duration statusAge = Clock.currTime() - lastServerStatusTimestamp;
|
|
|
|
serversMutex.lock();
|
|
|
|
scope(exit) serversMutex.unlock();
|
|
|
|
if (statusAge > minutes(15) && serverStatuses.length > 0) {
|
|
|
|
serverStatuses = [];
|
|
|
|
}
|
|
|
|
}
|