diff --git a/core/src/main/java/com/github/games647/fastlogin/core/shared/FastLoginCore.java b/core/src/main/java/com/github/games647/fastlogin/core/shared/FastLoginCore.java index ec3f924d..a5dfd05d 100644 --- a/core/src/main/java/com/github/games647/fastlogin/core/shared/FastLoginCore.java +++ b/core/src/main/java/com/github/games647/fastlogin/core/shared/FastLoginCore.java @@ -27,12 +27,14 @@ package com.github.games647.fastlogin.core.shared; import com.github.games647.craftapi.resolver.MojangResolver; import com.github.games647.craftapi.resolver.http.RotatingProxySelector; -import com.github.games647.fastlogin.core.AuthStorage; +import com.github.games647.fastlogin.core.storage.MySQLStorage; +import com.github.games647.fastlogin.core.storage.SQLStorage; import com.github.games647.fastlogin.core.CommonUtil; import com.github.games647.fastlogin.core.RateLimiter; import com.github.games647.fastlogin.core.hooks.AuthPlugin; import com.github.games647.fastlogin.core.hooks.DefaultPasswordGenerator; import com.github.games647.fastlogin.core.hooks.PasswordGenerator; +import com.github.games647.fastlogin.core.storage.SQLiteStorage; import com.google.common.net.HostAndPort; import com.zaxxer.hikari.HikariConfig; @@ -82,7 +84,7 @@ public class FastLoginCore

