├── 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 | --------------------------------------------------------------------------------