This commit is contained in:
6arelyFuture 2022-07-19 23:17:03 +02:00
commit 0c2a642ed4
Signed by: Future
GPG Key ID: FA77F074E98D98A5
8 changed files with 664 additions and 0 deletions

17
.gitignore vendored Normal file
View File

@ -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

18
nb-configuration.xml Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project-shared-configuration>
<!--
This file contains additional configuration written by modules in the NetBeans IDE.
The configuration is intended to be shared among all the users of project and
therefore it is assumed to be part of version control checkout.
Without this configuration present, some functionality in the IDE may be limited or fail altogether.
-->
<properties xmlns="http://www.netbeans.org/ns/maven-properties-data/1">
<!--
Properties that influence various parts of the IDE, especially code formatting and the like.
You can copy and paste the single properties, into the pom.xml file and the IDE will pick them up.
That way multiple projects can share the same settings (useful for formatting rules for example).
Any value defined here will override the pom.xml file value but is only applicable to the current project.
-->
<netbeans.checkstyle.format>true</netbeans.checkstyle.format>
</properties>
</project-shared-configuration>

25
pom.xml Normal file
View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.diamante.serverlist</groupId>
<artifactId>ServerList</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<configuration>
<configLocation>config/sun_checks.xml</configLocation>
</configuration>
</plugin>
</plugins>
</reporting>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<exec.mainClass>com.diamante.serverlist.Main</exec.mainClass>
</properties>
</project>

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Server> 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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}