Compare commits
10 commits
34b6a5061d
...
fefd3eee9a
| Author | SHA1 | Date | |
|---|---|---|---|
| fefd3eee9a | |||
| 871ffbdc2c | |||
|
|
26e0436b91 | ||
|
|
060afeeb36 | ||
|
|
56d4318dbe | ||
|
|
9f0ba79d91 | ||
|
|
6729de5894 | ||
|
|
7608e43669 | ||
|
|
5932933f77 | ||
|
|
ffd0758857 |
6 changed files with 301 additions and 75 deletions
2
pom.xml
2
pom.xml
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
<groupId>net.rubeta</groupId>
|
||||
<artifactId>LegacyPing</artifactId>
|
||||
<version>1.2</version>
|
||||
<version>2.2</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>LegacyPing</name>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ServerThread> 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<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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,3 +2,5 @@ host: '0.0.0.0'
|
|||
port:
|
||||
- 25566
|
||||
- 25567
|
||||
displayedVersion: 'b1.7.3'
|
||||
displayedProtocol: 17
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue