commit 0c2a642ed41beca618eb7490b1da4b2de13fcba8 Author: Diavolo Date: Tue Jul 19 23:17:03 2022 +0200 Init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f43530 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar + +# Eclipse m2e generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath diff --git a/nb-configuration.xml b/nb-configuration.xml new file mode 100644 index 0000000..8e1e369 --- /dev/null +++ b/nb-configuration.xml @@ -0,0 +1,18 @@ + + + + + + true + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..d294de5 --- /dev/null +++ b/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + com.diamante.serverlist + ServerList + 1.0-SNAPSHOT + jar + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + config/sun_checks.xml + + + + + + UTF-8 + 17 + 17 + com.diamante.serverlist.Main + + \ No newline at end of file diff --git a/src/main/java/com/diamante/serverlist/Main.java b/src/main/java/com/diamante/serverlist/Main.java new file mode 100644 index 0000000..f9116c1 --- /dev/null +++ b/src/main/java/com/diamante/serverlist/Main.java @@ -0,0 +1,56 @@ +/* + * 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.util.concurrent.atomic.AtomicBoolean; + +/** + * + * @author Diamante + */ +public class Main { + + public static final AtomicBoolean running = new AtomicBoolean(true); + + private final MasterServer server; + + public Main() { + server = new MasterServer(); + } + + public MasterServer getServer() { + return server; + } + + public static void main(String[] args) { + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + System.out.println("Running shutdown hook"); + running.compareAndSet(true, false); + } + }); + + var main = new Main(); + + while (running.get() && main.getServer().isValid()) { + main.getServer().await(); + } + + main.getServer().stop(); + } +} diff --git a/src/main/java/com/diamante/serverlist/MasterServer.java b/src/main/java/com/diamante/serverlist/MasterServer.java new file mode 100644 index 0000000..e44d7bc --- /dev/null +++ b/src/main/java/com/diamante/serverlist/MasterServer.java @@ -0,0 +1,175 @@ +/* + * 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.net.Socket; +import java.net.ServerSocket; + +import java.io.InputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; + +import java.util.Arrays; + +import java.io.IOException; + +/** + * + * @author Diamante + */ +public class MasterServer { + + public static final int PORT = 27017; + + private ServerSocket socket; + + private boolean valid; + + private final ServerList serverList; + + public MasterServer() { + serverList = new ServerList(); + + try { + socket = new ServerSocket(PORT); + valid = true; + } catch (IOException ex) { + System.err.println(String.format("Socket creation on port %d failed", PORT)); + valid = false; + } + } + + private void handlePacket(Socket from, ByteArrayOutputStream packetData) { + + if (packetData.size() < Utils.PACKET_MIN_LEN) { + System.out.println(String.format("handlePacket: packetData.size() is less than %d bytes", Utils.PACKET_MIN_LEN)); + return; + } + + var blob = packetData.toByteArray(); + + var magicLE = Arrays.copyOfRange(blob, 0, 4); + var magicBE = Utils.longSwap(magicLE); + + var versionLE = Arrays.copyOfRange(blob, 4, 8); + var versionBE = Utils.longSwap(versionLE); + + if (Utils.isServerMagic(magicBE)) { + System.out.println("handlePacket: magic is of type server"); + + if (packetData.size() < 10) { + System.out.println("handlePacket: server packet is less than 10 bytes"); + return; + } + + var portLE = Arrays.copyOfRange(blob, 8, 10); + var portBE = Utils.shortSwap(portLE); + System.out.println(String.format("handlePacket: server %s has net_port %d", from.getInetAddress(), portBE)); + + var server = new Server(from.getInetAddress(), portBE, versionBE); + serverList.addServer(server); + + } else if (Utils.isClientMagic(magicBE)) { + System.out.println("handlePacket: magic is of type client"); + + serverList.removeInactive(); + var toSend = serverList.createResponse(versionBE); + + try { + var out = new DataOutputStream(from.getOutputStream()); + out.write(toSend); + + // Clean things up + out.close(); + } catch (IOException ex) { + System.err.println("handlePacket: IOException in DataOutputStream(from.getOutputStream())"); + } + } else { + System.out.println("handlePacket: magic is not recognized"); + } + } + + public void await() { + Socket worker; + InputStream in; + + try { + worker = socket.accept(); + System.out.println("Accepted a connection"); + } catch (IOException ex) { + System.err.println("await: IOException in socket.accept()"); + return; + } + + try { + in = worker.getInputStream(); + } catch (IOException ex) { + System.err.println("await: IOException in worker.getInputStream()"); + return; + } + + var bytes = new byte[Utils.BUFFER_SIZE]; + var out = new ByteArrayOutputStream(); + + int count; + + try { + while ((count = in.read(bytes)) > 0) { + out.write(bytes, 0, count); + + // The client seems to cause this loop to never end + // We cut connection after PACKET_MIN_LEN is read + // Server does not cause problems + if (count >= Utils.PACKET_MIN_LEN) { + break; + } + } + } catch (IOException ex) { + System.err.println("await: IOException in in.read(bytes)"); + return; + } + + System.out.println(String.format("await: received %d", out.size())); + handlePacket(worker, out); + + // Clean things up + try { + worker.close(); + out.close(); + in.close(); + } catch (IOException ex) { + System.err.println("await: IOException while cleaning up"); + } + } + + public void stop() { + // Can happen if multiple instances are launched + if (socket == null || socket.isClosed()) { + return; + } + + try { + socket.close(); + } catch (IOException ex) { + System.err.println("stop: IOException in socket.close()"); + } + } + + public boolean isValid() { + return valid; + } +} diff --git a/src/main/java/com/diamante/serverlist/Server.java b/src/main/java/com/diamante/serverlist/Server.java new file mode 100644 index 0000000..bf50c1c --- /dev/null +++ b/src/main/java/com/diamante/serverlist/Server.java @@ -0,0 +1,108 @@ +/* + * 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.util.Objects; + +import java.net.InetAddress; + +/** + * + * @author Diamante + */ +public class Server { + + private InetAddress address; + + private Short netPort; + + private Long time; + + private Integer version; + + public Server(InetAddress address, Short netPort, Integer version) { + this.address = address; + this.netPort = netPort; + this.version = version; + + this.time = System.currentTimeMillis() / 1000L; + } + + public InetAddress getAddress() { + return address; + } + + public short getNetPort(){ + return netPort; + } + + public long getTime() { + return time; + } + + public int getVersion() { + return version; + } + + public void updateTime() { + this.time = System.currentTimeMillis() / 1000L; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null) { + return false; + } + + if (getClass() != obj.getClass()) { + return false; + } + + final Server other = (Server) obj; + if (!Objects.equals(this.address, other.address)) { + return false; + } + + if (!Objects.equals(this.netPort, other.netPort)) { + return false; + } + + if (!Objects.equals(this.version, other.version)) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int hash = 3; + hash = 71 * hash + Objects.hashCode(this.address); + hash = 71 * hash + Objects.hashCode(this.netPort); + hash = 71 * hash + Objects.hashCode(this.version); + return hash; + } + + @Override + public String toString() { + return String.format("%s:%d", this.getAddress().toString(), this.netPort); + } +} diff --git a/src/main/java/com/diamante/serverlist/ServerList.java b/src/main/java/com/diamante/serverlist/ServerList.java new file mode 100644 index 0000000..30a8426 --- /dev/null +++ b/src/main/java/com/diamante/serverlist/ServerList.java @@ -0,0 +1,133 @@ +/* + * 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.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * + * @author Diamante + */ +public class ServerList { + + private final Set serverList; + + public ServerList() { + serverList = Collections.synchronizedSet(new HashSet<>()); + } + + public boolean isServerRegistered(Server server) { + synchronized (serverList) { + var it = serverList.iterator(); // Must be in the synchronized block + while (it.hasNext()) { + var other = it.next(); + if (server.equals(other)) { + // Update the time so we don't accidentally remove the server + other.updateTime(); + return true; + } + } + } + + return false; + } + + public void addServer(Server server) { + + if (!isServerRegistered(server)) { + serverList.add(server); + } + + System.out.println(String.format("addServer: Tried to add server %s", server.toString())); + } + + public void removeInactive() { + synchronized (serverList) { + var it = serverList.iterator(); + var time = System.currentTimeMillis() / 1000L; + + while (it.hasNext()) { + var server = it.next(); + if (time - server.getTime() > 60) { + System.out.println(String.format("Removing server %s because of inactivity", server.getAddress().toString())); + it.remove(); + } + } + } + } + + /** + * 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 + * + * @param version the version of the client + * @return the raw bytes to send to the client + */ + public byte[] createResponse(int version) { + var builder = new ByteArrayOutputStream(); + + // We need to swap everything to LE + var sizeBE = serverList.size(); + var sizeLE = Utils.longSwap(sizeBE); + + try { + builder.write(sizeLE); + } + catch (IOException ex) { + System.err.println("createResponse: IOException in builder.write(sizeLE)"); + } + + synchronized (serverList) { + var it = serverList.iterator(); // Must be in the synchronized block + while (it.hasNext()) { + var server = it.next(); + // Let's make sure we send the client only servers on the same version + if (server.getVersion() == version) { + try { + // Let's flip the bytes of this one too + var ipBE = server.getAddress().getAddress(); + Utils.swapByteArray(ipBE); + + // And the port too of course + var portBE = server.getNetPort(); + var portLE = Utils.shortSwap(portBE); + + builder.write(ipBE); + builder.write(portLE); + } + catch (IOException ex) { + System.err.println("createResponse: IOException while writing server data)"); + } + } + } + } + + // I forgot why I do this + var byteBuffer = ByteBuffer.allocate(builder.size()); + byteBuffer.clear(); + byteBuffer.put(builder.toByteArray()); + byteBuffer.flip(); + return byteBuffer.array(); + } +} diff --git a/src/main/java/com/diamante/serverlist/Utils.java b/src/main/java/com/diamante/serverlist/Utils.java new file mode 100644 index 0000000..beb6e7b --- /dev/null +++ b/src/main/java/com/diamante/serverlist/Utils.java @@ -0,0 +1,132 @@ +/* + * 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.BufferOverflowException; +import java.nio.ReadOnlyBufferException; + +import java.nio.ByteBuffer; + +/** + * + * @author Diamante + */ +public class Utils { + + public static final int BUFFER_SIZE = 512; + + public static final int PACKET_MIN_LEN = 8; + public static final int PACKET_CLIENT_LEN = 8; + public static final int PACKET_SERVERT_LEN = 10; + + // (Warning: Remember to take into account endianness) + // 'HELP' + public static final int SERVER_MAGIC = 1212501072; + // 'THEM' + public static final int CLIENT_MAGIC = 1414022477; + + public static boolean isServerMagic(int magic) { + return magic == SERVER_MAGIC; + } + + public static boolean isClientMagic(int magic) { + return magic == CLIENT_MAGIC; + } + + /** + * Flips the array of in around + * + * @param in array of in. Length must be multiple of 4 + */ + public static void swapByteArray(byte[] in) { + assert in.length % 4 == 0; + + for (var i = 0; i < in.length; i += 4) { + // swap 0 and 3 + byte tmp = in[i]; + in[i] = in[i + 3]; + in[i + 3] = tmp; + // swap 1 and 2 + byte tmp2 = in[i + 1]; + in[i + 1] = in[i + 2]; + in[i + 2] = tmp2; + } + } + + /** + * + * @param in raw bytes in LE order + * @return integer in BE + */ + public static int longSwap(byte[] in) { + assert in.length == 4; + + var buffer = ByteBuffer.allocate(4); + buffer.order(java.nio.ByteOrder.LITTLE_ENDIAN); + try { + buffer.put(in); + return buffer.getInt(0); + } + catch (BufferOverflowException | ReadOnlyBufferException ex) { + System.err.println("longSwap: BufferOverflowException or ReadOnlyBufferException"); + return -1; + } + } + + /** + * + * @param in + * @return raw bytes in LE order + */ + public static byte[] longSwap(int in) { + var buffer = ByteBuffer.allocate(4); + buffer.order(java.nio.ByteOrder.LITTLE_ENDIAN); + buffer.putInt(in); + return buffer.array(); + } + + /** + * @param in raw bytes in LE order + * @return short in BE + */ + public static short shortSwap(byte[] in) { + assert in.length == 2; + + var buffer = ByteBuffer.allocate(2); + buffer.order(java.nio.ByteOrder.LITTLE_ENDIAN); + try { + buffer.put(in); + return buffer.getShort(0); + } + catch (BufferOverflowException | ReadOnlyBufferException ex) { + System.err.println("longSwap: BufferOverflowException or ReadOnlyBufferException"); + return -1; + } + } + + /** + * + * @param in + * @return raw bytes in LE order + */ + public static byte[] shortSwap(short in) { + var buffer = ByteBuffer.allocate(2); + buffer.order(java.nio.ByteOrder.LITTLE_ENDIAN); + buffer.putShort(in); + return buffer.array(); + } +}