reupload files

This commit is contained in:
HaHaWTH
2023-07-11 20:45:01 +08:00
parent b014da245d
commit 7e49e26735
465 changed files with 93823 additions and 0 deletions
@@ -0,0 +1,172 @@
package fr.xephi.authme.datasource;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import ch.jalu.datasourcecolumns.data.DataSourceValueImpl;
import ch.jalu.datasourcecolumns.data.DataSourceValues;
import ch.jalu.datasourcecolumns.predicate.AlwaysTruePredicate;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.columnshandler.AuthMeColumns;
import fr.xephi.authme.datasource.columnshandler.AuthMeColumnsHandler;
import fr.xephi.authme.security.crypts.HashedPassword;
import java.sql.SQLException;
import java.util.Collections;
import java.util.List;
import static ch.jalu.datasourcecolumns.data.UpdateValues.with;
import static ch.jalu.datasourcecolumns.predicate.StandardPredicates.eq;
import static ch.jalu.datasourcecolumns.predicate.StandardPredicates.eqIgnoreCase;
import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException;
/**
* Common type for SQL-based data sources. Classes implementing this
* must ensure that {@link #columnsHandler} is initialized on creation.
*/
public abstract class AbstractSqlDataSource implements DataSource {
protected AuthMeColumnsHandler columnsHandler;
@Override
public boolean isAuthAvailable(String user) {
try {
return columnsHandler.retrieve(user, AuthMeColumns.NAME).rowExists();
} catch (SQLException e) {
logSqlException(e);
return false;
}
}
@Override
public HashedPassword getPassword(String user) {
try {
DataSourceValues values = columnsHandler.retrieve(user, AuthMeColumns.PASSWORD, AuthMeColumns.SALT);
if (values.rowExists()) {
return new HashedPassword(values.get(AuthMeColumns.PASSWORD), values.get(AuthMeColumns.SALT));
}
} catch (SQLException e) {
logSqlException(e);
}
return null;
}
@Override
public boolean saveAuth(PlayerAuth auth) {
return columnsHandler.insert(auth,
AuthMeColumns.NAME, AuthMeColumns.NICK_NAME, AuthMeColumns.PASSWORD, AuthMeColumns.SALT,
AuthMeColumns.EMAIL, AuthMeColumns.REGISTRATION_DATE, AuthMeColumns.REGISTRATION_IP,
AuthMeColumns.UUID);
}
@Override
public boolean hasSession(String user) {
try {
DataSourceValue<Integer> result = columnsHandler.retrieve(user, AuthMeColumns.HAS_SESSION);
return result.rowExists() && Integer.valueOf(1).equals(result.getValue());
} catch (SQLException e) {
logSqlException(e);
return false;
}
}
@Override
public boolean updateSession(PlayerAuth auth) {
return columnsHandler.update(auth, AuthMeColumns.LAST_IP, AuthMeColumns.LAST_LOGIN, AuthMeColumns.NICK_NAME);
}
@Override
public boolean updatePassword(PlayerAuth auth) {
return updatePassword(auth.getNickname(), auth.getPassword());
}
@Override
public boolean updatePassword(String user, HashedPassword password) {
return columnsHandler.update(user,
with(AuthMeColumns.PASSWORD, password.getHash())
.and(AuthMeColumns.SALT, password.getSalt()).build());
}
@Override
public boolean updateQuitLoc(PlayerAuth auth) {
return columnsHandler.update(auth,
AuthMeColumns.LOCATION_X, AuthMeColumns.LOCATION_Y, AuthMeColumns.LOCATION_Z,
AuthMeColumns.LOCATION_WORLD, AuthMeColumns.LOCATION_YAW, AuthMeColumns.LOCATION_PITCH);
}
@Override
public List<String> getAllAuthsByIp(String ip) {
try {
return columnsHandler.retrieve(eq(AuthMeColumns.LAST_IP, ip), AuthMeColumns.NAME);
} catch (SQLException e) {
logSqlException(e);
return Collections.emptyList();
}
}
@Override
public int countAuthsByEmail(String email) {
return columnsHandler.count(eqIgnoreCase(AuthMeColumns.EMAIL, email));
}
@Override
public boolean updateEmail(PlayerAuth auth) {
return columnsHandler.update(auth, AuthMeColumns.EMAIL);
}
@Override
public boolean isLogged(String user) {
try {
DataSourceValue<Integer> result = columnsHandler.retrieve(user, AuthMeColumns.IS_LOGGED);
return result.rowExists() && Integer.valueOf(1).equals(result.getValue());
} catch (SQLException e) {
logSqlException(e);
return false;
}
}
@Override
public void setLogged(String user) {
columnsHandler.update(user, AuthMeColumns.IS_LOGGED, 1);
}
@Override
public void setUnlogged(String user) {
columnsHandler.update(user, AuthMeColumns.IS_LOGGED, 0);
}
@Override
public void grantSession(String user) {
columnsHandler.update(user, AuthMeColumns.HAS_SESSION, 1);
}
@Override
public void revokeSession(String user) {
columnsHandler.update(user, AuthMeColumns.HAS_SESSION, 0);
}
@Override
public void purgeLogged() {
columnsHandler.update(eq(AuthMeColumns.IS_LOGGED, 1), AuthMeColumns.IS_LOGGED, 0);
}
@Override
public int getAccountsRegistered() {
return columnsHandler.count(new AlwaysTruePredicate<>());
}
@Override
public boolean updateRealName(String user, String realName) {
return columnsHandler.update(user, AuthMeColumns.NICK_NAME, realName);
}
@Override
public DataSourceValue<String> getEmail(String user) {
try {
return columnsHandler.retrieve(user, AuthMeColumns.EMAIL);
} catch (SQLException e) {
logSqlException(e);
return DataSourceValueImpl.unknownRow();
}
}
abstract String getJdbcUrl(String host, String port, String database);
}
@@ -0,0 +1,303 @@
package fr.xephi.authme.datasource;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import ch.jalu.datasourcecolumns.data.DataSourceValueImpl;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.util.Utils;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
public class CacheDataSource implements DataSource {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(CacheDataSource.class);
private final DataSource source;
private final PlayerCache playerCache;
private final LoadingCache<String, Optional<PlayerAuth>> cachedAuths;
private final ListeningExecutorService executorService;
/**
* Constructor for CacheDataSource.
*
* @param source the source
* @param playerCache the player cache
*/
public CacheDataSource(DataSource source, PlayerCache playerCache) {
this.source = source;
this.playerCache = playerCache;
executorService = MoreExecutors.listeningDecorator(
Executors.newCachedThreadPool(new ThreadFactoryBuilder()
.setDaemon(true)
.setNameFormat("AuthMe-CacheLoader")
.build())
);
cachedAuths = CacheBuilder.newBuilder()
.refreshAfterWrite(5, TimeUnit.MINUTES)
.expireAfterAccess(15, TimeUnit.MINUTES)
.build(new CacheLoader<String, Optional<PlayerAuth>>() {
@Override
public Optional<PlayerAuth> load(String key) {
return Optional.ofNullable(source.getAuth(key));
}
@Override
public ListenableFuture<Optional<PlayerAuth>> reload(final String key, Optional<PlayerAuth> oldValue) {
return executorService.submit(() -> load(key));
}
});
}
public LoadingCache<String, Optional<PlayerAuth>> getCachedAuths() {
return cachedAuths;
}
@Override
public void reload() {
source.reload();
}
@Override
public boolean isCached() {
return true;
}
@Override
public boolean isAuthAvailable(String user) {
return getAuth(user) != null;
}
@Override
public HashedPassword getPassword(String user) {
user = user.toLowerCase(Locale.ROOT);
Optional<PlayerAuth> pAuthOpt = cachedAuths.getIfPresent(user);
if (pAuthOpt != null && pAuthOpt.isPresent()) {
return pAuthOpt.get().getPassword();
}
return source.getPassword(user);
}
@Override
public PlayerAuth getAuth(String user) {
user = user.toLowerCase(Locale.ROOT);
return cachedAuths.getUnchecked(user).orElse(null);
}
@Override
public boolean saveAuth(PlayerAuth auth) {
boolean result = source.saveAuth(auth);
if (result) {
cachedAuths.refresh(auth.getNickname());
}
return result;
}
@Override
public boolean updatePassword(PlayerAuth auth) {
boolean result = source.updatePassword(auth);
if (result) {
cachedAuths.refresh(auth.getNickname());
}
return result;
}
@Override
public boolean updatePassword(String user, HashedPassword password) {
user = user.toLowerCase(Locale.ROOT);
boolean result = source.updatePassword(user, password);
if (result) {
cachedAuths.refresh(user);
}
return result;
}
@Override
public boolean updateSession(PlayerAuth auth) {
boolean result = source.updateSession(auth);
if (result) {
cachedAuths.refresh(auth.getNickname());
}
return result;
}
@Override
public boolean updateQuitLoc(final PlayerAuth auth) {
boolean result = source.updateQuitLoc(auth);
if (result) {
cachedAuths.refresh(auth.getNickname());
}
return result;
}
@Override
public Set<String> getRecordsToPurge(long until) {
return source.getRecordsToPurge(until);
}
@Override
public boolean removeAuth(String name) {
name = name.toLowerCase(Locale.ROOT);
boolean result = source.removeAuth(name);
if (result) {
cachedAuths.invalidate(name);
}
return result;
}
@Override
public void closeConnection() {
executorService.shutdown();
try {
executorService.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
logger.logException("Could not close executor service:", e);
}
cachedAuths.invalidateAll();
source.closeConnection();
}
@Override
public boolean updateEmail(final PlayerAuth auth) {
boolean result = source.updateEmail(auth);
if (result) {
cachedAuths.refresh(auth.getNickname());
}
return result;
}
@Override
public List<String> getAllAuthsByIp(String ip) {
return source.getAllAuthsByIp(ip);
}
@Override
public int countAuthsByEmail(String email) {
return source.countAuthsByEmail(email);
}
@Override
public void purgeRecords(Collection<String> banned) {
source.purgeRecords(banned);
cachedAuths.invalidateAll(banned);
}
@Override
public DataSourceType getType() {
return source.getType();
}
@Override
public boolean isLogged(String user) {
return source.isLogged(user);
}
@Override
public void setLogged(final String user) {
source.setLogged(user.toLowerCase(Locale.ROOT));
}
@Override
public void setUnlogged(final String user) {
source.setUnlogged(user.toLowerCase(Locale.ROOT));
}
@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
public void purgeLogged() {
source.purgeLogged();
cachedAuths.invalidateAll();
}
@Override
public int getAccountsRegistered() {
return source.getAccountsRegistered();
}
@Override
public boolean updateRealName(String user, String realName) {
boolean result = source.updateRealName(user, realName);
if (result) {
cachedAuths.refresh(user);
}
return result;
}
@Override
public DataSourceValue<String> getEmail(String user) {
return cachedAuths.getUnchecked(user)
.map(auth -> DataSourceValueImpl.of(auth.getEmail()))
.orElse(DataSourceValueImpl.unknownRow());
}
@Override
public List<PlayerAuth> getAllAuths() {
return source.getAllAuths();
}
@Override
public List<String> getLoggedPlayersWithEmptyMail() {
return playerCache.getCache().values().stream()
.filter(auth -> Utils.isEmailEmpty(auth.getEmail()))
.map(PlayerAuth::getRealName)
.collect(Collectors.toList());
}
@Override
public List<PlayerAuth> getRecentlyLoggedInPlayers() {
return source.getRecentlyLoggedInPlayers();
}
@Override
public boolean setTotpKey(String user, String totpKey) {
boolean result = source.setTotpKey(user, totpKey);
if (result) {
cachedAuths.refresh(user);
}
return result;
}
@Override
public void invalidateCache(String playerName) {
cachedAuths.invalidate(playerName);
}
@Override
public void refreshCache(String playerName) {
if (cachedAuths.getIfPresent(playerName) != null) {
cachedAuths.refresh(playerName);
}
}
}
@@ -0,0 +1,59 @@
package fr.xephi.authme.datasource;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.DatabaseSettings;
/**
* Database column names.
*/
// Justification: String is immutable and this class is used to easily access the configurable column names
@SuppressWarnings({"checkstyle:VisibilityModifier", "checkstyle:MemberName", "checkstyle:AbbreviationAsWordInName"})
public final class Columns {
public final String NAME;
public final String REAL_NAME;
public final String PASSWORD;
public final String SALT;
public final String TOTP_KEY;
public final String LAST_IP;
public final String LAST_LOGIN;
public final String GROUP;
public final String LASTLOC_X;
public final String LASTLOC_Y;
public final String LASTLOC_Z;
public final String LASTLOC_WORLD;
public final String LASTLOC_YAW;
public final String LASTLOC_PITCH;
public final String EMAIL;
public final String ID;
public final String IS_LOGGED;
public final String HAS_SESSION;
public final String REGISTRATION_DATE;
public final String REGISTRATION_IP;
public final String PLAYER_UUID;
public Columns(Settings settings) {
NAME = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME);
REAL_NAME = settings.getProperty(DatabaseSettings.MYSQL_COL_REALNAME);
PASSWORD = settings.getProperty(DatabaseSettings.MYSQL_COL_PASSWORD);
SALT = settings.getProperty(DatabaseSettings.MYSQL_COL_SALT);
TOTP_KEY = settings.getProperty(DatabaseSettings.MYSQL_COL_TOTP_KEY);
LAST_IP = settings.getProperty(DatabaseSettings.MYSQL_COL_LAST_IP);
LAST_LOGIN = settings.getProperty(DatabaseSettings.MYSQL_COL_LASTLOGIN);
GROUP = settings.getProperty(DatabaseSettings.MYSQL_COL_GROUP);
LASTLOC_X = settings.getProperty(DatabaseSettings.MYSQL_COL_LASTLOC_X);
LASTLOC_Y = settings.getProperty(DatabaseSettings.MYSQL_COL_LASTLOC_Y);
LASTLOC_Z = settings.getProperty(DatabaseSettings.MYSQL_COL_LASTLOC_Z);
LASTLOC_WORLD = settings.getProperty(DatabaseSettings.MYSQL_COL_LASTLOC_WORLD);
LASTLOC_YAW = settings.getProperty(DatabaseSettings.MYSQL_COL_LASTLOC_YAW);
LASTLOC_PITCH = settings.getProperty(DatabaseSettings.MYSQL_COL_LASTLOC_PITCH);
EMAIL = settings.getProperty(DatabaseSettings.MYSQL_COL_EMAIL);
ID = settings.getProperty(DatabaseSettings.MYSQL_COL_ID);
IS_LOGGED = settings.getProperty(DatabaseSettings.MYSQL_COL_ISLOGGED);
HAS_SESSION = settings.getProperty(DatabaseSettings.MYSQL_COL_HASSESSION);
REGISTRATION_DATE = settings.getProperty(DatabaseSettings.MYSQL_COL_REGISTER_DATE);
REGISTRATION_IP = settings.getProperty(DatabaseSettings.MYSQL_COL_REGISTER_IP);
PLAYER_UUID = settings.getProperty(DatabaseSettings.MYSQL_COL_PLAYER_UUID);
}
}
@@ -0,0 +1,286 @@
package fr.xephi.authme.datasource;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.initialization.Reloadable;
import fr.xephi.authme.security.crypts.HashedPassword;
import java.util.Collection;
import java.util.List;
import java.util.Set;
/**
* Interface for manipulating {@link PlayerAuth} objects from a data source.
*/
public interface DataSource extends Reloadable {
/**
* Return whether the data source is cached and needs to send plugin messaging updates.
*
* @return true if the data source is cached.
*/
default boolean isCached() {
return false;
}
/**
* Return whether there is a record for the given username.
*
* @param user The username to look up
* @return True if there is a record, false otherwise
*/
boolean isAuthAvailable(String user);
/**
* Return the hashed password of the player.
*
* @param user The user whose password should be retrieve
* @return The password hash of the player
*/
HashedPassword getPassword(String user);
/**
* Retrieve the entire PlayerAuth object associated with the username.
*
* @param user The user to retrieve
* @return The PlayerAuth object for the given username
*/
PlayerAuth getAuth(String user);
/**
* Save a new PlayerAuth object.
*
* @param auth The new PlayerAuth to persist
* @return True upon success, false upon failure
*/
boolean saveAuth(PlayerAuth auth);
/**
* Update the session of a record (IP, last login, real name).
*
* @param auth The PlayerAuth object to update in the database
* @return True upon success, false upon failure
*/
boolean updateSession(PlayerAuth auth);
/**
* Update the password of the given PlayerAuth object.
*
* @param auth The PlayerAuth whose password should be updated
* @return True upon success, false upon failure
*/
boolean updatePassword(PlayerAuth auth);
/**
* Update the password of the given player.
*
* @param user The user whose password should be updated
* @param password The new password
* @return True upon success, false upon failure
*/
boolean updatePassword(String user, HashedPassword password);
/**
* Get all records in the database whose last login was before the given time.
*
* @param until The minimum last login
* @return The account names selected to purge
*/
Set<String> getRecordsToPurge(long until);
/**
* Purge the given players from the database.
*
* @param toPurge The players to purge
*/
void purgeRecords(Collection<String> toPurge);
/**
* Remove a user record from the database.
*
* @param user The user to remove
* @return True upon success, false upon failure
*/
boolean removeAuth(String user);
/**
* Update the quit location of a PlayerAuth.
*
* @param auth The entry whose quit location should be updated
* @return True upon success, false upon failure
*/
boolean updateQuitLoc(PlayerAuth auth);
/**
* Return all usernames associated with the given IP address.
*
* @param ip The IP address to look up
* @return Usernames associated with the given IP address
*/
List<String> getAllAuthsByIp(String ip);
/**
* Return the number of accounts associated with the given email address.
*
* @param email The email address to look up
* @return Number of accounts using the given email address
*/
int countAuthsByEmail(String email);
/**
* Update the email of the PlayerAuth in the data source.
*
* @param auth The PlayerAuth whose email should be updated
* @return True upon success, false upon failure
*/
boolean updateEmail(PlayerAuth auth);
/**
* Close the underlying connections to the data source.
*/
void closeConnection();
/**
* Return the data source type.
*
* @return the data source type
*/
DataSourceType getType();
/**
* Query the datasource whether the player is logged in or not.
*
* @param user The name of the player to verify
* @return True if logged in, false otherwise
*/
boolean isLogged(String user);
/**
* Set a player as logged in.
*
* @param user The name of the player to change
*/
void setLogged(String user);
/**
* Set a player as unlogged (not logged in).
*
* @param user The name of the player to change
*/
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.
*/
void purgeLogged();
/**
* Return all players which are logged in and whose email is not set.
*
* @return logged in players with no email
*/
List<String> getLoggedPlayersWithEmptyMail();
/**
* Return the number of registered accounts.
*
* @return Total number of accounts
*/
int getAccountsRegistered();
/**
* Update a player's real name (capitalization).
*
* @param user The name of the user (lowercase)
* @param realName The real name of the user (proper casing)
* @return True upon success, false upon failure
*/
boolean updateRealName(String user, String realName);
/**
* Returns the email of the user.
*
* @param user the user to retrieve an email for
* @return the email saved for the user, or null if user or email is not present
*/
DataSourceValue<String> getEmail(String user);
/**
* Return all players of the database.
*
* @return List of all players
*/
List<PlayerAuth> getAllAuths();
/**
* Returns the last ten players who have recently logged in (first ten players with highest last login date).
*
* @return the 10 last players who last logged in
*/
List<PlayerAuth> getRecentlyLoggedInPlayers();
/**
* Sets the given TOTP key to the player's account.
*
* @param user the name of the player to modify
* @param totpKey the totp key to set
* @return True upon success, false upon failure
*/
boolean setTotpKey(String user, String totpKey);
/**
* Removes the TOTP key if present of the given player's account.
*
* @param user the name of the player to modify
* @return True upon success, false upon failure
*/
default boolean removeTotpKey(String user) {
return setTotpKey(user, null);
}
/**
* Reload the data source.
*/
@Override
void reload();
/**
* Invalidate any cached data related to the specified player name.
*
* @param playerName the player name
*/
default void invalidateCache(String playerName) {
}
/**
* Refresh any cached data (if present) related to the specified player name.
*
* @param playerName the player name
*/
default void refreshCache(String playerName) {
}
}
@@ -0,0 +1,16 @@
package fr.xephi.authme.datasource;
/**
* DataSource type.
*/
public enum DataSourceType {
MYSQL,
MARIADB,
POSTGRESQL,
SQLITE
}
@@ -0,0 +1,27 @@
package fr.xephi.authme.datasource;
import fr.xephi.authme.datasource.mysqlextensions.MySqlExtensionsFactory;
import fr.xephi.authme.settings.Settings;
import java.sql.SQLException;
public class MariaDB extends MySQL {
public MariaDB(Settings settings, MySqlExtensionsFactory extensionsFactory) throws SQLException {
super(settings, extensionsFactory);
}
@Override
String getJdbcUrl(String host, String port, String database) {
return "jdbc:mariadb://" + host + ":" + port + "/" + database;
}
@Override
protected String getDriverClassName() {
return "org.mariadb.jdbc.Driver";
}
@Override
public DataSourceType getType() {
return DataSourceType.MARIADB;
}
}
@@ -0,0 +1,515 @@
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.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.columnshandler.AuthMeColumnsHandler;
import fr.xephi.authme.datasource.mysqlextensions.MySqlExtension;
import fr.xephi.authme.datasource.mysqlextensions.MySqlExtensionsFactory;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import fr.xephi.authme.settings.properties.HooksSettings;
import fr.xephi.authme.util.UuidUtils;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
import static fr.xephi.authme.datasource.SqlDataSourceUtils.getNullableLong;
import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException;
/**
* MySQL data source.
*/
@SuppressWarnings({"checkstyle:AbbreviationAsWordInName"}) // Justification: Class name cannot be changed anymore
public class MySQL extends AbstractSqlDataSource {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(MySQL.class);
private boolean useSsl;
private boolean serverCertificateVerification;
private boolean allowPublicKeyRetrieval;
private String host;
private String port;
private String username;
private String password;
private String database;
private String tableName;
private int poolSize;
private int maxLifetime;
private List<String> columnOthers;
private Columns col;
private MySqlExtension sqlExtension;
private HikariDataSource ds;
public MySQL(Settings settings, MySqlExtensionsFactory extensionsFactory) throws SQLException {
setParameters(settings, extensionsFactory);
// Set the connection arguments (and check if connection is ok)
try {
this.setConnectionArguments();
} catch (RuntimeException e) {
if (e instanceof IllegalArgumentException) {
logger.warning("Invalid database arguments! Please check your configuration!");
logger.warning("If this error persists, please report it to the developer!");
}
if (e instanceof PoolInitializationException) {
logger.warning("Can't initialize database connection! Please check your configuration!");
logger.warning("If this error persists, please report it to the developer!");
}
logger.warning("Can't use the Hikari Connection Pool! Please, report this error to the developer!");
throw e;
}
// Initialize the database
try {
checkTablesAndColumns();
} catch (SQLException e) {
closeConnection();
logger.logException("Can't initialize the MySQL database:", e);
logger.warning("Please check your database settings in the config.yml file!");
throw e;
}
}
@VisibleForTesting
MySQL(Settings settings, HikariDataSource hikariDataSource, MySqlExtensionsFactory extensionsFactory) {
ds = hikariDataSource;
setParameters(settings, extensionsFactory);
}
/**
* Returns the path of the Driver class to use when connecting to the database.
*
* @return the dotted path of the SQL driver class to be used
*/
protected String getDriverClassName() {
return "com.mysql.cj.jdbc.Driver";
}
/**
* Retrieves various settings.
*
* @param settings the settings to read properties from
* @param extensionsFactory factory to create the MySQL extension
*/
private void setParameters(Settings settings, MySqlExtensionsFactory extensionsFactory) {
this.host = settings.getProperty(DatabaseSettings.MYSQL_HOST);
this.port = settings.getProperty(DatabaseSettings.MYSQL_PORT);
this.username = settings.getProperty(DatabaseSettings.MYSQL_USERNAME);
this.password = settings.getProperty(DatabaseSettings.MYSQL_PASSWORD);
this.database = settings.getProperty(DatabaseSettings.MYSQL_DATABASE);
this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE);
this.columnOthers = settings.getProperty(HooksSettings.MYSQL_OTHER_USERNAME_COLS);
this.col = new Columns(settings);
this.columnsHandler = AuthMeColumnsHandler.createForMySql(this::getConnection, settings);
this.sqlExtension = extensionsFactory.buildExtension(col);
this.poolSize = settings.getProperty(DatabaseSettings.MYSQL_POOL_SIZE);
this.maxLifetime = settings.getProperty(DatabaseSettings.MYSQL_CONNECTION_MAX_LIFETIME);
this.useSsl = settings.getProperty(DatabaseSettings.MYSQL_USE_SSL);
this.serverCertificateVerification = settings.getProperty(DatabaseSettings.MYSQL_CHECK_SERVER_CERTIFICATE);
this.allowPublicKeyRetrieval = settings.getProperty(DatabaseSettings.MYSQL_ALLOW_PUBLIC_KEY_RETRIEVAL);
}
/**
* Sets up the connection arguments to the database.
*/
private void setConnectionArguments() {
ds = new HikariDataSource();
ds.setPoolName("AuthMeMYSQLPool");
// Pool Settings
ds.setMaximumPoolSize(poolSize);
ds.setMaxLifetime(maxLifetime * 1000L);
// Database URL
ds.setJdbcUrl(this.getJdbcUrl(this.host, this.port, this.database));
// Auth
ds.setUsername(this.username);
ds.setPassword(this.password);
// Driver
ds.setDriverClassName(this.getDriverClassName());
// Request mysql over SSL
ds.addDataSourceProperty("useSSL", String.valueOf(useSsl));
// Disabling server certificate verification on need
if (!serverCertificateVerification) {
ds.addDataSourceProperty("verifyServerCertificate", String.valueOf(false));
} // Disabling server certificate verification on need
if (allowPublicKeyRetrieval) {
ds.addDataSourceProperty("allowPublicKeyRetrieval", String.valueOf(true));
}
// Encoding
ds.addDataSourceProperty("characterEncoding", "utf8");
ds.addDataSourceProperty("encoding", "UTF-8");
ds.addDataSourceProperty("useUnicode", "true");
// Random stuff
ds.addDataSourceProperty("rewriteBatchedStatements", "true");
ds.addDataSourceProperty("jdbcCompliantTruncation", "false");
// Caching
ds.addDataSourceProperty("cachePrepStmts", "true");
ds.addDataSourceProperty("prepStmtCacheSize", "275");
ds.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
logger.info("Connection arguments loaded, Hikari ConnectionPool ready!");
}
@Override
public void reload() {
if (ds != null) {
ds.close();
}
setConnectionArguments();
logger.info("Hikari ConnectionPool arguments reloaded!");
}
private Connection getConnection() throws SQLException {
return ds.getConnection();
}
/**
* Creates the table or any of its required columns if they don't exist.
*/
@SuppressWarnings({"checkstyle:CyclomaticComplexity", "checkstyle:JavaNCSS"})
private void checkTablesAndColumns() throws SQLException {
try (Connection con = getConnection(); Statement st = con.createStatement()) {
// 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,"
+ "PRIMARY KEY (" + col.ID + ")"
+ ") CHARACTER SET = utf8;";
st.executeUpdate(sql);
DatabaseMetaData md = con.getMetaData();
if (isColumnMissing(md, col.NAME)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.NAME + " VARCHAR(255) NOT NULL UNIQUE AFTER " + col.ID + ";");
}
if (isColumnMissing(md, col.REAL_NAME)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.REAL_NAME + " VARCHAR(255) NOT NULL AFTER " + col.NAME + ";");
}
if (isColumnMissing(md, col.PASSWORD)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.PASSWORD + " VARCHAR(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL;");
}
if (!col.SALT.isEmpty() && isColumnMissing(md, col.SALT)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.SALT + " VARCHAR(255);");
}
if (isColumnMissing(md, col.LAST_IP)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.LAST_IP + " VARCHAR(40) CHARACTER SET ascii COLLATE ascii_bin;");
} else {
MySqlMigrater.migrateLastIpColumn(st, md, tableName, col);
}
if (isColumnMissing(md, col.LAST_LOGIN)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.LAST_LOGIN + " BIGINT;");
} else {
MySqlMigrater.migrateLastLoginColumn(st, md, tableName, col);
}
if (isColumnMissing(md, col.REGISTRATION_DATE)) {
MySqlMigrater.addRegistrationDateColumn(st, tableName, col);
}
if (isColumnMissing(md, col.REGISTRATION_IP)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.REGISTRATION_IP + " VARCHAR(40) CHARACTER SET ascii COLLATE ascii_bin;");
}
if (isColumnMissing(md, col.LASTLOC_X)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
+ col.LASTLOC_X + " DOUBLE NOT NULL DEFAULT '0.0' AFTER " + col.LAST_LOGIN + " , ADD "
+ col.LASTLOC_Y + " DOUBLE NOT NULL DEFAULT '0.0' AFTER " + col.LASTLOC_X + " , ADD "
+ col.LASTLOC_Z + " DOUBLE NOT NULL DEFAULT '0.0' AFTER " + col.LASTLOC_Y);
} else {
st.executeUpdate("ALTER TABLE " + tableName + " MODIFY "
+ col.LASTLOC_X + " DOUBLE NOT NULL DEFAULT '0.0', MODIFY "
+ col.LASTLOC_Y + " DOUBLE NOT NULL DEFAULT '0.0', MODIFY "
+ col.LASTLOC_Z + " DOUBLE NOT NULL DEFAULT '0.0';");
}
if (isColumnMissing(md, col.LASTLOC_WORLD)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
+ col.LASTLOC_WORLD + " VARCHAR(255) NOT NULL DEFAULT 'world' AFTER " + col.LASTLOC_Z);
}
if (isColumnMissing(md, col.LASTLOC_YAW)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
+ col.LASTLOC_YAW + " FLOAT;");
}
if (isColumnMissing(md, col.LASTLOC_PITCH)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
+ col.LASTLOC_PITCH + " FLOAT;");
}
if (isColumnMissing(md, col.EMAIL)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
+ col.EMAIL + " VARCHAR(255);");
}
if (isColumnMissing(md, col.IS_LOGGED)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
+ 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);
}
if (isColumnMissing(md, col.TOTP_KEY)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.TOTP_KEY + " VARCHAR(32);");
} else if (SqlDataSourceUtils.getColumnSize(md, tableName, col.TOTP_KEY) != 32) {
st.executeUpdate("ALTER TABLE " + tableName
+ " MODIFY " + col.TOTP_KEY + " VARCHAR(32);");
}
if (!col.PLAYER_UUID.isEmpty() && isColumnMissing(md, col.PLAYER_UUID)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.PLAYER_UUID + " VARCHAR(36)");
}
}
logger.info("MySQL setup finished");
}
private boolean isColumnMissing(DatabaseMetaData metaData, String columnName) throws SQLException {
try (ResultSet rs = metaData.getColumns(database, null, tableName, columnName)) {
return !rs.next();
}
}
@Override
public PlayerAuth getAuth(String user) {
String sql = "SELECT * FROM " + tableName + " WHERE " + col.NAME + "=?;";
PlayerAuth auth;
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user.toLowerCase(Locale.ROOT));
try (ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
int id = rs.getInt(col.ID);
auth = buildAuthFromResultSet(rs);
sqlExtension.extendAuth(auth, id, con);
return auth;
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return null;
}
@Override
public boolean saveAuth(PlayerAuth auth) {
super.saveAuth(auth);
try (Connection con = getConnection()) {
if (!columnOthers.isEmpty()) {
for (String column : columnOthers) {
try (PreparedStatement pst = con.prepareStatement(
"UPDATE " + tableName + " SET " + column + "=? WHERE " + col.NAME + "=?;")) {
pst.setString(1, auth.getRealName());
pst.setString(2, auth.getNickname());
pst.executeUpdate();
}
}
}
sqlExtension.saveAuth(auth, con);
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
String getJdbcUrl(String host, String port, String database) {
return "jdbc:mysql://" + host + ":" + port + "/" + database;
}
@Override
public Set<String> getRecordsToPurge(long until) {
Set<String> list = new HashSet<>();
String select = "SELECT " + col.NAME + " FROM " + tableName + " WHERE GREATEST("
+ " COALESCE(" + col.LAST_LOGIN + ", 0),"
+ " COALESCE(" + col.REGISTRATION_DATE + ", 0)"
+ ") < ?;";
try (Connection con = getConnection();
PreparedStatement selectPst = con.prepareStatement(select)) {
selectPst.setLong(1, until);
try (ResultSet rs = selectPst.executeQuery()) {
while (rs.next()) {
list.add(rs.getString(col.NAME));
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return list;
}
@Override
public boolean removeAuth(String user) {
user = user.toLowerCase(Locale.ROOT);
String sql = "DELETE FROM " + tableName + " WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
sqlExtension.removeAuth(user, con);
pst.setString(1, user.toLowerCase(Locale.ROOT));
pst.executeUpdate();
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public void closeConnection() {
if (ds != null && !ds.isClosed()) {
ds.close();
}
}
@Override
public void purgeRecords(Collection<String> toPurge) {
String sql = "DELETE FROM " + tableName + " WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
for (String name : toPurge) {
pst.setString(1, name.toLowerCase(Locale.ROOT));
pst.executeUpdate();
}
} catch (SQLException ex) {
logSqlException(ex);
}
}
@Override
public DataSourceType getType() {
return DataSourceType.MYSQL;
}
@Override
public List<PlayerAuth> getAllAuths() {
List<PlayerAuth> auths = new ArrayList<>();
try (Connection con = getConnection(); Statement st = con.createStatement()) {
try (ResultSet rs = st.executeQuery("SELECT * FROM " + tableName)) {
while (rs.next()) {
PlayerAuth auth = buildAuthFromResultSet(rs);
sqlExtension.extendAuth(auth, rs.getInt(col.ID), con);
auths.add(auth);
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return auths;
}
@Override
public List<String> getLoggedPlayersWithEmptyMail() {
List<String> players = new ArrayList<>();
String sql = "SELECT " + col.REAL_NAME + " FROM " + tableName + " WHERE " + col.IS_LOGGED + " = 1"
+ " AND (" + col.EMAIL + " = 'your@email.com' OR " + col.EMAIL + " IS NULL);";
try (Connection con = getConnection();
Statement st = con.createStatement();
ResultSet rs = st.executeQuery(sql)) {
while (rs.next()) {
players.add(rs.getString(1));
}
} catch (SQLException ex) {
logSqlException(ex);
}
return players;
}
@Override
public List<PlayerAuth> getRecentlyLoggedInPlayers() {
List<PlayerAuth> players = new ArrayList<>();
String sql = "SELECT * FROM " + tableName + " ORDER BY " + col.LAST_LOGIN + " DESC LIMIT 10;";
try (Connection con = getConnection();
Statement st = con.createStatement();
ResultSet rs = st.executeQuery(sql)) {
while (rs.next()) {
players.add(buildAuthFromResultSet(rs));
}
} catch (SQLException e) {
logSqlException(e);
}
return players;
}
@Override
public boolean setTotpKey(String user, String totpKey) {
String sql = "UPDATE " + tableName + " SET " + col.TOTP_KEY + " = ? WHERE " + col.NAME + " = ?";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, totpKey);
pst.setString(2, user.toLowerCase(Locale.ROOT));
pst.executeUpdate();
return true;
} catch (SQLException e) {
logSqlException(e);
}
return false;
}
/**
* Creates a {@link PlayerAuth} object with the data from the provided result set.
*
* @param row the result set to read from
* @return generated player auth object with the data from the result set
* @throws SQLException .
*/
private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException {
String salt = col.SALT.isEmpty() ? null : row.getString(col.SALT);
int group = col.GROUP.isEmpty() ? -1 : row.getInt(col.GROUP);
UUID uuid = col.PLAYER_UUID.isEmpty()
? null : UuidUtils.parseUuidSafely(row.getString(col.PLAYER_UUID));
return PlayerAuth.builder()
.name(row.getString(col.NAME))
.realName(row.getString(col.REAL_NAME))
.password(row.getString(col.PASSWORD), salt)
.totpKey(row.getString(col.TOTP_KEY))
.lastLogin(getNullableLong(row, col.LAST_LOGIN))
.lastIp(row.getString(col.LAST_IP))
.email(row.getString(col.EMAIL))
.registrationDate(row.getLong(col.REGISTRATION_DATE))
.registrationIp(row.getString(col.REGISTRATION_IP))
.groupId(group)
.locWorld(row.getString(col.LASTLOC_WORLD))
.locX(row.getDouble(col.LASTLOC_X))
.locY(row.getDouble(col.LASTLOC_Y))
.locZ(row.getDouble(col.LASTLOC_Z))
.locYaw(row.getFloat(col.LASTLOC_YAW))
.locPitch(row.getFloat(col.LASTLOC_PITCH))
.uuid(uuid)
.build();
}
}
@@ -0,0 +1,116 @@
package fr.xephi.authme.datasource;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
/**
* Performs migrations on the MySQL data source if necessary.
*/
final class MySqlMigrater {
private static ConsoleLogger logger = ConsoleLoggerFactory.get(MySqlMigrater.class);
private MySqlMigrater() {
}
/**
* Changes the last IP column to be nullable if it has a NOT NULL constraint without a default value.
* Background: Before 5.2, the last IP column was initialized to be {@code NOT NULL} without a default.
* With the introduction of a registration IP column we no longer want to set the last IP column on registration.
*
* @param st Statement object to the database
* @param metaData column metadata for the table
* @param tableName the MySQL table's name
* @param col the column names configuration
*/
static void migrateLastIpColumn(Statement st, DatabaseMetaData metaData,
String tableName, Columns col) throws SQLException {
final boolean isNotNullWithoutDefault = SqlDataSourceUtils.isNotNullColumn(metaData, tableName, col.LAST_IP)
&& SqlDataSourceUtils.getColumnDefaultValue(metaData, tableName, col.LAST_IP) == null;
if (isNotNullWithoutDefault) {
String sql = String.format("ALTER TABLE %s MODIFY %s VARCHAR(40) CHARACTER SET ascii COLLATE ascii_bin",
tableName, col.LAST_IP);
st.execute(sql);
logger.info("Changed last login column to allow NULL values. Please verify the registration feature "
+ "if you are hooking into a forum.");
}
}
/**
* Checks if the last login column has a type that needs to be migrated.
*
* @param st Statement object to the database
* @param metaData column metadata for the table
* @param tableName the MySQL table's name
* @param col the column names configuration
*/
static void migrateLastLoginColumn(Statement st, DatabaseMetaData metaData,
String tableName, Columns col) throws SQLException {
final int columnType;
try (ResultSet rs = metaData.getColumns(null, null, tableName, col.LAST_LOGIN)) {
if (!rs.next()) {
logger.warning("Could not get LAST_LOGIN meta data. This should never happen!");
return;
}
columnType = rs.getInt("DATA_TYPE");
}
if (columnType == Types.INTEGER) {
migrateLastLoginColumnFromInt(st, tableName, col);
}
}
/**
* Performs conversion of lastlogin column from int to bigint.
*
* @param st Statement object to the database
* @param tableName the table name
* @param col the column names configuration
* @see <a href="https://github.com/AuthMe/AuthMeReloaded/issues/887">
* #887: Migrate lastlogin column from int32 to bigint</a>
*/
private static void migrateLastLoginColumnFromInt(Statement st, String tableName, Columns col) throws SQLException {
// Change from int to bigint
logger.info("Migrating lastlogin column from int to bigint");
String sql = String.format("ALTER TABLE %s MODIFY %s BIGINT;", tableName, col.LAST_LOGIN);
st.execute(sql);
// Migrate timestamps in seconds format to milliseconds format if they are plausible
int rangeStart = 1262304000; // timestamp for 2010-01-01
int rangeEnd = 1514678400; // timestamp for 2017-12-31
sql = String.format("UPDATE %s SET %s = %s * 1000 WHERE %s > %d AND %s < %d;",
tableName, col.LAST_LOGIN, col.LAST_LOGIN, col.LAST_LOGIN, rangeStart, col.LAST_LOGIN, rangeEnd);
int changedRows = st.executeUpdate(sql);
logger.warning("You may have entries with invalid timestamps. Please check your data "
+ "before purging. " + changedRows + " rows were migrated from seconds to milliseconds.");
}
/**
* Creates the column for registration date and sets all entries to the current timestamp.
* We do so in order to avoid issues with purging, where entries with 0 / NULL might get
* purged immediately on startup otherwise.
*
* @param st Statement object to the database
* @param tableName the table name
* @param col the column names configuration
*/
static void addRegistrationDateColumn(Statement st, String tableName, Columns col) throws SQLException {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.REGISTRATION_DATE + " BIGINT NOT NULL DEFAULT 0;");
// Use the timestamp from Java to avoid timezone issues in case JVM and database are out of sync
long currentTimestamp = System.currentTimeMillis();
int updatedRows = st.executeUpdate(String.format("UPDATE %s SET %s = %d;",
tableName, col.REGISTRATION_DATE, currentTimestamp));
logger.info("Created column '" + col.REGISTRATION_DATE + "' and set the current timestamp, "
+ currentTimestamp + ", to all " + updatedRows + " rows");
}
}
@@ -0,0 +1,472 @@
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.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.columnshandler.AuthMeColumnsHandler;
import fr.xephi.authme.datasource.mysqlextensions.MySqlExtension;
import fr.xephi.authme.datasource.mysqlextensions.MySqlExtensionsFactory;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import fr.xephi.authme.settings.properties.HooksSettings;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import static fr.xephi.authme.datasource.SqlDataSourceUtils.getNullableLong;
import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException;
/**
* PostgreSQL data source.
*/
public class PostgreSqlDataSource extends AbstractSqlDataSource {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(PostgreSqlDataSource.class);
private String host;
private String port;
private String username;
private String password;
private String database;
private String tableName;
private int poolSize;
private int maxLifetime;
private List<String> columnOthers;
private Columns col;
private MySqlExtension sqlExtension;
private HikariDataSource ds;
public PostgreSqlDataSource(Settings settings, MySqlExtensionsFactory extensionsFactory) throws SQLException {
setParameters(settings, extensionsFactory);
// Set the connection arguments (and check if connection is ok)
try {
this.setConnectionArguments();
} catch (RuntimeException e) {
if (e instanceof IllegalArgumentException) {
logger.warning("Invalid database arguments! Please check your configuration!");
logger.warning("If this error persists, please report it to the developer!");
}
if (e instanceof PoolInitializationException) {
logger.warning("Can't initialize database connection! Please check your configuration!");
logger.warning("If this error persists, please report it to the developer!");
}
logger.warning("Can't use the Hikari Connection Pool! Please, report this error to the developer!");
throw e;
}
// Initialize the database
try {
checkTablesAndColumns();
} catch (SQLException e) {
closeConnection();
logger.logException("Can't initialize the PostgreSQL database:", e);
logger.warning("Please check your database settings in the config.yml file!");
throw e;
}
}
@VisibleForTesting
PostgreSqlDataSource(Settings settings, HikariDataSource hikariDataSource,
MySqlExtensionsFactory extensionsFactory) {
ds = hikariDataSource;
setParameters(settings, extensionsFactory);
}
/**
* Retrieves various settings.
*
* @param settings the settings to read properties from
* @param extensionsFactory factory to create the MySQL extension
*/
private void setParameters(Settings settings, MySqlExtensionsFactory extensionsFactory) {
this.host = settings.getProperty(DatabaseSettings.MYSQL_HOST);
this.port = settings.getProperty(DatabaseSettings.MYSQL_PORT);
this.username = settings.getProperty(DatabaseSettings.MYSQL_USERNAME);
this.password = settings.getProperty(DatabaseSettings.MYSQL_PASSWORD);
this.database = settings.getProperty(DatabaseSettings.MYSQL_DATABASE);
this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE);
this.columnOthers = settings.getProperty(HooksSettings.MYSQL_OTHER_USERNAME_COLS);
this.col = new Columns(settings);
this.columnsHandler = AuthMeColumnsHandler.createForMySql(this::getConnection, settings);
this.sqlExtension = extensionsFactory.buildExtension(col);
this.poolSize = settings.getProperty(DatabaseSettings.MYSQL_POOL_SIZE);
this.maxLifetime = settings.getProperty(DatabaseSettings.MYSQL_CONNECTION_MAX_LIFETIME);
}
/**
* Sets up the connection arguments to the database.
*/
private void setConnectionArguments() {
ds = new HikariDataSource();
ds.setPoolName("AuthMePostgreSQLPool");
// Pool Settings
ds.setMaximumPoolSize(poolSize);
ds.setMaxLifetime(maxLifetime * 1000);
// Database URL
ds.setDriverClassName("org.postgresql.Driver");
ds.setJdbcUrl(this.getJdbcUrl(this.host, this.port, this.database));
// Auth
ds.setUsername(this.username);
ds.setPassword(this.password);
// Random stuff
ds.addDataSourceProperty("reWriteBatchedInserts", "true");
// Caching
ds.addDataSourceProperty("cachePrepStmts", "true");
ds.addDataSourceProperty("preparedStatementCacheQueries", "275");
logger.info("Connection arguments loaded, Hikari ConnectionPool ready!");
}
@Override
public void reload() {
if (ds != null) {
ds.close();
}
setConnectionArguments();
logger.info("Hikari ConnectionPool arguments reloaded!");
}
private Connection getConnection() throws SQLException {
return ds.getConnection();
}
/**
* Creates the table or any of its required columns if they don't exist.
*/
private void checkTablesAndColumns() throws SQLException {
try (Connection con = getConnection(); Statement st = con.createStatement()) {
// Create table with ID column if it doesn't exist
String sql = "CREATE TABLE IF NOT EXISTS " + tableName + " ("
+ col.ID + " BIGSERIAL,"
+ "PRIMARY KEY (" + col.ID + ")"
+ ");";
st.executeUpdate(sql);
DatabaseMetaData md = con.getMetaData();
if (isColumnMissing(md, col.NAME)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.NAME + " VARCHAR(255) NOT NULL UNIQUE;");
}
if (isColumnMissing(md, col.REAL_NAME)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.REAL_NAME + " VARCHAR(255) NOT NULL;");
}
if (isColumnMissing(md, col.PASSWORD)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.PASSWORD + " VARCHAR(255) NOT NULL;");
}
if (!col.SALT.isEmpty() && isColumnMissing(md, col.SALT)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.SALT + " VARCHAR(255);");
}
if (isColumnMissing(md, col.LAST_IP)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.LAST_IP + " VARCHAR(40);");
} else {
MySqlMigrater.migrateLastIpColumn(st, md, tableName, col);
}
if (isColumnMissing(md, col.LAST_LOGIN)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.LAST_LOGIN + " BIGINT;");
} else {
MySqlMigrater.migrateLastLoginColumn(st, md, tableName, col);
}
if (isColumnMissing(md, col.REGISTRATION_DATE)) {
MySqlMigrater.addRegistrationDateColumn(st, tableName, col);
}
if (isColumnMissing(md, col.REGISTRATION_IP)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.REGISTRATION_IP + " VARCHAR(40);");
}
if (isColumnMissing(md, col.LASTLOC_X)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
+ col.LASTLOC_X + " DOUBLE PRECISION NOT NULL DEFAULT '0.0' , ADD "
+ col.LASTLOC_Y + " DOUBLE PRECISION NOT NULL DEFAULT '0.0' , ADD "
+ col.LASTLOC_Z + " DOUBLE PRECISION NOT NULL DEFAULT '0.0';");
}
if (isColumnMissing(md, col.LASTLOC_WORLD)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
+ col.LASTLOC_WORLD + " VARCHAR(255) NOT NULL DEFAULT 'world';");
}
if (isColumnMissing(md, col.LASTLOC_YAW)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
+ col.LASTLOC_YAW + " FLOAT;");
}
if (isColumnMissing(md, col.LASTLOC_PITCH)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
+ col.LASTLOC_PITCH + " FLOAT;");
}
if (isColumnMissing(md, col.EMAIL)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
+ col.EMAIL + " VARCHAR(255);");
}
if (isColumnMissing(md, col.IS_LOGGED)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
+ col.IS_LOGGED + " SMALLINT NOT NULL DEFAULT '0';");
}
if (isColumnMissing(md, col.HAS_SESSION)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
+ col.HAS_SESSION + " SMALLINT NOT NULL DEFAULT '0';");
}
if (isColumnMissing(md, col.TOTP_KEY)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.TOTP_KEY + " VARCHAR(32);");
} else if (SqlDataSourceUtils.getColumnSize(md, tableName, col.TOTP_KEY) != 32) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ALTER COLUMN " + col.TOTP_KEY + " TYPE VARCHAR(32);");
}
if (!col.PLAYER_UUID.isEmpty() && isColumnMissing(md, col.PLAYER_UUID)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.PLAYER_UUID + " VARCHAR(36)");
}
}
logger.info("PostgreSQL setup finished");
}
private boolean isColumnMissing(DatabaseMetaData metaData, String columnName) throws SQLException {
try (ResultSet rs = metaData.getColumns(null, null, tableName, columnName.toLowerCase(Locale.ROOT))) {
return !rs.next();
}
}
@Override
public PlayerAuth getAuth(String user) {
String sql = "SELECT * FROM " + tableName + " WHERE " + col.NAME + "=?;";
PlayerAuth auth;
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user.toLowerCase(Locale.ROOT));
try (ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
int id = rs.getInt(col.ID);
auth = buildAuthFromResultSet(rs);
sqlExtension.extendAuth(auth, id, con);
return auth;
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return null;
}
@Override
public boolean saveAuth(PlayerAuth auth) {
super.saveAuth(auth);
try (Connection con = getConnection()) {
if (!columnOthers.isEmpty()) {
for (String column : columnOthers) {
try (PreparedStatement pst = con.prepareStatement(
"UPDATE " + tableName + " SET " + column + "=? WHERE " + col.NAME + "=?;")) {
pst.setString(1, auth.getRealName());
pst.setString(2, auth.getNickname());
pst.executeUpdate();
}
}
}
sqlExtension.saveAuth(auth, con);
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
String getJdbcUrl(String host, String port, String database) {
return "jdbc:postgresql://" + host + ":" + port + "/" + database;
}
@Override
public Set<String> getRecordsToPurge(long until) {
Set<String> list = new HashSet<>();
String select = "SELECT " + col.NAME + " FROM " + tableName + " WHERE GREATEST("
+ " COALESCE(" + col.LAST_LOGIN + ", 0),"
+ " COALESCE(" + col.REGISTRATION_DATE + ", 0)"
+ ") < ?;";
try (Connection con = getConnection();
PreparedStatement selectPst = con.prepareStatement(select)) {
selectPst.setLong(1, until);
try (ResultSet rs = selectPst.executeQuery()) {
while (rs.next()) {
list.add(rs.getString(col.NAME));
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return list;
}
@Override
public boolean removeAuth(String user) {
user = user.toLowerCase(Locale.ROOT);
String sql = "DELETE FROM " + tableName + " WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
sqlExtension.removeAuth(user, con);
pst.setString(1, user.toLowerCase(Locale.ROOT));
pst.executeUpdate();
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public void closeConnection() {
if (ds != null && !ds.isClosed()) {
ds.close();
}
}
@Override
public void purgeRecords(Collection<String> toPurge) {
String sql = "DELETE FROM " + tableName + " WHERE " + col.NAME + "=?;";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
for (String name : toPurge) {
pst.setString(1, name.toLowerCase(Locale.ROOT));
pst.executeUpdate();
}
} catch (SQLException ex) {
logSqlException(ex);
}
}
@Override
public DataSourceType getType() {
return DataSourceType.POSTGRESQL;
}
@Override
public List<PlayerAuth> getAllAuths() {
List<PlayerAuth> auths = new ArrayList<>();
try (Connection con = getConnection(); Statement st = con.createStatement()) {
try (ResultSet rs = st.executeQuery("SELECT * FROM " + tableName)) {
while (rs.next()) {
PlayerAuth auth = buildAuthFromResultSet(rs);
sqlExtension.extendAuth(auth, rs.getInt(col.ID), con);
auths.add(auth);
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return auths;
}
@Override
public List<String> getLoggedPlayersWithEmptyMail() {
List<String> players = new ArrayList<>();
String sql = "SELECT " + col.REAL_NAME + " FROM " + tableName + " WHERE " + col.IS_LOGGED + " = 1"
+ " AND (" + col.EMAIL + " = 'your@email.com' OR " + col.EMAIL + " IS NULL);";
try (Connection con = getConnection();
Statement st = con.createStatement();
ResultSet rs = st.executeQuery(sql)) {
while (rs.next()) {
players.add(rs.getString(1));
}
} catch (SQLException ex) {
logSqlException(ex);
}
return players;
}
@Override
public List<PlayerAuth> getRecentlyLoggedInPlayers() {
List<PlayerAuth> players = new ArrayList<>();
String sql = "SELECT * FROM " + tableName + " ORDER BY " + col.LAST_LOGIN + " DESC LIMIT 10;";
try (Connection con = getConnection();
Statement st = con.createStatement();
ResultSet rs = st.executeQuery(sql)) {
while (rs.next()) {
players.add(buildAuthFromResultSet(rs));
}
} catch (SQLException e) {
logSqlException(e);
}
return players;
}
@Override
public boolean setTotpKey(String user, String totpKey) {
String sql = "UPDATE " + tableName + " SET " + col.TOTP_KEY + " = ? WHERE " + col.NAME + " = ?";
try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, totpKey);
pst.setString(2, user.toLowerCase(Locale.ROOT));
pst.executeUpdate();
return true;
} catch (SQLException e) {
logSqlException(e);
}
return false;
}
/**
* Creates a {@link PlayerAuth} object with the data from the provided result set.
*
* @param row the result set to read from
*
* @return generated player auth object with the data from the result set
*
* @throws SQLException .
*/
private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException {
String salt = col.SALT.isEmpty() ? null : row.getString(col.SALT);
int group = col.GROUP.isEmpty() ? -1 : row.getInt(col.GROUP);
return PlayerAuth.builder()
.name(row.getString(col.NAME))
.realName(row.getString(col.REAL_NAME))
.password(row.getString(col.PASSWORD), salt)
.totpKey(row.getString(col.TOTP_KEY))
.lastLogin(getNullableLong(row, col.LAST_LOGIN))
.lastIp(row.getString(col.LAST_IP))
.email(row.getString(col.EMAIL))
.registrationDate(row.getLong(col.REGISTRATION_DATE))
.registrationIp(row.getString(col.REGISTRATION_IP))
.groupId(group)
.locWorld(row.getString(col.LASTLOC_WORLD))
.locX(row.getDouble(col.LASTLOC_X))
.locY(row.getDouble(col.LASTLOC_Y))
.locZ(row.getDouble(col.LASTLOC_Z))
.locYaw(row.getFloat(col.LASTLOC_YAW))
.locPitch(row.getFloat(col.LASTLOC_PITCH))
.build();
}
}
@@ -0,0 +1,423 @@
package fr.xephi.authme.datasource;
import com.google.common.annotations.VisibleForTesting;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.columnshandler.AuthMeColumnsHandler;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import java.io.File;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import static fr.xephi.authme.datasource.SqlDataSourceUtils.getNullableLong;
import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException;
/**
* SQLite data source.
*/
@SuppressWarnings({"checkstyle:AbbreviationAsWordInName"}) // Justification: Class name cannot be changed anymore
public class SQLite extends AbstractSqlDataSource {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(SQLite.class);
private final Settings settings;
private final File dataFolder;
private final String database;
private final String tableName;
private final Columns col;
private Connection con;
/**
* Constructor for SQLite.
*
* @param settings The settings instance
* @param dataFolder The data folder
*
* @throws SQLException when initialization of a SQL datasource failed
*/
public SQLite(Settings settings, File dataFolder) throws SQLException {
this.settings = settings;
this.dataFolder = dataFolder;
this.database = settings.getProperty(DatabaseSettings.MYSQL_DATABASE);
this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE);
this.col = new Columns(settings);
try {
this.connect();
this.setup();
this.migrateIfNeeded();
} catch (Exception ex) {
logger.logException("Error during SQLite initialization:", ex);
throw ex;
}
}
@VisibleForTesting
SQLite(Settings settings, File dataFolder, Connection connection) {
this.settings = settings;
this.dataFolder = dataFolder;
this.database = settings.getProperty(DatabaseSettings.MYSQL_DATABASE);
this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE);
this.col = new Columns(settings);
this.con = connection;
this.columnsHandler = AuthMeColumnsHandler.createForSqlite(con, settings);
}
/**
* Initializes the connection to the SQLite database.
*
* @throws SQLException when an SQL error occurs while connecting
*/
protected void connect() throws SQLException {
try {
Class.forName("org.sqlite.JDBC");
} catch (ClassNotFoundException e) {
throw new IllegalStateException("Failed to load SQLite JDBC class", e);
}
logger.debug("SQLite driver loaded");
this.con = DriverManager.getConnection(this.getJdbcUrl(this.dataFolder.getAbsolutePath(), "", this.database));
this.columnsHandler = AuthMeColumnsHandler.createForSqlite(con, settings);
}
/**
* Creates the table if necessary, or adds any missing columns to the table.
*
* @throws SQLException when an SQL error occurs while initializing the database
*/
@VisibleForTesting
@SuppressWarnings("checkstyle:CyclomaticComplexity")
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';");
}
if (isColumnMissing(md, col.PASSWORD)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.PASSWORD + " VARCHAR(255) NOT NULL DEFAULT '';");
}
if (!col.SALT.isEmpty() && isColumnMissing(md, col.SALT)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.SALT + " VARCHAR(255);");
}
if (isColumnMissing(md, col.LAST_IP)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.LAST_IP + " VARCHAR(40);");
}
if (isColumnMissing(md, col.LAST_LOGIN)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.LAST_LOGIN + " TIMESTAMP;");
}
if (isColumnMissing(md, col.REGISTRATION_IP)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.REGISTRATION_IP + " VARCHAR(40);");
}
if (isColumnMissing(md, col.REGISTRATION_DATE)) {
addRegistrationDateColumn(st);
}
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';");
}
if (isColumnMissing(md, col.LASTLOC_WORLD)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.LASTLOC_WORLD + " VARCHAR(255) NOT NULL DEFAULT 'world';");
}
if (isColumnMissing(md, col.LASTLOC_YAW)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
+ col.LASTLOC_YAW + " FLOAT;");
}
if (isColumnMissing(md, col.LASTLOC_PITCH)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
+ col.LASTLOC_PITCH + " FLOAT;");
}
if (isColumnMissing(md, col.EMAIL)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.EMAIL + " VARCHAR(255);");
}
if (isColumnMissing(md, col.IS_LOGGED)) {
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';");
}
if (isColumnMissing(md, col.TOTP_KEY)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.TOTP_KEY + " VARCHAR(32);");
}
if (!col.PLAYER_UUID.isEmpty() && isColumnMissing(md, col.PLAYER_UUID)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.PLAYER_UUID + " VARCHAR(36)");
}
}
logger.info("SQLite Setup finished");
}
/**
* Migrates the database if necessary. See {@link SqLiteMigrater} for details.
*/
@VisibleForTesting
void migrateIfNeeded() throws SQLException {
DatabaseMetaData metaData = con.getMetaData();
if (SqLiteMigrater.isMigrationRequired(metaData, tableName, col)) {
new SqLiteMigrater(settings, dataFolder).performMigration(this);
// Migration deletes the table and recreates it, therefore call connect() again
// to get an up-to-date Connection to the database
connect();
}
}
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);
try {
this.connect();
this.setup();
this.migrateIfNeeded();
} catch (SQLException ex) {
logger.logException("Error while reloading SQLite:", ex);
}
}
@Override
public PlayerAuth getAuth(String user) {
String sql = "SELECT * FROM " + tableName + " WHERE LOWER(" + col.NAME + ")=LOWER(?);";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user);
try (ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
return buildAuthFromResultSet(rs);
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return null;
}
@Override
public Set<String> getRecordsToPurge(long until) {
Set<String> list = new HashSet<>();
String select = "SELECT " + col.NAME + " FROM " + tableName + " WHERE MAX("
+ " COALESCE(" + col.LAST_LOGIN + ", 0),"
+ " COALESCE(" + col.REGISTRATION_DATE + ", 0)"
+ ") < ?;";
try (PreparedStatement selectPst = con.prepareStatement(select)) {
selectPst.setLong(1, until);
try (ResultSet rs = selectPst.executeQuery()) {
while (rs.next()) {
list.add(rs.getString(col.NAME));
}
}
} catch (SQLException ex) {
logSqlException(ex);
}
return list;
}
@Override
public void purgeRecords(Collection<String> toPurge) {
String delete = "DELETE FROM " + tableName + " WHERE " + col.NAME + "=?;";
try (PreparedStatement deletePst = con.prepareStatement(delete)) {
for (String name : toPurge) {
deletePst.setString(1, name.toLowerCase(Locale.ROOT));
deletePst.executeUpdate();
}
} catch (SQLException ex) {
logSqlException(ex);
}
}
@Override
public boolean removeAuth(String user) {
String sql = "DELETE FROM " + tableName + " WHERE " + col.NAME + "=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, user.toLowerCase(Locale.ROOT));
pst.executeUpdate();
return true;
} catch (SQLException ex) {
logSqlException(ex);
}
return false;
}
@Override
public void closeConnection() {
try {
if (con != null && !con.isClosed()) {
con.close();
}
} catch (SQLException ex) {
logSqlException(ex);
}
}
@Override
public DataSourceType getType() {
return DataSourceType.SQLITE;
}
@Override
public List<PlayerAuth> getAllAuths() {
List<PlayerAuth> auths = new ArrayList<>();
String sql = "SELECT * FROM " + tableName + ";";
try (PreparedStatement pst = con.prepareStatement(sql); ResultSet rs = pst.executeQuery()) {
while (rs.next()) {
PlayerAuth auth = buildAuthFromResultSet(rs);
auths.add(auth);
}
} catch (SQLException ex) {
logSqlException(ex);
}
return auths;
}
@Override
public List<String> getLoggedPlayersWithEmptyMail() {
List<String> players = new ArrayList<>();
String sql = "SELECT " + col.REAL_NAME + " FROM " + tableName + " WHERE " + col.IS_LOGGED + " = 1"
+ " AND (" + col.EMAIL + " = 'your@email.com' OR " + col.EMAIL + " IS NULL);";
try (Statement st = con.createStatement(); ResultSet rs = st.executeQuery(sql)) {
while (rs.next()) {
players.add(rs.getString(1));
}
} catch (SQLException ex) {
logSqlException(ex);
}
return players;
}
@Override
public List<PlayerAuth> getRecentlyLoggedInPlayers() {
List<PlayerAuth> players = new ArrayList<>();
String sql = "SELECT * FROM " + tableName + " ORDER BY " + col.LAST_LOGIN + " DESC LIMIT 10;";
try (Statement st = con.createStatement(); ResultSet rs = st.executeQuery(sql)) {
while (rs.next()) {
players.add(buildAuthFromResultSet(rs));
}
} catch (SQLException e) {
logSqlException(e);
}
return players;
}
@Override
public boolean setTotpKey(String user, String totpKey) {
String sql = "UPDATE " + tableName + " SET " + col.TOTP_KEY + " = ? WHERE " + col.NAME + " = ?";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, totpKey);
pst.setString(2, user.toLowerCase(Locale.ROOT));
pst.executeUpdate();
return true;
} catch (SQLException e) {
logSqlException(e);
}
return false;
}
private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException {
String salt = !col.SALT.isEmpty() ? row.getString(col.SALT) : null;
return PlayerAuth.builder()
.name(row.getString(col.NAME))
.email(row.getString(col.EMAIL))
.realName(row.getString(col.REAL_NAME))
.password(row.getString(col.PASSWORD), salt)
.totpKey(row.getString(col.TOTP_KEY))
.lastLogin(getNullableLong(row, col.LAST_LOGIN))
.lastIp(row.getString(col.LAST_IP))
.registrationDate(row.getLong(col.REGISTRATION_DATE))
.registrationIp(row.getString(col.REGISTRATION_IP))
.locX(row.getDouble(col.LASTLOC_X))
.locY(row.getDouble(col.LASTLOC_Y))
.locZ(row.getDouble(col.LASTLOC_Z))
.locWorld(row.getString(col.LASTLOC_WORLD))
.locYaw(row.getFloat(col.LASTLOC_YAW))
.locPitch(row.getFloat(col.LASTLOC_PITCH))
.build();
}
/**
* Creates the column for registration date and sets all entries to the current timestamp.
* We do so in order to avoid issues with purging, where entries with 0 / NULL might get
* purged immediately on startup otherwise.
*
* @param st Statement object to the database
*/
private void addRegistrationDateColumn(Statement st) throws SQLException {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.REGISTRATION_DATE + " TIMESTAMP NOT NULL DEFAULT '0';");
// Use the timestamp from Java to avoid timezone issues in case JVM and database are out of sync
long currentTimestamp = System.currentTimeMillis();
int updatedRows = st.executeUpdate(String.format("UPDATE %s SET %s = %d;",
tableName, col.REGISTRATION_DATE, currentTimestamp));
logger.info("Created column '" + col.REGISTRATION_DATE + "' and set the current timestamp, "
+ currentTimestamp + ", to all " + updatedRows + " rows");
}
@Override
String getJdbcUrl(String dataPath, String ignored, String database) {
return "jdbc:sqlite:" + dataPath + File.separator + database + ".db";
}
private static void close(Connection con) {
if (con != null) {
try {
con.close();
} catch (SQLException ex) {
logSqlException(ex);
}
}
}
}
@@ -0,0 +1,147 @@
package fr.xephi.authme.datasource;
import com.google.common.io.Files;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import fr.xephi.authme.util.FileUtils;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.sql.Statement;
/**
* Migrates the SQLite database when necessary.
*/
class SqLiteMigrater {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(SqLiteMigrater.class);
private final File dataFolder;
private final String databaseName;
private final String tableName;
private final Columns col;
SqLiteMigrater(Settings settings, File dataFolder) {
this.dataFolder = dataFolder;
this.databaseName = settings.getProperty(DatabaseSettings.MYSQL_DATABASE);
this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE);
this.col = new Columns(settings);
}
/**
* Returns whether the database needs to be migrated.
* <p>
* Background: Before commit 22911a0 (July 2016), new SQLite databases initialized the last IP column to be NOT NULL
* without a default value. Allowing the last IP to be null (#792) is therefore not compatible.
*
* @param metaData the database meta data
* @param tableName the table name (SQLite file name)
* @param col column names configuration
* @return true if a migration is necessary, false otherwise
*/
static boolean isMigrationRequired(DatabaseMetaData metaData, String tableName, Columns col) throws SQLException {
return SqlDataSourceUtils.isNotNullColumn(metaData, tableName, col.LAST_IP)
&& SqlDataSourceUtils.getColumnDefaultValue(metaData, tableName, col.LAST_IP) == null;
}
/**
* Migrates the given SQLite instance.
*
* @param sqLite the instance to migrate
*/
void performMigration(SQLite sqLite) throws SQLException {
logger.warning("YOUR SQLITE DATABASE NEEDS MIGRATING! DO NOT TURN OFF YOUR SERVER");
String backupName = createBackup();
logger.info("Made a backup of your database at 'backups/" + backupName + "'");
recreateDatabaseWithNewDefinitions(sqLite);
logger.info("SQLite database migrated successfully");
}
private String createBackup() {
File sqLite = new File(dataFolder, databaseName + ".db");
File backupDirectory = new File(dataFolder, "backups");
FileUtils.createDirectory(backupDirectory);
String backupName = "backup-" + databaseName + FileUtils.createCurrentTimeString() + ".db";
File backup = new File(backupDirectory, backupName);
try {
Files.copy(sqLite, backup);
return backupName;
} catch (IOException e) {
throw new IllegalStateException("Failed to create SQLite backup before migration", e);
}
}
/**
* Renames the current database, creates a new database under the name and copies the data
* from the renamed database to the newly created one. This is necessary because SQLite
* does not support dropping or modifying a column.
*
* @param sqLite the SQLite instance to migrate
*/
// cf. https://stackoverflow.com/questions/805363/how-do-i-rename-a-column-in-a-sqlite-database-table
private void recreateDatabaseWithNewDefinitions(SQLite sqLite) throws SQLException {
Connection connection = getConnection(sqLite);
String tempTable = "tmp_" + tableName;
try (Statement st = connection.createStatement()) {
st.execute("ALTER TABLE " + tableName + " RENAME TO " + tempTable + ";");
}
sqLite.reload();
connection = getConnection(sqLite);
try (Statement st = connection.createStatement()) {
String copySql = "INSERT INTO $table ($id, $name, $realName, $password, $lastIp, $lastLogin, $regIp, "
+ "$regDate, $locX, $locY, $locZ, $locWorld, $locPitch, $locYaw, $email, $isLogged)"
+ "SELECT $id, $name, $realName,"
+ " $password, CASE WHEN $lastIp = '127.0.0.1' OR $lastIp = '' THEN NULL else $lastIp END,"
+ " $lastLogin, $regIp, $regDate, $locX, $locY, $locZ, $locWorld, $locPitch, $locYaw,"
+ " CASE WHEN $email = 'your@email.com' THEN NULL ELSE $email END, $isLogged"
+ " FROM " + tempTable + ";";
int insertedEntries = st.executeUpdate(replaceColumnVariables(copySql));
logger.info("Copied over " + insertedEntries + " from the old table to the new one");
st.execute("DROP TABLE " + tempTable + ";");
}
}
private String replaceColumnVariables(String sql) {
String replacedSql = sql.replace("$table", tableName).replace("$id", col.ID)
.replace("$name", col.NAME).replace("$realName", col.REAL_NAME)
.replace("$password", col.PASSWORD).replace("$lastIp", col.LAST_IP)
.replace("$lastLogin", col.LAST_LOGIN).replace("$regIp", col.REGISTRATION_IP)
.replace("$regDate", col.REGISTRATION_DATE).replace("$locX", col.LASTLOC_X)
.replace("$locY", col.LASTLOC_Y).replace("$locZ", col.LASTLOC_Z)
.replace("$locWorld", col.LASTLOC_WORLD).replace("$locPitch", col.LASTLOC_PITCH)
.replace("$locYaw", col.LASTLOC_YAW).replace("$email", col.EMAIL)
.replace("$isLogged", col.IS_LOGGED);
if (replacedSql.contains("$")) {
throw new IllegalStateException("SQL still statement still has '$' in it - was a tag not replaced?"
+ " Replacement result: " + replacedSql);
}
return replacedSql;
}
/**
* Returns the connection from the given SQLite instance.
*
* @param sqLite the SQLite instance to process
* @return the connection to the SQLite database
*/
private static Connection getConnection(SQLite sqLite) {
try {
Field connectionField = SQLite.class.getDeclaredField("con");
connectionField.setAccessible(true);
return (Connection) connectionField.get(sqLite);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new IllegalStateException("Failed to get the connection from SQLite", e);
}
}
}
@@ -0,0 +1,109 @@
package fr.xephi.authme.datasource;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* Utilities for SQL data sources.
*/
public final class SqlDataSourceUtils {
private static final ConsoleLogger logger = ConsoleLoggerFactory.get(SqlDataSourceUtils.class);
private SqlDataSourceUtils() {
}
/**
* Logs a SQL exception.
*
* @param e the exception to log
*/
public static void logSqlException(SQLException e) {
logger.logException("Error during SQL operation:", e);
}
/**
* Returns the long value of a column, or null when appropriate. This method is necessary because
* JDBC's {@link ResultSet#getLong} returns {@code 0} if the entry in the database is {@code null}.
*
* @param rs the result set to read from
* @param columnName the name of the column to retrieve
* @return the value (which may be null)
* @throws SQLException :)
*/
public static Long getNullableLong(ResultSet rs, String columnName) throws SQLException {
long longValue = rs.getLong(columnName);
return rs.wasNull() ? null : longValue;
}
/**
* Returns whether the given column has a NOT NULL constraint.
*
* @param metaData the database meta data
* @param tableName the name of the table in which the column is
* @param columnName the name of the column to check
* @return true if the column is NOT NULL, false otherwise
* @throws SQLException :)
*/
public static boolean isNotNullColumn(DatabaseMetaData metaData, String tableName,
String columnName) throws SQLException {
try (ResultSet rs = metaData.getColumns(null, null, tableName, columnName)) {
if (!rs.next()) {
throw new IllegalStateException("Did not find meta data for column '"
+ columnName + "' while checking for not-null constraint");
}
int nullableCode = rs.getInt("NULLABLE");
if (nullableCode == DatabaseMetaData.columnNoNulls) {
return true;
} else if (nullableCode == DatabaseMetaData.columnNullableUnknown) {
logger.warning("Unknown nullable status for column '" + columnName + "'");
}
}
return false;
}
/**
* Returns the default value of a column (as per its SQL definition).
*
* @param metaData the database meta data
* @param tableName the name of the table in which the column is
* @param columnName the name of the column to check
* @return the default value of the column (may be null)
* @throws SQLException :)
*/
public static Object getColumnDefaultValue(DatabaseMetaData metaData, String tableName,
String columnName) throws SQLException {
try (ResultSet rs = metaData.getColumns(null, null, tableName, columnName)) {
if (!rs.next()) {
throw new IllegalStateException("Did not find meta data for column '"
+ columnName + "' while checking its default value");
}
return rs.getObject("COLUMN_DEF");
}
}
/**
* Returns the size of a column (as per its SQL definition).
*
* @param metaData the database meta data
* @param tableName the name of the table in which the column is
* @param columnName the name of the column to check
* @return the size of the column
* @throws SQLException :)
*/
public static int getColumnSize(DatabaseMetaData metaData, String tableName,
String columnName) throws SQLException {
try (ResultSet rs = metaData.getColumns(null, null, tableName, columnName)) {
if (!rs.next()) {
throw new IllegalStateException("Did not find meta data for column '"
+ columnName + "' while checking its size");
}
return rs.getInt("COLUMN_SIZE");
}
}
}
@@ -0,0 +1,86 @@
package fr.xephi.authme.datasource.columnshandler;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.ColumnOptions.DEFAULT_FOR_NULL;
import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.ColumnOptions.OPTIONAL;
import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createDouble;
import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createFloat;
import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createInteger;
import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createLong;
import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createString;
/**
* Contains column definitions for the AuthMe table.
*/
public final class AuthMeColumns {
public static final PlayerAuthColumn<String> NAME = createString(
DatabaseSettings.MYSQL_COL_NAME, PlayerAuth::getNickname);
public static final PlayerAuthColumn<String> NICK_NAME = createString(
DatabaseSettings.MYSQL_COL_REALNAME, PlayerAuth::getRealName);
public static final PlayerAuthColumn<String> PASSWORD = createString(
DatabaseSettings.MYSQL_COL_PASSWORD, auth -> auth.getPassword().getHash());
public static final PlayerAuthColumn<String> SALT = createString(
DatabaseSettings.MYSQL_COL_SALT, auth -> auth.getPassword().getSalt(), OPTIONAL);
public static final PlayerAuthColumn<String> EMAIL = createString(
DatabaseSettings.MYSQL_COL_EMAIL, PlayerAuth::getEmail, DEFAULT_FOR_NULL);
public static final PlayerAuthColumn<String> LAST_IP = createString(
DatabaseSettings.MYSQL_COL_LAST_IP, PlayerAuth::getLastIp);
public static final PlayerAuthColumn<Integer> GROUP_ID = createInteger(
DatabaseSettings.MYSQL_COL_GROUP, PlayerAuth::getGroupId, OPTIONAL);
public static final PlayerAuthColumn<Long> LAST_LOGIN = createLong(
DatabaseSettings.MYSQL_COL_LASTLOGIN, PlayerAuth::getLastLogin);
public static final PlayerAuthColumn<String> REGISTRATION_IP = createString(
DatabaseSettings.MYSQL_COL_REGISTER_IP, PlayerAuth::getRegistrationIp);
public static final PlayerAuthColumn<Long> REGISTRATION_DATE = createLong(
DatabaseSettings.MYSQL_COL_REGISTER_DATE, PlayerAuth::getRegistrationDate);
public static final PlayerAuthColumn<String> UUID = createString(
DatabaseSettings.MYSQL_COL_PLAYER_UUID,
auth -> ( auth.getUuid() == null ? null : auth.getUuid().toString()),
OPTIONAL);
// --------
// Location columns
// --------
public static final PlayerAuthColumn<Double> LOCATION_X = createDouble(
DatabaseSettings.MYSQL_COL_LASTLOC_X, PlayerAuth::getQuitLocX);
public static final PlayerAuthColumn<Double> LOCATION_Y = createDouble(
DatabaseSettings.MYSQL_COL_LASTLOC_Y, PlayerAuth::getQuitLocY);
public static final PlayerAuthColumn<Double> LOCATION_Z = createDouble(
DatabaseSettings.MYSQL_COL_LASTLOC_Z, PlayerAuth::getQuitLocZ);
public static final PlayerAuthColumn<String> LOCATION_WORLD = createString(
DatabaseSettings.MYSQL_COL_LASTLOC_WORLD, PlayerAuth::getWorld);
public static final PlayerAuthColumn<Float> LOCATION_YAW = createFloat(
DatabaseSettings.MYSQL_COL_LASTLOC_YAW, PlayerAuth::getYaw);
public static final PlayerAuthColumn<Float> LOCATION_PITCH = createFloat(
DatabaseSettings.MYSQL_COL_LASTLOC_PITCH, PlayerAuth::getPitch);
// --------
// Columns not on PlayerAuth
// --------
public static final DataSourceColumn<Integer> IS_LOGGED = createInteger(
DatabaseSettings.MYSQL_COL_ISLOGGED);
public static final DataSourceColumn<Integer> HAS_SESSION = createInteger(
DatabaseSettings.MYSQL_COL_HASSESSION);
private AuthMeColumns() {
}
}
@@ -0,0 +1,83 @@
package fr.xephi.authme.datasource.columnshandler;
import ch.jalu.configme.properties.Property;
import ch.jalu.datasourcecolumns.ColumnType;
import ch.jalu.datasourcecolumns.StandardTypes;
import fr.xephi.authme.data.auth.PlayerAuth;
import java.util.function.Function;
/**
* Util class for initializing {@link DataSourceColumn} objects.
*/
final class AuthMeColumnsFactory {
private AuthMeColumnsFactory() {
}
static DataSourceColumn<Integer> createInteger(Property<String> nameProperty,
ColumnOptions... options) {
return new DataSourceColumn<>(StandardTypes.INTEGER, nameProperty,
isOptional(options), hasDefaultForNull(options));
}
static PlayerAuthColumn<Integer> createInteger(Property<String> nameProperty,
Function<PlayerAuth, Integer> playerAuthGetter,
ColumnOptions... options) {
return createInternal(StandardTypes.INTEGER, nameProperty, playerAuthGetter, options);
}
static PlayerAuthColumn<Long> createLong(Property<String> nameProperty,
Function<PlayerAuth, Long> playerAuthGetter,
ColumnOptions... options) {
return createInternal(StandardTypes.LONG, nameProperty, playerAuthGetter, options);
}
static PlayerAuthColumn<String> createString(Property<String> nameProperty,
Function<PlayerAuth, String> playerAuthGetter,
ColumnOptions... options) {
return createInternal(StandardTypes.STRING, nameProperty, playerAuthGetter, options);
}
static PlayerAuthColumn<Double> createDouble(Property<String> nameProperty,
Function<PlayerAuth, Double> playerAuthGetter,
ColumnOptions... options) {
return createInternal(StandardTypes.DOUBLE, nameProperty, playerAuthGetter, options);
}
static PlayerAuthColumn<Float> createFloat(Property<String> nameProperty,
Function<PlayerAuth, Float> playerAuthGetter,
ColumnOptions... options) {
return createInternal(StandardTypes.FLOAT, nameProperty, playerAuthGetter, options);
}
private static <T> PlayerAuthColumn<T> createInternal(ColumnType<T> type, Property<String> nameProperty,
Function<PlayerAuth, T> authGetter,
ColumnOptions... options) {
return new PlayerAuthColumn<>(type, nameProperty, isOptional(options), hasDefaultForNull(options), authGetter);
}
private static boolean isOptional(ColumnOptions[] options) {
return containsInArray(ColumnOptions.OPTIONAL, options);
}
private static boolean hasDefaultForNull(ColumnOptions[] options) {
return containsInArray(ColumnOptions.DEFAULT_FOR_NULL, options);
}
private static boolean containsInArray(ColumnOptions needle, ColumnOptions[] haystack) {
for (ColumnOptions option : haystack) {
if (option == needle) {
return true;
}
}
return false;
}
enum ColumnOptions {
OPTIONAL,
DEFAULT_FOR_NULL
}
}
@@ -0,0 +1,207 @@
package fr.xephi.authme.datasource.columnshandler;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import ch.jalu.datasourcecolumns.data.DataSourceValues;
import ch.jalu.datasourcecolumns.data.UpdateValues;
import ch.jalu.datasourcecolumns.predicate.Predicate;
import ch.jalu.datasourcecolumns.sqlimplementation.PredicateSqlGenerator;
import ch.jalu.datasourcecolumns.sqlimplementation.SqlColumnsHandler;
import ch.jalu.datasourcecolumns.sqlimplementation.statementgenerator.ConnectionSupplier;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import java.util.Locale;
import static ch.jalu.datasourcecolumns.sqlimplementation.SqlColumnsHandlerConfig.forConnectionPool;
import static ch.jalu.datasourcecolumns.sqlimplementation.SqlColumnsHandlerConfig.forSingleConnection;
import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException;
/**
* Wrapper of {@link SqlColumnsHandler} for the AuthMe data table.
* Wraps exceptions and provides better support for operations based on a {@link PlayerAuth} object.
*/
public final class AuthMeColumnsHandler {
private final SqlColumnsHandler<ColumnContext, String> internalHandler;
private AuthMeColumnsHandler(SqlColumnsHandler<ColumnContext, String> internalHandler) {
this.internalHandler = internalHandler;
}
/**
* Creates a column handler for SQLite.
*
* @param connection the connection to the database
* @param settings plugin settings
* @return created column handler
*/
public static AuthMeColumnsHandler createForSqlite(Connection connection, Settings settings) {
ColumnContext columnContext = new ColumnContext(settings, false);
String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE);
String nameColumn = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME);
SqlColumnsHandler<ColumnContext, String> sqlColHandler = new SqlColumnsHandler<>(
forSingleConnection(connection, tableName, nameColumn, columnContext)
.setPredicateSqlGenerator(new PredicateSqlGenerator<>(columnContext, true))
);
return new AuthMeColumnsHandler(sqlColHandler);
}
/**
* Creates a column handler for MySQL.
*
* @param connectionSupplier supplier of connections from the connection pool
* @param settings plugin settings
* @return created column handler
*/
public static AuthMeColumnsHandler createForMySql(ConnectionSupplier connectionSupplier, Settings settings) {
ColumnContext columnContext = new ColumnContext(settings, true);
String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE);
String nameColumn = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME);
SqlColumnsHandler<ColumnContext, String> sqlColHandler = new SqlColumnsHandler<>(
forConnectionPool(connectionSupplier, tableName, nameColumn, columnContext));
return new AuthMeColumnsHandler(sqlColHandler);
}
/**
* Changes a column from a specific row to the given value.
*
* @param name name of the account to modify
* @param column the column to modify
* @param value the value to set the column to
* @param <T> the column type
* @return true upon success, false otherwise
*/
public <T> boolean update(String name, DataSourceColumn<T> column, T value) {
try {
return internalHandler.update(name.toLowerCase(Locale.ROOT), column, value);
} catch (SQLException e) {
logSqlException(e);
return false;
}
}
/**
* Updates a row to have the values as retrieved from the PlayerAuth object.
*
* @param auth the player auth object to modify and to get values from
* @param columns the columns to update in the row
* @return true upon success, false otherwise
*/
public boolean update(PlayerAuth auth, PlayerAuthColumn<?>... columns) {
try {
return internalHandler.update(auth.getNickname(), auth, columns);
} catch (SQLException e) {
logSqlException(e);
return false;
}
}
/**
* Updates a row to have the given values.
*
* @param name the name of the account to modify
* @param updateValues the values to set on the row
* @return true upon success, false otherwise
*/
public boolean update(String name, UpdateValues<ColumnContext> updateValues) {
try {
return internalHandler.update(name.toLowerCase(Locale.ROOT), updateValues);
} catch (SQLException e) {
logSqlException(e);
return false;
}
}
/**
* Sets the given value to the provided column for all rows which match the predicate.
*
* @param predicate the predicate to filter rows by
* @param column the column to modify on the matched rows
* @param value the new value to set
* @param <T> the column type
* @return number of modified rows
*/
public <T> int update(Predicate<ColumnContext> predicate, DataSourceColumn<T> column, T value) {
try {
return internalHandler.update(predicate, column, value);
} catch (SQLException e) {
logSqlException(e);
return 0;
}
}
/**
* Retrieves the given column from a given row.
*
* @param name the account name to look up
* @param column the column whose value should be retrieved
* @param <T> the column type
* @return the result of the lookup
* @throws SQLException .
*/
public <T> DataSourceValue<T> retrieve(String name, DataSourceColumn<T> column) throws SQLException {
return internalHandler.retrieve(name.toLowerCase(Locale.ROOT), column);
}
/**
* Retrieves multiple values from a given row.
*
* @param name the account name to look up
* @param columns the columns to retrieve
* @return map-like object with the requested values
* @throws SQLException .
*/
public DataSourceValues retrieve(String name, DataSourceColumn<?>... columns) throws SQLException {
return internalHandler.retrieve(name.toLowerCase(Locale.ROOT), columns);
}
/**
* Retrieves a column's value for all rows that satisfy the given predicate.
*
* @param predicate the predicate to fulfill
* @param column the column to retrieve from the matching rows
* @param <T> the column's value type
* @return the values of the matching rows
* @throws SQLException .
*/
public <T> List<T> retrieve(Predicate<ColumnContext> predicate, DataSourceColumn<T> column) throws SQLException {
return internalHandler.retrieve(predicate, column);
}
/**
* Inserts the given values into a new row, as taken from the player auth.
*
* @param auth the player auth to get values from
* @param columns the columns to insert
* @return true upon success, false otherwise
*/
public boolean insert(PlayerAuth auth, PlayerAuthColumn<?>... columns) {
try {
return internalHandler.insert(auth, columns);
} catch (SQLException e) {
logSqlException(e);
return false;
}
}
/**
* Returns the number of rows that match the provided predicate.
*
* @param predicate the predicate to test the rows for
* @return number of rows fulfilling the predicate
*/
public int count(Predicate<ColumnContext> predicate) {
try {
return internalHandler.count(predicate);
} catch (SQLException e) {
logSqlException(e);
return 0;
}
}
}
@@ -0,0 +1,35 @@
package fr.xephi.authme.datasource.columnshandler;
import fr.xephi.authme.settings.Settings;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Context for resolving the properties of {@link AuthMeColumns} entries.
*/
public class ColumnContext {
private final Settings settings;
private final Map<DataSourceColumn<?>, String> columnNames = new ConcurrentHashMap<>();
private final boolean hasDefaultSupport;
/**
* Constructor.
*
* @param settings plugin settings
* @param hasDefaultSupport whether or not the underlying database has support for the {@code DEFAULT} keyword
*/
public ColumnContext(Settings settings, boolean hasDefaultSupport) {
this.settings = settings;
this.hasDefaultSupport = hasDefaultSupport;
}
public String getName(DataSourceColumn<?> column) {
return columnNames.computeIfAbsent(column, k -> settings.getProperty(k.getNameProperty()));
}
public boolean hasDefaultSupport() {
return hasDefaultSupport;
}
}
@@ -0,0 +1,57 @@
package fr.xephi.authme.datasource.columnshandler;
import ch.jalu.configme.properties.Property;
import ch.jalu.datasourcecolumns.Column;
import ch.jalu.datasourcecolumns.ColumnType;
/**
* Basic {@link Column} implementation for AuthMe.
*
* @param <T> column type
*/
public class DataSourceColumn<T> implements Column<T, ColumnContext> {
private final ColumnType<T> columnType;
private final Property<String> nameProperty;
private final boolean isOptional;
private final boolean useDefaultForNull;
/**
* Constructor.
*
* @param type type of the column
* @param nameProperty property defining the column name
* @param isOptional whether or not the column can be skipped (if name is configured to empty string)
* @param useDefaultForNull whether SQL DEFAULT should be used for null values (if supported by the database)
*/
DataSourceColumn(ColumnType<T> type, Property<String> nameProperty, boolean isOptional, boolean useDefaultForNull) {
this.columnType = type;
this.nameProperty = nameProperty;
this.isOptional = isOptional;
this.useDefaultForNull = useDefaultForNull;
}
public Property<String> getNameProperty() {
return nameProperty;
}
@Override
public String resolveName(ColumnContext columnContext) {
return columnContext.getName(this);
}
@Override
public ColumnType<T> getType() {
return columnType;
}
@Override
public boolean isColumnUsed(ColumnContext columnContext) {
return !isOptional || !resolveName(columnContext).isEmpty();
}
@Override
public boolean useDefaultForNullValue(ColumnContext columnContext) {
return useDefaultForNull && columnContext.hasDefaultSupport();
}
}
@@ -0,0 +1,32 @@
package fr.xephi.authme.datasource.columnshandler;
import ch.jalu.configme.properties.Property;
import ch.jalu.datasourcecolumns.ColumnType;
import ch.jalu.datasourcecolumns.DependentColumn;
import fr.xephi.authme.data.auth.PlayerAuth;
import java.util.function.Function;
/**
* Implementation for columns which can also be retrieved from a {@link PlayerAuth} object.
*
* @param <T> column type
*/
public class PlayerAuthColumn<T> extends DataSourceColumn<T> implements DependentColumn<T, ColumnContext, PlayerAuth> {
private final Function<PlayerAuth, T> playerAuthGetter;
/*
* Constructor. See parent class for details.
*/
PlayerAuthColumn(ColumnType<T> type, Property<String> nameProperty, boolean isOptional, boolean useDefaultForNull,
Function<PlayerAuth, T> playerAuthGetter) {
super(type, nameProperty, isOptional, useDefaultForNull);
this.playerAuthGetter = playerAuthGetter;
}
@Override
public T getValueFromDependent(PlayerAuth auth) {
return playerAuthGetter.apply(auth);
}
}
@@ -0,0 +1,85 @@
package fr.xephi.authme.datasource.converter;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.datasource.DataSourceType;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import org.bukkit.command.CommandSender;
import java.util.ArrayList;
import java.util.List;
import static fr.xephi.authme.util.Utils.logAndSendMessage;
/**
* Converts from one AuthMe data source type to another.
*
* @param <S> the source type to convert from
*/
public abstract class AbstractDataSourceConverter<S extends DataSource> implements Converter {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(MySqlToSqlite.class);
private final DataSource destination;
private final DataSourceType destinationType;
/**
* Constructor.
*
* @param destination the data source to convert to
* @param destinationType the data source type of the destination. The given data source is checked that its
* type corresponds to this type before the conversion is started, enabling us to just pass
* the current data source and letting this class check that the types correspond.
*/
public AbstractDataSourceConverter(DataSource destination, DataSourceType destinationType) {
this.destination = destination;
this.destinationType = destinationType;
}
// Implementation note: Because of ForceFlatToSqlite it is possible that the CommandSender is null,
// which is never the case when a converter is launched from the /authme converter command.
@Override
public void execute(CommandSender sender) {
if (destinationType != destination.getType()) {
if (sender != null) {
sender.sendMessage("Please configure your connection to "
+ destinationType + " and re-run this command");
}
return;
}
S source;
try {
source = getSource();
} catch (Exception e) {
logAndSendMessage(sender, "The data source to convert from could not be initialized");
logger.logException("Could not initialize source:", e);
return;
}
List<String> skippedPlayers = new ArrayList<>();
for (PlayerAuth auth : source.getAllAuths()) {
if (destination.isAuthAvailable(auth.getNickname())) {
skippedPlayers.add(auth.getNickname());
} else {
destination.saveAuth(auth);
destination.updateSession(auth);
destination.updateQuitLoc(auth);
}
}
if (!skippedPlayers.isEmpty()) {
logAndSendMessage(sender, "Skipped conversion for players which were already in "
+ destinationType + ": " + String.join(", ", skippedPlayers));
}
logAndSendMessage(sender, "Database successfully converted from " + source.getType()
+ " to " + destinationType);
}
/**
* @return the data source to convert from
* @throws Exception during initialization of source
*/
protected abstract S getSource() throws Exception;
}
@@ -0,0 +1,16 @@
package fr.xephi.authme.datasource.converter;
import org.bukkit.command.CommandSender;
/**
* Interface for AuthMe converters.
*/
public interface Converter {
/**
* Execute the conversion.
*
* @param sender the sender who initialized the conversion
*/
void execute(CommandSender sender);
}
@@ -0,0 +1,82 @@
package fr.xephi.authme.datasource.converter;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.ConverterSettings;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.Locale;
/**
* Converter for CrazyLogin to AuthMe.
*/
public class CrazyLoginConverter implements Converter {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(CrazyLoginConverter.class);
private final DataSource database;
private final Settings settings;
private final File dataFolder;
@Inject
CrazyLoginConverter(@DataFolder File dataFolder, DataSource dataSource, Settings settings) {
this.dataFolder = dataFolder;
this.database = dataSource;
this.settings = settings;
}
@Override
public void execute(CommandSender sender) {
String fileName = settings.getProperty(ConverterSettings.CRAZYLOGIN_FILE_NAME);
File source = new File(dataFolder, fileName);
if (!source.exists()) {
sender.sendMessage("CrazyLogin file not found, please put " + fileName + " in AuthMe folder!");
return;
}
String line;
try (BufferedReader users = new BufferedReader(new FileReader(source))) {
while ((line = users.readLine()) != null) {
if (line.contains("|")) {
migrateAccount(line);
}
}
logger.info("CrazyLogin database has been imported correctly");
} catch (IOException ex) {
logger.warning("Can't open the crazylogin database file! Does it exist?");
logger.logException("Encountered", ex);
}
}
/**
* Moves an account from CrazyLogin to the AuthMe database.
*
* @param line line read from the CrazyLogin file (one account)
*/
private void migrateAccount(String line) {
String[] args = line.split("\\|");
if (args.length < 2 || "name".equalsIgnoreCase(args[0])) {
return;
}
String playerName = args[0];
String password = args[1];
if (password != null) {
PlayerAuth auth = PlayerAuth.builder()
.name(playerName.toLowerCase(Locale.ROOT))
.realName(playerName)
.password(password, null)
.build();
database.saveAuth(auth);
}
}
}
@@ -0,0 +1,210 @@
package fr.xephi.authme.datasource.converter;
import com.google.common.annotations.VisibleForTesting;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.ConverterSettings;
import fr.xephi.authme.util.UuidUtils;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static fr.xephi.authme.util.Utils.logAndSendMessage;
/**
* Converts data from LoginSecurity to AuthMe.
*/
public class LoginSecurityConverter implements Converter {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(LoginSecurityConverter.class);
private final File dataFolder;
private final DataSource dataSource;
private final boolean useSqlite;
private final String mySqlHost;
private final String mySqlDatabase;
private final String mySqlUser;
private final String mySqlPassword;
@Inject
LoginSecurityConverter(@DataFolder File dataFolder, DataSource dataSource, Settings settings) {
this.dataFolder = dataFolder;
this.dataSource = dataSource;
useSqlite = settings.getProperty(ConverterSettings.LOGINSECURITY_USE_SQLITE);
mySqlHost = settings.getProperty(ConverterSettings.LOGINSECURITY_MYSQL_HOST);
mySqlDatabase = settings.getProperty(ConverterSettings.LOGINSECURITY_MYSQL_DATABASE);
mySqlUser = settings.getProperty(ConverterSettings.LOGINSECURITY_MYSQL_USER);
mySqlPassword = settings.getProperty(ConverterSettings.LOGINSECURITY_MYSQL_PASSWORD);
}
@Override
public void execute(CommandSender sender) {
try (Connection connection = createConnectionOrInformSender(sender)) {
if (connection != null) {
performConversion(sender, connection);
logger.info("LoginSecurity conversion completed! Please remember to set \"legacyHashes: ['BCRYPT']\" "
+ "in your configuration file!");
}
} catch (SQLException e) {
sender.sendMessage("Failed to convert from SQLite. Please see the log for more info");
logger.logException("Could not fetch or migrate data:", e);
}
}
/**
* Performs the conversion from LoginSecurity to AuthMe.
*
* @param sender the command sender who launched the conversion
* @param connection connection to the LoginSecurity data source
*/
@VisibleForTesting
void performConversion(CommandSender sender, Connection connection) throws SQLException {
try (Statement statement = connection.createStatement()) {
statement.execute(
"SELECT * from ls_players LEFT JOIN ls_locations ON ls_locations.id = ls_players.id");
try (ResultSet resultSet = statement.getResultSet()) {
migrateData(sender, resultSet);
}
}
}
/**
* Migrates the accounts.
*
* @param sender the command sender
* @param resultSet result set with the account data to migrate
*/
private void migrateData(CommandSender sender, ResultSet resultSet) throws SQLException {
List<String> skippedPlayers = new ArrayList<>();
long successfulSaves = 0;
while (resultSet.next()) {
String name = resultSet.getString("last_name");
if (dataSource.isAuthAvailable(name)) {
skippedPlayers.add(name);
} else {
PlayerAuth auth = buildAuthFromLoginSecurity(name, resultSet);
dataSource.saveAuth(auth);
dataSource.updateSession(auth);
++successfulSaves;
}
}
logAndSendMessage(sender, "Migrated " + successfulSaves + " accounts successfully from LoginSecurity");
if (!skippedPlayers.isEmpty()) {
logAndSendMessage(sender, "Skipped conversion for players which were already in AuthMe: "
+ String.join(", ", skippedPlayers));
}
}
/**
* Creates a PlayerAuth based on data extracted from the given result set.
*
* @param name the name of the player to build
* @param resultSet the result set to extract data from
* @return the created player auth object
*/
private static PlayerAuth buildAuthFromLoginSecurity(String name, ResultSet resultSet) throws SQLException {
Long lastLoginMillis = Optional.ofNullable(resultSet.getTimestamp("last_login"))
.map(Timestamp::getTime).orElse(null);
long regDate = Optional.ofNullable(resultSet.getDate("registration_date"))
.map(Date::getTime).orElse(System.currentTimeMillis());
UUID uuid = UuidUtils.parseUuidSafely(resultSet.getString("unique_user_id"));
return PlayerAuth.builder()
.name(name)
.realName(name)
.password(resultSet.getString("password"), null)
.lastIp(resultSet.getString("ip_address"))
.lastLogin(lastLoginMillis)
.registrationDate(regDate)
.locX(resultSet.getDouble("x"))
.locY(resultSet.getDouble("y"))
.locZ(resultSet.getDouble("z"))
.locWorld(resultSet.getString("world"))
.locYaw(resultSet.getFloat("yaw"))
.locPitch(resultSet.getFloat("pitch"))
.uuid(uuid)
.build();
}
/**
* Creates a {@link Connection} to the LoginSecurity data source based on the settings,
* or informs the sender of the error that occurred.
*
* @param sender the command sender who launched the conversion
* @return the created connection object, or null if it failed
*/
private Connection createConnectionOrInformSender(CommandSender sender) {
Connection connection;
if (useSqlite) {
File sqliteDatabase = new File(dataFolder.getParentFile(), "LoginSecurity/LoginSecurity.db");
if (!sqliteDatabase.exists()) {
sender.sendMessage("The file '" + sqliteDatabase.getPath() + "' does not exist");
return null;
}
connection = createSqliteConnection("plugins/LoginSecurity/LoginSecurity.db");
} else {
if (mySqlDatabase.isEmpty() || mySqlUser.isEmpty()) {
sender.sendMessage("The LoginSecurity database or username is not configured in AuthMe's config.yml");
return null;
}
connection = createMySqlConnection();
}
if (connection == null) {
sender.sendMessage("Could not connect to LoginSecurity using Sqlite = "
+ useSqlite + ", see log for more info");
return null;
}
return connection;
}
/**
* Creates a connection to SQLite.
*
* @param path the path to the SQLite database
* @return the created connection
*/
@VisibleForTesting
Connection createSqliteConnection(String path) {
try {
Class.forName("org.sqlite.JDBC");
} catch (ClassNotFoundException e) {
throw new IllegalStateException(e);
}
try {
return DriverManager.getConnection(
"jdbc:sqlite:" + path, "trump", "donald");
} catch (SQLException e) {
logger.logException("Could not connect to SQLite database", e);
return null;
}
}
private Connection createMySqlConnection() {
try {
return DriverManager.getConnection(
"jdbc:mysql://" + mySqlHost + "/" + mySqlDatabase, mySqlUser, mySqlPassword);
} catch (SQLException e) {
logger.logException("Could not connect to SQLite database", e);
return null;
}
}
}
@@ -0,0 +1,31 @@
package fr.xephi.authme.datasource.converter;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.datasource.DataSourceType;
import fr.xephi.authme.datasource.MySQL;
import fr.xephi.authme.datasource.mysqlextensions.MySqlExtensionsFactory;
import fr.xephi.authme.settings.Settings;
import javax.inject.Inject;
import java.sql.SQLException;
/**
* Converts from MySQL to SQLite.
*/
public class MySqlToSqlite extends AbstractDataSourceConverter<MySQL> {
private final Settings settings;
private final MySqlExtensionsFactory mySqlExtensionsFactory;
@Inject
MySqlToSqlite(DataSource dataSource, Settings settings, MySqlExtensionsFactory mySqlExtensionsFactory) {
super(dataSource, DataSourceType.SQLITE);
this.settings = settings;
this.mySqlExtensionsFactory = mySqlExtensionsFactory;
}
@Override
protected MySQL getSource() throws SQLException {
return new MySQL(settings, mySqlExtensionsFactory);
}
}
@@ -0,0 +1,96 @@
package fr.xephi.authme.datasource.converter;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.security.PasswordSecurity;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.ConverterSettings;
import fr.xephi.authme.util.Utils;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
/**
* @author Xephi59
*/
public class RakamakConverter implements Converter {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(RakamakConverter.class);
private final DataSource database;
private final Settings settings;
private final File pluginFolder;
private final PasswordSecurity passwordSecurity;
@Inject
RakamakConverter(@DataFolder File dataFolder, DataSource dataSource, Settings settings,
PasswordSecurity passwordSecurity) {
this.database = dataSource;
this.settings = settings;
this.pluginFolder = dataFolder;
this.passwordSecurity = passwordSecurity;
}
@Override
//TODO ljacqu 20151229: Restructure this into smaller portions
public void execute(CommandSender sender) {
boolean useIp = settings.getProperty(ConverterSettings.RAKAMAK_USE_IP);
String fileName = settings.getProperty(ConverterSettings.RAKAMAK_FILE_NAME);
String ipFileName = settings.getProperty(ConverterSettings.RAKAMAK_IP_FILE_NAME);
File source = new File(pluginFolder, fileName);
File ipFiles = new File(pluginFolder, ipFileName);
Map<String, String> playerIp = new HashMap<>();
Map<String, HashedPassword> playerPassword = new HashMap<>();
try {
BufferedReader ipFile = new BufferedReader(new FileReader(ipFiles));
String line;
if (useIp) {
String tempLine;
while ((tempLine = ipFile.readLine()) != null) {
if (tempLine.contains("=")) {
String[] args = tempLine.split("=");
playerIp.put(args[0], args[1]);
}
}
}
ipFile.close();
BufferedReader users = new BufferedReader(new FileReader(source));
while ((line = users.readLine()) != null) {
if (line.contains("=")) {
String[] arguments = line.split("=");
HashedPassword hashedPassword = passwordSecurity.computeHash(arguments[1], arguments[0]);
playerPassword.put(arguments[0], hashedPassword);
}
}
users.close();
for (Entry<String, HashedPassword> m : playerPassword.entrySet()) {
String playerName = m.getKey();
HashedPassword psw = playerPassword.get(playerName);
String ip = playerIp.get(playerName);
PlayerAuth auth = PlayerAuth.builder()
.name(playerName)
.realName(playerName)
.lastIp(ip)
.password(psw)
.build();
database.saveAuth(auth);
database.updateSession(auth);
}
Utils.logAndSendMessage(sender, "Rakamak database has been imported successfully");
} catch (IOException ex) {
logger.logException("Can't open the rakamak database file! Does it exist?", ex);
}
}
}
@@ -0,0 +1,61 @@
package fr.xephi.authme.datasource.converter;
import fr.xephi.authme.AuthMe;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import javax.inject.Inject;
import java.io.File;
import java.util.Locale;
import static fr.xephi.authme.util.FileUtils.makePath;
public class RoyalAuthConverter implements Converter {
private static final String LAST_LOGIN_PATH = "timestamps.quit";
private static final String PASSWORD_PATH = "login.password";
private final ConsoleLogger logger = ConsoleLoggerFactory.get(RoyalAuthConverter.class);
private final AuthMe plugin;
private final DataSource dataSource;
@Inject
RoyalAuthConverter(AuthMe plugin, DataSource dataSource) {
this.plugin = plugin;
this.dataSource = dataSource;
}
@Override
public void execute(CommandSender sender) {
for (OfflinePlayer player : plugin.getServer().getOfflinePlayers()) {
try {
String name = player.getName().toLowerCase(Locale.ROOT);
File file = new File(makePath(".", "plugins", "RoyalAuth", "userdata", name + ".yml"));
if (dataSource.isAuthAvailable(name) || !file.exists()) {
continue;
}
FileConfiguration configuration = YamlConfiguration.loadConfiguration(file);
PlayerAuth auth = PlayerAuth.builder()
.name(name)
.password(configuration.getString(PASSWORD_PATH), null)
.lastLogin(configuration.getLong(LAST_LOGIN_PATH))
.realName(player.getName())
.build();
dataSource.saveAuth(auth);
dataSource.updateSession(auth);
} catch (Exception e) {
logger.logException("Error while trying to import " + player.getName() + " RoyalAuth data", e);
}
}
}
}
@@ -0,0 +1,32 @@
package fr.xephi.authme.datasource.converter;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.datasource.DataSourceType;
import fr.xephi.authme.datasource.SQLite;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.settings.Settings;
import javax.inject.Inject;
import java.io.File;
import java.sql.SQLException;
/**
* Converts from SQLite to MySQL.
*/
public class SqliteToSql extends AbstractDataSourceConverter<SQLite> {
private final Settings settings;
private final File dataFolder;
@Inject
SqliteToSql(Settings settings, DataSource dataSource, @DataFolder File dataFolder) {
super(dataSource, DataSourceType.MYSQL);
this.settings = settings;
this.dataFolder = dataFolder;
}
@Override
protected SQLite getSource() throws SQLException {
return new SQLite(settings, dataFolder);
}
}
@@ -0,0 +1,81 @@
package fr.xephi.authme.datasource.converter;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.util.Locale;
import java.util.Scanner;
import java.util.UUID;
import static fr.xephi.authme.util.FileUtils.makePath;
public class VAuthConverter implements Converter {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(VAuthConverter.class);
private final DataSource dataSource;
private final File vAuthPasswordsFile;
@Inject
VAuthConverter(@DataFolder File dataFolder, DataSource dataSource) {
vAuthPasswordsFile = new File(dataFolder.getParent(), makePath("vAuth", "passwords.yml"));
this.dataSource = dataSource;
}
@Override
public void execute(CommandSender sender) {
try (Scanner scanner = new Scanner(vAuthPasswordsFile)) {
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
String name = line.split(": ")[0];
String password = line.split(": ")[1];
PlayerAuth auth;
if (isUuidInstance(password)) {
String pname;
try {
pname = Bukkit.getOfflinePlayer(UUID.fromString(name)).getName();
} catch (Exception | NoSuchMethodError e) {
pname = getName(UUID.fromString(name));
}
if (pname == null) {
continue;
}
auth = PlayerAuth.builder()
.name(pname.toLowerCase(Locale.ROOT))
.realName(pname)
.password(password, null).build();
} else {
auth = PlayerAuth.builder()
.name(name.toLowerCase(Locale.ROOT))
.realName(name)
.password(password, null).build();
}
dataSource.saveAuth(auth);
}
} catch (IOException e) {
logger.logException("Error while trying to import some vAuth data", e);
}
}
private static boolean isUuidInstance(String s) {
return s.length() > 8 && s.charAt(8) == '-';
}
private String getName(UUID uuid) {
for (OfflinePlayer op : Bukkit.getOfflinePlayers()) {
if (op.getUniqueId().compareTo(uuid) == 0) {
return op.getName();
}
}
return null;
}
}
@@ -0,0 +1,149 @@
package fr.xephi.authme.datasource.converter;
import de.luricos.bukkit.xAuth.database.DatabaseTables;
import de.luricos.bukkit.xAuth.utils.xAuthLog;
import de.luricos.bukkit.xAuth.xAuth;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.util.Utils;
import org.bukkit.command.CommandSender;
import org.bukkit.plugin.PluginManager;
import javax.inject.Inject;
import java.io.File;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import static fr.xephi.authme.util.FileUtils.makePath;
public class XAuthConverter implements Converter {
@Inject
@DataFolder
private File dataFolder;
@Inject
private DataSource database;
@Inject
private PluginManager pluginManager;
XAuthConverter() {
}
@Override
public void execute(CommandSender sender) {
try {
Class.forName("de.luricos.bukkit.xAuth.xAuth");
convert(sender);
} catch (ClassNotFoundException ce) {
sender.sendMessage("xAuth has not been found, please put xAuth.jar in your plugin folder and restart!");
}
}
private void convert(CommandSender sender) {
if (pluginManager.getPlugin("xAuth") == null) {
sender.sendMessage("[AuthMe] xAuth plugin not found");
return;
}
//TODO ljacqu 20160702: xAuthDb is not used except for the existence check -- is this intended?
File xAuthDb = new File(dataFolder.getParent(), makePath("xAuth", "xAuth.h2.db"));
if (!xAuthDb.exists()) {
sender.sendMessage("[AuthMe] xAuth H2 database not found, checking for MySQL or SQLite data...");
}
List<Integer> players = getXAuthPlayers();
if (Utils.isCollectionEmpty(players)) {
sender.sendMessage("[AuthMe] Error while importing xAuthPlayers: did not find any players");
return;
}
sender.sendMessage("[AuthMe] Starting import...");
for (int id : players) {
String pl = getIdPlayer(id);
String psw = getPassword(id);
if (psw != null && !psw.isEmpty() && pl != null) {
PlayerAuth auth = PlayerAuth.builder()
.name(pl.toLowerCase(Locale.ROOT))
.realName(pl)
.password(psw, null).build();
database.saveAuth(auth);
}
}
sender.sendMessage("[AuthMe] Successfully converted from xAuth database");
}
private String getIdPlayer(int id) {
String realPass = "";
Connection conn = xAuth.getPlugin().getDatabaseController().getConnection();
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = String.format("SELECT `playername` FROM `%s` WHERE `id` = ?",
xAuth.getPlugin().getDatabaseController().getTable(DatabaseTables.ACCOUNT));
ps = conn.prepareStatement(sql);
ps.setInt(1, id);
rs = ps.executeQuery();
if (!rs.next()) {
return null;
}
realPass = rs.getString("playername").toLowerCase(Locale.ROOT);
} catch (SQLException e) {
xAuthLog.severe("Failed to retrieve name for account: " + id, e);
return null;
} finally {
xAuth.getPlugin().getDatabaseController().close(conn, ps, rs);
}
return realPass;
}
private List<Integer> getXAuthPlayers() {
List<Integer> xP = new ArrayList<>();
Connection conn = xAuth.getPlugin().getDatabaseController().getConnection();
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = String.format("SELECT * FROM `%s`",
xAuth.getPlugin().getDatabaseController().getTable(DatabaseTables.ACCOUNT));
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
while (rs.next()) {
xP.add(rs.getInt("id"));
}
} catch (SQLException e) {
xAuthLog.severe("Cannot import xAuthPlayers", e);
return new ArrayList<>();
} finally {
xAuth.getPlugin().getDatabaseController().close(conn, ps, rs);
}
return xP;
}
private String getPassword(int accountId) {
String realPass = "";
Connection conn = xAuth.getPlugin().getDatabaseController().getConnection();
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = String.format("SELECT `password`, `pwtype` FROM `%s` WHERE `id` = ?",
xAuth.getPlugin().getDatabaseController().getTable(DatabaseTables.ACCOUNT));
ps = conn.prepareStatement(sql);
ps.setInt(1, accountId);
rs = ps.executeQuery();
if (!rs.next()) {
return null;
}
realPass = rs.getString("password");
} catch (SQLException e) {
xAuthLog.severe("Failed to retrieve password hash for account: " + accountId, e);
return null;
} finally {
xAuth.getPlugin().getDatabaseController().close(conn, ps, rs);
}
return realPass;
}
}
@@ -0,0 +1,55 @@
package fr.xephi.authme.datasource.mysqlextensions;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.Columns;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.HooksSettings;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* Extension for IPB4.
*/
class Ipb4Extension extends MySqlExtension {
private final String ipbPrefix;
private final int ipbGroup;
Ipb4Extension(Settings settings, Columns col) {
super(settings, col);
this.ipbPrefix = settings.getProperty(HooksSettings.IPB_TABLE_PREFIX);
this.ipbGroup = settings.getProperty(HooksSettings.IPB_ACTIVATED_GROUP_ID);
}
@Override
public void saveAuth(PlayerAuth auth, Connection con) throws SQLException {
// Update player group in core_members
String sql = "UPDATE " + ipbPrefix + tableName
+ " SET " + tableName + ".member_group_id=? WHERE " + col.NAME + "=?;";
try (PreparedStatement pst2 = con.prepareStatement(sql)) {
pst2.setInt(1, ipbGroup);
pst2.setString(2, auth.getNickname());
pst2.executeUpdate();
}
// Get current time without ms
long time = System.currentTimeMillis() / 1000;
// update joined date
sql = "UPDATE " + ipbPrefix + tableName
+ " SET " + tableName + ".joined=? WHERE " + col.NAME + "=?;";
try (PreparedStatement pst2 = con.prepareStatement(sql)) {
pst2.setLong(1, time);
pst2.setString(2, auth.getNickname());
pst2.executeUpdate();
}
// Update last_visit
sql = "UPDATE " + ipbPrefix + tableName
+ " SET " + tableName + ".last_visit=? WHERE " + col.NAME + "=?;";
try (PreparedStatement pst2 = con.prepareStatement(sql)) {
pst2.setLong(1, time);
pst2.setString(2, auth.getNickname());
pst2.executeUpdate();
}
}
}
@@ -0,0 +1,96 @@
package fr.xephi.authme.datasource.mysqlextensions;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.Columns;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.OptionalInt;
/**
* Extension for the MySQL data source for forums. For certain password hashes (e.g. phpBB), we want
* to hook into the forum board and execute some actions specific to the forum software.
*/
public abstract class MySqlExtension {
protected final Columns col;
protected final String tableName;
MySqlExtension(Settings settings, Columns col) {
this.col = col;
this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE);
}
/**
* Performs additional actions when a new player is saved.
*
* @param auth the player auth that has been saved
* @param con connection to the sql table
* @throws SQLException .
*/
public void saveAuth(PlayerAuth auth, Connection con) throws SQLException {
// extend for custom behavior
}
/**
* Writes properties to the given PlayerAuth object that need to be retrieved in a specific manner
* when a PlayerAuth object is read from the table.
*
* @param auth the player auth object to extend
* @param id the database id of the player auth entry
* @param con connection to the sql table
* @throws SQLException .
*/
public void extendAuth(PlayerAuth auth, int id, Connection con) throws SQLException {
// extend for custom behavior
}
/**
* Performs additional actions when a user's password is changed.
*
* @param user the name of the player (lowercase)
* @param password the new password to set
* @param con connection to the sql table
* @throws SQLException .
*/
public void changePassword(String user, HashedPassword password, Connection con) throws SQLException {
// extend for custom behavior
}
/**
* Performs additional actions when a player is removed from the database.
*
* @param user the user to remove
* @param con connection to the sql table
* @throws SQLException .
*/
public void removeAuth(String user, Connection con) throws SQLException {
// extend for custom behavior
}
/**
* Fetches the database ID of the given name from the database.
*
* @param name the name to get the ID for
* @param con connection to the sql table
* @return id of the playerAuth, or empty OptionalInt if the name is not registered
* @throws SQLException .
*/
protected OptionalInt retrieveIdFromTable(String name, Connection con) throws SQLException {
String sql = "SELECT " + col.ID + " FROM " + tableName + " WHERE " + col.NAME + "=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, name);
try (ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
return OptionalInt.of(rs.getInt(col.ID));
}
}
}
return OptionalInt.empty();
}
}
@@ -0,0 +1,39 @@
package fr.xephi.authme.datasource.mysqlextensions;
import fr.xephi.authme.datasource.Columns;
import fr.xephi.authme.security.HashAlgorithm;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import javax.inject.Inject;
/**
* Creates the appropriate {@link MySqlExtension}, depending on the configured password hashing algorithm.
*/
public class MySqlExtensionsFactory {
@Inject
private Settings settings;
/**
* Creates a new {@link MySqlExtension} object according to the configured hash algorithm.
*
* @param columnsConfig the columns configuration
* @return the extension the MySQL data source should use
*/
public MySqlExtension buildExtension(Columns columnsConfig) {
HashAlgorithm hash = settings.getProperty(SecuritySettings.PASSWORD_HASH);
switch (hash) {
case IPB4:
return new Ipb4Extension(settings, columnsConfig);
case PHPBB:
return new PhpBbExtension(settings, columnsConfig);
case WORDPRESS:
return new WordpressExtension(settings, columnsConfig);
case XFBCRYPT:
return new XfBcryptExtension(settings, columnsConfig);
default:
return new NoOpExtension(settings, columnsConfig);
}
}
}
@@ -0,0 +1,14 @@
package fr.xephi.authme.datasource.mysqlextensions;
import fr.xephi.authme.datasource.Columns;
import fr.xephi.authme.settings.Settings;
/**
* Extension implementation that does not do anything.
*/
class NoOpExtension extends MySqlExtension {
NoOpExtension(Settings settings, Columns col) {
super(settings, col);
}
}
@@ -0,0 +1,83 @@
package fr.xephi.authme.datasource.mysqlextensions;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.Columns;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.HooksSettings;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.OptionalInt;
/**
* Extensions for phpBB when MySQL is used as data source.
*/
class PhpBbExtension extends MySqlExtension {
private final String phpBbPrefix;
private final int phpBbGroup;
PhpBbExtension(Settings settings, Columns col) {
super(settings, col);
this.phpBbPrefix = settings.getProperty(HooksSettings.PHPBB_TABLE_PREFIX);
this.phpBbGroup = settings.getProperty(HooksSettings.PHPBB_ACTIVATED_GROUP_ID);
}
@Override
public void saveAuth(PlayerAuth auth, Connection con) throws SQLException {
OptionalInt authId = retrieveIdFromTable(auth.getNickname(), con);
if (authId.isPresent()) {
updateSpecificsOnSave(authId.getAsInt(), auth.getNickname(), con);
}
}
private void updateSpecificsOnSave(int id, String name, Connection con) throws SQLException {
// Insert player in phpbb_user_group
String sql = "INSERT INTO " + phpBbPrefix
+ "user_group (group_id, user_id, group_leader, user_pending) VALUES (?,?,?,?);";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, phpBbGroup);
pst.setInt(2, id);
pst.setInt(3, 0);
pst.setInt(4, 0);
pst.executeUpdate();
}
// Update username_clean in phpbb_users
sql = "UPDATE " + tableName + " SET " + tableName + ".username_clean=? WHERE " + col.NAME + "=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, name);
pst.setString(2, name);
pst.executeUpdate();
}
// Update player group in phpbb_users
sql = "UPDATE " + tableName + " SET " + tableName + ".group_id=? WHERE " + col.NAME + "=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, phpBbGroup);
pst.setString(2, name);
pst.executeUpdate();
}
// Get current time without ms
long time = System.currentTimeMillis() / 1000;
// Update user_regdate
sql = "UPDATE " + tableName + " SET " + tableName + ".user_regdate=? WHERE " + col.NAME + "=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setLong(1, time);
pst.setString(2, name);
pst.executeUpdate();
}
// Update user_lastvisit
sql = "UPDATE " + tableName + " SET " + tableName + ".user_lastvisit=? WHERE " + col.NAME + "=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setLong(1, time);
pst.setString(2, name);
pst.executeUpdate();
}
// Increment num_users
sql = "UPDATE " + phpBbPrefix
+ "config SET config_value = config_value + 1 WHERE config_name = 'num_users';";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.executeUpdate();
}
}
}
@@ -0,0 +1,84 @@
package fr.xephi.authme.datasource.mysqlextensions;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.Columns;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.HooksSettings;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.OptionalInt;
/**
* MySQL extensions for Wordpress.
*/
class WordpressExtension extends MySqlExtension {
private final String wordpressPrefix;
WordpressExtension(Settings settings, Columns col) {
super(settings, col);
this.wordpressPrefix = settings.getProperty(HooksSettings.WORDPRESS_TABLE_PREFIX);
}
@Override
public void saveAuth(PlayerAuth auth, Connection con) throws SQLException {
OptionalInt authId = retrieveIdFromTable(auth.getNickname(), con);
if (authId.isPresent()) {
saveSpecifics(auth, authId.getAsInt(), con);
}
}
/**
* Saves the required data to Wordpress tables.
*
* @param auth the player data
* @param id the player id
* @param con the sql connection
* @throws SQLException .
*/
private void saveSpecifics(PlayerAuth auth, int id, Connection con) throws SQLException {
String sql = "INSERT INTO " + wordpressPrefix + "usermeta (user_id, meta_key, meta_value) VALUES (?,?,?)";
try (PreparedStatement pst = con.prepareStatement(sql)) {
new UserMetaBatchAdder(pst, id)
.addMetaRow("first_name", "")
.addMetaRow("last_name", "")
.addMetaRow("nickname", auth.getNickname())
.addMetaRow("description", "")
.addMetaRow("rich_editing", "true")
.addMetaRow("comment_shortcuts", "false")
.addMetaRow("admin_color", "fresh")
.addMetaRow("use_ssl", "0")
.addMetaRow("show_admin_bar_front", "true")
.addMetaRow(wordpressPrefix + "capabilities", "a:1:{s:10:\"subscriber\";b:1;}")
.addMetaRow(wordpressPrefix + "user_level", "0")
.addMetaRow("default_password_nag", "");
// Execute queries
pst.executeBatch();
pst.clearBatch();
}
}
/** Helper to add batch entries to the wrapped prepared statement. */
private static final class UserMetaBatchAdder {
private final PreparedStatement pst;
private final int userId;
UserMetaBatchAdder(PreparedStatement pst, int userId) {
this.pst = pst;
this.userId = userId;
}
UserMetaBatchAdder addMetaRow(String metaKey, String metaValue) throws SQLException {
pst.setInt(1, userId);
pst.setString(2, metaKey);
pst.setString(3, metaValue);
pst.addBatch();
return this;
}
}
}
@@ -0,0 +1,148 @@
package fr.xephi.authme.datasource.mysqlextensions;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.Columns;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.security.crypts.XfBCrypt;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.HooksSettings;
import java.sql.Blob;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.OptionalInt;
/**
* Extension for XFBCRYPT.
*/
class XfBcryptExtension extends MySqlExtension {
private final String xfPrefix;
private final int xfGroup;
XfBcryptExtension(Settings settings, Columns col) {
super(settings, col);
this.xfPrefix = settings.getProperty(HooksSettings.XF_TABLE_PREFIX);
this.xfGroup = settings.getProperty(HooksSettings.XF_ACTIVATED_GROUP_ID);
}
@Override
public void saveAuth(PlayerAuth auth, Connection con) throws SQLException {
OptionalInt authId = retrieveIdFromTable(auth.getNickname(), con);
if (authId.isPresent()) {
updateXenforoTablesOnSave(auth, authId.getAsInt(), con);
}
}
/**
* Updates the xenforo tables after a player auth has been saved.
*
* @param auth the player auth which was saved
* @param id the account id
* @param con connection to the database
*/
private void updateXenforoTablesOnSave(PlayerAuth auth, int id, Connection con) throws SQLException {
// Insert player password, salt in xf_user_authenticate
String sql = "INSERT INTO " + xfPrefix + "user_authenticate (user_id, scheme_class, data) VALUES (?,?,?)";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, id);
pst.setString(2, XfBCrypt.SCHEME_CLASS);
String serializedHash = XfBCrypt.serializeHash(auth.getPassword().getHash());
byte[] bytes = serializedHash.getBytes();
Blob blob = con.createBlob();
blob.setBytes(1, bytes);
pst.setBlob(3, blob);
pst.executeUpdate();
}
// Update player group in xf_users
sql = "UPDATE " + tableName + " SET " + tableName + ".user_group_id=? WHERE " + col.NAME + "=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, xfGroup);
pst.setString(2, auth.getNickname());
pst.executeUpdate();
}
// Update player permission combination in xf_users
sql = "UPDATE " + tableName + " SET " + tableName + ".permission_combination_id=? WHERE " + col.NAME + "=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, xfGroup);
pst.setString(2, auth.getNickname());
pst.executeUpdate();
}
// Insert player privacy combination in xf_user_privacy
sql = "INSERT INTO " + xfPrefix + "user_privacy (user_id, allow_view_profile, allow_post_profile, "
+ "allow_send_personal_conversation, allow_view_identities, allow_receive_news_feed) VALUES (?,?,?,?,?,?)";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, id);
pst.setString(2, "everyone");
pst.setString(3, "members");
pst.setString(4, "members");
pst.setString(5, "everyone");
pst.setString(6, "everyone");
pst.executeUpdate();
}
// Insert player group relation in xf_user_group_relation
sql = "INSERT INTO " + xfPrefix + "user_group_relation (user_id, user_group_id, is_primary) VALUES (?,?,?)";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setInt(1, id);
pst.setInt(2, xfGroup);
pst.setString(3, "1");
pst.executeUpdate();
}
}
@Override
public void extendAuth(PlayerAuth auth, int id, Connection con) throws SQLException {
try (PreparedStatement pst = con.prepareStatement(
"SELECT data FROM " + xfPrefix + "user_authenticate WHERE " + col.ID + "=?;")) {
pst.setInt(1, id);
try (ResultSet rs = pst.executeQuery()) {
if (rs.next()) {
Blob blob = rs.getBlob("data");
byte[] bytes = blob.getBytes(1, (int) blob.length());
auth.setPassword(new HashedPassword(XfBCrypt.getHashFromBlob(bytes)));
}
}
}
}
@Override
public void changePassword(String user, HashedPassword password, Connection con) throws SQLException {
OptionalInt authId = retrieveIdFromTable(user, con);
if (authId.isPresent()) {
final int id = authId.getAsInt();
// Insert password in the correct table
String sql = "UPDATE " + xfPrefix + "user_authenticate SET data=? WHERE " + col.ID + "=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
String serializedHash = XfBCrypt.serializeHash(password.getHash());
byte[] bytes = serializedHash.getBytes();
Blob blob = con.createBlob();
blob.setBytes(1, bytes);
pst.setBlob(1, blob);
pst.setInt(2, id);
pst.executeUpdate();
}
// ...
sql = "UPDATE " + xfPrefix + "user_authenticate SET scheme_class=? WHERE " + col.ID + "=?;";
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setString(1, XfBCrypt.SCHEME_CLASS);
pst.setInt(2, id);
pst.executeUpdate();
}
}
}
@Override
public void removeAuth(String user, Connection con) throws SQLException {
OptionalInt authId = retrieveIdFromTable(user, con);
if (authId.isPresent()) {
String sql = "DELETE FROM " + xfPrefix + "user_authenticate WHERE " + col.ID + "=?;";
try (PreparedStatement xfDelete = con.prepareStatement(sql)) {
xfDelete.setInt(1, authId.getAsInt());
xfDelete.executeUpdate();
}
}
}
}