diff --git a/pom.xml b/pom.xml index 0bbf5cf..ded2704 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ net.rubeta LegacyPing - 1.2 + 2.2 jar LegacyPing diff --git a/src/main/java/net/rubeta/legacyping/CrutchBukkitMirrorThread.java b/src/main/java/net/rubeta/legacyping/CrutchBukkitMirrorThread.java new file mode 100644 index 0000000..b422206 --- /dev/null +++ b/src/main/java/net/rubeta/legacyping/CrutchBukkitMirrorThread.java @@ -0,0 +1,106 @@ +package net.rubeta.legacyping; + +import net.minecraft.network.packets.Packet; +import net.minecraft.network.packets.Packet255KickDisconnect; +import org.bukkit.craftbukkit.CraftServer; +import org.bukkit.craftbukkit.event.CraftEventFactory; +import org.bukkit.event.server.ServerListPingEvent; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.net.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class CrutchBukkitMirrorThread extends Thread { + + private final AtomicBoolean working = new AtomicBoolean(false); + private final Logger logger; + private ServerSocket serverSocket; + private final InetAddress address; + private final LegacyPing plugin; + + public CrutchBukkitMirrorThread(final LegacyPing plugin) { + this.plugin = plugin; + this.logger = Logger.getLogger("mirror:25565"); + + try { + this.address = InetAddress.getByName("0.0.0.0"); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + } + + @Override + public void run() { + this.working.set(true); + while (this.working.get()) { + try { + this.serverSocket = new ServerSocket(25565, 10, address); + this.serverSocket.setSoTimeout((int) TimeUnit.SECONDS.toMillis(10L)); + this.serverSocket.setPerformancePreferences(0, 2, 1); + } catch (IOException e) { + this.logger.log(Level.WARNING, "Failed to start ServerSocket", e); + this.pause(); + continue; + } + + while (!this.serverSocket.isClosed()) { + if (!this.working.get()) { + break; + } + + InetAddress address = null; + try (final Socket socket = this.serverSocket.accept()) { + final DataInputStream inputStream = new DataInputStream(socket.getInputStream()); + final DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream()); + address = socket.getInetAddress(); + socket.setTrafficClass(SocketOptions.TCP_NODELAY & SocketOptions.IP_TOS); + inputStream.readInt(); // Packet length + Packet packet = Packet.readPacket(inputStream); + if (packet != null && packet.getPacketId() == 254) { + CraftServer server = (CraftServer) this.plugin.getServer(); + ServerListPingEvent pingEvent = CraftEventFactory.callServerListPingEvent( + server, address, server.getServer().motd, + server.getOnlinePlayers().length, server.getMaxPlayers() + ); + String s = pingEvent.getMotd() + "\u0000" + server.getOnlinePlayers().length + "\u0000" + pingEvent.getMaxPlayers(); + Packet255KickDisconnect response = new Packet255KickDisconnect(s); + outputStream.writeInt(s.length() + 1); + outputStream.writeByte(255); + response.writePacketData(outputStream); + } + } catch (SocketTimeoutException | EOFException ignore) { + } catch (Exception ex) { + this.logger.warning("An exception occurred while communicating with " + address); + this.logger.log(Level.WARNING, "Exception in " + this.getName(), ex); + } + } + } + } + + + public void shutdown() { + this.working.set(false); + try { + this.serverSocket.close(); + } catch (IOException ignore) { + } + } + + + void pause() { + this.pause(TimeUnit.SECONDS.toMillis(30L)); + } + + void pause(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException ignored) { + } + } +} diff --git a/src/main/java/net/rubeta/legacyping/LegacyPing.java b/src/main/java/net/rubeta/legacyping/LegacyPing.java index 1b3b756..fc15d1d 100644 --- a/src/main/java/net/rubeta/legacyping/LegacyPing.java +++ b/src/main/java/net/rubeta/legacyping/LegacyPing.java @@ -1,5 +1,8 @@ package net.rubeta.legacyping; +import org.bukkit.ChatColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.plugin.java.JavaPlugin; @@ -10,25 +13,54 @@ import java.util.logging.Level; public final class LegacyPing extends JavaPlugin { private final List threads = new ArrayList<>(); + private CrutchBukkitMirrorThread mirror = null; @Override public void onEnable() { this.saveDefaultConfig(); + this.startThreads(); + } + + private void startThreads() { final FileConfiguration config = this.getConfig(); final String host = config.getString("host", "0.0.0.0"); final List ports = config.getIntegerList("port"); + final String version = config.getString("displayedVersion", "b1.7.3"); + final int protocol = config.getInt("displayedProtocol", 14); for (int port : ports) { try { - this.threads.add(new ServerThread(this.getServer(), host, port)); + this.threads.add(new ServerThread(this, host, port, version, protocol)); } catch (IOException e) { this.getLogger().log(Level.WARNING, "Failed to create ServerSocket", e); } } + if (config.getBoolean("enableMirrorPort", false)) { + this.mirror = new CrutchBukkitMirrorThread(this); + this.mirror.start(); + } this.threads.forEach(Thread::start); } @Override public void onDisable() { + if (this.mirror != null) { + this.mirror.shutdown(); + } this.threads.forEach(ServerThread::shutdown); } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!sender.isOp()) { + return true; + } + if (this.mirror != null) { + this.mirror.shutdown(); + } + this.threads.forEach(ServerThread::shutdown); + this.threads.clear(); + this.startThreads(); + sender.sendMessage(ChatColor.GOLD + "Restarted"); + return true; + } } diff --git a/src/main/java/net/rubeta/legacyping/ServerThread.java b/src/main/java/net/rubeta/legacyping/ServerThread.java index d03ee9b..8448a5f 100644 --- a/src/main/java/net/rubeta/legacyping/ServerThread.java +++ b/src/main/java/net/rubeta/legacyping/ServerThread.java @@ -5,67 +5,127 @@ import org.bukkit.craftbukkit.CraftServer; import java.io.*; import java.net.*; +import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; public class ServerThread extends Thread { private static final String MODERN_RESPONSE = - "{\"version\": {\"name\": \"b1.7.3\",\"protocol\": -17}," + - "\"players\": {\"max\": %max%,\"online\": %now%,\"sample\": []}," + - "\"description\": {\"text\": \"%motd%\"},\"enforcesSecureChat\": false}"; - private final ServerSocket serverSocket; + "{\"version\": {\"name\": \"%version%\",\"protocol\": %protocol%}," + + "\"players\": {\"max\": %max%,\"online\": %now%}," + + "\"description\": {\"text\": \"%motd%\"}," + + "\"enforcesSecureChat\": false,\"previewsChat\": false}"; + private static final String DISCONNECT_REASON = "[\"\",{\"text\":\"This is b1.7.3 server. Visit \"}," + + "{\"text\":\"https://rubeta.net\",\"underlined\":true},{\"text\":\" for more information.\"}]"; + private static final String SLOWSTACK_ID = "slowstack.tv"; + private static final String MATSCAN_ID = "matscan"; private final AtomicBoolean working = new AtomicBoolean(false); private final Server minecraftServer; + private final String version; + private final int protocol; + private final InetAddress address; + private final int port; + private final Logger logger; + private ServerSocket serverSocket; - public ServerThread(final Server server, final String host, int port) throws IOException { - this.minecraftServer = server; + public ServerThread(final LegacyPing plugin, final String host, int port, String version, int protocol) throws IOException { + this.minecraftServer = plugin.getServer(); + this.logger = plugin.getLogger(); this.setName("LegacyPing thread listening " + host + ":" + port); - final InetAddress address = InetAddress.getByName(host); - this.serverSocket = new ServerSocket(port, 10, address); - this.serverSocket.setSoTimeout((int) TimeUnit.SECONDS.toMillis(10L)); - this.serverSocket.setPerformancePreferences(0, 2, 1); + this.version = version; + this.protocol = protocol; + this.address = InetAddress.getByName(host); + this.port = port; } public ServerThread(final String host, int port) throws IOException { this.minecraftServer = null; this.setName("LegacyPing thread listening " + host + ":" + port); - final InetAddress address = InetAddress.getByName(host); - this.serverSocket = new ServerSocket(port, 10, address); - this.serverSocket.setSoTimeout((int) TimeUnit.SECONDS.toMillis(10L)); - this.serverSocket.setPerformancePreferences(0, 2, 1); + this.logger = Logger.getLogger(host + ":" + port); + this.version = "dummy"; + this.protocol = 0; + this.address = InetAddress.getByName(host); + this.port = port; } public static void main(String[] args) throws IOException { - new ServerThread("", 25565).start(); + new ServerThread("", 25569).start(); } + @SuppressWarnings("unused") @Override public void run() { this.working.set(true); - while (this.working.get() && !this.serverSocket.isClosed()) { + while (this.working.get()) { try { - final Socket socket = this.serverSocket.accept(); - socket.setTrafficClass(SocketOptions.TCP_NODELAY & SocketOptions.IP_TOS); - System.out.println("[LegacyPing] Got new ping from " + socket.getRemoteSocketAddress()); - final InputStream inputStream = socket.getInputStream(); - final OutputStream outputStream = socket.getOutputStream(); - final int firstByte = inputStream.read(); - switch (firstByte) { - case 0: - this.responseForRuBetaModdedClient(outputStream); - break; - case 254: - this.responseForLegacyPing(outputStream); - break; - default: - this.responseForModernPing(outputStream); + this.serverSocket = new ServerSocket(port, 10, address); + this.serverSocket.setSoTimeout((int) TimeUnit.SECONDS.toMillis(10L)); + this.serverSocket.setPerformancePreferences(0, 2, 1); + } catch (IOException e) { + this.logger.log(Level.WARNING, "Failed to start ServerSocket", e); + this.pause(); + continue; + } + + while (!this.serverSocket.isClosed()) { + if (!this.working.get()) { + break; + } + InetAddress address = null; + try (final Socket socket = this.serverSocket.accept()) { + final DataInputStream inputStream = new DataInputStream(socket.getInputStream()); + final DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream()); + address = socket.getInetAddress(); + socket.setTrafficClass(SocketOptions.TCP_NODELAY & SocketOptions.IP_TOS); + + final int packetLength = inputStream.readByte() | inputStream.readByte() << 1; // idk why it doesn't a VarInt + final int protocolVersion = this.readVarInt(inputStream); + final String hostname = this.readUtf(inputStream); + final int port = inputStream.readShort(); + final int status = this.readVarInt(inputStream); + this.logger.info("Ping from " + socket.getRemoteSocketAddress() + " to " + hostname + ":" + port); + if (status != 2) { + if (hostname.equalsIgnoreCase(SLOWSTACK_ID)) { + this.responseForModernPing(outputStream, MODERN_RESPONSE.replace( + "%motd%", "%motd% \nHello, slowstack.tv!" + )); + } else if (hostname.equalsIgnoreCase(MATSCAN_ID)) { + this.responseForModernPing(outputStream, MODERN_RESPONSE + .replace("%motd%", "%motd% \nHello, matscan!") + .replace("%version%", "⚠ Error") + ); + } else { + this.responseForModernPing(outputStream, MODERN_RESPONSE); + } + } else { + this.responseForModernPing(outputStream, DISCONNECT_REASON); + socket.close(); + continue; + } + outputStream.flush(); + + this.pause(50L); + if (socket.isClosed() || !socket.isConnected() || inputStream.available() == 0) { + continue; + } + inputStream.readShort(); // legacy data + this.pause(50L); + if (socket.isClosed() || !socket.isConnected() || inputStream.available() == 0) { + continue; + } + this.writeVarInt(outputStream, this.readVarInt(inputStream)); // Packet length (9) + this.writeVarInt(outputStream, this.readVarInt(inputStream)); // Packet id (1) + outputStream.writeLong(inputStream.readLong()); // timestamp + } catch (SocketTimeoutException | EOFException ignore) { + } catch (Exception ex) { + if (this.working.get()) { + this.logger.warning("An exception occurred while communicating with " + address); + this.logger.log(Level.WARNING, "Exception in " + this.getName(), ex); + } } - outputStream.flush(); - socket.close(); - } catch (SocketTimeoutException ignore) { - } catch (IOException ex) { - System.out.println("Exception caught: " + ex); } } } @@ -78,44 +138,31 @@ public class ServerThread extends Thread { } } - private void responseForRuBetaModdedClient(final OutputStream outputStream) throws IOException { - final ByteArrayOutputStream result = new ByteArrayOutputStream(); - final DataOutputStream dos = new DataOutputStream(result); - dos.write(255); // Packet 0xFF (PacketDisconnect) - final String response = this.getMotd() + '\u0000' + this.getOnlinePlayers() + '\u0000' + this.getMaxPlayers(); - dos.writeShort(response.length()); - for (char c : response.toCharArray()) { - dos.writeChar(c); - } - // Packet data end - final DataOutputStream clientOutput = new DataOutputStream(outputStream); - clientOutput.writeInt(result.size()); - clientOutput.write(result.toByteArray()); + void pause() { + this.pause(TimeUnit.SECONDS.toMillis(30L)); } - @SuppressWarnings("UnnecessaryUnicodeEscape") - private void responseForLegacyPing(final OutputStream outputStream) throws IOException { - final DataOutputStream clientOutput = new DataOutputStream(outputStream); - clientOutput.write(255); - final String response = this.getMotd() + '\u00a7' + this.getOnlinePlayers() + '\u00a7' + this.getMaxPlayers(); - clientOutput.writeShort(response.length()); - for (char c : response.toCharArray()) { - clientOutput.writeChar(c); + void pause(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException ignored) { } - // Packet data end } - private void responseForModernPing(final OutputStream outputStream) throws IOException { - final String responseJson = MODERN_RESPONSE.replace("%max%", String.valueOf(this.getMaxPlayers())) + private void responseForModernPing(final DataOutputStream outputStream, String response) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + final String responseJson = response + .replace("%version%", String.valueOf(this.version)) + .replace("%protocol%", String.valueOf(this.protocol)) + .replace("%max%", String.valueOf(this.getMaxPlayers())) .replace("%now%", String.valueOf(this.getOnlinePlayers())) .replace("%motd%", this.getMotd()); - byte[] bytes = responseJson.getBytes(); - final DataOutputStream clientOutput = new DataOutputStream(outputStream); - this.writeVarInt(clientOutput, bytes.length + 3); - this.writeVarInt(clientOutput, 0); - this.writeVarInt(clientOutput, bytes.length); - clientOutput.write(bytes); - // Packet data end + this.writeVarInt(dos, 0); + this.writeUtf(dos, responseJson); + dos.close(); + this.writeVarInt(outputStream, baos.size()); + outputStream.write(baos.toByteArray()); } private String getMotd() { @@ -130,14 +177,46 @@ public class ServerThread extends Thread { return this.minecraftServer == null ? 37 : this.minecraftServer.getMaxPlayers(); } + public int readVarInt(DataInputStream in) { + int i = 0; + int j = 0; + try { + byte b0; + do { + b0 = in.readByte(); + i |= (b0 & 127) << j++ * 7; + if (j > 5) { + throw new RuntimeException("VarInt too big"); + } + } while ((b0 & 128) == 128); + } catch (Exception ignore) { + return 0; + } + return i; + } + private void writeVarInt(DataOutputStream out, int paramInt) throws IOException { - while (true) { - if ((paramInt & 0xFFFFFF80) == 0) { - out.writeByte(paramInt); - return; - } - out.writeByte(paramInt & 0x7F | 0x80); + while ((paramInt & 0xffffff80) != 0) { + out.writeByte(paramInt & 0x7f | 0x80); paramInt >>>= 7; } + out.writeByte(paramInt); + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + private String readUtf(DataInputStream in) throws IOException { + int length = this.readVarInt(in); + byte[] data = new byte[length]; + in.read(data); + return new String(data, StandardCharsets.UTF_8); + } + + private void writeUtf(DataOutputStream out, String string) throws IOException { + byte[] data = string.getBytes(StandardCharsets.UTF_8); + if (data.length > 32767) { + throw new RuntimeException("String too big (was " + data.length + " bytes encoded, max 32767"); + } + this.writeVarInt(out, data.length); + out.write(data); } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 803f05f..f06ef01 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -2,3 +2,5 @@ host: '0.0.0.0' port: - 25566 - 25567 +displayedVersion: 'b1.7.3' +displayedProtocol: 17 diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 284ec33..20fa2c4 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,3 +1,10 @@ name: LegacyPing version: '${project.version}' main: net.rubeta.legacyping.LegacyPing +commands: + legacyping: + permission: legacyping.command.legacyping + +permissions: + legacyping.command.legacyping: + default: op