diff --git a/LaunchServer/LaunchServer.iml b/LaunchServer/LaunchServer.iml index 0c1bb78..f54725d 100644 --- a/LaunchServer/LaunchServer.iml +++ b/LaunchServer/LaunchServer.iml @@ -69,5 +69,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/LaunchServer/MANIFEST.MF b/LaunchServer/MANIFEST.MF index 0eb35aa..8a1a321 100644 --- a/LaunchServer/MANIFEST.MF +++ b/LaunchServer/MANIFEST.MF @@ -2,7 +2,8 @@ Class-Path: libraries/jansi.jar libraries/jline2.jar libraries/mysql.j ar libraries/hikaricp/hikaricp.jar libraries/hikaricp/javassist.jar l ibraries/hikaricp/slf4j-api.jar libraries/hikaricp/slf4j-simple.jar l - ibraries/launch4j/launch4j.jar ibraries/jbcrypt.jar + ibraries/launch4j/launch4j.jar ibraries/jbcrypt.jar ibraries/postgres + ql.jar Main-Class: launchserver.LaunchServer Name: launchserver/ diff --git a/LaunchServer/source/auth/MySQLSourceConfig.java b/LaunchServer/source/auth/MySQLSourceConfig.java index e65b26e..387b544 100644 --- a/LaunchServer/source/auth/MySQLSourceConfig.java +++ b/LaunchServer/source/auth/MySQLSourceConfig.java @@ -14,7 +14,7 @@ import java.sql.Connection; import java.sql.SQLException; -public final class MySQLSourceConfig extends ConfigObject implements AutoCloseable +public final class MySQLSourceConfig extends ConfigObject implements AutoCloseable, SQLSourceConfig { @LauncherAPI public static final int TIMEOUT = VerifyHelper.verifyInt( diff --git a/LaunchServer/source/auth/PostgreSQLSourceConfig.java b/LaunchServer/source/auth/PostgreSQLSourceConfig.java new file mode 100644 index 0000000..7686e62 --- /dev/null +++ b/LaunchServer/source/auth/PostgreSQLSourceConfig.java @@ -0,0 +1,105 @@ +package launchserver.auth; + +import org.postgresql.ds.PGSimpleDataSource; +import com.zaxxer.hikari.HikariDataSource; +import launcher.LauncherAPI; +import launcher.helper.LogHelper; +import launcher.helper.VerifyHelper; +import launcher.serialize.config.ConfigObject; +import launcher.serialize.config.entry.BlockConfigEntry; +import launcher.serialize.config.entry.IntegerConfigEntry; +import launcher.serialize.config.entry.StringConfigEntry; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +public final class PostgreSQLSourceConfig extends ConfigObject implements AutoCloseable, SQLSourceConfig +{ + @LauncherAPI + public static final int TIMEOUT = VerifyHelper.verifyInt( + Integer.parseInt(System.getProperty("launcher.postgresql.idleTimeout", Integer.toString(5000))), + VerifyHelper.POSITIVE, "launcher.postgresql.idleTimeout can't be <= 5000"); + private static final int MAX_POOL_SIZE = VerifyHelper.verifyInt( + Integer.parseInt(System.getProperty("launcher.postgresql.maxPoolSize", Integer.toString(3))), + VerifyHelper.POSITIVE, "launcher.postgresql.maxPoolSize can't be <= 0"); + + // Instance + private final String poolName; + + // Config + private final String address; + private final int port; + private final String username; + private final String password; + private final String database; + + // Cache + private DataSource source; + private boolean hikari; + + @LauncherAPI + public PostgreSQLSourceConfig(String poolName, BlockConfigEntry block) + { + super(block); + this.poolName = poolName; + address = VerifyHelper.verify(block.getEntryValue("address", StringConfigEntry.class), + VerifyHelper.NOT_EMPTY, "PostgreSQL address can't be empty"); + port = VerifyHelper.verifyInt(block.getEntryValue("port", IntegerConfigEntry.class), + VerifyHelper.range(0, 65535), "Illegal PostgreSQL port"); + username = VerifyHelper.verify(block.getEntryValue("username", StringConfigEntry.class), + VerifyHelper.NOT_EMPTY, "PostgreSQL username can't be empty"); + password = block.getEntryValue("password", StringConfigEntry.class); + database = VerifyHelper.verify(block.getEntryValue("database", StringConfigEntry.class), + VerifyHelper.NOT_EMPTY, "PostgreSQL database can't be empty"); + + // Password shouldn't be verified + } + + @Override + public synchronized void close() + { + if (hikari) + { // Shutdown hikari pool + ((HikariDataSource) source).close(); + } + } + + @LauncherAPI + public synchronized Connection getConnection() throws SQLException { + if (source == null) { // New data source + PGSimpleDataSource postgresqlSource = new PGSimpleDataSource(); + + // Set credentials + postgresqlSource.setServerName(address); + postgresqlSource.setPortNumber(port); + postgresqlSource.setUser(username); + postgresqlSource.setPassword(password); + postgresqlSource.setDatabaseName(database); + + // Try using HikariCP + source = postgresqlSource; + try { + Class.forName("com.zaxxer.hikari.HikariDataSource"); + hikari = true; // Used for shutdown. Not instanceof because of possible classpath error + + // Set HikariCP pool + HikariDataSource hikariSource = new HikariDataSource(); + hikariSource.setDataSource(source); + + // Set pool settings + hikariSource.setPoolName(poolName); + hikariSource.setMinimumIdle(0); + hikariSource.setMaximumPoolSize(MAX_POOL_SIZE); + hikariSource.setIdleTimeout(TIMEOUT * 1000L); + + // Replace source with hds + source = hikariSource; + LogHelper.info("HikariCP pooling enabled for '%s'", poolName); + } catch (ClassNotFoundException ignored) { + LogHelper.warning("HikariCP isn't in classpath for '%s'", poolName); + } + } + return source.getConnection(); + } +} diff --git a/LaunchServer/source/auth/SQLSourceConfig.java b/LaunchServer/source/auth/SQLSourceConfig.java new file mode 100644 index 0000000..df5a6e4 --- /dev/null +++ b/LaunchServer/source/auth/SQLSourceConfig.java @@ -0,0 +1,10 @@ +package launchserver.auth; + +import java.sql.Connection; +import java.sql.SQLException; + +public interface SQLSourceConfig +{ + void close(); + Connection getConnection() throws SQLException; +} diff --git a/LaunchServer/source/auth/handler/AuthHandler.java b/LaunchServer/source/auth/handler/AuthHandler.java index 7c69691..bca0496 100644 --- a/LaunchServer/source/auth/handler/AuthHandler.java +++ b/LaunchServer/source/auth/handler/AuthHandler.java @@ -26,6 +26,7 @@ registerHandler("binaryFile", BinaryFileAuthHandler::new); registerHandler("textFile", TextFileAuthHandler::new); registerHandler("mysql", MySQLAuthHandler::new); + registerHandler("postgresql", PostgreSQLAuthHandler::new); } @LauncherAPI diff --git a/LaunchServer/source/auth/handler/PostgreSQLAuthHandler.java b/LaunchServer/source/auth/handler/PostgreSQLAuthHandler.java new file mode 100644 index 0000000..4fe8921 --- /dev/null +++ b/LaunchServer/source/auth/handler/PostgreSQLAuthHandler.java @@ -0,0 +1,134 @@ +package launchserver.auth.handler; + +import launcher.helper.LogHelper; +import launcher.helper.VerifyHelper; +import launcher.serialize.config.entry.BlockConfigEntry; +import launcher.serialize.config.entry.BooleanConfigEntry; +import launcher.serialize.config.entry.StringConfigEntry; +import launchserver.auth.PostgreSQLSourceConfig; + +import java.io.IOException; +import java.sql.*; +import java.util.UUID; + +public final class PostgreSQLAuthHandler extends CachedAuthHandler { + private final PostgreSQLSourceConfig postgreSQLHolder; + private final String uuidColumn; + private final String usernameColumn; + private final String accessTokenColumn; + private final String serverIDColumn; + + + private final String queryByUUIDSQL; + private final String queryByUsernameSQL; + private final String updateAuthSQL; + private final String updateServerIDSQL; + + PostgreSQLAuthHandler(BlockConfigEntry block) { + super(block); + postgreSQLHolder = new PostgreSQLSourceConfig("authHandlerPool", block); + + // Read query params + String table = VerifyHelper.verifyIDName( + block.getEntryValue("table", StringConfigEntry.class)); + uuidColumn = VerifyHelper.verifyIDName( + block.getEntryValue("uuidColumn", StringConfigEntry.class)); + usernameColumn = VerifyHelper.verifyIDName( + block.getEntryValue("usernameColumn", StringConfigEntry.class)); + accessTokenColumn = VerifyHelper.verifyIDName( + block.getEntryValue("accessTokenColumn", StringConfigEntry.class)); + serverIDColumn = VerifyHelper.verifyIDName( + block.getEntryValue("serverIDColumn", StringConfigEntry.class)); + + // Prepare SQL queries + queryByUUIDSQL = String.format("SELECT %s, %s, %s, %s FROM %s WHERE %s=? LIMIT 1", + uuidColumn, usernameColumn, accessTokenColumn, serverIDColumn, table, uuidColumn); + queryByUsernameSQL = String.format("SELECT %s, %s, %s, %s FROM %s WHERE %s=? LIMIT 1", + uuidColumn, usernameColumn, accessTokenColumn, serverIDColumn, table, usernameColumn); + updateAuthSQL = String.format("UPDATE %s SET %s=?, %s=?, %s=NULL WHERE %s=? LIMIT 1", + table, usernameColumn, accessTokenColumn, serverIDColumn, uuidColumn); + updateServerIDSQL = String.format("UPDATE %s SET %s=? WHERE %s=? LIMIT 1", + table, serverIDColumn, uuidColumn); + + // Fetch all entries + if (block.getEntryValue("fetchAll", BooleanConfigEntry.class)) { + LogHelper.info("Fetching all AuthHandler entries"); + String query = String.format("SELECT %s, %s, %s, %s FROM %s", + uuidColumn, usernameColumn, accessTokenColumn, serverIDColumn, table); + try (Connection c = postgreSQLHolder.getConnection(); Statement statement = c.createStatement(); + ResultSet set = statement.executeQuery(query)) { + for (Entry entry = constructEntry(set); entry != null; entry = constructEntry(set)) { + addEntry(entry); + } + } catch (SQLException e) { + LogHelper.error(e); + } + } + } + + @Override + public void close() { + postgreSQLHolder.close(); + } + + private Entry constructEntry(ResultSet set) throws SQLException { + return set.next() ? new Entry(UUID.fromString(set.getString(uuidColumn)), + set.getString(usernameColumn), set.getString(accessTokenColumn), set.getString(serverIDColumn)) : null; + } + + @Override + protected Entry fetchEntry(String username) throws IOException { + return query(queryByUsernameSQL, username); + } + + @Override + protected Entry fetchEntry(UUID uuid) throws IOException { + return query(queryByUUIDSQL, uuid.toString()); + } + + @Override + protected boolean updateAuth(UUID uuid, String username, String accessToken) throws IOException { + try (Connection c = postgreSQLHolder.getConnection(); + PreparedStatement s = c.prepareStatement(updateAuthSQL)) { + s.setString(1, username); // Username case + s.setString(2, accessToken); + s.setString(3, uuid.toString()); + + // Execute update + s.setQueryTimeout(PostgreSQLSourceConfig.TIMEOUT); + return s.executeUpdate() > 0; + } catch (SQLException e) { + throw new IOException(e); + } + } + + @Override + protected boolean updateServerID(UUID uuid, String serverID) throws IOException { + try (Connection c = postgreSQLHolder.getConnection(); + PreparedStatement s = c.prepareStatement(updateServerIDSQL)) { + s.setString(1, serverID); + s.setString(2, uuid.toString()); + + // Execute update + s.setQueryTimeout(PostgreSQLSourceConfig.TIMEOUT); + return s.executeUpdate() > 0; + } catch (SQLException e) { + throw new IOException(e); + } + } + + private Entry query(String sql, String value) throws IOException { + try (Connection c = postgreSQLHolder.getConnection(); + PreparedStatement s = c.prepareStatement(sql)) { + s.setString(1, value); + + // Execute query + s.setQueryTimeout(PostgreSQLSourceConfig.TIMEOUT); + try (ResultSet set = s.executeQuery()) { + return constructEntry(set); + } + } catch (SQLException e) { + throw new IOException(e); + } + } +} \ No newline at end of file diff --git a/LaunchServer/source/auth/provider/AuthProvider.java b/LaunchServer/source/auth/provider/AuthProvider.java index 9ff33bc..1590414 100644 --- a/LaunchServer/source/auth/provider/AuthProvider.java +++ b/LaunchServer/source/auth/provider/AuthProvider.java @@ -27,6 +27,7 @@ registerProvider("mysql", MySQLAuthProvider::new); registerProvider("mysql-bcrypt", MySQLBcryptAuthProvider::new); registerProvider("request", RequestAuthProvider::new); + registerProvider("postgresql", PostgreSQLAuthProvider::new); } @LauncherAPI diff --git a/LaunchServer/source/auth/provider/PostgreSQLAuthProvider.java b/LaunchServer/source/auth/provider/PostgreSQLAuthProvider.java new file mode 100644 index 0000000..c331d7f --- /dev/null +++ b/LaunchServer/source/auth/provider/PostgreSQLAuthProvider.java @@ -0,0 +1,59 @@ +package launchserver.auth.provider; + +import launcher.helper.CommonHelper; +import launcher.helper.SecurityHelper; +import launcher.helper.VerifyHelper; +import launcher.serialize.config.entry.BlockConfigEntry; +import launcher.serialize.config.entry.ListConfigEntry; +import launcher.serialize.config.entry.StringConfigEntry; +import launchserver.auth.AuthException; +import launchserver.auth.PostgreSQLSourceConfig; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public final class PostgreSQLAuthProvider extends AuthProvider +{ + private final PostgreSQLSourceConfig postgreSQLHolder; + private final String query; + private final String[] queryParams; + + PostgreSQLAuthProvider(BlockConfigEntry block) { + super(block); + postgreSQLHolder = new PostgreSQLSourceConfig("authProviderPool", block); + + // Read query + query = VerifyHelper.verify(block.getEntryValue("query", StringConfigEntry.class), + VerifyHelper.NOT_EMPTY, "PostgreSQL query can't be empty"); + queryParams = block.getEntry("queryParams", ListConfigEntry.class). + stream(StringConfigEntry.class).toArray(String[]::new); + } + + @Override + public AuthProviderResult auth(String login, String password, String ip) throws SQLException, AuthException + { + try (Connection c = postgreSQLHolder.getConnection(); PreparedStatement s = c.prepareStatement(query)) + { + String[] replaceParams = {"login", login, "password", password}; + for (int i = 0; i < queryParams.length; i++) + { + s.setString(i + 1, CommonHelper.replace(queryParams[i], replaceParams)); + } + + // Execute SQL query + s.setQueryTimeout(PostgreSQLSourceConfig.TIMEOUT); + try (ResultSet set = s.executeQuery()) + { + return set.next() ? new AuthProviderResult(set.getString(1), SecurityHelper.randomStringToken()) : authError("Incorrect username or password"); + } + } + } + + @Override + public void close() + { + // Do nothing + } +} \ No newline at end of file diff --git a/build/libraries/postgresql.jar b/build/libraries/postgresql.jar new file mode 100644 index 0000000..21bc998 --- /dev/null +++ b/build/libraries/postgresql.jar Binary files differ