From 22e95493deff2b238736f4e196e1a1b3f73d5dff Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Sun, 15 Oct 2017 18:32:51 +0200 Subject: [PATCH] #1031 Introduce hasSession field in datasource (#1351) * Introduce hasSession field in datasource That makes isLogged more consistent as it will be '1' only when the player is online. * Fixes * Fix unit testing * Update config doc * Create SessionService * Create test for SessionService, avoid DB operations if sessions are disabled * Cleanup: remove outdated warning for session timeout = 0 - Remove outdated warning - Encapsulate session enabled check in SessionService * Fix failing SessionServiceTest, add data source integration tests for session methods --- docs/config.md | 6 +- src/main/java/fr/xephi/authme/AuthMe.java | 7 - .../authme/RegisterAdminCommand.java | 51 ++-- .../authme/datasource/CacheDataSource.java | 15 ++ .../fr/xephi/authme/datasource/Columns.java | 2 + .../xephi/authme/datasource/DataSource.java | 23 ++ .../fr/xephi/authme/datasource/FlatFile.java | 13 + .../fr/xephi/authme/datasource/MySQL.java | 43 ++++ .../fr/xephi/authme/datasource/SQLite.java | 83 ++++--- .../authme/process/join/AsynchronousJoin.java | 37 +-- .../process/login/AsynchronousLogin.java | 7 +- .../process/logout/AsynchronousLogout.java | 1 + .../authme/process/quit/AsynchronousQuit.java | 5 +- .../xephi/authme/service/SessionService.java | 86 +++++++ .../settings/properties/DatabaseSettings.java | 4 + .../authme/RegisterAdminCommandTest.java | 2 - .../AbstractDataSourceIntegrationTest.java | 32 +++ .../authme/service/SessionServiceTest.java | 222 ++++++++++++++++++ .../authme/datasource/sql-initialize.sql | 4 +- 19 files changed, 537 insertions(+), 106 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/service/SessionService.java create mode 100644 src/test/java/fr/xephi/authme/service/SessionServiceTest.java diff --git a/docs/config.md b/docs/config.md index f343840d8..a49a8f839 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1,5 +1,5 @@ - + ## AuthMe Configuration The first time you run AuthMe it will create a config.yml file in the plugins/AuthMe folder, @@ -41,6 +41,8 @@ DataSource: mySQLColumnEmail: 'email' # Column for storing if a player is logged in or not mySQLColumnLogged: 'isLogged' + # Column for storing if a player has a valid session or not + mySQLColumnHasSession: 'hasSession' # Column for storing players ips mySQLColumnIp: 'ip' # Column for storing players lastlogins @@ -545,4 +547,4 @@ To change settings on a running server, save your changes to config.yml and use --- -This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Mon Oct 09 10:19:07 CEST 2017 +This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Tue Oct 10 13:51:56 CEST 2017 diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index 0f2574bc9..6ce2d103e 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -30,7 +30,6 @@ import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.MigrationService; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.EmailSettings; -import fr.xephi.authme.settings.properties.PluginSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.task.CleanupTask; @@ -264,12 +263,6 @@ public class AuthMe extends JavaPlugin { ConsoleLogger.warning("WARNING!!! By disabling ForceSingleSession, your server protection is inadequate!"); } - // Session timeout disabled - if (settings.getProperty(PluginSettings.SESSIONS_TIMEOUT) == 0 - && settings.getProperty(PluginSettings.SESSIONS_ENABLED)) { - ConsoleLogger.warning("WARNING!!! You set session timeout to 0, this may cause security issues!"); - } - // Use TLS property only affects port 25 if (!settings.getProperty(EmailSettings.PORT25_USE_TLS) && settings.getProperty(EmailSettings.SMTP_PORT) != 25) { diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommand.java index 2f1341b92..6500b2e5d 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommand.java @@ -51,38 +51,29 @@ public class RegisterAdminCommand implements ExecutableCommand { return; } - bukkitService.runTaskOptionallyAsync(new Runnable() { + bukkitService.runTaskOptionallyAsync(() -> { + if (dataSource.isAuthAvailable(playerNameLowerCase)) { + commonService.send(sender, MessageKey.NAME_ALREADY_REGISTERED); + return; + } + HashedPassword hashedPassword = passwordSecurity.computeHash(playerPass, playerNameLowerCase); + PlayerAuth auth = PlayerAuth.builder() + .name(playerNameLowerCase) + .realName(playerName) + .password(hashedPassword) + .build(); - @Override - public void run() { - if (dataSource.isAuthAvailable(playerNameLowerCase)) { - commonService.send(sender, MessageKey.NAME_ALREADY_REGISTERED); - return; - } - HashedPassword hashedPassword = passwordSecurity.computeHash(playerPass, playerNameLowerCase); - PlayerAuth auth = PlayerAuth.builder() - .name(playerNameLowerCase) - .realName(playerName) - .password(hashedPassword) - .build(); + if (!dataSource.saveAuth(auth)) { + commonService.send(sender, MessageKey.ERROR); + return; + } - if (!dataSource.saveAuth(auth)) { - commonService.send(sender, MessageKey.ERROR); - return; - } - dataSource.setUnlogged(playerNameLowerCase); - - commonService.send(sender, MessageKey.REGISTER_SUCCESS); - ConsoleLogger.info(sender.getName() + " registered " + playerName); - final Player player = bukkitService.getPlayerExact(playerName); - if (player != null) { - bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(new Runnable() { - @Override - public void run() { - player.kickPlayer(commonService.retrieveSingleMessage(MessageKey.KICK_FOR_ADMIN_REGISTER)); - } - }); - } + commonService.send(sender, MessageKey.REGISTER_SUCCESS); + ConsoleLogger.info(sender.getName() + " registered " + playerName); + final Player player = bukkitService.getPlayerExact(playerName); + if (player != null) { + bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(() -> + player.kickPlayer(commonService.retrieveSingleMessage(MessageKey.KICK_FOR_ADMIN_REGISTER))); } }); } diff --git a/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java b/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java index b7eb6a18c..3be25310b 100644 --- a/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java +++ b/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java @@ -208,6 +208,21 @@ public class CacheDataSource implements DataSource { source.setUnlogged(user.toLowerCase()); } + @Override + public boolean hasSession(final String user) { + return source.hasSession(user); + } + + @Override + public void grantSession(final String user) { + source.grantSession(user); + } + + @Override + public void revokeSession(final String user) { + source.revokeSession(user); + } + @Override public void purgeLogged() { source.purgeLogged(); diff --git a/src/main/java/fr/xephi/authme/datasource/Columns.java b/src/main/java/fr/xephi/authme/datasource/Columns.java index 7d6f50917..edb91f150 100644 --- a/src/main/java/fr/xephi/authme/datasource/Columns.java +++ b/src/main/java/fr/xephi/authme/datasource/Columns.java @@ -26,6 +26,7 @@ public final class Columns { public final String EMAIL; public final String ID; public final String IS_LOGGED; + public final String HAS_SESSION; public Columns(Settings settings) { NAME = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME); @@ -44,6 +45,7 @@ public final class Columns { EMAIL = settings.getProperty(DatabaseSettings.MYSQL_COL_EMAIL); ID = settings.getProperty(DatabaseSettings.MYSQL_COL_ID); IS_LOGGED = settings.getProperty(DatabaseSettings.MYSQL_COL_ISLOGGED); + HAS_SESSION = settings.getProperty(DatabaseSettings.MYSQL_COL_HASSESSION); } } diff --git a/src/main/java/fr/xephi/authme/datasource/DataSource.java b/src/main/java/fr/xephi/authme/datasource/DataSource.java index 14faaf39d..544ee3c00 100644 --- a/src/main/java/fr/xephi/authme/datasource/DataSource.java +++ b/src/main/java/fr/xephi/authme/datasource/DataSource.java @@ -161,6 +161,29 @@ public interface DataSource extends Reloadable { */ void setUnlogged(String user); + /** + * Query the datasource whether the player has an active session or not. + * Warning: this value won't expire, you have also to check the user's last login timestamp. + * + * @param user The name of the player to verify + * @return True if the user has a valid session, false otherwise + */ + boolean hasSession(String user); + + /** + * Mark the user's hasSession value to true. + * + * @param user The name of the player to change + */ + void grantSession(String user); + + /** + * Mark the user's hasSession value to false. + * + * @param user The name of the player to change + */ + void revokeSession(String user); + /** * Set all players who are marked as logged in as NOT logged in. */ diff --git a/src/main/java/fr/xephi/authme/datasource/FlatFile.java b/src/main/java/fr/xephi/authme/datasource/FlatFile.java index bb9aa3ef2..9697f0fa5 100644 --- a/src/main/java/fr/xephi/authme/datasource/FlatFile.java +++ b/src/main/java/fr/xephi/authme/datasource/FlatFile.java @@ -329,6 +329,19 @@ public class FlatFile implements DataSource { public void setUnlogged(String user) { } + @Override + public boolean hasSession(String user) { + throw new UnsupportedOperationException("Flat file no longer supported"); + } + + @Override + public void grantSession(String user) { + } + + @Override + public void revokeSession(String user) { + } + @Override public void purgeLogged() { } diff --git a/src/main/java/fr/xephi/authme/datasource/MySQL.java b/src/main/java/fr/xephi/authme/datasource/MySQL.java index 73f758403..98b205835 100644 --- a/src/main/java/fr/xephi/authme/datasource/MySQL.java +++ b/src/main/java/fr/xephi/authme/datasource/MySQL.java @@ -237,6 +237,11 @@ public class MySQL implements DataSource { st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.IS_LOGGED + " SMALLINT NOT NULL DEFAULT '0' AFTER " + col.EMAIL); } + + if (isColumnMissing(md, col.HAS_SESSION)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + + col.HAS_SESSION + " SMALLINT NOT NULL DEFAULT '0' AFTER " + col.IS_LOGGED); + } } ConsoleLogger.info("MySQL setup finished"); } @@ -565,6 +570,44 @@ public class MySQL implements DataSource { } } + @Override + public boolean hasSession(String user) { + String sql = "SELECT " + col.HAS_SESSION + " FROM " + tableName + " WHERE " + col.NAME + "=?;"; + try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { + pst.setString(1, user); + try (ResultSet rs = pst.executeQuery()) { + return rs.next() && (rs.getInt(col.HAS_SESSION) == 1); + } + } catch (SQLException ex) { + logSqlException(ex); + } + return false; + } + + @Override + public void grantSession(String user) { + String sql = "UPDATE " + tableName + " SET " + col.HAS_SESSION + "=? WHERE " + col.NAME + "=?;"; + try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { + pst.setInt(1, 1); + pst.setString(2, user.toLowerCase()); + pst.executeUpdate(); + } catch (SQLException ex) { + logSqlException(ex); + } + } + + @Override + public void revokeSession(String user) { + String sql = "UPDATE " + tableName + " SET " + col.HAS_SESSION + "=? WHERE " + col.NAME + "=?;"; + try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { + pst.setInt(1, 0); + pst.setString(2, user.toLowerCase()); + pst.executeUpdate(); + } catch (SQLException ex) { + logSqlException(ex); + } + } + @Override public void purgeLogged() { String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE " + col.IS_LOGGED + "=?;"; diff --git a/src/main/java/fr/xephi/authme/datasource/SQLite.java b/src/main/java/fr/xephi/authme/datasource/SQLite.java index 4652aad9b..966a684b6 100644 --- a/src/main/java/fr/xephi/authme/datasource/SQLite.java +++ b/src/main/java/fr/xephi/authme/datasource/SQLite.java @@ -134,7 +134,13 @@ public class SQLite implements DataSource { } if (isColumnMissing(md, col.IS_LOGGED)) { - st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.IS_LOGGED + " INT DEFAULT '0';"); + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.IS_LOGGED + " INT NOT NULL DEFAULT '0';"); + } + + if (isColumnMissing(md, col.HAS_SESSION)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.HAS_SESSION + " INT NOT NULL DEFAULT '0';"); } } ConsoleLogger.info("SQLite Setup finished"); @@ -426,7 +432,7 @@ public class SQLite implements DataSource { @Override public boolean isLogged(String user) { - String sql = "SELECT * FROM " + tableName + " WHERE LOWER(" + col.NAME + ")=?;"; + String sql = "SELECT " + col.IS_LOGGED + " FROM " + tableName + " WHERE LOWER(" + col.NAME + ")=?;"; try (PreparedStatement pst = con.prepareStatement(sql)) { pst.setString(1, user); try (ResultSet rs = pst.executeQuery()) { @@ -455,14 +461,52 @@ public class SQLite implements DataSource { @Override public void setUnlogged(String user) { String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE LOWER(" + col.NAME + ")=?;"; - if (user != null) { - try (PreparedStatement pst = con.prepareStatement(sql)) { - pst.setInt(1, 0); - pst.setString(2, user); - pst.executeUpdate(); - } catch (SQLException ex) { - logSqlException(ex); + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setInt(1, 0); + pst.setString(2, user); + pst.executeUpdate(); + } catch (SQLException ex) { + logSqlException(ex); + } + } + + @Override + public boolean hasSession(String user) { + String sql = "SELECT " + col.HAS_SESSION + " FROM " + tableName + " WHERE LOWER(" + col.NAME + ")=?;"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setString(1, user); + try (ResultSet rs = pst.executeQuery()) { + if (rs.next()) { + return rs.getInt(col.HAS_SESSION) == 1; + } } + } catch (SQLException ex) { + logSqlException(ex); + } + return false; + } + + @Override + public void grantSession(String user) { + String sql = "UPDATE " + tableName + " SET " + col.HAS_SESSION + "=? WHERE LOWER(" + col.NAME + ")=?;"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setInt(1, 1); + pst.setString(2, user); + pst.executeUpdate(); + } catch (SQLException ex) { + logSqlException(ex); + } + } + + @Override + public void revokeSession(String user) { + String sql = "UPDATE " + tableName + " SET " + col.HAS_SESSION + "=? WHERE LOWER(" + col.NAME + ")=?;"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setInt(1, 0); + pst.setString(2, user); + pst.executeUpdate(); + } catch (SQLException ex) { + logSqlException(ex); } } @@ -574,17 +618,6 @@ public class SQLite implements DataSource { return authBuilder.build(); } - - private static void close(Statement st) { - if (st != null) { - try { - st.close(); - } catch (SQLException ex) { - logSqlException(ex); - } - } - } - private static void close(Connection con) { if (con != null) { try { @@ -594,14 +627,4 @@ public class SQLite implements DataSource { } } } - - private static void close(ResultSet rs) { - if (rs != null) { - try { - rs.close(); - } catch (SQLException ex) { - logSqlException(ex); - } - } - } } 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 af325cbae..dc66b0b4a 100644 --- a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -1,12 +1,9 @@ package fr.xephi.authme.process.join; import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.data.auth.PlayerAuth; -import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.data.limbo.LimboService; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.events.ProtectInventoryEvent; -import fr.xephi.authme.events.RestoreSessionEvent; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.permission.PlayerStatePermission; import fr.xephi.authme.process.AsynchronousProcess; @@ -14,11 +11,11 @@ import fr.xephi.authme.process.login.AsynchronousLogin; import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.CommonService; import fr.xephi.authme.service.PluginHookService; +import fr.xephi.authme.service.SessionService; import fr.xephi.authme.service.ValidationService; import fr.xephi.authme.settings.WelcomeMessageConfiguration; import fr.xephi.authme.settings.commandconfig.CommandManager; import fr.xephi.authme.settings.properties.HooksSettings; -import fr.xephi.authme.settings.properties.PluginSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.util.PlayerUtils; @@ -48,9 +45,6 @@ public class AsynchronousJoin implements AsynchronousProcess { @Inject private CommonService service; - @Inject - private PlayerCache playerCache; - @Inject private LimboService limboService; @@ -72,6 +66,9 @@ public class AsynchronousJoin implements AsynchronousProcess { @Inject private WelcomeMessageConfiguration welcomeMessageConfiguration; + @Inject + private SessionService sessionService; + AsynchronousJoin() { } @@ -121,7 +118,7 @@ public class AsynchronousJoin implements AsynchronousProcess { } // Session logic - if (canResumeSession(player)) { + if (sessionService.canResumeSession(player)) { service.send(player, MessageKey.SESSION_RECONNECTION); // Run commands bukkitService.scheduleSyncTaskFromOptionallyAsyncTask( @@ -177,30 +174,6 @@ public class AsynchronousJoin implements AsynchronousProcess { }); } - private boolean canResumeSession(Player player) { - final String name = player.getName(); - if (database.isLogged(name)) { - database.setUnlogged(name); - playerCache.removePlayer(name); - if(service.getProperty(PluginSettings.SESSIONS_ENABLED)) { - PlayerAuth auth = database.getAuth(name); - if (auth != null) { - long timeSinceLastLogin = System.currentTimeMillis() - auth.getLastLogin(); - if(timeSinceLastLogin < 0 - || timeSinceLastLogin > (service.getProperty(PluginSettings.SESSIONS_TIMEOUT) * 60 * 1000) - || !auth.getIp().equals(PlayerUtils.getPlayerIp(player))) { - service.send(player, MessageKey.SESSION_EXPIRED); - } else { - RestoreSessionEvent event = bukkitService.createAndCallEvent( - isAsync -> new RestoreSessionEvent(player, isAsync)); - return !event.isCancelled(); - } - } - } - } - return false; - } - /** * Checks whether the maximum number of accounts has been exceeded for the given IP address (according to * settings and permissions). If this is the case, the player is kicked. diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index 46c97dc45..dda6ffcc3 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -20,6 +20,7 @@ import fr.xephi.authme.process.SyncProcessManager; import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.SessionService; import fr.xephi.authme.settings.properties.DatabaseSettings; import fr.xephi.authme.settings.properties.EmailSettings; import fr.xephi.authme.settings.properties.HooksSettings; @@ -69,6 +70,9 @@ public class AsynchronousLogin implements AsynchronousProcess { @Inject private EmailService emailService; + @Inject + private SessionService sessionService; + AsynchronousLogin() { } @@ -238,9 +242,10 @@ public class AsynchronousLogin implements AsynchronousProcess { ConsoleLogger.fine(player.getName() + " logged in!"); - // makes player isLoggedin via API + // makes player loggedin playerCache.updatePlayer(auth); dataSource.setLogged(name); + sessionService.grantSession(name); // As the scheduling executes the Task most likely after the current // task, we schedule it in the end diff --git a/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java b/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java index fb1ae36cf..6e60bc87f 100644 --- a/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java +++ b/src/main/java/fr/xephi/authme/process/logout/AsynchronousLogout.java @@ -53,6 +53,7 @@ public class AsynchronousLogout implements AsynchronousProcess { playerCache.removePlayer(name); database.setUnlogged(name); + database.revokeSession(name); syncProcessManager.processSyncPlayerLogout(player); } } diff --git a/src/main/java/fr/xephi/authme/process/quit/AsynchronousQuit.java b/src/main/java/fr/xephi/authme/process/quit/AsynchronousQuit.java index c22a0d64f..187388d67 100644 --- a/src/main/java/fr/xephi/authme/process/quit/AsynchronousQuit.java +++ b/src/main/java/fr/xephi/authme/process/quit/AsynchronousQuit.java @@ -82,8 +82,11 @@ public class AsynchronousQuit implements AsynchronousProcess { playerCache.removePlayer(name); //always update the database when the player quit the game (if sessions are disabled) - if(!wasLoggedIn || !service.getProperty(PluginSettings.SESSIONS_ENABLED)) { + if (wasLoggedIn) { database.setUnlogged(name); + if (!service.getProperty(PluginSettings.SESSIONS_ENABLED)) { + database.revokeSession(name); + } } if (plugin.isEnabled()) { diff --git a/src/main/java/fr/xephi/authme/service/SessionService.java b/src/main/java/fr/xephi/authme/service/SessionService.java new file mode 100644 index 000000000..ff6d7d559 --- /dev/null +++ b/src/main/java/fr/xephi/authme/service/SessionService.java @@ -0,0 +1,86 @@ +package fr.xephi.authme.service; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.events.RestoreSessionEvent; +import fr.xephi.authme.initialization.Reloadable; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.settings.properties.PluginSettings; +import fr.xephi.authme.util.PlayerUtils; +import org.bukkit.entity.Player; + +import javax.inject.Inject; + +/** + * Handles the user sessions. + */ +public class SessionService implements Reloadable { + + private final CommonService service; + private final BukkitService bukkitService; + private final DataSource database; + + private boolean isEnabled; + + @Inject + SessionService(CommonService service, BukkitService bukkitService, DataSource database) { + this.service = service; + this.bukkitService = bukkitService; + this.database = database; + reload(); + } + + /** + * Returns whether the player has a session he can resume. + * + * @param player the player to check + * @return true if there is a current session, false otherwise + */ + public boolean canResumeSession(Player player) { + final String name = player.getName(); + if (isEnabled && database.hasSession(name)) { + database.setUnlogged(name); + database.revokeSession(name); + PlayerAuth auth = database.getAuth(name); + if (hasValidSessionData(auth, player)) { + RestoreSessionEvent event = bukkitService.createAndCallEvent( + isAsync -> new RestoreSessionEvent(player, isAsync)); + return !event.isCancelled(); + } else { + service.send(player, MessageKey.SESSION_EXPIRED); + } + } + return false; + } + + /** + * Checks if the given Player has a current session by comparing its properties + * with the given PlayerAuth's. + * + * @param auth the player auth + * @param player the associated player + * @return true if the player may resume his login session, false otherwise + */ + private boolean hasValidSessionData(PlayerAuth auth, Player player) { + if (auth == null) { + ConsoleLogger.warning("No PlayerAuth in database for '" + player.getName() + "' during session check"); + return false; + } + long timeSinceLastLogin = System.currentTimeMillis() - auth.getLastLogin(); + return auth.getIp().equals(PlayerUtils.getPlayerIp(player)) + && timeSinceLastLogin > 0 + && timeSinceLastLogin < service.getProperty(PluginSettings.SESSIONS_TIMEOUT) * 60 * 1000; + } + + public void grantSession(String name) { + if (isEnabled) { + database.grantSession(name); + } + } + + @Override + public void reload() { + this.isEnabled = service.getProperty(PluginSettings.SESSIONS_ENABLED); + } +} diff --git a/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java b/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java index 4b3f5d3e0..bcc9925f2 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java @@ -75,6 +75,10 @@ public final class DatabaseSettings implements SettingsHolder { public static final Property MYSQL_COL_ISLOGGED = newProperty("DataSource.mySQLColumnLogged", "isLogged"); + @Comment("Column for storing if a player has a valid session or not") + public static final Property MYSQL_COL_HASSESSION = + newProperty("DataSource.mySQLColumnHasSession", "hasSession"); + @Comment("Column for storing players ips") public static final Property MYSQL_COL_IP = newProperty("DataSource.mySQLColumnIp", "ip"); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java index 5ee2161e1..bacbeb7b8 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommandTest.java @@ -144,7 +144,6 @@ public class RegisterAdminCommandTest { ArgumentCaptor captor = ArgumentCaptor.forClass(PlayerAuth.class); verify(dataSource).saveAuth(captor.capture()); assertAuthHasInfo(captor.getValue(), user, hashedPassword); - verify(dataSource).setUnlogged(user); } @Test @@ -174,7 +173,6 @@ public class RegisterAdminCommandTest { ArgumentCaptor captor = ArgumentCaptor.forClass(PlayerAuth.class); verify(dataSource).saveAuth(captor.capture()); assertAuthHasInfo(captor.getValue(), user, hashedPassword); - verify(dataSource).setUnlogged(user); verify(player).kickPlayer(kickForAdminRegister); } diff --git a/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java b/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java index bd0b92a22..16931c600 100644 --- a/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java @@ -416,4 +416,36 @@ public abstract class AbstractDataSourceIntegrationTest { // then assertThat(loggedPlayersWithEmptyMail, contains("Bobby")); } + + @Test + public void shouldGrantAndRetrieveSessionFlag() { + // given + DataSource dataSource = getDataSource(); + + // when + dataSource.grantSession("bobby"); + dataSource.grantSession("doesNotExist"); + + // then + assertThat(dataSource.hasSession("bobby"), equalTo(true)); + assertThat(dataSource.hasSession("user"), equalTo(false)); + assertThat(dataSource.hasSession("bogus"), equalTo(false)); + } + + @Test + public void shouldRevokeSession() { + // given + DataSource dataSource = getDataSource(); + dataSource.grantSession("bobby"); + dataSource.grantSession("user"); + + // when + dataSource.revokeSession("bobby"); + dataSource.revokeSession("userNotInDatabase"); + + // then + assertThat(dataSource.hasSession("bobby"), equalTo(false)); + assertThat(dataSource.hasSession("user"), equalTo(true)); + assertThat(dataSource.hasSession("nonExistentName"), equalTo(false)); + } } diff --git a/src/test/java/fr/xephi/authme/service/SessionServiceTest.java b/src/test/java/fr/xephi/authme/service/SessionServiceTest.java new file mode 100644 index 000000000..85bcff4c0 --- /dev/null +++ b/src/test/java/fr/xephi/authme/service/SessionServiceTest.java @@ -0,0 +1,222 @@ +package fr.xephi.authme.service; + +import ch.jalu.injector.testing.BeforeInjecting; +import ch.jalu.injector.testing.DelayedInjectionRunner; +import ch.jalu.injector.testing.InjectDelayed; +import fr.xephi.authme.TestHelper; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.events.RestoreSessionEvent; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.settings.properties.PluginSettings; +import org.bukkit.entity.Player; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +import java.util.function.Function; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.only; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link SessionService}. + */ +@RunWith(DelayedInjectionRunner.class) +public class SessionServiceTest { + + @InjectDelayed + private SessionService sessionService; + + @Mock + private DataSource dataSource; + @Mock + private CommonService commonService; + @Mock + private BukkitService bukkitService; + + @BeforeClass + public static void initLogger() { + TestHelper.setupLogger(); + } + + @BeforeInjecting + public void setUpEnabledProperty() { + given(commonService.getProperty(PluginSettings.SESSIONS_ENABLED)).willReturn(true); + } + + @Test + public void shouldCheckSessionsEnabledSetting() { + // given + Player player = mock(Player.class); + given(commonService.getProperty(PluginSettings.SESSIONS_ENABLED)).willReturn(false); + sessionService.reload(); + + // when + boolean result = sessionService.canResumeSession(player); + + // then + assertThat(result, equalTo(false)); + verifyZeroInteractions(dataSource); + } + + @Test + public void shouldCheckIfUserHasSession() { + // given + String name = "Bobby"; + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + given(dataSource.hasSession(name)).willReturn(false); + + // when + boolean result = sessionService.canResumeSession(player); + + // then + assertThat(result, equalTo(false)); + verify(commonService, only()).getProperty(PluginSettings.SESSIONS_ENABLED); + verify(dataSource, only()).hasSession(name); + } + + @Test + public void shouldCheckLastLoginDate() { + // given + String name = "Bobby"; + String ip = "127.3.12.15"; + Player player = mockPlayerWithNameAndIp(name, ip); + given(commonService.getProperty(PluginSettings.SESSIONS_TIMEOUT)).willReturn(8); + given(dataSource.hasSession(name)).willReturn(true); + PlayerAuth auth = PlayerAuth.builder() + .name(name) + .lastLogin(System.currentTimeMillis() - 10 * 60 * 1000) + .ip(ip).build(); + given(dataSource.getAuth(name)).willReturn(auth); + + // when + boolean result = sessionService.canResumeSession(player); + + // then + assertThat(result, equalTo(false)); + verify(commonService).getProperty(PluginSettings.SESSIONS_ENABLED); + verify(commonService).send(player, MessageKey.SESSION_EXPIRED); + verify(dataSource).hasSession(name); + verify(dataSource).setUnlogged(name); + verify(dataSource).revokeSession(name); + } + + @Test + public void shouldRefuseSessionForAuthWithZeroLastLoginTimestamp() { + // given + String name = "Bobby"; + String ip = "127.3.12.15"; + Player player = mockPlayerWithNameAndIp(name, ip); + given(commonService.getProperty(PluginSettings.SESSIONS_TIMEOUT)).willReturn(8); + given(dataSource.hasSession(name)).willReturn(true); + PlayerAuth auth = PlayerAuth.builder() + .name(name) + .lastLogin(0) + .ip(ip).build(); + given(dataSource.getAuth(name)).willReturn(auth); + + // when + boolean result = sessionService.canResumeSession(player); + + // then + assertThat(result, equalTo(false)); + verify(commonService).getProperty(PluginSettings.SESSIONS_ENABLED); + verify(commonService).send(player, MessageKey.SESSION_EXPIRED); + verify(dataSource).hasSession(name); + verify(dataSource).setUnlogged(name); + verify(dataSource).revokeSession(name); + } + + @Test + public void shouldCheckLastLoginIp() { + // given + String name = "Bobby"; + String ip = "127.3.12.15"; + Player player = mockPlayerWithNameAndIp(name, ip); + given(dataSource.hasSession(name)).willReturn(true); + PlayerAuth auth = PlayerAuth.builder() + .name(name) + .lastLogin(System.currentTimeMillis()) + .ip("8.8.8.8").build(); + given(dataSource.getAuth(name)).willReturn(auth); + + // when + boolean result = sessionService.canResumeSession(player); + + // then + assertThat(result, equalTo(false)); + verify(commonService).getProperty(PluginSettings.SESSIONS_ENABLED); + verify(commonService).send(player, MessageKey.SESSION_EXPIRED); + verify(dataSource).hasSession(name); + verify(dataSource).setUnlogged(name); + verify(dataSource).revokeSession(name); + } + + @Test + public void shouldEmitEventForValidSession() { + // given + String name = "Bobby"; + String ip = "127.3.12.15"; + Player player = mockPlayerWithNameAndIp(name, ip); + given(commonService.getProperty(PluginSettings.SESSIONS_TIMEOUT)).willReturn(8); + given(dataSource.hasSession(name)).willReturn(true); + PlayerAuth auth = PlayerAuth.builder() + .name(name) + .lastLogin(System.currentTimeMillis() - 7 * 60 * 1000) + .ip(ip).build(); + given(dataSource.getAuth(name)).willReturn(auth); + RestoreSessionEvent event = spy(new RestoreSessionEvent(player, false)); + given(bukkitService.createAndCallEvent(any(Function.class))).willReturn(event); + + // when + boolean result = sessionService.canResumeSession(player); + + // then + assertThat(result, equalTo(true)); + verify(commonService).getProperty(PluginSettings.SESSIONS_ENABLED); + verify(commonService).getProperty(PluginSettings.SESSIONS_TIMEOUT); + verifyNoMoreInteractions(commonService); + verify(dataSource).hasSession(name); + verify(dataSource).setUnlogged(name); + verify(dataSource).revokeSession(name); + verify(event).isCancelled(); + } + + @Test + public void shouldHandleNullPlayerAuth() { + // given + String name = "Bobby"; + Player player = mockPlayerWithNameAndIp(name, "127.3.12.15"); + given(dataSource.hasSession(name)).willReturn(true); + given(dataSource.getAuth(name)).willReturn(null); + + // when + boolean result = sessionService.canResumeSession(player); + + // then + assertThat(result, equalTo(false)); + verify(commonService).getProperty(PluginSettings.SESSIONS_ENABLED); + verify(dataSource).hasSession(name); + verify(dataSource).setUnlogged(name); + verify(dataSource).revokeSession(name); + verify(dataSource).getAuth(name); + } + + private static Player mockPlayerWithNameAndIp(String name, String ip) { + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + TestHelper.mockPlayerIp(player, ip); + return player; + } +} diff --git a/src/test/resources/fr/xephi/authme/datasource/sql-initialize.sql b/src/test/resources/fr/xephi/authme/datasource/sql-initialize.sql index 8edcfe26d..5c4b5eb53 100644 --- a/src/test/resources/fr/xephi/authme/datasource/sql-initialize.sql +++ b/src/test/resources/fr/xephi/authme/datasource/sql-initialize.sql @@ -13,10 +13,12 @@ CREATE TABLE authme ( yaw FLOAT, pitch FLOAT, email VARCHAR(255) DEFAULT 'your@email.com', - isLogged INT DEFAULT '0', realname VARCHAR(255) NOT NULL DEFAULT 'Player', + isLogged INT DEFAULT '0', + realname VARCHAR(255) NOT NULL DEFAULT 'Player', salt varchar(255), recoverycode VARCHAR(20), recoveryexpiration BIGINT, + hasSession INT NOT NULL DEFAULT '0', CONSTRAINT table_const_prim PRIMARY KEY (id) );