Added initial api implementation.

This commit is contained in:
Andrew Lalis 2024-06-25 21:01:35 -04:00
parent b38217df5e
commit 15a7adff7e
5 changed files with 216 additions and 0 deletions

1
.gitignore vendored
View File

@ -24,3 +24,4 @@ docs/
# Code coverage # Code coverage
*.lst *.lst
mc-server-manager

12
dub.json Normal file
View File

@ -0,0 +1,12 @@
{
"authors": [
"Andrew Lalis"
],
"copyright": "Copyright © 2024, Andrew Lalis",
"dependencies": {
"handy-httpd": "~>8.4.0"
},
"description": "Simple web server where players can see a list of available servers, player counts, and request to turn on a server if they have a passcode.",
"license": "proprietary",
"name": "mc-server-manager"
}

10
dub.selections.json Normal file
View File

@ -0,0 +1,10 @@
{
"fileVersion": 1,
"versions": {
"handy-httpd": "8.4.0",
"httparsed": "1.2.1",
"path-matcher": "1.2.0",
"slf4d": "3.0.1",
"streams": "3.5.0"
}
}

76
source/app.d Normal file
View File

@ -0,0 +1,76 @@
import handy_httpd;
import handy_httpd.handlers.path_handler;
import handy_httpd.components.optional;
import std.json;
import std.stdio;
import core.sync.mutex;
import server_protocol;
__gshared JSONValue latestServerData = JSONValue.emptyArray;
__gshared Mutex dataMutex;
__gshared string clientKey;
__gshared string serverKey;
void main(string[] args) {
clientKey = args[1];
serverKey = args[2];
dataMutex = new Mutex();
PathHandler handler = new PathHandler();
handler.addMapping(Method.GET, "/servers", &listServers);
handler.addMapping(Method.POST, "/servers", &postServerStatus);
handler.addMapping(Method.POST, "/servers/:name/requests", &requestServerStartup);
ServerConfig config;
config.connectionQueueSize = 20;
config.receiveBufferSize = 4096;
config.workerPoolSize = 3;
HttpServer server = new HttpServer(handler, config);
server.start();
}
void listServers(ref HttpRequestContext ctx) {
dataMutex.lock();
ctx.response.writeBodyString(latestServerData.toJSON(), "application/json");
dataMutex.unlock();
}
void postServerStatus(ref HttpRequestContext ctx) {
Optional!string key = ctx.request.headers.getFirst("X-Server-Key");
if (!key || key.value != serverKey) {
ctx.response.status = HttpStatus.UNAUTHORIZED;
return;
}
JSONValue data = ctx.request.readBodyAsJson();
dataMutex.lock();
latestServerData = data;
writeln("Set server status to ", data.toJSON());
dataMutex.unlock();
}
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 serverName = ctx.request.getPathParamAs!string("name");
writeln(serverName);
dataMutex.lock();
scope(exit) {
dataMutex.unlock();
}
foreach (JSONValue serverNode; latestServerData.array) {
if (serverNode.object["name"].str == serverName) {
writeln("Found match!");
serverNode.object["requested"] = JSONValue(true);
return;
}
}
ctx.response.status = HttpStatus.NOT_FOUND;
}

117
source/server_protocol.d Normal file
View File

@ -0,0 +1,117 @@
module server_protocol;
import std.socket;
import std.algorithm;
import std.conv;
import std.stdio;
import streams;
const int SEGMENT_BITS = 0x7F;
const int CONTINUE_BIT = 0x80;
struct ServerStatus {
bool online;
int playersOnline;
int maxPlayers;
string[] playerNames;
}
ServerStatus fetchStatus(string ipAndPort) {
ptrdiff_t portIdx = countUntil(ipAndPort, ":");
if (portIdx == -1) throw new Exception("Invalid IP address. Port is required.");
string ip = ipAndPort[0..portIdx];
ushort port = ipAndPort[portIdx + 1 .. $].to!ushort;
Address address = new InternetAddress(ip, port);
writeln(address);
Socket socket = new TcpSocket(address);
auto sIn = SocketInputStream(socket);
auto sOut = SocketOutputStream(socket);
auto bufferedOut = bufferedOutputStreamFor(sOut);
auto arrayOut = byteArrayOutputStream();
writeVarInt(&arrayOut, 0x00);
writeVarInt(&arrayOut, 763);
writeString(&arrayOut, ip);
auto dOut = dataOutputStreamFor(&arrayOut, Endianness.BigEndian);
dOut.writeToStream(port);
writeVarInt(&arrayOut, 1);
ubyte[] handshakePacket = arrayOut.toArray();
writeln(handshakePacket);
writeVarInt(&bufferedOut, cast(int) handshakePacket.length);
bufferedOut.writeToStream(handshakePacket);
bufferedOut.flushStream();
writeln("Sent handshake packet.");
auto arrayOut2 = byteArrayOutputStream();
writeVarInt(arrayOut2, 0x00);
ubyte[] statusRequestPacket = arrayOut2.toArray();
writeVarInt(bufferedOut, cast(int) statusRequestPacket.length);
bufferedOut.writeToStream(statusRequestPacket);
bufferedOut.flushStream();
writeln("Sent status request packet.");
int responsePacketSize = readVarInt(sIn);
writefln!"Got response of %d bytes"(responsePacketSize);
ubyte[] packetIdAndData = new ubyte[responsePacketSize];
StreamResult result = sIn.readFromStream(packetIdAndData);
if (result.hasError || result.count != responsePacketSize) throw new Exception("Failed to read response packet.");
auto packetIn = arrayInputStreamFor(packetIdAndData);
int packetId = readVarInt(packetIn);
if (packetId != 0x00) throw new Exception("Received invalid packetId when receiving status response.");
string jsonStr = readString(packetIn);
writeln(jsonStr);
ServerStatus status;
return status;
}
int readVarInt(S)(S s) if (isByteInputStream!S) {
int value = 0;
int position = 0;
ubyte[1] buf;
while (true) {
writeln("Attempting to read from stream...");
StreamResult result = s.readFromStream(buf);
writeln(result);
if (result.hasError) throw new Exception(cast(string) result.error.message);
ubyte currentByte = buf[0];
value |= (currentByte & SEGMENT_BITS) << position;
if ((currentByte & CONTINUE_BIT) == 0) break;
position += 7;
if (position >= 32) throw new Exception("VarInt is too big.");
}
return value;
}
void writeVarInt(S)(S s, int value) if (isByteOutputStream!S) {
while (true) {
if ((value & ~SEGMENT_BITS) == 0) {
StreamResult r = s.writeToStream([cast(ubyte) value]);
if (r.hasError || r.count != 1) throw new Exception("Failed to write byte to stream");
return;
}
StreamResult r = s.writeToStream([(value & SEGMENT_BITS) | CONTINUE_BIT]);
if (r.hasError || r.count != 1) throw new Exception("Failed to write byte to stream");
value >>>= 7;
}
}
string readString(S)(S s) if (isByteInputStream!S) {
int length = readVarInt(s);
ubyte[] data = new ubyte[length];
StreamResult result = s.readFromStream(data);
if (result.hasError || result.count != length) throw new Exception("Couldn't read string.");
return cast(string) data.idup;
}
void writeString(S)(S s, string str) if (isByteOutputStream!S) {
ubyte[] bytes = cast(ubyte[]) str;
writeVarInt(s, cast(int) bytes.length);
s.writeToStream(bytes);
}