#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
This commit is contained in:
Gabriele C 2017-10-15 18:32:51 +02:00 committed by ljacqu
parent 1487fc0d9e
commit 22e95493de
19 changed files with 537 additions and 106 deletions

View File

@ -1,5 +1,5 @@
<!-- AUTO-GENERATED FILE! Do not edit this directly --> <!-- AUTO-GENERATED FILE! Do not edit this directly -->
<!-- File auto-generated on Mon Oct 09 10:19:07 CEST 2017. See docs/config/config.tpl.md --> <!-- File auto-generated on Tue Oct 10 13:51:56 CEST 2017. See docs/config/config.tpl.md -->
## AuthMe Configuration ## AuthMe Configuration
The first time you run AuthMe it will create a config.yml file in the plugins/AuthMe folder, 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' mySQLColumnEmail: 'email'
# Column for storing if a player is logged in or not # Column for storing if a player is logged in or not
mySQLColumnLogged: 'isLogged' mySQLColumnLogged: 'isLogged'
# Column for storing if a player has a valid session or not
mySQLColumnHasSession: 'hasSession'
# Column for storing players ips # Column for storing players ips
mySQLColumnIp: 'ip' mySQLColumnIp: 'ip'
# Column for storing players lastlogins # 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

View File

@ -30,7 +30,6 @@ import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.MigrationService; import fr.xephi.authme.service.MigrationService;
import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.EmailSettings; 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.RestrictionSettings;
import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.settings.properties.SecuritySettings;
import fr.xephi.authme.task.CleanupTask; 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!"); 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 // Use TLS property only affects port 25
if (!settings.getProperty(EmailSettings.PORT25_USE_TLS) if (!settings.getProperty(EmailSettings.PORT25_USE_TLS)
&& settings.getProperty(EmailSettings.SMTP_PORT) != 25) { && settings.getProperty(EmailSettings.SMTP_PORT) != 25) {

View File

@ -51,38 +51,29 @@ public class RegisterAdminCommand implements ExecutableCommand {
return; 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 if (!dataSource.saveAuth(auth)) {
public void run() { commonService.send(sender, MessageKey.ERROR);
if (dataSource.isAuthAvailable(playerNameLowerCase)) { return;
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.REGISTER_SUCCESS);
commonService.send(sender, MessageKey.ERROR); ConsoleLogger.info(sender.getName() + " registered " + playerName);
return; final Player player = bukkitService.getPlayerExact(playerName);
} if (player != null) {
dataSource.setUnlogged(playerNameLowerCase); bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(() ->
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(new Runnable() {
@Override
public void run() {
player.kickPlayer(commonService.retrieveSingleMessage(MessageKey.KICK_FOR_ADMIN_REGISTER));
}
});
}
} }
}); });
} }

View File

