Compare commits

..

No commits in common. "fefd3eee9a7d3ddf24084022aaef227d741dea68" and "34b6a5061dbffe55320bfb4fddcde32333532fcb" have entirely different histories.

6 changed files with 77 additions and 303 deletions

View file

@ -6,7 +6,7 @@
<groupId>net.rubeta</groupId> <groupId>net.rubeta</groupId>
<artifactId>LegacyPing</artifactId> <artifactId>LegacyPing</artifactId>
<version>2.2</version> <version>1.2</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>LegacyPing</name> <name>LegacyPing</name>

View file

@ -1,106 +0,0 @@
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) {
}
}
}

View file

@ -1,8 +1,5 @@
package net.rubeta.legacyping; 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.configuration.file.FileConfiguration;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
@ -13,54 +10,25 @@ import java.util.logging.Level;
public final class LegacyPing extends JavaPlugin { public final class LegacyPing extends JavaPlugin {
private final List<ServerThread> threads = new ArrayList<>(); private final List<ServerThread> threads = new ArrayList<>();
private CrutchBukkitMirrorThread mirror = null;
@Override @Override
public void onEnable() { public void onEnable() {
this.saveDefaultConfig(); this.saveDefaultConfig();
this.startThreads();
}
private void startThreads() {
final FileConfiguration config = this.getConfig(); final FileConfiguration config = this.getConfig();
final String host = config.getString("host", "0.0.0.0"); final String host = config.getString("host", "0.0.0.0");
final List<Integer> ports = config.getIntegerList("port"); final List<Integer> ports = config.getIntegerList("port");
final String version = config.getString("displayedVersion", "b1.7.3");
final int protocol = config.getInt("displayedProtocol", 14);
for (int port : ports) { for (int port : ports) {
try { try {
this.threads.add(new ServerThread(this, host, port, version, protocol)); this.threads.add(new ServerThread(this.getServer(), host, port));
} catch (IOException e) { } catch (IOException e) {
this.getLogger().log(Level.WARNING, "Failed to create ServerSocket", 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); this.threads.forEach(Thread::start);
} }
@Override @Override
public void onDisable() { public void onDisable() {
if (this.mirror != null) {
this.mirror.shutdown();
}
this.threads.forEach(ServerThread::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;
}
} }

View file

@ -5,127 +5,67 @@ import org.bukkit.craftbukkit.CraftServer;
import java.io.*; import java.io.*;
import java.net.*; import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
public class ServerThread extends Thread { public class ServerThread extends Thread {
private static final String MODERN_RESPONSE = private static final String MODERN_RESPONSE =
"{\"version\": {\"name\": \"%version%\",\"protocol\": %protocol%}," + "{\"version\": {\"name\": \"b1.7.3\",\"protocol\": -17}," +
"\"players\": {\"max\": %max%,\"online\": %now%}," + "\"players\": {\"max\": %max%,\"online\": %now%,\"sample\": []}," +
"\"description\": {\"text\": \"%motd%\"}," + "\"description\": {\"text\": \"%motd%\"},\"enforcesSecureChat\": false}";
"\"enforcesSecureChat\": false,\"previewsChat\": false}"; private final ServerSocket serverSocket;
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 AtomicBoolean working = new AtomicBoolean(false);
private final Server minecraftServer; 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 LegacyPing plugin, final String host, int port, String version, int protocol) throws IOException { public ServerThread(final Server server, final String host, int port) throws IOException {
this.minecraftServer = plugin.getServer(); this.minecraftServer = server;
this.logger = plugin.getLogger();
this.setName("LegacyPing thread listening " + host + ":" + port); this.setName("LegacyPing thread listening " + host + ":" + port);
this.version = version; final InetAddress address = InetAddress.getByName(host);
this.protocol = protocol; this.serverSocket = new ServerSocket(port, 10, address);
this.address = InetAddress.getByName(host); this.serverSocket.setSoTimeout((int) TimeUnit.SECONDS.toMillis(10L));
this.port = port; this.serverSocket.setPerformancePreferences(0, 2, 1);
} }
public ServerThread(final String host, int port) throws IOException { public ServerThread(final String host, int port) throws IOException {
this.minecraftServer = null; this.minecraftServer = null;
this.setName("LegacyPing thread listening " + host + ":" + port); this.setName("LegacyPing thread listening " + host + ":" + port);
this.logger = Logger.getLogger(host + ":" + port); final InetAddress address = InetAddress.getByName(host);
this.version = "dummy"; this.serverSocket = new ServerSocket(port, 10, address);
this.protocol = 0; this.serverSocket.setSoTimeout((int) TimeUnit.SECONDS.toMillis(10L));
this.address = InetAddress.getByName(host); this.serverSocket.setPerformancePreferences(0, 2, 1);
this.port = port;
} }
public static void main(String[] args) throws IOException { public static void main(String[] args) throws IOException {
new ServerThread("", 25569).start(); new ServerThread("", 25565).start();
} }
@SuppressWarnings("unused")
@Override @Override
public void run() { public void run() {
this.working.set(true); this.working.set(true);
while (this.working.get()) { while (this.working.get() && !this.serverSocket.isClosed()) {
try { try {
this.serverSocket = new ServerSocket(port, 10, address); final Socket socket = this.serverSocket.accept();
this.serverSocket.setSoTimeout((int) TimeUnit.SECONDS.toMillis(10L)); socket.setTrafficClass(SocketOptions.TCP_NODELAY & SocketOptions.IP_TOS);
this.serverSocket.setPerformancePreferences(0, 2, 1); System.out.println("[LegacyPing] Got new ping from " + socket.getRemoteSocketAddress());
} catch (IOException e) { final InputStream inputStream = socket.getInputStream();
this.logger.log(Level.WARNING, "Failed to start ServerSocket", e); final OutputStream outputStream = socket.getOutputStream();
this.pause(); final int firstByte = inputStream.read();
continue; switch (firstByte) {
} case 0:
this.responseForRuBetaModdedClient(outputStream);
while (!this.serverSocket.isClosed()) { break;
if (!this.working.get()) { case 254:
break; this.responseForLegacyPing(outputStream);
} break;
InetAddress address = null; default:
try (final Socket socket = this.serverSocket.accept()) { this.responseForModernPing(outputStream);
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);
} }
} }
} }
@ -138,31 +78,44 @@ public class ServerThread extends Thread {
} }
} }
void pause() { private void responseForRuBetaModdedClient(final OutputStream outputStream) throws IOException {
this.pause(TimeUnit.SECONDS.toMillis(30L)); final ByteArrayOutputStream result = new ByteArrayOutputStream();
} final DataOutputStream dos = new DataOutputStream(result);
dos.write(255); // Packet 0xFF (PacketDisconnect)
void pause(long millis) { final String response = this.getMotd() + '\u0000' + this.getOnlinePlayers() + '\u0000' + this.getMaxPlayers();
try { dos.writeShort(response.length());
Thread.sleep(millis); for (char c : response.toCharArray()) {
} catch (InterruptedException ignored) { dos.writeChar(c);
} }
// Packet data end
final DataOutputStream clientOutput = new DataOutputStream(outputStream);
clientOutput.writeInt(result.size());
clientOutput.write(result.toByteArray());
} }
private void responseForModernPing(final DataOutputStream outputStream, String response) throws IOException { @SuppressWarnings("UnnecessaryUnicodeEscape")
ByteArrayOutputStream baos = new ByteArrayOutputStream(); private void responseForLegacyPing(final OutputStream outputStream) throws IOException {
DataOutputStream dos = new DataOutputStream(baos); final DataOutputStream clientOutput = new DataOutputStream(outputStream);
final String responseJson = response clientOutput.write(255);
.replace("%version%", String.valueOf(this.version)) final String response = this.getMotd() + '\u00a7' + this.getOnlinePlayers() + '\u00a7' + this.getMaxPlayers();
.replace("%protocol%", String.valueOf(this.protocol)) clientOutput.writeShort(response.length());
.replace("%max%", String.valueOf(this.getMaxPlayers())) for (char c : response.toCharArray()) {
clientOutput.writeChar(c);
}
// Packet data end
}
private void responseForModernPing(final OutputStream outputStream) throws IOException {
final String responseJson = MODERN_RESPONSE.replace("%max%", String.valueOf(this.getMaxPlayers()))
.replace("%now%", String.valueOf(this.getOnlinePlayers())) .replace("%now%", String.valueOf(this.getOnlinePlayers()))
.replace("%motd%", this.getMotd()); .replace("%motd%", this.getMotd());
this.writeVarInt(dos, 0); byte[] bytes = responseJson.getBytes();
this.writeUtf(dos, responseJson); final DataOutputStream clientOutput = new DataOutputStream(outputStream);
dos.close(); this.writeVarInt(clientOutput, bytes.length + 3);
this.writeVarInt(outputStream, baos.size()); this.writeVarInt(clientOutput, 0);
outputStream.write(baos.toByteArray()); this.writeVarInt(clientOutput, bytes.length);
clientOutput.write(bytes);
// Packet data end
} }
private String getMotd() { private String getMotd() {
@ -177,46 +130,14 @@ public class ServerThread extends Thread {
return this.minecraftServer == null ? 37 : this.minecraftServer.getMaxPlayers(); 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 { private void writeVarInt(DataOutputStream out, int paramInt) throws IOException {
while ((paramInt & 0xffffff80) != 0) { while (true) {
out.writeByte(paramInt & 0x7f | 0x80); if ((paramInt & 0xFFFFFF80) == 0) {
out.writeByte(paramInt);
return;
}
out.writeByte(paramInt & 0x7F | 0x80);
paramInt >>>= 7; 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);
} }
} }

View file

@ -2,5 +2,3 @@ host: '0.0.0.0'
port: port:
- 25566 - 25566
- 25567 - 25567
displayedVersion: 'b1.7.3'
displayedProtocol: 17

View file

@ -1,10 +1,3 @@
name: LegacyPing name: LegacyPing
version: '${project.version}' version: '${project.version}'
main: net.rubeta.legacyping.LegacyPing main: net.rubeta.legacyping.LegacyPing
commands:
legacyping:
permission: legacyping.command.legacyping
permissions:
legacyping.command.legacyping:
default: op