diff --git a/src/main/java/fr/xephi/authme/datasource/MySQL.java b/src/main/java/fr/xephi/authme/datasource/MySQL.java index 3902e3e55..d60f867fc 100644 --- a/src/main/java/fr/xephi/authme/datasource/MySQL.java +++ b/src/main/java/fr/xephi/authme/datasource/MySQL.java @@ -3,14 +3,12 @@ package fr.xephi.authme.datasource; import com.google.common.annotations.VisibleForTesting; import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.pool.HikariPool.PoolInitializationException; - import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.security.HashAlgorithm; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.security.crypts.XFBCRYPT; import fr.xephi.authme.settings.NewSetting; -import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.DatabaseSettings; import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.SecuritySettings; @@ -69,7 +67,7 @@ public class MySQL implements DataSource { // Initialize the database try { - setupConnection(); + checkTablesAndColumns(); } catch (SQLException e) { close(); ConsoleLogger.logException("Can't initialize the MySQL database:", e); @@ -140,24 +138,13 @@ public class MySQL implements DataSource { return ds.getConnection(); } - private void setupConnection() throws SQLException { + private void checkTablesAndColumns() throws SQLException { try (Connection con = getConnection(); Statement st = con.createStatement()) { - // Create table if not exists. + // Create table with ID column if it doesn't exist String sql = "CREATE TABLE IF NOT EXISTS " + tableName + " (" + col.ID + " MEDIUMINT(8) UNSIGNED AUTO_INCREMENT," - + col.NAME + " VARCHAR(255) NOT NULL UNIQUE," - + col.REAL_NAME + " VARCHAR(255) NOT NULL," - + col.PASSWORD + " VARCHAR(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL," - + col.IP + " VARCHAR(40) CHARACTER SET ascii COLLATE ascii_bin NOT NULL DEFAULT '127.0.0.1'," - + col.LAST_LOGIN + " BIGINT NOT NULL DEFAULT 0," - + col.LASTLOC_X + " DOUBLE NOT NULL DEFAULT '0.0'," - + col.LASTLOC_Y + " DOUBLE NOT NULL DEFAULT '0.0'," - + col.LASTLOC_Z + " DOUBLE NOT NULL DEFAULT '0.0'," - + col.LASTLOC_WORLD + " VARCHAR(255) NOT NULL DEFAULT '" + Settings.defaultWorld + "'," - + col.EMAIL + " VARCHAR(255) DEFAULT 'your@email.com'," - + col.IS_LOGGED + " SMALLINT NOT NULL DEFAULT '0'," + "PRIMARY KEY (" + col.ID + ")" - + ") CHARACTER SET = utf8"; + + ") CHARACTER SET = utf8;"; st.executeUpdate(sql); DatabaseMetaData md = con.getMetaData(); @@ -177,8 +164,7 @@ public class MySQL implements DataSource { } if (!col.SALT.isEmpty() && isColumnMissing(md, col.SALT)) { - st.executeUpdate("ALTER TABLE " + tableName - + " ADD COLUMN " + col.SALT + " VARCHAR(255);"); + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.SALT + " VARCHAR(255);"); } if (isColumnMissing(md, col.IP)) { @@ -219,8 +205,6 @@ public class MySQL implements DataSource { st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.IS_LOGGED + " SMALLINT NOT NULL DEFAULT '0' AFTER " + col.EMAIL); } - - st.close(); } ConsoleLogger.info("MySQL setup finished"); } diff --git a/src/main/java/fr/xephi/authme/datasource/SQLite.java b/src/main/java/fr/xephi/authme/datasource/SQLite.java index d5109af26..a60dfd0dc 100644 --- a/src/main/java/fr/xephi/authme/datasource/SQLite.java +++ b/src/main/java/fr/xephi/authme/datasource/SQLite.java @@ -5,11 +5,11 @@ import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.settings.NewSetting; -import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.DatabaseSettings; import fr.xephi.authme.util.StringUtils; import java.sql.Connection; +import java.sql.DatabaseMetaData; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -69,67 +69,73 @@ public class SQLite implements DataSource { this.con = DriverManager.getConnection("jdbc:sqlite:plugins/AuthMe/" + database + ".db"); } - private void setup() throws SQLException { - Statement st = null; - ResultSet rs = null; - try { - st = con.createStatement(); - st.executeUpdate("CREATE TABLE IF NOT EXISTS " + tableName + " (" + col.ID + " INTEGER AUTO_INCREMENT," + col.NAME + " VARCHAR(255) NOT NULL UNIQUE," + col.PASSWORD + " VARCHAR(255) NOT NULL," + col.IP + " VARCHAR(40) NOT NULL," + col.LAST_LOGIN + " BIGINT," + col.LASTLOC_X + " DOUBLE NOT NULL DEFAULT '0.0'," + col.LASTLOC_Y + " DOUBLE NOT NULL DEFAULT '0.0'," + col.LASTLOC_Z + " DOUBLE NOT NULL DEFAULT '0.0'," + col.LASTLOC_WORLD + " VARCHAR(255) NOT NULL DEFAULT '" + Settings.defaultWorld + "'," + col.EMAIL + " VARCHAR(255) DEFAULT 'your@email.com'," + "CONSTRAINT table_const_prim PRIMARY KEY (" + col.ID + "));"); - rs = con.getMetaData().getColumns(null, null, tableName, col.PASSWORD); - if (!rs.next()) { - st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.PASSWORD + " VARCHAR(255) NOT NULL;"); + @VisibleForTesting + protected void setup() throws SQLException { + try (Statement st = con.createStatement()) { + // Note: cannot add unique fields later on in SQLite, so we add it on initialization + st.executeUpdate("CREATE TABLE IF NOT EXISTS " + tableName + " (" + + col.ID + " INTEGER AUTO_INCREMENT, " + + col.NAME + " VARCHAR(255) NOT NULL UNIQUE, " + + "CONSTRAINT table_const_prim PRIMARY KEY (" + col.ID + "));"); + + DatabaseMetaData md = con.getMetaData(); + + if (isColumnMissing(md, col.REAL_NAME)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + + col.REAL_NAME + " VARCHAR(255) NOT NULL DEFAULT 'Player';"); } - rs.close(); - if (!col.SALT.isEmpty()) { - rs = con.getMetaData().getColumns(null, null, tableName, col.SALT); - if (!rs.next()) { - st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.SALT + " VARCHAR(255);"); - } - rs.close(); + + if (isColumnMissing(md, col.PASSWORD)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.PASSWORD + " VARCHAR(255) NOT NULL DEFAULT '';"); } - rs = con.getMetaData().getColumns(null, null, tableName, col.IP); - if (!rs.next()) { - st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.IP + " VARCHAR(40) NOT NULL;"); + + if (!col.SALT.isEmpty() && isColumnMissing(md, col.SALT)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.SALT + " VARCHAR(255);"); } - rs.close(); - rs = con.getMetaData().getColumns(null, null, tableName, col.LAST_LOGIN); - if (!rs.next()) { - st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LAST_LOGIN + " TIMESTAMP DEFAULT current_timestamp;"); + + if (isColumnMissing(md, col.IP)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.IP + " VARCHAR(40) NOT NULL DEFAULT '';"); } - rs.close(); - rs = con.getMetaData().getColumns(null, null, tableName, col.LASTLOC_X); - if (!rs.next()) { - st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LASTLOC_X + " DOUBLE NOT NULL DEFAULT '0.0';"); - st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LASTLOC_Y + " DOUBLE NOT NULL DEFAULT '0.0';"); - st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LASTLOC_Z + " DOUBLE NOT NULL DEFAULT '0.0';"); + + if (isColumnMissing(md, col.LAST_LOGIN)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.LAST_LOGIN + " TIMESTAMP;"); } - rs.close(); - rs = con.getMetaData().getColumns(null, null, tableName, col.LASTLOC_WORLD); - if (!rs.next()) { - st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LASTLOC_WORLD + " VARCHAR(255) NOT NULL DEFAULT 'world';"); + + if (isColumnMissing(md, col.LASTLOC_X)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LASTLOC_X + + " DOUBLE NOT NULL DEFAULT '0.0';"); + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LASTLOC_Y + + " DOUBLE NOT NULL DEFAULT '0.0';"); + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LASTLOC_Z + + " DOUBLE NOT NULL DEFAULT '0.0';"); } - rs.close(); - rs = con.getMetaData().getColumns(null, null, tableName, col.EMAIL); - if (!rs.next()) { - st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.EMAIL + " VARCHAR(255) DEFAULT 'your@email.com';"); + + if (isColumnMissing(md, col.LASTLOC_WORLD)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.LASTLOC_WORLD + " VARCHAR(255) NOT NULL DEFAULT 'world';"); } - rs.close(); - rs = con.getMetaData().getColumns(null, null, tableName, col.IS_LOGGED); - if (!rs.next()) { + + if (isColumnMissing(md, col.EMAIL)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.EMAIL + " VARCHAR(255) DEFAULT 'your@email.com';"); + } + + if (isColumnMissing(md, col.IS_LOGGED)) { st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.IS_LOGGED + " INT DEFAULT '0';"); } - rs.close(); - rs = con.getMetaData().getColumns(null, null, tableName, col.REAL_NAME); - if (!rs.next()) { - st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.REAL_NAME + " VARCHAR(255) NOT NULL DEFAULT 'Player';"); - } - } finally { - close(rs); - close(st); } ConsoleLogger.info("SQLite Setup finished"); } + private boolean isColumnMissing(DatabaseMetaData metaData, String columnName) throws SQLException { + try (ResultSet rs = metaData.getColumns(null, null, tableName, columnName)) { + return !rs.next(); + } + } + @Override public void reload() { close(con); @@ -146,7 +152,7 @@ public class SQLite implements DataSource { PreparedStatement pst = null; ResultSet rs = null; try { - pst = con.prepareStatement("SELECT * FROM " + tableName + " WHERE LOWER(" + col.NAME + ")=LOWER(?);"); + pst = con.prepareStatement("SELECT 1 FROM " + tableName + " WHERE LOWER(" + col.NAME + ")=LOWER(?);"); pst.setString(1, user); rs = pst.executeQuery(); return rs.next(); diff --git a/src/main/java/fr/xephi/authme/settings/Settings.java b/src/main/java/fr/xephi/authme/settings/Settings.java index 6e8430010..d13fc4dea 100644 --- a/src/main/java/fr/xephi/authme/settings/Settings.java +++ b/src/main/java/fr/xephi/authme/settings/Settings.java @@ -18,20 +18,16 @@ public final class Settings { public static List getUnrestrictedName; public static boolean isPermissionCheckEnabled; public static boolean isTeleportToSpawnEnabled; - public static boolean isSessionsEnabled; public static boolean isAllowRestrictedIp; public static boolean isSaveQuitLocationEnabled; public static boolean protectInventoryBeforeLogInEnabled; public static boolean isStopEnabled; public static boolean reloadSupport; public static boolean noTeleport; - public static boolean isRemoveSpeedEnabled; public static String getUnloggedinGroup; public static String unRegisteredGroup; public static String getRegisteredGroup; public static String defaultWorld; - public static String crazyloginFileName; - public static int getSessionTimeout; public static int getNonActivatedGroup; private static FileConfiguration configFile; @@ -48,10 +44,7 @@ public final class Settings { private static void loadVariables() { isPermissionCheckEnabled = load(PluginSettings.ENABLE_PERMISSION_CHECK); isTeleportToSpawnEnabled = load(RestrictionSettings.TELEPORT_UNAUTHED_TO_SPAWN); - isSessionsEnabled = load(PluginSettings.SESSIONS_ENABLED); - getSessionTimeout = configFile.getInt("settings.sessions.timeout", 10); isAllowRestrictedIp = load(RestrictionSettings.ENABLE_RESTRICTED_USERS); - isRemoveSpeedEnabled = load(RestrictionSettings.REMOVE_SPEED); isSaveQuitLocationEnabled = load(RestrictionSettings.SAVE_QUIT_LOCATION); getUnloggedinGroup = load(SecuritySettings.UNLOGGEDIN_GROUP); getNonActivatedGroup = configFile.getInt("ExternalBoardOptions.nonActivedUserGroup", -1); @@ -63,7 +56,6 @@ public final class Settings { reloadSupport = configFile.getBoolean("Security.ReloadCommand.useReloadCommandSupport", true); defaultWorld = configFile.getString("Purge.defaultWorld", "world"); noTeleport = load(RestrictionSettings.NO_TELEPORT); - crazyloginFileName = configFile.getString("Converter.CrazyLogin.fileName", "accounts.db"); } /** diff --git a/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java b/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java index e9488a40e..0c0d217bb 100644 --- a/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java @@ -12,6 +12,9 @@ import java.util.Set; import static fr.xephi.authme.AuthMeMatchers.equalToHash; import static fr.xephi.authme.AuthMeMatchers.hasAuthBasicData; import static fr.xephi.authme.AuthMeMatchers.hasAuthLocation; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasSize; @@ -170,6 +173,22 @@ public abstract class AbstractDataSourceIntegrationTest { assertThat(dataSource.getPassword("user"), equalToHash("new_hash")); } + @Test + public void shouldUpdatePasswordWithPlayerAuth() { + // given + DataSource dataSource = getDataSource("salt"); + PlayerAuth bobbyAuth = PlayerAuth.builder().name("bobby").password(new HashedPassword("tt", "cc")).build(); + PlayerAuth invalidAuth = PlayerAuth.builder().name("invalid").password(new HashedPassword("tt", "cc")).build(); + + // when + boolean response1 = dataSource.updatePassword(bobbyAuth); + boolean response2 = dataSource.updatePassword(invalidAuth); + + // then + assertThat(response1 && response2, equalTo(true)); + assertThat(dataSource.getPassword("bobby"), equalToHash("tt", "cc")); + } + @Test public void shouldRemovePlayerAuth() { // given @@ -321,4 +340,49 @@ public abstract class AbstractDataSourceIntegrationTest { assertThat(dataSource.getAuth("bobby"), hasAuthBasicData("bobby", "BOBBY", "your@email.com", "123.45.67.89")); } + @Test + public void shouldGetRecordsToPurge() { + // given + DataSource dataSource = getDataSource(); + // 1453242857 -> user, 1449136800 -> bobby + + // when + Set records1 = dataSource.getRecordsToPurge(1450000000); + Set records2 = dataSource.getRecordsToPurge(1460000000); + + // then + assertThat(records1, contains("bobby")); + assertThat(records2, containsInAnyOrder("bobby", "user")); + // check that the entry was not deleted because of running this command + assertThat(dataSource.isAuthAvailable("bobby"), equalTo(true)); + } + + @Test + public void shouldPerformOperationsOnIsLoggedColumnSuccessfully() { + DataSource dataSource = getDataSource(); + // on startup no one should be marked as logged + assertThat(dataSource.getLoggedPlayers(), empty()); + + // Mark user as logged + dataSource.setLogged("user"); + // non-existent user should not break database + dataSource.setLogged("does-not-exist"); + + assertThat(dataSource.isLogged("user"), equalTo(true)); + assertThat(dataSource.isLogged("bobby"), equalTo(false)); + + // Set bobby logged and unlog user + dataSource.setLogged("bobby"); + dataSource.setUnlogged("user"); + assertThat(dataSource.getLoggedPlayers(), + contains(hasAuthBasicData("bobby", "Bobby", "your@email.com", "123.45.67.89"))); + + // Set both as logged (even if Bobby already is logged) + dataSource.setLogged("user"); + dataSource.setLogged("bobby"); + dataSource.purgeLogged(); + assertThat(dataSource.isLogged("user"), equalTo(false)); + assertThat(dataSource.getLoggedPlayers(), empty()); + } + } diff --git a/src/test/java/fr/xephi/authme/datasource/MySqlIntegrationTest.java b/src/test/java/fr/xephi/authme/datasource/MySqlIntegrationTest.java index 397693069..8289d8373 100644 --- a/src/test/java/fr/xephi/authme/datasource/MySqlIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/datasource/MySqlIntegrationTest.java @@ -6,6 +6,7 @@ import fr.xephi.authme.TestHelper; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.domain.Property; import fr.xephi.authme.settings.properties.DatabaseSettings; +import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.mockito.invocation.InvocationOnMock; @@ -60,7 +61,6 @@ public class MySqlIntegrationTest extends AbstractDataSourceIntegrationTest { @Before public void initializeConnectionAndTable() throws SQLException { - silentClose(hikariSource); HikariConfig config = new HikariConfig(); config.setDataSourceClassName("org.h2.jdbcx.JdbcDataSource"); config.setConnectionTestQuery("VALUES 1"); @@ -77,6 +77,11 @@ public class MySqlIntegrationTest extends AbstractDataSourceIntegrationTest { hikariSource = ds; } + @After + public void closeConnection() { + silentClose(hikariSource); + } + @Override protected DataSource getDataSource(String saltColumn) { when(settings.getProperty(DatabaseSettings.MYSQL_COL_SALT)).thenReturn(saltColumn); diff --git a/src/test/java/fr/xephi/authme/datasource/SQLiteIntegrationTest.java b/src/test/java/fr/xephi/authme/datasource/SQLiteIntegrationTest.java index a654fca66..76d01655b 100644 --- a/src/test/java/fr/xephi/authme/datasource/SQLiteIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/datasource/SQLiteIntegrationTest.java @@ -1,11 +1,14 @@ package fr.xephi.authme.datasource; import fr.xephi.authme.TestHelper; +import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.settings.NewSetting; import fr.xephi.authme.settings.domain.Property; import fr.xephi.authme.settings.properties.DatabaseSettings; +import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Test; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -17,6 +20,8 @@ import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.Assert.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -61,7 +66,6 @@ public class SQLiteIntegrationTest extends AbstractDataSourceIntegrationTest { @Before public void initializeConnectionAndTable() throws SQLException { - silentClose(con); Connection connection = DriverManager.getConnection("jdbc:sqlite::memory:"); try (Statement st = connection.createStatement()) { st.execute("DROP TABLE IF EXISTS authme"); @@ -72,6 +76,55 @@ public class SQLiteIntegrationTest extends AbstractDataSourceIntegrationTest { con = connection; } + @After + public void closeConnection() { + silentClose(con); + } + + @Test + public void shouldSetUpTableIfMissing() throws SQLException { + // given + Statement st = con.createStatement(); + // table is absent + st.execute("DROP TABLE authme"); + SQLite sqLite = new SQLite(settings, con); + + // when + sqLite.setup(); + + // then + // Save some player to verify database is operational + sqLite.saveAuth(PlayerAuth.builder().name("Name").build()); + assertThat(sqLite.getAllAuths(), hasSize(1)); + } + + @Test + public void shouldCreateMissingColumns() throws SQLException { + // given + Statement st = con.createStatement(); + // drop table and create one with only some of the columns: SQLite doesn't support ALTER TABLE t DROP COLUMN c + st.execute("DROP TABLE authme"); + st.execute("CREATE TABLE authme (" + + "id bigint, " + + "username varchar(255) unique, " + + "password varchar(255) not null, " + + "primary key (id));"); + SQLite sqLite = new SQLite(settings, con); + + // when + sqLite.setup(); + + // then + // Save some player to verify database is operational + sqLite.saveAuth(PlayerAuth.builder().name("Name").build()); + assertThat(sqLite.getAllAuths(), hasSize(1)); + } + + @Test + public void shouldCreate2() throws SQLException { + con.createStatement().execute("SELECT 1 from authme"); + } + @Override protected DataSource getDataSource(String saltColumn) { when(settings.getProperty(DatabaseSettings.MYSQL_COL_SALT)).thenReturn(saltColumn);