This commit is contained in:
2022-07-19 23:17:03 +02:00
commit 0c2a642ed4
8 changed files with 664 additions and 0 deletions

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();
}
}