package net.minecraft.network.rcon; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; 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.SocketException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Random; import java.util.Map.Entry; import net.minecraft.server.MinecraftServer; @SideOnly(Side.SERVER) public class RConThreadQuery extends RConThreadBase { /** The time of the last client auth check */ private long lastAuthCheckTime; /** The RCon query port */ private int queryPort; /** Port the server is running on */ private int serverPort; /** The maximum number of players allowed on the server */ private int maxPlayers; /** The current server message of the day */ private String serverMotd; /** The name of the currently loaded world */ private String worldName; /** The remote socket querying the server */ private DatagramSocket querySocket; /** A buffer for incoming DatagramPackets */ private byte[] buffer = new byte[1460]; /** Storage for incoming DatagramPackets */ private DatagramPacket incomingPacket; private Map field_72644_p; /** The hostname of this query server */ private String queryHostname; /** The hostname of the running server */ private String serverHostname; /** A map of SocketAddress objects to RConThreadQueryAuth objects */ private Map queryClients; /** The time that this RConThreadQuery was constructed, from (new Date()).getTime() */ private long time; /** The RConQuery output stream */ private RConOutputStream output; /** The time of the last query response sent */ private long lastQueryResponseTime; private static final String __OBFID = "CL_00001802"; public RConThreadQuery(IServer p_i1536_1_) { super(p_i1536_1_, "Query Listener"); this.queryPort = p_i1536_1_.getIntProperty("query.port", 0); this.serverHostname = p_i1536_1_.getHostname(); this.serverPort = p_i1536_1_.getPort(); this.serverMotd = p_i1536_1_.getMotd(); this.maxPlayers = p_i1536_1_.getMaxPlayers(); this.worldName = p_i1536_1_.getFolderName(); this.lastQueryResponseTime = 0L; this.queryHostname = "0.0.0.0"; if (0 != this.serverHostname.length() && !this.queryHostname.equals(this.serverHostname)) { this.queryHostname = this.serverHostname; } else { this.serverHostname = "0.0.0.0"; try { InetAddress inetaddress = InetAddress.getLocalHost(); this.queryHostname = inetaddress.getHostAddress(); } catch (UnknownHostException unknownhostexception) { this.logWarning("Unable to determine local host IP, please set server-ip in \'" + p_i1536_1_.getSettingsFilename() + "\' : " + unknownhostexception.getMessage()); } } if (0 == this.queryPort) { this.queryPort = this.serverPort; this.logInfo("Setting default query port to " + this.queryPort); p_i1536_1_.setProperty("query.port", Integer.valueOf(this.queryPort)); p_i1536_1_.setProperty("debug", Boolean.valueOf(false)); p_i1536_1_.saveProperties(); } this.field_72644_p = new HashMap(); this.output = new RConOutputStream(1460); this.queryClients = new HashMap(); this.time = (new Date()).getTime(); } /** * Sends a byte array as a DatagramPacket response to the client who sent the given DatagramPacket */ private void sendResponsePacket(byte[] data, DatagramPacket requestPacket) throws IOException { this.querySocket.send(new DatagramPacket(data, data.length, requestPacket.getSocketAddress())); } /** * Parses an incoming DatagramPacket, returning true if the packet was valid */ private boolean parseIncomingPacket(DatagramPacket requestPacket) throws IOException { byte[] abyte = requestPacket.getData(); int i = requestPacket.getLength(); SocketAddress socketaddress = requestPacket.getSocketAddress(); this.logDebug("Packet len " + i + " [" + socketaddress + "]"); if (3 <= i && -2 == abyte[0] && -3 == abyte[1]) { this.logDebug("Packet \'" + RConUtils.getByteAsHexString(abyte[2]) + "\' [" + socketaddress + "]"); switch (abyte[2]) { case 0: if (!this.verifyClientAuth(requestPacket).booleanValue()) { this.logDebug("Invalid challenge [" + socketaddress + "]"); return false; } else if (15 == i) { this.sendResponsePacket(this.createQueryResponse(requestPacket), requestPacket); this.logDebug("Rules [" + socketaddress + "]"); } else { RConOutputStream rconoutputstream = new RConOutputStream(1460); rconoutputstream.writeInt(0); rconoutputstream.writeByteArray(this.getRequestID(requestPacket.getSocketAddress())); rconoutputstream.writeString(this.serverMotd); rconoutputstream.writeString("SMP"); rconoutputstream.writeString(this.worldName); rconoutputstream.writeString(Integer.toString(this.getNumberOfPlayers())); rconoutputstream.writeString(Integer.toString(this.maxPlayers)); rconoutputstream.writeShort((short)this.serverPort); rconoutputstream.writeString(this.queryHostname); this.sendResponsePacket(rconoutputstream.toByteArray(), requestPacket); this.logDebug("Status [" + socketaddress + "]"); } case 9: this.sendAuthChallenge(requestPacket); this.logDebug("Challenge [" + socketaddress + "]"); return true; default: return true; } } else { this.logDebug("Invalid packet [" + socketaddress + "]"); return false; } } /** * Creates a query response as a byte array for the specified query DatagramPacket */ private byte[] createQueryResponse(DatagramPacket requestPacket) throws IOException { long i = MinecraftServer.getCurrentTimeMillis(); if (i < this.lastQueryResponseTime + 5000L) { byte[] abyte = this.output.toByteArray(); byte[] abyte1 = this.getRequestID(requestPacket.getSocketAddress()); abyte[1] = abyte1[0]; abyte[2] = abyte1[1]; abyte[3] = abyte1[2]; abyte[4] = abyte1[3]; return abyte; } else { this.lastQueryResponseTime = i; this.output.reset(); this.output.writeInt(0); this.output.writeByteArray(this.getRequestID(requestPacket.getSocketAddress())); this.output.writeString("splitnum"); this.output.writeInt(128); this.output.writeInt(0); this.output.writeString("hostname"); this.output.writeString(this.serverMotd); this.output.writeString("gametype"); this.output.writeString("SMP"); this.output.writeString("game_id"); this.output.writeString("MINECRAFT"); this.output.writeString("version"); this.output.writeString(this.server.getMinecraftVersion()); this.output.writeString("plugins"); this.output.writeString(this.server.getPlugins()); this.output.writeString("map"); this.output.writeString(this.worldName); this.output.writeString("numplayers"); this.output.writeString("" + this.getNumberOfPlayers()); this.output.writeString("maxplayers"); this.output.writeString("" + this.maxPlayers); this.output.writeString("hostport"); this.output.writeString("" + this.serverPort); this.output.writeString("hostip"); this.output.writeString(this.queryHostname); this.output.writeInt(0); this.output.writeInt(1); this.output.writeString("player_"); this.output.writeInt(0); String[] astring = this.server.getAllUsernames(); String[] astring1 = astring; int j = astring.length; for (int k = 0; k < j; ++k) { String s = astring1[k]; this.output.writeString(s); } this.output.writeInt(0); return this.output.toByteArray(); } } /** * Returns the request ID provided by the authorized client */ private byte[] getRequestID(SocketAddress address) { return ((RConThreadQuery.Auth)this.queryClients.get(address)).getRequestId(); } /** * Returns true if the client has a valid auth, otherwise false */ private Boolean verifyClientAuth(DatagramPacket requestPacket) { SocketAddress socketaddress = requestPacket.getSocketAddress(); if (!this.queryClients.containsKey(socketaddress)) { return Boolean.valueOf(false); } else { byte[] abyte = requestPacket.getData(); return ((RConThreadQuery.Auth)this.queryClients.get(socketaddress)).getRandomChallenge() != RConUtils.getBytesAsBEint(abyte, 7, requestPacket.getLength()) ? Boolean.valueOf(false) : Boolean.valueOf(true); } } /** * Sends an auth challenge DatagramPacket to the client and adds the client to the queryClients map */ private void sendAuthChallenge(DatagramPacket requestPacket) throws IOException { RConThreadQuery.Auth auth = new RConThreadQuery.Auth(requestPacket); this.queryClients.put(requestPacket.getSocketAddress(), auth); this.sendResponsePacket(auth.getChallengeValue(), requestPacket); } /** * Removes all clients whose auth is no longer valid */ private void cleanQueryClientsMap() { if (this.running) { long i = MinecraftServer.getCurrentTimeMillis(); if (i >= this.lastAuthCheckTime + 30000L) { this.lastAuthCheckTime = i; Iterator iterator = this.queryClients.entrySet().iterator(); while (iterator.hasNext()) { Entry entry = (Entry)iterator.next(); if (((RConThreadQuery.Auth)entry.getValue()).hasExpired(i).booleanValue()) { iterator.remove(); } } } } } public void run() { this.logInfo("Query running on " + this.serverHostname + ":" + this.queryPort); this.lastAuthCheckTime = MinecraftServer.getCurrentTimeMillis(); this.incomingPacket = new DatagramPacket(this.buffer, this.buffer.length); try { while (this.running) { try { this.querySocket.receive(this.incomingPacket); this.cleanQueryClientsMap(); this.parseIncomingPacket(this.incomingPacket); } catch (SocketTimeoutException sockettimeoutexception) { this.cleanQueryClientsMap(); } catch (PortUnreachableException portunreachableexception) { ; } catch (IOException ioexception) { this.stopWithException(ioexception); } } } finally { this.closeAllSockets(); } } /** * Creates a new Thread object from this class and starts running */ public void startThread() { if (!this.running) { if (0 < this.queryPort && 65535 >= this.queryPort) { if (this.initQuerySystem()) { super.startThread(); } } else { this.logWarning("Invalid query port " + this.queryPort + " found in \'" + this.server.getSettingsFilename() + "\' (queries disabled)"); } } } /** * Stops the query server and reports the given Exception */ private void stopWithException(Exception exception) { if (this.running) { this.logWarning("Unexpected exception, buggy JRE? (" + exception.toString() + ")"); if (!this.initQuerySystem()) { this.logSevere("Failed to recover from buggy JRE, shutting down!"); this.running = false; } } } /** * Initializes the query system by binding it to a port */ private boolean initQuerySystem() { try { this.querySocket = new DatagramSocket(this.queryPort, InetAddress.getByName(this.serverHostname)); this.registerSocket(this.querySocket); this.querySocket.setSoTimeout(500); return true; } catch (SocketException socketexception) { this.logWarning("Unable to initialise query system on " + this.serverHostname + ":" + this.queryPort + " (Socket): " + socketexception.getMessage()); } catch (UnknownHostException unknownhostexception) { this.logWarning("Unable to initialise query system on " + this.serverHostname + ":" + this.queryPort + " (Unknown Host): " + unknownhostexception.getMessage()); } catch (Exception exception) { this.logWarning("Unable to initialise query system on " + this.serverHostname + ":" + this.queryPort + " (E): " + exception.getMessage()); } return false; } @SideOnly(Side.SERVER) class Auth { /** The creation timestamp for this auth */ private long timestamp = (new Date()).getTime(); /** A random integer value to be used for client response authentication */ private int randomChallenge; /** A client-provided request ID associated with this query. */ private byte[] requestId; /** A unique string of bytes used to verify client auth */ private byte[] challengeValue; /** The request ID stored as a String */ private String requestIdAsString; private static final String __OBFID = "CL_00001803"; public Auth(DatagramPacket requestPacket) { byte[] abyte = requestPacket.getData(); this.requestId = new byte[4]; this.requestId[0] = abyte[3]; this.requestId[1] = abyte[4]; this.requestId[2] = abyte[5]; this.requestId[3] = abyte[6]; this.requestIdAsString = new String(this.requestId); this.randomChallenge = (new Random()).nextInt(16777216); this.challengeValue = String.format("\t%s%d\u0000", new Object[] {this.requestIdAsString, Integer.valueOf(this.randomChallenge)}).getBytes(); } /** * Returns true if the auth's creation timestamp is less than the given time, otherwise false */ public Boolean hasExpired(long currentTime) { return Boolean.valueOf(this.timestamp < currentTime); } /** * Returns the random challenge number assigned to this auth */ public int getRandomChallenge() { return this.randomChallenge; } /** * Returns the auth challenge value */ public byte[] getChallengeValue() { return this.challengeValue; } /** * Returns the request ID provided by the client. */ public byte[] getRequestId() { return this.requestId; } } }