diff --git a/.gitignore b/.gitignore index 2758450..6f6f59f 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ docs/ # Code coverage *.lst +mc-server-manager diff --git a/dub.json b/dub.json new file mode 100644 index 0000000..55d3db0 --- /dev/null +++ b/dub.json @@ -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" +} \ No newline at end of file diff --git a/dub.selections.json b/dub.selections.json new file mode 100644 index 0000000..07cab5f --- /dev/null +++ b/dub.selections.json @@ -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" + } +} diff --git a/source/app.d b/source/app.d new file mode 100644 index 0000000..39195d3 --- /dev/null +++ b/source/app.d @@ -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; +} diff --git a/source/server_protocol.d b/source/server_protocol.d new file mode 100644 index 0000000..b9507bc --- /dev/null +++ b/source/server_protocol.d @@ -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); +}