Added initial api implementation.
This commit is contained in:
parent
b38217df5e
commit
15a7adff7e
|
@ -24,3 +24,4 @@ docs/
|
||||||
# Code coverage
|
# Code coverage
|
||||||
*.lst
|
*.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