package launcher.client; import com.eclipsesource.json.Json; import com.eclipsesource.json.JsonObject; import launcher.LauncherAPI; import launcher.client.ClientProfile.Version; import launcher.helper.IOHelper; import launcher.helper.JVMHelper; import launcher.helper.LogHelper; import launcher.helper.VerifyHelper; import launcher.serialize.HInput; import launcher.serialize.HOutput; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.util.Objects; import java.util.regex.Pattern; public final class ServerPinger { // Constants private static final String LEGACY_PING_HOST_MAGIC = "§1"; private static final String LEGACY_PING_HOST_CHANNEL = "MC|PingHost"; private static final Pattern LEGACY_PING_HOST_DELIMETER = Pattern.compile("\0", Pattern.LITERAL); private static final int PACKET_LENGTH = 65535; // Instance private final InetSocketAddress address; private final Version version; // Cache private final Object cacheLock = new Object(); private Result cache = null; private Throwable cacheError = null; private long cacheUntil = Long.MIN_VALUE; @LauncherAPI public ServerPinger(InetSocketAddress address, Version version) { this.address = Objects.requireNonNull(address, "address"); this.version = Objects.requireNonNull(version, "version"); } private static String readUTF16String(HInput input) throws IOException { int length = input.readUnsignedShort() << 1; byte[] encoded = input.readByteArray(-length); return new String(encoded, StandardCharsets.UTF_16BE); } private static void writeUTF16String(HOutput output, String s) throws IOException { output.writeShort((short) s.length()); output.stream.write(s.getBytes(StandardCharsets.UTF_16BE)); } @LauncherAPI public Result ping() { synchronized (cacheLock) { // Update ping cache if (System.currentTimeMillis() >= cacheUntil) { try { cache = doPing(); cacheError = null; } catch (Throwable exc) { cache = null; cacheError = exc; } finally { cacheUntil = System.currentTimeMillis() + IOHelper.SOCKET_TIMEOUT; } } // Verify is result available if (cacheError != null) { JVMHelper.UNSAFE.throwException(cacheError); } // We're done return cache; } } private Result doPing() throws IOException { try (Socket socket = IOHelper.newSocket()) { socket.connect(IOHelper.resolve(address), IOHelper.SOCKET_TIMEOUT); try (HInput input = new HInput(socket.getInputStream()); HOutput output = new HOutput(socket.getOutputStream())) { return version.compareTo(Version.MC172) >= 0 ? modernPing(input, output) : legacyPing(input, output, version.compareTo(Version.MC164) >= 0); } } } private Result legacyPing(HInput input, HOutput output, boolean mc16) throws IOException { output.writeUnsignedByte(0xFE); // 254 packet ID, Server list ping output.writeUnsignedByte(0x01); // Server ping payload if (mc16) { output.writeUnsignedByte(0xFA); // 250 packet ID, Custom payload writeUTF16String(output, LEGACY_PING_HOST_CHANNEL); // Custom payload name // Prepare custom payload packet byte[] customPayloadPacket; try (ByteArrayOutputStream packetArray = IOHelper.newByteArrayOutput()) { try (HOutput packetOutput = new HOutput(packetArray)) { packetOutput.writeUnsignedByte(78); // Protocol version // Для пинга можно указывать любой (здесь с 1.6.4) writeUTF16String(packetOutput, address.getHostString()); // Server address packetOutput.writeInt(address.getPort()); // Server port } customPayloadPacket = packetArray.toByteArray(); } // Write custom payload packet output.writeShort((short) customPayloadPacket.length); output.stream.write(customPayloadPacket); } output.flush(); // Raed kick (response) packet int kickPacketID = input.readUnsignedByte(); if (kickPacketID != 0xFF) { throw new IOException("Illegal kick packet ID: " + kickPacketID); } // Read and parse response String response = readUTF16String(input); LogHelper.debug("Ping response (legacy): '%s'", response); String[] splitted = LEGACY_PING_HOST_DELIMETER.split(response); if (splitted.length != 6) { throw new IOException("Tokens count mismatch"); } // Verify all parts String magic = splitted[0]; if (!magic.equals(LEGACY_PING_HOST_MAGIC)) { throw new IOException("Magic string mismatch: " + magic); } int protocol = Integer.parseInt(splitted[1]); if (protocol != 78) // Смотри строку 123 { throw new IOException("Protocol mismatch: " + protocol); } String clientVersion = splitted[2]; if (!clientVersion.equals(version.name)) { throw new IOException(String.format("Version mismatch: '%s'", clientVersion)); } int onlinePlayers = VerifyHelper.verifyInt(Integer.parseInt(splitted[4]), VerifyHelper.NOT_NEGATIVE, "onlinePlayers can't be < 0"); int maxPlayers = VerifyHelper.verifyInt(Integer.parseInt(splitted[5]), VerifyHelper.NOT_NEGATIVE, "maxPlayers can't be < 0"); // Return ping status return new Result(onlinePlayers, maxPlayers, response); } private Result modernPing(HInput input, HOutput output) throws IOException { // Prepare handshake packet byte[] handshakePacket; try (ByteArrayOutputStream packetArray = IOHelper.newByteArrayOutput()) { try (HOutput packetOutput = new HOutput(packetArray)) { packetOutput.writeVarInt(0x0); // Handshake packet ID packetOutput.writeVarInt(-1); // Protocol version // Опять же для пинга версия протокола не важна packetOutput.writeString(address.getHostString(), 0); // Server address packetOutput.writeShort((short) address.getPort()); // Server port packetOutput.writeVarInt(0x1); // Next state - status } handshakePacket = packetArray.toByteArray(); } // Write handshake packet output.writeByteArray(handshakePacket, PACKET_LENGTH); // Request status packet output.writeVarInt(1); // Status packet size (single byte) output.writeVarInt(0x0); // Status packet ID output.flush(); // ab is a dirty fix for some servers (noticed KCauldron 1.7.10) int ab = 0; while (ab <= 0) { ab = IOHelper.verifyLength(input.readVarInt(), PACKET_LENGTH); } // Read outer status response packet ID String response; byte[] statusPacket = input.readByteArray(-ab); try (HInput packetInput = new HInput(statusPacket)) { int statusPacketID = packetInput.readVarInt(); if (statusPacketID != 0x0) { throw new IOException("Illegal status packet ID: " + statusPacketID); } response = packetInput.readString(PACKET_LENGTH); LogHelper.debug("Ping response (modern): '%s'", response); } // Parse JSON response JsonObject object = Json.parse(response).asObject(); JsonObject playersObject = object.get("players").asObject(); int online = playersObject.get("online").asInt(); int max = playersObject.get("max").asInt(); // Return ping status return new Result(online, max, response); } public static final class Result { @LauncherAPI public final int onlinePlayers; @LauncherAPI public final int maxPlayers; @LauncherAPI public final String raw; public Result(int onlinePlayers, int maxPlayers, String raw) { this.onlinePlayers = VerifyHelper.verifyInt(onlinePlayers, VerifyHelper.NOT_NEGATIVE, "onlinePlayers can't be < 0"); this.maxPlayers = VerifyHelper.verifyInt(maxPlayers, VerifyHelper.NOT_NEGATIVE, "maxPlayers can't be < 0"); this.raw = raw; } @LauncherAPI public boolean isOverfilled() { return onlinePlayers >= maxPlayers; } } }