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.getSettingsFilename() + "\' : " + 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.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 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    }