    001    package net.minecraft.network.rcon;
    002
    003    import java.io.IOException;
    004    import java.net.DatagramPacket;
    005    import java.net.DatagramSocket;
    006    import java.net.InetAddress;
    007    import java.net.PortUnreachableException;
    008    import java.net.SocketAddress;
    009    import java.net.SocketException;
    010    import java.net.SocketTimeoutException;
    011    import java.net.UnknownHostException;
    012    import java.util.Date;
    013    import java.util.HashMap;
    014    import java.util.Iterator;
    015    import java.util.Map;
    016    import java.util.Map.Entry;
    017
    018    public class RConThreadQuery extends RConThreadBase
    019    {
    020        /** The time of the last client auth check */
    021        private long lastAuthCheckTime;
    022
    023        /** The RCon query port */
    024        private int queryPort;
    025
    026        /** Port the server is running on */
    027        private int serverPort;
    028
    029        /** The maximum number of players allowed on the server */
    030        private int maxPlayers;
    031
    032        /** The current server message of the day */
    033        private String serverMotd;
    034
    035        /** The name of the currently loaded world */
    036        private String worldName;
    037
    038        /** The remote socket querying the server */
    039        private DatagramSocket querySocket = null;
    040
    041        /** A buffer for incoming DatagramPackets */
    042        private byte[] buffer = new byte[1460];
    043
    044        /** Storage for incoming DatagramPackets */
    045        private DatagramPacket incomingPacket = null;
    046        private Map field_72644_p;
    047
    048        /** The hostname of this query server */
    049        private String queryHostname;
    050
    051        /** The hostname of the running server */
    052        private String serverHostname;
    053
    054        /** A map of SocketAddress objects to RConThreadQueryAuth objects */
    055        private Map queryClients;
    056
    057        /**
    058         * The time that this RConThreadQuery was constructed, from (new Date()).getTime()
    059         */
    060        private long time;
    061
    062        /** The RConQuery output stream */
    063        private RConOutputStream output;
    064
    065        /** The time of the last query response sent */
    066        private long lastQueryResponseTime;
    067
    068        public RConThreadQuery(IServer par1IServer)
    069        {
    070            super(par1IServer);
    071            this.queryPort = par1IServer.getIntProperty("query.port", 0);
    072            this.serverHostname = par1IServer.getHostname();
    073            this.serverPort = par1IServer.getPort();
    074            this.serverMotd = par1IServer.getServerMOTD();
    075            this.maxPlayers = par1IServer.getMaxPlayers();
    076            this.worldName = par1IServer.getFolderName();
    077            this.lastQueryResponseTime = 0L;
    078            this.queryHostname = "0.0.0.0";
    079
    080            if (0 != this.serverHostname.length() && !this.queryHostname.equals(this.serverHostname))
    081            {
    082                this.queryHostname = this.serverHostname;
    083            }
    084            else
    085            {
    086                this.serverHostname = "0.0.0.0";
    087
    088                try
    089                {
    090                    InetAddress var2 = InetAddress.getLocalHost();
    091                    this.queryHostname = var2.getHostAddress();
    092                }
    093                catch (UnknownHostException var3)
    094                {
    095                    this.logWarning("Unable to determine local host IP, please set server-ip in \'" + par1IServer.getSettingsFilePath() + "\' : " + var3.getMessage());
    096                }
    097            }
    098
    099            if (0 == this.queryPort)
    100            {
    101                this.queryPort = this.serverPort;
    102                this.logInfo("Setting default query port to " + this.queryPort);
    103                par1IServer.setProperty("query.port", Integer.valueOf(this.queryPort));
    104                par1IServer.setProperty("debug", Boolean.valueOf(false));
    105                par1IServer.saveProperties();
    106            }
    107
    108            this.field_72644_p = new HashMap();
    109            this.output = new RConOutputStream(1460);
    110            this.queryClients = new HashMap();
    111            this.time = (new Date()).getTime();
    112        }
    113
    114        /**
    115         * Sends a byte array as a DatagramPacket response to the client who sent the given DatagramPacket
    116         */
    117        private void sendResponsePacket(byte[] par1ArrayOfByte, DatagramPacket par2DatagramPacket) throws IOException
    118        {
    119            this.querySocket.send(new DatagramPacket(par1ArrayOfByte, par1ArrayOfByte.length, par2DatagramPacket.getSocketAddress()));
    120        }
    121
    122        /**
    123         * Parses an incoming DatagramPacket, returning true if the packet was valid
    124         */
    125        private boolean parseIncomingPacket(DatagramPacket par1DatagramPacket) throws IOException
    126        {
    127            byte[] var2 = par1DatagramPacket.getData();
    128            int var3 = par1DatagramPacket.getLength();
    129            SocketAddress var4 = par1DatagramPacket.getSocketAddress();
    130            this.logDebug("Packet len " + var3 + " [" + var4 + "]");
    131
    132            if (3 <= var3 && -2 == var2[0] && -3 == var2[1])
    133            {
    134                this.logDebug("Packet \'" + RConUtils.getByteAsHexString(var2[2]) + "\' [" + var4 + "]");
    135
    136                switch (var2[2])
    137                {
    138                    case 0:
    139                        if (!this.verifyClientAuth(par1DatagramPacket).booleanValue())
    140                        {
    141                            this.logDebug("Invalid challenge [" + var4 + "]");
    142                            return false;
    143                        }
    144                        else if (15 == var3)
    145                        {
    146                            this.sendResponsePacket(this.createQueryResponse(par1DatagramPacket), par1DatagramPacket);
    147                            this.logDebug("Rules [" + var4 + "]");
    148                        }
    149                        else
    150                        {
    151                            RConOutputStream var5 = new RConOutputStream(1460);
    152                            var5.writeInt(0);
    153                            var5.writeByteArray(this.getRequestId(par1DatagramPacket.getSocketAddress()));
    154                            var5.writeString(this.serverMotd);
    155                            var5.writeString("SMP");
    156                            var5.writeString(this.worldName);
    157                            var5.writeString(Integer.toString(this.getNumberOfPlayers()));
    158                            var5.writeString(Integer.toString(this.maxPlayers));
    159                            var5.writeShort((short)this.serverPort);
    160                            var5.writeString(this.queryHostname);
    161                            this.sendResponsePacket(var5.toByteArray(), par1DatagramPacket);
    162                            this.logDebug("Status [" + var4 + "]");
    163                        }
    164                    case 9:
    165                        this.sendAuthChallenge(par1DatagramPacket);
    166                        this.logDebug("Challenge [" + var4 + "]");
    167                        return true;
    168                    default:
    169                        return true;
    170                }
    171            }
    172            else
    173            {
    174                this.logDebug("Invalid packet [" + var4 + "]");
    175                return false;
    176            }
    177        }
    178
    179        /**
    180         * Creates a query response as a byte array for the specified query DatagramPacket
    181         */
    182        private byte[] createQueryResponse(DatagramPacket par1DatagramPacket) throws IOException
    183        {
    184            long var2 = System.currentTimeMillis();
    185
    186            if (var2 < this.lastQueryResponseTime + 5000L)
    187            {
    188                byte[] var7 = this.output.toByteArray();
    189                byte[] var8 = this.getRequestId(par1DatagramPacket.getSocketAddress());
    190                var7[1] = var8[0];
    191                var7[2] = var8[1];
    192                var7[3] = var8[2];
    193                var7[4] = var8[3];
    194                return var7;
    195            }
    196            else
    197            {
    198                this.lastQueryResponseTime = var2;
    199                this.output.reset();
    200                this.output.writeInt(0);
    201                this.output.writeByteArray(this.getRequestId(par1DatagramPacket.getSocketAddress()));
    202                this.output.writeString("splitnum");
    203                this.output.writeInt(128);
    204                this.output.writeInt(0);
    205                this.output.writeString("hostname");
    206                this.output.writeString(this.serverMotd);
    207                this.output.writeString("gametype");
    208                this.output.writeString("SMP");
    209                this.output.writeString("game_id");
    210                this.output.writeString("MINECRAFT");
    211                this.output.writeString("version");
    212                this.output.writeString(this.server.getMinecraftVersion());
    213                this.output.writeString("plugins");
    214                this.output.writeString(this.server.getPlugins());
    215                this.output.writeString("map");
    216                this.output.writeString(this.worldName);
    217                this.output.writeString("numplayers");
    218                this.output.writeString("" + this.getNumberOfPlayers());
    219                this.output.writeString("maxplayers");
    220                this.output.writeString("" + this.maxPlayers);
    221                this.output.writeString("hostport");
    222                this.output.writeString("" + this.serverPort);
    223                this.output.writeString("hostip");
    224                this.output.writeString(this.queryHostname);
    225                this.output.writeInt(0);
    226                this.output.writeInt(1);
    227                this.output.writeString("player_");
    228                this.output.writeInt(0);
    229                String[] var4 = this.server.getAllUsernames();
    230                byte var5 = (byte)var4.length;
    231
    232                for (byte var6 = (byte)(var5 - 1); var6 >= 0; --var6)
    233                {
    234                    this.output.writeString(var4[var6]);
    235                }
    236
    237                this.output.writeInt(0);
    238                return this.output.toByteArray();
    239            }
    240        }
    241
    242        /**
    243         * Returns the request ID provided by the authorized client
    244         */
    245        private byte[] getRequestId(SocketAddress par1SocketAddress)
    246        {
    247            return ((RConThreadQueryAuth)this.queryClients.get(par1SocketAddress)).getRequestId();
    248        }
    249
    250        /**
    251         * Returns true if the client has a valid auth, otherwise false
    252         */
    253        private Boolean verifyClientAuth(DatagramPacket par1DatagramPacket)
    254        {
    255            SocketAddress var2 = par1DatagramPacket.getSocketAddress();
    256
    257            if (!this.queryClients.containsKey(var2))
    258            {
    259                return Boolean.valueOf(false);
    260            }
    261            else
    262            {
    263                byte[] var3 = par1DatagramPacket.getData();
    264                return ((RConThreadQueryAuth)this.queryClients.get(var2)).getRandomChallenge() != RConUtils.getBytesAsBEint(var3, 7, par1DatagramPacket.getLength()) ? Boolean.valueOf(false) : Boolean.valueOf(true);
    265            }
    266        }
    267
    268        /**
    269         * Sends an auth challenge DatagramPacket to the client and adds the client to the queryClients map
    270         */
    271        private void sendAuthChallenge(DatagramPacket par1DatagramPacket) throws IOException
    272        {
    273            RConThreadQueryAuth var2 = new RConThreadQueryAuth(this, par1DatagramPacket);
    274            this.queryClients.put(par1DatagramPacket.getSocketAddress(), var2);
    275            this.sendResponsePacket(var2.getChallengeValue(), par1DatagramPacket);
    276        }
    277
    278        /**
    279         * Removes all clients whose auth is no longer valid
    280         */
    281        private void cleanQueryClientsMap()
    282        {
    283            if (this.running)
    284            {
    285                long var1 = System.currentTimeMillis();
    286
    287                if (var1 >= this.lastAuthCheckTime + 30000L)
    288                {
    289                    this.lastAuthCheckTime = var1;
    290                    Iterator var3 = this.queryClients.entrySet().iterator();
    291
    292                    while (var3.hasNext())
    293                    {
    294                        Entry var4 = (Entry)var3.next();
    295
    296                        if (((RConThreadQueryAuth)var4.getValue()).hasExpired(var1).booleanValue())
    297                        {
    298                            var3.remove();
    299                        }
    300                    }
    301                }
    302            }
    303        }
    304
    305        public void run()
    306        {
    307            this.logInfo("Query running on " + this.serverHostname + ":" + this.queryPort);
    308            this.lastAuthCheckTime = System.currentTimeMillis();
    309            this.incomingPacket = new DatagramPacket(this.buffer, this.buffer.length);
    310
    311            try
    312            {
    313                while (this.running)
    314                {
    315                    try
    316                    {
    317                        this.querySocket.receive(this.incomingPacket);
    318                        this.cleanQueryClientsMap();
    319                        this.parseIncomingPacket(this.incomingPacket);
    320                    }
    321                    catch (SocketTimeoutException var7)
    322                    {
    323                        this.cleanQueryClientsMap();
    324                    }
    325                    catch (PortUnreachableException var8)
    326                    {
    327                        ;
    328                    }
    329                    catch (IOException var9)
    330                    {
    331                        this.stopWithException(var9);
    332                    }
    333                }
    334            }
    335            finally
    336            {
    337                this.closeAllSockets();
    338            }
    339        }
    340
    341        /**
    342         * Creates a new Thread object from this class and starts running
    343         */
    344        public void startThread()
    345        {
    346            if (!this.running)
    347            {
    348                if (0 < this.queryPort && 65535 >= this.queryPort)
    349                {
    350                    if (this.initQuerySystem())
    351                    {
    352                        super.startThread();
    353                    }
    354                }
    355                else
    356                {
    357                    this.logWarning("Invalid query port " + this.queryPort + " found in \'" + this.server.getSettingsFilePath() + "\' (queries disabled)");
    358                }
    359            }
    360        }
    361
    362        /**
    363         * Stops the query server and reports the given Exception
    364         */
    365        private void stopWithException(Exception par1Exception)
    366        {
    367            if (this.running)
    368            {
    369                this.logWarning("Unexpected exception, buggy JRE? (" + par1Exception.toString() + ")");
    370
    371                if (!this.initQuerySystem())
    372                {
    373                    this.logSevere("Failed to recover from buggy JRE, shutting down!");
    374                    this.running = false;
    375                }
    376            }
    377        }
    378
    379        /**
    380         * Initializes the query system by binding it to a port
    381         */
    382        private boolean initQuerySystem()
    383        {
    384            try
    385            {
    386                this.querySocket = new DatagramSocket(this.queryPort, InetAddress.getByName(this.serverHostname));
    387                this.registerSocket(this.querySocket);
    388                this.querySocket.setSoTimeout(500);
    389                return true;
    390            }
    391            catch (SocketException var2)
    392            {
    393                this.logWarning("Unable to initialise query system on " + this.serverHostname + ":" + this.queryPort + " (Socket): " + var2.getMessage());
    394            }
    395            catch (UnknownHostException var3)
    396            {
    397                this.logWarning("Unable to initialise query system on " + this.serverHostname + ":" + this.queryPort + " (Unknown Host): " + var3.getMessage());
    398            }
    399            catch (Exception var4)
    400            {
    401                this.logWarning("Unable to initialise query system on " + this.serverHostname + ":" + this.queryPort + " (E): " + var4.getMessage());
    402            }
    403
    404            return false;
    405        }
    406    }