> { private final MojangResolver resolver = new MojangResolver(); private Configuration config; - private AuthStorage storage; + private SQLStorage storage; private RateLimiter rateLimiter; private PasswordGenerator

passwordGenerator = new DefaultPasswordGenerator<>(); private AuthPlugin

authPlugin; @@ -169,7 +171,7 @@ public class FastLoginCore

> { return resolver; } - public AuthStorage getStorage() { + public SQLStorage getStorage() { return storage; } @@ -189,26 +191,31 @@ public class FastLoginCore

> { } public boolean setupDatabase() { - if (!checkDriver(config.getString("driver"))) { + String driver = config.getString("driver"); + if (!checkDriver(driver)) { return false; } HikariConfig databaseConfig = new HikariConfig(); - databaseConfig.setDriverClassName(config.getString("driver")); + databaseConfig.setDriverClassName(driver); - String host = config.get("host", ""); - int port = config.get("port", 3306); String database = config.getString("database"); - boolean useSSL = config.get("useSSL", false); - - databaseConfig.setUsername(config.get("username", "")); - databaseConfig.setPassword(config.getString("password")); - databaseConfig.setConnectionTimeout(config.getInt("timeout", 30) * 1_000L); databaseConfig.setMaxLifetime(config.getInt("lifetime", 30) * 1_000L); - storage = new AuthStorage(this, host, port, database, databaseConfig, useSSL); + if (driver.contains("sqlite")) { + storage = new SQLiteStorage(this, database, databaseConfig); + } else { + String host = config.get("host", ""); + int port = config.get("port", 3306); + boolean useSSL = config.get("useSSL", false); + + databaseConfig.setUsername(config.get("username", "")); + databaseConfig.setPassword(config.getString("password")); + storage = new MySQLStorage(this, host, port, database, databaseConfig, useSSL); + } + try { storage.createTables(); return true; diff --git a/core/src/main/java/com/github/games647/fastlogin/core/shared/ForceLoginManagement.java b/core/src/main/java/com/github/games647/fastlogin/core/shared/ForceLoginManagement.java index 7efb157a..0aee4f43 100644 --- a/core/src/main/java/com/github/games647/fastlogin/core/shared/ForceLoginManagement.java +++ b/core/src/main/java/com/github/games647/fastlogin/core/shared/ForceLoginManagement.java @@ -25,7 +25,7 @@ */ package com.github.games647.fastlogin.core.shared; -import com.github.games647.fastlogin.core.AuthStorage; +import com.github.games647.fastlogin.core.storage.SQLStorage; import com.github.games647.fastlogin.core.StoredProfile; import com.github.games647.fastlogin.core.hooks.AuthPlugin; import com.github.games647.fastlogin.core.shared.event.FastLoginAutoLoginEvent; @@ -55,7 +55,7 @@ public abstract class ForceLoginManagement

core, String host, int port, String database, HikariConfig config, boolean useSSL) { + super(core, + "mysql://" + host + ':' + port + '/' + database, + setParams(config, useSSL)); + } + + private static HikariConfig setParams(HikariConfig config, boolean useSSL) { + // Require SSL on the server if requested in config - this will also verify certificate + // Those values are deprecated in favor of sslMode + config.addDataSourceProperty("useSSL", useSSL); + config.addDataSourceProperty("requireSSL", useSSL); + + // prefer encrypted if possible + config.addDataSourceProperty("sslMode", "PREFERRED"); + + // adding paranoid hides hostname, username, version and so + // could be useful for hiding server details + config.addDataSourceProperty("paranoid", true); + + // enable MySQL specific optimizations + // disabled by default - will return the same prepared statement instance + config.addDataSourceProperty("cachePrepStmts", true); + // default prepStmtCacheSize 25 - amount of cached statements + config.addDataSourceProperty("prepStmtCacheSize", 250); + // default prepStmtCacheSqlLimit 256 - length of SQL + config.addDataSourceProperty("prepStmtCacheSqlLimit", 2048); + // default false - available in newer versions caches the statements server-side + config.addDataSourceProperty("useServerPrepStmts", true); + // default false - prefer use of local values for autocommit and + // transaction isolation (alwaysSendSetIsolation) should only be enabled if always use the set* methods + // instead of raw SQL + // https://forums.mysql.com/read.php?39,626495,626512 + config.addDataSourceProperty("useLocalSessionState", true); + // rewrite batched statements to a single statement, adding them behind each other + // only useful for addBatch statements and inserts + config.addDataSourceProperty("rewriteBatchedStatements", true); + // cache result metadata + config.addDataSourceProperty("cacheResultSetMetadata", true); + // cache results of show variables and collation per URL + config.addDataSourceProperty("cacheServerConfiguration", true); + // default false - set auto commit only if not matching + config.addDataSourceProperty("elideSetAutoCommits", true); + + // default true - internal timers for idle calculation -> removes System.getCurrentTimeMillis call per query + // Some platforms are slow on this and it could affect the throughput about 3% according to MySQL + // performance gems presentation + // In our case it can be useful to see the time in error messages + // config.addDataSourceProperty("maintainTimeStats", false); + + return config; + } +} diff --git a/core/src/main/java/com/github/games647/fastlogin/core/AuthStorage.java b/core/src/main/java/com/github/games647/fastlogin/core/storage/SQLStorage.java similarity index 55% rename from core/src/main/java/com/github/games647/fastlogin/core/AuthStorage.java rename to core/src/main/java/com/github/games647/fastlogin/core/storage/SQLStorage.java index 94500cba..33007b7d 100644 --- a/core/src/main/java/com/github/games647/fastlogin/core/AuthStorage.java +++ b/core/src/main/java/com/github/games647/fastlogin/core/storage/SQLStorage.java @@ -23,9 +23,10 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -package com.github.games647.fastlogin.core; +package com.github.games647.fastlogin.core.storage; import com.github.games647.craftapi.UUIDAdapter; +import com.github.games647.fastlogin.core.StoredProfile; import com.github.games647.fastlogin.core.shared.FastLoginCore; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; @@ -42,23 +43,34 @@ import java.util.concurrent.ThreadFactory; import static java.sql.Statement.RETURN_GENERATED_KEYS; -public class AuthStorage { +public abstract class SQLStorage implements AuthStorage { - private static final String PREMIUM_TABLE = "premium"; + private static final String JDBC_PROTOCOL = "jdbc:"; - private static final String LOAD_BY_NAME = "SELECT * FROM `" + PREMIUM_TABLE + "` WHERE `Name`=? LIMIT 1"; - private static final String LOAD_BY_UUID = "SELECT * FROM `" + PREMIUM_TABLE + "` WHERE `UUID`=? LIMIT 1"; - private static final String INSERT_PROFILE = "INSERT INTO `" + PREMIUM_TABLE + protected static final String PREMIUM_TABLE = "premium"; + protected static final String CREATE_TABLE_STMT = "CREATE TABLE IF NOT EXISTS `" + PREMIUM_TABLE + "` (" + + "`UserID` INTEGER PRIMARY KEY AUTO_INCREMENT, " + + "`UUID` CHAR(36), " + + "`Name` VARCHAR(16) NOT NULL, " + + "`Premium` BOOLEAN NOT NULL, " + + "`LastIp` VARCHAR(255) NOT NULL, " + + "`LastLogin` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + //the premium shouldn't steal the cracked account by changing the name + + "UNIQUE (`Name`) " + + ')'; + + protected static final String LOAD_BY_NAME = "SELECT * FROM `" + PREMIUM_TABLE + "` WHERE `Name`=? LIMIT 1"; + protected static final String LOAD_BY_UUID = "SELECT * FROM `" + PREMIUM_TABLE + "` WHERE `UUID`=? LIMIT 1"; + protected static final String INSERT_PROFILE = "INSERT INTO `" + PREMIUM_TABLE + "` (`UUID`, `Name`, `Premium`, `LastIp`) " + "VALUES (?, ?, ?, ?) "; // limit not necessary here, because it's unique - private static final String UPDATE_PROFILE = "UPDATE `" + PREMIUM_TABLE + protected static final String UPDATE_PROFILE = "UPDATE `" + PREMIUM_TABLE + "` SET `UUID`=?, `Name`=?, `Premium`=?, `LastIp`=?, `LastLogin`=CURRENT_TIMESTAMP WHERE `UserID`=?"; - private final FastLoginCore core; - private final HikariDataSource dataSource; + protected final FastLoginCore core; + protected final HikariDataSource dataSource; - public AuthStorage(FastLoginCore core, String host, int port, String databasePath, - HikariConfig config, boolean useSSL) { + public SQLStorage(FastLoginCore core, String jdbcURL, HikariConfig config) { this.core = core; config.setPoolName(core.getPlugin().getName()); @@ -67,68 +79,7 @@ public class AuthStorage { config.setThreadFactory(platformThreadFactory); } - String jdbcUrl = "jdbc:"; - if (config.getDriverClassName().contains("sqlite")) { - String pluginFolder = core.getPlugin().getPluginFolder().toAbsolutePath().toString(); - databasePath = databasePath.replace("{pluginDir}", pluginFolder); - - jdbcUrl += "sqlite://" + databasePath; - config.setConnectionTestQuery("SELECT 1"); - config.setMaximumPoolSize(1); - - //a try to fix https://www.spigotmc.org/threads/fastlogin.101192/page-26#post-1874647 - // format strings retrieved by the timestamp column to match them from MySQL - config.addDataSourceProperty("date_string_format", "yyyy-MM-dd HH:mm:ss"); - - // TODO: test first for compatibility - // config.addDataSourceProperty("date_precision", "seconds"); - } else { - jdbcUrl += "mysql://" + host + ':' + port + '/' + databasePath; - - // Require SSL on the server if requested in config - this will also verify certificate - // Those values are deprecated in favor of sslMode - config.addDataSourceProperty("useSSL", useSSL); - config.addDataSourceProperty("requireSSL", useSSL); - - // prefer encrypted if possible - config.addDataSourceProperty("sslMode", "PREFERRED"); - - // adding paranoid hides hostname, username, version and so - // could be useful for hiding server details - config.addDataSourceProperty("paranoid", true); - - // enable MySQL specific optimizations - // disabled by default - will return the same prepared statement instance - config.addDataSourceProperty("cachePrepStmts", true); - // default prepStmtCacheSize 25 - amount of cached statements - config.addDataSourceProperty("prepStmtCacheSize", 250); - // default prepStmtCacheSqlLimit 256 - length of SQL - config.addDataSourceProperty("prepStmtCacheSqlLimit", 2048); - // default false - available in newer versions caches the statements server-side - config.addDataSourceProperty("useServerPrepStmts", true); - // default false - prefer use of local values for autocommit and - // transaction isolation (alwaysSendSetIsolation) should only be enabled if always use the set* methods - // instead of raw SQL - // https://forums.mysql.com/read.php?39,626495,626512 - config.addDataSourceProperty("useLocalSessionState", true); - // rewrite batched statements to a single statement, adding them behind each other - // only useful for addBatch statements and inserts - config.addDataSourceProperty("rewriteBatchedStatements", true); - // cache result metadata - config.addDataSourceProperty("cacheResultSetMetadata", true); - // cache results of show variables and collation per URL - config.addDataSourceProperty("cacheServerConfiguration", true); - // default false - set auto commit only if not matching - config.addDataSourceProperty("elideSetAutoCommits", true); - - // default true - internal timers for idle calculation -> removes System.getCurrentTimeMillis call per query - // Some platforms are slow on this and it could affect the throughput about 3% according to MySQL - // performance gems presentation - // In our case it can be useful to see the time in error messages - // config.addDataSourceProperty("maintainTimeStats", false); - } - - config.setJdbcUrl(jdbcUrl); + config.setJdbcUrl(JDBC_PROTOCOL + jdbcURL); this.dataSource = new HikariDataSource(config); } @@ -136,28 +87,15 @@ public class AuthStorage { // choose surrogate PK(ID), because UUID can be null for offline players // if UUID is always Premium UUID we would have to update offline player entries on insert // name cannot be PK, because it can be changed for premium players - String createDataStmt = "CREATE TABLE IF NOT EXISTS `" + PREMIUM_TABLE + "` (" - + "`UserID` INTEGER PRIMARY KEY AUTO_INCREMENT, " - + "`UUID` CHAR(36), " - + "`Name` VARCHAR(16) NOT NULL, " - + "`Premium` BOOLEAN NOT NULL, " - + "`LastIp` VARCHAR(255) NOT NULL, " - + "`LastLogin` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " - //the premium shouldn't steal the cracked account by changing the name - + "UNIQUE (`Name`) " - + ')'; - - if (dataSource.getJdbcUrl().contains("sqlite")) { - createDataStmt = createDataStmt.replace("AUTO_INCREMENT", "AUTOINCREMENT"); - } //todo: add unique uuid index usage try (Connection con = dataSource.getConnection(); Statement createStmt = con.createStatement()) { - createStmt.executeUpdate(createDataStmt); + createStmt.executeUpdate(CREATE_TABLE_STMT); } } + @Override public StoredProfile loadProfile(String name) { try (Connection con = dataSource.getConnection(); PreparedStatement loadStmt = con.prepareStatement(LOAD_BY_NAME) @@ -174,6 +112,7 @@ public class AuthStorage { return null; } + @Override public StoredProfile loadProfile(UUID uuid) { try (Connection con = dataSource.getConnection(); PreparedStatement loadStmt = con.prepareStatement(LOAD_BY_UUID)) { @@ -205,6 +144,7 @@ public class AuthStorage { return Optional.empty(); } + @Override public void save(StoredProfile playerProfile) { try (Connection con = dataSource.getConnection()) { String uuid = playerProfile.getOptId().map(UUIDAdapter::toMojangId).orElse(null); @@ -245,6 +185,7 @@ public class AuthStorage { } } + @Override public void close() { dataSource.close(); } diff --git a/core/src/main/java/com/github/games647/fastlogin/core/storage/SQLiteStorage.java b/core/src/main/java/com/github/games647/fastlogin/core/storage/SQLiteStorage.java new file mode 100644 index 00000000..67fe67f3 --- /dev/null +++ b/core/src/main/java/com/github/games647/fastlogin/core/storage/SQLiteStorage.java @@ -0,0 +1,82 @@ +package com.github.games647.fastlogin.core.storage; + +import com.github.games647.fastlogin.core.StoredProfile; +import com.github.games647.fastlogin.core.shared.FastLoginCore; +import com.github.games647.fastlogin.core.shared.PlatformPlugin; +import com.zaxxer.hikari.HikariConfig; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.UUID; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +public class SQLiteStorage extends SQLStorage { + + private final Lock lock = new ReentrantLock(); + + public SQLiteStorage(FastLoginCore core, String databasePath, HikariConfig config) { + super(core, + "sqlite://" + replacePathVariables(core.getPlugin(), databasePath), + setParams(config)); + } + + private static HikariConfig setParams(HikariConfig config) { + config.setConnectionTestQuery("SELECT 1"); + config.setMaximumPoolSize(1); + + //a try to fix https://www.spigotmc.org/threads/fastlogin.101192/page-26#post-1874647 + // format strings retrieved by the timestamp column to match them from MySQL + config.addDataSourceProperty("date_string_format", "yyyy-MM-dd HH:mm:ss"); + + // TODO: test first for compatibility + // config.addDataSourceProperty("date_precision", "seconds"); + + return config; + } + + @Override + public StoredProfile loadProfile(String name) { + lock.lock(); + try { + return super.loadProfile(name); + } finally { + lock.unlock(); + } + } + + @Override + public StoredProfile loadProfile(UUID uuid) { + lock.lock(); + try { + return super.loadProfile(uuid); + } finally { + lock.unlock(); + } + } + + @Override + public void save(StoredProfile playerProfile) { + lock.lock(); + try { + super.save(playerProfile); + } finally { + lock.unlock(); + } + } + + @Override + public void createTables() throws SQLException { + try (Connection con = dataSource.getConnection(); + Statement createStmt = con.createStatement()) { + // SQLite has a different syntax for auto increment + createStmt.executeUpdate(CREATE_TABLE_STMT.replace("AUTO_INCREMENT", "AUTOINCREMENT")); + } + } + + private static String replacePathVariables(PlatformPlugin plugin, String input) { + String pluginFolder = plugin.getPluginFolder().toAbsolutePath().toString(); + return input.replace("{pluginDir}", pluginFolder); + } +}