Skip to content

Commit

Permalink
clean up everything, add challenge token check
Browse files Browse the repository at this point in the history
  • Loading branch information
KurtThiemann committed Sep 16, 2023
1 parent 4899220 commit 8260fb3
Show file tree
Hide file tree
Showing 12 changed files with 395 additions and 147 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ buildscript {

apply plugin: 'foxloader.dev'

version '1.0.1'
version '1.1.0'

foxloader {
// forceReload = true
Expand Down
5 changes: 3 additions & 2 deletions src/server/java/io/thiemann/kurt/query/QueryModServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public void onServerStart(NetworkPlayer.ConnectionType connectionType) {
boolean queryEnabled = props.getBooleanProperty("enable-query", false);

if (!queryEnabled) {
getLogger().info("Query server disabled");
getLogger().info("Query server is disabled");
return;
}

Expand All @@ -33,7 +33,8 @@ public void onServerStart(NetworkPlayer.ConnectionType connectionType) {
if (queryAddressString.length() > 0) {
try {
inetAddress = InetAddress.getByName(queryAddressString);
} catch (UnknownHostException ignore) {}
} catch (UnknownHostException ignore) {
}
}

try {
Expand Down
227 changes: 83 additions & 144 deletions src/server/java/io/thiemann/kurt/query/query/QueryServer.java
Original file line number Diff line number Diff line change
@@ -1,198 +1,137 @@
package io.thiemann.kurt.query.query;

import com.fox2code.foxloader.network.NetworkPlayer;
import io.thiemann.kurt.query.query.packet.*;
import net.minecraft.server.MinecraftServer;
import org.luaj.vm2.ast.Str;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;

//Async udp server to handle Minecraft query requests
import java.net.*;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;


public class QueryServer {
private static final byte[] challenge = "9513307\0".getBytes();
private final MinecraftServer server;
private final DatagramSocket socket;
private final Thread thread;
private final Map<String, Integer> challenges = new ConcurrentHashMap<>();
private long lastChallengeClear = 0;

public QueryServer(MinecraftServer server, int port, InetAddress laddr) throws SocketException {
this.server = server;
this.socket = new DatagramSocket(port, laddr);
this.thread = new Thread(this::run);
this.thread.start();

}

private void run() {
while (!this.socket.isClosed()) {
if (System.currentTimeMillis() - this.lastChallengeClear > 30000) {
this.challenges.clear();
this.lastChallengeClear = System.currentTimeMillis();
}

try {
byte[] buf = new byte[1024];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
this.socket.receive(packet);
this.handlePacket(packet);
} catch (IOException ignored) {}
} catch (IOException ignored) {
}
}
}

/**
* Handle incoming packet
* <a href="https://wiki.vg/Query#Client_to_Server_Packet_Format">Packet format</a>
*
* @param packet incoming packet
* @param msg incoming packet
*/
private void handlePacket(DatagramPacket packet) {
ByteBuffer buffer = ByteBuffer.wrap(packet.getData());
buffer.order(ByteOrder.BIG_ENDIAN);
short magic = buffer.getShort();
if (magic != (short)0xFEFD) {
private void handlePacket(DatagramPacket msg) {
ServerBoundPacket packet;
try {
packet = ServerBoundPacket.fromBuffer(msg.getData(), msg.getLength());
} catch (QueryProtocolException e) {
return;
}
byte type = buffer.get();
int sessionId = buffer.getInt();
if (type == 9) {
//send handshake response
byte[] response = makeHandshakeResponse(sessionId);
DatagramPacket responsePacket = new DatagramPacket(response, response.length, packet.getAddress(), packet.getPort());
try {
this.socket.send(responsePacket);
} catch (IOException ignored) {}
}

if(type != 0) {
ClientBoundPacket response;
if (packet instanceof ServerBoundHandshakePacket) {
response = this.handshake((ServerBoundHandshakePacket) packet, msg.getSocketAddress());
} else if (packet instanceof ServerBoundStatPacket) {
if (((ServerBoundStatPacket) packet).isFull()) {
response = this.fullStat((ServerBoundStatPacket) packet, msg.getSocketAddress());
} else {
response = this.basicStat((ServerBoundStatPacket) packet, msg.getSocketAddress());
}
} else {
return;
}

//get payload as byte array
byte[] payload = new byte[packet.getLength() - 7];
boolean isFull = payload.length == 8;

byte[] response;
if(isFull) {
//send full stat response
response = makeFullStatResponse(sessionId);
} else {
//send basic stat response
response = makeBasicStatResponse(sessionId);
if (response == null) {
return;
}

DatagramPacket responsePacket = new DatagramPacket(response, response.length, packet.getAddress(), packet.getPort());
byte[] payload = response.serialize();
DatagramPacket responsePacket = new DatagramPacket(payload, payload.length, msg.getSocketAddress());
try {
this.socket.send(responsePacket);
} catch (IOException ignored) {}
} catch (IOException ignored) {
}
}

/**
* Generate a Handshake response
* <a href="https://wiki.vg/Query#Response">Packet format</a>
* Instead of an actual challenge we just send a constant value
*
* @param sessionId session id
* @return payload
*/
private byte[] makeHandshakeResponse(int sessionId) {
ByteBuffer buffer = ByteBuffer.allocate(13);
buffer.order(ByteOrder.BIG_ENDIAN);
buffer.put((byte)9);
buffer.putInt(sessionId);
buffer.put(challenge);
return buffer.array();
}
private ClientBoundHandshakePacket handshake(ServerBoundHandshakePacket packet, SocketAddress address) {
int randomToken = ((int) (Math.random() * 1000000)) & 0x0F0F0F0F;
this.challenges.put(address.toString(), randomToken);

/**
* Generate a basic stat response
* <a href="https://wiki.vg/Query#Response_2">Packet format</a>
*
* @param sessionId session id
* @return payload
*/
private byte[] makeBasicStatResponse(int sessionId) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.order(ByteOrder.BIG_ENDIAN);
buffer.put((byte)0);
buffer.putInt(sessionId);
buffer.put((this.server.getMotd() + "\0").getBytes(StandardCharsets.UTF_8));
buffer.put("SMP\0".getBytes());
buffer.put("world\0".getBytes());
buffer.put((this.server.configManager.playersOnline() + "\0").getBytes());
buffer.put((this.server.configManager.getMaxPlayers() + "\0").getBytes());
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.putShort((short)this.getMinecraftServerPort());
buffer.put((this.getMinecraftServerIp() + "\0").getBytes());

//only return written length as byte array
byte[] response = new byte[buffer.position()];
((Buffer) buffer).rewind();
buffer.get(response);
return response;
return new ClientBoundHandshakePacket(packet.getSessionId(), randomToken);
}

/**
* Generate a full stat response
* <a href="https://wiki.vg/Query#Response_3">Packet format</a>
*
* @param sessionId session id
* @return payload
*/
private byte[] makeFullStatResponse(int sessionId) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.order(ByteOrder.BIG_ENDIAN);
buffer.put((byte)0);
buffer.putInt(sessionId);
buffer.put(new byte[] {0x73, 0x70, 0x6C, 0x69, 0x74, 0x6E, 0x75, 0x6D, 0x00, (byte)0x80, 0x00});

buffer.put("hostname\0".getBytes());
buffer.put((this.server.getMotd() + "\0").getBytes(StandardCharsets.UTF_8));

buffer.put("gametype\0".getBytes());
buffer.put("SMP\0".getBytes());

buffer.put("game_id\0".getBytes());
buffer.put("MINECRAFT\0".getBytes());

buffer.put("version\0".getBytes());
buffer.put("Beta 1.7\0".getBytes());

buffer.put("plugins\0".getBytes());
buffer.put("FoxLoader\0".getBytes());

buffer.put("map\0".getBytes());
buffer.put("world\0".getBytes());

buffer.put("numplayers\0".getBytes());
buffer.put((this.server.configManager.playersOnline() + "\0").getBytes());

buffer.put("maxplayers\0".getBytes());
buffer.put((this.server.configManager.getMaxPlayers() + "\0").getBytes());

buffer.put("hostport\0".getBytes());
buffer.put((this.getMinecraftServerPort() + "\0").getBytes());

buffer.put("hostip\0".getBytes());
buffer.put((this.getMinecraftServerIp() + "\0").getBytes());

buffer.put((byte)0x00);
private ClientBoundBasicStatPacket basicStat(ServerBoundStatPacket packet, SocketAddress address) {
if (!this.challenges.containsKey(address.toString())) {
return null;
}

buffer.put(new byte[] {0x01, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x5F, 0x00, 0x00});
if (packet.getChallenge() != this.challenges.get(address.toString())) {
return null;
}

this.server.configManager.playerEntities.forEach((player) -> {
System.out.println(player.getPlayerName());
buffer.put((player.getPlayerName() + "\0").getBytes(StandardCharsets.UTF_8));
});
return new ClientBoundBasicStatPacket(
packet.getSessionId(),
this.server.getMotd(),
"SMP",
"world",
this.server.configManager.playersOnline(),
this.server.configManager.getMaxPlayers(),
this.getMinecraftServerPort(),
this.getMinecraftServerIp()
);
}

private ClientBoundFullStatPacket fullStat(ServerBoundStatPacket packet, SocketAddress address) {
if (!this.challenges.containsKey(address.toString())) {
return null;
}

buffer.put((byte)0x00);
if (packet.getChallenge() != this.challenges.get(address.toString())) {
System.out.println("Wrong challenge: " + packet.getChallenge() + " != " + this.challenges.get(address.toString()) + " (" + address.toString() + ")");
return null;
}

//only return written length as byte array
byte[] response = new byte[buffer.position()];
((Buffer) buffer).rewind();
buffer.get(response);
return response;
return new ClientBoundFullStatPacket(
packet.getSessionId(),
this.server.getMotd(),
"SMP",
"MINECRAFT",
"Beta 1.7",
"FoxLoader",
"world",
this.server.configManager.playersOnline(),
this.server.configManager.getMaxPlayers(),
this.getMinecraftServerPort(),
this.getMinecraftServerIp(),
this.server.configManager.playerEntities.stream().map(NetworkPlayer::getPlayerName).toArray(String[]::new)
);
}

private int getMinecraftServerPort() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.thiemann.kurt.query.query.packet;

import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;

public class ClientBoundBasicStatPacket extends ClientBoundPacket {

private final String motd;
private final String gameType;
private final String map;
private final int playersOnline;
private final int maxPlayers;
private final int port;
private final String host;

public ClientBoundBasicStatPacket(int sessionId, String motd, String gameType, String map, int playersOnline, int maxPlayers, int port, String host) {
super(PacketType.STAT, sessionId);
this.motd = motd;
this.gameType = gameType;
this.map = map;
this.playersOnline = playersOnline;
this.maxPlayers = maxPlayers;
this.port = port;
this.host = host;
}

@Override
protected byte[] serializePayload() {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.order(ByteOrder.LITTLE_ENDIAN);

buffer.put((this.motd + "\0").getBytes(StandardCharsets.UTF_8));
buffer.put((this.gameType + "\0").getBytes(StandardCharsets.UTF_8));
buffer.put((this.map + "\0").getBytes(StandardCharsets.UTF_8));
buffer.put((this.playersOnline + "\0").getBytes(StandardCharsets.UTF_8));
buffer.put((this.maxPlayers + "\0").getBytes(StandardCharsets.UTF_8));
buffer.putShort((short) this.port);
buffer.put((this.host + "\0").getBytes(StandardCharsets.UTF_8));

//only return written length as byte array
byte[] response = new byte[buffer.position()];
((Buffer) buffer).rewind();
buffer.get(response);
return response;
}
}
Loading

0 comments on commit 8260fb3

Please sign in to comment.