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}