Search code examples
javasocketspingminecraftbukkit

Minecraft - Bukkit Sockets


I tried to get MOTD of the remote server, but I can't get colors. When MOTD is colored, the plugin isn't working. I know why, but I don't know how to resolve it.

public PingServer(String host, int port) {
    this.host = host;
    this.port = port;

    try {
        socket.connect(new InetSocketAddress(host, port));
        OutputStream out = socket.getOutputStream();
        InputStream in = socket.getInputStream();
        out.write(0xFE);

        int b;
        StringBuffer str = new StringBuffer();
        while ((b = in.read()) != -1) {
            if (b != 0 && b > 16 && b != 255 && b != 23 && b != 24) {
                str.append((char) b);
            }
        }

        data = str.toString().split("§");
        data[0] = data[0].substring(1, data[0].length());
    } catch (IOException e) {
        e.printStackTrace();
    }
}

According to the specification, the plugin will get response like this: MOTD§ONLINEPLAYERS§MAXPLAYERS, which should be split on § to get the different portions. However, § is also used for chat messages, and I'm not sure how to differentiate between the two. How can I work around this?


Solution

  • You're currently using the legacy server list ping, designed for beta 1.8 to 1.3. That one is triggered via sending just FE to the server. While current servers still support this ping, it's very old and has several flaws (including the one you found).

    You should instead perform the current ping. While this is slightly more complicated, you don't need to implement much of the protocol to actually perform it.

    There's only one complicated portion of the protocol you need to know about: VarInts. These are somewhat complicated because they take a varying number of bytes depending on the value. And as such, you have a packet length that can be somewhat hard to calculate.

    /** See http://wiki.vg/Protocol_version_numbers.  47 = 1.8.x */
    private static final int PROTOCOL_VERSION_NUMBER = 47;
    private static final int STATUS_PROTOCOL = 1;
    private static final JsonParser PARSER = new JsonParser();
    
    /** Pings a server, returning the MOTD */
    public static String pingServer(String host, int port) {
        this.host = host;
        this.port = port;
    
        try {
            socket.connect(new InetSocketAddress(host, port));
            OutputStream out = socket.getOutputStream();
            InputStream in = socket.getInputStream();
    
            byte[] hostBytes = host.getBytes("UTF-8");
            int handshakeLength =
                    varIntLength(0) + // Packet ID
                    varIntLength(PROTOCOL_VERSION_NUMBER) + // Protocol version number
                    varIntLength(hostBytes.length) + hostBytes.length + // Host
                    2 + // Port
                    varIntLength(STATUS_PROTOCOL);  // Next state
    
            writeVarInt(handshakeLength, out);
            writeVarInt(0, out);  // Handshake packet
            writeVarInt(PROTOCOL_VERSION_NUMBER, out);
            writeVarInt(hostBytes.length, out);
            out.write(hostBytes);
            out.write((port & 0xFF00) >> 8);
            out.write(port & 0xFF);
            writeVarInt(STATUS_PROTOCOL, out);
    
            writeVarInt(varIntLength(0));
            writeVarInt(0);  // Request packet (has no payload)
    
            int packetLength = readVarInt(in);
            int payloadLength = readVarInt(in);
            byte[] payloadBytes = new int[payloadLength];
            int readLength = in.read(payloadBytes);
            if (readLength < payloadLength) {
                throw new RuntimeException("Unexpected end of stream");
            }
            String payload = new String(payloadBytes, "UTF-8");
    
            // Now you need to parse the JSON; this is using GSON
            // See https://github.com/google/gson
            // and http://www.javadoc.io/doc/com.google.code.gson/gson/2.8.0
            JsonObject element = (JsonObject) PARSER.parse(payload);
            JsonElement description = element.get("description");
            // This is a naive implementation; it assumes a specific format for the description
            // rather than parsing the entire chat format.  But it works for the way the
            // notchian server impmlements the ping.
            String motd = ((JsonObject) description).get("text").getAsString();
    
            return motd;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
    
    public static int varIntLength(int value) {
        int length = 0;
        do {
            // Note: >>> means that the sign bit is shifted with the rest of the number rather than being left alone
            value >>>= 7;
            length++;
        } while (value != 0);
    }
    
    public static void writeVarInt(int value, OutputStream out) {
        do {
            byte temp = (byte)(value & 0b01111111);
            // Note: >>> means that the sign bit is shifted with the rest of the number rather than being left alone
            value >>>= 7;
            if (value != 0) {
                temp |= 0b10000000;
            }
            out.write(temp);
        } while (value != 0);
    }
    public static int readVarInt(InputStream in) {
        int numRead = 0;
        int result = 0;
        int read;
        do {
            read = in.read();
            if (read < 0) {
                throw new RuntimeException("Unexpected end of stream");
            }
            int value = (read & 0b01111111);
            result |= (value << (7 * numRead));
    
            numRead++;
            if (numRead > 5) {
                throw new RuntimeException("VarInt is too big");
            }
        } while ((read & 0b10000000) != 0);
    
        return result;
    }
    

    The current ping does use JSON, which means you need to use GSON. Also, this implementation makes some assumptions about the chat format; this implementation could break on custom servers that implement chat more completely, but it'll work for servers that embed § into the motd instead of using the more complete chat system (this includes the Notchian server implementation).


    If you need to use the legacy ping, you can assume that the 2nd-to-last § marks the end of the MOTD (rather than the 1st §). Something like this:

    String legacyPingResult = str.toString();
    String[] data = new String[3];
    int splitPoint2 = legacyPingResult.lastIndexOf('§');
    int splitPoint1 = legacyPingResult.lastIndexOf('§', splitPoint2 - 1);
    
    data[0] = legacyPingResult.substring(0, splitPoint1);
    data[1] = legacyPingResult.substring(splitPoint1 + 1, splitPoint2);
    data[2] = legacyPingResult.substring(splitPoint2 + 1);
    

    However, I still don't recommend using the legacy ping.