module shared_utils.server_protocol; import std.socket; import std.algorithm; import std.conv; import std.array; import std.range; import std.bitmanip; import std.json; import std.stdio; import std.datetime; const ubyte SEGMENT_BITS = 0x7F; const ubyte CONTINUE_BIT = 0x80; /** * The data describing a minecraft server's status. */ struct ServerStatus { string name; bool online; int playersOnline; int maxPlayers; string[] playerNames; bool startupRequested = false; JSONValue toJsonObject() { JSONValue obj = JSONValue.emptyObject; obj.object["name"] = JSONValue(name); obj.object["online"] = JSONValue(online); obj.object["playersOnline"] = JSONValue(playersOnline); obj.object["maxPlayers"] = JSONValue(maxPlayers); obj.object["playerNames"] = JSONValue.emptyArray; foreach (name; playerNames) { obj.object["playerNames"].array ~= JSONValue(name); } obj.object["startupRequested"] = JSONValue(startupRequested); return obj; } } /** * Attempts to fetch the current server status by connecting to the server * with a status request packet. In the case of communication errors, the * server will be reported as offline. * Params: * name = The name of the server (only used to add to the ServerStatus struct). * ipAndPort = The combined IP:PORT string used to connect to the server. * Returns: The server status. */ ServerStatus fetchStatus(string name, 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; writeln("Got IP " ~ ip ~ " and port ", port); Address address = new InternetAddress(ip, port); writeln("Got address: ", address); Socket socket = new TcpSocket(); try { writeln("Socket created."); socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, seconds(5)); socket.setOption(SocketOptionLevel.SOCKET, SocketOption.SNDTIMEO, seconds(5)); socket.connect(address); writeln("opened socket"); } catch (SocketException e) { writeln(e); return ServerStatus(name, false, 0, -1, []); } writeln("About to send handshake packet..."); socket.send(getHandshakePacket(763, ip, port, 1)); writeln("About to send status packet..."); socket.send(getStatusPacket()); writeln("Sent status packet request."); ResponsePacket packet = readResponse(socket); writeln("Received packet response", packet.data.length); JSONValue statusJson = readStatusResponse(packet.data); return ServerStatus( name, true, cast(int) statusJson.object["players"].object["online"].integer, cast(int) statusJson.object["players"].object["max"].integer, [] ); } private struct ResponsePacket { int packetId; ubyte[] data; } private ResponsePacket readResponse(Socket socket) { ubyte[4096] buffer; ptrdiff_t initialBytesReceived = socket.receive(buffer); if (initialBytesReceived == Socket.ERROR || initialBytesReceived == 0) { throw new Exception("Couldn't read initial server response."); } ubyte[] bytes = buffer[0..initialBytesReceived].dup; int length = readVarInt(bytes); while (bytes.length < length) { ptrdiff_t bytesReceived = socket.receive(buffer); if (bytesReceived == Socket.ERROR || bytesReceived == 0) { throw new Exception("Failed to read more data from server."); } bytes ~= buffer[0..bytesReceived].dup; } ResponsePacket packet; packet.packetId = readVarInt(bytes); packet.data = bytes; return packet; } private ubyte[] getHandshakePacket(int protocolVersion, string ip, ushort port, int nextState) { Appender!(ubyte[]) dataApp; writeVarInt(dataApp, 0x00); writeVarInt(dataApp, protocolVersion); writeString(dataApp, ip); ubyte[2] shortData = nativeToBigEndian(port); dataApp ~= shortData[0]; dataApp ~= shortData[1]; writeVarInt(dataApp, nextState); size_t packetLength = dataApp[].length; Appender!(ubyte[]) packetLengthApp; writeVarInt(packetLengthApp, cast(int) packetLength); return packetLengthApp[] ~ dataApp[]; } private ubyte[] getStatusPacket() { return [1, 0]; } private JSONValue readStatusResponse(R)(ref R r) if (isInputRange!R && is(ElementType!R == ubyte)) { string jsonStr = readString(r); return parseJSON(jsonStr); } private int readVarInt(R)(ref R r) if (isInputRange!R && is(ElementType!R == ubyte)) { int value = 0; int position = 0; ubyte currentByte; while (true) { currentByte = r.front(); r.popFront(); 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; } private string readString(R)(ref R r) if (isInputRange!R && is(ElementType!R == ubyte)) { int length = readVarInt(r); Appender!(ubyte[]) app; for (size_t i = 0; i < length; i++) { if (r.empty()) { writefln!"Range empty after %d instead of expected %d."(i, length); break; } app ~= r.front(); r.popFront(); } return cast(string) app[]; } private void writeVarInt(R)(ref R r, int value) if (isOutputRange!(R, ubyte)) { while (true) { if ((value & ~SEGMENT_BITS) == 0) { r.put(cast(ubyte) value); return; } r.put(cast(ubyte) ((value & SEGMENT_BITS) | CONTINUE_BIT)); value >>>= 7; } } private void writeString(R)(ref R r, string s) if (isOutputRange!(R, ubyte)) { writeVarInt(r, cast(int) s.length); ubyte[] bytes = cast(ubyte[]) s; foreach (b; bytes) r.put(b); } unittest { void testReadWriteInt(int value, ubyte[] bytes) { Appender!(ubyte[]) app; writeVarInt(app, value); ubyte[] data = app[]; assert(data == bytes); int readValue = readVarInt(data); assert(readValue == value); } testReadWriteInt(0, [0]); testReadWriteInt(1, [1]); testReadWriteInt(2, [2]); testReadWriteInt(127, [127]); testReadWriteInt(128, [128, 1]); testReadWriteInt(255, [255, 1]); testReadWriteInt(25_565, [221, 199, 1]); testReadWriteInt(5181, [189, 40]); testReadWriteInt(5178, [186, 40]); }