diff --git a/.locale/en_US.yml b/.locale/en_US.yml index ce23a897..e0e92a25 100644 --- a/.locale/en_US.yml +++ b/.locale/en_US.yml @@ -9,7 +9,7 @@ log-error: "&7&l[&bLuck&3Perms&7&l] &4[ERROR] {0}" empty: "{0}" player-online: "&aOnline" player-offline: "&cOffline" -loading-error: "Permissions data could not be loaded. Please contact an administrator." +loading-error: "Permissions data could not be loaded. Please try again later." op-disabled: "&bThe vanilla OP system is disabled on this server." op-disabled-sponge: "&2Server Operator status has no effect when a permission plugin is installed. Please edit user data directly." log: "&3LOG &3&l> {0}" diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitListener.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitListener.java index 5d9ff17b..36da2046 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitListener.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitListener.java @@ -22,12 +22,15 @@ package me.lucko.luckperms.bukkit; +import lombok.RequiredArgsConstructor; + import me.lucko.luckperms.bukkit.model.Injector; import me.lucko.luckperms.bukkit.model.LPPermissible; +import me.lucko.luckperms.common.caching.UserCache; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.constants.Message; import me.lucko.luckperms.common.core.model.User; -import me.lucko.luckperms.common.utils.AbstractListener; +import me.lucko.luckperms.common.utils.LoginHelper; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -39,62 +42,78 @@ import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.player.PlayerLoginEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.server.PluginEnableEvent; -import org.bukkit.scheduler.BukkitTask; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; -import java.util.Map; import java.util.Set; import java.util.UUID; -class BukkitListener extends AbstractListener implements Listener { +@RequiredArgsConstructor +public class BukkitListener implements Listener { private final LPBukkitPlugin plugin; private final Set deniedAsyncLogin = Collections.synchronizedSet(new HashSet<>()); - private final Set deniedLogin = new HashSet<>(); - - private final Map cleanupTasks = Collections.synchronizedMap(new HashMap<>()); - - BukkitListener(LPBukkitPlugin plugin) { - super(plugin); - this.plugin = plugin; - } + private final Set deniedLogin = Collections.synchronizedSet(new HashSet<>()); @EventHandler(priority = EventPriority.LOW) public void onPlayerPreLogin(AsyncPlayerPreLoginEvent e) { + /* Called when the player first attempts a connection with the server. + Listening on LOW priority to allow plugins to modify username / UUID data here. (auth plugins) */ + + /* the player was denied entry to the server before this priority. + log this, so we can handle appropriately later. */ if (e.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) { + plugin.getLog().warn("Connection from " + e.getUniqueId() + " was already denied. No permissions data will be loaded."); deniedAsyncLogin.add(e.getUniqueId()); return; } + /* either the plugin hasn't finished starting yet, or there was an issue connecting to the DB, performing file i/o, etc. + we don't let players join in this case, because it means they can connect to the server without their permissions data. + some server admins rely on negating perms to stop users from causing damage etc, so it's really important that + this data is loaded. */ if (!plugin.isStarted() || !plugin.getStorage().isAcceptingLogins()) { + + // log that the user tried to login, but was denied at this stage. deniedAsyncLogin.add(e.getUniqueId()); - // The datastore is disabled, prevent players from joining the server - plugin.getLog().warn("The plugin storage is not loaded. Denying connection from: " + e.getUniqueId() + " - " + e.getName()); - e.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, Message.LOADING_ERROR.toString()); + // actually deny the connection. + plugin.getLog().warn("Permissions storage is not loaded yet. Denying connection from: " + e.getUniqueId() + " - " + e.getName()); + e.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, Message.LOADING_ERROR.asString(plugin.getLocaleManager())); return; } - // remove any pending cleanup tasks - BukkitTask task = cleanupTasks.remove(e.getUniqueId()); - if (task != null) { - task.cancel(); - } + /* Actually process the login for the connection. + We do this here to delay the login until the data is ready. + If the login gets cancelled later on, then this will be cleaned up. - // Process login - onAsyncLogin(e.getUniqueId(), e.getName()); + This includes: + - loading uuid data + - loading permissions + - creating a user instance in the UserManager for this connection. + - setting up cached data. */ + try { + LoginHelper.loadUser(plugin, e.getUniqueId(), e.getName()); + } catch (Exception ex) { + ex.printStackTrace(); + + // there was some error loading data. deny the connection + deniedAsyncLogin.add(e.getUniqueId()); + e.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, Message.LOADING_ERROR.asString(plugin.getLocaleManager())); + } } - @EventHandler(priority = EventPriority.HIGHEST) + @EventHandler(priority = EventPriority.MONITOR) public void onPlayerPreLoginMonitor(AsyncPlayerPreLoginEvent e) { - // If they were denied before/at LOW, then don't bother handling here. + /* Listen to see if the event was cancelled after we initially handled the connection + If the connection was cancelled here, we need to do something to clean up the data that was loaded. */ + + // Check to see if this connection was denied at LOW. if (deniedAsyncLogin.remove(e.getUniqueId())) { - // this is a problem, as they were denied at low priority, but are now being allowed. + // This is a problem, as they were denied at low priority, but are now being allowed. if (e.getLoginResult() == AsyncPlayerPreLoginEvent.Result.ALLOWED) { - new IllegalStateException("Player connection was re-allowed for " + e.getUniqueId()).printStackTrace(); + plugin.getLog().severe("Player connection was re-allowed for " + e.getUniqueId()); e.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, ""); } @@ -102,14 +121,22 @@ class BukkitListener extends AbstractListener implements Listener { } // Login event was cancelled by another plugin - if (plugin.isStarted() && plugin.getStorage().isAcceptingLogins() && e.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) { - cleanupUser(e.getUniqueId()); + if (e.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) { + // Schedule cleanup of this user. + plugin.getUserManager().scheduleUnload(e.getUniqueId()); } } @EventHandler(priority = EventPriority.LOW) public void onPlayerLogin(PlayerLoginEvent e) { + /* Called when the player starts logging into the server. + At this point, the users data should be present and loaded. + Listening on LOW priority to allow plugins to further modify data here. (auth plugins, etc.) */ + + /* the player was denied entry to the server before this priority. + log this, so we can handle appropriately later. */ if (e.getResult() != PlayerLoginEvent.Result.ALLOWED) { + plugin.getLog().warn("Login from " + e.getPlayer().getUniqueId() + " was denied before an attachment could be injected."); deniedLogin.add(e.getPlayer().getUniqueId()); return; } @@ -117,21 +144,17 @@ class BukkitListener extends AbstractListener implements Listener { final Player player = e.getPlayer(); final User user = plugin.getUserManager().get(plugin.getUuidCache().getUUID(player.getUniqueId())); + /* User instance is null for whatever reason. Could be that it was unloaded between asyncpre and now. */ if (user == null) { deniedLogin.add(e.getPlayer().getUniqueId()); - // User wasn't loaded for whatever reason. - plugin.getLog().warn("User " + player.getUniqueId() + " - " + player.getName() + " could not be loaded. - denying login."); - e.disallow(PlayerLoginEvent.Result.KICK_OTHER, Message.LOADING_ERROR.toString()); + plugin.getLog().warn("User " + player.getUniqueId() + " - " + player.getName() + " doesn't have data pre-loaded. - denying login."); + e.disallow(PlayerLoginEvent.Result.KICK_OTHER, Message.LOADING_ERROR.asString(plugin.getLocaleManager())); return; } - // remove any pending cleanup tasks - BukkitTask task = cleanupTasks.remove(e.getPlayer().getUniqueId()); - if (task != null) { - task.cancel(); - } - + // User instance is there, now we can inject our custom Permissible into the player. + // Care should be taken at this stage to ensure that async tasks which manipulate bukkit data check that the player is still online. try { // Make a new permissible for the user LPPermissible lpPermissible = new LPPermissible(player, user, plugin); @@ -147,30 +170,43 @@ class BukkitListener extends AbstractListener implements Listener { // We assume all users are not op, but those who are need extra calculation. if (player.isOp()) { - plugin.doAsync(() -> user.getUserData().preCalculate(plugin.getPreProcessContexts(true))); + plugin.doAsync(() -> { + UserCache userData = user.getUserData(); + if (userData == null) { + return; + } + + userData.preCalculate(plugin.getPreProcessContexts(true)); + }); } } - @EventHandler(priority = EventPriority.HIGHEST) + @EventHandler(priority = EventPriority.MONITOR) public void onPlayerLoginMonitor(PlayerLoginEvent e) { - // If they were denied before/at LOW, then don't bother handling here. + /* Listen to see if the event was cancelled after we initially handled the login + If the connection was cancelled here, we need to do something to clean up the data that was loaded. */ + + // Check to see if this connection was denied at LOW. if (deniedLogin.remove(e.getPlayer().getUniqueId())) { - // this is a problem, as they were denied at low priority, but are now being allowed. + // This is a problem, as they were denied at low priority, but are now being allowed. if (e.getResult() == PlayerLoginEvent.Result.ALLOWED) { - new IllegalStateException("Player connection was re-allowed for " + e.getPlayer().getUniqueId()).printStackTrace(); + plugin.getLog().severe("Player connection was re-allowed for " + e.getPlayer().getUniqueId()); e.disallow(PlayerLoginEvent.Result.KICK_OTHER, ""); } return; } + // Login event was cancelled by another plugin if (e.getResult() != PlayerLoginEvent.Result.ALLOWED) { - // The player got denied on sync login. - cleanupUser(e.getPlayer().getUniqueId()); - } else { - plugin.refreshAutoOp(e.getPlayer()); + // Schedule cleanup of this user. + plugin.getUserManager().scheduleUnload(e.getPlayer().getUniqueId()); + return; } + + // everything is going well. login was processed ok, this is just to refresh auto-op status. + plugin.refreshAutoOp(e.getPlayer()); } // Wait until the last priority to unload, so plugins can still perform permission checks on this event @@ -186,21 +222,8 @@ class BukkitListener extends AbstractListener implements Listener { player.setOp(false); } - // Call internal leave handling - onLeave(player.getUniqueId()); - } - - private void cleanupUser(UUID uuid) { - if (cleanupTasks.containsKey(uuid)) { - return; - } - - BukkitTask task = plugin.getServer().getScheduler().runTaskLater(plugin, () -> { - onLeave(uuid); - cleanupTasks.remove(uuid); - }, 60L); - - cleanupTasks.put(uuid, task); + // Request that the users data is unloaded. + plugin.getUserManager().scheduleUnload(player.getUniqueId()); } @EventHandler @@ -209,7 +232,7 @@ class BukkitListener extends AbstractListener implements Listener { return; } - String s = e.getMessage() + String s = e.getMessage().toLowerCase() .replace("/", "") .replace("bukkit:", "") .replace("spigot:", "") @@ -217,7 +240,7 @@ class BukkitListener extends AbstractListener implements Listener { if (s.equals("op") || s.startsWith("op ") || s.equals("deop") || s.startsWith("deop ")) { e.setCancelled(true); - e.getPlayer().sendMessage(Message.OP_DISABLED.toString()); + e.getPlayer().sendMessage(Message.OP_DISABLED.asString(plugin.getLocaleManager())); } } diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java index 9df16e07..a8eef4de 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java @@ -39,6 +39,7 @@ import me.lucko.luckperms.bukkit.model.LPPermissible; import me.lucko.luckperms.bukkit.vault.VaultHook; import me.lucko.luckperms.common.api.ApiHandler; import me.lucko.luckperms.common.api.ApiProvider; +import me.lucko.luckperms.common.caching.UserCache; import me.lucko.luckperms.common.caching.handlers.CachedStateManager; import me.lucko.luckperms.common.calculators.CalculatorFactory; import me.lucko.luckperms.common.commands.sender.Sender; @@ -73,6 +74,7 @@ import me.lucko.luckperms.common.treeview.PermissionVault; import me.lucko.luckperms.common.utils.BufferedRequest; import me.lucko.luckperms.common.utils.FileWatcher; import me.lucko.luckperms.common.utils.LoggerImpl; +import me.lucko.luckperms.common.utils.LoginHelper; import me.lucko.luckperms.common.verbose.VerboseHandler; import org.bukkit.World; @@ -302,7 +304,7 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin { // Load any online users (in the case of a reload) for (Player player : getServer().getOnlinePlayers()) { scheduler.doAsync(() -> { - listener.onAsyncLogin(player.getUniqueId(), player.getName()); + LoginHelper.loadUser(this, player.getUniqueId(), player.getName()); User user = getUserManager().get(getUuidCache().getUUID(player.getUniqueId())); if (user != null) { scheduler.doSync(() -> { @@ -431,11 +433,21 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin { if (getConfiguration().get(ConfigKeys.AUTO_OP)) { try { LPPermissible permissible = Injector.getPermissible(player.getUniqueId()); - if (permissible == null) { + if (permissible == null || !permissible.getActive().get()) { return; } - Map backing = permissible.getUser().getUserData().getPermissionData(permissible.calculateContexts()).getImmutableBacking(); + User user = permissible.getUser(); + if (user == null) { + return; + } + + UserCache userData = user.getUserData(); + if (userData == null) { + return; + } + + Map backing = userData.getPermissionData(permissible.calculateContexts()).getImmutableBacking(); boolean op = Optional.ofNullable(backing.get("luckperms.autoop")).orElse(false); player.setOp(op); } catch (Exception ignored) {} @@ -511,7 +523,8 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin { @Override public boolean isPlayerOnline(UUID external) { - return getServer().getPlayer(external) != null; + Player player = getServer().getPlayer(external); + return player != null && player.isOnline(); } @Override diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/model/Injector.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/model/Injector.java index 1944446b..96494ed9 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/model/Injector.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/model/Injector.java @@ -64,7 +64,7 @@ public class Injector { PermissibleBase existing = (PermissibleBase) HUMAN_ENTITY_FIELD.get(player); if (existing instanceof LPPermissible) { // uh oh - throw new IllegalStateException(); + throw new IllegalStateException("LPPermissible already injected into player " + player.toString()); } // Move attachments over from the old permissible. @@ -73,6 +73,7 @@ public class Injector { attachments.clear(); existing.clearPermissions(); + lpPermissible.getActive().set(true); lpPermissible.recalculatePermissions(); lpPermissible.setOldPermissible(existing); @@ -98,6 +99,8 @@ public class Injector { ((LPPermissible) permissible).unsubscribeFromAllAsync(); } + ((LPPermissible) permissible).getActive().set(false); + if (dummy) { HUMAN_ENTITY_FIELD.set(player, new DummyPermissibleBase()); } else { diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/model/LPPermissible.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/model/LPPermissible.java index 508ccc1f..eaceb72f 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/model/LPPermissible.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/model/LPPermissible.java @@ -49,6 +49,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.stream.Collectors; @@ -66,6 +67,8 @@ public class LPPermissible extends PermissibleBase { @Setter private PermissibleBase oldPermissible = null; + private final AtomicBoolean active = new AtomicBoolean(false); + // Attachment stuff. private final Map attachmentPermissions = new ConcurrentHashMap<>(); private final List attachments = Collections.synchronizedList(new LinkedList<>()); @@ -81,10 +84,18 @@ public class LPPermissible extends PermissibleBase { } public void updateSubscriptionsAsync() { + if (!active.get()) { + return; + } + plugin.doAsync(this::updateSubscriptions); } public void updateSubscriptions() { + if (!active.get()) { + return; + } + UserCache cache = user.getUserData(); if (cache == null) { return; @@ -110,9 +121,7 @@ public class LPPermissible extends PermissibleBase { } public void addAttachments(List attachments) { - for (PermissionAttachment attachment : attachments) { - this.attachments.add(attachment); - } + this.attachments.addAll(attachments); } public Contexts calculateContexts() { @@ -128,7 +137,7 @@ public class LPPermissible extends PermissibleBase { } private boolean hasData() { - return user.getUserData() != null; + return user != null && user.getUserData() != null; } @Override diff --git a/bungee/src/main/java/me/lucko/luckperms/bungee/BungeeListener.java b/bungee/src/main/java/me/lucko/luckperms/bungee/BungeeListener.java index 143a025c..0aab6299 100644 --- a/bungee/src/main/java/me/lucko/luckperms/bungee/BungeeListener.java +++ b/bungee/src/main/java/me/lucko/luckperms/bungee/BungeeListener.java @@ -22,6 +22,8 @@ package me.lucko.luckperms.bungee; +import lombok.RequiredArgsConstructor; + import me.lucko.luckperms.api.Contexts; import me.lucko.luckperms.api.caching.UserData; import me.lucko.luckperms.api.context.MutableContextSet; @@ -30,7 +32,6 @@ import me.lucko.luckperms.common.constants.Message; import me.lucko.luckperms.common.core.UuidCache; import me.lucko.luckperms.common.core.model.User; import me.lucko.luckperms.common.defaults.Rule; -import me.lucko.luckperms.common.utils.AbstractListener; import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.connection.PendingConnection; @@ -44,62 +45,61 @@ import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.event.EventHandler; import net.md_5.bungee.event.EventPriority; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; -@SuppressWarnings("WeakerAccess") -public class BungeeListener extends AbstractListener implements Listener { - private static final TextComponent WARN_MESSAGE = new TextComponent(Message.LOADING_ERROR.toString()); +@RequiredArgsConstructor +public class BungeeListener implements Listener { private final LPBungeePlugin plugin; - BungeeListener(LPBungeePlugin plugin) { - super(plugin); - this.plugin = plugin; - } + private final Set deniedLogin = Collections.synchronizedSet(new HashSet<>()); - @EventHandler(priority = EventPriority.HIGH) - public void onPlayerPermissionCheck(PermissionCheckEvent e) { - if (!(e.getSender() instanceof ProxiedPlayer)) { - e.setHasPermission(true); - return; - } - - final ProxiedPlayer player = ((ProxiedPlayer) e.getSender()); - - User user = plugin.getUserManager().get(plugin.getUuidCache().getUUID(player.getUniqueId())); - if (user == null) { - return; - } - - UserData userData = user.getUserData(); - if (userData == null) { - plugin.getLog().warn("Player " + player.getName() + " does not have any user data setup."); - return; - } - - Contexts contexts = new Contexts( - plugin.getContextManager().getApplicableContext(player), - plugin.getConfiguration().get(ConfigKeys.INCLUDING_GLOBAL_PERMS), - plugin.getConfiguration().get(ConfigKeys.INCLUDING_GLOBAL_WORLD_PERMS), - true, - plugin.getConfiguration().get(ConfigKeys.APPLYING_GLOBAL_GROUPS), - plugin.getConfiguration().get(ConfigKeys.APPLYING_GLOBAL_WORLD_GROUPS), - false - ); - - e.setHasPermission(userData.getPermissionData(contexts).getPermissionValue(e.getPermission()).asBoolean()); - } - - @EventHandler(priority = EventPriority.LOWEST) + @EventHandler(priority = EventPriority.LOW) public void onPlayerLogin(LoginEvent e) { - /* Delay the login here, as we want to cache UUID data before the player is connected to a backend bukkit server. + /* Called when the player first attempts a connection with the server. + Listening on LOW priority to allow plugins to modify username / UUID data here. (auth plugins) + + Delay the login here, as we want to cache UUID data before the player is connected to a backend bukkit server. This means that a player will have the same UUID across the network, even if parts of the network are running in Offline mode. */ + + // registers the plugins intent to modify this events state going forward. + // this will prevent the event from completing until we're finished handling. e.registerIntent(plugin); + + final long startTime = System.currentTimeMillis(); + final PendingConnection c = e.getConnection(); + + /* either the plugin hasn't finished starting yet, or there was an issue connecting to the DB, performing file i/o, etc. + as this is bungeecord, we will still allow the login, as players can't really do much harm without permissions data. + the proxy will just fallback to using the config file perms. */ + if (!plugin.getStorage().isAcceptingLogins()) { + // log that the user tried to login, but was denied at this stage. + deniedLogin.add(c.getUniqueId()); + return; + } + + /* another plugin (or the proxy itself) has cancelled this connection already */ + if (e.isCancelled()) { + plugin.getLog().warn("Connection from " + c.getUniqueId() + " was already denied. No permissions data will be loaded."); + deniedLogin.add(c.getUniqueId()); + return; + } + + /* Actually process the login for the connection. + We do this here to delay the login until the data is ready. + If the login gets cancelled later on, then this will be cleaned up. + + This includes: + - loading uuid data + - loading permissions + - creating a user instance in the UserManager for this connection. + - setting up cached data. */ plugin.doAsync(() -> { - final long startTime = System.currentTimeMillis(); final UuidCache cache = plugin.getUuidCache(); - final PendingConnection c = e.getConnection(); if (!plugin.getConfiguration().get(ConfigKeys.USE_SERVER_UUIDS)) { UUID uuid = plugin.getStorage().getUUID(c.getName()).join(); @@ -109,6 +109,8 @@ public class BungeeListener extends AbstractListener implements Listener { // No previous data for this player plugin.getApiProvider().getEventFactory().handleUserFirstLogin(c.getUniqueId(), c.getName()); cache.addToCache(c.getUniqueId(), c.getUniqueId()); + + // Join this call, as we want this to be set for when the player connects to the backend. plugin.getStorage().force().saveUUIDData(c.getName(), c.getUniqueId()).join(); } } else { @@ -118,12 +120,14 @@ public class BungeeListener extends AbstractListener implements Listener { } // Online mode, no cache needed. This is just for name -> uuid lookup. + // Again, join this call so the data is available for the backend. plugin.getStorage().force().saveUUIDData(c.getName(), c.getUniqueId()).join(); } - // We have to make a new user on this thread whilst the connection is being held, or we get concurrency issues as the Bukkit server - // and the BungeeCord server try to make a new user at the same time. + /* We have to make a new user on this thread whilst the connection is being held, or we get concurrency issues + as the Bukkit server and the BungeeCord server try to make a new user at the same time. */ plugin.getStorage().force().loadUser(cache.getUUID(c.getUniqueId()), c.getName()).join(); + User user = plugin.getUserManager().get(cache.getUUID(c.getUniqueId())); if (user == null) { plugin.getLog().warn("Failed to load user: " + c.getName()); @@ -148,7 +152,13 @@ public class BungeeListener extends AbstractListener implements Listener { if (time >= 1000) { plugin.getLog().warn("Processing login for " + c.getName() + " took " + time + "ms."); } + + // finally, complete out intent to modify state, so the proxy can continue handling the connection. e.completeIntent(plugin); + + // schedule a cleanup of the users data in a few seconds. + // this should cover the eventuality that the login fails. + plugin.getUserManager().scheduleUnload(c.getUniqueId()); }); } @@ -158,17 +168,57 @@ public class BungeeListener extends AbstractListener implements Listener { final User user = plugin.getUserManager().get(plugin.getUuidCache().getUUID(e.getPlayer().getUniqueId())); if (user == null) { - plugin.getProxy().getScheduler().schedule(plugin, () -> player.sendMessage(WARN_MESSAGE), 3, TimeUnit.SECONDS); + plugin.getProxy().getScheduler().schedule(plugin, () -> { + if (!player.isConnected()) { + return; + } + + player.sendMessage(new TextComponent(Message.LOADING_ERROR.asString(plugin.getLocaleManager()))); + }, 3, TimeUnit.SECONDS); } } // Wait until the last priority to unload, so plugins can still perform permission checks on this event @EventHandler(priority = EventPriority.HIGHEST) public void onPlayerQuit(PlayerDisconnectEvent e) { - onLeave(e.getPlayer().getUniqueId()); + // Request that the users data is unloaded. + plugin.getUserManager().scheduleUnload(e.getPlayer().getUniqueId()); } - // We don't preprocess all servers, so we may have to do it here. + @EventHandler(priority = EventPriority.HIGH) + public void onPlayerPermissionCheck(PermissionCheckEvent e) { + if (!(e.getSender() instanceof ProxiedPlayer)) { + return; + } + + final ProxiedPlayer player = ((ProxiedPlayer) e.getSender()); + + User user = plugin.getUserManager().get(plugin.getUuidCache().getUUID(player.getUniqueId())); + if (user == null) { + return; + } + + UserData userData = user.getUserData(); + if (userData == null) { + plugin.getLog().warn("Player " + player.getName() + " does not have any user data setup."); + plugin.doAsync(() -> user.setupData(false)); + return; + } + + Contexts contexts = new Contexts( + plugin.getContextManager().getApplicableContext(player), + plugin.getConfiguration().get(ConfigKeys.INCLUDING_GLOBAL_PERMS), + plugin.getConfiguration().get(ConfigKeys.INCLUDING_GLOBAL_WORLD_PERMS), + true, + plugin.getConfiguration().get(ConfigKeys.APPLYING_GLOBAL_GROUPS), + plugin.getConfiguration().get(ConfigKeys.APPLYING_GLOBAL_WORLD_GROUPS), + false + ); + + e.setHasPermission(userData.getPermissionData(contexts).getPermissionValue(e.getPermission()).asBoolean()); + } + + // We don't pre-process all servers, so we have to do it here. @EventHandler(priority = EventPriority.LOWEST) public void onServerSwitch(ServerConnectEvent e) { String serverName = e.getTarget().getName(); diff --git a/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeePlugin.java b/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeePlugin.java index c6eb92d7..1127857e 100644 --- a/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeePlugin.java +++ b/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeePlugin.java @@ -323,7 +323,8 @@ public class LPBungeePlugin extends Plugin implements LuckPermsPlugin { @Override public boolean isPlayerOnline(UUID external) { - return getProxy().getPlayer(external) != null; + ProxiedPlayer player = getProxy().getPlayer(external); + return player != null && player.isConnected(); } @Override diff --git a/common/src/main/java/me/lucko/luckperms/common/constants/Message.java b/common/src/main/java/me/lucko/luckperms/common/constants/Message.java index d262055f..9e10a5f6 100644 --- a/common/src/main/java/me/lucko/luckperms/common/constants/Message.java +++ b/common/src/main/java/me/lucko/luckperms/common/constants/Message.java @@ -27,6 +27,7 @@ import lombok.Getter; import me.lucko.luckperms.common.commands.sender.Sender; import me.lucko.luckperms.common.commands.utils.Util; +import me.lucko.luckperms.common.locale.LocaleManager; @SuppressWarnings("SpellCheckingInspection") @AllArgsConstructor @@ -44,7 +45,7 @@ public enum Message { EMPTY("{0}", true), PLAYER_ONLINE("&aOnline", false), PLAYER_OFFLINE("&cOffline", false), - LOADING_ERROR("Permissions data could not be loaded. Please contact an administrator.", true), + LOADING_ERROR("Permissions data could not be loaded. Please try again later.", true), OP_DISABLED("&bThe vanilla OP system is disabled on this server.", false), OP_DISABLED_SPONGE("&2Server Operator status has no effect when a permission plugin is installed. Please edit user data directly.", true), LOG("&3LOG &3&l> {0}", true), @@ -452,6 +453,21 @@ public enum Message { return Util.color(showPrefix ? PREFIX + message : message); } + public String asString(LocaleManager localeManager) { + String prefix = localeManager.getTranslation(PREFIX); + if (prefix == null) { + prefix = PREFIX.getMessage(); + } + + String s = localeManager.getTranslation(this); + if (s == null) { + s = message; + } + + s = s.replace("{PREFIX}", prefix).replace("\\n", "\n"); + return Util.color(showPrefix ? (prefix + s) : (s)); + } + public void send(Sender sender, Object... objects) { String prefix = sender.getPlatform().getLocaleManager().getTranslation(PREFIX); if (prefix == null) { diff --git a/common/src/main/java/me/lucko/luckperms/common/managers/UserManager.java b/common/src/main/java/me/lucko/luckperms/common/managers/UserManager.java index cc391b6e..b0a82195 100644 --- a/common/src/main/java/me/lucko/luckperms/common/managers/UserManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/managers/UserManager.java @@ -60,6 +60,13 @@ public interface UserManager extends Manager { */ void cleanup(User user); + /** + * Schedules a task to cleanup a user after a certain period of time, if they're not on the server anymore. + * + * @param uuid external uuid of the player + */ + void scheduleUnload(UUID uuid); + /** * Reloads the data of all online users */ diff --git a/common/src/main/java/me/lucko/luckperms/common/managers/impl/GenericUserManager.java b/common/src/main/java/me/lucko/luckperms/common/managers/impl/GenericUserManager.java index 9422dc1b..01a93770 100644 --- a/common/src/main/java/me/lucko/luckperms/common/managers/impl/GenericUserManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/managers/impl/GenericUserManager.java @@ -134,6 +134,18 @@ public class GenericUserManager extends AbstractManager im } } + @Override + public void scheduleUnload(UUID uuid) { + plugin.getScheduler().doAsyncLater(() -> { + User user = get(plugin.getUuidCache().getUUID(uuid)); + if (user != null && !plugin.isPlayerOnline(uuid)) { + user.unregisterData(); + unload(user); + } + plugin.getUuidCache().clearCache(uuid); + }, 40L); + } + @Override public void updateAllUsers() { plugin.doSync(() -> { diff --git a/common/src/main/java/me/lucko/luckperms/common/utils/AbstractListener.java b/common/src/main/java/me/lucko/luckperms/common/utils/LoginHelper.java similarity index 84% rename from common/src/main/java/me/lucko/luckperms/common/utils/AbstractListener.java rename to common/src/main/java/me/lucko/luckperms/common/utils/LoginHelper.java index 36b284df..a9d8b2e9 100644 --- a/common/src/main/java/me/lucko/luckperms/common/utils/AbstractListener.java +++ b/common/src/main/java/me/lucko/luckperms/common/utils/LoginHelper.java @@ -22,7 +22,7 @@ package me.lucko.luckperms.common.utils; -import lombok.AllArgsConstructor; +import lombok.experimental.UtilityClass; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.core.UuidCache; @@ -33,13 +33,12 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import java.util.UUID; /** - * An abstract listener shared by Bukkit & Sponge. + * Utilities for use in platform listeners */ -@AllArgsConstructor -public class AbstractListener { - private final LuckPermsPlugin plugin; +@UtilityClass +public class LoginHelper { - public void onAsyncLogin(UUID u, String username) { + public static void loadUser(LuckPermsPlugin plugin, UUID u, String username) { final long startTime = System.currentTimeMillis(); final UuidCache cache = plugin.getUuidCache(); @@ -67,6 +66,7 @@ public class AbstractListener { User user = plugin.getUserManager().get(cache.getUUID(u)); if (user == null) { plugin.getLog().warn("Failed to load user: " + username); + throw new RuntimeException("Failed to load user"); } else { // Setup defaults for the user boolean save = false; @@ -90,20 +90,7 @@ public class AbstractListener { } } - protected void onLeave(UUID uuid) { - final UuidCache cache = plugin.getUuidCache(); - - final User user = plugin.getUserManager().get(cache.getUUID(uuid)); - if (user != null) { - user.unregisterData(); - plugin.getUserManager().unload(user); - } - - // Unload the user from memory when they disconnect; - cache.clearCache(uuid); - } - - protected void refreshPlayer(UUID uuid) { + public static void refreshPlayer(LuckPermsPlugin plugin, UUID uuid) { final User user = plugin.getUserManager().get(plugin.getUuidCache().getUUID(uuid)); if (user != null) { user.getRefreshBuffer().requestDirectly(); diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java b/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java index e1be359a..bf86b2f0 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java @@ -440,7 +440,7 @@ public class LPSpongePlugin implements LuckPermsPlugin { @Override public boolean isPlayerOnline(UUID external) { - return game.getServer().getPlayer(external).isPresent(); + return game.getServer().getPlayer(external).map(Player::isOnline).orElse(false); } @Override diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/SpongeListener.java b/sponge/src/main/java/me/lucko/luckperms/sponge/SpongeListener.java index b3ba259a..a2794581 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/SpongeListener.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/SpongeListener.java @@ -22,12 +22,14 @@ package me.lucko.luckperms.sponge; +import lombok.RequiredArgsConstructor; + import me.lucko.luckperms.api.caching.UserData; import me.lucko.luckperms.api.context.MutableContextSet; import me.lucko.luckperms.common.constants.Message; import me.lucko.luckperms.common.core.UuidCache; import me.lucko.luckperms.common.core.model.User; -import me.lucko.luckperms.common.utils.AbstractListener; +import me.lucko.luckperms.common.utils.LoginHelper; import me.lucko.luckperms.sponge.timings.LPTiming; import org.spongepowered.api.command.CommandSource; @@ -35,49 +37,131 @@ import org.spongepowered.api.entity.living.player.Player; import org.spongepowered.api.event.Listener; import org.spongepowered.api.event.Order; import org.spongepowered.api.event.command.SendCommandEvent; +import org.spongepowered.api.event.filter.IsCancelled; import org.spongepowered.api.event.network.ClientConnectionEvent; import org.spongepowered.api.profile.GameProfile; import org.spongepowered.api.text.serializer.TextSerializers; +import org.spongepowered.api.util.Tristate; import org.spongepowered.api.world.World; import co.aikar.timings.Timing; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; -@SuppressWarnings("WeakerAccess") -public class SpongeListener extends AbstractListener { +@RequiredArgsConstructor +public class SpongeListener { private final LPSpongePlugin plugin; - SpongeListener(LPSpongePlugin plugin) { - super(plugin); - this.plugin = plugin; - } + private final Set deniedAsyncLogin = Collections.synchronizedSet(new HashSet<>()); + private final Set deniedLogin = Collections.synchronizedSet(new HashSet<>()); - @Listener(order = Order.EARLY) + @Listener(order = Order.AFTER_PRE) + @IsCancelled(Tristate.UNDEFINED) public void onClientAuth(ClientConnectionEvent.Auth e) { - if (!plugin.getStorage().isAcceptingLogins()) { - /* Datastore is disabled, prevent players from joining the server - Just don't load their data, they will be kicked at login */ + /* Called when the player first attempts a connection with the server. + Listening on AFTER_PRE priority to allow plugins to modify username / UUID data here. (auth plugins) */ + + final GameProfile p = e.getProfile(); + + /* the player was denied entry to the server before this priority. + log this, so we can handle appropriately later. */ + if (e.isCancelled()) { + plugin.getLog().warn("Connection from " + p.getUniqueId() + " was already denied. No permissions data will be loaded."); + deniedAsyncLogin.add(p.getUniqueId()); return; } - final GameProfile p = e.getProfile(); - onAsyncLogin(p.getUniqueId(), p.getName().get()); // Load the user into LuckPerms + /* either the plugin hasn't finished starting yet, or there was an issue connecting to the DB, performing file i/o, etc. + we don't let players join in this case, because it means they can connect to the server without their permissions data. + some server admins rely on negating perms to stop users from causing damage etc, so it's really important that + this data is loaded. */ + if (!plugin.getStorage().isAcceptingLogins()) { + + // log that the user tried to login, but was denied at this stage. + deniedAsyncLogin.add(p.getUniqueId()); + + // actually deny the connection. + plugin.getLog().warn("Permissions storage is not loaded yet. Denying connection from: " + p.getUniqueId() + " - " + p.getName()); + e.setCancelled(true); + e.setMessageCancelled(true); + //noinspection deprecation + e.setMessage(TextSerializers.LEGACY_FORMATTING_CODE.deserialize(Message.LOADING_ERROR.asString(plugin.getLocaleManager()))); + return; + } + + /* Actually process the login for the connection. + We do this here to delay the login until the data is ready. + If the login gets cancelled later on, then this will be cleaned up. + + This includes: + - loading uuid data + - loading permissions + - creating a user instance in the UserManager for this connection. + - setting up cached data. */ + try { + LoginHelper.loadUser(plugin, p.getUniqueId(), p.getName().orElseThrow(() -> new RuntimeException("No username present for user " + p.getUniqueId()))); + } catch (Exception ex) { + ex.printStackTrace(); + + e.setCancelled(true); + e.setMessageCancelled(true); + //noinspection deprecation + e.setMessage(TextSerializers.LEGACY_FORMATTING_CODE.deserialize(Message.LOADING_ERROR.asString(plugin.getLocaleManager()))); + } } - @SuppressWarnings("deprecation") - @Listener(order = Order.EARLY) + @Listener(order = Order.BEFORE_POST) + @IsCancelled(Tristate.UNDEFINED) + public void onClientAuthMonitor(ClientConnectionEvent.Auth e) { + /* Listen to see if the event was cancelled after we initially handled the connection + If the connection was cancelled here, we need to do something to clean up the data that was loaded. */ + + // Check to see if this connection was denied at LOW. + if (deniedAsyncLogin.remove(e.getProfile().getUniqueId())) { + + // This is a problem, as they were denied at low priority, but are now being allowed. + if (e.isCancelled()) { + plugin.getLog().severe("Player connection was re-allowed for " + e.getProfile().getUniqueId()); + e.setCancelled(true); + } + } + } + + @Listener(order = Order.AFTER_PRE) + @IsCancelled(Tristate.UNDEFINED) public void onClientLogin(ClientConnectionEvent.Login e) { try (Timing ignored = plugin.getTimings().time(LPTiming.ON_CLIENT_LOGIN)) { + /* Called when the player starts logging into the server. + At this point, the users data should be present and loaded. + Listening on LOW priority to allow plugins to further modify data here. (auth plugins, etc.) */ + final GameProfile player = e.getProfile(); + + /* the player was denied entry to the server before this priority. + log this, so we can handle appropriately later. */ + if (e.isCancelled()) { + plugin.getLog().warn("Login from " + player.getUniqueId() + " was denied before an attachment could be injected."); + deniedLogin.add(player.getUniqueId()); + return; + } + final User user = plugin.getUserManager().get(plugin.getUuidCache().getUUID(player.getUniqueId())); - // Check if the user was loaded successfully. + /* User instance is null for whatever reason. Could be that it was unloaded between asyncpre and now. */ if (user == null) { + deniedLogin.add(player.getUniqueId()); + + plugin.getLog().warn("User " + player.getUniqueId() + " - " + player.getName() + " doesn't have data pre-loaded. - denying login."); e.setCancelled(true); - e.setMessage(TextSerializers.LEGACY_FORMATTING_CODE.deserialize(Message.LOADING_ERROR.toString())); + e.setMessageCancelled(true); + //noinspection deprecation + e.setMessage(TextSerializers.LEGACY_FORMATTING_CODE.deserialize(Message.LOADING_ERROR.asString(plugin.getLocaleManager()))); return; } @@ -108,7 +192,7 @@ public class SpongeListener extends AbstractListener { @Listener(order = Order.EARLY) public void onClientJoin(ClientConnectionEvent.Join e) { // Refresh permissions again - plugin.doAsync(() -> refreshPlayer(e.getTargetEntity().getUniqueId())); + plugin.doAsync(() -> LoginHelper.refreshPlayer(plugin, e.getTargetEntity().getUniqueId())); } @Listener(order = Order.LAST) diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/managers/SpongeUserManager.java b/sponge/src/main/java/me/lucko/luckperms/sponge/managers/SpongeUserManager.java index 9ca1574b..acdf34db 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/managers/SpongeUserManager.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/managers/SpongeUserManager.java @@ -205,6 +205,11 @@ public class SpongeUserManager implements UserManager, LPSubjectCollection { // Do nothing - this instance uses other means in order to cleanup } + @Override + public void scheduleUnload(UUID uuid) { + // Do nothing - this instance uses other means in order to cleanup + } + @Override public void updateAllUsers() { plugin.doSync(() -> {