    001package net.minecraft.network.rcon;
    002
    003import java.io.IOException;
    004import java.net.DatagramPacket;
    005import java.net.DatagramSocket;
    006import java.net.InetAddress;
    007import java.net.PortUnreachableException;
    008import java.net.SocketAddress;
    009import java.net.SocketException;
    010import java.net.SocketTimeoutException;
    011import java.net.UnknownHostException;
    012import java.util.Date;
    013import java.util.HashMap;
    014import java.util.Iterator;
    015import java.util.Map;
    016import java.util.Map.Entry;
    017
    018public 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 inetaddress = InetAddress.getLocalHost();
    091                this.queryHostname = inetaddress.getHostAddress();
    092            }
    093            catch (UnknownHostException unknownhostexception)
    094            {
    095                this.logWarning("Unable to determine local host IP, please set server-ip in \'" + par1IServer.getSettingsFilename() + "\' : " + unknownhostexception.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[] abyte = par1DatagramPacket.getData();
    128        int i = par1DatagramPacket.getLength();
    129        SocketAddress socketaddress = par1DatagramPacket.getSocketAddress();
    130        this.logDebug("Packet len " + i + " [" + socketaddress + "]");
    131
    132        if (3 <= i && -2 == abyte[0] && -3 == abyte[1])
    133        {
    134            this.logDebug("Packet \'" + RConUtils.getByteAsHexString(abyte[2]) + "\' [" + socketaddress + "]");
    135
    136            switch (abyte[2])
    137            {
    138                case 0:
    139                    if (!this.verifyClientAuth(par1DatagramPacket).booleanValue())
    140                    {
    141                        this.logDebug("Invalid challenge [" + socketaddress + "]");
    142                        return false;
    143                    }
    144                    else if (15 == i)
    145                    {
    146                        this.sendResponsePacket(this.createQueryResponse(par1DatagramPacket), par1DatagramPacket);
    147                        this.logDebug("Rules [" + socketaddress + "]");
    148                    }
    149                    else
    150                    {
    151                        RConOutputStream rconoutputstream = new RConOutputStream(1460);
    152                        rconoutputstream.writeInt(0);
    153                        rconoutputstream.writeByteArray(this.getRequestID(par1DatagramPacket.getSocketAddress()));
    154                        rconoutputstream.writeString(this.serverMotd);
    155                        rconoutputstream.writeString("SMP");
    156                        rconoutputstream.writeString(this.worldName);
    157                        rconoutputstream.writeString(Integer.toString(this.getNumberOfPlayers()));
    158                        rconoutputstream.writeString(Integer.toString(this.maxPlayers));
    159                        rconoutputstream.writeShort((short)this.serverPort);
    160                        rconoutputstream.writeString(this.queryHostname);
    161                        this.sendResponsePacket(rconoutputstream.toByteArray(), par1DatagramPacket);
    162                        this.logDebug("Status [" + socketaddress + "]");
    163                    }
    164                case 9:
    165                    this.sendAuthChallenge(par1DatagramPacket);
    166                    this.logDebug("Challenge [" + socketaddress + "]");
    167                    return true;
    168                default:
    169                    return true;
    170            }
    171        }
    172        else
    173        {
    174            this.logDebug("Invalid packet [" + socketaddress + "]");
    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 i = System.currentTimeMillis();
    185
    186        if (i < this.lastQueryResponseTime + 5000L)
    187        {
    188            byte[] abyte = this.output.toByteArray();
    189            byte[] abyte1 = this.getRequestID(par1DatagramPacket.getSocketAddress());
    190            abyte[1] = abyte1[0];
    191            abyte[2] = abyte1[1];
    192            abyte[3] = abyte1[2];
    193            abyte[4] = abyte1[3];
    194            return abyte;
    195        }
    196        else
    197        {
    198            this.lastQueryResponseTime = i;
    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[] astring = this.server.getAllUsernames();
    230            byte b0 = (byte)astring.length;
    231
    232            for (byte b1 = (byte)(b0 - 1); b1 >= 0; --b1)
    233            {
    234                this.output.writeString(astring[b1]);
    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 socketaddress = par1DatagramPacket.getSocketAddress();
    256
    257        if (!this.queryClients.containsKey(socketaddress))
    258        {
    259            return Boolean.valueOf(false);
    260        }
    261        else
    262        {
    263            byte[] abyte = par1DatagramPacket.getData();
    264            return ((RConThreadQueryAuth)this.queryClients.get(socketaddress)).getRandomChallenge() != RConUtils.getBytesAsBEint(abyte, 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 rconthreadqueryauth = new RConThreadQueryAuth(this, par1DatagramPacket);
    274        this.queryClients.put(par1DatagramPacket.getSocketAddress(), rconthreadqueryauth);
    275        this.sendResponsePacket(rconthreadqueryauth.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 i = System.currentTimeMillis();
    286
    287            if (i >= this.lastAuthCheckTime + 30000L)
    288            {
    289                this.lastAuthCheckTime = i;
    290                Iterator iterator = this.queryClients.entrySet().iterator();
    291
    292                while (iterator.hasNext())
    293                {
    294                    Entry entry = (Entry)iterator.next();
    295
    296                    if (((RConThreadQueryAuth)entry.getValue()).hasExpired(i).booleanValue())
    297                    {
    298                        iterator.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 sockettimeoutexception)
    322                {
    323                    this.cleanQueryClientsMap();
    324                }
    325                catch (PortUnreachableException portunreachableexception)
    326                {
    327                    ;
    328                }
    329                catch (IOException ioexception)
    330                {
    331                    this.stopWithException(ioexception);
    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.getSettingsFilename() + "\' (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 socketexception)
    392        {
    393            this.logWarning("Unable to initialise query system on " + this.serverHostname + ":" + this.queryPort + " (Socket): " + socketexception.getMessage());
    394        }
    395        catch (UnknownHostException unknownhostexception)
    396        {
    397            this.logWarning("Unable to initialise query system on " + this.serverHostname + ":" + this.queryPort + " (Unknown Host): " + unknownhostexception.getMessage());
    398        }
    399        catch (Exception exception)
    400        {
    401            this.logWarning("Unable to initialise query system on " + this.serverHostname + ":" + this.queryPort + " (E): " + exception.getMessage());
    402        }
    403
    404        return false;
    405    }
    406}