@ -208,6 +208,21 @@ public class CacheDataSource implements DataSource {
source.setUnlogged(user.toLowerCase()); 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 @Override
public void purgeLogged() { public void purgeLogged() {
source.purgeLogged(); source.purgeLogged();

View File

@ -26,6 +26,7 @@ public final class Columns {
public final String EMAIL; public final String EMAIL;
public final String ID; public final String ID;
public final String IS_LOGGED; public final String IS_LOGGED;
public final String HAS_SESSION;
public Columns(Settings settings) { public Columns(Settings settings) {
NAME = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME); NAME = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME);
@ -44,6 +45,7 @@ public final class Columns {
EMAIL = settings.getProperty(DatabaseSettings.MYSQL_COL_EMAIL); EMAIL = settings.getProperty(DatabaseSettings.MYSQL_COL_EMAIL);
ID = settings.getProperty(DatabaseSettings.MYSQL_COL_ID); ID = settings.getProperty(DatabaseSettings.MYSQL_COL_ID);
IS_LOGGED = settings.getProperty(DatabaseSettings.MYSQL_COL_ISLOGGED); IS_LOGGED = settings.getProperty(DatabaseSettings.MYSQL_COL_ISLOGGED);
HAS_SESSION = settings.getProperty(DatabaseSettings.MYSQL_COL_HASSESSION);
} }
} }

View File

@ -161,6 +161,29 @@ public interface DataSource extends Reloadable {
*/ */
void setUnlogged(String user); 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. * Set all players who are marked as logged in as NOT logged in.
*/ */

View File

@ -329,6 +329,19 @@ public class FlatFile implements DataSource {
public void setUnlogged(String user) { 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 @Override
public void purgeLogged() { public void purgeLogged() {
} }

View File

@ -237,6 +237,11 @@ public class MySQL implements DataSource {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
+ col.IS_LOGGED + " SMALLINT NOT NULL DEFAULT '0' AFTER " + col.EMAIL); + 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"); 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 @Override
public void purgeLogged() { public void purgeLogged() {
String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE " + col.IS_LOGGED + "=?;"; String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE " + col.IS_LOGGED + "=?;";

View File

@ -134,7 +134,13 @@ public class SQLite implements DataSource {
} }
if (isColumnMissing(md, col.IS_LOGGED)) { 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"); ConsoleLogger.info("SQLite Setup finished");
@ -426,7 +432,7 @@ public class SQLite implements DataSource {
@Override @Override
public boolean isLogged(String user) { 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)) { try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user); pst.setString(1, user);
try (ResultSet rs = pst.executeQuery()) { try (ResultSet rs = pst.executeQuery()) {
@ -455,14 +461,52 @@ public class SQLite implements DataSource {
@Override @Override
public void setUnlogged(String user) { public void setUnlogged(String user) {
String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE LOWER(" + col.NAME + ")=?;"; String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE LOWER(" + col.NAME + ")=?;";
if (user != null) { try (PreparedStatement pst = con.prepareStatement(sql)) {
try (PreparedStatement pst = con.prepareStatement(sql)) { pst.setInt(1, 0);
pst.setInt(1, 0); pst.setString(2, user);
pst.setString(2, user); pst.executeUpdate();
pst.executeUpdate(); } catch (SQLException ex) {
} catch (SQLException ex) { logSqlException(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(); 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) { private static void close(Connection con) {
if (con != null) { if (con != null) {
try { 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);
}
}
}
} }

View File

@ -1,12 +1,9 @@
package fr.xephi.authme.process.join; package fr.xephi.authme.process.join;
import fr.xephi.authme.ConsoleLogger; 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.data.limbo.LimboService;
import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.events.ProtectInventoryEvent; import fr.xephi.authme.events.ProtectInventoryEvent;
import fr.xephi.authme.events.RestoreSessionEvent;
import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.permission.PlayerStatePermission; import fr.xephi.authme.permission.PlayerStatePermission;
import fr.xephi.authme.process.AsynchronousProcess; 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.BukkitService;
import fr.xephi.authme.service.CommonService; import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.service.PluginHookService; import fr.xephi.authme.service.PluginHookService;
import fr.xephi.authme.service.SessionService;
import fr.xephi.authme.service.ValidationService; import fr.xephi.authme.service.ValidationService;
import fr.xephi.authme.settings.WelcomeMessageConfiguration; import fr.xephi.authme.settings.WelcomeMessageConfiguration;
import fr.xephi.authme.settings.commandconfig.CommandManager; import fr.xephi.authme.settings.commandconfig.CommandManager;
import fr.xephi.authme.settings.properties.HooksSettings; 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.RegistrationSettings;
import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.settings.properties.RestrictionSettings;
import fr.xephi.authme.util.PlayerUtils; import fr.xephi.authme.util.PlayerUtils;
@ -48,9 +45,6 @@ public class AsynchronousJoin implements AsynchronousProcess {
@Inject @Inject
private CommonService service; private CommonService service;
@Inject
private PlayerCache playerCache;
@Inject @Inject
private LimboService limboService; private LimboService limboService;
@ -72,6 +66,9 @@ public class AsynchronousJoin implements AsynchronousProcess {
@Inject @Inject
private WelcomeMessageConfiguration welcomeMessageConfiguration; private WelcomeMessageConfiguration welcomeMessageConfiguration;
@Inject
private SessionService sessionService;
AsynchronousJoin() { AsynchronousJoin() {
} }
@ -121,7 +118,7 @@ public class AsynchronousJoin implements AsynchronousProcess {
} }
// Session logic // Session logic
if (canResumeSession(player)) { if (sessionService.canResumeSession(player)) {
service.send(player, MessageKey.SESSION_RECONNECTION); service.send(player, MessageKey.SESSION_RECONNECTION);
// Run commands // Run commands
bukkitService.scheduleSyncTaskFromOptionallyAsyncTask( 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 * 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. * settings and permissions). If this is the case, the player is kicked.

View File

@ -20,6 +20,7 @@ import fr.xephi.authme.process.SyncProcessManager;
import fr.xephi.authme.security.PasswordSecurity; import fr.xephi.authme.security.PasswordSecurity;
import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.CommonService; 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.DatabaseSettings;
import fr.xephi.authme.settings.properties.EmailSettings; import fr.xephi.authme.settings.properties.EmailSettings;
import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.HooksSettings;
@ -69,6 +70,9 @@ public class AsynchronousLogin implements AsynchronousProcess {
@Inject @Inject
private EmailService emailService; private EmailService emailService;
@Inject
private SessionService sessionService;
AsynchronousLogin() { AsynchronousLogin() {
} }
@ -238,9 +242,10 @@ public class AsynchronousLogin implements AsynchronousProcess {
ConsoleLogger.fine(player.getName() + " logged in!"); ConsoleLogger.fine(player.getName() + " logged in!");
// makes player isLoggedin via API // makes player loggedin
playerCache.updatePlayer(auth); playerCache.updatePlayer(auth);
dataSource.setLogged(name); dataSource.setLogged(name);
sessionService.grantSession(name);
// As the scheduling executes the Task most likely after the current // As the scheduling executes the Task most likely after the current
// task, we schedule it in the end // task, we schedule it in the end

View File

@ -53,6 +53,7 @@ public class AsynchronousLogout implements AsynchronousProcess {
playerCache.removePlayer(name); playerCache.removePlayer(name);
database.setUnlogged(name); database.setUnlogged(name);
database.revokeSession(name);
syncProcessManager.processSyncPlayerLogout(player); syncProcessManager.processSyncPlayerLogout(player);
} }
} }

View File

@ -82,8 +82,11 @@ public class AsynchronousQuit implements AsynchronousProcess {
playerCache.removePlayer(name); playerCache.removePlayer(name);
//always update the database when the player quit the game (if sessions are disabled) //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); database.setUnlogged(name);
if (!service.getProperty(PluginSettings.SESSIONS_ENABLED)) {
database.revokeSession(name);
}
} }
if (plugin.isEnabled()) { if (plugin.isEnabled()) {

View File

@ -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);
}
}

View File

@ -75,6 +75,10 @@ public final class DatabaseSettings implements SettingsHolder {
public static final Property<String> MYSQL_COL_ISLOGGED = public static final Property<String> MYSQL_COL_ISLOGGED =
newProperty("DataSource.mySQLColumnLogged", "isLogged"); newProperty("DataSource.mySQLColumnLogged", "isLogged");
@Comment("Column for storing if a player has a valid session or not")
public static final Property<String> MYSQL_COL_HASSESSION =
newProperty("DataSource.mySQLColumnHasSession", "hasSession");
@Comment("Column for storing players ips") @Comment("Column for storing players ips")
public static final Property<String> MYSQL_COL_IP = public static final Property<String> MYSQL_COL_IP =
newProperty("DataSource.mySQLColumnIp", "ip"); newProperty("DataSource.mySQLColumnIp", "ip");

View File

@ -144,7 +144,6 @@ public class RegisterAdminCommandTest {
ArgumentCaptor<PlayerAuth> captor = ArgumentCaptor.forClass(PlayerAuth.class); ArgumentCaptor<PlayerAuth> captor = ArgumentCaptor.forClass(PlayerAuth.class);
verify(dataSource).saveAuth(captor.capture()); verify(dataSource).saveAuth(captor.capture());
assertAuthHasInfo(captor.getValue(), user, hashedPassword); assertAuthHasInfo(captor.getValue(), user, hashedPassword);
verify(dataSource).setUnlogged(user);
} }
@Test @Test
@ -174,7 +173,6 @@ public class RegisterAdminCommandTest {
ArgumentCaptor<PlayerAuth> captor = ArgumentCaptor.forClass(PlayerAuth.class); ArgumentCaptor<PlayerAuth> captor = ArgumentCaptor.forClass(PlayerAuth.class);
verify(dataSource).saveAuth(captor.capture()); verify(dataSource).saveAuth(captor.capture());
assertAuthHasInfo(captor.getValue(), user, hashedPassword); assertAuthHasInfo(captor.getValue(), user, hashedPassword);
verify(dataSource).setUnlogged(user);
verify(player).kickPlayer(kickForAdminRegister); verify(player).kickPlayer(kickForAdminRegister);
} }

View File

@ -416,4 +416,36 @@ public abstract class AbstractDataSourceIntegrationTest {
// then // then
assertThat(loggedPlayersWithEmptyMail, contains("Bobby")); 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));
}
} }

View File

@ -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;
}
}

View File

@ -13,10 +13,12 @@ CREATE TABLE authme (
yaw FLOAT, yaw FLOAT,
pitch FLOAT, pitch FLOAT,
email VARCHAR(255) DEFAULT 'your@email.com', 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), salt varchar(255),
recoverycode VARCHAR(20), recoverycode VARCHAR(20),
recoveryexpiration BIGINT, recoveryexpiration BIGINT,
hasSession INT NOT NULL DEFAULT '0',
CONSTRAINT table_const_prim PRIMARY KEY (id) CONSTRAINT table_const_prim PRIMARY KEY (id)
); );