├── PowerServer.policy
├── PowerServerCleanup.xml
├── PowerServerFormater.xml
├── README.md
├── settings.cfg
└── src
└── main
└── java
└── de
└── poweruser
└── powerserver
├── commands
├── AddServerCommand.java
├── BanIPCommand.java
├── CommandBase.java
├── CommandInterface.java
├── CommandRegistry.java
├── CommandsCommand.java
├── ExitCommand.java
├── HelpCommand.java
├── LogLevelCommand.java
└── ReloadSettingsCommand.java
├── exceptions
├── LocalServerHostException.java
└── TooManyServersPerHostException.java
├── games
├── DataKeysInterface.java
├── GameBase.java
├── GameInterface.java
├── GameServerBase.java
├── GameServerInterface.java
├── GamesEnum.java
├── GeneralDataKeysEnum.java
└── opflashr
│ ├── DataKeyEnum.java
│ ├── OFPServer.java
│ └── OperationFlashpointResistance.java
├── gamespy
├── EncType.java
├── GamespyValidation.java
└── encoders
│ ├── EncType2Encoder.java
│ ├── EncoderInterface.java
│ ├── OFPMonitorEncoder.java
│ └── enctype2
│ ├── DataSize.java
│ ├── EncType2.java
│ ├── EncTypeShared.java
│ ├── UnsignedChar.java
│ ├── UnsignedCharPointer.java
│ ├── UnsignedCharToIntPointer.java
│ ├── UnsignedInt.java
│ └── UnsignedIntPointer.java
├── logger
├── LogLevel.java
└── Logger.java
├── main
├── CombineableInterface.java
├── ConsoleReader.java
├── Main.java
├── MessageData.java
├── PowerServer.java
├── QueryInfo.java
├── ServerHost.java
├── ServerList.java
├── gui
│ ├── MainWindow.java
│ └── TextAreaOutputStream.java
├── parser
│ ├── DataParserInterface.java
│ ├── GamespyProtocol1Parser.java
│ ├── ParserException.java
│ └── dataverification
│ │ ├── BooleanVerify.java
│ │ ├── DummyVerify.java
│ │ ├── IPAddressVerify.java
│ │ ├── IntVerify.java
│ │ ├── QueryIdFormatVerify.java
│ │ ├── StringLengthVerify.java
│ │ ├── StringVerify.java
│ │ └── VerificationInterface.java
└── security
│ ├── BanList.java
│ ├── BanManager.java
│ ├── SecurityAndBanManager.java
│ └── SecurityBanException.java
├── network
├── PacketFilter.java
├── QueryConnection.java
├── TCPManager.java
├── UDPManager.java
├── UDPMessage.java
├── UDPReceiverThread.java
└── UDPSender.java
└── settings
├── SectionGeneral.java
├── SectionMasterServerLists.java
├── SectionSupportedGames.java
├── Settings.java
├── SettingsReader.java
└── SettingsReaderInterface.java
/PowerServer.policy:
--------------------------------------------------------------------------------
1 | /* AUTOMATICALLY GENERATED ON Fri Jun 06 15:01:59 CEST 2014*/
2 | /* DO NOT EDIT */
3 |
4 | grant {
5 | permission java.awt.AWTPermission "accessClipboard";
6 | permission java.awt.AWTPermission "showWindowWithoutWarningBanner";
7 | permission java.io.FilePermission "settings.cfg", "read";
8 | permission java.io.FilePermission "banlist.cfg", "read";
9 | permission java.io.FilePermission "banlist.cfg", "write";
10 | permission java.io.FilePermission "server.log", "read";
11 | permission java.io.FilePermission "server.log", "write";
12 | permission java.lang.RuntimePermission "setIO";
13 | permission java.net.SocketPermission "localhost:27900", "listen";
14 | permission java.net.SocketPermission "localhost:28900", "listen";
15 | permission java.net.SocketPermission "*", "accept";
16 | permission java.net.SocketPermission "*", "connect";
17 | permission java.net.SocketPermission "*", "resolve";
18 | permission java.security.SecurityPermission "getPolicy";
19 | };
20 |
21 |
--------------------------------------------------------------------------------
/PowerServerCleanup.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | PowerServer
2 | ===========
3 |
4 | A substitute for GameSpy's master server.
5 |
6 |
7 | The goal of this project is to create a master server, which picks up the pieces that GameSpy leaves behind on May 31.2014. It won't be a fully qualified replacement of GameSpy's master server, with all the different services and features, though. Actually it will be a very limited version, with the main focus on serving game server IPs.
8 |
9 |
10 | This project currently evolves around and is driven by a single game: `Arma:Cold War Assault`, but I'll try to design it as game-independent as possible, with the intention to have it expanded for other games as well.
11 |
12 | For the beginning, only the [GameSpy protocol 1](http://int64.org/docs/gamestat-protocols/gamespy.html) will be supported and therefore only games which use that one. I tried to allow a game to define what protocol it is using, but this makes the whole code completly complicated, which made me fail so far.
13 | Once everything is up and running as planned, I might get back to adding support for the other protocols.
14 |
--------------------------------------------------------------------------------
/settings.cfg:
--------------------------------------------------------------------------------
1 | [General]
2 | publicmode = true
3 | masterserverlistsdownloadinterval = 4
4 | loglevel = 2
5 | queryServersOnHeartbeat = true
6 | maximumServerTimeout = 60
7 |
8 | maximumServersPerHost = 10
9 | maximumSendViolations = 50
10 | allowedMinimumSendInterval = 250
11 | connectionLimitPerClient = 4
12 | tempBanDuration = 15
13 |
14 | [SupportedGames]
15 | opflashr
16 |
17 | [MasterServerLists]
18 | https://raw.githubusercontent.com/wiki/poweruser/ofpmonitor/masterservers.txt
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/commands/AddServerCommand.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.commands;
2 |
3 | import de.poweruser.powerserver.games.GameBase;
4 | import de.poweruser.powerserver.logger.LogLevel;
5 | import de.poweruser.powerserver.logger.Logger;
6 | import de.poweruser.powerserver.main.PowerServer;
7 | import de.poweruser.powerserver.main.parser.dataverification.IPAddressVerify;
8 | import de.poweruser.powerserver.main.parser.dataverification.IntVerify;
9 |
10 | public class AddServerCommand extends CommandBase {
11 |
12 | private PowerServer server;
13 |
14 | public AddServerCommand(String commandString, PowerServer server) {
15 | super(commandString);
16 | this.server = server;
17 | }
18 |
19 | @Override
20 | public boolean handle(String[] arguments) {
21 | if(arguments.length == 2) {
22 | String gamename = arguments[0];
23 | GameBase game = GameBase.getGameForGameName(gamename);
24 | if(game != null) {
25 | String[] address = arguments[1].split(":");
26 | if(address.length == 2) {
27 | IPAddressVerify ipverify = new IPAddressVerify();
28 | IntVerify portverify = new IntVerify(0, 65535);
29 | if(ipverify.verify(address[0]) && portverify.verify(address[1])) {
30 | this.server.addServer(game, ipverify.getVerifiedAddress(), portverify.getVerifiedValue());
31 | return true;
32 | }
33 | }
34 | Logger.logStatic(LogLevel.VERY_LOW, "The entered server address (" + arguments[1] + ") is invalid.");
35 | } else {
36 | Logger.logStatic(LogLevel.VERY_LOW, "The game \"" + gamename + "\" was not recognized.");
37 | }
38 | }
39 | return false;
40 | }
41 |
42 | @Override
43 | public void showCommandHelp() {
44 | StringBuilder sb = new StringBuilder();
45 | sb.append("Command - addserver:\n\n");
46 | sb.append("Syntax: addserver \n\n");
47 | sb.append("Type 'help' to see the list of available games and their game id-keys (Identification keys)\n");
48 | Logger.logStatic(LogLevel.VERY_LOW, sb.toString());
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/commands/BanIPCommand.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.commands;
2 |
3 | import java.net.InetAddress;
4 | import java.net.UnknownHostException;
5 | import java.util.Calendar;
6 | import java.util.Date;
7 |
8 | import de.poweruser.powerserver.logger.LogLevel;
9 | import de.poweruser.powerserver.logger.Logger;
10 | import de.poweruser.powerserver.main.PowerServer;
11 |
12 | public class BanIPCommand extends CommandBase {
13 |
14 | private PowerServer server;
15 |
16 | public BanIPCommand(String commandString, PowerServer server) {
17 | super(commandString);
18 | this.server = server;
19 | }
20 |
21 | @Override
22 | public boolean handle(String[] arguments) {
23 | if(arguments.length >= 1 && arguments.length <= 2) {
24 | String add = arguments[0];
25 | InetAddress address = null;
26 | try {
27 | address = InetAddress.getByName(add);
28 | } catch(UnknownHostException e) {
29 | Logger.logStatic(LogLevel.VERY_LOW, "The entered address " + add + " could not be resolved.", true);
30 | return false;
31 | }
32 |
33 | if(arguments.length == 2) {
34 | Date date = null;
35 | try {
36 | date = this.parseBanDurationToDate(arguments[1]);
37 | } catch(IllegalArgumentException e) {
38 | Logger.logStatic(LogLevel.VERY_LOW, "The entered duration is invalid, refer to the command help.", true);
39 | return false;
40 | }
41 | if(date != null) {
42 | boolean newBan = this.server.getBanManager().addTempBanByTimeStamp(address, date.getTime());
43 | String expirationDate = this.server.getBanManager().getUnbanDate(address);
44 | String message = "Temporary ban for " + address.toString() + (newBan ? " issued." : " updated.");
45 | if(expirationDate != null) {
46 | message += " Expires on: " + expirationDate;
47 | }
48 | Logger.logStatic(LogLevel.VERY_LOW, message, true);
49 | return true;
50 | }
51 | } else {
52 | this.server.getBanManager().addPermBan(address);
53 | Logger.logStatic(LogLevel.VERY_LOW, "Permanent ban for " + address.toString() + " issued.", true);
54 | return true;
55 | }
56 | }
57 | return false;
58 | }
59 |
60 | @Override
61 | public void showCommandHelp() {
62 | StringBuilder sb = new StringBuilder();
63 | sb.append("Command - banip:\n\n");
64 | sb.append("Syntax: banip [time duration]\n\n");
65 | sb.append("The optional time duration is a combination of an integer followed by a time unit key.\n");
66 | sb.append("Multiple durations of different units can be combined, without spaces inbetween.\n");
67 | sb.append("Possible time unit keys are: M (months), w (weeks), d (days), h (hours), m (minutes), s (seconds)\n");
68 | sb.append("Examples: 3d (3 days), 2h30m (2 hours and 30 minutes), 1h20m10s (1 hour 20 minutes and 10 seconds)");
69 | Logger.logStatic(LogLevel.VERY_LOW, sb.toString());
70 | }
71 |
72 | public Date parseBanDurationToDate(String input) {
73 | Calendar cal = Calendar.getInstance();
74 |
75 | String currentNumber = "";
76 | for(int j = 0; j < input.length(); j++) {
77 |
78 | String c = input.substring(j, j + 1);
79 | try {
80 | Integer.parseInt(c);
81 | currentNumber += c;
82 | continue;
83 | } catch(Exception e) {}
84 |
85 | int num = Integer.parseInt(currentNumber);
86 |
87 | if(c.equals("w")) {
88 | cal.add(Calendar.WEEK_OF_YEAR, num);
89 | } else if(c.equals("d")) {
90 | cal.add(Calendar.DAY_OF_YEAR, num);
91 | } else if(c.equals("M")) {
92 | cal.add(Calendar.MONTH, num);
93 | } else if(c.equals("h")) {
94 | cal.add(Calendar.HOUR, num);
95 | } else if(c.equals("m")) {
96 | cal.add(Calendar.MINUTE, num);
97 | } else if(c.equals("s")) {
98 | cal.add(Calendar.SECOND, num);
99 | } else {
100 | throw new IllegalArgumentException("Invalid time measurement: " + c);
101 | }
102 | currentNumber = "";
103 | }
104 | return cal.getTime();
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/commands/CommandBase.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.commands;
2 |
3 | public abstract class CommandBase implements CommandInterface {
4 |
5 | private final String commandString;
6 |
7 | public CommandBase(String commandString) {
8 | this.commandString = commandString;
9 | }
10 |
11 | public String getCommandString() {
12 | return this.commandString;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/commands/CommandInterface.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.commands;
2 |
3 | public interface CommandInterface {
4 |
5 | /**
6 | * This method is called by the CommandRegistry, and executes the command.
7 | * Before the command does any change, the parsed arguments shall be checked
8 | * first. When these do not fit the command handle shall return false.
9 | * If all is valid, and the command was executed fine, return true.
10 | *
11 | * @param arguments
12 | * the arguments of the command, that the user had entered. The
13 | * command string which identified this command is not included.
14 | * If the array has a length of 0, no arguments were passed.
15 | *
16 | * @return If an error occurred and the command help shall be shown false,
17 | * otherwise true
18 | */
19 |
20 | public boolean handle(String[] arguments);
21 |
22 | /**
23 | * Generates a log message for the command. This method is called by the
24 | * CommandRegistry, when the handle method had returned false
25 | *
26 | */
27 |
28 | public void showCommandHelp();
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/commands/CommandRegistry.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.commands;
2 |
3 | import java.util.ArrayDeque;
4 | import java.util.HashMap;
5 | import java.util.Map;
6 |
7 | import de.poweruser.powerserver.logger.LogLevel;
8 | import de.poweruser.powerserver.logger.Logger;
9 |
10 | public class CommandRegistry {
11 |
12 | private Map commandMap;
13 | private ArrayDeque commandQueue;
14 |
15 | public CommandRegistry() {
16 | this.commandMap = new HashMap();
17 | this.commandQueue = new ArrayDeque();
18 | }
19 |
20 | /**
21 | * Registers the parsed command in the CommandRegistry. If a
22 | * command with the same command string was already registered,
23 | * that one is replaced
24 | *
25 | * @param command
26 | * The command that shall be registered
27 | */
28 |
29 | public void register(CommandBase command) {
30 | this.commandMap.put(command.getCommandString(), command);
31 | }
32 |
33 | /**
34 | * Takes the first element from the commandQueue and executes the one
35 | * registered command, that matches the first word of the entered command.
36 | * If no matching command was found, a log message is printed.
37 | * This method shall not be run from a different thread than the main
38 | * thread, unless thread-safety, on the things that the commands do, is
39 | * guaranteed
40 | */
41 |
42 | public void issueNextQueuedCommand() {
43 | String line = this.commandQueue.pollFirst();
44 | if(line == null || line.trim().isEmpty()) { return; };
45 | String formated = line.trim().replaceAll("\\s+", " ");
46 | String[] items = formated.split(" ");
47 | String[] arguments = new String[0];
48 | if(items.length > 1) {
49 | arguments = new String[items.length - 1];
50 | System.arraycopy(items, 1, arguments, 0, items.length - 1);
51 | }
52 | String commandString = items[0];
53 | if(this.commandMap.containsKey(commandString)) {
54 | Logger.logStatic(LogLevel.VERY_LOW, "Entered command: '" + formated + "'", true);
55 | CommandInterface com = this.commandMap.get(commandString);
56 | if(!com.handle(arguments)) {
57 | com.showCommandHelp();
58 | }
59 | } else {
60 | Logger.logStatic(LogLevel.VERY_LOW, "Unknown command '" + commandString + "'. Type 'commands' to get a list of all available commands.", true);
61 | }
62 | }
63 |
64 | /**
65 | * Adds a command as a String in the exact same way, that the user had
66 | * entered it to the command queue.
67 | *
68 | * @param command
69 | * The command that the user had entered
70 | */
71 |
72 | public void queueCommand(String command) {
73 | this.commandQueue.addLast(command);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/commands/CommandsCommand.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.commands;
2 |
3 | import de.poweruser.powerserver.logger.LogLevel;
4 | import de.poweruser.powerserver.logger.Logger;
5 |
6 | public class CommandsCommand extends CommandBase {
7 |
8 | public CommandsCommand(String commandString) {
9 | super(commandString);
10 | }
11 |
12 | @Override
13 | public boolean handle(String[] arguments) {
14 | return false;
15 | }
16 |
17 | @Override
18 | public void showCommandHelp() {
19 | StringBuilder sb = new StringBuilder();
20 | sb.append("Overview over all available commands:\n\n");
21 | sb.append("setloglevel <#>\t_\t_\tSets the level of logging. Values: ");
22 | for(LogLevel l: LogLevel.values()) {
23 | sb.append(l.getValue());
24 | sb.append("(");
25 | sb.append(l.toString());
26 | sb.append(") ");
27 | }
28 | sb.append("\n");
29 | sb.append("reload \t_\t_\tReloads the settings file\n");
30 | sb.append("addserver \tAdds a server to a game's server list\n");
31 | sb.append("banip [duration]\t\tAdds a certain IP address to the ban list\n");
32 | sb.append("commands\t_\t_\tShows this list of available commands\n");
33 | sb.append("help \t_\t_\tDisplays some general information about this application\n");
34 | sb.append("exit \t_\t_\tShuts down the server. Aliases: quit, shutdown, stop, end");
35 | Logger.logStatic(LogLevel.VERY_LOW, sb.toString(), true);
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/commands/ExitCommand.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.commands;
2 |
3 | import de.poweruser.powerserver.main.PowerServer;
4 |
5 | public class ExitCommand extends CommandBase {
6 |
7 | private PowerServer server;
8 |
9 | public ExitCommand(String commandString, PowerServer server) {
10 | super(commandString);
11 | this.server = server;
12 | }
13 |
14 | @Override
15 | public boolean handle(String[] arguments) {
16 | this.server.shutdown();
17 | return true;
18 | }
19 |
20 | @Override
21 | public void showCommandHelp() {
22 |
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/commands/HelpCommand.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.commands;
2 |
3 | import de.poweruser.powerserver.logger.LogLevel;
4 | import de.poweruser.powerserver.logger.Logger;
5 |
6 | public class HelpCommand extends CommandBase {
7 |
8 | public HelpCommand(String commandString) {
9 | super(commandString);
10 | }
11 |
12 | @Override
13 | public boolean handle(String[] arguments) {
14 | return false;
15 | }
16 |
17 | @Override
18 | public void showCommandHelp() {
19 | StringBuilder sb = new StringBuilder();
20 | sb.append("Information about this application:\n\n");
21 | sb.append(" - PowerServer - \n");
22 | sb.append("PowerServer is a master server application for games that had used the GameSpy service\n");
23 | sb.append("The currently supported Games are:\n\n");
24 | sb.append("GAME NAME [Identification key]\n");
25 |
26 | sb.append("Operation Flashpoint:Resistance [opflashr]\n");
27 | sb.append("Arma: Cold War Assault [opflashr]\n\n");
28 | sb.append("This application is open-source and the project's site is: https://github.com/Poweruser/PowerServer/\n");
29 | sb.append("If you need help with the settings, check: https://github.com/Poweruser/PowerServer/wiki\n");
30 | sb.append("Developed by Poweruser - 2014\n\n");
31 | sb.append("Type 'commands' to see a list of available commands");
32 | Logger.logStatic(LogLevel.VERY_LOW, sb.toString(), true);
33 |
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/commands/LogLevelCommand.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.commands;
2 |
3 | import de.poweruser.powerserver.logger.LogLevel;
4 | import de.poweruser.powerserver.logger.Logger;
5 | import de.poweruser.powerserver.main.parser.dataverification.IntVerify;
6 |
7 | public class LogLevelCommand extends CommandBase {
8 |
9 | public LogLevelCommand(String commandString) {
10 | super(commandString);
11 | }
12 |
13 | @Override
14 | public boolean handle(String[] arguments) {
15 | if(arguments.length == 1) {
16 | IntVerify v = new IntVerify(0, LogLevel.getMaxLevel().getValue());
17 | if(v.verify(arguments[0])) {
18 | int value = v.getVerifiedValue();
19 | Logger.setLogLevel(value);
20 | Logger.logStatic(LogLevel.VERY_LOW, "Log level set to " + LogLevel.valueToLevel(value).toString() + " (" + value + ")", true);
21 | return true;
22 | } else {
23 | boolean found = false;
24 | for(LogLevel l: LogLevel.values()) {
25 | if(l.toString().equalsIgnoreCase(arguments[0])) {
26 | Logger.setLogLevel(l.getValue());
27 | Logger.logStatic(LogLevel.VERY_LOW, "Log level set to " + l.toString() + " (" + l.getValue() + ")", true);
28 | found = true;
29 | break;
30 | }
31 | }
32 | if(!found) {
33 | Logger.logStatic(LogLevel.VERY_LOW, "Invalid argument specified", true);
34 | }
35 | }
36 | } else if(arguments.length > 1) {
37 | Logger.logStatic(LogLevel.VERY_LOW, "Invalid arguments specified", true);
38 | }
39 | return false;
40 | }
41 |
42 | @Override
43 | public void showCommandHelp() {
44 | StringBuilder sb = new StringBuilder();
45 | sb.append("Command - setloglevel:\n\n");
46 | sb.append("Syntax: setloglevel <#>\n\n");
47 | sb.append(" # The level of logging, that you want to set. Available levels are 0 through 4:\n");
48 | sb.append(" 0 - Very low: Only the most important events are logged\n");
49 | sb.append(" 1 - Low: Major errors that dont interrupt the server operation though are logged as well\n");
50 | sb.append(" 2 - Normal: Relevant information on the server events are logged as well\n");
51 | sb.append(" 3 - High: Minor errors that usually can be ignored are logged as well\n");
52 | sb.append(" 4 - Very high: Logs all kinds of error and events");
53 | Logger.logStatic(LogLevel.VERY_LOW, sb.toString(), true);
54 |
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/commands/ReloadSettingsCommand.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.commands;
2 |
3 | import de.poweruser.powerserver.main.PowerServer;
4 |
5 | public class ReloadSettingsCommand extends CommandBase {
6 |
7 | private PowerServer server;
8 |
9 | public ReloadSettingsCommand(String commandString, PowerServer server) {
10 | super(commandString);
11 | this.server = server;
12 | }
13 |
14 | @Override
15 | public boolean handle(String[] arguments) {
16 | this.server.reloadSettingsFile();
17 | return true;
18 | }
19 |
20 | @Override
21 | public void showCommandHelp() {
22 |
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/exceptions/LocalServerHostException.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.exceptions;
2 |
3 | public class LocalServerHostException extends Exception {
4 |
5 | private static final long serialVersionUID = 4848889399792974225L;
6 |
7 | public LocalServerHostException(String message) {
8 | super(message);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/exceptions/TooManyServersPerHostException.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.exceptions;
2 |
3 | import java.net.InetAddress;
4 |
5 | public class TooManyServersPerHostException extends Exception {
6 |
7 | private static final long serialVersionUID = -6878341095943010604L;
8 | private InetAddress host;
9 |
10 | public TooManyServersPerHostException(InetAddress host) {
11 | this.host = host;
12 | }
13 |
14 | public InetAddress getHostAddress() {
15 | return this.host;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/games/DataKeysInterface.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.games;
2 |
3 | public interface DataKeysInterface {
4 |
5 | /**
6 | * Returns the key string that is assigned to this data key. This key string
7 | * will be used primarily by the parsers, but it may find use at a lot of
8 | * other places as well.
9 | *
10 | * @return the key string assigned to this data key
11 | */
12 |
13 | public String getKeyString();
14 |
15 | /**
16 | * Verifies the passed String data against the verifier that is assigned to
17 | * this data key
18 | *
19 | * @param The
20 | * data to check as a String
21 | * @return true, if the data passes the checks of the verifier that is
22 | * assigned to this data key
23 | * false, if it does not pass the checks
24 | */
25 |
26 | public boolean verifyData(String data);
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/games/GameBase.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.games;
2 |
3 | import java.net.InetSocketAddress;
4 | import java.util.HashMap;
5 | import java.util.List;
6 | import java.util.Map;
7 |
8 | import de.poweruser.powerserver.main.MessageData;
9 | import de.poweruser.powerserver.main.ServerList;
10 | import de.poweruser.powerserver.main.parser.DataParserInterface;
11 | import de.poweruser.powerserver.main.parser.ParserException;
12 | import de.poweruser.powerserver.network.UDPMessage;
13 | import de.poweruser.powerserver.settings.Settings;
14 |
15 | public abstract class GameBase implements GameInterface {
16 |
17 | private static Map gameNameMap = new HashMap();
18 | protected Map keyMap;
19 | protected String gamename;
20 | protected String gamespyKey;
21 | protected DataParserInterface parser;
22 | private ServerList serverList;
23 | private Settings settings;
24 |
25 | protected GameBase(String gamename, String gamespyKey, DataParserInterface parser, DataKeysInterface[] dataKeys) {
26 | this.gamename = gamename;
27 | if(!gameNameMap.containsKey(this.getGameName())) {
28 | gameNameMap.put(this.getGameName(), this);
29 | }
30 | this.gamespyKey = gamespyKey;
31 | this.parser = parser;
32 | this.keyMap = new HashMap();
33 | for(GeneralDataKeysEnum datakey: GeneralDataKeysEnum.values()) {
34 | this.keyMap.put(datakey.getKeyString(), datakey);
35 | }
36 | for(DataKeysInterface d: dataKeys) {
37 | this.keyMap.put(d.getKeyString(), d);
38 | }
39 | this.serverList = new ServerList(this);
40 | }
41 |
42 | public void setSettings(Settings settings) {
43 | this.settings = settings;
44 | }
45 |
46 | public Settings getSettings() {
47 | return this.settings;
48 | }
49 |
50 | @Override
51 | public String getGameName() {
52 | return this.gamename;
53 | }
54 |
55 | public String getGamespyKey() {
56 | return this.gamespyKey;
57 | }
58 |
59 | public static GameBase getGameForGameName(String gamename) {
60 | if(gamename != null) { return gameNameMap.get(gamename); }
61 | return null;
62 | }
63 |
64 | @Override
65 | public MessageData parseMessage(UDPMessage msg) throws ParserException {
66 | return this.parser.parse(this, msg);
67 | }
68 |
69 | @Override
70 | public boolean verifyDataKeyAndValue(String key, String value) {
71 | if(this.keyMap.containsKey(key)) {
72 | DataKeysInterface dataKey = this.keyMap.get(key);
73 | return dataKey.verifyData(value);
74 | } else {
75 | return false;
76 | }
77 | }
78 |
79 | @Override
80 | public int hashCode() {
81 | return this.gamename.hashCode();
82 | }
83 |
84 | @Override
85 | public boolean equals(Object o) {
86 | return (o != null && o instanceof GameBase && ((GameBase) o).getGameName().equalsIgnoreCase(this.gamename));
87 | }
88 |
89 | public List getActiveServers() {
90 | return this.serverList.getActiveServers(this.settings);
91 | }
92 |
93 | public ServerList getServerList() {
94 | return this.serverList;
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/games/GameInterface.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.games;
2 |
3 | import java.net.DatagramPacket;
4 | import java.net.InetSocketAddress;
5 |
6 | import de.poweruser.powerserver.main.MessageData;
7 | import de.poweruser.powerserver.main.parser.ParserException;
8 | import de.poweruser.powerserver.network.UDPMessage;
9 |
10 | /**
11 | * This interface must be implemented by all games in PowerServer. The games
12 | * usually do not directly implement it, but inherit it from the abstract class
13 | * GameBase that they extend. Most methods of this interface are already covered
14 | * by GameBase, but not all.
15 | *
16 | * @author Poweruser
17 | *
18 | */
19 |
20 | public interface GameInterface {
21 |
22 | /**
23 | * Getter method for the game name, that is the key for the game that
24 | * GameSpy uses to identify it
25 | *
26 | * @return The GameSpy key that identifies the game, as a String
27 | */
28 | public String getGameName();
29 |
30 | /**
31 | * Gets the display name of the game that is currently running on
32 | * gameServer. The display name may vary within a game, with different
33 | * versions of it for example.
34 | * The game that is represented by the gameServer must match the game that
35 | * this is method is called upon. If it does not match an
36 | * IllegalArgumentException is thrown
37 | *
38 | * @param gameServer
39 | * the server that the display name shall be generated for
40 | * @return The display name of the game that is represented by gameServer
41 | * @throws IllegalArgumentException
42 | * When the both games, the game that getGameDisplayName is
43 | * called on and the game of the gameServer, do not match
44 | */
45 |
46 | public String getGameDisplayName(GameServerBase gameServer) throws IllegalArgumentException;
47 |
48 | /**
49 | * Gets the game port that the passed game server is running on. If that
50 | * information was not received through a query answer yet, this method
51 | * returns null.
52 | * The game that is represented by the gameServer must match the game that
53 | * this is method is called upon. If it does not match an
54 | * IllegalArgumentException is thrown
55 | *
56 | * @param gameServer
57 | * the server of which its game port shall be returned
58 | * @return the game port the server is running on as a String. If none is
59 | * set yet, it returns null
60 | * @throws IllegalArgumentException
61 | * An IllegalArgumentException is thrown, when the both games,
62 | * the game that getGamePort is called on and the game of the
63 | * gameServer, do not match
64 | */
65 |
66 | public String getGamePort(GameServerBase gameServer) throws IllegalArgumentException;
67 |
68 | /**
69 | * Parses a UDPMessage specifically for this game. The game uses the parser
70 | * that is assigned to it for this task.
71 | *
72 | * @param msg
73 | * The UDPMessage to parse
74 | * @return The parsed data as a MessageData object
75 | * @throws ParserException
76 | * A ParserException is thrown when an error happens during
77 | * parsing or when the input is in a unexpected or corrupt
78 | * format. When, or in what cases a ParserException is thrown
79 | * completely depends on the parser that the game, this method
80 | * is
81 | * called upon, uses.
82 | */
83 |
84 | public MessageData parseMessage(UDPMessage msg) throws ParserException;
85 |
86 | /**
87 | * Verifies a key-value pair based on the available data keys for this game.
88 | * These include the general data keys defined in
89 | * de.poweruser.powerserver.games.GeneralDataKeysEnum and the ones that were
90 | * additionally defined for this game. This method returns only true if a
91 | * matching data key could be found and the value passes the test of its
92 | * assigned verifier. In all other cases this method returns false.
93 | *
94 | * @param key
95 | * The data key of the key-value pair to check, as a String
96 | * @param value
97 | * The value of the key-value pair to check, as a String
98 | * @return true, if a matching data key was found and the value passes the
99 | * test of its assigned verifier, otherwise false
100 | */
101 |
102 | public boolean verifyDataKeyAndValue(String key, String value);
103 |
104 | /**
105 | * Returns the data key of this game, that is responsible for marking a sent
106 | * message as a server heart-beat.
107 | *
108 | * @return the heart-beat data key or null if the game has none
109 | */
110 |
111 | public DataKeysInterface getHeartBeatDataKey();
112 |
113 | /**
114 | * Returns the data key of this game, that is responsible for marking a sent
115 | * message as a server heart-beat broadcast.
116 | *
117 | * @return the heart-beat broadcast data key or null if the game has none
118 | */
119 |
120 | public DataKeysInterface getHeartBeatBroadcastDataKey();
121 |
122 | /**
123 | * Creates the heart-beat broadcast in form of a DatagramPacket, that only
124 | * contains the necessary data, but no target address or port yet. The
125 | * required input are the query address of the game server, and the parsed
126 | * key-value data from the corresponding received heart-beat.
127 | *
128 | * @param server
129 | * The query address of the game server, that this heart-beat
130 | * broadcast is for
131 | * @param data
132 | * The parsed data of the received heart-beat as MessageData
133 | * @return A DatagramPacket without any target information, it only contains
134 | * the heart-beat broadcast data specific to the game
135 | */
136 |
137 | public DatagramPacket createHeartbeatBroadcast(InetSocketAddress server, MessageData data);
138 |
139 | /**
140 | * Creates a new game server object for the game that this method is called
141 | * upon. The game server object is initialized with the corresponding game
142 | * and the query address of the game server.
143 | *
144 | * @param server
145 | * The query address of the game server
146 | * @return The created game server object, concealed as GameServerInterface
147 | */
148 | public GameServerInterface createNewServer(InetSocketAddress server);
149 |
150 | /**
151 | * Creates a server query in form of a DatagramPacket for the corresponding
152 | * game, that only contains the query data.
153 | *
154 | * @param queryPlayers
155 | * A boolean flag that decides whether the server's player
156 | * information shall be queried as well or not
157 | * @return A DatagramPacket without any target information, it only contains
158 | * the query data specific to the game
159 | */
160 |
161 | public DatagramPacket createStatusQuery(boolean queryPlayers);
162 | }
163 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/games/GameServerBase.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.games;
2 |
3 | import java.net.InetSocketAddress;
4 | import java.util.HashMap;
5 | import java.util.Map;
6 | import java.util.concurrent.TimeUnit;
7 |
8 | import de.poweruser.powerserver.logger.LogLevel;
9 | import de.poweruser.powerserver.logger.Logger;
10 | import de.poweruser.powerserver.main.MessageData;
11 | import de.poweruser.powerserver.main.QueryInfo;
12 | import de.poweruser.powerserver.main.parser.dataverification.IntVerify;
13 |
14 | public abstract class GameServerBase implements GameServerInterface {
15 |
16 | private int queryPort;
17 | private long lastHeartbeat;
18 | private long lastMessage;
19 | private long lastQueryRequest;
20 | private boolean realHeartbeat;
21 | private QueryBuffer queryBuffer;
22 | private boolean hasAnswered;
23 | protected MessageData queryInfo;
24 | protected GameBase game;
25 | protected InetSocketAddress serverAddress;
26 | private boolean manuallyAdded;
27 |
28 | public GameServerBase(GameBase game, InetSocketAddress server) {
29 | this.lastHeartbeat = -1L;
30 | this.lastMessage = -1L;
31 | this.lastQueryRequest = -1L;
32 | this.realHeartbeat = false;
33 | this.game = game;
34 | this.queryInfo = new MessageData();
35 | this.hasAnswered = false;
36 | this.queryBuffer = new QueryBuffer();
37 | this.serverAddress = server;
38 | this.manuallyAdded = false;
39 | }
40 |
41 | @Override
42 | public boolean incomingHeartbeat(InetSocketAddress serverAddress, MessageData data, boolean manuallyAdded) {
43 | if(data.isHeartBeat() && this.serverAddress.equals(serverAddress)) {
44 | this.realHeartbeat = true;
45 | this.manuallyAdded = manuallyAdded;
46 | this.setQueryPort(data);
47 | return this.newHeartBeat();
48 | }
49 | return false;
50 | }
51 |
52 | private boolean newHeartBeat() {
53 | boolean firstHeartBeat = this.lastHeartbeat < 0L;
54 | long currentTime = System.currentTimeMillis();
55 | this.lastHeartbeat = currentTime;
56 | this.lastMessage = currentTime;
57 | return firstHeartBeat;
58 | }
59 |
60 | @Override
61 | public boolean incomingHeartBeatBroadcast(InetSocketAddress serverAddress, MessageData data) {
62 | if(data.isHeartBeatBroadcast() && this.isBroadcastedServer() && this.serverAddress.equals(serverAddress)) {
63 | this.setQueryPort(data);
64 | return this.newHeartBeat();
65 | }
66 | return false;
67 | }
68 |
69 | @Override
70 | public boolean incomingQueryAnswer(InetSocketAddress serverAddress, MessageData data) {
71 | if(this.serverAddress.equals(serverAddress)) {
72 | if(this.queryBuffer.put(data)) {
73 | MessageData completeQuery = this.queryBuffer.getQueryIfComplete();
74 | if(completeQuery != null) { return this.processNewMessage(completeQuery); }
75 | }
76 | }
77 | return false;
78 | }
79 |
80 | @Override
81 | public int getQueryPort() {
82 | return this.queryPort;
83 | }
84 |
85 | @Override
86 | public boolean checkLastHeartbeat(long timeDiff, TimeUnit unit) {
87 | return (System.currentTimeMillis() - this.lastHeartbeat) < TimeUnit.MILLISECONDS.convert(timeDiff, unit);
88 | }
89 |
90 | @Override
91 | public boolean checkLastMessage(long timeDiff, TimeUnit unit) {
92 | return (System.currentTimeMillis() - this.lastMessage) < TimeUnit.MILLISECONDS.convert(timeDiff, unit);
93 | }
94 |
95 | @Override
96 | public boolean checkLastQueryRequest(long timeDiff, TimeUnit unit) {
97 | return (System.currentTimeMillis() - this.lastQueryRequest) < TimeUnit.MILLISECONDS.convert(timeDiff, unit);
98 | }
99 |
100 | private void setQueryPort(MessageData data) {
101 | String queryPort = null;
102 | if(data.isHeartBeat()) {
103 | queryPort = data.getData(GeneralDataKeysEnum.HEARTBEAT);
104 | } else if(data.isHeartBeatBroadcast()) {
105 | queryPort = data.getData(GeneralDataKeysEnum.HEARTBEATBROADCAST);
106 | }
107 | IntVerify verifier = new IntVerify(1024, 65535);
108 | if(verifier.verify(queryPort)) {
109 | this.queryPort = verifier.getVerifiedValue();
110 | }
111 | }
112 |
113 | @Override
114 | public boolean processNewMessage(MessageData completeQuery) {
115 | if(completeQuery.isQueryAnswer()) {
116 | this.queryInfo.update(completeQuery);
117 | if(!this.hasAnswered) {
118 | String logMessage = "New server for game " + this.getDisplayName() + ": " + this.serverAddress.getAddress().getHostAddress();
119 | String gamePort = this.getGamePort();
120 | if(gamePort != null) {
121 | logMessage += ":" + gamePort;
122 | }
123 | String name = this.getServerName();
124 | if(name != null) {
125 | logMessage += " -> " + name;
126 | }
127 | Logger.logStatic(LogLevel.NORMAL, logMessage);
128 | }
129 | this.hasAnswered = true;
130 | this.lastMessage = System.currentTimeMillis();
131 | return this.manuallyAdded;
132 | }
133 | return false;
134 | }
135 |
136 | @Override
137 | public boolean hasAnsweredToQuery() {
138 | return this.hasAnswered;
139 | }
140 |
141 | @Override
142 | public void markQueryRequestAsSentWithCurrentTime() {
143 | this.lastQueryRequest = System.currentTimeMillis();
144 | }
145 |
146 | public MessageData getQueryInfo() {
147 | return this.queryInfo;
148 | }
149 |
150 | @Override
151 | public boolean isBroadcastedServer() {
152 | return !this.realHeartbeat;
153 | }
154 |
155 | public String getDisplayName() {
156 | return this.game.getGameDisplayName(this);
157 | }
158 |
159 | public String getGamePort() {
160 | return this.game.getGamePort(this);
161 | }
162 |
163 | public GameBase getGame() {
164 | return this.game;
165 | }
166 |
167 | private class QueryBuffer {
168 | private Map queries;
169 | private Map infos;
170 | private int lastId;
171 |
172 | public QueryBuffer() {
173 | this.queries = new HashMap();
174 | this.infos = new HashMap();
175 | this.lastId = 0;
176 | }
177 |
178 | public boolean put(MessageData query) {
179 | if(query.isQueryAnswer()) {
180 | QueryInfo info = query.getQueryInfo();
181 | if(info.getId() >= this.lastId) {
182 | this.lastId = info.getId();
183 | int part = info.getPart();
184 | if(part >= 1) {
185 | this.queries.put(part, query);
186 | this.infos.put(part, info);
187 | return true;
188 | }
189 | }
190 | }
191 | return false;
192 | }
193 |
194 | public MessageData getQueryIfComplete() {
195 | boolean ok = true;
196 | int lastPart = -1;
197 | int i = 1;
198 | while(ok) {
199 | QueryInfo info = this.infos.get(i);
200 | if(ok = (info != null && info.getId() == this.lastId && info.getPart() == i)) {
201 | if(info.isFinal()) {
202 | lastPart = info.getPart();
203 | break;
204 | }
205 | }
206 | i++;
207 | }
208 | MessageData out = null;
209 | if(ok && lastPart >= 1) {
210 | out = this.queries.get(1);
211 | for(int j = 2; j <= lastPart; j++) {
212 | out = out.combine(this.queries.get(j));
213 | }
214 | this.clear();
215 | }
216 | return out;
217 | }
218 |
219 | private void clear() {
220 | this.queries.clear();
221 | this.infos.clear();
222 | }
223 | }
224 |
225 | @Override
226 | public InetSocketAddress getSocketAddress() {
227 | return this.serverAddress;
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/games/GameServerInterface.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.games;
2 |
3 | import java.net.InetSocketAddress;
4 | import java.util.concurrent.TimeUnit;
5 |
6 | import de.poweruser.powerserver.main.MessageData;
7 |
8 | public interface GameServerInterface {
9 | public boolean processNewMessage(MessageData completeQuery);
10 |
11 | public boolean incomingHeartbeat(InetSocketAddress serverAddress, MessageData message, boolean manuallyAdded);
12 |
13 | public boolean incomingHeartBeatBroadcast(InetSocketAddress serverAddress, MessageData data);
14 |
15 | public boolean incomingQueryAnswer(InetSocketAddress serverAddress, MessageData data);
16 |
17 | public int getQueryPort();
18 |
19 | /**
20 | * Checks whether the last heart-beat for this server has been received not
21 | * longer ago, than the specified time duration.
22 | *
23 | * @param timeDiff
24 | * The time duration to check
25 | * @param unit
26 | * The time unit of the provided time duration
27 | * @return true, if the last heart-beat was received within the specified
28 | * duration
29 | * false, if a heart-beat has been received already and it was
30 | * received longer ago than the specified duration, or if no
31 | * heart-beat has been received yet
32 | */
33 |
34 | public boolean checkLastHeartbeat(long timeDiff, TimeUnit unit);
35 |
36 | /**
37 | * Checks whether the last message of this server has been received not
38 | * longer ago, than the specified time duration
39 | *
40 | * @param timeDiff
41 | * The time duration to check
42 | * @param unit
43 | * The time unit of the provided time duration
44 | * @return true, if the last message was received within the specified
45 | * duration
46 | * false, if a message has been received already and it was
47 | * received longer ago than the specified duration, or if no message
48 | * has been received yet
49 | */
50 |
51 | public boolean checkLastMessage(long timeDiff, TimeUnit unit);
52 |
53 | /**
54 | * Checks whether the last query request of this server has been sent not
55 | * longer ago, than the specified time duration
56 | *
57 | * @param timeDiff
58 | * The time duration to check
59 | * @param unit
60 | * The time unit of the provided time duration
61 | * @return true, if the last query request was sent within the specified
62 | * duration
63 | * false, if a query request has been sent yet and it was sent
64 | * longer ago than the specified duration, or if no query request
65 | * has been sent yet
66 | */
67 |
68 | public boolean checkLastQueryRequest(long timeDiff, TimeUnit unit);
69 |
70 | /**
71 | * Checks if this server has answered to a query request yet
72 | *
73 | * @return true, if a query answer has been received from this server yet,
74 | * otherwise false
75 | */
76 | public boolean hasAnsweredToQuery();
77 |
78 | /**
79 | * Checks if another master server is sending heart-beat broadcasts
80 | * for this game server, or if the master server is receiving the real
81 | * heart-beats from this game server.
82 | *
83 | * @return true, if another master server is sending heart-beat broadcasts
84 | * for this game server
85 | * false, if this master server is receiving the real heart-beats of
86 | * the game server
87 | */
88 |
89 | public boolean isBroadcastedServer();
90 |
91 | /**
92 | * Gets the name of the game server, if no name of the game server has been
93 | * received yet, it returns null
94 | *
95 | * @return The server name if one has been received yet.
96 | * null, if none has been received yet
97 | */
98 |
99 | public String getServerName();
100 |
101 | public InetSocketAddress getSocketAddress();
102 |
103 | /**
104 | * Sets a flag with the current time for the last sent query request. This
105 | * flag will be used when testing for the last send time of a query request
106 | * with the method checkLastQueryRequest(long, TimeUnit)
107 | */
108 |
109 | public void markQueryRequestAsSentWithCurrentTime();
110 | }
111 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/games/GamesEnum.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.games;
2 |
3 | import de.poweruser.powerserver.games.opflashr.OperationFlashpointResistance;
4 | import de.poweruser.powerserver.main.parser.GamespyProtocol1Parser;
5 |
6 | public enum GamesEnum {
7 | OPERATIONFLASHPOINT_RESISTANCE(new OperationFlashpointResistance("opflashr", "Y3k7x1", new GamespyProtocol1Parser()));
8 |
9 | private GameBase game;
10 |
11 | private GamesEnum(GameBase game) {
12 | this.game = game;
13 | }
14 |
15 | public GameBase getGame() {
16 | return this.game;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/games/GeneralDataKeysEnum.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.games;
2 |
3 | import de.poweruser.powerserver.main.parser.dataverification.DummyVerify;
4 | import de.poweruser.powerserver.main.parser.dataverification.IPAddressVerify;
5 | import de.poweruser.powerserver.main.parser.dataverification.IntVerify;
6 | import de.poweruser.powerserver.main.parser.dataverification.QueryIdFormatVerify;
7 | import de.poweruser.powerserver.main.parser.dataverification.StringLengthVerify;
8 | import de.poweruser.powerserver.main.parser.dataverification.StringVerify;
9 | import de.poweruser.powerserver.main.parser.dataverification.VerificationInterface;
10 |
11 | public enum GeneralDataKeysEnum implements DataKeysInterface {
12 | ENCTYPE("enctype", new IntVerify(0, 3)),
13 | FINAL("final", new StringVerify(new String[] {}, false)),
14 | GAMENAME("gamename"),
15 | HEARTBEAT("heartbeat", new IntVerify(1024, 65535)),
16 | HEARTBEATBROADCAST("heartbeatbroadcast", new IntVerify(1024, 65535)),
17 | HOST("host", new IPAddressVerify()),
18 | LIST("list", new StringVerify(new String[] { "cmp" }, false)),
19 | LOCATION("location", new IntVerify(0, 20)),
20 | QUERYID("queryid", new QueryIdFormatVerify()),
21 | STATECHANGED("statechanged", new IntVerify(0, 1)),
22 | VALIDATE("validate", new StringLengthVerify(8, 8)),
23 | WHERE("where");
24 |
25 | private String key;
26 | private VerificationInterface verifier;
27 |
28 | private GeneralDataKeysEnum(String key) {
29 | this.key = key;
30 | this.verifier = null;
31 | }
32 |
33 | private GeneralDataKeysEnum(String key, VerificationInterface verifier) {
34 | this.key = key;
35 | this.verifier = verifier;
36 | }
37 |
38 | @Override
39 | public String getKeyString() {
40 | return this.key;
41 | }
42 |
43 | @Override
44 | public boolean verifyData(String data) {
45 | if(this.verifier == null || this.verifier.verify(data)) {
46 | return true;
47 | } else {
48 | return false;
49 | }
50 | }
51 |
52 | public VerificationInterface getVerifierCopy() {
53 | if(this.verifier == null) { return new DummyVerify(); }
54 | return this.verifier.createCopy();
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/games/opflashr/DataKeyEnum.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.games.opflashr;
2 |
3 | import de.poweruser.powerserver.games.DataKeysInterface;
4 | import de.poweruser.powerserver.main.parser.dataverification.DummyVerify;
5 | import de.poweruser.powerserver.main.parser.dataverification.IntVerify;
6 | import de.poweruser.powerserver.main.parser.dataverification.QueryIdFormatVerify;
7 | import de.poweruser.powerserver.main.parser.dataverification.StringVerify;
8 | import de.poweruser.powerserver.main.parser.dataverification.VerificationInterface;
9 |
10 | public enum DataKeyEnum implements DataKeysInterface {
11 | ACTUALVERSION("actver", new IntVerify(100, Integer.MAX_VALUE)),
12 | EQUALMODREQUIRED("equalModRequired", new IntVerify(0, 1)),
13 | FINAL("final", new StringVerify(new String[] { "" }, false)),
14 | GAMEMODE("gamemode"),
15 | GAMENAME("gamename"),
16 | GAMESTATE("gstate", new IntVerify(0, 14)),
17 | GAMEVERSION("gamever"),
18 | GROUPID("groupid", new IntVerify(261, 261)),
19 | HEARTBEAT("heartbeat", new IntVerify(1024, 65535)),
20 | HEARTBEATBROADCAST("heartbeatbroadcast", new IntVerify(1024, 65535)),
21 | HOSTNAME("hostname"),
22 | HOSTPORT("hostport", new IntVerify(1024, 65535)),
23 | IMPLENTATION("impl", new StringVerify(new String[] { "sockets", "dplay" }, false)),
24 | ISLANDNAME("mapname"),
25 | LOADEDMODS("mod"),
26 | MAXPLAYERS("maxplayers", new IntVerify(0, 128)),
27 | MISSIONNAME("gametype"),
28 | NUMBEROFPLAYERS("numplayers", new IntVerify(0, 128)),
29 | PARAMETER1("param1"),
30 | PARAMETER2("param2"),
31 | PASSWORD("password", new IntVerify(0, 1)),
32 | PLATFORM("platform", new StringVerify(new String[] { "win", "linux" }, false)),
33 | QUERYID("queryid", new QueryIdFormatVerify()),
34 | REQUIREDVERSION("reqver", new IntVerify(100, Integer.MAX_VALUE)),
35 | TIMELEFT("timeleft", new IntVerify(0, 1440)),
36 |
37 | // Special cases
38 | PLAYER_("player_"),
39 | SCORE_("score_", new IntVerify(Integer.MIN_VALUE, Integer.MAX_VALUE)),
40 | DEATHS_("deaths_", new IntVerify(0, Integer.MAX_VALUE)),
41 | TEAM_("team_");
42 |
43 | private String key;
44 | private VerificationInterface verifier;
45 |
46 | private DataKeyEnum(String key) {
47 | this.key = key;
48 | this.verifier = null;
49 | }
50 |
51 | private DataKeyEnum(String key, VerificationInterface verifier) {
52 | this.key = key;
53 | this.verifier = verifier;
54 | }
55 |
56 | @Override
57 | public String getKeyString() {
58 | return this.key;
59 | }
60 |
61 | @Override
62 | public boolean verifyData(String data) {
63 | if(this.verifier == null || this.verifier.verify(data)) {
64 | return true;
65 | } else {
66 | return false;
67 | }
68 | }
69 |
70 | public Object getVerifierCopy() {
71 | if(this.verifier == null) { return new DummyVerify(); }
72 | return this.verifier.createCopy();
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/games/opflashr/OFPServer.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.games.opflashr;
2 |
3 | import java.net.InetSocketAddress;
4 |
5 | import de.poweruser.powerserver.games.GameBase;
6 | import de.poweruser.powerserver.games.GameServerBase;
7 | import de.poweruser.powerserver.main.MessageData;
8 |
9 | public class OFPServer extends GameServerBase {
10 |
11 | public OFPServer(GameBase game, InetSocketAddress server) {
12 | super(game, server);
13 | }
14 |
15 | @Override
16 | public boolean processNewMessage(MessageData message) {
17 | return super.processNewMessage(message);
18 | }
19 |
20 | @Override
21 | public String getServerName() {
22 | if(this.queryInfo.containsKey(DataKeyEnum.HOSTNAME)) { return this.queryInfo.getData(DataKeyEnum.HOSTNAME); }
23 | return null;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/games/opflashr/OperationFlashpointResistance.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.games.opflashr;
2 |
3 | import java.net.DatagramPacket;
4 | import java.net.InetSocketAddress;
5 |
6 | import de.poweruser.powerserver.games.DataKeysInterface;
7 | import de.poweruser.powerserver.games.GameBase;
8 | import de.poweruser.powerserver.games.GameServerBase;
9 | import de.poweruser.powerserver.games.GameServerInterface;
10 | import de.poweruser.powerserver.games.GeneralDataKeysEnum;
11 | import de.poweruser.powerserver.main.MessageData;
12 | import de.poweruser.powerserver.main.parser.DataParserInterface;
13 |
14 | public class OperationFlashpointResistance extends GameBase {
15 |
16 | private String displayName = "Operation Flashpoint:Resistance";
17 |
18 | public OperationFlashpointResistance(String gamename, String gamespyKey, DataParserInterface parser) {
19 | super(gamename, gamespyKey, parser, DataKeyEnum.values());
20 | this.adjustPlayerInfoDataKeys();
21 | }
22 |
23 | private void adjustPlayerInfoDataKeys() {
24 | DataKeyEnum player = DataKeyEnum.PLAYER_;
25 | DataKeyEnum score = DataKeyEnum.SCORE_;
26 | DataKeyEnum deaths = DataKeyEnum.DEATHS_;
27 | DataKeyEnum team = DataKeyEnum.TEAM_;
28 | this.keyMap.remove(player.getKeyString());
29 | this.keyMap.remove(score.getKeyString());
30 | this.keyMap.remove(deaths.getKeyString());
31 | this.keyMap.remove(team.getKeyString());
32 | for(int i = 0; i <= 30; i++) {
33 | String index = String.valueOf(i);
34 | this.keyMap.put(player.getKeyString() + index, player);
35 | this.keyMap.put(score.getKeyString() + index, score);
36 | this.keyMap.put(deaths.getKeyString() + index, deaths);
37 | this.keyMap.put(team.getKeyString() + index, team);
38 | }
39 | }
40 |
41 | @Override
42 | public DataKeysInterface getHeartBeatDataKey() {
43 | return DataKeyEnum.HEARTBEAT;
44 | }
45 |
46 | @Override
47 | public DataKeysInterface getHeartBeatBroadcastDataKey() {
48 | return DataKeyEnum.HEARTBEATBROADCAST;
49 | }
50 |
51 | @Override
52 | public GameServerInterface createNewServer(InetSocketAddress server) {
53 | return new OFPServer(this, server);
54 | }
55 |
56 | @Override
57 | public String getGameDisplayName(GameServerBase gameServer) throws IllegalArgumentException {
58 | if(!this.equals(gameServer.getGame())) { throw new IllegalArgumentException("The game of the gameServer does not match the selected game: " + gameServer.getGame().getGameName() + " vs " + this.getGameName()); }
59 | MessageData data = gameServer.getQueryInfo();
60 | String out = this.displayName;
61 | String version = null;
62 | if(data.containsKey(DataKeyEnum.GAMEVERSION)) {
63 | version = data.getData(DataKeyEnum.GAMEVERSION);
64 | } else if(data.containsKey(DataKeyEnum.ACTUALVERSION)) {
65 | version = data.getData(DataKeyEnum.ACTUALVERSION);
66 | }
67 | if(version != null) {
68 | if(version.equals("1.99")) {
69 | out = "Arma:ColdWarAssault";
70 | }
71 | out += " (version " + version + ")";
72 | }
73 | return out;
74 | }
75 |
76 | @Override
77 | public String getGamePort(GameServerBase gameServer) {
78 | if(!this.equals(gameServer.getGame())) { throw new IllegalArgumentException("The game of the gameServer does not match the selected game: " + gameServer.getGame().getGameName() + " vs " + this.getGameName()); }
79 | MessageData data = gameServer.getQueryInfo();
80 | String out = null;
81 | if(data.containsKey(DataKeyEnum.HOSTPORT)) {
82 | out = data.getData(DataKeyEnum.HOSTPORT);
83 | }
84 | return out;
85 | }
86 |
87 | @Override
88 | public DatagramPacket createHeartbeatBroadcast(InetSocketAddress server, MessageData data) {
89 | DataKeysInterface heartbeat = this.getHeartBeatDataKey();
90 | DataKeysInterface broadcast = this.getHeartBeatBroadcastDataKey();
91 | String queryPort = (heartbeat != null && data.containsKey(heartbeat) ? data.getData(heartbeat) : String.valueOf(server.getPort()));
92 | if(broadcast != null) {
93 | StringBuilder builder = new StringBuilder();
94 | builder.append("\\");
95 | builder.append(broadcast.getKeyString());
96 | builder.append("\\");
97 | builder.append(queryPort);
98 | builder.append("\\");
99 | builder.append(GeneralDataKeysEnum.HOST.getKeyString());
100 | builder.append("\\");
101 | builder.append(server.getAddress().getHostAddress());
102 | builder.append("\\");
103 | builder.append(GeneralDataKeysEnum.GAMENAME.getKeyString());
104 | builder.append("\\");
105 | builder.append(this.gamename);
106 | if(data.containsKey(GeneralDataKeysEnum.STATECHANGED)) {
107 | builder.append("\\");
108 | builder.append(GeneralDataKeysEnum.STATECHANGED.getKeyString());
109 | builder.append("\\");
110 | builder.append(data.getData(GeneralDataKeysEnum.STATECHANGED));
111 | }
112 | byte[] message = builder.toString().getBytes();
113 | return new DatagramPacket(message, message.length);
114 | }
115 | return null;
116 | }
117 |
118 | @Override
119 | public DatagramPacket createStatusQuery(boolean queryPlayers) {
120 | StringBuilder builder = new StringBuilder();
121 | builder.append("\\");
122 | if(queryPlayers) {
123 | builder.append("info\\rules\\players");
124 | } else {
125 | builder.append("status");
126 | }
127 | builder.append("\\");
128 | byte[] message = builder.toString().getBytes();
129 | return new DatagramPacket(message, message.length);
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/gamespy/EncType.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.gamespy;
2 |
3 | import de.poweruser.powerserver.gamespy.encoders.EncType2Encoder;
4 | import de.poweruser.powerserver.gamespy.encoders.EncoderInterface;
5 | import de.poweruser.powerserver.gamespy.encoders.OFPMonitorEncoder;
6 |
7 | public enum EncType {
8 | BASIC(0, null),
9 | ADVANCED1(1, null),
10 | ADVANCED2(2, new EncType2Encoder()),
11 | OFPMONITOR(3, new OFPMonitorEncoder());
12 |
13 | private int type;
14 | private EncoderInterface encoder;
15 |
16 | private EncType(int type, EncoderInterface encoder) {
17 | this.type = type;
18 | this.encoder = encoder;
19 | }
20 |
21 | public int getTypeValue() {
22 | return this.type;
23 | }
24 |
25 | public static EncType getTypeFromValue(int value) {
26 | switch(value) {
27 | case 0:
28 | return BASIC;
29 | case 1:
30 | return ADVANCED1;
31 | case 2:
32 | return ADVANCED2;
33 | case 3:
34 | return OFPMONITOR;
35 | default:
36 | }
37 | return null;
38 | }
39 |
40 | public EncoderInterface getEncoder() {
41 | return this.encoder;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/gamespy/GamespyValidation.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.gamespy;
2 |
3 | import de.poweruser.powerserver.games.GameBase;
4 | import de.poweruser.powerserver.logger.LogLevel;
5 | import de.poweruser.powerserver.logger.Logger;
6 |
7 | /*
8 | * This is a Java port of the algorithm, that the Gamespy master server and the games use,
9 | * to generate the challenge-response for Gamespy's "secure" protocol
10 | * The original C code (GSMSALG) was written by Luigi Auriemma: http://aluigi.altervista.org/
11 | * Previous ports from C to PHP by Lithium and from PHP to C# by FordGT90Concept were most helpful in order to get this done
12 | */
13 |
14 | public class GamespyValidation {
15 |
16 | private static final short[] enctype1_data = new short[] { 0x01, 0xba, 0xfa, 0xb2, 0x51, 0x00, 0x54, 0x80, 0x75, 0x16, 0x8e, 0x8e, 0x02, 0x08, 0x36, 0xa5, 0x2d, 0x05, 0x0d, 0x16, 0x52, 0x07, 0xb4, 0x22, 0x8c, 0xe9, 0x09, 0xd6, 0xb9, 0x26, 0x00, 0x04, 0x06, 0x05, 0x00, 0x13, 0x18, 0xc4, 0x1e, 0x5b, 0x1d, 0x76, 0x74, 0xfc, 0x50, 0x51, 0x06, 0x16, 0x00, 0x51, 0x28, 0x00, 0x04, 0x0a, 0x29, 0x78, 0x51, 0x00, 0x01, 0x11, 0x52, 0x16, 0x06, 0x4a, 0x20, 0x84, 0x01, 0xa2, 0x1e, 0x16, 0x47, 0x16, 0x32, 0x51, 0x9a, 0xc4, 0x03, 0x2a, 0x73, 0xe1, 0x2d, 0x4f, 0x18, 0x4b, 0x93, 0x4c, 0x0f, 0x39, 0x0a, 0x00, 0x04, 0xc0, 0x12, 0x0c, 0x9a, 0x5e, 0x02, 0xb3, 0x18, 0xb8, 0x07, 0x0c, 0xcd, 0x21, 0x05, 0xc0, 0xa9, 0x41, 0x43, 0x04, 0x3c, 0x52, 0x75, 0xec, 0x98, 0x80, 0x1d, 0x08, 0x02, 0x1d, 0x58, 0x84, 0x01, 0x4e, 0x3b, 0x6a, 0x53, 0x7a, 0x55, 0x56, 0x57, 0x1e, 0x7f, 0xec, 0xb8, 0xad, 0x00, 0x70, 0x1f, 0x82, 0xd8, 0xfc, 0x97, 0x8b, 0xf0, 0x83, 0xfe, 0x0e, 0x76, 0x03, 0xbe, 0x39, 0x29, 0x77, 0x30, 0xe0, 0x2b, 0xff, 0xb7, 0x9e, 0x01, 0x04, 0xf8, 0x01, 0x0e, 0xe8, 0x53, 0xff, 0x94, 0x0c, 0xb2, 0x45, 0x9e, 0x0a, 0xc7, 0x06, 0x18, 0x01, 0x64, 0xb0, 0x03, 0x98, 0x01, 0xeb, 0x02, 0xb0, 0x01, 0xb4, 0x12, 0x49, 0x07, 0x1f, 0x5f, 0x5e, 0x5d, 0xa0, 0x4f, 0x5b, 0xa0, 0x5a, 0x59, 0x58, 0xcf, 0x52, 0x54, 0xd0, 0xb8, 0x34, 0x02, 0xfc, 0x0e, 0x42, 0x29, 0xb8, 0xda, 0x00, 0xba, 0xb1, 0xf0, 0x12, 0xfd, 0x23, 0xae, 0xb6, 0x45, 0xa9, 0xbb, 0x06, 0xb8, 0x88, 0x14, 0x24, 0xa9, 0x00, 0x14, 0xcb, 0x24, 0x12, 0xae, 0xcc, 0x57, 0x56, 0xee, 0xfd, 0x08, 0x30, 0xd9, 0xfd, 0x8b, 0x3e, 0x0a, 0x84, 0x46, 0xfa, 0x77, 0xb8 };
17 |
18 | private byte[] securekey;
19 | private byte[] gamekey;
20 | private EncType type;
21 | private String validateString;
22 |
23 | public GamespyValidation(String securekey, String gamekey, EncType type) {
24 | if(securekey == null || gamekey == null || type == null) { throw new IllegalArgumentException("GamespyValidation: null parameters are not permitted."); }
25 | if(securekey.length() != 6) { throw new IllegalArgumentException("GamespyValidation: The challenge string must have a length of 6 characters. <" + securekey + "> has " + securekey.length()); }
26 | if(gamekey.length() != 6) { throw new IllegalArgumentException("GamespyValidation: The game key must have a length of 6 characters. <" + gamekey + "> has " + gamekey.length()); }
27 | this.gamekey = gamekey.getBytes();
28 | this.securekey = securekey.getBytes();
29 | this.type = type;
30 | this.validateString = null;
31 | }
32 |
33 | public GamespyValidation(String securekey, GameBase game, EncType type) {
34 | this(securekey, game.getGamespyKey(), type);
35 | }
36 |
37 | public GamespyValidation() {
38 | this.securekey = createChallengeString().getBytes();
39 | this.validateString = null;
40 | this.type = null;
41 | this.gamekey = null;
42 | }
43 |
44 | public boolean verifyChallengeResponse(GameBase game, EncType type, String response) {
45 | this.gamekey = game.getGamespyKey().getBytes();
46 | this.type = type;
47 | String correctValidate = this.getValidationString();
48 | if(correctValidate.equals(response)) {
49 | return true;
50 | } else {
51 | Logger.logStatic(LogLevel.HIGH, "Comparing of challengeResponse failed!");
52 | Logger.logStatic(LogLevel.HIGH, "Gamename: " + game.getGameName());
53 | Logger.logStatic(LogLevel.HIGH, "Gamekey: " + game.getGamespyKey());
54 | Logger.logStatic(LogLevel.HIGH, "Challenge: " + new String(this.securekey));
55 | Logger.logStatic(LogLevel.HIGH, "Validate " + correctValidate);
56 | Logger.logStatic(LogLevel.HIGH, "Client-Response: " + response);
57 | }
58 | return false;
59 | }
60 |
61 | public String getValidationString() {
62 | if(this.validateString != null) { return this.validateString; }
63 |
64 | short[] table = new short[256];
65 | int[] temp = new int[4]; // Some Temporary variables
66 |
67 | for(short i = 0; i < 256; i++) {
68 | table[i] = i;
69 | }
70 |
71 | for(short i = 0; i < 256; i++) {
72 | // Scramble the Table with our Handoff
73 | temp[0] = (temp[0] + table[i] + this.gamekey[i % this.gamekey.length]) & 255;
74 | temp[2] = table[temp[0]];
75 |
76 | // Update the buffer
77 | table[temp[0]] = table[i];
78 | table[i] = (short) temp[2];
79 | }
80 |
81 | temp[0] = 0;
82 | short[] key = new short[6];
83 | for(int i = 0; i < this.securekey.length; i++) {
84 | // Add the next char to the array
85 | key[i] = (byte) this.securekey[i];
86 |
87 | temp[0] = (temp[0] + key[i] + 1) & 255;
88 | temp[2] = table[temp[0]];
89 |
90 | temp[1] = (temp[1] + temp[2]) & 255;
91 | temp[3] = table[temp[1]];
92 |
93 | table[temp[1]] = (short) temp[2];
94 | table[temp[0]] = (short) temp[3];
95 |
96 | // XOR the Buffer
97 | key[i] ^= table[(temp[2] + temp[3]) & 255];
98 | }
99 |
100 | switch(this.type) {
101 | case ADVANCED1:
102 | for(short i = 0; i < this.securekey.length; i++) {
103 | key[i] = enctype1_data[key[i]];
104 | }
105 | break;
106 | case OFPMONITOR:
107 | case ADVANCED2:
108 | for(short i = 0; i < this.securekey.length; i++) {
109 | key[i] ^= this.gamekey[i % this.gamekey.length];
110 | }
111 | break;
112 | default:
113 | break;
114 | }
115 |
116 | int length = this.securekey.length / 3;
117 | StringBuilder sb = new StringBuilder();
118 |
119 | int j = 0;
120 | while(length >= 1) {
121 | length--;
122 |
123 | temp[2] = key[j];
124 | temp[3] = key[j + 1];
125 |
126 | sb.append(addChar(temp[2] >> 2));
127 | sb.append(addChar((temp[2] & 3) << 4 | (temp[3] >> 4)));
128 |
129 | temp[2] = key[j + 2];
130 |
131 | sb.append(addChar((temp[3] & 15) << 2 | (temp[2] >> 6)));
132 | sb.append(addChar(temp[2] & 63));
133 |
134 | j += 3;
135 | }
136 |
137 | this.validateString = sb.toString();
138 | return this.validateString;
139 | }
140 |
141 | private char addChar(int value) {
142 | char out = 0;
143 | if(value < 26) {
144 | out = (char) (value + 65);
145 | } else if(value < 52) {
146 | out = (char) (value + 71);
147 | } else if(value < 62) {
148 | out = (char) (value - 4);
149 | } else if(value == 62) {
150 | out = '+';
151 | } else if(value == 63) {
152 | out = '/';
153 | }
154 | return out;
155 | }
156 |
157 | public static String createChallengeString() {
158 | byte[] out = new byte[6];
159 | long rnd = System.nanoTime();
160 | for(int i = 0; i < 6; i++) {
161 | do {
162 | rnd = ((rnd * 0x343FDL) + 0x269EC3L) & 0x7FL;
163 | } while((rnd < 0x21) || (rnd >= 0x7F));
164 | out[i] = (byte) rnd;
165 | }
166 | return new String(out);
167 | }
168 |
169 | public String getChallengeString() {
170 | return new String(this.securekey);
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/gamespy/encoders/EncType2Encoder.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.gamespy.encoders;
2 |
3 | import java.io.ByteArrayOutputStream;
4 | import java.io.DataOutputStream;
5 | import java.io.IOException;
6 | import java.net.Inet4Address;
7 | import java.net.InetAddress;
8 | import java.net.InetSocketAddress;
9 | import java.util.ArrayList;
10 | import java.util.Iterator;
11 | import java.util.List;
12 |
13 | import de.poweruser.powerserver.gamespy.encoders.enctype2.DataSize;
14 | import de.poweruser.powerserver.gamespy.encoders.enctype2.EncType2;
15 | import de.poweruser.powerserver.gamespy.encoders.enctype2.UnsignedChar;
16 | import de.poweruser.powerserver.gamespy.encoders.enctype2.UnsignedCharPointer;
17 |
18 | public class EncType2Encoder implements EncoderInterface {
19 |
20 | @Override
21 | public byte[] encode(String gamekey, String validate, List servers) throws IOException {
22 | if(gamekey.length() != 6) { throw new IllegalArgumentException("The game key is not 6 bytes long! >" + gamekey + "<"); }
23 | if(validate.length() != 8) { throw new IllegalArgumentException("The validate word is not 8 bytes long! >" + validate + "<"); }
24 | ByteArrayOutputStream byteArray = new ByteArrayOutputStream();
25 | DataOutputStream stream = new DataOutputStream(byteArray);
26 |
27 | if(servers != null) {
28 | List ipv4 = new ArrayList();
29 | Iterator iter = servers.iterator();
30 | while(iter.hasNext()) {
31 | InetSocketAddress i = iter.next();
32 | InetAddress address = i.getAddress();
33 | if(address instanceof Inet4Address) {
34 | ipv4.add(i);
35 | }
36 | iter.remove();
37 | }
38 |
39 | for(InetSocketAddress i: ipv4) {
40 | this.writeAddress(stream, i);
41 | }
42 | }
43 | stream.writeBytes("\\final\\");
44 |
45 | byte[] data = byteArray.toByteArray();
46 | int len = data.length;
47 | stream.close();
48 |
49 | UnsignedChar[] array = new UnsignedChar[len + (1 + 8)];
50 | for(int i = 0; i < array.length; i++) {
51 | if(i < data.length) {
52 | array[i] = new UnsignedChar(data[i]);
53 | } else {
54 | array[i] = new UnsignedChar(0);
55 | }
56 | }
57 | UnsignedCharPointer p = new UnsignedCharPointer(array, 0);
58 | DataSize s = new DataSize(len);
59 | EncType2 enc = new EncType2();
60 | int encSize = enc.enctype2_encoder(gamekey, validate, p, s);
61 | byte[] output = new byte[encSize];
62 | UnsignedCharPointer z = new UnsignedCharPointer(array, 0);
63 | for(int i = 0; i < encSize; i++) {
64 | output[i] = z.getDataAtOffset(i).toSignedByte();
65 | }
66 | return output;
67 | }
68 |
69 | private void writeAddress(DataOutputStream stream, InetSocketAddress address) throws IOException {
70 | stream.write(address.getAddress().getAddress());
71 | int port = address.getPort();
72 | stream.writeByte((port >> 8) & 0xFF);
73 | stream.writeByte(port & 0xFF);
74 | }
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/gamespy/encoders/EncoderInterface.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.gamespy.encoders;
2 |
3 | import java.io.IOException;
4 | import java.net.InetSocketAddress;
5 | import java.util.List;
6 |
7 | public interface EncoderInterface {
8 |
9 | public byte[] encode(String gamekey, String validate, List servers) throws IOException;
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/gamespy/encoders/OFPMonitorEncoder.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.gamespy.encoders;
2 |
3 | import java.io.ByteArrayOutputStream;
4 | import java.io.DataOutputStream;
5 | import java.io.IOException;
6 | import java.net.Inet4Address;
7 | import java.net.Inet6Address;
8 | import java.net.InetAddress;
9 | import java.net.InetSocketAddress;
10 | import java.util.ArrayList;
11 | import java.util.Iterator;
12 | import java.util.List;
13 |
14 | public class OFPMonitorEncoder implements EncoderInterface {
15 |
16 | /*
17 | * Data-Format
18 | *
19 | * 8Bytes Header contains:
20 | * Integer - Length (number of bytes) of IPV4 addresses. Each address takes
21 | * 6Bytes (4 for IP, 2 for Port)
22 | * Integer - Length (number of bytes) of IPV6 addresses. Each address takes
23 | * 18Bytes (16 for IP, 2 for Port)
24 | *
25 | * IPV4 addresses
26 | * IPV6 addresses
27 | */
28 |
29 | @Override
30 | public byte[] encode(String gamekey, String validate, List servers) throws IOException {
31 | ByteArrayOutputStream byteArray = new ByteArrayOutputStream();
32 | DataOutputStream stream = new DataOutputStream(byteArray);
33 | List ipv4 = new ArrayList();
34 | List ipv6 = new ArrayList();
35 | if(servers != null) {
36 | Iterator iter = servers.iterator();
37 | while(iter.hasNext()) {
38 | InetSocketAddress i = iter.next();
39 | InetAddress address = i.getAddress();
40 | if(address instanceof Inet4Address) {
41 | ipv4.add(i);
42 | } else if(address instanceof Inet6Address) {
43 | ipv6.add(i);
44 | }
45 | iter.remove();
46 | }
47 | }
48 | int ipv4Len = ipv4.size() * 6;
49 | int ipv6Len = ipv6.size() * 18;
50 | for(int i = 0; i < 4; i++) {
51 | stream.writeByte(0xFF);
52 | }
53 | stream.writeInt(ipv4Len);
54 | stream.writeInt(ipv6Len);
55 | for(InetSocketAddress i: ipv4) {
56 | this.writeAddress(stream, i);
57 | }
58 | for(InetSocketAddress i: ipv6) {
59 | this.writeAddress(stream, i);
60 | }
61 | stream.flush();
62 | byte[] out = byteArray.toByteArray();
63 | stream.close();
64 | return out;
65 | }
66 |
67 | private void writeAddress(DataOutputStream stream, InetSocketAddress address) throws IOException {
68 | stream.write(address.getAddress().getAddress());
69 | int port = address.getPort();
70 | stream.writeByte((port >> 8) & 0xFF);
71 | stream.writeByte(port & 0xFF);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/gamespy/encoders/enctype2/DataSize.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.gamespy.encoders.enctype2;
2 |
3 | public class DataSize {
4 |
5 | private int size;
6 |
7 | public DataSize(int size) {
8 | this.size = size;
9 | }
10 |
11 | public int toValue() {
12 | return this.size;
13 | }
14 |
15 | public void sub(int i) {
16 | this.size -= i;
17 | }
18 |
19 | public void set(int i) {
20 | this.size = i;
21 | }
22 |
23 | public void add(int i) {
24 | this.size += i;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/gamespy/encoders/enctype2/EncType2.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.gamespy.encoders.enctype2;
2 |
3 | public class EncType2 {
4 |
5 | public int enctype2_wrapper(String key, UnsignedCharPointer data, int size) {
6 | UnsignedCharPointer p;
7 | DataSize s = new DataSize(size);
8 | p = enctype2_decoder(key, data, s);
9 | if(p.greaterThan(data)) {
10 | for(int i = 0; i < size; i++) {
11 | data.setDataAtOffset(i, p.getDataAtOffset(i));
12 | }
13 | }
14 | return s.toValue();
15 | }
16 |
17 | public UnsignedCharPointer enctype2_decoder(String key, UnsignedCharPointer data, DataSize s) {
18 | UnsignedInt[] dest = new UnsignedInt[326];
19 |
20 | data.XORAtOffset(0, new UnsignedChar(0xEC));
21 | UnsignedCharPointer datap = data.createPointerAtOffset(1);
22 |
23 | byte[] bkey = key.getBytes();
24 | for(int i = 0; i < key.length(); i++) {
25 | datap.XORAtOffset(i, new UnsignedChar(bkey[i]));
26 | }
27 |
28 | for(int i = 0; i < 326; i++) {
29 | dest[i] = new UnsignedInt(0);
30 | }
31 | if(!data.getDataAtOffset(0).sameValueAs(new UnsignedChar(0))) {
32 | EncTypeShared.encshare4(datap.createPointerAtOffset(0), data.getDataAtOffset(0).toInt(), dest);
33 | }
34 | int diff = data.getDataAtOffset(0).toInt();
35 | datap.movePosition(diff);
36 |
37 | s.sub(diff + 1);
38 | if(s.toValue() < 6) {
39 | s.set(0);
40 | return data;
41 | }
42 |
43 | EncTypeShared.encshare1(dest, datap.createPointerAtOffset(0), s);
44 |
45 | s.sub(6);
46 | return datap;
47 | }
48 |
49 | public int enctype2_encoder(String gamekey, String validate, UnsignedCharPointer data, DataSize s) {
50 | int header_size = 8;
51 | UnsignedInt[] dest = new UnsignedInt[326];
52 | for(int i = 0; i < dest.length; i++) {
53 | dest[i] = new UnsignedInt(0);
54 | }
55 | for(int i = s.toValue() - 1; i >= 0; i--) {
56 | data.getDataAtOffset(1 + header_size + i).set(data.getDataAtOffset(i));
57 | }
58 | data.setDataAtOffset(0, new UnsignedChar(header_size));
59 | UnsignedCharPointer datap = data.createPointerAtOffset(1);
60 | byte[] valBytes = validate.getBytes();
61 | for(int i = 0; i < header_size; i++) {
62 | datap.setDataAtOffset(i, new UnsignedChar(valBytes[i] & 0xFF));
63 | }
64 | for(int i = 256; i < 326; i++) {
65 | dest[i] = new UnsignedInt(0);
66 | }
67 | EncTypeShared.encshare4(datap.createPointerAtOffset(0), data.getDataAtOffset(0).toInt(), dest);
68 |
69 | EncTypeShared.encshare1(dest, datap.createPointerAtOffset(data.getDataAtOffset(0).toInt()), new DataSize(s.toValue()));
70 |
71 | byte[] keybytes = gamekey.getBytes();
72 | for(int i = 0; i < gamekey.length(); i++) {
73 | datap.XORAtOffset(i, new UnsignedChar(keybytes[i] & 0xFF));
74 | }
75 | s.add(1 + data.getDataAtOffset(0).toInt());
76 | data.XORAtOffset(0, new UnsignedChar(0xEC));
77 | return s.toValue();
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/gamespy/encoders/enctype2/EncTypeShared.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.gamespy.encoders.enctype2;
2 |
3 | public class EncTypeShared {
4 |
5 | protected static void encshare1(UnsignedInt[] dest, UnsignedCharPointer datap, DataSize size) {
6 | int len = size.toValue();
7 |
8 | UnsignedIntPointer q = new UnsignedIntPointer(dest, 309);
9 | EncTypeShared.encshare2(dest, q, 16);
10 |
11 | UnsignedCharToIntPointer p = new UnsignedCharToIntPointer(dest, 309);
12 | UnsignedCharToIntPointer s = p.copy();
13 |
14 | UnsignedCharPointer datapc = datap.createPointerAtOffset(0);
15 |
16 | while(len-- > 0) {
17 | int y = p.getDistanceInChar(s);
18 | if(y == 63) {
19 | p = s.copy();
20 | EncTypeShared.encshare2(dest, p.createIntPointer(), 16);
21 | }
22 | UnsignedChar x = p.getData();
23 | datapc.XORAtOffset(0, x);
24 | datapc.movePosition(1);
25 | p.movePositionByChar(1);
26 | }
27 | }
28 |
29 | protected static void encshare2(UnsignedInt[] tbuff, UnsignedIntPointer tbuffq, int len) {
30 | UnsignedInt t2 = tbuff[304].copy();
31 | UnsignedInt t1 = tbuff[305].copy();
32 | UnsignedInt t3 = tbuff[306].copy();
33 | UnsignedInt t5 = tbuff[307].copy();
34 | UnsignedInt t4 = new UnsignedInt(0);
35 |
36 | UnsignedIntPointer tbuffp = tbuffq.createPointerAtOffset(0);
37 | UnsignedIntPointer limit = tbuffp.createPointerAtOffset(len);
38 | UnsignedIntPointer p;
39 |
40 | while(limit.greaterThan(tbuffp)) {
41 | p = new UnsignedIntPointer(tbuff, t2.toLong() + 272L);
42 | while(t5.toLong() < 65535L) {
43 | t1.add(t5);
44 | p.movePosition(1);
45 | t3.add(t1);
46 | t1.add(t3);
47 | p.setDataAtOffset(-17, t1);
48 | p.setDataAtOffset(-1, t3);
49 | t4 = t3.createRotationRight(8);
50 | p.setDataAtOffset(15, t5);
51 | t5.shiftLeft(1);
52 | t2.add(new UnsignedInt(1));
53 | t1.XOR(tbuff[t1.toByteValue()]);
54 | t4.XOR(tbuff[t4.toByteValue()]);
55 | t3 = t4.createRotationRight(8);
56 | t4 = t1.createRotationLeft(8);
57 | t4.XOR(tbuff[t4.toByteValue()]);
58 | t3.XOR(tbuff[t3.toByteValue()]);
59 | t1 = t4.createRotationLeft(8);
60 | }
61 |
62 | t3.XOR(t1);
63 | UnsignedInt x = tbuffp.getDataAtOffset(0);
64 | x.set(t3);
65 | tbuffp.movePosition(1);
66 | t2.sub(new UnsignedInt(1));
67 | t1.set(tbuff[t2.toInt() + 256]);
68 | t5.set(tbuff[t2.toInt() + 272]);
69 | t1.complement();
70 | t3 = t1.createRotationRight(8);
71 | t3.XOR(tbuff[t3.toByteValue()]);
72 | t5.XOR(tbuff[t5.toByteValue()]);
73 | t1 = t3.createRotationRight(8);
74 | t4 = t5.createRotationLeft(8);
75 | t1.XOR(tbuff[t1.toByteValue()]);
76 | t4.XOR(tbuff[t4.toByteValue()]);
77 | t3 = t4.createRotationLeft(8);
78 | UnsignedInt x2 = tbuff[t2.toInt() + 288].createShiftLeft(1);
79 | x2.add(new UnsignedInt(1));
80 | t5.set(x2);
81 | }
82 | tbuff[304].set(t2);
83 | tbuff[305].set(t1);
84 | tbuff[306].set(t3);
85 | tbuff[307].set(t5);
86 | }
87 |
88 | protected static void encshare3(UnsignedInt[] data, int n1, int n2) {
89 | UnsignedInt t2 = new UnsignedInt(n1);
90 | UnsignedInt t1 = new UnsignedInt(0);
91 | UnsignedInt t3 = new UnsignedInt(0);
92 | UnsignedInt t4 = new UnsignedInt(1);
93 | data[304].set(new UnsignedInt(0));
94 |
95 | for(int i = 32768; i > 0; i >>= 1) {
96 |
97 | t2.add(t4);
98 | t1.add(t2);
99 | t2.add(t1);
100 |
101 | if((n2 & i) != 0) {
102 | t2.complement();
103 | t4.shiftLeft(1);
104 | t4.add(new UnsignedInt(1));
105 | t3.set(t2.createRotationRight(8));
106 | t3.XOR(data[t3.toByteValue()]);
107 | t1.XOR(data[t1.toByteValue()]);
108 | t2.set(t3.createRotationRight(8));
109 | t3.set(t1.createRotationLeft(8));
110 | t2.XOR(data[t2.toByteValue()]);
111 | t3.XOR(data[t3.toByteValue()]);
112 | t1.set(t3.createRotationLeft(8));
113 | } else {
114 | data[data[304].toInt() + 256].set(t2);
115 | data[data[304].toInt() + 272].set(t1);
116 | data[data[304].toInt() + 288].set(t4);
117 | data[304].add(new UnsignedInt(1));
118 | t3.set(t1.createRotationRight(8));
119 | t2.XOR(data[t2.toByteValue()]);
120 | t3.XOR(data[t3.toByteValue()]);
121 | t1.set(t3.createRotationRight(8));
122 | t3.set(t2.createRotationLeft(8));
123 | t3.XOR(data[t3.toByteValue()]);
124 | t1.XOR(data[t1.toByteValue()]);
125 | t2.set(t3.createRotationLeft(8));
126 | t4.shiftLeft(1);
127 | }
128 | }
129 |
130 | data[305].set(t2);
131 | data[306].set(t1);
132 | data[307].set(t4);
133 | data[308].set(new UnsignedInt(n1));
134 | }
135 |
136 | protected static void encshare4(UnsignedCharPointer src, int size, UnsignedInt[] dest) {
137 | for(int i = 0; i < 256; i++) {
138 | dest[i].set(new UnsignedInt(0));
139 | }
140 |
141 | UnsignedInt tmp;
142 |
143 | for(int y = 0; y < 4; y++) {
144 | for(int i = 0; i < 256; i++) {
145 | dest[i].shiftLeft(8);
146 | dest[i].add(new UnsignedInt(i));
147 | }
148 |
149 | for(int pos = y, x = 0; x < 2; x++) {
150 | for(int j = 0; j < 256; j++) {
151 | tmp = dest[j].copy();
152 | pos += tmp.toLong() + src.getDataAtOffset(j % size).toInt();
153 | pos &= 0xFF;
154 | dest[j].set(dest[pos]);
155 | dest[pos].set(tmp);
156 | }
157 | }
158 | }
159 |
160 | for(int i = 0; i < 256; i++) {
161 | dest[i].XOR(new UnsignedInt(i));
162 | }
163 |
164 | EncTypeShared.encshare3(dest, 0, 0);
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/gamespy/encoders/enctype2/UnsignedChar.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.gamespy.encoders.enctype2;
2 |
3 | public class UnsignedChar {
4 |
5 | private short value;
6 | public static final short MAX = 0xFF;
7 |
8 | public UnsignedChar(int i) {
9 | this.value = (short) (i & MAX);
10 | }
11 |
12 | public int toInt() {
13 | return this.value;
14 | }
15 |
16 | public byte toSignedByte() {
17 | return (byte) this.value;
18 | }
19 |
20 | public void set(UnsignedChar dataAtOffset) {
21 | this.value = dataAtOffset.value;
22 | }
23 |
24 | public void XOR(UnsignedChar j) {
25 | this.value = (short) ((this.value ^ j.value) & MAX);
26 | }
27 |
28 | public boolean sameValueAs(UnsignedChar ui) {
29 | return this.toInt() == ui.toInt();
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/gamespy/encoders/enctype2/UnsignedCharPointer.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.gamespy.encoders.enctype2;
2 |
3 | public class UnsignedCharPointer {
4 |
5 | private UnsignedChar[] array;
6 | private int position;
7 |
8 | public UnsignedCharPointer(UnsignedChar[] array, int offset) {
9 | this.array = array;
10 | this.position = offset;
11 | }
12 |
13 | public boolean greaterThan(UnsignedCharPointer data) {
14 | return this.position > data.position;
15 | }
16 |
17 | public UnsignedChar getDataAtOffset(int i) {
18 | return this.array[this.position + i];
19 | }
20 |
21 | public void setDataAtOffset(int i, UnsignedChar dataAtOffset) {
22 | this.setData(this.position + i, dataAtOffset);
23 | }
24 |
25 | public void XORAtOffset(int i, UnsignedChar j) {
26 | UnsignedChar u = this.getData(this.position + i);
27 | u.XOR(j);
28 | }
29 |
30 | public UnsignedCharPointer createPointerAtOffset(int i) {
31 | return new UnsignedCharPointer(this.array, this.position + i);
32 | }
33 |
34 | public void movePosition(int diff) {
35 | this.position += diff;
36 | }
37 |
38 | private UnsignedChar getData(int position) {
39 | return this.array[position];
40 | }
41 |
42 | private void setData(int position, UnsignedChar ui) {
43 | this.array[position].set(ui);
44 | }
45 |
46 | public int getPointerPosition() {
47 | return this.position;
48 | }
49 |
50 | public UnsignedChar[] getDataArray() {
51 | return this.array;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/gamespy/encoders/enctype2/UnsignedCharToIntPointer.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.gamespy.encoders.enctype2;
2 |
3 | public class UnsignedCharToIntPointer {
4 |
5 | private UnsignedInt[] data;
6 | private int absolutePosition;
7 |
8 | public UnsignedCharToIntPointer(UnsignedInt[] array, int offset) {
9 | this.data = array;
10 | this.absolutePosition = 0;
11 | this.movePositionByInt(offset);
12 | }
13 |
14 | private void movePositionByInt(int diff) {
15 | this.absolutePosition += (4 * diff);
16 | }
17 |
18 | public void movePositionByChar(int diff) {
19 | this.absolutePosition += diff;
20 | }
21 |
22 | public int getDistanceInChar(UnsignedCharToIntPointer ui) {
23 | return this.absolutePosition - ui.absolutePosition;
24 | }
25 |
26 | public UnsignedCharToIntPointer copy() {
27 | UnsignedCharToIntPointer out = new UnsignedCharToIntPointer(this.data, 0);
28 | out.absolutePosition = this.absolutePosition;
29 | return out;
30 | }
31 |
32 | public UnsignedIntPointer createIntPointer() {
33 | if(this.toCharPosition() == 0) { return new UnsignedIntPointer(this.data, this.toIntPosition()); }
34 | return null;
35 | }
36 |
37 | private int toIntPosition() {
38 | return this.absolutePosition / 4;
39 | }
40 |
41 | private int toCharPosition() {
42 | return this.absolutePosition % 4;
43 | }
44 |
45 | public UnsignedChar getData() {
46 | UnsignedInt i = this.data[this.toIntPosition()];
47 | switch(this.toCharPosition()) {
48 | default:
49 | case 0:
50 | return i.getFirstByteOfValue();
51 | case 1:
52 | return i.getSecondByteOfValue();
53 | case 2:
54 | return i.getThirdByteOfValue();
55 | case 3:
56 | return i.getForthByteOfValue();
57 | }
58 | }
59 |
60 | public int getPointerPositionInInt() {
61 | if(this.toCharPosition() == 0) { return this.toIntPosition(); }
62 | return -1;
63 | }
64 |
65 | public UnsignedInt[] getDataArray() {
66 | return this.data;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/gamespy/encoders/enctype2/UnsignedInt.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.gamespy.encoders.enctype2;
2 |
3 | public class UnsignedInt {
4 |
5 | private long value;
6 | private static final long MAX = 0xFFFFFFFFL;
7 |
8 | public UnsignedInt(long n1) {
9 | this.set(n1);
10 | }
11 |
12 | public UnsignedInt(int n1) {
13 | this.set(n1);
14 | }
15 |
16 | public void add(UnsignedInt t2) {
17 | this.set(this.value + t2.value);
18 | }
19 |
20 | public void sub(UnsignedInt t2) {
21 | this.set(this.value - t2.value);
22 | }
23 |
24 | public void complement() {
25 | this.set(~this.value);
26 |
27 | }
28 |
29 | public void shiftLeft(int i) {
30 | this.set(this.value << i);
31 | }
32 |
33 | private void set(long l) {
34 | this.value = (l & MAX);
35 | }
36 |
37 | public void set(UnsignedInt ui) {
38 | this.set(ui.value);
39 | }
40 |
41 | public UnsignedInt createRotationRight(int range) {
42 | long fullRange = (long) (Math.pow(2, range) - 1L);
43 | long split = this.value & fullRange;
44 | long out = (this.value >> range) | (split << (32 - range));
45 | return new UnsignedInt(out);
46 | }
47 |
48 | public UnsignedInt createRotationLeft(int range) {
49 | long upperBits = 0xFFFFFFFF00000000L;
50 | long leftshift = this.value << range;
51 | long out = (leftshift & MAX) | ((leftshift & upperBits) >> 32);
52 | return new UnsignedInt(out);
53 | }
54 |
55 | public int toInt() {
56 | return (int) this.value;
57 | }
58 |
59 | public long toLong() {
60 | return this.value;
61 | }
62 |
63 | public void XOR(UnsignedInt unsignedInt) {
64 | this.set(this.value ^ unsignedInt.value);
65 |
66 | }
67 |
68 | public int toByteValue() {
69 | return (int) (this.value & 0xFFL);
70 | }
71 |
72 | public UnsignedInt createShiftLeft(int i) {
73 | return new UnsignedInt(this.value << i);
74 | }
75 |
76 | public UnsignedInt copy() {
77 | return new UnsignedInt(this.value);
78 | }
79 |
80 | public UnsignedChar getFirstByteOfValue() {
81 | return new UnsignedChar((int) (this.value & 0xFFL));
82 | }
83 |
84 | public UnsignedChar getSecondByteOfValue() {
85 | return new UnsignedChar((int) ((this.value >> 8) & 0xFFL));
86 | }
87 |
88 | public UnsignedChar getThirdByteOfValue() {
89 | return new UnsignedChar((int) ((this.value >> 16) & 0xFFL));
90 | }
91 |
92 | public UnsignedChar getForthByteOfValue() {
93 | return new UnsignedChar((int) ((this.value >> 24) & 0xFFL));
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/gamespy/encoders/enctype2/UnsignedIntPointer.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.gamespy.encoders.enctype2;
2 |
3 | public class UnsignedIntPointer {
4 |
5 | private UnsignedInt[] array;
6 | private int position;
7 |
8 | public UnsignedIntPointer(UnsignedInt[] dest, long i) {
9 | this.array = dest;
10 | this.position = (int) i;
11 | }
12 |
13 | public UnsignedIntPointer createPointerAtOffset(int len) {
14 | return new UnsignedIntPointer(this.array, this.position + len);
15 | }
16 |
17 | public boolean greaterThan(UnsignedIntPointer tbuffp) {
18 | return this.position > tbuffp.position;
19 | }
20 |
21 | public void movePosition(int i) {
22 | this.position += i;
23 | }
24 |
25 | public void setDataAtOffset(int i, UnsignedInt t1) {
26 | this.array[this.position + i].set(t1);
27 | }
28 |
29 | public UnsignedInt getDataAtOffset(int i) {
30 | return this.array[this.position + i];
31 | }
32 |
33 | public int getPointerPosition() {
34 | return this.position;
35 | }
36 |
37 | public UnsignedInt[] getDataArray() {
38 | return this.array;
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/logger/LogLevel.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.logger;
2 |
3 | public enum LogLevel {
4 |
5 | VERY_LOW(0),
6 | LOW(1),
7 | NORMAL(2),
8 | HIGH(3),
9 | VERY_HIGH(4);
10 |
11 | private int value;
12 |
13 | private LogLevel(int value) {
14 | this.value = value;
15 | }
16 |
17 | public int getValue() {
18 | return this.value;
19 | }
20 |
21 | public static LogLevel getMaxLevel() {
22 | return VERY_HIGH;
23 | }
24 |
25 | public static LogLevel valueToLevel(int value) {
26 | for(LogLevel l: values()) {
27 | if(l.getValue() == value) { return l; }
28 | }
29 | return null;
30 | }
31 |
32 | public boolean doesPass(LogLevel filter) {
33 | return this.getValue() <= filter.getValue();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/logger/Logger.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.logger;
2 |
3 | import java.io.BufferedWriter;
4 | import java.io.ByteArrayOutputStream;
5 | import java.io.File;
6 | import java.io.FileWriter;
7 | import java.io.IOException;
8 | import java.io.PrintWriter;
9 | import java.text.SimpleDateFormat;
10 | import java.util.Date;
11 |
12 | import de.poweruser.powerserver.main.parser.ParserException;
13 |
14 | public class Logger {
15 |
16 | private File logFile;
17 | private SimpleDateFormat dateFormat;
18 | private static Logger instance;
19 | public static boolean guiInUse;
20 | private LogLevel logLevel;
21 |
22 | public Logger(File logFile) throws IOException {
23 | guiInUse = false;
24 | instance = this;
25 | this.logLevel = LogLevel.NORMAL;
26 | this.logFile = logFile;
27 | if(!this.logFile.exists()) {
28 | this.logFile.createNewFile();
29 | }
30 | if(!this.logFile.canWrite()) { throw new IOException("Unable to write to the log file " + this.logFile.getAbsolutePath()); }
31 | this.dateFormat = new SimpleDateFormat("[dd.MM.yy-HH:mm:ss]");
32 | }
33 |
34 | public static void log(LogLevel level, ParserException exception) {
35 | StringBuilder sb = new StringBuilder();
36 | sb.append(exception.getErrorMessage());
37 | sb.append(" For Game \"");
38 | sb.append(exception.getGameName());
39 | sb.append("\" with received data:\n");
40 | sb.append(exception.getData());
41 | logStatic(level, sb.toString());
42 | }
43 |
44 | public static void logStackTraceStatic(LogLevel level, String message, Exception e, boolean printToSystemOut) {
45 | ByteArrayOutputStream ba = new ByteArrayOutputStream();
46 | PrintWriter pw = new PrintWriter(ba);
47 | pw.println(message);
48 | e.printStackTrace(pw);
49 | pw.close();
50 | logStatic(level, ba.toString(), printToSystemOut);
51 | }
52 |
53 | public static void logStackTraceStatic(LogLevel level, String message, Exception e) {
54 | Logger.logStackTraceStatic(level, message, e, false);
55 | }
56 |
57 | public void log(LogLevel level, String message, boolean printToSystemOut) {
58 | if(level.doesPass(this.getLogLevel())) {
59 | this.writeToFile(message, printToSystemOut);
60 | }
61 | }
62 |
63 | public void log(LogLevel level, String message) {
64 | this.log(level, message, false);
65 | }
66 |
67 | public static void logStatic(LogLevel level, String message, boolean printToSystemOut) {
68 | if(instance != null && level.doesPass(instance.getLogLevel())) {
69 | instance.writeToFile(message, printToSystemOut);
70 | }
71 | }
72 |
73 | public static void logStatic(LogLevel level, String message) {
74 | Logger.logStatic(level, message, false);
75 | }
76 |
77 | private synchronized void writeToFile(String message, boolean printToSystemOut) {
78 | String output = this.currentTimeString() + " " + message;
79 | if(guiInUse || printToSystemOut) {
80 | System.out.println(output);
81 | }
82 | BufferedWriter bw = null;
83 | try {
84 | bw = new BufferedWriter(new FileWriter(this.logFile, true));
85 | bw.newLine();
86 | bw.write(output);
87 | bw.flush();
88 | } catch(IOException e) {}
89 | if(bw != null) {
90 | try {
91 | bw.close();
92 | } catch(IOException e) {}
93 | }
94 | }
95 |
96 | private String currentTimeString() {
97 | return this.dateFormat.format(new Date());
98 | }
99 |
100 | public void setLogLevel(LogLevel level) {
101 | this.logLevel = level;
102 | }
103 |
104 | public static void setLogLevel(int value) {
105 | if(instance != null) {
106 | LogLevel l = LogLevel.valueToLevel(value);
107 | if(l != null) {
108 | instance.setLogLevel(l);
109 | }
110 | }
111 | }
112 |
113 | public LogLevel getLogLevel() {
114 | return this.logLevel;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/main/CombineableInterface.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.main;
2 |
3 | public interface CombineableInterface {
4 |
5 | public T combine(T combineable);
6 |
7 | public void update(T combineable);
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/main/ConsoleReader.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.main;
2 |
3 | import java.io.BufferedReader;
4 | import java.io.IOException;
5 | import java.io.InputStream;
6 | import java.io.InputStreamReader;
7 |
8 | public class ConsoleReader implements Runnable {
9 |
10 | private PowerServer server;
11 | private boolean running;
12 | private Thread thread;
13 | private BufferedReader reader;
14 | private Object waitObject;
15 |
16 | public ConsoleReader(PowerServer server) {
17 | this.server = server;
18 | this.running = true;
19 | this.waitObject = new Object();
20 | this.thread = new Thread(this);
21 | this.thread.setName("PowerServer - ConsoleReader");
22 | this.thread.start();
23 | }
24 |
25 | @Override
26 | public void run() {
27 | InputStream input = System.in;
28 | InputStreamReader isr = new InputStreamReader(System.in);
29 | this.reader = new BufferedReader(isr);
30 |
31 | while(this.running) {
32 | String line = null;
33 | try {
34 | if(input.available() > 0) {
35 | line = this.reader.readLine();
36 | if(line != null) {
37 | this.server.queueCommand(line);
38 | }
39 | } else {
40 | synchronized(this.waitObject) {
41 | try {
42 | this.waitObject.wait(1000L);
43 | } catch(InterruptedException e) {}
44 | }
45 | }
46 | } catch(IOException e) {}
47 | }
48 | try {
49 | this.reader.close();
50 | } catch(IOException e) {}
51 | }
52 |
53 | public void shutdown() {
54 | this.running = false;
55 | synchronized(this.waitObject) {
56 | this.waitObject.notifyAll();
57 | }
58 | try {
59 | this.reader.close();
60 | } catch(IOException e) {}
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/main/Main.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.main;
2 |
3 | import java.io.File;
4 | import java.io.IOException;
5 |
6 | import de.poweruser.powerserver.logger.LogLevel;
7 | import de.poweruser.powerserver.logger.Logger;
8 | import de.poweruser.powerserver.main.gui.MainWindow;
9 | import de.poweruser.powerserver.main.security.SecurityAndBanManager;
10 |
11 | public class Main {
12 |
13 | public static void main(String[] args) throws IOException {
14 | boolean gui = true;
15 | for(String s: args) {
16 | if(s.equalsIgnoreCase("-nogui")) {
17 | gui = false;
18 | }
19 | }
20 | File logFile = new File("server.log");
21 | Logger logger = new Logger(logFile);
22 |
23 | MainWindow m = null;
24 | if(gui) {
25 | m = new MainWindow();
26 | m.setVisible(true);
27 | } else {
28 | logger.log(LogLevel.VERY_LOW, "Operating in console mode. Check the log file " + logFile.getAbsolutePath() + " for a more detailed log", true);
29 | }
30 | File policyFile = new File("PowerServer.policy");
31 | SecurityAndBanManager secManager = new SecurityAndBanManager();
32 | if(policyFile.exists()) {
33 | System.setProperty("java.security.policy", policyFile.getName());
34 | System.setSecurityManager(secManager);
35 | Logger.logStatic(LogLevel.VERY_LOW, "SecurityManager enabled.");
36 | } else {
37 | Logger.logStatic(LogLevel.VERY_LOW, "SecurityManager not enabled. The policy file \"PowerServer.policy\" is missing.");
38 | }
39 | PowerServer server = null;
40 | ConsoleReader reader = null;
41 | secManager.loadBanListFromFile();
42 | logger.log(LogLevel.VERY_LOW, "Starting the master server ...", true);
43 | try {
44 | server = new PowerServer(secManager);
45 | if(m != null) {
46 | m.setModel(server);
47 | } else {
48 | reader = new ConsoleReader(server);
49 | }
50 | } catch(IOException e) {
51 | Logger.logStackTraceStatic(LogLevel.VERY_LOW, "Failed to set up the server: " + e.toString(), e, true);
52 | }
53 | boolean crash = false;
54 | if(server != null) {
55 | try {
56 | server.mainloop();
57 | logger.log(LogLevel.VERY_LOW, "Shutting down the master server ...", true);
58 | } catch(Exception e) {
59 | crash = true;
60 | Logger.logStackTraceStatic(LogLevel.VERY_LOW, "The server quit unexpectedly with an exception of type: " + e.toString(), e, true);
61 | }
62 | server.shutdown();
63 | }
64 | if(m != null && !crash) {
65 | m.shutdown();
66 | }
67 | if(reader != null) {
68 | reader.shutdown();
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/main/MessageData.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.main;
2 |
3 | import java.net.InetAddress;
4 | import java.net.InetSocketAddress;
5 | import java.util.HashMap;
6 |
7 | import de.poweruser.powerserver.games.DataKeysInterface;
8 | import de.poweruser.powerserver.games.GameBase;
9 | import de.poweruser.powerserver.games.GeneralDataKeysEnum;
10 | import de.poweruser.powerserver.main.parser.dataverification.IPAddressVerify;
11 | import de.poweruser.powerserver.main.parser.dataverification.IntVerify;
12 | import de.poweruser.powerserver.main.parser.dataverification.QueryIdFormatVerify;
13 |
14 | public class MessageData implements CombineableInterface {
15 |
16 | /**
17 | * The key-value mapping of the stored data. The keys of this map are the
18 | * data keys in their String representation that
19 | * DataKeysInterface.getKeyString() returns. The values of this map are the
20 | * corresponding data values as String
21 | */
22 | private HashMap map;
23 |
24 | /**
25 | * A constructor which initializes a empty mapping
26 | */
27 |
28 | public MessageData() {
29 | this.map = new HashMap();
30 | }
31 |
32 | /**
33 | * A constructor which initializes the key-value mapping of this MessageData
34 | * with an passed mapping
35 | *
36 | * @param map
37 | * The mapping that shall be assigned to this MessageData
38 | */
39 |
40 | public MessageData(HashMap map) {
41 | this.map = map;
42 | }
43 |
44 | /**
45 | * Checks if the mapping of this MessageData contains the passed key
46 | *
47 | * @param key
48 | * A data key that implements the DataKeysInterface
49 | * @return true if the mapping contains the key, otherwise false
50 | */
51 |
52 | public boolean containsKey(DataKeysInterface key) {
53 | return this.map.containsKey(key.getKeyString());
54 | }
55 |
56 | /**
57 | * Returns the data that is assigned to the passed data key within the
58 | * mapping of MessageData. If the mapping does not contain this key, the
59 | * returned value is the one that the mapping returns in this case, which
60 | * should be null.
61 | *
62 | * @param key
63 | * A data key that implements the DataKeysInterface
64 | * @return the data as String that is assigned to the passed data key. If
65 | * the mapping does not contain the key, null
66 | */
67 |
68 | public String getData(DataKeysInterface key) {
69 | return this.map.get(key.getKeyString());
70 | }
71 |
72 | /**
73 | * Checks if this MessageData represents the data of a received heart-beat.
74 | * More specifically it checks if the mapping of this MessageData contains
75 | * the data key that represents the key for a heart-beat.
76 | *
77 | * The assigned value to the heart-beat data key is verified as well. If
78 | * it does not pass the checks, this method returns false
79 | *
80 | * @return true, if mapping contains a valid heart-beat data key and value.
81 | * Otherwise false
82 | */
83 |
84 | public boolean isHeartBeat() {
85 | return this.checkKeyAndValue(GeneralDataKeysEnum.HEARTBEAT);
86 | }
87 |
88 | /**
89 | * Checks if this MessageData represents the data of a received heart-beat
90 | * broadcast. More specifically it checks if the mapping of this MessageData
91 | * contains a valid data key that represents the key for a heart-beat
92 | * broadcast and the second required key in a heart-beat broadcast, a valid
93 | * host address key.
94 | * The assigned values to the data keys are verified as well. If they do not
95 | * pass the checks, this method returns false
96 | *
97 | * @return true, if the mapping contains the valid heart-beat broadcast and
98 | * valid host data key and value.
99 | * Otherwise false
100 | */
101 |
102 | public boolean isHeartBeatBroadcast() {
103 | return this.checkKeyAndValue(GeneralDataKeysEnum.HEARTBEATBROADCAST) && this.checkKeyAndValue(GeneralDataKeysEnum.HOST);
104 | }
105 |
106 | /**
107 | * Checks if this MessageData represents the data contains a valid
108 | * statechanged data key flag. This flag usually is only part of heart-beats
109 | * and heart-beat broadcasts, but it is not guaranteed. So check if this
110 | * MessageData represents a heart-beat, a heart-beat broadcast or something
111 | * else first.
112 | * The assigned value to the statechanged data key is verified as well. If
113 | * it does not pass the checks, this method returns false
114 | *
115 | * @return true, if the mapping contains a valid statechanged data key and
116 | * value. Otherwise false
117 | */
118 |
119 | public boolean hasStateChanged() {
120 | return this.checkKeyAndValue(GeneralDataKeysEnum.STATECHANGED);
121 | }
122 |
123 | /**
124 | * Checks if this MessageData represents a query answer. More specifically
125 | * this method checks if the mapping of this MessageData contains a valid
126 | * queryid data key.
127 | * The assigned value to the statechanged data key is verified as well. If
128 | * it does not pass the checks, this method returns false
129 | *
130 | * @return true, if the mapping contains a valid queryid data key and value.
131 | * Otherwise false
132 | */
133 |
134 | public boolean isQueryAnswer() {
135 | return this.checkKeyAndValue(GeneralDataKeysEnum.QUERYID);
136 | }
137 |
138 | private boolean checkKeyAndValue(GeneralDataKeysEnum key) {
139 | if(this.containsKey(key)) { return key.getVerifierCopy().verify(this.getData(key)); }
140 | return false;
141 | }
142 |
143 | /**
144 | * This method constructs the game server's query address from this
145 | * MessageData, that is used for sending queries to and receiving query
146 | * answers from. Depending on the type of data that is represented by this
147 | * MessageData (heart-beat / heart-beat broadcast / query answer) the query
148 | * port is stored in different locations.
149 | *
150 | * For a heart-beat:
151 | * game server address: the address the message was sent from
152 | * game server query port: the value of the data key HEARTBEAT
153 | *
154 | * For a heart-beat broadcast:
155 | * game server address: the value of the data key HOST
156 | * game server query port: the value of the data key HEARTBEATBROADCAST
157 | *
158 | * For a query answer:
159 | * The game servers address and query port is the senders socket address
160 | * that the message was received from
161 | *
162 | * @param sender
163 | * The sender's InetSocketAddress that has sent this message data
164 | * @return The InetSocketAddress that targets the game server's query port
165 | */
166 |
167 | public InetSocketAddress constructQuerySocketAddress(InetSocketAddress sender) {
168 | InetAddress server = null;
169 | int queryPort = 0;
170 | IntVerify intVerifier = new IntVerify(1024, 65535);
171 | if(this.isHeartBeat()) {
172 | server = sender.getAddress();
173 | if(intVerifier.verify(this.getData(GeneralDataKeysEnum.HEARTBEAT))) {
174 | queryPort = intVerifier.getVerifiedValue();
175 | }
176 | } else if(this.isHeartBeatBroadcast()) {
177 | IPAddressVerify verifier = new IPAddressVerify();
178 | if(verifier.verify(this.getData(GeneralDataKeysEnum.HOST))) {
179 | server = verifier.getVerifiedAddress();
180 | }
181 | if(intVerifier.verify(this.getData(GeneralDataKeysEnum.HEARTBEATBROADCAST))) {
182 | queryPort = intVerifier.getVerifiedValue();
183 | }
184 | } else if(this.isQueryAnswer()) { return sender; }
185 | if(server != null && queryPort != 0) { return new InetSocketAddress(server, queryPort); }
186 | return null;
187 | }
188 |
189 | public GameBase getGame() {
190 | return GameBase.getGameForGameName(this.getData(GeneralDataKeysEnum.GAMENAME));
191 | }
192 |
193 | @Override
194 | public MessageData combine(MessageData combineable) {
195 | MessageData combination = new MessageData();
196 | combination.map.putAll(combineable.map);
197 | combination.map.putAll(this.map);
198 | return combination;
199 | }
200 |
201 | @Override
202 | public void update(MessageData combineable) {
203 | this.map.putAll(combineable.map);
204 | }
205 |
206 | public QueryInfo getQueryInfo() {
207 | if(this.isQueryAnswer()) {
208 | QueryIdFormatVerify verifier = new QueryIdFormatVerify();
209 | QueryInfo info = null;
210 | if(verifier.verify(this.getData(GeneralDataKeysEnum.QUERYID))) {
211 | info = verifier.getVerifiedQueryInfo();
212 | if(this.containsKey(GeneralDataKeysEnum.FINAL)) {
213 | info.setFinal();
214 | }
215 | return info;
216 | }
217 | }
218 | return null;
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/main/QueryInfo.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.main;
2 |
3 | public class QueryInfo {
4 | private int id;
5 | private int part;
6 | private boolean isFinal;
7 |
8 | public QueryInfo(int id, int part) {
9 | this.id = id;
10 | this.part = part;
11 | this.isFinal = false;
12 | }
13 |
14 | public int getId() {
15 | return this.id;
16 | }
17 |
18 | public int getPart() {
19 | return this.part;
20 | }
21 |
22 | public boolean isFinal() {
23 | return this.isFinal;
24 | }
25 |
26 | public void setFinal() {
27 | this.isFinal = true;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/main/ServerHost.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.main;
2 |
3 | import java.net.InetAddress;
4 | import java.net.InetSocketAddress;
5 | import java.util.ArrayList;
6 | import java.util.HashMap;
7 | import java.util.Iterator;
8 | import java.util.List;
9 | import java.util.Map;
10 | import java.util.Map.Entry;
11 | import java.util.concurrent.TimeUnit;
12 |
13 | import de.poweruser.powerserver.exceptions.LocalServerHostException;
14 | import de.poweruser.powerserver.games.GameBase;
15 | import de.poweruser.powerserver.games.GameServerBase;
16 | import de.poweruser.powerserver.games.GameServerInterface;
17 | import de.poweruser.powerserver.logger.LogLevel;
18 | import de.poweruser.powerserver.logger.Logger;
19 | import de.poweruser.powerserver.settings.Settings;
20 |
21 | public class ServerHost {
22 |
23 | private InetAddress hostAddress;
24 | private Map serverList;
25 | private GameBase gameBase;
26 |
27 | public ServerHost(InetAddress hostAddress, GameBase gameBase) throws LocalServerHostException {
28 | this.hostAddress = hostAddress;
29 | if(hostAddress.isLoopbackAddress() || hostAddress.isSiteLocalAddress() || hostAddress.isAnyLocalAddress() || hostAddress.isLinkLocalAddress()) { throw new LocalServerHostException("Local addresses are not permitted for servers."); }
30 | this.gameBase = gameBase;
31 | this.serverList = new HashMap();
32 | }
33 |
34 | protected GameServerInterface getServerOnPort(int port) {
35 | if(this.serverList.containsKey(port)) { return this.serverList.get(port); }
36 | return null;
37 | }
38 |
39 | public int getServerCount() {
40 | return this.serverList.size();
41 | }
42 |
43 | protected GameServerInterface getOrCreateServer(int port) {
44 | if(this.serverList.containsKey(port)) {
45 | return this.serverList.get(port);
46 | } else {
47 | GameServerInterface server = this.gameBase.createNewServer(new InetSocketAddress(this.hostAddress, port));
48 | this.serverList.put(port, server);
49 | return server;
50 | }
51 | }
52 |
53 | protected List checkForServersToQueryAndOutdatedServers(Settings settings) {
54 | List serversToQuery = null;
55 | Iterator> iter = this.serverList.entrySet().iterator();
56 | while(iter.hasNext()) {
57 | GameServerInterface gsi = iter.next().getValue();
58 | InetSocketAddress socketAddress = gsi.getSocketAddress();
59 | if(!gsi.checkLastHeartbeat(settings.getAllowedHeartbeatTimeout(TimeUnit.MINUTES), TimeUnit.MINUTES)) {
60 | if(!gsi.checkLastMessage(settings.getMaximumServerTimeout(TimeUnit.MINUTES), TimeUnit.MINUTES)) {
61 | iter.remove();
62 | Logger.logStatic(LogLevel.NORMAL, "Removed server " + socketAddress.toString() + " (" + gsi.getServerName() + ") of game " + ((GameServerBase) gsi).getDisplayName() + ". Timeout reached.");
63 | } else if(!gsi.checkLastQueryRequest(settings.getEmergencyQueryInterval(TimeUnit.MINUTES), TimeUnit.MINUTES)) {
64 | if(serversToQuery == null) {
65 | serversToQuery = new ArrayList();
66 | }
67 | serversToQuery.add(socketAddress);
68 | String logMessage = "The server " + socketAddress.toString();
69 | String serverName = gsi.getServerName();
70 | if(serverName != null) {
71 | logMessage += " (" + serverName + ")";
72 | }
73 | logMessage += " does not send heartbeats anymore, or they dont reach this server. Sending back a query instead.";
74 | Logger.logStatic(LogLevel.VERY_HIGH, logMessage);
75 | }
76 | }
77 | }
78 | return serversToQuery;
79 | }
80 |
81 | protected List getActiveServers(Settings settings) {
82 | List activeServers = null;
83 | Iterator> iter = this.serverList.entrySet().iterator();
84 | while(iter.hasNext()) {
85 | GameServerInterface gsi = iter.next().getValue();
86 | if(gsi.checkLastMessage(settings.getMaximumServerTimeout(TimeUnit.MINUTES), TimeUnit.MINUTES)) {
87 | if(gsi.hasAnsweredToQuery()) {
88 | if(activeServers == null) {
89 | activeServers = new ArrayList();
90 | }
91 | activeServers.add(gsi.getSocketAddress());
92 | }
93 | }
94 | }
95 | return activeServers;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/main/ServerList.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.main;
2 |
3 | import java.net.InetAddress;
4 | import java.net.InetSocketAddress;
5 | import java.util.HashMap;
6 | import java.util.Iterator;
7 | import java.util.List;
8 | import java.util.Map;
9 | import java.util.Map.Entry;
10 | import java.util.concurrent.TimeUnit;
11 |
12 | import de.poweruser.powerserver.exceptions.LocalServerHostException;
13 | import de.poweruser.powerserver.exceptions.TooManyServersPerHostException;
14 | import de.poweruser.powerserver.games.GameBase;
15 | import de.poweruser.powerserver.games.GameServerInterface;
16 | import de.poweruser.powerserver.games.GeneralDataKeysEnum;
17 | import de.poweruser.powerserver.logger.LogLevel;
18 | import de.poweruser.powerserver.logger.Logger;
19 | import de.poweruser.powerserver.main.security.BanManager;
20 | import de.poweruser.powerserver.network.UDPSender;
21 | import de.poweruser.powerserver.settings.Settings;
22 |
23 | public class ServerList {
24 |
25 | private GameBase game;
26 | private Map servers;
27 |
28 | public ServerList(GameBase game) {
29 | this.game = game;
30 | this.servers = new HashMap();
31 | }
32 |
33 | public boolean incomingHeartBeat(InetSocketAddress serverAddress, MessageData data, boolean manuallyAdded) throws TooManyServersPerHostException, LocalServerHostException {
34 | if(serverAddress != null) {
35 | GameServerInterface server = this.getOrCreateServer(serverAddress);
36 | String serverName = server.getServerName();
37 | String logMessage = "Received a heartbeat from the server " + serverAddress.toString();
38 | if(serverName != null) {
39 | logMessage += (" ( " + serverName + " )");
40 | }
41 | Logger.logStatic(LogLevel.VERY_HIGH, logMessage);
42 | return server.incomingHeartbeat(serverAddress, data, manuallyAdded);
43 | }
44 | return false;
45 | }
46 |
47 | public boolean incomingHeartBeatBroadcast(InetSocketAddress serverAddress, MessageData data) throws TooManyServersPerHostException, LocalServerHostException {
48 | if(data.containsKey(GeneralDataKeysEnum.HEARTBEATBROADCAST) && data.containsKey(GeneralDataKeysEnum.HOST)) {
49 | GameServerInterface server = this.getOrCreateServer(serverAddress);
50 | if(server.isBroadcastedServer()) {
51 | String serverName = server.getServerName();
52 | String logMessage = "Received a heartbeat broadcast for the server " + serverAddress.toString();
53 | if(serverName != null) {
54 | logMessage += (" ( " + serverName + " )");
55 | }
56 | Logger.logStatic(LogLevel.VERY_HIGH, logMessage);
57 | return server.incomingHeartBeatBroadcast(serverAddress, data);
58 | }
59 | } else {
60 | Logger.logStatic(LogLevel.HIGH, "Got a heartbeatbroadcast, that is missing the host key");
61 | }
62 | return false;
63 | }
64 |
65 | public boolean incomingQueryAnswer(InetSocketAddress sender, MessageData data) {
66 | GameServerInterface server = this.getServer(sender);
67 | if(server != null) { return server.incomingQueryAnswer(sender, data); }
68 | return false;
69 | }
70 |
71 | public boolean hasServer(InetSocketAddress server) {
72 | return this.getServer(server) != null;
73 | }
74 |
75 | private GameServerInterface getServer(InetSocketAddress server) {
76 | if(this.servers.containsKey(server.getAddress())) { return this.servers.get(server.getAddress()).getServerOnPort(server.getPort()); }
77 | return null;
78 | }
79 |
80 | public boolean isBroadcastedServer(InetSocketAddress server) {
81 | GameServerInterface gameServer = this.getServer(server);
82 | if(gameServer != null) { return gameServer.isBroadcastedServer(); }
83 | return false;
84 | }
85 |
86 | private GameServerInterface getOrCreateServer(InetSocketAddress server) throws TooManyServersPerHostException, LocalServerHostException {
87 | if(this.hasServer(server)) {
88 | return this.getServer(server);
89 | } else {
90 | ServerHost host = null;
91 | InetAddress hostAddress = server.getAddress();
92 | if(this.servers.containsKey(hostAddress)) {
93 | host = this.servers.get(hostAddress);
94 | if(host.getServerCount() >= this.game.getSettings().getMaximumServersPerHost()) { throw new TooManyServersPerHostException(hostAddress); }
95 | } else {
96 | host = new ServerHost(hostAddress, this.game);
97 | this.servers.put(hostAddress, host);
98 | }
99 | return host.getOrCreateServer(server.getPort());
100 | }
101 | }
102 |
103 | public List checkForServersToQueryAndOutdatedServers(Settings settings) {
104 | List serversToQuery = null;
105 | Iterator> iter = this.servers.entrySet().iterator();
106 | while(iter.hasNext()) {
107 | Entry entry = iter.next();
108 | ServerHost host = entry.getValue();
109 | List list = host.checkForServersToQueryAndOutdatedServers(settings);
110 | if(list != null) {
111 | if(serversToQuery != null) {
112 | serversToQuery.addAll(list);
113 | } else {
114 | serversToQuery = list;
115 | }
116 | } else if(host.getServerCount() == 0) {
117 | iter.remove();
118 | }
119 | }
120 | return serversToQuery;
121 | }
122 |
123 | public List getActiveServers(Settings settings) {
124 | List list = null;
125 | Iterator> iter = this.servers.entrySet().iterator();
126 | while(iter.hasNext()) {
127 | Entry entry = iter.next();
128 | ServerHost host = entry.getValue();
129 | List hostList = host.getActiveServers(settings);
130 | if(hostList != null) {
131 | if(list == null) {
132 | list = hostList;
133 | } else {
134 | list.addAll(hostList);
135 | }
136 | }
137 | }
138 | return list;
139 | }
140 |
141 | public void queryServer(InetSocketAddress server, UDPSender udpSender, boolean queryPlayers) {
142 | GameServerInterface gsi = this.getServer(server);
143 | if(gsi != null) {
144 | gsi.markQueryRequestAsSentWithCurrentTime();
145 | udpSender.queueQuery(server, this.game.createStatusQuery(queryPlayers));
146 | }
147 | }
148 |
149 | public void blockHost(InetAddress host, BanManager banManager, TimeUnit timeUnit, long tempBanDuration) {
150 | if(banManager.addTempBanByDuration(host, tempBanDuration, timeUnit)) {
151 | Logger.logStatic(LogLevel.NORMAL, "Temporary ban of " + String.valueOf(tempBanDuration) + " " + timeUnit.toString().toLowerCase() + " for " + host.toString() + ". This host exceeds the 'servers per host' limit");
152 | }
153 | if(this.servers.containsKey(host)) {
154 | this.servers.remove(host);
155 | }
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/main/gui/MainWindow.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.main.gui;
2 |
3 | import java.awt.BorderLayout;
4 | import java.awt.Dimension;
5 | import java.awt.event.KeyEvent;
6 | import java.awt.event.KeyListener;
7 | import java.awt.event.MouseAdapter;
8 | import java.awt.event.MouseEvent;
9 | import java.awt.event.WindowEvent;
10 | import java.io.PrintStream;
11 | import java.util.ArrayList;
12 |
13 | import javax.swing.JFrame;
14 | import javax.swing.JMenuItem;
15 | import javax.swing.JOptionPane;
16 | import javax.swing.JPopupMenu;
17 | import javax.swing.JScrollPane;
18 | import javax.swing.JTextArea;
19 | import javax.swing.JTextField;
20 | import javax.swing.ScrollPaneConstants;
21 | import javax.swing.text.DefaultCaret;
22 | import javax.swing.text.DefaultEditorKit;
23 |
24 | import de.poweruser.powerserver.logger.Logger;
25 | import de.poweruser.powerserver.main.PowerServer;
26 |
27 | public class MainWindow extends JFrame {
28 |
29 | private static final long serialVersionUID = 2846198182943968671L;
30 |
31 | private PowerServer server;
32 | private boolean alreadyShuttingDown;
33 | private ArrayList commandHistory;
34 | private CommandHistoryPos selectedCommandIndex;
35 | private final int maxHistorySize;
36 |
37 | public MainWindow() {
38 | this.server = null;
39 | this.alreadyShuttingDown = false;
40 | this.commandHistory = new ArrayList();
41 | this.selectedCommandIndex = CommandHistoryPos.POSITION;
42 | this.maxHistorySize = 20;
43 |
44 | this.addWindowListener(new java.awt.event.WindowAdapter() {
45 | @Override
46 | public void windowClosing(java.awt.event.WindowEvent windowEvent) {
47 | if(!MainWindow.this.alreadyShuttingDown) {
48 | if(JOptionPane.showConfirmDialog(MainWindow.this, "Do you really want to stop the master server?", "Really Closing?", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE) == JOptionPane.YES_OPTION) {
49 | if(MainWindow.this.server != null) {
50 | MainWindow.this.server.shutdown();
51 | }
52 | MainWindow.this.shutdown();
53 | }
54 | }
55 | }
56 | });
57 | this.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
58 | this.setLayout(new BorderLayout());
59 | final JTextArea textArea = new JTextArea();
60 | JScrollPane pane = new JScrollPane(textArea);
61 | pane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
62 | pane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
63 | textArea.setEditable(false);
64 | textArea.setLineWrap(true);
65 | textArea.addMouseListener(new MouseAdapter() {
66 | @Override
67 | public void mouseReleased(final MouseEvent e) {
68 | if(e.isPopupTrigger()) {
69 | final JPopupMenu menu = new JPopupMenu();
70 | JMenuItem copyItem = new JMenuItem(textArea.getActionMap().get(DefaultEditorKit.copyAction));
71 | copyItem.setText("Copy");
72 | copyItem.setEnabled(textArea.getSelectionStart() != textArea.getSelectionEnd());
73 | menu.add(copyItem);
74 | JMenuItem selectAllItem = new JMenuItem(textArea.getActionMap().get(DefaultEditorKit.selectAllAction));
75 | selectAllItem.setText("Select all");
76 | menu.add(selectAllItem);
77 | menu.show(e.getComponent(), e.getX(), e.getY());
78 | }
79 | }
80 | });
81 |
82 | this.add(pane, BorderLayout.CENTER);
83 | JTextField textField = new JTextField();
84 | textField.addKeyListener(new KeyListener() {
85 |
86 | @Override
87 | public void keyReleased(KeyEvent event) {}
88 |
89 | @Override
90 | public void keyPressed(KeyEvent event) {
91 | if(event.getKeyCode() == KeyEvent.VK_ENTER) {
92 | JTextField source = (JTextField) event.getSource();
93 | String line = source.getText();
94 | if(!line.trim().isEmpty()) {
95 | source.setText("");
96 | textArea.setCaretPosition(textArea.getDocument().getLength());
97 | MainWindow.this.addCommandToHistory(line);
98 | MainWindow.this.server.queueCommand(line);
99 | }
100 | } else if(event.getKeyCode() == KeyEvent.VK_UP) {
101 | String previous = MainWindow.this.selectPreviousCommand();
102 | if(previous != null) {
103 | JTextField source = (JTextField) event.getSource();
104 | source.setText(previous);
105 | }
106 | } else if(event.getKeyCode() == KeyEvent.VK_DOWN) {
107 | String next = MainWindow.this.selectNextCommand();
108 | if(next != null) {
109 | JTextField source = (JTextField) event.getSource();
110 | source.setText(next);
111 | }
112 | } else if(event.getKeyCode() == KeyEvent.VK_ESCAPE) {
113 | JTextField source = (JTextField) event.getSource();
114 | source.setText("");
115 | }
116 | }
117 |
118 | @Override
119 | public void keyTyped(KeyEvent e) {}
120 | });
121 | this.add(textField, BorderLayout.SOUTH);
122 | ((DefaultCaret) textArea.getCaret()).setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
123 | this.setMinimumSize(new Dimension(600, 400));
124 | this.setResizable(true);
125 | this.setLocationRelativeTo(null);
126 | pack();
127 | TextAreaOutputStream taos = new TextAreaOutputStream(textArea);
128 | taos.setVerticalScrollBar(pane.getVerticalScrollBar());
129 | PrintStream ps = new PrintStream(taos);
130 | System.setErr(ps);
131 | System.setOut(ps);
132 | Logger.guiInUse = true;
133 | }
134 |
135 | public void shutdown() {
136 | this.alreadyShuttingDown = true;
137 | this.setDefaultCloseOperation(EXIT_ON_CLOSE);
138 | this.dispatchEvent(new WindowEvent(MainWindow.this, WindowEvent.WINDOW_CLOSING));
139 | }
140 |
141 | public void setModel(PowerServer server) {
142 | this.server = server;
143 | this.setTitle("PowerServer " + server.getVersion());
144 | }
145 |
146 | private void addCommandToHistory(String line) {
147 | if(this.commandHistory.contains(line)) {
148 | this.commandHistory.remove(line);
149 | }
150 | this.commandHistory.add(line);
151 | while(this.commandHistory.size() > this.maxHistorySize) {
152 | this.commandHistory.remove(0);
153 | }
154 | this.selectedCommandIndex.setValue(CommandHistoryPos.END);
155 | }
156 |
157 | private String selectPreviousCommand() {
158 | if(this.selectedCommandIndex.equals(CommandHistoryPos.START)) { return null; }
159 | if(this.selectedCommandIndex.equals(CommandHistoryPos.END) && this.commandHistory.size() > 0) {
160 | this.selectedCommandIndex.setValue(this.commandHistory.size() - 1);
161 | } else if(0 < this.selectedCommandIndex.getValue() && this.selectedCommandIndex.getValue() <= this.commandHistory.size()) {
162 | this.selectedCommandIndex.setValue(this.selectedCommandIndex.getValue() - 1);
163 | } else {
164 | this.selectedCommandIndex.setValue(CommandHistoryPos.START);
165 | return null;
166 | }
167 | return this.commandHistory.get(this.selectedCommandIndex.getValue());
168 | }
169 |
170 | private String selectNextCommand() {
171 | if(this.selectedCommandIndex.equals(CommandHistoryPos.END)) { return null; }
172 | if(this.selectedCommandIndex.equals(CommandHistoryPos.START) && this.commandHistory.size() > 0) {
173 | this.selectedCommandIndex.setValue(0);
174 | } else if(-1 <= this.selectedCommandIndex.getValue() && this.selectedCommandIndex.getValue() < this.commandHistory.size() - 1) {
175 | this.selectedCommandIndex.setValue(this.selectedCommandIndex.getValue() + 1);
176 | } else {
177 | this.selectedCommandIndex.setValue(CommandHistoryPos.END);
178 | return null;
179 | }
180 | return this.commandHistory.get(this.selectedCommandIndex.getValue());
181 | }
182 |
183 | private enum CommandHistoryPos {
184 | START(Integer.MIN_VALUE),
185 | POSITION(Integer.MAX_VALUE),
186 | END(Integer.MAX_VALUE);
187 |
188 | private int value;
189 |
190 | private CommandHistoryPos(int value) {
191 | this.value = value;
192 | }
193 |
194 | public boolean equals(CommandHistoryPos chp) {
195 | return this.value == chp.value;
196 | }
197 |
198 | public void setValue(CommandHistoryPos chp) {
199 | this.setValue(chp.value);
200 | }
201 |
202 | public void setValue(int value) {
203 | if(this == POSITION) {
204 | this.value = value;
205 | }
206 | }
207 |
208 | public int getValue() {
209 | return this.value;
210 | }
211 | }
212 | }
213 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/main/gui/TextAreaOutputStream.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.main.gui;
2 |
3 | import java.awt.EventQueue;
4 | import java.io.OutputStream;
5 | import java.io.UnsupportedEncodingException;
6 | import java.util.ArrayList;
7 | import java.util.LinkedList;
8 | import java.util.List;
9 |
10 | import javax.swing.JScrollBar;
11 | import javax.swing.JTextArea;
12 |
13 | public class TextAreaOutputStream extends OutputStream {
14 |
15 | // *************************************************************************************************
16 | // INSTANCE MEMBERS
17 | // *************************************************************************************************
18 |
19 | private byte[] oneByte; // array for write(int val);
20 | private Appender appender; // most recent action
21 |
22 | public TextAreaOutputStream(JTextArea txtara) {
23 | this(txtara, 1000);
24 | }
25 |
26 | public TextAreaOutputStream(JTextArea txtara, int maxlin) {
27 | if(maxlin < 1) { throw new IllegalArgumentException("TextAreaOutputStream maximum lines must be positive (value=" + maxlin + ")"); }
28 | oneByte = new byte[1];
29 | appender = new Appender(txtara, maxlin);
30 | }
31 |
32 | public void setVerticalScrollBar(JScrollBar verticalScrollBar) {
33 | this.appender.setVerticalScrollBar(verticalScrollBar);
34 | }
35 |
36 | /** Clear the current console text area. */
37 | public synchronized void clear() {
38 | if(appender != null) {
39 | appender.clear();
40 | }
41 | }
42 |
43 | @Override
44 | public synchronized void close() {
45 | appender = null;
46 | }
47 |
48 | @Override
49 | public synchronized void flush() {}
50 |
51 | @Override
52 | public synchronized void write(int val) {
53 | oneByte[0] = (byte) val;
54 | write(oneByte, 0, 1);
55 | }
56 |
57 | @Override
58 | public synchronized void write(byte[] ba) {
59 | write(ba, 0, ba.length);
60 | }
61 |
62 | @Override
63 | public synchronized void write(byte[] ba, int str, int len) {
64 | if(appender != null) {
65 | appender.append(bytesToString(ba, str, len));
66 | }
67 | }
68 |
69 | static private String bytesToString(byte[] ba, int str, int len) {
70 | try {
71 | return new String(ba, str, len, "UTF-8");
72 | } catch(UnsupportedEncodingException thr) {
73 | return new String(ba, str, len);
74 | } // all JVMs are required to support UTF-8
75 | }
76 |
77 | // *************************************************************************************************
78 | // STATIC MEMBERS
79 | // *************************************************************************************************
80 |
81 | static class Appender implements Runnable {
82 | private final JTextArea textArea;
83 | private JScrollBar vScrollBar;
84 | private final int maxLines; // maximum lines allowed in text area
85 | private final LinkedList lengths; // length of lines within
86 | // text area
87 | private final List values; // values waiting to be appended
88 |
89 | private int curLength; // length of current line
90 | private boolean clear;
91 | private boolean queue;
92 |
93 | private static final String EOL1 = "\n";
94 | private static final String EOL2 = System.getProperty("line.separator", EOL1);
95 |
96 | Appender(JTextArea txtara, int maxlin) {
97 | textArea = txtara;
98 | maxLines = maxlin;
99 | lengths = new LinkedList();
100 | values = new ArrayList();
101 | vScrollBar = null;
102 |
103 | curLength = 0;
104 | clear = false;
105 | queue = true;
106 | }
107 |
108 | synchronized void append(String val) {
109 | values.add(val);
110 | if(queue) {
111 | queue = false;
112 | EventQueue.invokeLater(this);
113 | }
114 | }
115 |
116 | synchronized void clear() {
117 | clear = true;
118 | curLength = 0;
119 | lengths.clear();
120 | values.clear();
121 | if(queue) {
122 | queue = false;
123 | EventQueue.invokeLater(this);
124 | }
125 | }
126 |
127 | // MUST BE THE ONLY METHOD THAT TOUCHES textArea!
128 | @Override
129 | public synchronized void run() {
130 | if(clear) {
131 | textArea.setText("");
132 | }
133 | for(String val: values) {
134 | curLength += val.length();
135 | if(val.endsWith(EOL1) || val.endsWith(EOL2)) {
136 | if(lengths.size() >= maxLines) {
137 | textArea.replaceRange("", 0, lengths.removeFirst());
138 | }
139 | lengths.addLast(curLength);
140 | curLength = 0;
141 | }
142 | boolean scroll = false;
143 | if(vScrollBar != null) {
144 | int currentPosition = vScrollBar.getValue();
145 | int maxPosition = vScrollBar.getMaximum() - vScrollBar.getVisibleAmount();
146 | scroll = (currentPosition >= maxPosition - 200);
147 | }
148 | textArea.append(val);
149 | if(scroll) {
150 | textArea.setCaretPosition(textArea.getDocument().getLength());
151 | }
152 | }
153 | values.clear();
154 | clear = false;
155 | queue = true;
156 | }
157 |
158 | public void setVerticalScrollBar(JScrollBar verticalScrollBar) {
159 | vScrollBar = verticalScrollBar;
160 | }
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/main/parser/DataParserInterface.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.main.parser;
2 |
3 | import de.poweruser.powerserver.games.GameBase;
4 | import de.poweruser.powerserver.main.MessageData;
5 | import de.poweruser.powerserver.network.UDPMessage;
6 |
7 | public interface DataParserInterface {
8 | public MessageData parse(GameBase game, UDPMessage msg) throws ParserException;
9 |
10 | public MessageData parse(GameBase game, String message) throws ParserException;
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/main/parser/GamespyProtocol1Parser.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.main.parser;
2 |
3 | import java.util.HashMap;
4 | import java.util.Map;
5 | import java.util.Map.Entry;
6 |
7 | import de.poweruser.powerserver.games.GameBase;
8 | import de.poweruser.powerserver.games.GeneralDataKeysEnum;
9 | import de.poweruser.powerserver.main.MessageData;
10 | import de.poweruser.powerserver.network.UDPMessage;
11 |
12 | public class GamespyProtocol1Parser implements DataParserInterface {
13 |
14 | private GameBase parsedGame;
15 |
16 | @Override
17 | public MessageData parse(GameBase game, UDPMessage msg) throws ParserException {
18 | return this.parse(game, msg.toString());
19 | }
20 |
21 | @Override
22 | public MessageData parse(GameBase game, String message) throws ParserException {
23 | if(game != null) {
24 | this.parsedGame = game;
25 | }
26 | int start = message.indexOf("\\");
27 | if(start < 0) { throw new ParserException("The message does not contain a backslash \"\\\". Is it a gamespy protocol version 2 message?", message, game); }
28 | message = message.substring(start + 1);
29 | String[] split = message.split("\\\\");
30 | HashMap map = new HashMap();
31 | int i = 0;
32 | for(i = 0; i < split.length - 1; i = i + 2) {
33 | String key = split[i];
34 | if(key.trim().isEmpty()) {
35 | i--;
36 | continue;
37 | }
38 | String value = split[i + 1];
39 | this.processPair(map, key, value, message);
40 | if(this.isKeyFinalKey(key)) {
41 | if(i + 3 < split.length) {
42 | key = split[i + 2];
43 | if(this.isKeyQueryKey(key)) {
44 | value = split[i + 3];
45 | this.processPair(map, key, value, message);
46 | }
47 | }
48 | break;
49 | }
50 | }
51 | if(i == split.length - 1) {
52 | String key = split[i];
53 | if(this.isKeyFinalKey(key)) {
54 | this.processPair(map, key, "", message);
55 | }
56 | }
57 | return new MessageData(map);
58 | }
59 |
60 | private void processPair(Map map, String key, String value, String message) throws ParserException {
61 | if(this.parsedGame == null && this.isGameNameKey(key)) {
62 | GameBase foundGame = GameBase.getGameForGameName(value);
63 | if(foundGame != null) {
64 | this.parsedGame = foundGame;
65 | for(Entry entry: map.entrySet()) {
66 | if(!this.parsedGame.verifyDataKeyAndValue(entry.getKey(), entry.getValue())) { throw new ParserException("Found invalid data key \"" + entry.getKey() + "\" and value \"" + entry.getValue() + "\".", message, this.parsedGame); }
67 | }
68 | } else {
69 | throw new ParserException("The gamename \"" + value + "\" in this message was not recognised.", message, this.parsedGame);
70 | }
71 | }
72 | if(this.parsedGame == null || this.parsedGame.verifyDataKeyAndValue(key, value)) {
73 | map.put(key, value);
74 | } else {
75 | throw new ParserException("Found invalid data key \"" + key + "\" and value \"" + value + "\".", message, this.parsedGame);
76 | }
77 | }
78 |
79 | private boolean isKeyFinalKey(String key) {
80 | return GeneralDataKeysEnum.FINAL.getKeyString().equalsIgnoreCase(key);
81 | }
82 |
83 | private boolean isKeyQueryKey(String key) {
84 | return GeneralDataKeysEnum.QUERYID.getKeyString().equalsIgnoreCase(key);
85 | }
86 |
87 | private boolean isGameNameKey(String key) {
88 | return GeneralDataKeysEnum.GAMENAME.getKeyString().equalsIgnoreCase(key);
89 | }
90 |
91 | public void reset() {
92 | this.parsedGame = null;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/main/parser/ParserException.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.main.parser;
2 |
3 | import de.poweruser.powerserver.games.GameBase;
4 | import de.poweruser.powerserver.network.UDPMessage;
5 |
6 | public class ParserException extends Exception {
7 | private static final long serialVersionUID = 4057281636598893525L;
8 | private String message;
9 | private GameBase game;
10 | private String errorMessage;
11 |
12 | public ParserException(String errorMessage, UDPMessage message, GameBase game) {
13 | this(errorMessage, message.toString(), game);
14 | }
15 |
16 | public ParserException(String errorMessage, String message, GameBase game) {
17 | this.errorMessage = errorMessage;
18 | this.message = message;
19 | this.game = game;
20 | }
21 |
22 | public String getData() {
23 | return this.message;
24 | }
25 |
26 | public String getGameName() {
27 | if(this.game == null) { return "None specified"; }
28 | return this.game.getGameName();
29 | }
30 |
31 | public String getErrorMessage() {
32 | return this.errorMessage;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/main/parser/dataverification/BooleanVerify.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.main.parser.dataverification;
2 |
3 | public class BooleanVerify implements VerificationInterface {
4 |
5 | @Override
6 | public boolean verify(String data) {
7 | String trimmed = data.trim().toLowerCase();
8 | return (trimmed.equalsIgnoreCase("true") || trimmed.equalsIgnoreCase("1") || trimmed.equalsIgnoreCase("yes"));
9 | }
10 |
11 | @Override
12 | public BooleanVerify createCopy() {
13 | return new BooleanVerify();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/main/parser/dataverification/DummyVerify.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.main.parser.dataverification;
2 |
3 | public class DummyVerify implements VerificationInterface {
4 |
5 | @Override
6 | public boolean verify(String data) {
7 | return true;
8 | }
9 |
10 | @Override
11 | public VerificationInterface createCopy() {
12 | return new DummyVerify();
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/main/parser/dataverification/IPAddressVerify.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.main.parser.dataverification;
2 |
3 | import java.net.InetAddress;
4 | import java.net.UnknownHostException;
5 |
6 | public class IPAddressVerify implements VerificationInterface {
7 |
8 | private InetAddress verifiedAddress;
9 |
10 | @Override
11 | public boolean verify(String data) {
12 | this.verifiedAddress = null;
13 | try {
14 | // TODO is this already sufficient enough?
15 | this.verifiedAddress = InetAddress.getByName(data);
16 | return true;
17 | } catch(UnknownHostException e) {
18 | return false;
19 | }
20 | }
21 |
22 | public InetAddress getVerifiedAddress() {
23 | return this.verifiedAddress;
24 | }
25 |
26 | @Override
27 | public IPAddressVerify createCopy() {
28 | return new IPAddressVerify();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/main/parser/dataverification/IntVerify.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.main.parser.dataverification;
2 |
3 | public class IntVerify implements VerificationInterface {
4 |
5 | private final int min;
6 | private final int max;
7 | private int verifiedValue;
8 |
9 | public IntVerify(int min, int max) {
10 | this.min = min;
11 | this.max = max;
12 | this.verifiedValue = 0;
13 | }
14 |
15 | @Override
16 | public boolean verify(String data) {
17 | try {
18 | int i = Integer.parseInt(data);
19 | boolean result = i >= this.min && i <= this.max;
20 | if(result) {
21 | this.verifiedValue = i;
22 | }
23 | return result;
24 | } catch(NumberFormatException e) {
25 | return false;
26 | }
27 | }
28 |
29 | public int getVerifiedValue() {
30 | return this.verifiedValue;
31 | }
32 |
33 | @Override
34 | public IntVerify createCopy() {
35 | return new IntVerify(this.min, this.max);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/main/parser/dataverification/QueryIdFormatVerify.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.main.parser.dataverification;
2 |
3 | import de.poweruser.powerserver.main.QueryInfo;
4 |
5 | public class QueryIdFormatVerify implements VerificationInterface {
6 |
7 | public QueryInfo verifiedQueryInfo;
8 |
9 | @Override
10 | public boolean verify(String data) {
11 | this.verifiedQueryInfo = null;
12 | String[] split = data.split("\\.");
13 | if(split.length == 2) {
14 | boolean ok = true;
15 | int[] values = new int[2];
16 | for(int i = 0; i < 2; i++) {
17 | String str = split[i];
18 | try {
19 | values[i] = Integer.parseInt(str);
20 | ok &= (values[i] >= 1);
21 | } catch(NumberFormatException e) {
22 | return false;
23 | }
24 | }
25 | if(ok) {
26 | this.verifiedQueryInfo = new QueryInfo(values[0], values[1]);
27 | }
28 | return ok;
29 | }
30 | return false;
31 | }
32 |
33 | public QueryInfo getVerifiedQueryInfo() {
34 | return this.verifiedQueryInfo;
35 | }
36 |
37 | @Override
38 | public QueryIdFormatVerify createCopy() {
39 | return new QueryIdFormatVerify();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/main/parser/dataverification/StringLengthVerify.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.main.parser.dataverification;
2 |
3 | public class StringLengthVerify implements VerificationInterface {
4 |
5 | private int min;
6 | private int max;
7 |
8 | public StringLengthVerify(int min, int max) {
9 | this.min = min;
10 | this.max = max;
11 | }
12 |
13 | @Override
14 | public boolean verify(String data) {
15 | return data.length() >= this.min && data.length() <= this.max;
16 | }
17 |
18 | @Override
19 | public StringLengthVerify createCopy() {
20 | return new StringLengthVerify(this.min, this.max);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/main/parser/dataverification/StringVerify.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.main.parser.dataverification;
2 |
3 | public class StringVerify implements VerificationInterface {
4 |
5 | private final String[] options;
6 | private final boolean caseSensitive;
7 |
8 | public StringVerify(String[] options, boolean caseSenstive) {
9 | this.options = options;
10 | this.caseSensitive = caseSenstive;
11 | }
12 |
13 | @Override
14 | public boolean verify(String data) {
15 | for(String str: this.options) {
16 | if(this.caseSensitive && str.equals(data)) {
17 | return true;
18 | } else if(!this.caseSensitive && str.equalsIgnoreCase(data)) { return true; }
19 | }
20 | return false;
21 | }
22 |
23 | @Override
24 | public StringVerify createCopy() {
25 | return new StringVerify(this.options, this.caseSensitive);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/main/parser/dataverification/VerificationInterface.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.main.parser.dataverification;
2 |
3 | public interface VerificationInterface {
4 | public boolean verify(String data);
5 |
6 | public VerificationInterface createCopy();
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/main/security/BanList.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.main.security;
2 |
3 | import java.text.DateFormat;
4 | import java.util.Date;
5 | import java.util.HashMap;
6 | import java.util.Map;
7 | import java.util.concurrent.ConcurrentHashMap;
8 | import java.util.concurrent.TimeUnit;
9 |
10 | public class BanList {
11 | private Map banlist;
12 | private boolean hasChanged;
13 |
14 | public BanList() {
15 | this.banlist = new ConcurrentHashMap(16, 0.75f, 1);
16 | this.hasChanged = false;
17 | }
18 |
19 | public boolean addTempBanByDuration(T item, long duration, TimeUnit unit) {
20 | return this.addTempBanByTimeStamp(item, System.currentTimeMillis() + TimeUnit.MILLISECONDS.convert(duration, unit));
21 | }
22 |
23 | public boolean addTempBanByTimeStamp(T item, long timeStamp) {
24 | boolean alreadyContained = this.banlist.containsKey(item);
25 | this.banlist.put(item, timeStamp);
26 | this.hasChanged = true;
27 | return !alreadyContained;
28 | }
29 |
30 | public boolean addPermBan(T item) {
31 | return this.addTempBanByTimeStamp(item, Long.MAX_VALUE);
32 | }
33 |
34 | public boolean isBanned(T item) {
35 | if(this.banlist.containsKey(item)) {
36 | long unbanTime = this.banlist.get(item).longValue();
37 | if(unbanTime > System.currentTimeMillis()) {
38 | return true;
39 | } else {
40 | this.banlist.remove(item);
41 | this.hasChanged = true;
42 | }
43 | }
44 | return false;
45 | }
46 |
47 | public String getUnbanDate(T item) {
48 | if(this.isBanned(item)) { return DateFormat.getInstance().format(new Date(this.banlist.get(item).longValue())); }
49 | return null;
50 | }
51 |
52 | public boolean hasChanged() {
53 | return this.hasChanged;
54 | }
55 |
56 | public void setUnChanged() {
57 | this.hasChanged = false;
58 | }
59 |
60 | public Map getEntries() {
61 | Map map = new HashMap();
62 | map.putAll(this.banlist);
63 | return map;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/main/security/BanManager.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.main.security;
2 |
3 | import java.util.concurrent.TimeUnit;
4 |
5 | public interface BanManager {
6 |
7 | public boolean isBanned(T item);
8 |
9 | public boolean addTempBanByTimeStamp(T item, long timeStamp);
10 |
11 | public boolean addTempBanByDuration(T item, long duration, TimeUnit unit);
12 |
13 | public boolean addPermBan(T item);
14 |
15 | public boolean saveBanListToFile();
16 |
17 | public String getUnbanDate(T item);
18 |
19 | public boolean hasChanged();
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/main/security/SecurityAndBanManager.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.main.security;
2 |
3 | import java.io.BufferedReader;
4 | import java.io.BufferedWriter;
5 | import java.io.File;
6 | import java.io.FileReader;
7 | import java.io.FileWriter;
8 | import java.io.IOException;
9 | import java.net.InetAddress;
10 | import java.net.UnknownHostException;
11 | import java.util.Map.Entry;
12 | import java.util.concurrent.TimeUnit;
13 |
14 | import de.poweruser.powerserver.logger.LogLevel;
15 | import de.poweruser.powerserver.logger.Logger;
16 |
17 | public class SecurityAndBanManager extends SecurityManager implements BanManager {
18 |
19 | private BanList banList;
20 | private File banListFile;
21 | private String banEntryDelimiter = ";";
22 |
23 | public SecurityAndBanManager() {
24 | super();
25 | this.banList = new BanList();
26 | this.banListFile = new File("banlist.cfg");
27 | }
28 |
29 | public synchronized void loadBanListFromFile() {
30 | if(this.banListFile == null || this.banListFile.isDirectory() || !this.banListFile.exists()) { return; }
31 |
32 | BufferedReader br = null;
33 | int entriesLoaded = 0;
34 | try {
35 | br = new BufferedReader(new FileReader(this.banListFile));
36 | String line = null;
37 | while((line = br.readLine()) != null) {
38 | line = line.trim();
39 | if(line.startsWith("#") || line.isEmpty()) {
40 | continue;
41 | }
42 | String[] split = line.split("\\" + banEntryDelimiter);
43 | InetAddress address = null;
44 | long timeStamp = Long.MAX_VALUE;
45 | for(int i = 0; i < split.length; i++) {
46 | String item = split[i];
47 | switch(i) {
48 | case 0:
49 | try {
50 | address = InetAddress.getByName(item);
51 | } catch(UnknownHostException e) {}
52 | break;
53 | case 1:
54 | try {
55 | timeStamp = Long.parseLong(item);
56 | } catch(NumberFormatException e) {}
57 | break;
58 | default:
59 | break;
60 | }
61 | }
62 | if(address != null) {
63 | if(this.addTempBanByTimeStamp(address, timeStamp)) {
64 | entriesLoaded++;
65 | }
66 | }
67 | }
68 | if(entriesLoaded > 0) {
69 | Logger.logStatic(LogLevel.VERY_LOW, "Ban list loaded. " + String.valueOf(entriesLoaded) + " entries", true);
70 | }
71 | } catch(IOException e) {
72 | Logger.logStatic(LogLevel.VERY_LOW, "Error while loading the ban list from file " + this.banListFile.getName() + ": " + e.toString(), true);
73 | } finally {
74 | if(br != null) {
75 | try {
76 | br.close();
77 | } catch(IOException e) {}
78 | }
79 | }
80 | }
81 |
82 | @Override
83 | public boolean isBanned(InetAddress address) {
84 | return this.banList.isBanned(address);
85 | }
86 |
87 | @Override
88 | public void checkAccept(String host, int port) {
89 | try {
90 | InetAddress address = InetAddress.getByName(host);
91 | if(this.isBanned(address)) { throw new SecurityBanException("The host " + host + " is banned!", address); }
92 | } catch(UnknownHostException e) {
93 | throw new SecurityException("The host " + host + " is unknown and cant be resolved!");
94 | }
95 | }
96 |
97 | static {
98 | java.security.Security.setProperty("networkaddress.cache.ttl", "20");
99 | }
100 |
101 | @Override
102 | public boolean addTempBanByTimeStamp(InetAddress item, long timeStamp) {
103 | return this.banList.addTempBanByTimeStamp(item, timeStamp);
104 | }
105 |
106 | @Override
107 | public synchronized boolean saveBanListToFile() {
108 | this.banList.setUnChanged();
109 | if(!this.banListFile.exists() || !this.banListFile.isFile()) {
110 | try {
111 | this.banListFile.createNewFile();
112 | } catch(IOException e) {
113 | Logger.logStatic(LogLevel.VERY_LOW, "Failed to create a new file " + this.banListFile.getName() + " to store the ban list:" + e.toString(), true);
114 | return false;
115 | }
116 | }
117 | if(this.banListFile.exists() && this.banListFile.isFile() && this.banListFile.canWrite()) {
118 | BufferedWriter writer = null;
119 | try {
120 | writer = new BufferedWriter(new FileWriter(this.banListFile));
121 | for(Entry entry: this.banList.getEntries().entrySet()) {
122 | String line = entry.getKey().getHostAddress() + banEntryDelimiter + entry.getValue().toString();
123 | writer.write(line);
124 | writer.newLine();
125 | }
126 | writer.flush();
127 | } catch(IOException e) {
128 | Logger.logStatic(LogLevel.VERY_LOW, "Error while saving the ban list to file " + this.banListFile.getName() + ": " + e.toString(), true);
129 | } finally {
130 | if(writer != null) {
131 | try {
132 | writer.close();
133 | } catch(IOException e) {}
134 | }
135 | }
136 | return true;
137 | } else {
138 | Logger.logStatic(LogLevel.VERY_LOW, "Failed to save the ban list to file " + this.banListFile.getName() + ". Check the write access permissions.", true);
139 | }
140 | return false;
141 | }
142 |
143 | @Override
144 | public boolean addTempBanByDuration(InetAddress item, long duration, TimeUnit unit) {
145 | return this.banList.addTempBanByDuration(item, duration, unit);
146 | }
147 |
148 | @Override
149 | public boolean addPermBan(InetAddress item) {
150 | return this.banList.addPermBan(item);
151 | }
152 |
153 | @Override
154 | public String getUnbanDate(InetAddress item) {
155 | return this.banList.getUnbanDate(item);
156 | }
157 |
158 | @Override
159 | public boolean hasChanged() {
160 | return this.banList.hasChanged();
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/main/security/SecurityBanException.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.main.security;
2 |
3 | import java.net.InetAddress;
4 |
5 | public class SecurityBanException extends SecurityException {
6 |
7 | private static final long serialVersionUID = 2169133405309368766L;
8 | private final InetAddress bannedHost;
9 |
10 | public SecurityBanException(String message, InetAddress bannedHost) {
11 | super(message);
12 | this.bannedHost = bannedHost;
13 | }
14 |
15 | public InetAddress getBannedHost() {
16 | return this.bannedHost;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/network/PacketFilter.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.network;
2 |
3 | import java.net.InetAddress;
4 | import java.util.LinkedHashMap;
5 | import java.util.Map;
6 | import java.util.concurrent.TimeUnit;
7 |
8 | import de.poweruser.powerserver.settings.Settings;
9 |
10 | public class PacketFilter {
11 |
12 | private LinkedFilterInfoHashMap filterMap;
13 | private Settings settings;
14 |
15 | public PacketFilter(Settings settings) {
16 | this.settings = settings;
17 | this.filterMap = new LinkedFilterInfoHashMap(16, 0.75f, true);
18 | }
19 |
20 | public boolean newIncoming(InetAddress address, long time) {
21 | FilterInfo filterInfo = this.getOrCreateEntry(address, time);
22 | if(!filterInfo.incomingAndCheckViolations(time)) {
23 | this.filterMap.remove(address);
24 | return false;
25 | }
26 | return true;
27 | }
28 |
29 | private FilterInfo getOrCreateEntry(InetAddress address, long time) {
30 | FilterInfo filterInfo;
31 | if(this.filterMap.containsKey(address)) {
32 | filterInfo = this.filterMap.get(address);
33 | } else {
34 | filterInfo = new FilterInfo();
35 | this.filterMap.put(address, filterInfo);
36 | }
37 | return filterInfo;
38 | }
39 |
40 | private enum FilterCheckResult {
41 | VIOLATION,
42 | INTERVAL_OK,
43 | INTERVAL_LONG;
44 | }
45 |
46 | private class FilterInfo {
47 |
48 | private long lastIncoming;
49 | private int violations;
50 |
51 | public FilterInfo() {
52 | this.lastIncoming = -1L;
53 | this.violations = 0;
54 | }
55 |
56 | public boolean incomingAndCheckViolations(long time) {
57 | FilterCheckResult result = this.checkInterval(time, settings.getAllowedMinimumSendInterval(), settings.getAllowedMinimumSendInterval() * 100);
58 | this.lastIncoming = time;
59 | switch(result) {
60 | case VIOLATION:
61 | if(++this.violations > settings.getMaximumSendViolations()) { return false; }
62 | break;
63 | case INTERVAL_OK:
64 | --this.violations;
65 | break;
66 | case INTERVAL_LONG:
67 | this.violations -= 10;
68 | default:
69 | break;
70 | }
71 | if(this.violations < 0) {
72 | this.violations = 0;
73 | }
74 | return true;
75 | }
76 |
77 | private FilterCheckResult checkInterval(long time, long allowedMinimumInterval, long timeoutDuration) {
78 | if(this.lastIncoming > (time - allowedMinimumInterval)) {
79 | return FilterCheckResult.VIOLATION;
80 | } else if(this.lastIncoming < time - timeoutDuration) { return FilterCheckResult.INTERVAL_LONG; }
81 | return FilterCheckResult.INTERVAL_OK;
82 | }
83 |
84 | public boolean checkLastIncoming(long duration, TimeUnit unit) {
85 | return this.lastIncoming < (System.currentTimeMillis() - TimeUnit.MILLISECONDS.convert(duration, unit));
86 | }
87 | }
88 |
89 | private class LinkedFilterInfoHashMap extends LinkedHashMap {
90 |
91 | private static final long serialVersionUID = 1357559109479352534L;
92 |
93 | public LinkedFilterInfoHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
94 | super(initialCapacity, loadFactor, accessOrder);
95 | }
96 |
97 | @Override
98 | protected boolean removeEldestEntry(Map.Entry eldest) {
99 | return eldest.getValue().checkLastIncoming(10L, TimeUnit.MINUTES);
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/network/QueryConnection.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.network;
2 |
3 | import java.io.BufferedInputStream;
4 | import java.io.BufferedOutputStream;
5 | import java.io.DataInputStream;
6 | import java.io.DataOutputStream;
7 | import java.io.IOException;
8 | import java.net.InetAddress;
9 | import java.net.InetSocketAddress;
10 | import java.net.Socket;
11 | import java.util.List;
12 | import java.util.concurrent.TimeUnit;
13 |
14 | import de.poweruser.powerserver.games.GameBase;
15 | import de.poweruser.powerserver.games.GeneralDataKeysEnum;
16 | import de.poweruser.powerserver.gamespy.EncType;
17 | import de.poweruser.powerserver.gamespy.GamespyValidation;
18 | import de.poweruser.powerserver.gamespy.encoders.EncoderInterface;
19 | import de.poweruser.powerserver.logger.LogLevel;
20 | import de.poweruser.powerserver.logger.Logger;
21 | import de.poweruser.powerserver.main.MessageData;
22 | import de.poweruser.powerserver.main.parser.GamespyProtocol1Parser;
23 | import de.poweruser.powerserver.main.parser.ParserException;
24 | import de.poweruser.powerserver.main.parser.dataverification.IntVerify;
25 |
26 | public class QueryConnection {
27 |
28 | public enum State {
29 | NEW,
30 | CHALLENGE_SENT,
31 | CHALLENGE_VALID,
32 | CHALLENGE_INVALID,
33 | QUERY_RECEIVED,
34 | QUERY_INVALID,
35 | LIST_SENT,
36 | SENDING_FAILED,
37 | TOOMUCHDATA,
38 | TIMEOUT,
39 | SUCCESSFUL,
40 | DONE;
41 | }
42 |
43 | private Socket client;
44 | private State state;
45 | private State failedState;
46 | private String failMessage;
47 | private long lastStateChange;
48 | private DataInputStream in;
49 | private DataOutputStream out;
50 | private GamespyValidation validation;
51 | private byte[] receiveBuffer;
52 | private int receivePos;
53 | private GameBase requestedGame;
54 | private EncType encType;
55 |
56 | public QueryConnection(Socket client) throws IOException {
57 | this.client = client;
58 | this.failedState = null;
59 | this.failMessage = null;
60 | this.changeState(State.NEW);
61 | this.in = new DataInputStream(new BufferedInputStream(this.client.getInputStream()));
62 | this.out = new DataOutputStream(new BufferedOutputStream(this.client.getOutputStream()));
63 | this.receiveBuffer = new byte[512];
64 | this.receivePos = 0;
65 | this.requestedGame = null;
66 | this.encType = null;
67 | }
68 |
69 | public void close() {
70 | try {
71 | this.in.close();
72 | } catch(IOException e1) {}
73 | try {
74 | this.out.close();
75 | } catch(IOException e1) {}
76 | try {
77 | this.client.close();
78 | } catch(IOException e) {}
79 | }
80 |
81 | public void forceClose() {
82 | this.close();
83 | this.changeState(State.DONE);
84 | }
85 |
86 | public boolean check() {
87 | if(this.state.equals(State.LIST_SENT) && !this.checkLastStateChange(TimeUnit.SECONDS, 3)) {
88 | this.changeState(State.SUCCESSFUL);
89 | } else if(!this.checkLastStateChange(TimeUnit.SECONDS, 10)) {
90 | this.changeStateFail(State.TIMEOUT, "The connection timed out. Last state: " + this.state.toString());
91 | }
92 | switch(this.state) {
93 | case NEW:
94 | this.validation = new GamespyValidation();
95 | String challenge = this.validation.getChallengeString();
96 | this.sendData("\\basic\\\\secure\\" + challenge);
97 | this.changeState(State.CHALLENGE_SENT);
98 | break;
99 | case CHALLENGE_SENT:
100 | this.readInput();
101 | Result response = this.checkChallengeResponse();
102 | if(response != null) {
103 | if(response.getResult()) {
104 | this.changeState(State.CHALLENGE_VALID);
105 | } else {
106 | this.changeStateFail(State.CHALLENGE_INVALID, response.getMessage());
107 | }
108 | }
109 | break;
110 | case CHALLENGE_VALID:
111 | this.readInput();
112 | Result query = this.checkListQuery();
113 | if(query != null) {
114 | if(query.getResult()) {
115 | this.changeState(State.QUERY_RECEIVED);
116 | } else {
117 | this.changeStateFail(State.QUERY_INVALID, query.getMessage());
118 | }
119 | }
120 | break;
121 | case QUERY_RECEIVED:
122 | Result sent = this.sendServerList();
123 | if(sent.getResult()) {
124 | this.changeState(State.LIST_SENT);
125 | } else {
126 | this.changeStateFail(State.SENDING_FAILED, sent.getMessage());
127 | }
128 | break;
129 | case LIST_SENT:
130 | break;
131 | case QUERY_INVALID:
132 | case CHALLENGE_INVALID:
133 | case TOOMUCHDATA:
134 | case TIMEOUT:
135 | case SUCCESSFUL:
136 | this.close();
137 | this.changeState(State.DONE);
138 | break;
139 | case DONE:
140 | return true;
141 | default:
142 | break;
143 | }
144 | return false;
145 | }
146 |
147 | private Result sendServerList() {
148 | Result out = new Result(true);
149 | if(this.requestedGame != null && this.encType != null) {
150 | EncoderInterface encoder = this.encType.getEncoder();
151 | if(encoder != null) {
152 |
153 | List serverList = this.requestedGame.getActiveServers();
154 | int serverCount = serverList != null ? serverList.size() : 0;
155 | byte[] data = null;
156 | try {
157 | data = encoder.encode(this.requestedGame.getGamespyKey(), this.validation.getValidationString(), serverList);
158 | } catch(IOException e) {
159 | Logger.logStackTraceStatic(LogLevel.LOW, "Error while encoding a serverlist with Encoder " + encoder.getClass().getSimpleName() + " for EncType " + this.encType.toString() + ": " + e.toString(), e);
160 | } catch(IllegalArgumentException e) {
161 | Logger.logStackTraceStatic(LogLevel.LOW, "Error while encoding a serverlist with Encoder " + encoder.getClass().getSimpleName() + " for EncType " + this.encType.toString() + ": " + e.toString(), e);
162 | }
163 | if(data != null) {
164 | String logMessage = "QUERY (EncType: " + this.encType.toString() + ") Successful from " + this.client.getInetAddress().toString() + " : Sent " + data.length + " Bytes (" + serverCount + " servers)";
165 | Logger.logStatic(LogLevel.HIGH, logMessage);
166 |
167 | this.sendData(data);
168 | } else {
169 | out = new Result(false, "Encoding of the server list failed");
170 | }
171 | } else {
172 | out = new Result(false, "No encoder available yet for EncType: " + this.encType.toString());
173 | }
174 | } else if(this.requestedGame == null) {
175 | out = new Result(false, "The query did not mention a game");
176 | } else if(this.encType == null) {
177 | out = new Result(false, "The query did not mention an EncType");
178 | }
179 | return out;
180 | }
181 |
182 | private Result checkListQuery() {
183 | Result out = null;
184 | String str = new String(this.receiveBuffer, 0, this.receivePos);
185 | GamespyProtocol1Parser parser = new GamespyProtocol1Parser();
186 | MessageData data = null;
187 | try {
188 | data = parser.parse(null, str);
189 | } catch(ParserException e) {
190 | Logger.logStatic(LogLevel.LOW, "Error while checking list query:");
191 | Logger.log(LogLevel.LOW, e);
192 | }
193 | if(data != null) {
194 | if(data.containsKey(GeneralDataKeysEnum.GAMENAME) && data.containsKey(GeneralDataKeysEnum.LIST) && data.containsKey(GeneralDataKeysEnum.FINAL)) {
195 | String gameString = data.getData(GeneralDataKeysEnum.GAMENAME);
196 | GameBase game = GameBase.getGameForGameName(gameString);
197 | if(game != null) {
198 | this.requestedGame = game;
199 | EncType enctype = this.getEncTypeFromData(data);
200 | if(enctype != null) {
201 | this.encType = enctype;
202 | }
203 | out = new Result(true);
204 | } else {
205 | out = new Result(false, "Could not find a matching game for \"" + gameString + "\" in the list query: " + str);
206 | }
207 | this.clearBufferUpToKey(GeneralDataKeysEnum.FINAL);
208 | }
209 | }
210 | return out;
211 | }
212 |
213 | private void readInput() {
214 | try {
215 | int len = this.in.available();
216 | if(len > 0) {
217 | int newSize = this.receivePos + len;
218 | if(newSize > this.receiveBuffer.length) {
219 | if(newSize > 1024) {
220 | this.failedState = this.state;
221 | this.failMessage = "The client has sent too much data";
222 | this.changeState(State.TOOMUCHDATA);
223 | return;
224 | }
225 | byte[] newBuffer = new byte[newSize];
226 | System.arraycopy(this.receiveBuffer, 0, newBuffer, 0, this.receivePos);
227 | this.receiveBuffer = newBuffer;
228 | }
229 | this.in.read(this.receiveBuffer, this.receivePos, len);
230 | this.receivePos += len;
231 | }
232 | } catch(IOException e) {
233 | Logger.logStackTraceStatic(LogLevel.LOW, "Error while reading input: " + e.toString(), e);
234 | }
235 | }
236 |
237 | private Result checkChallengeResponse() {
238 | Result out = null;
239 | String str = new String(this.receiveBuffer, 0, this.receivePos);
240 | if(str.isEmpty()) { return out; }
241 | GamespyProtocol1Parser parser = new GamespyProtocol1Parser();
242 | MessageData data = null;
243 | try {
244 | data = parser.parse(null, str);
245 | } catch(ParserException e) {
246 | Logger.logStatic(LogLevel.HIGH, "Error while checking challenge response:");
247 | Logger.log(LogLevel.HIGH, e);
248 | }
249 | if(data != null) {
250 | if(data.containsKey(GeneralDataKeysEnum.GAMENAME) && data.containsKey(GeneralDataKeysEnum.FINAL) && data.containsKey(GeneralDataKeysEnum.ENCTYPE) && data.containsKey(GeneralDataKeysEnum.VALIDATE)) {
251 | String gameString = data.getData(GeneralDataKeysEnum.GAMENAME);
252 | EncType enctype = this.getEncTypeFromData(data);
253 | String response = data.getData(GeneralDataKeysEnum.VALIDATE);
254 | GameBase game = GameBase.getGameForGameName(gameString);
255 | if(game != null && enctype != null) {
256 | if(this.validation.verifyChallengeResponse(game, enctype, response)) {
257 | this.encType = enctype;
258 | out = new Result(true);
259 | } else {
260 | out = new Result(false, "Validation failed. EncType: " + enctype.toString() + " Secure: " + this.validation.getChallengeString() + " Received: " + response + " Should have been: " + this.validation.getValidationString());
261 | }
262 | } else if(game == null) {
263 | out = new Result(false, "Could not find a matching game for \"" + gameString + "\" in the challenge response: " + str);
264 | } else if(enctype == null) {
265 | out = new Result(false, "Could not recognise the EncType \"" + data.getData(GeneralDataKeysEnum.ENCTYPE) + "\" in the challenge response: " + str);
266 | }
267 | this.clearBufferUpToKey(GeneralDataKeysEnum.FINAL);
268 | }
269 | }
270 | return out;
271 | }
272 |
273 | public InetAddress getClientAddress() {
274 | return this.client.getInetAddress();
275 | }
276 |
277 | private void sendData(String data) {
278 | this.sendData(data.getBytes());
279 | }
280 |
281 | private void sendData(byte[] data) {
282 | try {
283 | this.out.write(data);
284 | this.out.flush();
285 | } catch(IOException e) {
286 | Logger.logStackTraceStatic(LogLevel.HIGH, "Error while sending data to " + this.client.getInetAddress().toString(), e);
287 | }
288 | }
289 |
290 | private EncType getEncTypeFromData(MessageData data) {
291 | GeneralDataKeysEnum enc = GeneralDataKeysEnum.ENCTYPE;
292 | if(data.containsKey(enc)) {
293 | IntVerify v = (IntVerify) enc.getVerifierCopy();
294 | if(v.verify(data.getData(enc))) { return EncType.getTypeFromValue(v.getVerifiedValue()); }
295 | }
296 | return null;
297 | }
298 |
299 | public String getEncTypeToString() {
300 | if(this.encType != null) { return this.encType.toString(); }
301 | return "UNKNOWN";
302 | }
303 |
304 | private void clearBufferUpToKey(GeneralDataKeysEnum key) {
305 | String content = new String(this.receiveBuffer, 0, this.receivePos);
306 | String str = "\\" + key.getKeyString();
307 | int index = content.toLowerCase().indexOf(str);
308 | if(index >= 0) {
309 | int newStart = index + str.length();
310 | int newLength = this.receivePos - newStart;
311 | System.arraycopy(this.receiveBuffer, newStart, this.receiveBuffer, 0, newLength);
312 | this.receivePos = newLength;
313 | }
314 | }
315 |
316 | private void changeState(State state) {
317 | this.lastStateChange = System.currentTimeMillis();
318 | this.state = state;
319 | }
320 |
321 | private void changeStateFail(State state, String message) {
322 | this.changeState(state);
323 | this.failedState = state;
324 | this.failMessage = message;
325 | }
326 |
327 | private boolean checkLastStateChange(TimeUnit unit, int value) {
328 | return (System.currentTimeMillis() - this.lastStateChange) < TimeUnit.MILLISECONDS.convert(value, unit);
329 | }
330 |
331 | public State getFailedState() {
332 | return this.failedState;
333 | }
334 |
335 | public String getFailMessage() {
336 | return this.failMessage;
337 | }
338 |
339 | private class Result {
340 | private String message;
341 | private boolean result;
342 |
343 | public Result(boolean result) {
344 | this.result = result;
345 | }
346 |
347 | public Result(boolean result, String message) {
348 | this(result);
349 | this.message = message;
350 | }
351 |
352 | public boolean getResult() {
353 | return this.result;
354 | }
355 |
356 | public String getMessage() {
357 | return this.message;
358 | }
359 | }
360 | }
361 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/network/TCPManager.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.network;
2 |
3 | import java.io.IOException;
4 | import java.net.InetAddress;
5 | import java.net.ServerSocket;
6 | import java.net.Socket;
7 | import java.net.SocketException;
8 | import java.util.ArrayList;
9 | import java.util.HashMap;
10 | import java.util.Iterator;
11 | import java.util.List;
12 | import java.util.Queue;
13 | import java.util.concurrent.ConcurrentLinkedQueue;
14 | import java.util.concurrent.TimeUnit;
15 |
16 | import de.poweruser.powerserver.logger.LogLevel;
17 | import de.poweruser.powerserver.logger.Logger;
18 | import de.poweruser.powerserver.main.security.BanManager;
19 | import de.poweruser.powerserver.main.security.SecurityBanException;
20 | import de.poweruser.powerserver.network.QueryConnection.State;
21 | import de.poweruser.powerserver.settings.Settings;
22 |
23 | public class TCPManager implements Runnable {
24 |
25 | private ServerSocket serverSocket;
26 | private Queue connections;
27 | private boolean running;
28 | private Thread thread;
29 | private ConnectionGuard guard;
30 | private Settings settings;
31 |
32 | public TCPManager(int port, Settings settings, BanManager banManager) throws IOException {
33 | this.running = true;
34 | this.serverSocket = new ServerSocket(port);
35 | this.settings = settings;
36 | this.guard = new ConnectionGuard(banManager);
37 | this.connections = new ConcurrentLinkedQueue();
38 | this.thread = new Thread(this);
39 | this.thread.setName("PowerServer - TCPManager");
40 | this.thread.start();
41 | }
42 |
43 | @Override
44 | public void run() {
45 | while(this.running) {
46 | Socket client = null;
47 | try {
48 | client = this.serverSocket.accept();
49 | } catch(SecurityBanException e) {
50 | InetAddress host = e.getBannedHost();
51 | this.guard.checkBan(host);
52 | } catch(SecurityException e) {
53 | Logger.logStackTraceStatic(LogLevel.VERY_LOW, "SecurityException while accepting an incoming TCP connection.", e);
54 | } catch(SocketException e) {
55 | if(this.running) {
56 | Logger.logStatic(LogLevel.VERY_LOW, "The TCPManager failed to accept an incoming query connection: " + e.toString());
57 | }
58 | } catch(IOException e) {
59 | if(this.running) {
60 | Logger.logStatic(LogLevel.VERY_LOW, "The TCPManager failed to accept an incoming query connection: " + e.toString());
61 | }
62 | }
63 | if(client != null) {
64 | if(this.guard.isAnotherConnectionAllowed(client)) {
65 | QueryConnection connection = null;
66 | try {
67 | connection = new QueryConnection(client);
68 | } catch(IOException e) {
69 | Logger.logStackTraceStatic(LogLevel.LOW, "Error while creating a QueryConnection: " + e.toString(), e);
70 | }
71 | if(connection != null) {
72 | this.connections.add(connection);
73 | this.guard.trackConnection(connection);
74 | }
75 | } else {
76 | try {
77 | client.close();
78 | } catch(IOException e) {}
79 | }
80 | }
81 | }
82 | }
83 |
84 | public boolean isSocketClosed() {
85 | return this.serverSocket.isClosed();
86 | }
87 |
88 | public void shutdown() {
89 | this.running = false;
90 | Iterator iter = this.connections.iterator();
91 | while(iter.hasNext()) {
92 | QueryConnection c = iter.next();
93 | c.close();
94 | iter.remove();
95 | this.guard.untrackConnection(c);
96 | }
97 | try {
98 | this.serverSocket.close();
99 | } catch(IOException e) {}
100 | }
101 |
102 | public void processConnections() {
103 | Iterator iter = this.connections.iterator();
104 | while(iter.hasNext()) {
105 | QueryConnection c = iter.next();
106 | if(c.check()) {
107 | this.guard.untrackConnection(c);
108 | iter.remove();
109 | State failed = c.getFailedState();
110 | if(failed != null) {
111 | String logMessage = "QUERY (EncType: " + c.getEncTypeToString() + ") FAILED from " + c.getClientAddress().toString() + " State: " + failed.toString();
112 | String failMessage = c.getFailMessage();
113 | if(failMessage != null) {
114 | logMessage += " Reason: " + failMessage;
115 | }
116 | Logger.logStatic(LogLevel.LOW, logMessage);
117 | }
118 | }
119 | }
120 | }
121 |
122 | private class ConnectionGuard {
123 | private HashMap> map;
124 | private BanManager banManager;
125 |
126 | public ConnectionGuard(BanManager banManager) {
127 | this.map = new HashMap>();
128 | this.banManager = banManager;
129 | }
130 |
131 | public void checkBan(InetAddress host) {
132 | this.banManager.isBanned(host);
133 | }
134 |
135 | public void untrackConnection(QueryConnection c) {
136 | InetAddress a = c.getClientAddress();
137 | if(this.map.containsKey(a)) {
138 | List list = this.map.get(a);
139 | list.remove(c);
140 | if(list.isEmpty()) {
141 | this.map.remove(a);
142 | }
143 | }
144 | }
145 |
146 | public void trackConnection(QueryConnection c) {
147 | InetAddress a = c.getClientAddress();
148 | List list;
149 | if(this.map.containsKey(a)) {
150 | list = this.map.get(a);
151 | } else {
152 | list = new ArrayList();
153 | this.map.put(a, list);
154 | }
155 | list.add(c);
156 | }
157 |
158 | public boolean isAnotherConnectionAllowed(Socket client) {
159 | InetAddress address = client.getInetAddress();
160 | if(this.banManager.isBanned(address)) { return false; }
161 |
162 | if(this.map.containsKey(address)) {
163 | List list = this.map.get(address);
164 | boolean allowed = (list.size() < settings.getConnectionLimitPerClient());
165 | if(!allowed) {
166 | this.ban(address);
167 | }
168 | return allowed;
169 | }
170 | return true;
171 | }
172 |
173 | private void ban(InetAddress address) {
174 | TimeUnit unit = TimeUnit.MINUTES;
175 | long duration = settings.getTempBanDuration(unit);
176 | if(this.banManager.addTempBanByDuration(address, duration, unit)) {
177 | Logger.logStatic(LogLevel.NORMAL, "Temporary ban of " + duration + " " + unit.toString().toLowerCase() + " for " + address.toString() + ". Too many open connections");
178 | }
179 | if(this.map.containsKey(address)) {
180 | List list = this.map.get(address);
181 | for(QueryConnection q: list) {
182 | q.forceClose();
183 | }
184 | }
185 | }
186 | }
187 |
188 | public boolean isShutdown() {
189 | return this.serverSocket.isClosed() && !this.running;
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/network/UDPManager.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.network;
2 |
3 | import java.net.DatagramSocket;
4 | import java.net.InetAddress;
5 | import java.net.SocketException;
6 | import java.util.Observable;
7 | import java.util.Observer;
8 | import java.util.Queue;
9 | import java.util.concurrent.ConcurrentLinkedQueue;
10 |
11 | import de.poweruser.powerserver.main.security.BanManager;
12 | import de.poweruser.powerserver.settings.Settings;
13 |
14 | public class UDPManager implements Observer {
15 |
16 | private UDPReceiverThread receiver;
17 | private UDPSender sender;
18 | private DatagramSocket socket;
19 | private Queue messageQueue;
20 |
21 | public static final int MAX_MESSAGECOUNT_PER_CYCLE = 50;
22 |
23 | public UDPManager(int port, Settings settings, BanManager banManager) throws SocketException {
24 | this.socket = new DatagramSocket(port);
25 | this.socket.setSoTimeout(10000);
26 | this.receiver = new UDPReceiverThread(socket, settings, banManager);
27 | this.receiver.addObserver(this);
28 | this.sender = new UDPSender(socket);
29 | this.messageQueue = new ConcurrentLinkedQueue();
30 | }
31 |
32 | public void shutdown() {
33 | this.receiver.shutdown();
34 | this.receiver.deleteObserver(this);
35 | this.socket.close();
36 | }
37 |
38 | public boolean isSocketClosed() {
39 | return this.socket.isClosed();
40 | }
41 |
42 | @Override
43 | public void update(Observable observable, Object obj) {
44 | if(observable.equals(this.receiver)) {
45 | if(obj instanceof UDPMessage) {
46 | this.messageQueue.add((UDPMessage) obj);
47 | }
48 | }
49 | }
50 |
51 | public boolean hasMessages() {
52 | return !this.messageQueue.isEmpty();
53 | }
54 |
55 | public UDPMessage takeFirstMessage() {
56 | UDPMessage message = this.messageQueue.poll();
57 | if(!this.receiver.isBanned(message.getSender().getAddress())) { return message; }
58 | return null;
59 | }
60 |
61 | public int getPort() {
62 | return this.socket.getLocalPort();
63 | }
64 |
65 | public UDPSender getUDPSender() {
66 | return this.sender;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/network/UDPMessage.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.network;
2 |
3 | import java.io.UnsupportedEncodingException;
4 | import java.net.DatagramPacket;
5 | import java.net.InetSocketAddress;
6 |
7 | public class UDPMessage {
8 |
9 | private final byte[] data;
10 | private final InetSocketAddress sender;
11 |
12 | public UDPMessage(DatagramPacket packet) {
13 | int length = packet.getLength();
14 | this.data = new byte[length];
15 | System.arraycopy(packet.getData(), 0, this.data, 0, length);
16 | this.sender = new InetSocketAddress(packet.getAddress(), packet.getPort());
17 | }
18 |
19 | public final InetSocketAddress getSender() {
20 | return this.sender;
21 | }
22 |
23 | public final byte[] getData() {
24 | return this.data;
25 | }
26 |
27 | @Override
28 | public String toString() {
29 | try {
30 | return new String(data, 0, data.length, "UTF-8");
31 | } catch(UnsupportedEncodingException thr) {
32 | return new String(data, 0, data.length);
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/network/UDPReceiverThread.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.network;
2 |
3 | import java.io.IOException;
4 | import java.net.DatagramPacket;
5 | import java.net.DatagramSocket;
6 | import java.net.InetAddress;
7 | import java.net.SocketException;
8 | import java.net.SocketTimeoutException;
9 | import java.util.Observable;
10 | import java.util.concurrent.TimeUnit;
11 |
12 | import de.poweruser.powerserver.logger.LogLevel;
13 | import de.poweruser.powerserver.logger.Logger;
14 | import de.poweruser.powerserver.main.security.BanManager;
15 | import de.poweruser.powerserver.main.security.SecurityBanException;
16 | import de.poweruser.powerserver.settings.Settings;
17 |
18 | public class UDPReceiverThread extends Observable implements Runnable {
19 |
20 | private boolean running;
21 | private DatagramSocket socket;
22 | private Thread thread;
23 | private BanManager banManager;
24 | private PacketFilter packetFilter;
25 | private Settings settings;
26 |
27 | public UDPReceiverThread(DatagramSocket socket, Settings settings, BanManager banManager) throws SocketException {
28 | this.socket = socket;
29 | this.banManager = banManager;
30 | this.settings = settings;
31 | this.packetFilter = new PacketFilter(settings);
32 | this.running = true;
33 | this.thread = new Thread(this);
34 | this.thread.setName("PowerServer_UDPReceiverThread");
35 | this.thread.start();
36 | }
37 |
38 | @Override
39 | public void run() {
40 | DatagramPacket packet = new DatagramPacket(new byte[2048], 2048);
41 | while(this.running) {
42 | boolean received = false;
43 | try {
44 | this.socket.receive(packet);
45 | if(!this.banManager.isBanned(packet.getAddress())) {
46 | InetAddress sender = packet.getAddress();
47 | if(this.packetFilter.newIncoming(sender, System.currentTimeMillis())) {
48 | received = true;
49 | } else {
50 | this.banSender(sender, this.settings.getTempBanDuration(TimeUnit.MINUTES), TimeUnit.MINUTES);
51 | }
52 | }
53 | } catch(SecurityBanException e) {
54 | InetAddress host = e.getBannedHost();
55 | this.banManager.isBanned(host);
56 | } catch(SecurityException e) {
57 | Logger.logStackTraceStatic(LogLevel.VERY_LOW, "SecurityException while receiving a UDP packet.", e);
58 | } catch(SocketTimeoutException e) {
59 |
60 | } catch(IOException e) {
61 | if(this.running) {
62 | Logger.logStackTraceStatic(LogLevel.VERY_LOW, "An error occured in the UDPReceiverThread while listening for incoming packets", e);
63 | }
64 | }
65 | if(received && this.running) {
66 | this.setChanged();
67 | this.notifyObservers(new UDPMessage(packet));
68 | }
69 | }
70 | }
71 |
72 | public void shutdown() {
73 | this.running = false;
74 | }
75 |
76 | protected void banSender(InetAddress sender, long duration, TimeUnit unit) {
77 | if(this.banManager.addTempBanByDuration(sender, duration, unit)) {
78 | long minutes = TimeUnit.MINUTES.convert(duration, unit);
79 | Logger.logStatic(LogLevel.NORMAL, "Temporary Ban of " + minutes + " " + TimeUnit.MINUTES.toString().toLowerCase() + " for " + sender.getHostAddress() + ". Too much data is incoming too fast from this host.");
80 | }
81 | }
82 |
83 | public boolean isBanned(InetAddress address) {
84 | return this.banManager.isBanned(address);
85 | }
86 |
87 | }
88 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/network/UDPSender.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.network;
2 |
3 | import java.io.IOException;
4 | import java.net.DatagramPacket;
5 | import java.net.DatagramSocket;
6 | import java.net.InetAddress;
7 | import java.net.InetSocketAddress;
8 | import java.net.SocketAddress;
9 | import java.util.HashMap;
10 | import java.util.List;
11 | import java.util.Map.Entry;
12 |
13 | import de.poweruser.powerserver.logger.LogLevel;
14 | import de.poweruser.powerserver.logger.Logger;
15 | import de.poweruser.powerserver.main.PowerServer;
16 |
17 | public class UDPSender {
18 |
19 | private DatagramSocket udpSocket;
20 | private HashMap queries;
21 | private HashMap broadcasts;
22 |
23 | public UDPSender(DatagramSocket udpSocket) {
24 | this.queries = new HashMap();
25 | this.broadcasts = new HashMap();
26 | this.udpSocket = udpSocket;
27 | }
28 |
29 | public void queueHeartBeatBroadcast(List masterServers, DatagramPacket packet) {
30 | if(packet != null) {
31 | for(InetAddress ms: masterServers) {
32 | this.broadcasts.put(new InetSocketAddress(ms, PowerServer.MASTERSERVER_UDP_PORT), packet);
33 | }
34 | }
35 | }
36 |
37 | public void queueQuery(InetSocketAddress server, DatagramPacket packet) {
38 | packet.setAddress(server.getAddress());
39 | packet.setPort(server.getPort());
40 | this.queries.put(server, packet);
41 | }
42 |
43 | public void flush() {
44 | for(DatagramPacket packet: this.queries.values()) {
45 | try {
46 | this.udpSocket.send(packet);
47 | } catch(IOException e) {
48 | Logger.logStatic(LogLevel.VERY_LOW, e.toString() + "\nFailed to send a server query to " + packet.getSocketAddress().toString() + " - Content: " + new String(packet.getData()));
49 | }
50 | }
51 | this.queries.clear();
52 | for(Entry entry: this.broadcasts.entrySet()) {
53 | DatagramPacket packet = entry.getValue();
54 | try {
55 | packet.setSocketAddress(entry.getKey());
56 | this.udpSocket.send(packet);
57 | } catch(IOException e) {
58 | Logger.logStatic(LogLevel.VERY_LOW, e.toString() + "\nFailed to send a heartbeatbroadcast to a masterserver at " + packet.getAddress().getHostAddress() + ":" + packet.getPort() + ". Content: " + new String(packet.getData()));
59 | }
60 | }
61 | this.broadcasts.clear();
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/settings/SectionGeneral.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.settings;
2 |
3 | import de.poweruser.powerserver.logger.LogLevel;
4 | import de.poweruser.powerserver.logger.Logger;
5 | import de.poweruser.powerserver.main.parser.dataverification.BooleanVerify;
6 | import de.poweruser.powerserver.main.parser.dataverification.IntVerify;
7 |
8 | public class SectionGeneral extends SettingsReader {
9 |
10 | public SectionGeneral(Settings settings) {
11 | super(settings);
12 | }
13 |
14 | @Override
15 | public void readLine(String line) {
16 | String[] split = line.split("=");
17 | if(split.length == 2) {
18 | String key = split[0].trim();
19 | String value = split[1].trim();
20 | IntVerify intVerifier;
21 | BooleanVerify boolVerifier;
22 | if(key.equalsIgnoreCase("masterserverlistsdownloadinterval")) {
23 | intVerifier = new IntVerify(0, Integer.MAX_VALUE);
24 | if(intVerifier.verify(value)) {
25 | settings.setListsDownloadInterval(intVerifier.getVerifiedValue());
26 | }
27 | } else if(key.equalsIgnoreCase("publicmode")) {
28 | boolVerifier = new BooleanVerify();
29 | settings.setPublicMode(boolVerifier.verify(value));
30 | } else if(key.equalsIgnoreCase("loglevel")) {
31 | intVerifier = new IntVerify(0, LogLevel.getMaxLevel().getValue());
32 | if(intVerifier.verify(value)) {
33 | Logger.setLogLevel(intVerifier.getVerifiedValue());
34 | }
35 | } else if(key.equalsIgnoreCase("queryServersOnHeartbeat")) {
36 | boolVerifier = new BooleanVerify();
37 | settings.setQueryServersOnHeartbeat(boolVerifier.verify(value));
38 | } else if(key.equalsIgnoreCase("maximumServerTimeout")) {
39 | intVerifier = new IntVerify(0, Integer.MAX_VALUE);
40 | if(intVerifier.verify(value)) {
41 | settings.setMaximumServerTimeout(intVerifier.getVerifiedValue());
42 | }
43 | } else if(key.equalsIgnoreCase("maximumServersPerHost")) {
44 | intVerifier = new IntVerify(0, Integer.MAX_VALUE);
45 | if(intVerifier.verify(value)) {
46 | settings.setMaximumServersPerHost(intVerifier.getVerifiedValue());
47 | }
48 | } else if(key.equalsIgnoreCase("maximumSendViolations")) {
49 | intVerifier = new IntVerify(0, Integer.MAX_VALUE);
50 | if(intVerifier.verify(value)) {
51 | settings.setMaximumSendViolations(intVerifier.getVerifiedValue());
52 | }
53 | } else if(key.equalsIgnoreCase("allowedMinimumSendInterval")) {
54 | intVerifier = new IntVerify(1, Integer.MAX_VALUE);
55 | if(intVerifier.verify(value)) {
56 | settings.setAllowedMinimumSendInterval(intVerifier.getVerifiedValue());
57 | }
58 | } else if(key.equalsIgnoreCase("tempBanDuration")) {
59 | intVerifier = new IntVerify(0, Integer.MAX_VALUE);
60 | if(intVerifier.verify(value)) {
61 | settings.setTempBanDuration(intVerifier.getVerifiedValue());
62 | }
63 | } else if(key.equalsIgnoreCase("connectionLimitPerClient")) {
64 | intVerifier = new IntVerify(1, Integer.MAX_VALUE);
65 | if(intVerifier.verify(value)) {
66 | settings.setConnectionLimitPerClient(intVerifier.getVerifiedValue());
67 | }
68 | }
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/settings/SectionMasterServerLists.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.settings;
2 |
3 | import java.net.MalformedURLException;
4 | import java.net.URL;
5 |
6 | import de.poweruser.powerserver.logger.LogLevel;
7 | import de.poweruser.powerserver.logger.Logger;
8 |
9 | public class SectionMasterServerLists extends SettingsReader {
10 | public SectionMasterServerLists(Settings settings) {
11 | super(settings);
12 | }
13 |
14 | @Override
15 | public void readLine(String line) {
16 | try {
17 | URL url = new URL(line.trim().toLowerCase());
18 | this.settings.addMasterServerList(url);
19 | } catch(MalformedURLException e) {
20 | Logger.logStatic(LogLevel.VERY_LOW, "The url [ " + line + " ] of the master server list is malformed: " + e.toString());
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/settings/SectionSupportedGames.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.settings;
2 |
3 | public class SectionSupportedGames extends SettingsReader {
4 |
5 | public SectionSupportedGames(Settings settings) {
6 | super(settings);
7 | }
8 |
9 | @Override
10 | public void readLine(String line) {
11 | this.settings.addSupportedGame(line.trim());
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/settings/Settings.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.settings;
2 |
3 | import java.io.BufferedReader;
4 | import java.io.File;
5 | import java.io.FileReader;
6 | import java.io.IOException;
7 | import java.io.InputStreamReader;
8 | import java.net.InetAddress;
9 | import java.net.URL;
10 | import java.net.UnknownHostException;
11 | import java.util.ArrayList;
12 | import java.util.List;
13 | import java.util.concurrent.TimeUnit;
14 |
15 | import de.poweruser.powerserver.logger.LogLevel;
16 | import de.poweruser.powerserver.logger.Logger;
17 |
18 | public class Settings {
19 |
20 | private File settingsFile;
21 | private static Settings instance;
22 | private List masterServerLists;
23 | private List masterServers;
24 | private List supportedGames;
25 | private int downloadInterval;
26 | private boolean publicMode;
27 | private boolean queryServersOnHeartbeat;
28 | private int maximumServerTimeout;
29 | private int emergencyQueryInterval;
30 | private int maximumSendViolations;
31 | private int allowedMinimumSendInterval;
32 | private int tempBanDuration;
33 | private int connectionLimitPerClient;
34 | private int maximumServersPerHost;
35 |
36 | private static final int MINIMAL_SERVERTIMEOUT = 20;
37 | private static final int ALLOWED_HEARTBEATTIMEOUT = 15;
38 |
39 | public Settings(File settingsFile) {
40 | instance = this;
41 | this.downloadInterval = 24;
42 | this.publicMode = true;
43 | this.queryServersOnHeartbeat = true;
44 | this.maximumServerTimeout = 60;
45 | this.calcEmergencyQueryInterval();
46 | this.maximumServersPerHost = 10;
47 | this.maximumSendViolations = 50;
48 | this.allowedMinimumSendInterval = 250;
49 | this.tempBanDuration = 15;
50 | this.connectionLimitPerClient = 2;
51 | this.masterServerLists = new ArrayList();
52 | this.masterServers = new ArrayList();
53 | this.supportedGames = new ArrayList();
54 | this.settingsFile = settingsFile;
55 | }
56 |
57 | public void load() {
58 | this.masterServerLists.clear();
59 | this.supportedGames.clear();
60 | BufferedReader br = null;
61 | Logger.logStatic(LogLevel.NORMAL, "Loading the settings file ...", true);
62 | try {
63 | br = new BufferedReader(new FileReader(this.settingsFile));
64 | String line = null;
65 | ConfigSection activeSection = null;
66 | while((line = br.readLine()) != null) {
67 | line = line.trim();
68 | if(line.startsWith("#") || line.isEmpty()) {
69 | continue;
70 | }
71 | if(line.startsWith("[")) {
72 | activeSection = ConfigSection.getSection(line);
73 | continue;
74 | }
75 | if(activeSection != null) {
76 | activeSection.readLine(line);
77 | }
78 | }
79 | } catch(IOException e) {
80 | Logger.logStatic(LogLevel.VERY_LOW, "IO Error while reading settings file: " + this.settingsFile.getName() + ": " + e.getMessage(), true);
81 | } finally {
82 | if(br != null) {
83 | try {
84 | br.close();
85 | } catch(IOException e) {}
86 | }
87 | }
88 | Logger.logStatic(LogLevel.LOW, "The server is operating in " + (this.isPublicMode() ? "PUBLIC" : "PRIVATE") + " mode", true);
89 | }
90 |
91 | enum ConfigSection {
92 | GENERAL("[General]", new SectionGeneral(Settings.instance)),
93 | MASTERSERVERLISTS("[MasterServerLists]", new SectionMasterServerLists(Settings.instance)),
94 | SUPPORTEDGAMES("[SupportedGames]", new SectionSupportedGames(Settings.instance));
95 |
96 | private String sectionIdent;
97 | private SettingsReaderInterface reader;
98 |
99 | ConfigSection(String sectionIdent, SettingsReaderInterface settingsReader) {
100 | this.sectionIdent = sectionIdent;
101 | this.reader = settingsReader;
102 | }
103 |
104 | public void readLine(String line) {
105 | this.reader.readLine(line);
106 | }
107 |
108 | public static ConfigSection getSection(String line) {
109 | for(ConfigSection c: values()) {
110 | if(c.sectionIdent.equalsIgnoreCase(line)) { return c; }
111 | }
112 | return null;
113 | }
114 | }
115 |
116 | protected void addMasterServerList(URL url) {
117 | if(!this.masterServerLists.contains(url)) {
118 | this.masterServerLists.add(url);
119 | }
120 | }
121 |
122 | protected void addSupportedGame(String gamename) {
123 | if(!this.supportedGames.contains(gamename)) {
124 | this.supportedGames.add(gamename);
125 | }
126 | }
127 |
128 | protected void setListsDownloadInterval(int hours) {
129 | if(hours >= 1) {
130 | this.downloadInterval = hours;
131 | }
132 | }
133 |
134 | public long getListsDownloadInterval(TimeUnit outputUnit) {
135 | return outputUnit.convert((long) this.downloadInterval, TimeUnit.HOURS);
136 | }
137 |
138 | protected void setPublicMode(boolean active) {
139 | this.publicMode = active;
140 | }
141 |
142 | public boolean isPublicMode() {
143 | return this.publicMode;
144 | }
145 |
146 | public List getMasterServerList(boolean forceDownload) {
147 | if(forceDownload) {
148 | for(URL list: this.masterServerLists) {
149 | BufferedReader input = null;
150 | try {
151 | input = new BufferedReader(new InputStreamReader(list.openStream()));
152 | String inputLine = null;
153 | boolean readOnline = false;
154 | boolean readOffline = false;
155 | while((inputLine = input.readLine()) != null) {
156 | inputLine = inputLine.trim().toLowerCase();
157 | if(inputLine.startsWith("[\\online]")) {
158 | readOnline = false;
159 | } else if(inputLine.startsWith("[\\offline]")) {
160 | readOffline = false;
161 | break;
162 | }
163 | if(readOnline) {
164 | if(!this.masterServers.contains(inputLine)) {
165 | this.masterServers.add(inputLine);
166 | }
167 | } else if(readOffline) {
168 | if(this.masterServers.contains(inputLine)) {
169 | this.masterServers.remove(inputLine);
170 | }
171 | }
172 | if(inputLine.startsWith("[online]")) {
173 | readOnline = true;
174 | } else if(inputLine.startsWith("[offline]")) {
175 | readOffline = true;
176 | }
177 | }
178 | } catch(IOException e) {
179 | Logger.logStatic(LogLevel.LOW, "Could not read the master server list at " + list.toString() + " - Reason: " + e.toString());
180 | } finally {
181 | if(input != null) {
182 | try {
183 | input.close();
184 | } catch(IOException e) {}
185 | }
186 | }
187 | }
188 | }
189 | String logMessage = "";
190 | ArrayList list = new ArrayList();
191 | for(String s: this.masterServers) {
192 | try {
193 | InetAddress i = InetAddress.getByName(s);
194 | list.add(i);
195 | logMessage += (i.toString() + "\n");
196 | } catch(UnknownHostException e) {
197 | Logger.logStatic(LogLevel.LOW, "The master server domain '" + s + "' could not be resolved: " + e.toString());
198 | }
199 | }
200 | if(!logMessage.isEmpty()) {
201 | Logger.logStatic(LogLevel.LOW, "The loaded master servers are:\n" + logMessage, true);
202 | }
203 | return list;
204 | }
205 |
206 | public List getSupportedGamesList() {
207 | ArrayList list = new ArrayList();
208 | list.addAll(this.supportedGames);
209 | return list;
210 | }
211 |
212 | public boolean getQueryServersOnHeartbeat() {
213 | return this.queryServersOnHeartbeat;
214 | }
215 |
216 | protected void setQueryServersOnHeartbeat(boolean active) {
217 | this.queryServersOnHeartbeat = active;
218 | }
219 |
220 | protected void setMaximumServerTimeout(int timeout) {
221 | if(timeout >= MINIMAL_SERVERTIMEOUT) {
222 | this.maximumServerTimeout = timeout;
223 | this.calcEmergencyQueryInterval();
224 | }
225 | }
226 |
227 | public long getMaximumServerTimeout(TimeUnit unit) {
228 | return unit.convert(this.maximumServerTimeout, TimeUnit.MINUTES);
229 | }
230 |
231 | private void calcEmergencyQueryInterval() {
232 | if(this.maximumServerTimeout >= MINIMAL_SERVERTIMEOUT) {
233 | this.emergencyQueryInterval = Math.max(1, (this.maximumServerTimeout - ALLOWED_HEARTBEATTIMEOUT) / 5);
234 | } else {
235 | this.emergencyQueryInterval = 5;
236 | }
237 | }
238 |
239 | public long getEmergencyQueryInterval(TimeUnit unit) {
240 | return unit.convert(this.emergencyQueryInterval, TimeUnit.MINUTES);
241 | }
242 |
243 | public long getAllowedHeartbeatTimeout(TimeUnit unit) {
244 | return unit.convert(ALLOWED_HEARTBEATTIMEOUT, TimeUnit.MINUTES);
245 | }
246 |
247 | public int getMaximumSendViolations() {
248 | return this.maximumSendViolations;
249 | }
250 |
251 | public int getAllowedMinimumSendInterval() {
252 | return this.allowedMinimumSendInterval;
253 | }
254 |
255 | protected void setMaximumSendViolations(int max) {
256 | this.maximumSendViolations = max;
257 | }
258 |
259 | protected void setAllowedMinimumSendInterval(int interval) {
260 | this.allowedMinimumSendInterval = interval;
261 | }
262 |
263 | public long getTempBanDuration(TimeUnit unit) {
264 | return unit.convert(this.tempBanDuration, TimeUnit.MINUTES);
265 | }
266 |
267 | protected void setTempBanDuration(int duration) {
268 | this.tempBanDuration = duration;
269 | }
270 |
271 | public int getConnectionLimitPerClient() {
272 | return this.connectionLimitPerClient;
273 | }
274 |
275 | protected void setConnectionLimitPerClient(int limit) {
276 | this.connectionLimitPerClient = limit;
277 | }
278 |
279 | public int getMaximumServersPerHost() {
280 | return this.maximumServersPerHost;
281 | }
282 |
283 | public void setMaximumServersPerHost(int max) {
284 | this.maximumServersPerHost = max;
285 | }
286 | }
287 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/settings/SettingsReader.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.settings;
2 |
3 | public abstract class SettingsReader implements SettingsReaderInterface {
4 |
5 | protected Settings settings;
6 |
7 | public SettingsReader(Settings settings) {
8 | this.settings = settings;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/de/poweruser/powerserver/settings/SettingsReaderInterface.java:
--------------------------------------------------------------------------------
1 | package de.poweruser.powerserver.settings;
2 |
3 | public interface SettingsReaderInterface {
4 | public void readLine(String line);
5 | }
6 |
--------------------------------------------------------------------------------