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 34dfa94f6..0b88227a1 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,39 +51,30 @@ 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) + .registrationDate(System.currentTimeMillis()) + .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) - .registrationDate(System.currentTimeMillis()) - .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 7ef4908ad..3eaa815ce 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 92127d971..946c33de6 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 final String REGISTRATION_DATE; public final String REGISTRATION_IP; @@ -46,6 +47,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); REGISTRATION_DATE = settings.getProperty(DatabaseSettings.MYSQL_COL_REGISTER_DATE); REGISTRATION_IP = settings.getProperty(DatabaseSettings.MYSQL_COL_REGISTER_IP); } diff --git a/src/main/java/fr/xephi/authme/datasource/DataSource.java b/src/main/java/fr/xephi/authme/datasource/DataSource.java index d491a4921..c040b0808 100644 --- a/src/main/java/fr/xephi/authme/datasource/DataSource.java +++ b/src/main/java/fr/xephi/authme/datasource/DataSource.java @@ -159,6 +159,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 e439f9122..0ace15b1d 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 45dc9ab50..845ee4825 100644 --- a/src/main/java/fr/xephi/authme/datasource/MySQL.java +++ b/src/main/java/fr/xephi/authme/datasource/MySQL.java @@ -248,6 +248,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"); } @@ -575,6 +580,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 aebf05e87..1a709fae6 100644 --- a/src/main/java/fr/xephi/authme/datasource/SQLite.java +++ b/src/main/java/fr/xephi/authme/datasource/SQLite.java @@ -145,7 +145,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"); @@ -442,7 +448,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()) { @@ -471,14 +477,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); } } @@ -607,14 +651,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 2120fa5aa..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,32 +174,6 @@ public class AsynchronousJoin implements AsynchronousProcess { }); } - // TODO #792: lastlogin date might be null (not updating now because of has_session branch changes) - - 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.getLastIp().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 50d53aeaa..a32a88459 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 e376ccd03..534b754b0 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..ef51533ba --- /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 PlayerUtils.getPlayerIp(player).equals(auth.getLastIp()) + && 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 be4fcdae1..31c629833 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 the player's last IP") public static final Property MYSQL_COL_LAST_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 b452161cc..f72c92ae7 100644 --- a/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java @@ -435,4 +435,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..a560e7644 --- /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) + .lastIp(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(0L) + .lastIp(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()) + .lastIp("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) + .lastIp(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 699acf6db..306df4769 100644 --- a/src/test/resources/fr/xephi/authme/datasource/sql-initialize.sql +++ b/src/test/resources/fr/xephi/authme/datasource/sql-initialize.sql @@ -18,6 +18,7 @@ CREATE TABLE authme ( isLogged INT DEFAULT '0', realname VARCHAR(255) NOT NULL DEFAULT 'Player', salt varchar(255), + hasSession INT NOT NULL DEFAULT '0', CONSTRAINT table_const_prim PRIMARY KEY (id) );