mc-server-manager/shared-utils/source/shared_utils/server_protocol.d

206 lines
6.4 KiB
D
Raw Permalink Normal View History

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]);
}