diff --git a/src/main/java/fr/xephi/authme/data/auth/PlayerCache.java b/src/main/java/fr/xephi/authme/data/auth/PlayerCache.java index 46fafbf77..7c21ee4b3 100644 --- a/src/main/java/fr/xephi/authme/data/auth/PlayerCache.java +++ b/src/main/java/fr/xephi/authme/data/auth/PlayerCache.java @@ -10,6 +10,7 @@ public class PlayerCache { private final Map cache = new ConcurrentHashMap<>(); + private final Map registeredCache = new ConcurrentHashMap<>(); PlayerCache() { } @@ -20,6 +21,7 @@ public class PlayerCache { * @param auth the player auth object to save */ public void updatePlayer(PlayerAuth auth) { + registeredCache.put(auth.getNickname().toLowerCase(), RegistrationStatus.REGISTERED); cache.put(auth.getNickname().toLowerCase(), auth); } @@ -30,6 +32,7 @@ public void updatePlayer(PlayerAuth auth) { */ public void removePlayer(String user) { cache.remove(user.toLowerCase()); + registeredCache.remove(user.toLowerCase()); } /** @@ -43,6 +46,35 @@ public boolean isAuthenticated(String user) { return cache.containsKey(user.toLowerCase()); } + /** + * Add a registration entry to the cache for active use later like the player active playing. + * + * @param user player name + * @param status registration status + */ + public void addRegistrationStatus(String user, RegistrationStatus status) { + registeredCache.put(user.toLowerCase(), status); + } + + /** + * Update the status for existing entries like currently active users + * @param user player name + * @param status newest query result + */ + public void updateRegistrationStatus(String user, RegistrationStatus status) { + registeredCache.replace(user, status); + } + + /** + * Checks if there is cached result with the player having an account. + * Warning: This shouldn't be used for authentication, because the result could be outdated. + * @param user player name + * @return Cached result about being registered or unregistered and UNKNOWN if there is no cache entry + */ + public RegistrationStatus getRegistrationStatus(String user) { + return registeredCache.getOrDefault(user.toLowerCase(), RegistrationStatus.UNKNOWN); + } + /** * Returns the PlayerAuth associated with the given user, if available. * @@ -66,8 +98,13 @@ public int getLogged() { * * @return all player auths inside the player cache */ - public Map getCache() { + public Map getAuthCache() { return this.cache; } + public enum RegistrationStatus { + REGISTERED, + UNREGISTERED, + UNKNOWN + } } diff --git a/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java b/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java index e1418dcaf..9b4fad54a 100644 --- a/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java +++ b/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java @@ -267,7 +267,7 @@ public List getAllAuths() { @Override public List getLoggedPlayersWithEmptyMail() { - return playerCache.getCache().values().stream() + return playerCache.getAuthCache().values().stream() .filter(auth -> Utils.isEmailEmpty(auth.getEmail())) .map(PlayerAuth::getRealName) .collect(Collectors.toList()); diff --git a/src/main/java/fr/xephi/authme/listener/ListenerService.java b/src/main/java/fr/xephi/authme/listener/ListenerService.java index d283f3e4e..866a98c36 100644 --- a/src/main/java/fr/xephi/authme/listener/ListenerService.java +++ b/src/main/java/fr/xephi/authme/listener/ListenerService.java @@ -1,6 +1,7 @@ package fr.xephi.authme.listener; import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.data.auth.PlayerCache.RegistrationStatus; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.initialization.SettingsDependent; import fr.xephi.authme.service.ValidationService; @@ -17,7 +18,7 @@ /** * Service class for the AuthMe listeners to determine whether an event should be canceled. */ -class ListenerService implements SettingsDependent { +public class ListenerService implements SettingsDependent { private final DataSource dataSource; private final PlayerCache playerCache; @@ -77,28 +78,34 @@ public boolean shouldCancelEvent(PlayerEvent event) { * @return true if the associated event should be canceled, false otherwise */ public boolean shouldCancelEvent(Player player) { - return player != null && !checkAuth(player.getName()) && !PlayerUtils.isNpc(player); - } - - @Override - public void reload(Settings settings) { - isRegistrationForced = settings.getProperty(RegistrationSettings.FORCE); + return player != null && !PlayerUtils.isNpc(player) && shouldRestrictPlayer(player.getName()); } /** - * Checks whether the player is allowed to perform actions (i.e. whether he is logged in - * or if other settings permit playing). + * Check if restriction are required for the given player name. The check will be performed against the local + * cache. This means changes from other sources like web services will have a delay to it. * - * @param name the name of the player to verify - * @return true if the player may play, false otherwise + * @param name player name + * @return true if the player needs to be restricted */ - private boolean checkAuth(String name) { + public boolean shouldRestrictPlayer(String name) { if (validationService.isUnrestricted(name) || playerCache.isAuthenticated(name)) { - return true; + return false; } - if (!isRegistrationForced && !dataSource.isAuthAvailable(name)) { + + if (isRegistrationForced) { + // registration always required to play - so restrict everything return true; } - return false; + + // registration not enforced, but registered players needs to be restricted if not logged in + // if there is no data fall back to safer alternative to prevent any leakage + final RegistrationStatus status = playerCache.getRegistrationStatus(name); + return status != RegistrationStatus.UNREGISTERED; + } + + @Override + public void reload(Settings settings) { + isRegistrationForced = settings.getProperty(RegistrationSettings.FORCE); } } diff --git a/src/main/java/fr/xephi/authme/listener/protocollib/InventoryPacketAdapter.java b/src/main/java/fr/xephi/authme/listener/protocollib/InventoryPacketAdapter.java index bceeaca7a..eded77c5b 100644 --- a/src/main/java/fr/xephi/authme/listener/protocollib/InventoryPacketAdapter.java +++ b/src/main/java/fr/xephi/authme/listener/protocollib/InventoryPacketAdapter.java @@ -24,20 +24,21 @@ import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.reflect.StructureModifier; + import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.data.auth.PlayerCache; -import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.listener.ListenerService; import fr.xephi.authme.output.ConsoleLoggerFactory; import fr.xephi.authme.service.BukkitService; -import org.bukkit.Material; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; import java.lang.reflect.InvocationTargetException; import java.util.Arrays; import java.util.List; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + class InventoryPacketAdapter extends PacketAdapter { private static final int PLAYER_INVENTORY = 0; @@ -49,13 +50,12 @@ class InventoryPacketAdapter extends PacketAdapter { private static final int HOTBAR_SIZE = 9; private final ConsoleLogger logger = ConsoleLoggerFactory.get(InventoryPacketAdapter.class); - private final PlayerCache playerCache; - private final DataSource dataSource; - InventoryPacketAdapter(AuthMe plugin, PlayerCache playerCache, DataSource dataSource) { + private final ListenerService listenerService; + + InventoryPacketAdapter(AuthMe plugin, ListenerService listenerService) { super(plugin, PacketType.Play.Server.SET_SLOT, PacketType.Play.Server.WINDOW_ITEMS); - this.playerCache = playerCache; - this.dataSource = dataSource; + this.listenerService = listenerService; } @Override @@ -64,7 +64,7 @@ public void onPacketSending(PacketEvent packetEvent) { PacketContainer packet = packetEvent.getPacket(); int windowId = packet.getIntegers().read(0); - if (windowId == PLAYER_INVENTORY && shouldHideInventory(player.getName())) { + if (windowId == PLAYER_INVENTORY && listenerService.shouldRestrictPlayer(player.getName())) { packetEvent.setCancelled(true); } } @@ -78,14 +78,10 @@ public void register(BukkitService bukkitService) { ProtocolLibrary.getProtocolManager().addPacketListener(this); bukkitService.getOnlinePlayers().stream() - .filter(player -> shouldHideInventory(player.getName())) + .filter(player -> listenerService.shouldRestrictPlayer(player.getName())) .forEach(this::sendBlankInventoryPacket); } - private boolean shouldHideInventory(String playerName) { - return !playerCache.isAuthenticated(playerName) && dataSource.isAuthAvailable(playerName); - } - public void unregister() { ProtocolLibrary.getProtocolManager().removePacketListener(this); } diff --git a/src/main/java/fr/xephi/authme/listener/protocollib/ProtocolLibService.java b/src/main/java/fr/xephi/authme/listener/protocollib/ProtocolLibService.java index 024077570..8399c189a 100644 --- a/src/main/java/fr/xephi/authme/listener/protocollib/ProtocolLibService.java +++ b/src/main/java/fr/xephi/authme/listener/protocollib/ProtocolLibService.java @@ -1,19 +1,21 @@ package fr.xephi.authme.listener.protocollib; import ch.jalu.injector.annotations.NoFieldScan; + import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.auth.PlayerCache; -import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.initialization.SettingsDependent; +import fr.xephi.authme.listener.ListenerService; import fr.xephi.authme.output.ConsoleLoggerFactory; import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.RestrictionSettings; -import org.bukkit.entity.Player; import javax.inject.Inject; +import org.bukkit.entity.Player; + @NoFieldScan public class ProtocolLibService implements SettingsDependent { @@ -31,16 +33,16 @@ public class ProtocolLibService implements SettingsDependent { private boolean isEnabled; private final AuthMe plugin; private final BukkitService bukkitService; + private final ListenerService listenerService; private final PlayerCache playerCache; - private final DataSource dataSource; @Inject - ProtocolLibService(AuthMe plugin, Settings settings, BukkitService bukkitService, PlayerCache playerCache, - DataSource dataSource) { + ProtocolLibService(AuthMe plugin, Settings settings, BukkitService bukkitService, ListenerService listenerService, + PlayerCache playerCache) { this.plugin = plugin; this.bukkitService = bukkitService; + this.listenerService = listenerService; this.playerCache = playerCache; - this.dataSource = dataSource; reload(settings); } @@ -66,7 +68,7 @@ public void setup() { if (protectInvBeforeLogin) { if (inventoryPacketAdapter == null) { // register the packet listener and start hiding it for all already online players (reload) - inventoryPacketAdapter = new InventoryPacketAdapter(plugin, playerCache, dataSource); + inventoryPacketAdapter = new InventoryPacketAdapter(plugin, listenerService); inventoryPacketAdapter.register(bukkitService); } } else if (inventoryPacketAdapter != null) { @@ -76,7 +78,7 @@ public void setup() { if (denyTabCompleteBeforeLogin) { if (tabCompletePacketAdapter == null) { - tabCompletePacketAdapter = new TabCompletePacketAdapter(plugin, playerCache); + tabCompletePacketAdapter = new TabCompletePacketAdapter(plugin, listenerService); tabCompletePacketAdapter.register(); } } else if (tabCompletePacketAdapter != null) { @@ -118,8 +120,8 @@ public void sendBlankInventoryPacket(Player player) { public void reload(Settings settings) { boolean oldProtectInventory = this.protectInvBeforeLogin; - this.protectInvBeforeLogin = settings.getProperty(RestrictionSettings.PROTECT_INVENTORY_BEFORE_LOGIN); this.denyTabCompleteBeforeLogin = settings.getProperty(RestrictionSettings.DENY_TABCOMPLETE_BEFORE_LOGIN); + this.protectInvBeforeLogin = settings.getProperty(RestrictionSettings.PROTECT_INVENTORY_BEFORE_LOGIN); //it was true and will be deactivated now, so we need to restore the inventory for every player if (oldProtectInventory && !protectInvBeforeLogin && inventoryPacketAdapter != null) { diff --git a/src/main/java/fr/xephi/authme/listener/protocollib/TabCompletePacketAdapter.java b/src/main/java/fr/xephi/authme/listener/protocollib/TabCompletePacketAdapter.java index 3f0bb3161..4a67f550e 100644 --- a/src/main/java/fr/xephi/authme/listener/protocollib/TabCompletePacketAdapter.java +++ b/src/main/java/fr/xephi/authme/listener/protocollib/TabCompletePacketAdapter.java @@ -8,24 +8,26 @@ import com.comphenix.protocol.reflect.FieldAccessException; import fr.xephi.authme.AuthMe; import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.listener.ListenerService; import fr.xephi.authme.output.ConsoleLoggerFactory; class TabCompletePacketAdapter extends PacketAdapter { private final ConsoleLogger logger = ConsoleLoggerFactory.get(TabCompletePacketAdapter.class); - private final PlayerCache playerCache; - TabCompletePacketAdapter(AuthMe plugin, PlayerCache playerCache) { + private final ListenerService listenerService; + + TabCompletePacketAdapter(AuthMe plugin, ListenerService listenerService) { super(plugin, ListenerPriority.NORMAL, PacketType.Play.Client.TAB_COMPLETE); - this.playerCache = playerCache; + this.listenerService = listenerService; } @Override public void onPacketReceiving(PacketEvent event) { if (event.getPacketType() == PacketType.Play.Client.TAB_COMPLETE) { try { - if (!playerCache.isAuthenticated(event.getPlayer().getName())) { + String playerName = event.getPlayer().getName(); + if (listenerService.shouldRestrictPlayer(playerName)) { event.setCancelled(true); } } catch (FieldAccessException e) { diff --git a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index 7b16e564a..ee54d6381 100644 --- a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -2,6 +2,8 @@ import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.ProxySessionManager; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.data.auth.PlayerCache.RegistrationStatus; import fr.xephi.authme.data.limbo.LimboService; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.events.ProtectInventoryEvent; @@ -73,6 +75,9 @@ public class AsynchronousJoin implements AsynchronousProcess { @Inject private SessionService sessionService; + @Inject + private PlayerCache playerCache; + @Inject private ProxySessionManager proxySessionManager; @@ -112,17 +117,10 @@ public void processJoin(final Player player) { } final boolean isAuthAvailable = database.isAuthAvailable(name); - + RegistrationStatus status = isAuthAvailable ? RegistrationStatus.REGISTERED : RegistrationStatus.UNREGISTERED; + playerCache.addRegistrationStatus(name, status); if (isAuthAvailable) { - // Protect inventory - if (service.getProperty(PROTECT_INVENTORY_BEFORE_LOGIN)) { - ProtectInventoryEvent ev = bukkitService.createAndCallEvent( - isAsync -> new ProtectInventoryEvent(player, isAsync)); - if (ev.isCancelled()) { - player.updateInventory(); - logger.fine("ProtectInventoryEvent has been cancelled for " + player.getName() + "..."); - } - } + protectInventory(player); // Session logic if (sessionService.canResumeSession(player)) { @@ -154,6 +152,18 @@ public void processJoin(final Player player) { processJoinSync(player, isAuthAvailable); } + private void protectInventory(Player player) { + // Protect inventory + if (service.getProperty(PROTECT_INVENTORY_BEFORE_LOGIN)) { + ProtectInventoryEvent ev = bukkitService.createAndCallEvent( + isAsync -> new ProtectInventoryEvent(player, isAsync)); + if (ev.isCancelled()) { + player.updateInventory(); + logger.fine("ProtectInventoryEvent has been cancelled for " + player.getName() + "..."); + } + } + } + private void handlePlayerWithUnmetNameRestriction(Player player, String ip) { bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(() -> { player.kickPlayer(service.retrieveSingleMessage(player, MessageKey.NOT_OWNER_ERROR)); diff --git a/src/main/java/fr/xephi/authme/service/TeleportationService.java b/src/main/java/fr/xephi/authme/service/TeleportationService.java index f0eb7f851..76bae2873 100644 --- a/src/main/java/fr/xephi/authme/service/TeleportationService.java +++ b/src/main/java/fr/xephi/authme/service/TeleportationService.java @@ -3,6 +3,7 @@ import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.data.auth.PlayerCache.RegistrationStatus; import fr.xephi.authme.data.limbo.LimboPlayer; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.events.AbstractTeleportEvent; @@ -112,7 +113,8 @@ public void teleportNewPlayerToFirstSpawn(final Player player) { return; } - if (!player.hasPlayedBefore() || !dataSource.isAuthAvailable(player.getName())) { + RegistrationStatus registrationStatus = playerCache.getRegistrationStatus(player.getName()); + if (!player.hasPlayedBefore() || registrationStatus == RegistrationStatus.UNREGISTERED) { logger.debug("Attempting to teleport player `{0}` to first spawn", player.getName()); performTeleportation(player, new FirstSpawnTeleportEvent(player, firstSpawn)); } diff --git a/src/test/java/fr/xephi/authme/listener/ListenerServiceTest.java b/src/test/java/fr/xephi/authme/listener/ListenerServiceTest.java index 76877fcac..28e17c664 100644 --- a/src/test/java/fr/xephi/authme/listener/ListenerServiceTest.java +++ b/src/test/java/fr/xephi/authme/listener/ListenerServiceTest.java @@ -4,6 +4,7 @@ import ch.jalu.injector.testing.DelayedInjectionRunner; import ch.jalu.injector.testing.InjectDelayed; import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.data.auth.PlayerCache.RegistrationStatus; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.service.ValidationService; import fr.xephi.authme.settings.Settings; @@ -119,6 +120,7 @@ public void shouldAllowUnloggedPlayerForOptionalRegistration() { String playerName = "myPlayer1"; Player player = mockPlayerWithName(playerName); given(playerCache.isAuthenticated(playerName)).willReturn(false); + given(playerCache.getRegistrationStatus(playerName)).willReturn(RegistrationStatus.UNREGISTERED); given(settings.getProperty(RegistrationSettings.FORCE)).willReturn(false); EntityEvent event = mock(EntityEvent.class); given(event.getEntity()).willReturn(player); @@ -130,7 +132,7 @@ public void shouldAllowUnloggedPlayerForOptionalRegistration() { // then assertThat(result, equalTo(false)); verify(playerCache).isAuthenticated(playerName); - verify(dataSource).isAuthAvailable(playerName); + verify(playerCache).getRegistrationStatus(playerName); } @Test @@ -154,10 +156,9 @@ public void shouldAllowUnrestrictedName() { public void shouldAllowNpcPlayer() { // given String playerName = "other_npc"; - Player player = mockPlayerWithName(playerName); + Player player = mockPlayerWithName(playerName, true); EntityEvent event = mock(EntityEvent.class); given(event.getEntity()).willReturn(player); - given(player.hasMetadata("NPC")).willReturn(true); // when boolean result = listenerService.shouldCancelEvent(event); @@ -214,8 +215,13 @@ public void shouldVerifyBasedOnPlayer() { } private static Player mockPlayerWithName(String name) { + return mockPlayerWithName(name,false); + } + + private static Player mockPlayerWithName(String name, boolean npc) { Player player = mock(Player.class); given(player.getName()).willReturn(name); + given(player.hasMetadata("NPC")).willReturn(npc); return player; }