diff --git a/src/main/java/fr/xephi/authme/AntiBot.java b/src/main/java/fr/xephi/authme/AntiBot.java index 071842bc4..d85cc7843 100644 --- a/src/main/java/fr/xephi/authme/AntiBot.java +++ b/src/main/java/fr/xephi/authme/AntiBot.java @@ -29,8 +29,8 @@ public class AntiBot { private AntiBotStatus antiBotStatus = AntiBotStatus.DISABLED; @Inject - public AntiBot(NewSetting settings, Messages messages, PermissionsManager permissionsManager, - BukkitService bukkitService) { + AntiBot(NewSetting settings, Messages messages, PermissionsManager permissionsManager, + BukkitService bukkitService) { this.settings = settings; this.messages = messages; this.permissionsManager = permissionsManager; @@ -86,7 +86,12 @@ public class AntiBot { }, duration * TICKS_PER_MINUTE); } - public void checkAntiBot(final Player player) { + /** + * Handles a player joining the server and checks if AntiBot needs to be activated. + * + * @param player the player who joined the server + */ + public void handlePlayerJoin(final Player player) { if (antiBotStatus == AntiBotStatus.ACTIVE || antiBotStatus == AntiBotStatus.DISABLED) { return; } diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index 05023a288..9c2124d7a 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -34,7 +34,6 @@ import fr.xephi.authme.output.Log4JFilter; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; import fr.xephi.authme.permission.PermissionsManager; -import fr.xephi.authme.permission.PlayerStatePermission; import fr.xephi.authme.process.Management; import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.crypts.SHA256; @@ -650,16 +649,6 @@ public class AuthMe extends JavaPlugin { return pluginHooks != null && pluginHooks.isNpc(player) || player.hasMetadata("NPC"); } - // Select the player to kick when a vip player joins the server when full - public Player generateKickPlayer(Collection collection) { - for (Player player : collection) { - if (!getPermissionsManager().hasPermission(player, PlayerStatePermission.IS_VIP)) { - return player; - } - } - return null; - } - // Purge inactive players from the database, as defined in the configuration private void runAutoPurge() { if (!newSettings.getProperty(PurgeSettings.USE_AUTO_PURGE) || autoPurging) { diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java index 4a02cfdc5..5c4800edf 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/ConverterCommand.java @@ -1,6 +1,5 @@ package fr.xephi.authme.command.executable.authme; -import fr.xephi.authme.AuthMe; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.converter.Converter; @@ -23,9 +22,6 @@ import java.util.List; */ public class ConverterCommand implements ExecutableCommand { - @Inject - private AuthMe authMe; - @Inject private BukkitService bukkitService; diff --git a/src/main/java/fr/xephi/authme/listener/AuthMeEntityListener.java b/src/main/java/fr/xephi/authme/listener/AuthMeEntityListener.java index 897607eea..f2686f6ad 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMeEntityListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMeEntityListener.java @@ -16,20 +16,24 @@ import org.bukkit.event.entity.FoodLevelChangeEvent; import org.bukkit.event.entity.ProjectileLaunchEvent; import org.bukkit.projectiles.ProjectileSource; +import fr.xephi.authme.ConsoleLogger; + +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import static fr.xephi.authme.listener.ListenerService.shouldCancelEvent; public class AuthMeEntityListener implements Listener { - private static Method getShooter; - private static boolean shooterIsProjectileSource; + private Method getShooter; + private boolean shooterIsProjectileSource; public AuthMeEntityListener() { try { - Method m = Projectile.class.getDeclaredMethod("getShooter"); - shooterIsProjectileSource = m.getReturnType() != LivingEntity.class; - } catch (Exception ignored) { + getShooter = Projectile.class.getDeclaredMethod("getShooter"); + shooterIsProjectileSource = getShooter.getReturnType() != LivingEntity.class; + } catch (NoSuchMethodException | SecurityException e) { + ConsoleLogger.logException("Cannot load getShooter() method on Projectile class", e); } } @@ -87,7 +91,7 @@ public class AuthMeEntityListener implements Listener { } } - // TODO #568: Need to check this, player can't throw snowball but the item is taken. + // TODO #733: Player can't throw snowball but the item is taken. @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onProjectileLaunch(ProjectileLaunchEvent event) { if (event.getEntity() == null) { @@ -96,6 +100,7 @@ public class AuthMeEntityListener implements Listener { Player player = null; Projectile projectile = event.getEntity(); + // In old versions of the Bukkit API getShooter() returns a Player object instead of a ProjectileSource if (shooterIsProjectileSource) { ProjectileSource shooter = projectile.getShooter(); if (shooter == null || !(shooter instanceof Player)) { @@ -103,14 +108,14 @@ public class AuthMeEntityListener implements Listener { } player = (Player) shooter; } else { - // TODO #568 20151220: Invoking getShooter() with null but method isn't static try { if (getShooter == null) { getShooter = Projectile.class.getMethod("getShooter"); } - Object obj = getShooter.invoke(null); + Object obj = getShooter.invoke(projectile); player = (Player) obj; - } catch (Exception ignored) { + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + ConsoleLogger.logException("Error getting shooter", e); } } diff --git a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java index 307c9d31f..a974421a5 100644 --- a/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/AuthMePlayerListener.java @@ -1,32 +1,18 @@ package fr.xephi.authme.listener; -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; import fr.xephi.authme.AntiBot; -import fr.xephi.authme.AntiBot.AntiBotStatus; -import fr.xephi.authme.AuthMe; -import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; -import fr.xephi.authme.cache.auth.PlayerCache; -import fr.xephi.authme.cache.limbo.LimboCache; -import fr.xephi.authme.cache.limbo.LimboPlayer; import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.initialization.Reloadable; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; -import fr.xephi.authme.permission.PermissionsManager; -import fr.xephi.authme.permission.PlayerStatePermission; import fr.xephi.authme.process.Management; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.SpawnLoader; import fr.xephi.authme.settings.properties.HooksSettings; -import fr.xephi.authme.settings.properties.ProtectionSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.util.BukkitService; import fr.xephi.authme.util.Utils; -import fr.xephi.authme.util.ValidationService; -import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -54,27 +40,22 @@ import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerRespawnEvent; import org.bukkit.event.player.PlayerShearEntityEvent; -import javax.annotation.PostConstruct; import javax.inject.Inject; import java.util.Iterator; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.regex.Pattern; import static fr.xephi.authme.listener.ListenerService.shouldCancelEvent; import static fr.xephi.authme.settings.properties.RestrictionSettings.ALLOWED_MOVEMENT_RADIUS; -import static fr.xephi.authme.settings.properties.RestrictionSettings.ALLOW_ALL_COMMANDS_IF_REGISTRATION_IS_OPTIONAL; import static fr.xephi.authme.settings.properties.RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT; /** * Listener class for player events. */ -public class AuthMePlayerListener implements Listener, Reloadable { +public class AuthMePlayerListener implements Listener { public static final ConcurrentHashMap joinMessage = new ConcurrentHashMap<>(); - @Inject - private AuthMe plugin; @Inject private NewSetting settings; @Inject @@ -90,47 +71,23 @@ public class AuthMePlayerListener implements Listener, Reloadable { @Inject private SpawnLoader spawnLoader; @Inject - private ValidationService validationService; - @Inject - private PermissionsManager permissionsManager; - - private Pattern nicknamePattern; - - private void sendLoginOrRegisterMessage(final Player player) { - bukkitService.runTaskAsynchronously(new Runnable() { - @Override - public void run() { - if (dataSource.isAuthAvailable(player.getName().toLowerCase())) { - m.send(player, MessageKey.LOGIN_MESSAGE); - } else { - if (settings.getProperty(RegistrationSettings.USE_EMAIL_REGISTRATION)) { - m.send(player, MessageKey.REGISTER_EMAIL_MESSAGE); - } else { - m.send(player, MessageKey.REGISTER_MESSAGE); - } - } - } - }); - } + private OnJoinVerifier onJoinVerifier; @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) { String cmd = event.getMessage().split(" ")[0].toLowerCase(); - if (settings.getProperty(HooksSettings.USE_ESSENTIALS_MOTD) && "/motd".equals(cmd)) { - return; - } - if (!settings.getProperty(RegistrationSettings.FORCE) - && settings.getProperty(ALLOW_ALL_COMMANDS_IF_REGISTRATION_IS_OPTIONAL)) { + if (settings.getProperty(HooksSettings.USE_ESSENTIALS_MOTD) && cmd.equals("/motd")) { return; } if (settings.getProperty(RestrictionSettings.ALLOW_COMMANDS).contains(cmd)) { return; } - if (Utils.checkAuth(event.getPlayer())) { + final Player player = event.getPlayer(); + if (!shouldCancelEvent(player)) { return; } event.setCancelled(true); - sendLoginOrRegisterMessage(event.getPlayer()); + m.send(player, MessageKey.DENIED_COMMAND); } @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) @@ -142,12 +99,7 @@ public class AuthMePlayerListener implements Listener, Reloadable { final Player player = event.getPlayer(); if (shouldCancelEvent(player)) { event.setCancelled(true); - bukkitService.runTaskAsynchronously(new Runnable() { - @Override - public void run() { - m.send(player, MessageKey.DENIED_CHAT); - } - }); + m.send(player, MessageKey.DENIED_CHAT); } else if (settings.getProperty(RestrictionSettings.HIDE_CHAT)) { Set recipients = event.getRecipients(); Iterator iter = recipients.iterator(); @@ -157,6 +109,9 @@ public class AuthMePlayerListener implements Listener, Reloadable { iter.remove(); } } + if (recipients.size() == 0) { + event.setCancelled(true); + } } } @@ -170,18 +125,21 @@ public class AuthMePlayerListener implements Listener, Reloadable { * Limit player X and Z movements to 1 block * Deny player Y+ movements (allows falling) */ - if (event.getFrom().getBlockX() == event.getTo().getBlockX() - && event.getFrom().getBlockZ() == event.getTo().getBlockZ() - && event.getFrom().getY() - event.getTo().getY() >= 0) { + Location from = event.getFrom(); + Location to = event.getTo(); + if (from.getBlockX() == to.getBlockX() + && from.getBlockZ() == to.getBlockZ() + && from.getY() - to.getY() >= 0) { return; } Player player = event.getPlayer(); - if (Utils.checkAuth(player)) { + if (!shouldCancelEvent(player)) { return; } if (!settings.getProperty(RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT)) { + // "cancel" the event event.setTo(event.getFrom()); if (settings.getProperty(RestrictionSettings.REMOVE_SPEED)) { player.setFlySpeed(0.0f); @@ -234,156 +192,72 @@ public class AuthMePlayerListener implements Listener, Reloadable { @EventHandler(priority = EventPriority.LOW) public void onPlayerJoin(PlayerJoinEvent event) { final Player player = event.getPlayer(); - if (player == null) { - return; + if (player != null) { + // Schedule login task so works after the prelogin + // (Fix found by Koolaid5000) + bukkitService.runTask(new Runnable() { + @Override + public void run() { + management.performJoin(player); + } + }); } - - // Schedule login task so works after the prelogin - // (Fix found by Koolaid5000) - bukkitService.runTask(new Runnable() { - @Override - public void run() { - management.performJoin(player); - } - }); } // Note ljacqu 20160528: AsyncPlayerPreLoginEvent is not fired by all servers in offline mode + // e.g. CraftBukkit does not. So we need to run crucial things in onPlayerLogin, too @EventHandler(priority = EventPriority.HIGHEST) public void onPreLogin(AsyncPlayerPreLoginEvent event) { - PlayerAuth auth = dataSource.getAuth(event.getName()); - if (auth == null && antiBot.getAntiBotStatus() == AntiBotStatus.ACTIVE) { - event.setKickMessage(m.retrieveSingle(MessageKey.KICK_ANTIBOT)); - event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); - antiBot.antibotKicked.addIfAbsent(event.getName()); - return; - } - if (auth == null && settings.getProperty(RestrictionSettings.KICK_NON_REGISTERED)) { - event.setKickMessage(m.retrieveSingle(MessageKey.MUST_REGISTER_MESSAGE)); - event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); - return; - } final String name = event.getName().toLowerCase(); - if (name.length() > settings.getProperty(RestrictionSettings.MAX_NICKNAME_LENGTH) || name.length() < settings.getProperty(RestrictionSettings.MIN_NICKNAME_LENGTH)) { - event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_LENGTH)); - event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); - return; - } - if (settings.getProperty(RegistrationSettings.PREVENT_OTHER_CASE) && auth != null && auth.getRealName() != null) { - String realName = auth.getRealName(); - if (!realName.isEmpty() && !"Player".equals(realName) && !realName.equals(event.getName())) { - event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); - event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_CASE, realName, event.getName())); - return; - } - if (realName.isEmpty() || "Player".equals(realName)) { - dataSource.updateRealName(event.getName().toLowerCase(), event.getName()); - } - } + final boolean isAuthAvailable = dataSource.isAuthAvailable(event.getName()); - if (auth == null && settings.getProperty(ProtectionSettings.ENABLE_PROTECTION)) { - String playerIp = event.getAddress().getHostAddress(); - if (!validationService.isCountryAdmitted(playerIp)) { - event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); - event.setKickMessage(m.retrieveSingle(MessageKey.COUNTRY_BANNED_ERROR)); - return; - } - } - - final Player player = bukkitService.getPlayerExact(name); - // Check if forceSingleSession is set to true, so kick player that has - // joined with same nick of online player - if (player != null && settings.getProperty(RestrictionSettings.FORCE_SINGLE_SESSION)) { + try { + // Potential performance improvement: make checkAntiBot not require `isAuthAvailable` info and use + // "checkKickNonRegistered" as last -> no need to query the DB before checking antibot / name + onJoinVerifier.checkAntibot(name, isAuthAvailable); + onJoinVerifier.checkKickNonRegistered(isAuthAvailable); + onJoinVerifier.checkIsValidName(name); + } catch (FailedVerificationException e) { + event.setKickMessage(m.retrieveSingle(e.getReason(), e.getArgs())); event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); - event.setKickMessage(m.retrieveSingle(MessageKey.USERNAME_ALREADY_ONLINE_ERROR)); - LimboPlayer limbo = LimboCache.getInstance().getLimboPlayer(name); - if (limbo != null && PlayerCache.getInstance().isAuthenticated(name)) { - Utils.addNormal(player, limbo.getGroup()); - LimboCache.getInstance().deleteLimboPlayer(name); - } } } @EventHandler(priority = EventPriority.HIGHEST) public void onPlayerLogin(PlayerLoginEvent event) { final Player player = event.getPlayer(); - if (player == null || Utils.isUnrestricted(player)) { + if (Utils.isUnrestricted(player)) { return; - } - - if (event.getResult() == PlayerLoginEvent.Result.KICK_FULL) { - if (permissionsManager.hasPermission(player, PlayerStatePermission.IS_VIP)) { - int playersOnline = bukkitService.getOnlinePlayers().size(); - if (playersOnline > plugin.getServer().getMaxPlayers()) { - event.allow(); - } else { - Player pl = plugin.generateKickPlayer(bukkitService.getOnlinePlayers()); - if (pl != null) { - pl.kickPlayer(m.retrieveSingle(MessageKey.KICK_FOR_VIP)); - event.allow(); - } else { - ConsoleLogger.info("The player " + event.getPlayer().getName() + " tried to join, but the server was full"); - event.setKickMessage(m.retrieveSingle(MessageKey.KICK_FULL_SERVER)); - event.setResult(PlayerLoginEvent.Result.KICK_FULL); - } - } - } else { - event.setKickMessage(m.retrieveSingle(MessageKey.KICK_FULL_SERVER)); - event.setResult(PlayerLoginEvent.Result.KICK_FULL); - return; - } - } - - if (event.getResult() != PlayerLoginEvent.Result.ALLOWED) { + } else if (onJoinVerifier.refusePlayerForFullServer(event)) { + return; + } else if (event.getResult() != PlayerLoginEvent.Result.ALLOWED) { return; } final String name = player.getName().toLowerCase(); - boolean isAuthAvailable = dataSource.isAuthAvailable(name); + final PlayerAuth auth = dataSource.getAuth(player.getName()); + final boolean isAuthAvailable = (auth != null); - if (antiBot.getAntiBotStatus() == AntiBotStatus.ACTIVE && !isAuthAvailable) { - event.setKickMessage(m.retrieveSingle(MessageKey.KICK_ANTIBOT)); - event.setResult(PlayerLoginEvent.Result.KICK_OTHER); - antiBot.antibotKicked.addIfAbsent(player.getName()); - return; - } - - if (settings.getProperty(RestrictionSettings.KICK_NON_REGISTERED) && !isAuthAvailable) { - event.setKickMessage(m.retrieveSingle(MessageKey.MUST_REGISTER_MESSAGE)); + try { + onJoinVerifier.checkAntibot(name, isAuthAvailable); + onJoinVerifier.checkKickNonRegistered(isAuthAvailable); + onJoinVerifier.checkIsValidName(name); + onJoinVerifier.checkNameCasing(player, auth); + onJoinVerifier.checkSingleSession(player); + onJoinVerifier.checkPlayerCountry(isAuthAvailable, event); + } catch (FailedVerificationException e) { + event.setKickMessage(m.retrieveSingle(e.getReason(), e.getArgs())); event.setResult(PlayerLoginEvent.Result.KICK_OTHER); return; } - if (name.length() > settings.getProperty(RestrictionSettings.MAX_NICKNAME_LENGTH) || name.length() < settings.getProperty(RestrictionSettings.MIN_NICKNAME_LENGTH)) { - event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_LENGTH)); - event.setResult(PlayerLoginEvent.Result.KICK_OTHER); - return; - } - - if (name.equalsIgnoreCase("Player") || !nicknamePattern.matcher(player.getName()).matches()) { - event.setKickMessage(m.retrieveSingle(MessageKey.INVALID_NAME_CHARACTERS) - .replace("REG_EX", nicknamePattern.pattern())); - event.setResult(PlayerLoginEvent.Result.KICK_OTHER); - return; - } - - antiBot.checkAntiBot(player); - - if (settings.getProperty(HooksSettings.BUNGEECORD)) { - ByteArrayDataOutput out = ByteStreams.newDataOutput(); - out.writeUTF("IP"); - player.sendPluginMessage(plugin, "BungeeCord", out.toByteArray()); - } + antiBot.handlePlayerJoin(player); } @EventHandler(priority = EventPriority.HIGHEST) public void onPlayerQuit(PlayerQuitEvent event) { Player player = event.getPlayer(); - if (player == null) { - return; - } - if (settings.getProperty(RegistrationSettings.REMOVE_LEAVE_MESSAGE)) { event.setQuitMessage(null); } @@ -399,18 +273,8 @@ public class AuthMePlayerListener implements Listener, Reloadable { public void onPlayerKick(PlayerKickEvent event) { Player player = event.getPlayer(); - if (player == null) { - return; - } - - if (!settings.getProperty(RestrictionSettings.FORCE_SINGLE_SESSION) - && event.getReason().equals(m.retrieveSingle(MessageKey.USERNAME_ALREADY_ONLINE_ERROR))) { - event.setCancelled(true); - return; - } - if (!antiBot.antibotKicked.contains(player.getName())) { - plugin.getManagement().performQuit(player, true); + management.performQuit(player, true); } } @@ -439,7 +303,7 @@ public class AuthMePlayerListener implements Listener, Reloadable { public void onPlayerInventoryOpen(InventoryOpenEvent event) { final Player player = (Player) event.getPlayer(); - if (!ListenerService.shouldCancelEvent(player)) { + if (!shouldCancelEvent(player)) { return; } event.setCancelled(true); @@ -448,7 +312,7 @@ public class AuthMePlayerListener implements Listener, Reloadable { * @note little hack cause InventoryOpenEvent cannot be cancelled for * real, cause no packet is send to server by client for the main inv */ - Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, new Runnable() { + bukkitService.scheduleSyncDelayedTask(new Runnable() { @Override public void run() { player.closeInventory(); @@ -465,10 +329,7 @@ public class AuthMePlayerListener implements Listener, Reloadable { return; } Player player = (Player) event.getWhoClicked(); - if (Utils.checkAuth(player)) { - return; - } - if (plugin.getPluginHooks().isNpc(player)) { + if (!shouldCancelEvent(player)) { return; } event.setCancelled(true); @@ -476,7 +337,7 @@ public class AuthMePlayerListener implements Listener, Reloadable { @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerHitPlayerEvent(EntityDamageByEntityEvent event) { - if (ListenerService.shouldCancelEvent(event)) { + if (shouldCancelEvent(event)) { event.setCancelled(true); } } @@ -505,11 +366,12 @@ public class AuthMePlayerListener implements Listener, Reloadable { @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onSignChange(SignChangeEvent event) { Player player = event.getPlayer(); - if (ListenerService.shouldCancelEvent(player)) { + if (shouldCancelEvent(player)) { event.setCancelled(true); } } + // TODO: check this, why do we need to update the quit loc? -sgdc3 @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) public void onPlayerRespawn(PlayerRespawnEvent event) { if (settings.getProperty(RestrictionSettings.NO_TELEPORT)) { @@ -548,16 +410,4 @@ public class AuthMePlayerListener implements Listener, Reloadable { } } - @PostConstruct - @Override - public void reload() { - String nickRegEx = settings.getProperty(RestrictionSettings.ALLOWED_NICKNAME_CHARACTERS); - try { - nicknamePattern = Pattern.compile(nickRegEx); - } catch (Exception e) { - nicknamePattern = Pattern.compile(".*?"); - ConsoleLogger.showError("Nickname pattern is not a valid regular expression! " - + "Fallback to allowing all nicknames"); - } - } } diff --git a/src/main/java/fr/xephi/authme/listener/FailedVerificationException.java b/src/main/java/fr/xephi/authme/listener/FailedVerificationException.java new file mode 100644 index 000000000..31957cdb9 --- /dev/null +++ b/src/main/java/fr/xephi/authme/listener/FailedVerificationException.java @@ -0,0 +1,33 @@ +package fr.xephi.authme.listener; + +import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.util.StringUtils; + +/** + * Exception thrown when a verification has failed. + */ +@SuppressWarnings("serial") +public class FailedVerificationException extends Exception { + + private final MessageKey reason; + private final String[] args; + + public FailedVerificationException(MessageKey reason, String... args) { + this.reason = reason; + this.args = args; + } + + public MessageKey getReason() { + return reason; + } + + public String[] getArgs() { + return args; + } + + @Override + public String toString() { + return getClass().getSimpleName() + ": reason=" + (reason == null ? "null" : reason) + + ";args=" + (args == null ? "null" : StringUtils.join(", ", args)); + } +} diff --git a/src/main/java/fr/xephi/authme/listener/OnJoinVerifier.java b/src/main/java/fr/xephi/authme/listener/OnJoinVerifier.java new file mode 100644 index 000000000..64f8c3f7c --- /dev/null +++ b/src/main/java/fr/xephi/authme/listener/OnJoinVerifier.java @@ -0,0 +1,223 @@ +package fr.xephi.authme.listener; + +import fr.xephi.authme.AntiBot; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.cache.auth.PlayerAuth; +import fr.xephi.authme.cache.auth.PlayerCache; +import fr.xephi.authme.cache.limbo.LimboCache; +import fr.xephi.authme.cache.limbo.LimboPlayer; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.initialization.Reloadable; +import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.output.Messages; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.permission.PlayerStatePermission; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.properties.ProtectionSettings; +import fr.xephi.authme.settings.properties.RegistrationSettings; +import fr.xephi.authme.settings.properties.RestrictionSettings; +import fr.xephi.authme.util.BukkitService; +import fr.xephi.authme.util.StringUtils; +import fr.xephi.authme.util.Utils; +import fr.xephi.authme.util.ValidationService; +import org.bukkit.Server; +import org.bukkit.entity.Player; +import org.bukkit.event.player.PlayerLoginEvent; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.util.Collection; +import java.util.regex.Pattern; + +/** + * Service for performing various verifications when a player joins. + */ +class OnJoinVerifier implements Reloadable { + + @Inject + private NewSetting settings; + @Inject + private DataSource dataSource; + @Inject + private Messages messages; + @Inject + private PermissionsManager permissionsManager; + @Inject + private AntiBot antiBot; + @Inject + private ValidationService validationService; + @Inject + private BukkitService bukkitService; + @Inject + private LimboCache limboCache; + @Inject + private Server server; + + private Pattern nicknamePattern; + + OnJoinVerifier() { } + + + @PostConstruct + @Override + public void reload() { + String nickRegEx = settings.getProperty(RestrictionSettings.ALLOWED_NICKNAME_CHARACTERS); + try { + nicknamePattern = Pattern.compile(nickRegEx); + } catch (Exception e) { + nicknamePattern = Pattern.compile(".*?"); + ConsoleLogger.showError("Nickname pattern is not a valid regular expression! " + + "Fallback to allowing all nicknames"); + } + } + + /** + * Checks if Antibot is enabled. + * + * @param playerName the name of the player (lowercase) + * @param isAuthAvailable whether or not the player is registered + */ + public void checkAntibot(String playerName, boolean isAuthAvailable) throws FailedVerificationException { + if (antiBot.getAntiBotStatus() == AntiBot.AntiBotStatus.ACTIVE && !isAuthAvailable) { + antiBot.antibotKicked.addIfAbsent(playerName); + throw new FailedVerificationException(MessageKey.KICK_ANTIBOT); + } + } + + /** + * Checks whether non-registered players should be kicked, and if so, whether the player should be kicked. + * + * @param isAuthAvailable whether or not the player is registered + */ + public void checkKickNonRegistered(boolean isAuthAvailable) throws FailedVerificationException { + if (!isAuthAvailable && settings.getProperty(RestrictionSettings.KICK_NON_REGISTERED)) { + throw new FailedVerificationException(MessageKey.MUST_REGISTER_MESSAGE); + } + } + + /** + * Checks that the name adheres to the configured username restrictions. + * + * @param name the name to verify + */ + public void checkIsValidName(String name) throws FailedVerificationException { + if (name.length() > settings.getProperty(RestrictionSettings.MAX_NICKNAME_LENGTH) + || name.length() < settings.getProperty(RestrictionSettings.MIN_NICKNAME_LENGTH)) { + throw new FailedVerificationException(MessageKey.INVALID_NAME_LENGTH); + } + if (!nicknamePattern.matcher(name).matches()) { + throw new FailedVerificationException(MessageKey.INVALID_NAME_CHARACTERS, nicknamePattern.pattern()); + } + } + + /** + * Handles the case of a full server and verifies if the user's connection should really be refused + * by adjusting the event object accordingly. Attempts to kick a non-VIP player to make room if the + * joining player is a VIP. + * + * @param event the login event to verify + * @return true if the player's connection should be refused (i.e. the event does not need to be processed + * further), false if the player is not refused + */ + public boolean refusePlayerForFullServer(PlayerLoginEvent event) { + final Player player = event.getPlayer(); + if (event.getResult() != PlayerLoginEvent.Result.KICK_FULL) { + // Server is not full, no need to do anything + return false; + } else if (!permissionsManager.hasPermission(player, PlayerStatePermission.IS_VIP)) { + // Server is full and player is NOT VIP; set kick message and proceed with kick + event.setKickMessage(messages.retrieveSingle(MessageKey.KICK_FULL_SERVER)); + return true; + } + + // Server is full and player is VIP; attempt to kick a non-VIP player to make room + Collection onlinePlayers = bukkitService.getOnlinePlayers(); + if (onlinePlayers.size() < server.getMaxPlayers()) { + event.allow(); + return false; + } + Player nonVipPlayer = generateKickPlayer(onlinePlayers); + if (nonVipPlayer != null) { + nonVipPlayer.kickPlayer(messages.retrieveSingle(MessageKey.KICK_FOR_VIP)); + event.allow(); + return false; + } else { + ConsoleLogger.info("VIP player " + player.getName() + " tried to join, but the server was full"); + event.setKickMessage(messages.retrieveSingle(MessageKey.KICK_FULL_SERVER)); + return true; + } + } + + /** + * Checks that the casing in the username corresponds to the one in the database, if so configured. + * + * @param player the player to verify + * @param auth the auth object associated with the player + */ + public void checkNameCasing(Player player, PlayerAuth auth) throws FailedVerificationException { + if (auth != null && settings.getProperty(RegistrationSettings.PREVENT_OTHER_CASE)) { + String realName = auth.getRealName(); // might be null or "Player" + String connectingName = player.getName(); + + if (StringUtils.isEmpty(realName) || "Player".equals(realName)) { + dataSource.updateRealName(connectingName.toLowerCase(), connectingName); + } else if (!realName.equals(connectingName)) { + throw new FailedVerificationException(MessageKey.INVALID_NAME_CASE, realName, connectingName); + } + } + } + + /** + * Checks that the player's country is admitted if he is not registered. + * + * @param isAuthAvailable whether or not the user is registered + * @param event the login event of the player + */ + public void checkPlayerCountry(boolean isAuthAvailable, + PlayerLoginEvent event) throws FailedVerificationException { + if (!isAuthAvailable && settings.getProperty(ProtectionSettings.ENABLE_PROTECTION)) { + String playerIp = event.getAddress().getHostAddress(); + if (!validationService.isCountryAdmitted(playerIp)) { + throw new FailedVerificationException(MessageKey.COUNTRY_BANNED_ERROR); + } + } + } + + /** + * Checks if a player with the same name (case-insensitive) is already playing and refuses the + * connection if so configured. + * + * @param player the player to verify + */ + public void checkSingleSession(Player player) throws FailedVerificationException { + if (!settings.getProperty(RestrictionSettings.FORCE_SINGLE_SESSION)) { + return; + } + + Player onlinePlayer = bukkitService.getPlayerExact(player.getName()); + if (onlinePlayer != null) { + String name = player.getName().toLowerCase(); + LimboPlayer limbo = limboCache.getLimboPlayer(name); + if (limbo != null && PlayerCache.getInstance().isAuthenticated(name)) { + Utils.addNormal(player, limbo.getGroup()); + limboCache.deleteLimboPlayer(name); + } + throw new FailedVerificationException(MessageKey.USERNAME_ALREADY_ONLINE_ERROR); + } + } + + /** + * Selects a non-VIP player to kick when a VIP player joins the server when full. + * + * @param onlinePlayers list of online players + * @return the player to kick, or null if none applicable + */ + private Player generateKickPlayer(Collection onlinePlayers) { + for (Player player : onlinePlayers) { + if (!permissionsManager.hasPermission(player, PlayerStatePermission.IS_VIP)) { + return player; + } + } + return null; + } +} diff --git a/src/main/java/fr/xephi/authme/output/MessageKey.java b/src/main/java/fr/xephi/authme/output/MessageKey.java index 0f052c27a..eb7a52381 100644 --- a/src/main/java/fr/xephi/authme/output/MessageKey.java +++ b/src/main/java/fr/xephi/authme/output/MessageKey.java @@ -5,6 +5,8 @@ package fr.xephi.authme.output; */ public enum MessageKey { + DENIED_COMMAND("denied_command"), + SAME_IP_ONLINE("same_ip_online"), DENIED_CHAT("denied_chat"), diff --git a/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java b/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java index 7a9fd7cd3..3fd7b40b1 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java @@ -23,12 +23,6 @@ public class RestrictionSettings implements SettingsClass { public static final Property HIDE_CHAT = newProperty("settings.restrictions.hideChat", false); - @Comment({ - "Allow unlogged users to use all the commands if registration is not forced!", - "WARNING: use this only if you need it!"}) - public static final Property ALLOW_ALL_COMMANDS_IF_REGISTRATION_IS_OPTIONAL = - newProperty("settings.restrictions.allowAllCommandsIfRegistrationIsOptional", false); - @Comment("Allowed commands for unauthenticated players") public static final Property> ALLOW_COMMANDS = newLowercaseListProperty("settings.restrictions.allowCommands", diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 3cb5f23fa..abf71dafb 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -69,9 +69,6 @@ settings: allowChat: false # Can not authenticated players see the chat log? hideChat: false - # WARNING: use this only if you need it! - # Allow unlogged users to use all the commands if registration is not forced! - allowAllCommandsIfRegistrationIsOptional: false # Commands allowed when a player is not authenticated allowCommands: - /login diff --git a/src/main/resources/messages/messages_en.yml b/src/main/resources/messages/messages_en.yml index 2623ac0b9..75a4fcead 100644 --- a/src/main/resources/messages/messages_en.yml +++ b/src/main/resources/messages/messages_en.yml @@ -1,3 +1,4 @@ +denied_command: '&cIn order to be able to use this command you must be authenticated!' same_ip_online: 'A player with the same IP is already in game!' denied_chat: '&cIn order to be able to chat you must be authenticated!' kick_antibot: 'AntiBot protection mode is enabled! You have to wait some minutes before joining the server.' diff --git a/src/test/java/fr/xephi/authme/AntiBotTest.java b/src/test/java/fr/xephi/authme/AntiBotTest.java index 9950963ab..ddf418314 100644 --- a/src/test/java/fr/xephi/authme/AntiBotTest.java +++ b/src/test/java/fr/xephi/authme/AntiBotTest.java @@ -164,7 +164,7 @@ public class AntiBotTest { AntiBot antiBot = createListeningAntiBot(); // when - antiBot.checkAntiBot(player); + antiBot.handlePlayerJoin(player); // then @SuppressWarnings("unchecked") @@ -194,7 +194,7 @@ public class AntiBotTest { AntiBot antiBot = createListeningAntiBot(); // when - antiBot.checkAntiBot(player); + antiBot.handlePlayerJoin(player); // then @SuppressWarnings("rawtypes") diff --git a/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java b/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java new file mode 100644 index 000000000..eb7e15112 --- /dev/null +++ b/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java @@ -0,0 +1,419 @@ +package fr.xephi.authme.listener; + +import fr.xephi.authme.AntiBot; +import fr.xephi.authme.TestHelper; +import fr.xephi.authme.cache.auth.PlayerAuth; +import fr.xephi.authme.cache.limbo.LimboCache; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.output.MessageKey; +import fr.xephi.authme.output.Messages; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.permission.PlayerStatePermission; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.properties.RegistrationSettings; +import fr.xephi.authme.settings.properties.RestrictionSettings; +import fr.xephi.authme.util.BukkitService; +import fr.xephi.authme.util.StringUtils; +import fr.xephi.authme.util.ValidationService; +import org.bukkit.Server; +import org.bukkit.entity.Player; +import org.bukkit.event.player.PlayerLoginEvent; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link OnJoinVerifier}. + */ +@RunWith(MockitoJUnitRunner.class) +public class OnJoinVerifierTest { + + @InjectMocks + private OnJoinVerifier onJoinVerifier; + + @Mock + private NewSetting settings; + @Mock + private DataSource dataSource; + @Mock + private Messages messages; + @Mock + private PermissionsManager permissionsManager; + @Mock + private AntiBot antiBot; + @Mock + private ValidationService validationService; + @Mock + private BukkitService bukkitService; + @Mock + private LimboCache limboCache; + @Mock + private Server server; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @BeforeClass + public static void setUpLogger() { + TestHelper.setupLogger(); + } + + @Test + public void shouldNotDoAnythingForNormalEvent() { + // given + PlayerLoginEvent event = mock(PlayerLoginEvent.class); + given(event.getResult()).willReturn(PlayerLoginEvent.Result.ALLOWED); + + // when + boolean result = onJoinVerifier.refusePlayerForFullServer(event); + + // then + assertThat(result, equalTo(false)); + verify(event).getResult(); + verifyNoMoreInteractions(event); + verifyZeroInteractions(bukkitService); + verifyZeroInteractions(dataSource); + verifyZeroInteractions(permissionsManager); + } + + @Test + public void shouldRefuseNonVipPlayerForFullServer() { + // given + Player player = mock(Player.class); + PlayerLoginEvent event = new PlayerLoginEvent(player, "hostname", null); + event.setResult(PlayerLoginEvent.Result.KICK_FULL); + given(permissionsManager.hasPermission(player, PlayerStatePermission.IS_VIP)).willReturn(false); + String serverFullMessage = "server is full"; + given(messages.retrieveSingle(MessageKey.KICK_FULL_SERVER)).willReturn(serverFullMessage); + + // when + boolean result = onJoinVerifier.refusePlayerForFullServer(event); + + // then + assertThat(result, equalTo(true)); + assertThat(event.getResult(), equalTo(PlayerLoginEvent.Result.KICK_FULL)); + assertThat(event.getKickMessage(), equalTo(serverFullMessage)); + verifyZeroInteractions(bukkitService); + verifyZeroInteractions(dataSource); + } + + @Test + public void shouldKickNonVipForJoiningVipPlayer() { + // given + Player player = mock(Player.class); + PlayerLoginEvent event = new PlayerLoginEvent(player, "hostname", null); + event.setResult(PlayerLoginEvent.Result.KICK_FULL); + given(permissionsManager.hasPermission(player, PlayerStatePermission.IS_VIP)).willReturn(true); + List onlinePlayers = Arrays.asList(mock(Player.class), mock(Player.class)); + given(permissionsManager.hasPermission(onlinePlayers.get(0), PlayerStatePermission.IS_VIP)).willReturn(true); + given(permissionsManager.hasPermission(onlinePlayers.get(1), PlayerStatePermission.IS_VIP)).willReturn(false); + returnOnlineListFromBukkitServer(onlinePlayers); + given(server.getMaxPlayers()).willReturn(onlinePlayers.size()); + given(messages.retrieveSingle(MessageKey.KICK_FOR_VIP)).willReturn("kick for vip"); + + // when + boolean result = onJoinVerifier.refusePlayerForFullServer(event); + + // then + assertThat(result, equalTo(false)); + assertThat(event.getResult(), equalTo(PlayerLoginEvent.Result.ALLOWED)); + // First player is VIP, so expect no interactions there and second player to have been kicked + verifyZeroInteractions(onlinePlayers.get(0)); + verify(onlinePlayers.get(1)).kickPlayer("kick for vip"); + } + + @Test + public void shouldKickVipPlayerIfNoPlayerCanBeKicked() { + // given + Player player = mock(Player.class); + PlayerLoginEvent event = new PlayerLoginEvent(player, "hostname", null); + event.setResult(PlayerLoginEvent.Result.KICK_FULL); + given(permissionsManager.hasPermission(player, PlayerStatePermission.IS_VIP)).willReturn(true); + List onlinePlayers = Collections.singletonList(mock(Player.class)); + given(permissionsManager.hasPermission(onlinePlayers.get(0), PlayerStatePermission.IS_VIP)).willReturn(true); + returnOnlineListFromBukkitServer(onlinePlayers); + given(server.getMaxPlayers()).willReturn(onlinePlayers.size()); + given(messages.retrieveSingle(MessageKey.KICK_FULL_SERVER)).willReturn("kick full server"); + + // when + boolean result = onJoinVerifier.refusePlayerForFullServer(event); + + // then + assertThat(result, equalTo(true)); + assertThat(event.getResult(), equalTo(PlayerLoginEvent.Result.KICK_FULL)); + assertThat(event.getKickMessage(), equalTo("kick full server")); + verifyZeroInteractions(onlinePlayers.get(0)); + } + + @Test + public void shouldKickNonRegistered() throws FailedVerificationException { + // given + given(settings.getProperty(RestrictionSettings.KICK_NON_REGISTERED)).willReturn(true); + + // expect + expectValidationExceptionWith(MessageKey.MUST_REGISTER_MESSAGE); + + // when + onJoinVerifier.checkKickNonRegistered(false); + } + + @Test + public void shouldNotKickRegisteredPlayer() throws FailedVerificationException { + // given + given(settings.getProperty(RestrictionSettings.KICK_NON_REGISTERED)).willReturn(true); + + // when + onJoinVerifier.checkKickNonRegistered(true); + } + + @Test + public void shouldNotKickUnregisteredPlayer() throws FailedVerificationException { + // given + given(settings.getProperty(RestrictionSettings.KICK_NON_REGISTERED)).willReturn(false); + + // when + onJoinVerifier.checkKickNonRegistered(false); + } + + @Test + public void shouldAllowValidName() throws FailedVerificationException { + // given + given(settings.getProperty(RestrictionSettings.MIN_NICKNAME_LENGTH)).willReturn(4); + given(settings.getProperty(RestrictionSettings.MAX_NICKNAME_LENGTH)).willReturn(8); + given(settings.getProperty(RestrictionSettings.ALLOWED_NICKNAME_CHARACTERS)).willReturn("[a-zA-Z0-9]+"); + onJoinVerifier.reload(); // @PostConstruct method + + // when + onJoinVerifier.checkIsValidName("Bobby5"); + } + + @Test + public void shouldRejectTooLongName() throws FailedVerificationException { + // given + given(settings.getProperty(RestrictionSettings.MIN_NICKNAME_LENGTH)).willReturn(4); + given(settings.getProperty(RestrictionSettings.MAX_NICKNAME_LENGTH)).willReturn(8); + given(settings.getProperty(RestrictionSettings.ALLOWED_NICKNAME_CHARACTERS)).willReturn("[a-zA-Z0-9]+"); + onJoinVerifier.reload(); // @PostConstruct method + + // expect + expectValidationExceptionWith(MessageKey.INVALID_NAME_LENGTH); + + // when + onJoinVerifier.checkIsValidName("longerthaneight"); + } + + @Test + public void shouldRejectTooShortName() throws FailedVerificationException { + // given + given(settings.getProperty(RestrictionSettings.MIN_NICKNAME_LENGTH)).willReturn(4); + given(settings.getProperty(RestrictionSettings.MAX_NICKNAME_LENGTH)).willReturn(8); + given(settings.getProperty(RestrictionSettings.ALLOWED_NICKNAME_CHARACTERS)).willReturn("[a-zA-Z0-9]+"); + onJoinVerifier.reload(); // @PostConstruct method + + // expect + expectValidationExceptionWith(MessageKey.INVALID_NAME_LENGTH); + + // when + onJoinVerifier.checkIsValidName("abc"); + } + + @Test + public void shouldRejectNameWithInvalidCharacters() throws FailedVerificationException { + // given + given(settings.getProperty(RestrictionSettings.MIN_NICKNAME_LENGTH)).willReturn(4); + given(settings.getProperty(RestrictionSettings.MAX_NICKNAME_LENGTH)).willReturn(8); + given(settings.getProperty(RestrictionSettings.ALLOWED_NICKNAME_CHARACTERS)).willReturn("[a-zA-Z0-9]+"); + onJoinVerifier.reload(); // @PostConstruct method + + // expect + expectValidationExceptionWith(MessageKey.INVALID_NAME_CHARACTERS, "[a-zA-Z0-9]+"); + + // when + onJoinVerifier.checkIsValidName("Tester!"); + } + + @Test + public void shouldAllowProperlyCasedName() throws FailedVerificationException { + // given + Player player = newPlayerWithName("Bobby"); + PlayerAuth auth = PlayerAuth.builder().name("bobby").realName("Bobby").build(); + given(settings.getProperty(RegistrationSettings.PREVENT_OTHER_CASE)).willReturn(true); + + // when + onJoinVerifier.checkNameCasing(player, auth); + + // then + verifyZeroInteractions(dataSource); + } + + @Test + public void shouldRejectNameWithWrongCasing() throws FailedVerificationException { + // given + Player player = newPlayerWithName("Tester"); + PlayerAuth auth = PlayerAuth.builder().name("tester").realName("testeR").build(); + given(settings.getProperty(RegistrationSettings.PREVENT_OTHER_CASE)).willReturn(true); + + // expect + expectValidationExceptionWith(MessageKey.INVALID_NAME_CASE, "testeR", "Tester"); + + // when / then + onJoinVerifier.checkNameCasing(player, auth); + verifyZeroInteractions(dataSource); + } + + @Test + public void shouldUpdateMissingRealName() throws FailedVerificationException { + // given + Player player = newPlayerWithName("Authme"); + PlayerAuth auth = PlayerAuth.builder().name("authme").realName("").build(); + given(settings.getProperty(RegistrationSettings.PREVENT_OTHER_CASE)).willReturn(true); + + // when + onJoinVerifier.checkNameCasing(player, auth); + + // then + verify(dataSource).updateRealName("authme", "Authme"); + } + + @Test + public void shouldUpdateDefaultRealName() throws FailedVerificationException { + // given + Player player = newPlayerWithName("SOMEONE"); + PlayerAuth auth = PlayerAuth.builder().name("someone").realName("Player").build(); + given(settings.getProperty(RegistrationSettings.PREVENT_OTHER_CASE)).willReturn(true); + + // when + onJoinVerifier.checkNameCasing(player, auth); + + // then + verify(dataSource).updateRealName("someone", "SOMEONE"); + } + + @Test + public void shouldAcceptCasingMismatchForDisabledSetting() throws FailedVerificationException { + // given + Player player = newPlayerWithName("Test"); + PlayerAuth auth = PlayerAuth.builder().name("test").realName("TEST").build(); + given(settings.getProperty(RegistrationSettings.PREVENT_OTHER_CASE)).willReturn(false); + + // when + onJoinVerifier.checkNameCasing(player, auth); + + // then + verifyZeroInteractions(dataSource); + } + + @Test + public void shouldAcceptNameForUnregisteredAccount() throws FailedVerificationException { + // given + Player player = newPlayerWithName("MyPlayer"); + PlayerAuth auth = null; + given(settings.getProperty(RegistrationSettings.PREVENT_OTHER_CASE)).willReturn(true); + + // when + onJoinVerifier.checkNameCasing(player, auth); + + // then + verifyZeroInteractions(dataSource); + } + + @Test + public void shouldAcceptNameThatIsNotOnline() throws FailedVerificationException { + // given + Player player = newPlayerWithName("bobby"); + given(settings.getProperty(RestrictionSettings.FORCE_SINGLE_SESSION)).willReturn(true); + given(bukkitService.getPlayerExact("bobby")).willReturn(null); + + // when + onJoinVerifier.checkSingleSession(player); + + // then + verifyZeroInteractions(limboCache); + } + + @Test + public void shouldRejectNameAlreadyOnline() throws FailedVerificationException { + // given + Player player = newPlayerWithName("Charlie"); + Player onlinePlayer = newPlayerWithName("charlie"); + given(bukkitService.getPlayerExact("Charlie")).willReturn(onlinePlayer); + given(settings.getProperty(RestrictionSettings.FORCE_SINGLE_SESSION)).willReturn(true); + + // expect + expectValidationExceptionWith(MessageKey.USERNAME_ALREADY_ONLINE_ERROR); + + // when / then + onJoinVerifier.checkSingleSession(player); + verify(limboCache).getLimboPlayer("charlie"); + } + + @Test + public void shouldAcceptAlreadyOnlineNameForDisabledSetting() throws FailedVerificationException { + // given + Player player = newPlayerWithName("Felipe"); + given(settings.getProperty(RestrictionSettings.FORCE_SINGLE_SESSION)).willReturn(false); + + // when + onJoinVerifier.checkSingleSession(player); + + // then + verifyZeroInteractions(bukkitService); + verifyZeroInteractions(limboCache); + } + + private static Player newPlayerWithName(String name) { + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + return player; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private void returnOnlineListFromBukkitServer(Collection onlineList) { + // Note ljacqu 20160529: The compiler gets lost in generics because Collection is returned + // from getOnlinePlayers(). We need to uncheck onlineList to a simple Collection or it will refuse to compile. + given(bukkitService.getOnlinePlayers()).willReturn((Collection) onlineList); + } + + private void expectValidationExceptionWith(MessageKey messageKey, String... args) { + //expectedException.expect(FailedVerificationException.class); + expectedException.expect(exceptionWithData(messageKey, args)); + } + + private static Matcher exceptionWithData(final MessageKey messageKey, + final String... args) { + return new TypeSafeMatcher() { + @Override + protected boolean matchesSafely(FailedVerificationException item) { + return messageKey.equals(item.getReason()) && Arrays.equals(args, item.getArgs()); + } + + @Override + public void describeTo(Description description) { + description.appendValue("VerificationFailedException: reason=" + messageKey + ";args=" + + (args == null ? "null" : StringUtils.join(", ", args))); + } + }; + } + +}