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