diff --git a/pom.xml b/pom.xml index d294de5..4f0fb9a 100644 --- a/pom.xml +++ b/pom.xml @@ -5,6 +5,13 @@ ServerList 1.0-SNAPSHOT jar + + + commons-cli + commons-cli + 1.5.0 + + diff --git a/src/main/java/com/diamante/serverlist/ClientEmulator.java b/src/main/java/com/diamante/serverlist/ClientEmulator.java new file mode 100644 index 0000000..a745320 --- /dev/null +++ b/src/main/java/com/diamante/serverlist/ClientEmulator.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2022 Diamante + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.diamante.serverlist; + +import java.lang.management.ManagementFactory; + +import java.net.DatagramPacket; +import java.net.DatagramSocket; + +import java.io.IOException; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.nio.channels.IllegalBlockingModeException; + +import java.util.Set; + +/** + * The purpose of this module is to ping the servers on the server list. + * + * @author Diamante + */ +public class ClientEmulator implements Runnable { + + public static final int SERVER_QUERY = 1347374924; + + public static final int SERVER_INFO_SIZE = 2129; + + private final Set servers; + + private static final int SOCKET_TIMEOUT = 4000; + private DatagramSocket socket; + + public ClientEmulator(Set servers) { + try { + socket = new DatagramSocket(); + socket.setSoTimeout(SOCKET_TIMEOUT); + } + catch (SocketException ex) { + System.err.println("ClientEmulator: SocketException while creating new DatagramSocket"); + System.exit(-1); + } + + this.servers = servers; + } + + public ClientEmulator() { + try { + socket = new DatagramSocket(); + socket.setSoTimeout(SOCKET_TIMEOUT); + } + catch (SocketException ex) { + System.err.println("ClientEmulator: SocketException while creating new DatagramSocket"); + System.exit(-1); + } + + this.servers = null; + } + + public void pingSingleServer(String ip) { + var to = Utils.stringToServer(ip); + if (to != null) { + handleServer(to); + } + } + + private void sendDatagramPacket(DatagramPacket packet) { + try { + socket.send(packet); + } + catch (IOException | IllegalArgumentException | IllegalBlockingModeException ex) { + // Socket will timeout + System.err.println("sendDatagramPacket: exception while sending a packet"); + } + } + + private DatagramPacket receiveDatagramPacket() { + var buffer = new byte[SERVER_INFO_SIZE]; + var packet = new DatagramPacket(buffer, buffer.length); + + try { + socket.receive(packet); + System.out.println(String.format("receiveDatagramPacket: Server %s:%d returned a packet", packet.getAddress().toString(), packet.getPort())); + return packet; + } + catch (SocketException ex) { + System.err.println("receiveDatagramPacket: SocketException"); + return null; + } + catch (IllegalBlockingModeException ex) { + System.err.println("receiveDatagramPacket: IllegalBlockingModeException"); + return null; + } + catch (SocketTimeoutException ex) { + System.err.println("receiveDatagramPacket: SocketTimeoutException"); + return null; + } + catch (IOException ex) { + System.err.println("receiveDatagramPacket: IOException"); + return null; + } + } + + private byte[] generateClientPing() { + // Use this as tick + var jvmUpTime = (Long) ManagementFactory.getRuntimeMXBean().getUptime(); + + var tickLE = Utils.longSwap(jvmUpTime.intValue()); + var magicLE = Utils.longSwap(SERVER_QUERY); + + var data = new byte[8]; + + System.arraycopy(magicLE, 0, data, 0, 4); + System.arraycopy(tickLE, 0, data, 4, 4); + + return data; + } + + private void sendPingToServer(Server server) { + var data = generateClientPing(); + // We use the net_port like a client would. + var packet = new DatagramPacket(data, data.length, server.getAddress(), server.getNetPort()); + sendDatagramPacket(packet); + } + + private void handleServer(Server server) { + sendPingToServer(server); + var response = receiveDatagramPacket(); + if (response == null) { + return; + } + + var rawData = response.getData(); + InfoDumper.dumpServerResponse(rawData); + } + + @Override + public void run() { + synchronized (servers) { + var it = servers.iterator(); + while (it.hasNext()) { + var server = it.next(); + handleServer(server); + } + } + } +} diff --git a/src/main/java/com/diamante/serverlist/InfoDumper.java b/src/main/java/com/diamante/serverlist/InfoDumper.java new file mode 100644 index 0000000..4d9a025 --- /dev/null +++ b/src/main/java/com/diamante/serverlist/InfoDumper.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2022 Diamante + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.diamante.serverlist; + +import java.nio.charset.StandardCharsets; + +/** + * + * @author Diamante + */ +public class InfoDumper { + + public static void dumpServerResponse(byte[] data) { + assert data.length == ClientEmulator.SERVER_INFO_SIZE; + + var magicLE = new byte[4]; + + var playersLE = new byte[4]; + var maxPlayersLE = new byte[4]; + + var rawDataLE = new byte[2048]; + + System.arraycopy(data, 0, magicLE, 0, 4); + System.arraycopy(data, 8, playersLE, 0, 4); + System.arraycopy(data, 12, maxPlayersLE, 0, 4); + + System.arraycopy(data, 81, rawDataLE, 0, 2048); + + var playersBE = Utils.longSwap(playersLE); + var maxPlayersBE = Utils.longSwap(maxPlayersLE); + + System.out.println(String.format("dumpServerResponse: Players %d:%d", playersBE, maxPlayersBE)); + + String infoString = new String(rawDataLE, StandardCharsets.UTF_8); + System.out.println(infoString); + } +} diff --git a/src/main/java/com/diamante/serverlist/Main.java b/src/main/java/com/diamante/serverlist/Main.java index f9116c1..2a624c2 100644 --- a/src/main/java/com/diamante/serverlist/Main.java +++ b/src/main/java/com/diamante/serverlist/Main.java @@ -18,24 +18,67 @@ package com.diamante.serverlist; import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; + +import org.apache.commons.cli.ParseException; + /** * * @author Diamante */ public class Main { + private enum Mode { + Emulator, Master, Bad; + } + + private Mode mode; + public static final AtomicBoolean running = new AtomicBoolean(true); - private final MasterServer server; + private MasterServer server; public Main() { - server = new MasterServer(); + mode = Mode.Bad; + } + + private Mode getMode() { + return mode; + } + + private void setMode(Mode mode) { + this.mode = mode; } public MasterServer getServer() { return server; } + private void createMasterServer() { + server = new MasterServer(); + } + + private Options createOptions() { + var options = new Options(); + + var master = new Option("master", "master server mode"); + var emulator = new Option("emulator", "client emulator mode"); + + var ping = Option.builder("ping") + .argName("IP:Port") + .hasArg() + .desc("Server to ping") + .build(); + + options.addOption(master); + options.addOption(emulator); + options.addOption(ping); + + return options; + } + public static void main(String[] args) { Runtime.getRuntime().addShutdownHook(new Thread() { @Override @@ -46,11 +89,37 @@ public class Main { }); var main = new Main(); + var options = main.createOptions(); + var ip = new String(); - while (running.get() && main.getServer().isValid()) { - main.getServer().await(); + var parser = new DefaultParser(); + try { + var line = parser.parse(options, args); + if (line.hasOption("master")) { + main.setMode(Mode.Master); + } else if (line.hasOption("emulator")) { + main.setMode(Mode.Emulator); + } + + if (line.hasOption("ping")) { + ip = line.getOptionValue("ping"); + } + } + catch (ParseException exp) { + System.err.println("Parsing failed. Reason: " + exp.getMessage()); } - main.getServer().stop(); + if (main.getMode() == Mode.Master) { + main.createMasterServer(); + while (running.get() && main.getServer().isValid()) { + main.getServer().await(); + } + main.getServer().stop(); + } else if (main.getMode() == Mode.Emulator) { + var emulator = new ClientEmulator(); + emulator.pingSingleServer(ip); + } + + System.out.println("Normal shutdown"); } } diff --git a/src/main/java/com/diamante/serverlist/MasterServer.java b/src/main/java/com/diamante/serverlist/MasterServer.java index e44d7bc..48a03e3 100644 --- a/src/main/java/com/diamante/serverlist/MasterServer.java +++ b/src/main/java/com/diamante/serverlist/MasterServer.java @@ -101,6 +101,8 @@ public class MasterServer { } else { System.out.println("handlePacket: magic is not recognized"); } + + serverList.dumpOnlineServers(); } public void await() { diff --git a/src/main/java/com/diamante/serverlist/Server.java b/src/main/java/com/diamante/serverlist/Server.java index bf50c1c..1bb8819 100644 --- a/src/main/java/com/diamante/serverlist/Server.java +++ b/src/main/java/com/diamante/serverlist/Server.java @@ -42,11 +42,18 @@ public class Server { this.time = System.currentTimeMillis() / 1000L; } + public Server(InetAddress address, Short netPort) { + this.address = address; + this.netPort = netPort; + + this.time = System.currentTimeMillis() / 1000L; + } + public InetAddress getAddress() { return address; } - - public short getNetPort(){ + + public short getNetPort() { return netPort; } diff --git a/src/main/java/com/diamante/serverlist/ServerList.java b/src/main/java/com/diamante/serverlist/ServerList.java index 30a8426..e104138 100644 --- a/src/main/java/com/diamante/serverlist/ServerList.java +++ b/src/main/java/com/diamante/serverlist/ServerList.java @@ -79,7 +79,7 @@ public class ServerList { /** * The first 4 bytes will contain the numbers of servers we are going to * send in LE Then we have 4 bytes for the IP address in LE Finally 2 bytes - * for the net_port in LE Repeat for each server + * for the net_port in LE. Repeat for each server * * @param version the version of the client * @return the raw bytes to send to the client @@ -117,7 +117,7 @@ public class ServerList { builder.write(portLE); } catch (IOException ex) { - System.err.println("createResponse: IOException while writing server data)"); + System.err.println("createResponse: IOException while writing server data"); } } } @@ -128,6 +128,23 @@ public class ServerList { byteBuffer.clear(); byteBuffer.put(builder.toByteArray()); byteBuffer.flip(); + + try { + builder.close(); + } + catch (IOException ex) { + System.err.println("createResponse: IOException in builder.close()"); + } + return byteBuffer.array(); } + + public void dumpOnlineServers() { + if (!Main.running.get()) { + return; + } + + var thread = new Thread(new ClientEmulator(serverList)); + thread.start(); + } } diff --git a/src/main/java/com/diamante/serverlist/Utils.java b/src/main/java/com/diamante/serverlist/Utils.java index beb6e7b..9a4d85c 100644 --- a/src/main/java/com/diamante/serverlist/Utils.java +++ b/src/main/java/com/diamante/serverlist/Utils.java @@ -16,6 +16,10 @@ */ package com.diamante.serverlist; +import java.net.InetAddress; + +import java.net.UnknownHostException; + import java.nio.BufferOverflowException; import java.nio.ReadOnlyBufferException; @@ -48,7 +52,7 @@ public class Utils { } /** - * Flips the array of in around + * Flips the array around * * @param in array of in. Length must be multiple of 4 */ @@ -113,7 +117,7 @@ public class Utils { return buffer.getShort(0); } catch (BufferOverflowException | ReadOnlyBufferException ex) { - System.err.println("longSwap: BufferOverflowException or ReadOnlyBufferException"); + System.err.println("shortSwap: BufferOverflowException or ReadOnlyBufferException"); return -1; } } @@ -129,4 +133,19 @@ public class Utils { buffer.putShort(in); return buffer.array(); } + + public static Server stringToServer(String in) { + try { + var parts = in.split(":"); + + if (parts.length < 2) { + return null; + } + + return new Server(InetAddress.getByName(parts[0]), Short.parseShort(parts[1])); + } + catch (UnknownHostException ex) { + return null; + } + } }