mirror of
https://github.com/diamante0018/ServerList.git
synced 2025-07-04 18:21:49 +00:00
Compare commits
4 Commits
b7e3d82b51
...
master
Author | SHA1 | Date | |
---|---|---|---|
361f2b6388
|
|||
1d24dd6d91
|
|||
9a57cc938e
|
|||
13ed5f0d77
|
26
.github/workflows/ci.yml
vendored
Normal file
26
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
name: Maven CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@main
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@main
|
||||
with:
|
||||
java-version: '21' # Change this to the required JDK version
|
||||
distribution: 'oracle' # Use Eclipse Temurin distribution
|
||||
cache: maven
|
||||
|
||||
- name: Build with Maven
|
||||
run: mvn clean install
|
13
README.md
Normal file
13
README.md
Normal file
@ -0,0 +1,13 @@
|
||||
# ServerList: A Central Server & Utilities for an abandoned MW3 Client
|
||||
|
||||
This project implements a 'central' server for the now-defunct MW3 modded client known as Tekno.
|
||||
|
||||
The project was initially created in 2021 as a way to explore socket management in Java, a topic I studied at university. Two years ago, I refactored the code to simplify it, and more recently, I added new features such as logging and health monitoring for the central server.
|
||||
|
||||
Interestingly, the original 'master' server for Tekno was written in just 20 lines of Python code. This highlights the complexity of working with Java. However, this project aims to go beyond the basics by introducing additional features, such as a self-cleaning thread to automatically remove servers from the list if they contained "crasher" strings. While this feature was part of the 2021 version, it is not included in this newer version due to time constraints. Additionally, this version lacks unit testing.
|
||||
|
||||
## Features
|
||||
|
||||
- **Central Server**: Manages and lists servers for the Tekno MW3 client.
|
||||
- **Server Pinger**: Fetches and dumps information from various servers currently listed on the central server.
|
||||
- **Central Server Pinger**: Retrieves and displays basic information about the central server.
|
2
pom.xml
2
pom.xml
@ -9,7 +9,7 @@
|
||||
<dependency>
|
||||
<groupId>commons-cli</groupId>
|
||||
<artifactId>commons-cli</artifactId>
|
||||
<version>1.5.0</version>
|
||||
<version>1.9.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.googlecode.json-simple</groupId>
|
||||
|
@ -21,6 +21,7 @@ import java.lang.management.ManagementFactory;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.net.SocketException;
|
||||
import java.net.SocketTimeoutException;
|
||||
@ -28,6 +29,11 @@ import java.nio.channels.IllegalBlockingModeException;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.parser.JSONParser;
|
||||
import org.json.simple.parser.ParseException;
|
||||
|
||||
/**
|
||||
* The purpose of this module is to ping the servers on the server list.
|
||||
*
|
||||
@ -77,6 +83,34 @@ public class ClientEmulator implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
public void pingServers(String filePath) {
|
||||
try {
|
||||
var parser = new JSONParser();
|
||||
var root = (JSONObject) parser.parse(new FileReader(filePath));
|
||||
|
||||
var serversToParse = (JSONArray) root.get("servers");
|
||||
|
||||
for (Object obj : serversToParse) {
|
||||
var server = (JSONObject) obj;
|
||||
|
||||
var ip = (String) server.get("IP");
|
||||
var port = (long) server.get("port"); // Port is stored as a number
|
||||
|
||||
System.out.println(ip + ":" + port);
|
||||
var to = Utils.stringToServer(ip + ":" + port);
|
||||
if (to != null) {
|
||||
handleServer(to);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
System.err.println("Error reading the file: " + e.getMessage());
|
||||
}
|
||||
catch (ParseException e) {
|
||||
System.err.println("Error parsing JSON: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void sendDatagramPacket(DatagramPacket packet) {
|
||||
try {
|
||||
socket.send(packet);
|
||||
|
@ -62,18 +62,8 @@ public class InfoDumper {
|
||||
obj.put("magic", magicBE);
|
||||
obj.put("players", playersBE);
|
||||
obj.put("sv_maxClients", maxPlayersBE);
|
||||
obj.put("info", infoString);
|
||||
|
||||
saveJSONFile(String.format("stats_%d.json", Math.abs(server.hashCode())), obj);
|
||||
}
|
||||
|
||||
public static void saveJSONFile(String fileName, JSONObject obj) {
|
||||
try {
|
||||
var writer = new BufferedWriter(new FileWriter(fileName));
|
||||
writer.write(obj.toJSONString());
|
||||
writer.close();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
System.err.println("saveJSONFile: IOException while writing a JSON file");
|
||||
}
|
||||
Utils.saveJSONFile(String.format("stats_%d.json", Math.abs(server.hashCode())), obj);
|
||||
}
|
||||
}
|
||||
|
@ -67,6 +67,7 @@ public class Main {
|
||||
var emulator = new Option("emulator", "client emulator mode");
|
||||
var masterPing = new Option("master_ping", "ping the master server");
|
||||
var serverPing = new Option("server_ping", "ping the master server as a server");
|
||||
var dumpReplyFromMaster = new Option("dump_reply", "dump info from all the servers listed on the master");
|
||||
|
||||
var ping = Option.builder("ping")
|
||||
.argName("IP:Port")
|
||||
@ -74,11 +75,19 @@ public class Main {
|
||||
.desc("Server to ping")
|
||||
.build();
|
||||
|
||||
var fileList = Option.builder("file_list")
|
||||
.argName("<filename>")
|
||||
.hasArg()
|
||||
.desc("Servers to ping")
|
||||
.build();
|
||||
|
||||
options.addOption(master);
|
||||
options.addOption(emulator);
|
||||
options.addOption(masterPing);
|
||||
options.addOption(serverPing);
|
||||
options.addOption(dumpReplyFromMaster);
|
||||
options.addOption(ping);
|
||||
options.addOption(fileList);
|
||||
|
||||
return options;
|
||||
}
|
||||
@ -95,6 +104,8 @@ public class Main {
|
||||
var main = new Main();
|
||||
var options = main.createOptions();
|
||||
var ip = new String();
|
||||
var fileList = new String();
|
||||
boolean dumpReply = false;
|
||||
|
||||
var parser = new DefaultParser();
|
||||
try {
|
||||
@ -109,12 +120,21 @@ public class Main {
|
||||
main.setMode(Mode.ServerPing);
|
||||
}
|
||||
|
||||
if (line.hasOption("dump_reply")) {
|
||||
dumpReply = true;
|
||||
}
|
||||
|
||||
if (line.hasOption("ping")) {
|
||||
ip = line.getOptionValue("ping");
|
||||
}
|
||||
|
||||
if (line.hasOption("file_list")) {
|
||||
fileList = line.getOptionValue("file_list");
|
||||
}
|
||||
}
|
||||
catch (ParseException exp) {
|
||||
System.err.println("Parsing failed. Reason: " + exp.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if (main.getMode() == Mode.Master) {
|
||||
@ -125,15 +145,22 @@ public class Main {
|
||||
}
|
||||
main.getServer().stop();
|
||||
} else if (main.getMode() == Mode.Emulator) {
|
||||
var emulator = new ClientEmulator();
|
||||
emulator.pingSingleServer(ip);
|
||||
if (!fileList.isEmpty()) {
|
||||
var emulator = new ClientEmulator();
|
||||
emulator.pingServers(fileList);
|
||||
}
|
||||
|
||||
if (!ip.isEmpty()) {
|
||||
var emulator = new ClientEmulator();
|
||||
emulator.pingSingleServer(ip);
|
||||
}
|
||||
} else if (main.getMode() == Mode.MasterPing) {
|
||||
var ping = new MasterServerPinger();
|
||||
ping.pingMaster();
|
||||
ping.readReplyFromMaster();
|
||||
ping.readReplyFromMaster(dumpReply);
|
||||
} else if (main.getMode() == Mode.ServerPing) {
|
||||
var ping = new ServerEmulator();
|
||||
ping.pingLoop();
|
||||
var ping = new ServerEmulator();
|
||||
ping.pingLoop();
|
||||
}
|
||||
|
||||
System.out.println("Normal shutdown");
|
||||
|
@ -16,6 +16,10 @@
|
||||
*/
|
||||
package com.diamante.serverlist;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
@ -23,6 +27,9 @@ import java.net.Socket;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Diamante
|
||||
@ -61,7 +68,7 @@ public class MasterServerPinger {
|
||||
}
|
||||
}
|
||||
|
||||
public void readReplyFromMaster() {
|
||||
public void readReplyFromMaster(Boolean dump) {
|
||||
var out = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
@ -72,7 +79,7 @@ public class MasterServerPinger {
|
||||
|
||||
int count = input.read(bytes);
|
||||
out.write(bytes, 0, count);
|
||||
|
||||
|
||||
System.out.println("readReplyFromMaster: finished reading bytes from socket");
|
||||
}
|
||||
catch (IOException ex) {
|
||||
@ -102,6 +109,54 @@ public class MasterServerPinger {
|
||||
|
||||
System.out.println(String.format("readReplyFromMaster: got %d servers", serverCountBE));
|
||||
|
||||
var root = new JSONObject();
|
||||
var serverArray = new JSONArray();
|
||||
|
||||
Set<Server> serverList = Collections.synchronizedSet(new HashSet<>());
|
||||
|
||||
// Process server data
|
||||
for (int i = 4; i < bytes.length; i += 6) {
|
||||
if (i + 6 > bytes.length) {
|
||||
System.err.println("readReplyFromMaster: Incomplete server data detected");
|
||||
break;
|
||||
}
|
||||
|
||||
byte[] ipBytesLE = new byte[4];
|
||||
System.arraycopy(bytes, i, ipBytesLE, 0, 4);
|
||||
var ipBytesBE = Utils.bytesToInt(ipBytesLE);
|
||||
|
||||
var ipAddress = Utils.bytesToIP(ipBytesBE);
|
||||
|
||||
var portBytesLE = new byte[2];
|
||||
System.arraycopy(bytes, i + 4, portBytesLE, 0, 2);
|
||||
var port = Utils.shortSwap(portBytesLE);
|
||||
|
||||
System.out.println(String.format("Server: %s:%d", ipAddress, port));
|
||||
|
||||
if (!dump) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var server = Utils.stringToServer(ipAddress + ":" + port);
|
||||
serverList.add(server);
|
||||
|
||||
var serverObject = new JSONObject();
|
||||
serverObject.put("IP", ipAddress);
|
||||
serverObject.put("port", port);
|
||||
|
||||
serverArray.add(serverObject);
|
||||
}
|
||||
|
||||
if (dump) {
|
||||
var thread = new Thread(new ClientEmulator(serverList));
|
||||
thread.start();
|
||||
|
||||
root.put("totalServers", serverCountBE);
|
||||
root.put("servers", serverArray);
|
||||
|
||||
Utils.saveJSONFile(String.format("server_dump_%d.json", System.currentTimeMillis() / 1000L), root);
|
||||
}
|
||||
|
||||
try {
|
||||
out.close();
|
||||
}
|
||||
|
@ -105,6 +105,7 @@ public class Server {
|
||||
hash = 71 * hash + Objects.hashCode(this.address);
|
||||
hash = 71 * hash + Objects.hashCode(this.netPort);
|
||||
hash = 71 * hash + Objects.hashCode(this.version);
|
||||
hash = 71 * hash + Objects.hashCode(this.time);
|
||||
return hash;
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Diamante
|
||||
* Copyright (C) 2025 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
|
||||
@ -16,15 +16,19 @@
|
||||
*/
|
||||
package com.diamante.serverlist;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import java.nio.BufferOverflowException;
|
||||
import java.nio.ReadOnlyBufferException;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Diamante
|
||||
@ -43,15 +47,27 @@ public class Utils {
|
||||
|
||||
public static final int OLD_CLIENT_MAGIC = 1414022477; // THEM
|
||||
public static final int NEW_CLIENT_MAGIC = 1129268293;
|
||||
|
||||
public static final int CLIENT_VERSION = 17039893;
|
||||
|
||||
public static final int CLIENT_VERSION = 17039893; // CODE
|
||||
|
||||
public static boolean isServerMagic(int magic) {
|
||||
return magic == OLD_SERVER_MAGIC;
|
||||
}
|
||||
|
||||
public static boolean isClientMagic(int magic) {
|
||||
return magic == OLD_CLIENT_MAGIC;
|
||||
return magic == NEW_CLIENT_MAGIC;
|
||||
}
|
||||
|
||||
public static int bytesToInt(byte[] bytes) {
|
||||
if (bytes.length != 4) {
|
||||
throw new IllegalArgumentException("Array must contain exactly 4 bytes.");
|
||||
}
|
||||
|
||||
// Combine the bytes into an int (assuming the bytes are in Big-Endian order)
|
||||
int result = ((bytes[0] & 0xFF) << 24) | ((bytes[1] & 0xFF) << 16)
|
||||
| ((bytes[2] & 0xFF) << 8) | (bytes[3] & 0xFF);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -151,4 +167,27 @@ public class Utils {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String bytesToIP(int in) {
|
||||
String ipAddress = String.format(
|
||||
"%d.%d.%d.%d",
|
||||
(in & 0xff),
|
||||
(in >> 8 & 0xff),
|
||||
(in >> 16 & 0xff),
|
||||
(in >> 24 & 0xff)
|
||||
);
|
||||
|
||||
return ipAddress;
|
||||
}
|
||||
|
||||
public static void saveJSONFile(String fileName, JSONObject obj) {
|
||||
try {
|
||||
var writer = new BufferedWriter(new FileWriter(fileName));
|
||||
writer.write(obj.toJSONString());
|
||||
writer.close();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
System.err.println("saveJSONFile: IOException while writing a JSON file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user