diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/FastLoginBukkit.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/FastLoginBukkit.java index cd6647f5..b307f4ef 100644 --- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/FastLoginBukkit.java +++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/FastLoginBukkit.java @@ -18,7 +18,9 @@ import com.google.common.reflect.ClassPath; import java.io.IOException; import java.security.KeyPair; +import java.sql.SQLException; import java.util.Set; +import java.util.UUID; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import java.util.logging.Level; @@ -33,6 +35,14 @@ import org.bukkit.plugin.java.JavaPlugin; */ public class FastLoginBukkit extends JavaPlugin { + public static UUID parseId(String withoutDashes) { + return UUID.fromString(withoutDashes.substring(0, 8) + + "-" + withoutDashes.substring(8, 12) + + "-" + withoutDashes.substring(12, 16) + + "-" + withoutDashes.substring(16, 20) + + "-" + withoutDashes.substring(20, 32)); + } + //provide a immutable key pair to be thread safe | used for encrypting and decrypting traffic private final KeyPair keyPair = EncryptionUtil.generateKeyPair(); @@ -40,6 +50,7 @@ public class FastLoginBukkit extends JavaPlugin { private final Set enabledPremium = Sets.newConcurrentHashSet(); private boolean bungeeCord; + private Storage storage; //this map is thread-safe for async access (Packet Listener) //SafeCacheBuilder is used in order to be version independent @@ -63,7 +74,6 @@ public class FastLoginBukkit extends JavaPlugin { public void onEnable() { saveDefaultConfig(); - bungeeCord = Bukkit.spigot().getConfig().getBoolean("settings.bungeecord"); if (getServer().getOnlineMode()) { //we need to require offline to prevent a session request for a offline player getLogger().severe("Server have to be in offline mode"); @@ -71,18 +81,50 @@ public class FastLoginBukkit extends JavaPlugin { return; } - registerHooks(); + bungeeCord = Bukkit.spigot().getConfig().getBoolean("settings.bungeecord"); + boolean hookFound = registerHooks(); + if (bungeeCord) { + getLogger().info("BungeeCord setting detected. No auth plugin is required"); + } else if (!hookFound) { + getLogger().info("No auth plugin were found and bungeecord is deactivated. " + + "Either one or both of the checks have to pass in order to use this plugin"); + setEnabled(false); + return; + } - //register listeners on success - if (getServer().getPluginManager().isPluginEnabled("ProtocolSupport")) { - getServer().getPluginManager().registerEvents(new ProtocolSupportListener(this), this); + if (bungeeCord) { + //check for incoming messages from the bungeecord version of this plugin + getServer().getMessenger().registerIncomingPluginChannel(this, getName(), new BungeeCordListener(this)); + getServer().getMessenger().registerOutgoingPluginChannel(this, getName()); + //register listeners on success } else { - ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager(); + String driver = getConfig().getString("driver"); + String host = getConfig().getString("host", ""); + int port = getConfig().getInt("port", 3306); + String database = getConfig().getString("database"); - //we are performing HTTP request on these so run it async (seperate from the Netty IO threads) - AsynchronousManager asynchronousManager = protocolManager.getAsynchronousManager(); - asynchronousManager.registerAsyncHandler(new StartPacketListener(this, protocolManager)).start(); - asynchronousManager.registerAsyncHandler(new EncryptionPacketListener(this, protocolManager)).start(); + String username = getConfig().getString("username", ""); + String password = getConfig().getString("password", ""); + + this.storage = new Storage(this, driver, host, port, database, username, password); + try { + storage.createTables(); + } catch (SQLException sqlEx) { + getLogger().log(Level.SEVERE, "Failed to create database tables. Disabling plugin...", sqlEx); + setEnabled(false); + return; + } + + if (getServer().getPluginManager().isPluginEnabled("ProtocolSupport")) { + getServer().getPluginManager().registerEvents(new ProtocolSupportListener(this), this); + } else { + ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager(); + + //we are performing HTTP request on these so run it async (seperate from the Netty IO threads) + AsynchronousManager asynchronousManager = protocolManager.getAsynchronousManager(); + asynchronousManager.registerAsyncHandler(new StartPacketListener(this, protocolManager)).start(); + asynchronousManager.registerAsyncHandler(new EncryptionPacketListener(this, protocolManager)).start(); + } } getServer().getPluginManager().registerEvents(new BukkitJoinListener(this), this); @@ -90,24 +132,21 @@ public class FastLoginBukkit extends JavaPlugin { //register commands using a unique name getCommand("premium").setExecutor(new PremiumCommand(this)); getCommand("cracked").setExecutor(new CrackedCommand(this)); - - if (bungeeCord) { - //check for incoming messages from the bungeecord version of this plugin - getServer().getMessenger().registerIncomingPluginChannel(this, getName(), new BungeeCordListener(this)); - getServer().getMessenger().registerOutgoingPluginChannel(this, getName()); - } } @Override public void onDisable() { //clean up session.clear(); - enabledPremium.clear(); //remove old blacklists for (Player player : getServer().getOnlinePlayers()) { player.removeMetadata(getName(), this); } + + if (storage != null) { + storage.close(); + } } public String generateStringPassword() { @@ -143,8 +182,8 @@ public class FastLoginBukkit extends JavaPlugin { } /** - * Gets the auth plugin hook in order to interact with the plugins. - * This can be null if no supporting auth plugin was found. + * Gets the auth plugin hook in order to interact with the plugins. This can be null if no supporting auth plugin + * was found. * * @return interface to any supported auth plugin */ @@ -153,8 +192,7 @@ public class FastLoginBukkit extends JavaPlugin { } /** - * Gets the a connection in order to access important - * features from the Mojang API. + * Gets the a connection in order to access important features from the Mojang API. * * @return the connector instance */ diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/PlayerProfile.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/PlayerProfile.java new file mode 100644 index 00000000..bba5a3ec --- /dev/null +++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/PlayerProfile.java @@ -0,0 +1,74 @@ +package com.github.games647.fastlogin.bukkit; + +import java.util.UUID; + +public class PlayerProfile { + + private final UUID uuid; + private final String playerName; + + private long userId; + + private boolean premium; + private String lastIp; + private long lastLogin; + + public PlayerProfile(long userId, UUID uuid, String playerName, boolean premium + , String lastIp, long lastLogin) { + this.userId = userId; + this.uuid = uuid; + this.playerName = playerName; + this.premium = premium; + this.lastIp = lastIp; + this.lastLogin = lastLogin; + } + + public PlayerProfile(UUID uuid, String playerName, boolean premium, String lastIp) { + this.userId = -1; + + this.uuid = uuid; + this.playerName = playerName; + this.premium = premium; + this.lastIp = lastIp; + } + + public synchronized long getUserId() { + return userId; + } + + protected synchronized void setUserId(long generatedId) { + this.userId = generatedId; + } + + public UUID getUuid() { + return uuid; + } + + public String getPlayerName() { + return playerName; + } + + public boolean isPremium() { + return premium; + } + + public void setPremium(boolean premium) { + this.premium = premium; + } + + public String getLastIp() { + return lastIp; + } + + public void setLastIp(String lastIp) { + this.lastIp = lastIp; + } + + public long getLastLogin() { + return lastLogin; + } + + public void setLastLogin(long lastLogin) { + this.lastLogin = lastLogin; + } +} diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/Storage.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/Storage.java new file mode 100644 index 00000000..01f77109 --- /dev/null +++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/Storage.java @@ -0,0 +1,178 @@ +package com.github.games647.fastlogin.bukkit; + +import com.comphenix.protocol.utility.SafeCacheBuilder; +import com.google.common.cache.CacheLoader; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.util.UUID; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +public class Storage { + + private static final String PREMIUM_TABLE = "premium"; + + private final ConcurrentMap profileCache = SafeCacheBuilder + .newBuilder() + .concurrencyLevel(20) + .expireAfterAccess(30, TimeUnit.MINUTES) + .build(new CacheLoader() { + @Override + public PlayerProfile load(String key) throws Exception { + //should be fetched manually + throw new UnsupportedOperationException("Not supported yet."); + } + }); + + private final HikariDataSource dataSource; + private final FastLoginBukkit plugin; + + public Storage(FastLoginBukkit plugin, String driver, String host, int port, String databasePath + , String user, String pass) { + this.plugin = plugin; + + HikariConfig databaseConfig = new HikariConfig(); + databaseConfig.setUsername(user); + databaseConfig.setPassword(pass); + databaseConfig.setDriverClassName(driver); + + databasePath = databasePath.replace("{pluginDir}", plugin.getDataFolder().getAbsolutePath()); + + String jdbcUrl = "jdbc:"; + if (driver.contains("sqlite")) { + jdbcUrl += "sqlite" + "://" + databasePath; + databaseConfig.setConnectionTestQuery("SELECT 1"); + } else { + jdbcUrl += "mysql" + "://" + host + ':' + port + '/' + databasePath; + } + + databaseConfig.setJdbcUrl(jdbcUrl); + this.dataSource = new HikariDataSource(databaseConfig); + } + + public void createTables() throws SQLException { + Connection con = null; + try { + con = dataSource.getConnection(); + Statement statement = con.createStatement(); + String createDataStmt = "CREATE TABLE IF NOT EXISTS " + PREMIUM_TABLE + " (" + + "`UserID` INTEGER PRIMARY KEY AUTO_INCREMENT, " + + "`UUID` CHAR(36) NOT NULL, " + + "`Name` VARCHAR(16) NOT NULL, " + + "`Premium` BOOLEAN NOT NULL, " + + "`LastIp` VARCHAR(255) NOT NULL, " + + "`LastLogin` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + "UNIQUE (`UUID`), " + //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"); + } + + statement.executeUpdate(createDataStmt); + } finally { + closeQuietly(con); + } + } + + public PlayerProfile getProfile(String name, boolean fetch) { + if (profileCache.containsKey(name)) { + return profileCache.get(name); + } else if (fetch) { + Connection con = null; + try { + con = dataSource.getConnection(); + PreparedStatement loadStatement = con.prepareStatement("SELECT * FROM " + PREMIUM_TABLE + + " WHERE `Name`=? LIMIT 1"); + loadStatement.setString(1, name); + + ResultSet resultSet = loadStatement.executeQuery(); + if (resultSet.next()) { + long userId = resultSet.getInt(1); + UUID uuid = FastLoginBukkit.parseId(resultSet.getString(2)); +// String name = resultSet.getString(3); + boolean premium = resultSet.getBoolean(4); + String lastIp = resultSet.getString(5); + long lastLogin = resultSet.getTimestamp(6).getTime(); + PlayerProfile playerProfile = new PlayerProfile(userId, uuid, name, premium, lastIp, lastLogin); + profileCache.put(name, playerProfile); + return playerProfile; + } + } catch (SQLException sqlEx) { + plugin.getLogger().log(Level.SEVERE, "Failed to query profile", sqlEx); + } finally { + closeQuietly(con); + } + } + + return null; + } + + public boolean save(PlayerProfile playerProfile) { + Connection con = null; + try { + con = dataSource.getConnection(); + + if (playerProfile.getUserId() == -1) { + PreparedStatement saveStatement = con.prepareStatement("INSERT INTO " + PREMIUM_TABLE + + " (UUID, Name, Premium, LastIp) VALUES (?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS); + + saveStatement.setString(1, playerProfile.getUuid().toString().replace("-", "")); + saveStatement.setString(2, playerProfile.getPlayerName()); + saveStatement.setBoolean(3, playerProfile.isPremium()); + saveStatement.setString(4, playerProfile.getLastIp()); + saveStatement.execute(); + + ResultSet generatedKeys = saveStatement.getGeneratedKeys(); + if (generatedKeys != null && generatedKeys.next()) { + playerProfile.setUserId(generatedKeys.getInt(1)); + } + } else { + PreparedStatement saveStatement = con.prepareStatement("UPDATE " + PREMIUM_TABLE + + " SET UUID=?, Name=?, Premium=?, LastIp=?, LastLogin=? WHERE UserID=?"); + + saveStatement.setString(1, playerProfile.getUuid().toString().replace("-", "")); + saveStatement.setString(2, playerProfile.getPlayerName()); + saveStatement.setBoolean(3, playerProfile.isPremium()); + saveStatement.setString(4, playerProfile.getLastIp()); + saveStatement.setTimestamp(5, new Timestamp(playerProfile.getLastLogin())); + + saveStatement.setLong(6, playerProfile.getUserId()); + saveStatement.execute(); + } + + return true; + } catch (SQLException ex) { + plugin.getLogger().log(Level.SEVERE, "Failed to save playerProfile", ex); + } finally { + closeQuietly(con); + } + + return false; + } + + public void close() { + dataSource.close(); + profileCache.clear(); + } + + private void closeQuietly(Connection con) { + if (con != null) { + try { + con.close(); + } catch (SQLException sqlEx) { + plugin.getLogger().log(Level.SEVERE, "Failed to close connection", sqlEx); + } + } + } +} diff --git a/bukkit/src/main/resources/config.yml b/bukkit/src/main/resources/config.yml index ec738b5c..090a2760 100644 --- a/bukkit/src/main/resources/config.yml +++ b/bukkit/src/main/resources/config.yml @@ -53,12 +53,16 @@ forwardSkin: true # Database configuration # Recommened is the use of MariaDB (a better version of MySQL) -driver: mysql -host: localhost -port: 3306 -database: FastLogin -username: myUser -password: myPassword -# If you use an existing database which will be used by other programs too -# you define here a prefix, to prevent conflicts. -tablePrefix: '' + +# Single file SQLite database +driver: org.sqlite.JDBC +# File location +database: '{pluginDir}/FastLogin.db' + +# MySQL and SQLite +#driver: com.mysql.jdbc.Driver +#host: localhost +#port: 3306 +#database: FastLogin +#username: myUser +#password: myPassword \ No newline at end of file diff --git a/universal/pom.xml b/universal/pom.xml index 5c89c490..ba080623 100644 --- a/universal/pom.xml +++ b/universal/pom.xml @@ -30,6 +30,7 @@ ${project.groupId}:* com.zaxxer:HikariCP + org.slf4j:*