package games.strategy.engine.framework.startup.login; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.HashMap; import java.util.Map; import games.strategy.engine.ClientContext; import games.strategy.net.ILoginValidator; import games.strategy.net.IServerMessenger; import games.strategy.util.MD5Crypt; import games.strategy.util.ThreadUtil; import games.strategy.util.Version; /** * If we require a password, then we challenge the client with a salt value, the salt * being different for each login attempt. . The client hashes the password entered by * the user with this salt, and sends it back to us. This prevents the password from * travelling over the network in plain text, and also prevents someone listening on * the connection from getting enough information to log in (since the salt will change * on the next login attempt) */ public class ClientLoginValidator implements ILoginValidator { public static final String SALT_PROPERTY = "Salt"; public static final String PASSWORD_REQUIRED_PROPERTY = "Password Required"; static final String YOU_HAVE_BEEN_BANNED = "The host has banned you from this game"; static final String UNABLE_TO_OBTAIN_MAC = "Unable to obtain mac address"; static final String INVALID_MAC = "Invalid mac address"; private final IServerMessenger m_serverMessenger; private String m_password; public ClientLoginValidator(final IServerMessenger serverMessenger) { m_serverMessenger = serverMessenger; } /** * Set the password required for the game, or to null if no password is required. */ public void setGamePassword(final String password) { m_password = password; } @Override public Map<String, String> getChallengeProperties(final String userName, final SocketAddress remoteAddress) { final Map<String, String> challengeProperties = new HashMap<>(); challengeProperties.put("Sever Version", ClientContext.engineVersion().toString()); if (m_password != null) { /** * Get a new random salt. */ final String encryptedPassword = MD5Crypt.crypt(m_password); challengeProperties.put(SALT_PROPERTY, MD5Crypt.getSalt(MD5Crypt.MAGIC, encryptedPassword)); challengeProperties.put(PASSWORD_REQUIRED_PROPERTY, Boolean.TRUE.toString()); } else { challengeProperties.put(PASSWORD_REQUIRED_PROPERTY, Boolean.FALSE.toString()); } return challengeProperties; } @Override public String verifyConnection(final Map<String, String> propertiesSentToClient, final Map<String, String> propertiesReadFromClient, final String clientName, final String hashedMac, final SocketAddress remoteAddress) { final String versionString = propertiesReadFromClient.get(ClientLogin.ENGINE_VERSION_PROPERTY); if (versionString == null || versionString.length() > 20 || versionString.trim().length() == 0) { return "Invalid version " + versionString; } // check for version final Version clientVersion = new Version(versionString); if (!ClientContext.engineVersion().getVersion().equals(clientVersion, false)) { final String error = "Client is using " + clientVersion + " but server requires version " + ClientContext.engineVersion().getVersion(); return error; } final String realName = clientName.split(" ")[0]; if (m_serverMessenger.IsUsernameMiniBanned(realName)) { return YOU_HAVE_BEEN_BANNED; } final String remoteIp = ((InetSocketAddress) remoteAddress).getAddress().getHostAddress(); if (m_serverMessenger.IsIpMiniBanned(remoteIp)) { return YOU_HAVE_BEEN_BANNED; } if (hashedMac == null) { return UNABLE_TO_OBTAIN_MAC; } if (hashedMac.length() != 28 || !hashedMac.startsWith(MD5Crypt.MAGIC + "MH$") || !hashedMac.matches("[0-9a-zA-Z$./]+")) { // Must have been tampered with return INVALID_MAC; } if (m_serverMessenger.IsMacMiniBanned(hashedMac)) { return YOU_HAVE_BEEN_BANNED; } if (propertiesSentToClient.get(PASSWORD_REQUIRED_PROPERTY).equals(Boolean.TRUE.toString())) { final String readPassword = propertiesReadFromClient.get(ClientLogin.PASSWORD_PROPERTY); if (readPassword == null) { return "No password"; } if (!readPassword.equals(MD5Crypt.crypt(m_password, propertiesSentToClient.get(SALT_PROPERTY)))) { // sleep on average 2 seconds // try to prevent flooding to guess the password // TODO: verify this prevention, does this protect against parallel connections? ThreadUtil.sleep(4000 * Math.random()); // usage of sleep is okay. return "Invalid password"; } } return null; } }