Added initial api implementation.
This commit is contained in:
parent
b38217df5e
commit
15a7adff7e
|
@ -24,3 +24,4 @@ docs/
|
|||
# Code coverage
|
||||
*.lst
|
||||
|
||||
mc-server-manager
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
Loading…
Reference in New Issue