348 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			Java
		
	
	
	
	
	
			
		
		
	
	
			348 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			Java
		
	
	
	
	
	
| package net.minecraft.server.rcon.thread;
 | |
| 
 | |
| import com.google.common.collect.Maps;
 | |
| import com.mojang.logging.LogUtils;
 | |
| 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.SocketTimeoutException;
 | |
| import java.net.UnknownHostException;
 | |
| import java.nio.charset.StandardCharsets;
 | |
| import java.util.Date;
 | |
| import java.util.Locale;
 | |
| import java.util.Map;
 | |
| import net.minecraft.Util;
 | |
| import net.minecraft.server.ServerInterface;
 | |
| import net.minecraft.server.rcon.NetworkDataOutputStream;
 | |
| import net.minecraft.server.rcon.PktUtils;
 | |
| import net.minecraft.util.RandomSource;
 | |
| import org.jetbrains.annotations.Nullable;
 | |
| import org.slf4j.Logger;
 | |
| 
 | |
| public class QueryThreadGs4 extends GenericThread {
 | |
| 	private static final Logger LOGGER = LogUtils.getLogger();
 | |
| 	private static final String GAME_TYPE = "SMP";
 | |
| 	private static final String GAME_ID = "MINECRAFT";
 | |
| 	private static final long CHALLENGE_CHECK_INTERVAL = 30000L;
 | |
| 	private static final long RESPONSE_CACHE_TIME = 5000L;
 | |
| 	private long lastChallengeCheck;
 | |
| 	private final int port;
 | |
| 	private final int serverPort;
 | |
| 	private final int maxPlayers;
 | |
| 	private final String serverName;
 | |
| 	private final String worldName;
 | |
| 	private DatagramSocket socket;
 | |
| 	private final byte[] buffer = new byte[1460];
 | |
| 	private String hostIp;
 | |
| 	private String serverIp;
 | |
| 	private final Map<SocketAddress, QueryThreadGs4.RequestChallenge> validChallenges;
 | |
| 	private final NetworkDataOutputStream rulesResponse;
 | |
| 	private long lastRulesResponse;
 | |
| 	private final ServerInterface serverInterface;
 | |
| 
 | |
| 	private QueryThreadGs4(ServerInterface serverInterface, int port) {
 | |
| 		super("Query Listener");
 | |
| 		this.serverInterface = serverInterface;
 | |
| 		this.port = port;
 | |
| 		this.serverIp = serverInterface.getServerIp();
 | |
| 		this.serverPort = serverInterface.getServerPort();
 | |
| 		this.serverName = serverInterface.getServerName();
 | |
| 		this.maxPlayers = serverInterface.getMaxPlayers();
 | |
| 		this.worldName = serverInterface.getLevelIdName();
 | |
| 		this.lastRulesResponse = 0L;
 | |
| 		this.hostIp = "0.0.0.0";
 | |
| 		if (!this.serverIp.isEmpty() && !this.hostIp.equals(this.serverIp)) {
 | |
| 			this.hostIp = this.serverIp;
 | |
| 		} else {
 | |
| 			this.serverIp = "0.0.0.0";
 | |
| 
 | |
| 			try {
 | |
| 				InetAddress inetAddress = InetAddress.getLocalHost();
 | |
| 				this.hostIp = inetAddress.getHostAddress();
 | |
| 			} catch (UnknownHostException var4) {
 | |
| 				LOGGER.warn("Unable to determine local host IP, please set server-ip in server.properties", (Throwable)var4);
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		this.rulesResponse = new NetworkDataOutputStream(1460);
 | |
| 		this.validChallenges = Maps.<SocketAddress, QueryThreadGs4.RequestChallenge>newHashMap();
 | |
| 	}
 | |
| 
 | |
| 	@Nullable
 | |
| 	public static QueryThreadGs4 create(ServerInterface serverInterface) {
 | |
| 		int i = serverInterface.getProperties().queryPort;
 | |
| 		if (0 < i && 65535 >= i) {
 | |
| 			QueryThreadGs4 queryThreadGs4 = new QueryThreadGs4(serverInterface, i);
 | |
| 			return !queryThreadGs4.start() ? null : queryThreadGs4;
 | |
| 		} else {
 | |
| 			LOGGER.warn("Invalid query port {} found in server.properties (queries disabled)", i);
 | |
| 			return null;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Sends a byte array as a DatagramPacket response to the client who sent the given DatagramPacket
 | |
| 	 */
 | |
| 	private void sendTo(byte[] data, DatagramPacket requestPacket) throws IOException {
 | |
| 		this.socket.send(new DatagramPacket(data, data.length, requestPacket.getSocketAddress()));
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Parses an incoming DatagramPacket, returning true if the packet was valid
 | |
| 	 */
 | |
| 	private boolean processPacket(DatagramPacket requestPacket) throws IOException {
 | |
| 		byte[] bs = requestPacket.getData();
 | |
| 		int i = requestPacket.getLength();
 | |
| 		SocketAddress socketAddress = requestPacket.getSocketAddress();
 | |
| 		LOGGER.debug("Packet len {} [{}]", i, socketAddress);
 | |
| 		if (3 <= i && -2 == bs[0] && -3 == bs[1]) {
 | |
| 			LOGGER.debug("Packet '{}' [{}]", PktUtils.toHexString(bs[2]), socketAddress);
 | |
| 			switch (bs[2]) {
 | |
| 				case 0:
 | |
| 					if (!this.validChallenge(requestPacket)) {
 | |
| 						LOGGER.debug("Invalid challenge [{}]", socketAddress);
 | |
| 						return false;
 | |
| 					} else if (15 == i) {
 | |
| 						this.sendTo(this.buildRuleResponse(requestPacket), requestPacket);
 | |
| 						LOGGER.debug("Rules [{}]", socketAddress);
 | |
| 					} else {
 | |
| 						NetworkDataOutputStream networkDataOutputStream = new NetworkDataOutputStream(1460);
 | |
| 						networkDataOutputStream.write(0);
 | |
| 						networkDataOutputStream.writeBytes(this.getIdentBytes(requestPacket.getSocketAddress()));
 | |
| 						networkDataOutputStream.writeString(this.serverName);
 | |
| 						networkDataOutputStream.writeString("SMP");
 | |
| 						networkDataOutputStream.writeString(this.worldName);
 | |
| 						networkDataOutputStream.writeString(Integer.toString(this.serverInterface.getPlayerCount()));
 | |
| 						networkDataOutputStream.writeString(Integer.toString(this.maxPlayers));
 | |
| 						networkDataOutputStream.writeShort((short)this.serverPort);
 | |
| 						networkDataOutputStream.writeString(this.hostIp);
 | |
| 						this.sendTo(networkDataOutputStream.toByteArray(), requestPacket);
 | |
| 						LOGGER.debug("Status [{}]", socketAddress);
 | |
| 					}
 | |
| 				default:
 | |
| 					return true;
 | |
| 				case 9:
 | |
| 					this.sendChallenge(requestPacket);
 | |
| 					LOGGER.debug("Challenge [{}]", socketAddress);
 | |
| 					return true;
 | |
| 			}
 | |
| 		} else {
 | |
| 			LOGGER.debug("Invalid packet [{}]", socketAddress);
 | |
| 			return false;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Creates a query response as a byte array for the specified query DatagramPacket
 | |
| 	 */
 | |
| 	private byte[] buildRuleResponse(DatagramPacket requestPacket) throws IOException {
 | |
| 		long l = Util.getMillis();
 | |
| 		if (l < this.lastRulesResponse + 5000L) {
 | |
| 			byte[] bs = this.rulesResponse.toByteArray();
 | |
| 			byte[] cs = this.getIdentBytes(requestPacket.getSocketAddress());
 | |
| 			bs[1] = cs[0];
 | |
| 			bs[2] = cs[1];
 | |
| 			bs[3] = cs[2];
 | |
| 			bs[4] = cs[3];
 | |
| 			return bs;
 | |
| 		} else {
 | |
| 			this.lastRulesResponse = l;
 | |
| 			this.rulesResponse.reset();
 | |
| 			this.rulesResponse.write(0);
 | |
| 			this.rulesResponse.writeBytes(this.getIdentBytes(requestPacket.getSocketAddress()));
 | |
| 			this.rulesResponse.writeString("splitnum");
 | |
| 			this.rulesResponse.write(128);
 | |
| 			this.rulesResponse.write(0);
 | |
| 			this.rulesResponse.writeString("hostname");
 | |
| 			this.rulesResponse.writeString(this.serverName);
 | |
| 			this.rulesResponse.writeString("gametype");
 | |
| 			this.rulesResponse.writeString("SMP");
 | |
| 			this.rulesResponse.writeString("game_id");
 | |
| 			this.rulesResponse.writeString("MINECRAFT");
 | |
| 			this.rulesResponse.writeString("version");
 | |
| 			this.rulesResponse.writeString(this.serverInterface.getServerVersion());
 | |
| 			this.rulesResponse.writeString("plugins");
 | |
| 			this.rulesResponse.writeString(this.serverInterface.getPluginNames());
 | |
| 			this.rulesResponse.writeString("map");
 | |
| 			this.rulesResponse.writeString(this.worldName);
 | |
| 			this.rulesResponse.writeString("numplayers");
 | |
| 			this.rulesResponse.writeString(this.serverInterface.getPlayerCount() + "");
 | |
| 			this.rulesResponse.writeString("maxplayers");
 | |
| 			this.rulesResponse.writeString(this.maxPlayers + "");
 | |
| 			this.rulesResponse.writeString("hostport");
 | |
| 			this.rulesResponse.writeString(this.serverPort + "");
 | |
| 			this.rulesResponse.writeString("hostip");
 | |
| 			this.rulesResponse.writeString(this.hostIp);
 | |
| 			this.rulesResponse.write(0);
 | |
| 			this.rulesResponse.write(1);
 | |
| 			this.rulesResponse.writeString("player_");
 | |
| 			this.rulesResponse.write(0);
 | |
| 			String[] strings = this.serverInterface.getPlayerNames();
 | |
| 
 | |
| 			for (String string : strings) {
 | |
| 				this.rulesResponse.writeString(string);
 | |
| 			}
 | |
| 
 | |
| 			this.rulesResponse.write(0);
 | |
| 			return this.rulesResponse.toByteArray();
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Returns the request ID provided by the authorized client
 | |
| 	 */
 | |
| 	private byte[] getIdentBytes(SocketAddress address) {
 | |
| 		return ((QueryThreadGs4.RequestChallenge)this.validChallenges.get(address)).getIdentBytes();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Returns {@code true} if the client has a valid auth, otherwise {@code false}.
 | |
| 	 */
 | |
| 	private Boolean validChallenge(DatagramPacket requestPacket) {
 | |
| 		SocketAddress socketAddress = requestPacket.getSocketAddress();
 | |
| 		if (!this.validChallenges.containsKey(socketAddress)) {
 | |
| 			return false;
 | |
| 		} else {
 | |
| 			byte[] bs = requestPacket.getData();
 | |
| 			return ((QueryThreadGs4.RequestChallenge)this.validChallenges.get(socketAddress)).getChallenge()
 | |
| 				== PktUtils.intFromNetworkByteArray(bs, 7, requestPacket.getLength());
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Sends an auth challenge DatagramPacket to the client and adds the client to the queryClients map
 | |
| 	 */
 | |
| 	private void sendChallenge(DatagramPacket requestPacket) throws IOException {
 | |
| 		QueryThreadGs4.RequestChallenge requestChallenge = new QueryThreadGs4.RequestChallenge(requestPacket);
 | |
| 		this.validChallenges.put(requestPacket.getSocketAddress(), requestChallenge);
 | |
| 		this.sendTo(requestChallenge.getChallengeBytes(), requestPacket);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Removes all clients whose auth is no longer valid
 | |
| 	 */
 | |
| 	private void pruneChallenges() {
 | |
| 		if (this.running) {
 | |
| 			long l = Util.getMillis();
 | |
| 			if (l >= this.lastChallengeCheck + 30000L) {
 | |
| 				this.lastChallengeCheck = l;
 | |
| 				this.validChallenges.values().removeIf(requestChallenge -> requestChallenge.before(l));
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	public void run() {
 | |
| 		LOGGER.info("Query running on {}:{}", this.serverIp, this.port);
 | |
| 		this.lastChallengeCheck = Util.getMillis();
 | |
| 		DatagramPacket datagramPacket = new DatagramPacket(this.buffer, this.buffer.length);
 | |
| 
 | |
| 		try {
 | |
| 			while (this.running) {
 | |
| 				try {
 | |
| 					this.socket.receive(datagramPacket);
 | |
| 					this.pruneChallenges();
 | |
| 					this.processPacket(datagramPacket);
 | |
| 				} catch (SocketTimeoutException var8) {
 | |
| 					this.pruneChallenges();
 | |
| 				} catch (PortUnreachableException var9) {
 | |
| 				} catch (IOException var10) {
 | |
| 					this.recoverSocketError(var10);
 | |
| 				}
 | |
| 			}
 | |
| 		} finally {
 | |
| 			LOGGER.debug("closeSocket: {}:{}", this.serverIp, this.port);
 | |
| 			this.socket.close();
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	@Override
 | |
| 	public boolean start() {
 | |
| 		if (this.running) {
 | |
| 			return true;
 | |
| 		} else {
 | |
| 			return !this.initSocket() ? false : super.start();
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Stops the query server and reports the given Exception
 | |
| 	 */
 | |
| 	private void recoverSocketError(Exception exception) {
 | |
| 		if (this.running) {
 | |
| 			LOGGER.warn("Unexpected exception", (Throwable)exception);
 | |
| 			if (!this.initSocket()) {
 | |
| 				LOGGER.error("Failed to recover from exception, shutting down!");
 | |
| 				this.running = false;
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Initializes the query system by binding it to a port
 | |
| 	 */
 | |
| 	private boolean initSocket() {
 | |
| 		try {
 | |
| 			this.socket = new DatagramSocket(this.port, InetAddress.getByName(this.serverIp));
 | |
| 			this.socket.setSoTimeout(500);
 | |
| 			return true;
 | |
| 		} catch (Exception var2) {
 | |
| 			LOGGER.warn("Unable to initialise query system on {}:{}", this.serverIp, this.port, var2);
 | |
| 			return false;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	static class RequestChallenge {
 | |
| 		private final long time = new Date().getTime();
 | |
| 		private final int challenge;
 | |
| 		private final byte[] identBytes;
 | |
| 		private final byte[] challengeBytes;
 | |
| 		private final String ident;
 | |
| 
 | |
| 		public RequestChallenge(DatagramPacket datagramPacket) {
 | |
| 			byte[] bs = datagramPacket.getData();
 | |
| 			this.identBytes = new byte[4];
 | |
| 			this.identBytes[0] = bs[3];
 | |
| 			this.identBytes[1] = bs[4];
 | |
| 			this.identBytes[2] = bs[5];
 | |
| 			this.identBytes[3] = bs[6];
 | |
| 			this.ident = new String(this.identBytes, StandardCharsets.UTF_8);
 | |
| 			this.challenge = RandomSource.create().nextInt(16777216);
 | |
| 			this.challengeBytes = String.format(Locale.ROOT, "\t%s%d\u0000", this.ident, this.challenge).getBytes(StandardCharsets.UTF_8);
 | |
| 		}
 | |
| 
 | |
| 		/**
 | |
| 		 * Returns {@code true} if the auth's creation timestamp is less than the given time, otherwise {@code false}.
 | |
| 		 */
 | |
| 		public Boolean before(long currentTime) {
 | |
| 			return this.time < currentTime;
 | |
| 		}
 | |
| 
 | |
| 		/**
 | |
| 		 * Returns the random challenge number assigned to this auth
 | |
| 		 */
 | |
| 		public int getChallenge() {
 | |
| 			return this.challenge;
 | |
| 		}
 | |
| 
 | |
| 		/**
 | |
| 		 * Returns the auth challenge value
 | |
| 		 */
 | |
| 		public byte[] getChallengeBytes() {
 | |
| 			return this.challengeBytes;
 | |
| 		}
 | |
| 
 | |
| 		/**
 | |
| 		 * Returns the request ID provided by the client.
 | |
| 		 */
 | |
| 		public byte[] getIdentBytes() {
 | |
| 			return this.identBytes;
 | |
| 		}
 | |
| 
 | |
| 		public String getIdent() {
 | |
| 			return this.ident;
 | |
| 		}
 | |
| 	}
 | |
| }
 |