206 lines
6.4 KiB
D
206 lines
6.4 KiB
D
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]);
|
|
}
|