package net.minecraft.server.rcon.thread; import com.google.common.collect.Maps; import com.mojang.logging.LogUtils; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.PortUnreachableException; import java.net.SocketAddress; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.Locale; import java.util.Map; import javax.annotation.Nullable; import net.minecraft.Util; import net.minecraft.server.ServerInterface; import net.minecraft.server.rcon.NetworkDataOutputStream; import net.minecraft.server.rcon.PktUtils; import net.minecraft.util.RandomSource; import org.slf4j.Logger; public class QueryThreadGs4 extends GenericThread { private static final Logger LOGGER = LogUtils.getLogger(); private static final String GAME_TYPE = "SMP"; private static final String GAME_ID = "MINECRAFT"; private static final long CHALLENGE_CHECK_INTERVAL = 30000L; private static final long RESPONSE_CACHE_TIME = 5000L; private long lastChallengeCheck; private final int port; private final int serverPort; private final int maxPlayers; private final String serverName; private final String worldName; private DatagramSocket socket; private final byte[] buffer = new byte[1460]; private String hostIp; private String serverIp; private final Map validChallenges; private final NetworkDataOutputStream rulesResponse; private long lastRulesResponse; private final ServerInterface serverInterface; private QueryThreadGs4(ServerInterface p_11541_, int p_11542_) { super("Query Listener"); this.serverInterface = p_11541_; this.port = p_11542_; this.serverIp = p_11541_.getServerIp(); this.serverPort = p_11541_.getServerPort(); this.serverName = p_11541_.getServerName(); this.maxPlayers = p_11541_.getMaxPlayers(); this.worldName = p_11541_.getLevelIdName(); this.lastRulesResponse = 0L; this.hostIp = "0.0.0.0"; if (!this.serverIp.isEmpty() && !this.hostIp.equals(this.serverIp)) { this.hostIp = this.serverIp; } else { this.serverIp = "0.0.0.0"; try { InetAddress inetaddress = InetAddress.getLocalHost(); this.hostIp = inetaddress.getHostAddress(); } catch (UnknownHostException unknownhostexception) { LOGGER.warn("Unable to determine local host IP, please set server-ip in server.properties", (Throwable)unknownhostexception); } } this.rulesResponse = new NetworkDataOutputStream(1460); this.validChallenges = Maps.newHashMap(); } @Nullable public static QueryThreadGs4 create(ServerInterface p_11554_) { int i = p_11554_.getProperties().queryPort; if (0 < i && 65535 >= i) { QueryThreadGs4 querythreadgs4 = new QueryThreadGs4(p_11554_, i); return !querythreadgs4.start() ? null : querythreadgs4; } else { LOGGER.warn("Invalid query port {} found in server.properties (queries disabled)", i); return null; } } private void sendTo(byte[] p_11556_, DatagramPacket p_11557_) throws IOException { this.socket.send(new DatagramPacket(p_11556_, p_11556_.length, p_11557_.getSocketAddress())); } private boolean processPacket(DatagramPacket p_11550_) throws IOException { byte[] abyte = p_11550_.getData(); int i = p_11550_.getLength(); SocketAddress socketaddress = p_11550_.getSocketAddress(); LOGGER.debug("Packet len {} [{}]", i, socketaddress); if (3 <= i && -2 == abyte[0] && -3 == abyte[1]) { LOGGER.debug("Packet '{}' [{}]", PktUtils.toHexString(abyte[2]), socketaddress); switch (abyte[2]) { case 0: if (!this.validChallenge(p_11550_)) { LOGGER.debug("Invalid challenge [{}]", socketaddress); return false; } else if (15 == i) { this.sendTo(this.buildRuleResponse(p_11550_), p_11550_); LOGGER.debug("Rules [{}]", socketaddress); } else { NetworkDataOutputStream networkdataoutputstream = new NetworkDataOutputStream(1460); networkdataoutputstream.write(0); networkdataoutputstream.writeBytes(this.getIdentBytes(p_11550_.getSocketAddress())); networkdataoutputstream.writeString(this.serverName); networkdataoutputstream.writeString("SMP"); networkdataoutputstream.writeString(this.worldName); networkdataoutputstream.writeString(Integer.toString(this.serverInterface.getPlayerCount())); networkdataoutputstream.writeString(Integer.toString(this.maxPlayers)); networkdataoutputstream.writeShort((short)this.serverPort); networkdataoutputstream.writeString(this.hostIp); this.sendTo(networkdataoutputstream.toByteArray(), p_11550_); LOGGER.debug("Status [{}]", socketaddress); } default: return true; case 9: this.sendChallenge(p_11550_); LOGGER.debug("Challenge [{}]", socketaddress); return true; } } else { LOGGER.debug("Invalid packet [{}]", socketaddress); return false; } } private byte[] buildRuleResponse(DatagramPacket p_11559_) throws IOException { long i = Util.getMillis(); if (i < this.lastRulesResponse + 5000L) { byte[] abyte = this.rulesResponse.toByteArray(); byte[] abyte1 = this.getIdentBytes(p_11559_.getSocketAddress()); abyte[1] = abyte1[0]; abyte[2] = abyte1[1]; abyte[3] = abyte1[2]; abyte[4] = abyte1[3]; return abyte; } else { this.lastRulesResponse = i; this.rulesResponse.reset(); this.rulesResponse.write(0); this.rulesResponse.writeBytes(this.getIdentBytes(p_11559_.getSocketAddress())); this.rulesResponse.writeString("splitnum"); this.rulesResponse.write(128); this.rulesResponse.write(0); this.rulesResponse.writeString("hostname"); this.rulesResponse.writeString(this.serverName); this.rulesResponse.writeString("gametype"); this.rulesResponse.writeString("SMP"); this.rulesResponse.writeString("game_id"); this.rulesResponse.writeString("MINECRAFT"); this.rulesResponse.writeString("version"); this.rulesResponse.writeString(this.serverInterface.getServerVersion()); this.rulesResponse.writeString("plugins"); this.rulesResponse.writeString(this.serverInterface.getPluginNames()); this.rulesResponse.writeString("map"); this.rulesResponse.writeString(this.worldName); this.rulesResponse.writeString("numplayers"); this.rulesResponse.writeString(this.serverInterface.getPlayerCount() + ""); this.rulesResponse.writeString("maxplayers"); this.rulesResponse.writeString(this.maxPlayers + ""); this.rulesResponse.writeString("hostport"); this.rulesResponse.writeString(this.serverPort + ""); this.rulesResponse.writeString("hostip"); this.rulesResponse.writeString(this.hostIp); this.rulesResponse.write(0); this.rulesResponse.write(1); this.rulesResponse.writeString("player_"); this.rulesResponse.write(0); String[] astring = this.serverInterface.getPlayerNames(); for (String s : astring) { this.rulesResponse.writeString(s); } this.rulesResponse.write(0); return this.rulesResponse.toByteArray(); } } private byte[] getIdentBytes(SocketAddress p_11552_) { return this.validChallenges.get(p_11552_).getIdentBytes(); } private Boolean validChallenge(DatagramPacket p_11561_) { SocketAddress socketaddress = p_11561_.getSocketAddress(); if (!this.validChallenges.containsKey(socketaddress)) { return false; } else { byte[] abyte = p_11561_.getData(); return this.validChallenges.get(socketaddress).getChallenge() == PktUtils.intFromNetworkByteArray(abyte, 7, p_11561_.getLength()); } } private void sendChallenge(DatagramPacket p_11564_) throws IOException { QueryThreadGs4.RequestChallenge querythreadgs4$requestchallenge = new QueryThreadGs4.RequestChallenge(p_11564_); this.validChallenges.put(p_11564_.getSocketAddress(), querythreadgs4$requestchallenge); this.sendTo(querythreadgs4$requestchallenge.getChallengeBytes(), p_11564_); } private void pruneChallenges() { if (this.running) { long i = Util.getMillis(); if (i >= this.lastChallengeCheck + 30000L) { this.lastChallengeCheck = i; this.validChallenges.values().removeIf(p_11546_ -> p_11546_.before(i)); } } } @Override public void run() { LOGGER.info("Query running on {}:{}", this.serverIp, this.port); this.lastChallengeCheck = Util.getMillis(); DatagramPacket datagrampacket = new DatagramPacket(this.buffer, this.buffer.length); try { while (this.running) { try { this.socket.receive(datagrampacket); this.pruneChallenges(); this.processPacket(datagrampacket); } catch (SocketTimeoutException sockettimeoutexception) { this.pruneChallenges(); } catch (PortUnreachableException portunreachableexception) { } catch (IOException ioexception) { this.recoverSocketError(ioexception); } } } finally { LOGGER.debug("closeSocket: {}:{}", this.serverIp, this.port); this.socket.close(); } } @Override public boolean start() { if (this.running) { return true; } else { return !this.initSocket() ? false : super.start(); } } private void recoverSocketError(Exception p_11548_) { if (this.running) { LOGGER.warn("Unexpected exception", (Throwable)p_11548_); if (!this.initSocket()) { LOGGER.error("Failed to recover from exception, shutting down!"); this.running = false; } } } private boolean initSocket() { try { this.socket = new DatagramSocket(this.port, InetAddress.getByName(this.serverIp)); this.socket.setSoTimeout(500); return true; } catch (Exception exception) { LOGGER.warn("Unable to initialise query system on {}:{}", this.serverIp, this.port, exception); return false; } } static class RequestChallenge { private final long time = new Date().getTime(); private final int challenge; private final byte[] identBytes; private final byte[] challengeBytes; private final String ident; public RequestChallenge(DatagramPacket p_11573_) { byte[] abyte = p_11573_.getData(); this.identBytes = new byte[4]; this.identBytes[0] = abyte[3]; this.identBytes[1] = abyte[4]; this.identBytes[2] = abyte[5]; this.identBytes[3] = abyte[6]; this.ident = new String(this.identBytes, StandardCharsets.UTF_8); this.challenge = RandomSource.create().nextInt(16777216); this.challengeBytes = String.format(Locale.ROOT, "\t%s%d\u0000", this.ident, this.challenge).getBytes(StandardCharsets.UTF_8); } public Boolean before(long p_11576_) { return this.time < p_11576_; } public int getChallenge() { return this.challenge; } public byte[] getChallengeBytes() { return this.challengeBytes; } public byte[] getIdentBytes() { return this.identBytes; } public String getIdent() { return this.ident; } } }