Service cleanup

This commit is contained in:
Gabriele C
2016-10-04 19:08:18 +02:00
parent 5c2d7139bc
commit 42dbb27728
89 changed files with 319 additions and 257 deletions
@@ -7,15 +7,14 @@ import fr.xephi.authme.permission.AdminPermission;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.ProtectionSettings;
import fr.xephi.authme.util.BukkitService;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitTask;
import javax.inject.Inject;
import java.util.concurrent.CopyOnWriteArrayList;
import static fr.xephi.authme.util.BukkitService.TICKS_PER_MINUTE;
import static fr.xephi.authme.util.BukkitService.TICKS_PER_SECOND;
import static fr.xephi.authme.service.BukkitService.TICKS_PER_MINUTE;
import static fr.xephi.authme.service.BukkitService.TICKS_PER_SECOND;
/**
* The AntiBot Service Management class.
@@ -0,0 +1,201 @@
package fr.xephi.authme.service;
import fr.xephi.authme.AuthMe;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.datasource.DataSourceType;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.BackupSettings;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* The backup management class
*
* @author stefano
*/
public class BackupService {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd_HH-mm");
private final String dbName;
private final String dbUserName;
private final String dbPassword;
private final String tblname;
private final String path;
private final File dataFolder;
private final Settings settings;
/**
* Constructor for PerformBackup.
*
* @param instance AuthMe
* @param settings The plugin settings
*/
public BackupService(AuthMe instance, Settings settings) {
this.dataFolder = instance.getDataFolder();
this.settings = settings;
this.dbName = settings.getProperty(DatabaseSettings.MYSQL_DATABASE);
this.dbUserName = settings.getProperty(DatabaseSettings.MYSQL_USERNAME);
this.dbPassword = settings.getProperty(DatabaseSettings.MYSQL_PASSWORD);
this.tblname = settings.getProperty(DatabaseSettings.MYSQL_TABLE);
String dateString = DATE_FORMAT.format(new Date());
this.path = String.join(File.separator,
instance.getDataFolder().getPath(), "backups", "backup" + dateString);
}
/**
* Perform a backup with the given reason.
*
* @param cause The cause of the backup.
*/
public void doBackup(BackupCause cause) {
if (!settings.getProperty(BackupSettings.ENABLED)) {
// Print a warning if the backup was requested via command or by another plugin
if (cause == BackupCause.COMMAND || cause == BackupCause.OTHER) {
ConsoleLogger.warning("Can't perform a Backup: disabled in configuration. Cause of the Backup: "
+ cause.name());
}
return;
}
// Check whether a backup should be made at the specified point in time
if (BackupCause.START.equals(cause) && !settings.getProperty(BackupSettings.ON_SERVER_START)
|| BackupCause.STOP.equals(cause) && !settings.getProperty(BackupSettings.ON_SERVER_STOP)) {
return;
}
// Do backup and check return value!
if (doBackup()) {
ConsoleLogger.info("A backup has been performed successfully. Cause of the Backup: " + cause.name());
} else {
ConsoleLogger.warning("Error while performing a backup! Cause of the Backup: " + cause.name());
}
}
public boolean doBackup() {
DataSourceType dataSourceType = settings.getProperty(DatabaseSettings.BACKEND);
switch (dataSourceType) {
case FILE:
return fileBackup("auths.db");
case MYSQL:
return mySqlBackup();
case SQLITE:
return fileBackup(dbName + ".db");
default:
ConsoleLogger.warning("Unknown data source type '" + dataSourceType + "' for backup");
}
return false;
}
private boolean mySqlBackup() {
File dirBackup = new File(dataFolder + File.separator + "backups");
if (!dirBackup.exists()) {
dirBackup.mkdir();
}
String backupWindowsPath = settings.getProperty(BackupSettings.MYSQL_WINDOWS_PATH);
if (checkWindows(backupWindowsPath)) {
String executeCmd = backupWindowsPath + "\\bin\\mysqldump.exe -u " + dbUserName + " -p" + dbPassword + " " + dbName + " --tables " + tblname + " -r " + path + ".sql";
Process runtimeProcess;
try {
runtimeProcess = Runtime.getRuntime().exec(executeCmd);
int processComplete = runtimeProcess.waitFor();
if (processComplete == 0) {
ConsoleLogger.info("Backup created successfully.");
return true;
} else {
ConsoleLogger.warning("Could not create the backup! (Windows)");
}
} catch (IOException | InterruptedException e) {
ConsoleLogger.logException("Error during Windows backup:", e);
}
} else {
String executeCmd = "mysqldump -u " + dbUserName + " -p" + dbPassword + " " + dbName + " --tables " + tblname + " -r " + path + ".sql";
Process runtimeProcess;
try {
runtimeProcess = Runtime.getRuntime().exec(executeCmd);
int processComplete = runtimeProcess.waitFor();
if (processComplete == 0) {
ConsoleLogger.info("Backup created successfully.");
return true;
} else {
ConsoleLogger.warning("Could not create the backup!");
}
} catch (IOException | InterruptedException e) {
ConsoleLogger.logException("Error during backup:", e);
}
}
return false;
}
private boolean fileBackup(String backend) {
File dirBackup = new File(dataFolder + File.separator + "backups");
if (!dirBackup.exists())
dirBackup.mkdir();
try {
copy("plugins" + File.separator + "AuthMe" + File.separator + backend, path + ".db");
return true;
} catch (IOException ex) {
ConsoleLogger.logException("Encountered an error during file backup:", ex);
}
return false;
}
/**
* Check if we are under Windows and correct location of mysqldump.exe
* otherwise return error.
*
* @param windowsPath The path to check
* @return True if the path is correct, false if it is incorrect or the OS is not Windows
*/
private static boolean checkWindows(String windowsPath) {
String isWin = System.getProperty("os.name").toLowerCase();
if (isWin.contains("win")) {
if (new File(windowsPath + "\\bin\\mysqldump.exe").exists()) {
return true;
} else {
ConsoleLogger.warning("Mysql Windows Path is incorrect. Please check it");
return false;
}
}
return false;
}
private static void copy(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst);
// Transfer bytes from in to out
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
in.close();
out.close();
}
/**
* Possible backup causes.
*/
public enum BackupCause {
START,
STOP,
COMMAND,
OTHER
}
}
@@ -0,0 +1,311 @@
package fr.xephi.authme.service;
import fr.xephi.authme.AuthMe;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.PluginSettings;
import org.bukkit.BanEntry;
import org.bukkit.BanList;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitScheduler;
import org.bukkit.scheduler.BukkitTask;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Set;
/**
* Service for operations requiring the Bukkit API, such as for scheduling.
*/
public class BukkitService implements SettingsDependent {
/** Number of ticks per second in the Bukkit main thread. */
public static final int TICKS_PER_SECOND = 20;
/** Number of ticks per minute. */
public static final int TICKS_PER_MINUTE = 60 * TICKS_PER_SECOND;
private final AuthMe authMe;
private final boolean getOnlinePlayersIsCollection;
private Method getOnlinePlayers;
private boolean useAsyncTasks;
public BukkitService(AuthMe authMe, Settings settings) {
this.authMe = authMe;
getOnlinePlayersIsCollection = initializeOnlinePlayersIsCollectionField();
reload(settings);
}
/**
* Schedules a once off task to occur as soon as possible.
* <p>
* This task will be executed by the main server thread.
*
* @param task Task to be executed
* @return Task id number (-1 if scheduling failed)
*/
public int scheduleSyncDelayedTask(Runnable task) {
return Bukkit.getScheduler().scheduleSyncDelayedTask(authMe, task);
}
/**
* Schedules a once off task to occur after a delay.
* <p>
* This task will be executed by the main server thread.
*
* @param task Task to be executed
* @param delay Delay in server ticks before executing task
* @return Task id number (-1 if scheduling failed)
*/
public int scheduleSyncDelayedTask(Runnable task, long delay) {
return Bukkit.getScheduler().scheduleSyncDelayedTask(authMe, task, delay);
}
/**
* Schedules a synchronous task if async tasks are enabled; if not, it runs the task immediately.
* Use this when {@link #runTaskOptionallyAsync(Runnable) optionally asynchronous tasks} have to
* run something synchronously.
*
* @param task the task to be run
*/
public void scheduleSyncTaskFromOptionallyAsyncTask(Runnable task) {
if (useAsyncTasks) {
scheduleSyncDelayedTask(task);
} else {
task.run();
}
}
/**
* Returns a task that will run on the next server tick.
*
* @param task the task to be run
* @return a BukkitTask that contains the id number
* @throws IllegalArgumentException if plugin is null
* @throws IllegalArgumentException if task is null
*/
public BukkitTask runTask(Runnable task) {
return Bukkit.getScheduler().runTask(authMe, task);
}
/**
* Returns a task that will run after the specified number of server
* ticks.
*
* @param task the task to be run
* @param delay the ticks to wait before running the task
* @return a BukkitTask that contains the id number
* @throws IllegalArgumentException if plugin is null
* @throws IllegalArgumentException if task is null
*/
public BukkitTask runTaskLater(Runnable task, long delay) {
return Bukkit.getScheduler().runTaskLater(authMe, task, delay);
}
/**
* Schedules this task to run asynchronously or immediately executes it based on
* AuthMe's configuration.
*
* @param task the task to run
*/
public void runTaskOptionallyAsync(Runnable task) {
if (useAsyncTasks) {
runTaskAsynchronously(task);
} else {
task.run();
}
}
/**
* <b>Asynchronous tasks should never access any API in Bukkit. Great care
* should be taken to assure the thread-safety of asynchronous tasks.</b>
* <p>
* Returns a task that will run asynchronously.
*
* @param task the task to be run
* @return a BukkitTask that contains the id number
* @throws IllegalArgumentException if plugin is null
* @throws IllegalArgumentException if task is null
*/
public BukkitTask runTaskAsynchronously(Runnable task) {
return Bukkit.getScheduler().runTaskAsynchronously(authMe, task);
}
/**
* <b>Asynchronous tasks should never access any API in Bukkit. Great care
* should be taken to assure the thread-safety of asynchronous tasks.</b>
* <p>
* Returns a task that will repeatedly run asynchronously until cancelled,
* starting after the specified number of server ticks.
*
* @param task the task to be run
* @param delay the ticks to wait before running the task for the first
* time
* @param period the ticks to wait between runs
* @return a BukkitTask that contains the id number
* @throws IllegalArgumentException if task is null
*/
public BukkitTask runTaskTimerAsynchronously(Runnable task, long delay, long period) {
return Bukkit.getScheduler().runTaskTimerAsynchronously(authMe, task, delay, period);
}
/**
* Schedules the given task to repeatedly run until cancelled, starting after the
* specified number of server ticks.
*
* @param task the task to schedule
* @param delay the ticks to wait before running the task
* @param period the ticks to wait between runs
* @return a BukkitTask that contains the id number
* @throws IllegalArgumentException if plugin is null
* @throws IllegalStateException if this was already scheduled
* @see BukkitScheduler#runTaskTimer(Plugin, Runnable, long, long)
*/
public BukkitTask runTaskTimer(BukkitRunnable task, long delay, long period) {
return task.runTaskTimer(authMe, period, delay);
}
/**
* Broadcast a message to all players.
*
* @param message the message
* @return the number of players
*/
public int broadcastMessage(String message) {
return Bukkit.broadcastMessage(message);
}
/**
* Gets the player with the exact given name, case insensitive.
*
* @param name Exact name of the player to retrieve
* @return a player object if one was found, null otherwise
*/
public Player getPlayerExact(String name) {
return authMe.getServer().getPlayerExact(name);
}
/**
* Gets a set containing all banned players.
*
* @return a set containing banned players
*/
public Set<OfflinePlayer> getBannedPlayers() {
return Bukkit.getBannedPlayers();
}
/**
* Gets every player that has ever played on this server.
*
* @return an array containing all previous players
*/
public OfflinePlayer[] getOfflinePlayers() {
return Bukkit.getOfflinePlayers();
}
/**
* Safe way to retrieve the list of online players from the server. Depending on the
* implementation of the server, either an array of {@link Player} instances is being returned,
* or a Collection. Always use this wrapper to retrieve online players instead of {@link
* Bukkit#getOnlinePlayers()} directly.
*
* @return collection of online players
*
* @see <a href="https://www.spigotmc.org/threads/solved-cant-use-new-getonlineplayers.33061/">SpigotMC
* forum</a>
* @see <a href="http://stackoverflow.com/questions/32130851/player-changed-from-array-to-collection">StackOverflow</a>
*/
@SuppressWarnings("unchecked")
public Collection<? extends Player> getOnlinePlayers() {
if (getOnlinePlayersIsCollection) {
return Bukkit.getOnlinePlayers();
}
try {
// The lookup of a method via Reflections is rather expensive, so we keep a reference to it
if (getOnlinePlayers == null) {
getOnlinePlayers = Bukkit.class.getDeclaredMethod("getOnlinePlayers");
}
Object obj = getOnlinePlayers.invoke(null);
if (obj instanceof Collection<?>) {
return (Collection<? extends Player>) obj;
} else if (obj instanceof Player[]) {
return Arrays.asList((Player[]) obj);
} else {
String type = (obj == null) ? "null" : obj.getClass().getName();
ConsoleLogger.warning("Unknown list of online players of type " + type);
}
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
ConsoleLogger.logException("Could not retrieve list of online players:", e);
}
return Collections.emptyList();
}
/**
* Calls an event with the given details.
*
* @param event Event details
* @throws IllegalStateException Thrown when an asynchronous event is
* fired from synchronous code.
*/
public void callEvent(Event event) {
Bukkit.getPluginManager().callEvent(event);
}
/**
* Gets the world with the given name.
*
* @param name the name of the world to retrieve
* @return a world with the given name, or null if none exists
*/
public World getWorld(String name) {
return Bukkit.getWorld(name);
}
@Override
public void reload(Settings settings) {
useAsyncTasks = settings.getProperty(PluginSettings.USE_ASYNC_TASKS);
}
/**
* Method run upon initialization to verify whether or not the Bukkit implementation
* returns the online players as a Collection.
*
* @see #getOnlinePlayers()
*/
private static boolean initializeOnlinePlayersIsCollectionField() {
try {
Method method = Bukkit.class.getDeclaredMethod("getOnlinePlayers");
return method.getReturnType() == Collection.class;
} catch (NoSuchMethodException e) {
ConsoleLogger.warning("Error verifying if getOnlinePlayers is a collection! Method doesn't exist");
}
return false;
}
/**
* Adds a ban to the this list. If a previous ban exists, this will
* update the previous entry.
*
* @param ip the ip of the ban
* @param reason reason for the ban, null indicates implementation default
* @param expires date for the ban's expiration (unban), or null to imply
* forever
* @param source source of the ban, null indicates implementation default
* @return the entry for the newly created ban, or the entry for the
* (updated) previous ban
*/
public BanEntry banIp(String ip, String reason, Date expires, String source) {
return Bukkit.getServer().getBanList(BanList.Type.IP).addBan(ip, reason, expires, source);
}
}
@@ -0,0 +1,84 @@
package fr.xephi.authme.service;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.cache.auth.PlayerAuth;
import fr.xephi.authme.converter.ForceFlatToSqlite;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.datasource.DataSourceType;
import fr.xephi.authme.datasource.FlatFile;
import fr.xephi.authme.datasource.SQLite;
import fr.xephi.authme.security.HashAlgorithm;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.security.crypts.SHA256;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import java.util.List;
/**
* Migrations to perform during the initialization of AuthMe.
*/
public final class MigrationService {
private MigrationService() {
}
/**
* Hash all passwords to SHA256 and updated the setting if the password hash is set to the deprecated PLAINTEXT.
*
* @param settings The settings instance
* @param dataSource The data source
* @param authmeSha256 Instance to the AuthMe SHA256 encryption method implementation
*/
public static void changePlainTextToSha256(Settings settings, DataSource dataSource,
SHA256 authmeSha256) {
if (HashAlgorithm.PLAINTEXT == settings.getProperty(SecuritySettings.PASSWORD_HASH)) {
ConsoleLogger.warning("Your HashAlgorithm has been detected as plaintext and is now deprecated;"
+ " it will be changed and hashed now to the AuthMe default hashing method");
ConsoleLogger.warning("Don't stop your server; wait for the conversion to have been completed!");
List<PlayerAuth> allAuths = dataSource.getAllAuths();
for (PlayerAuth auth : allAuths) {
String hash = auth.getPassword().getHash();
if (hash.startsWith("$SHA$")) {
ConsoleLogger.warning("Skipping conversion for " + auth.getNickname() + "; detected SHA hash");
} else {
HashedPassword hashedPassword = authmeSha256.computeHash(hash, auth.getNickname());
auth.setPassword(hashedPassword);
dataSource.updatePassword(auth);
}
}
settings.setProperty(SecuritySettings.PASSWORD_HASH, HashAlgorithm.SHA256);
settings.save();
ConsoleLogger.info("Migrated " + allAuths.size() + " accounts from plaintext to SHA256");
}
}
/**
* Converts the data source from the deprecated FLATFILE type to SQLITE.
*
* @param settings The settings instance
* @param dataSource The data source
* @return The converted datasource (SQLite), or null if no migration was necessary
*/
public static DataSource convertFlatfileToSqlite(Settings settings, DataSource dataSource) {
if (DataSourceType.FILE == settings.getProperty(DatabaseSettings.BACKEND)) {
ConsoleLogger.warning("FlatFile backend has been detected and is now deprecated; it will be changed "
+ "to SQLite... Connection will be impossible until conversion is done!");
FlatFile flatFile = (FlatFile) dataSource;
try {
SQLite sqlite = new SQLite(settings);
ForceFlatToSqlite converter = new ForceFlatToSqlite(flatFile, sqlite);
converter.execute(null);
settings.setProperty(DatabaseSettings.BACKEND, DataSourceType.SQLITE);
settings.save();
return sqlite;
} catch (Exception e) {
ConsoleLogger.logException("Error during conversion from Flatfile to SQLite", e);
throw new IllegalStateException(e);
}
}
return null;
}
}
@@ -16,7 +16,7 @@ import static fr.xephi.authme.util.Utils.MILLIS_PER_HOUR;
/**
* Manager for recovery codes.
*/
public class RecoveryCodeManager implements SettingsDependent {
public class RecoveryCodeService implements SettingsDependent {
private Map<String, ExpiringEntry> recoveryCodes = new ConcurrentHashMap<>();
@@ -24,7 +24,7 @@ public class RecoveryCodeManager implements SettingsDependent {
private long recoveryCodeExpirationMillis;
@Inject
RecoveryCodeManager(Settings settings) {
RecoveryCodeService(Settings settings) {
reload(settings);
}
@@ -0,0 +1,171 @@
package fr.xephi.authme.service;
import fr.xephi.authme.cache.auth.PlayerAuth;
import fr.xephi.authme.cache.auth.PlayerCache;
import fr.xephi.authme.cache.limbo.PlayerData;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.events.AbstractTeleportEvent;
import fr.xephi.authme.events.AuthMeTeleportEvent;
import fr.xephi.authme.events.FirstSpawnTeleportEvent;
import fr.xephi.authme.events.SpawnTeleportEvent;
import fr.xephi.authme.initialization.Reloadable;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.SpawnLoader;
import fr.xephi.authme.settings.properties.RestrictionSettings;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.util.HashSet;
import java.util.Set;
import static fr.xephi.authme.settings.properties.RestrictionSettings.TELEPORT_UNAUTHED_TO_SPAWN;
/**
* Handles teleportation (placement of player to spawn).
*/
public class TeleportationService implements Reloadable {
@Inject
private Settings settings;
@Inject
private BukkitService bukkitService;
@Inject
private SpawnLoader spawnLoader;
@Inject
private PlayerCache playerCache;
@Inject
private DataSource dataSource;
private Set<String> spawnOnLoginWorlds;
TeleportationService() {
}
@PostConstruct
@Override
public void reload() {
// Use a Set for better performance with #contains()
spawnOnLoginWorlds = new HashSet<>(settings.getProperty(RestrictionSettings.FORCE_SPAWN_ON_WORLDS));
}
/**
* Teleports the player according to the settings when he joins.
* <p>
* Note: this is triggered by Bukkit's PlayerLoginEvent, during which you cannot use
* {@link Player#hasPlayedBefore()}: it always returns {@code false}. We trigger teleportation
* from the PlayerLoginEvent and not the PlayerJoinEvent to ensure that the location is overridden
* as fast as possible (cf. <a href="https://github.com/Xephi/AuthMeReloaded/issues/682">AuthMe #682</a>).
*
* @param player the player to process
* @see <a href="https://bukkit.atlassian.net/browse/BUKKIT-3521">BUKKIT-3521: Player.hasPlayedBefore()
* always false</a>
*/
public void teleportOnJoin(final Player player) {
if (!settings.getProperty(RestrictionSettings.NO_TELEPORT)
&& settings.getProperty(TELEPORT_UNAUTHED_TO_SPAWN)) {
teleportToSpawn(player, playerCache.isAuthenticated(player.getName()));
}
}
/**
* Teleports the player to the first spawn if he is new and the first spawn is configured.
*
* @param player the player to process
*/
public void teleportNewPlayerToFirstSpawn(final Player player) {
if (settings.getProperty(RestrictionSettings.NO_TELEPORT)) {
return;
}
Location firstSpawn = spawnLoader.getFirstSpawn();
if (firstSpawn == null) {
return;
}
if (!player.hasPlayedBefore() || !dataSource.isAuthAvailable(player.getName())) {
performTeleportation(player, new FirstSpawnTeleportEvent(player, firstSpawn));
}
}
/**
* Teleports the player according to the settings after having successfully logged in.
*
* @param player the player
* @param auth corresponding PlayerAuth object
* @param limbo corresponding PlayerData object
*/
public void teleportOnLogin(final Player player, PlayerAuth auth, PlayerData limbo) {
if (settings.getProperty(RestrictionSettings.NO_TELEPORT)) {
return;
}
// #856: If PlayerData comes from a persisted file, the Location might be null
String worldName = (limbo != null && limbo.getLocation() != null)
? limbo.getLocation().getWorld().getName()
: null;
// The world in PlayerData is from where the player comes, before any teleportation by AuthMe
if (mustForceSpawnAfterLogin(worldName)) {
teleportToSpawn(player, true);
} else if (settings.getProperty(TELEPORT_UNAUTHED_TO_SPAWN)) {
if (settings.getProperty(RestrictionSettings.SAVE_QUIT_LOCATION) && auth.getQuitLocY() != 0) {
Location location = buildLocationFromAuth(player, auth);
teleportBackFromSpawn(player, location);
} else if (limbo != null && limbo.getLocation() != null) {
teleportBackFromSpawn(player, limbo.getLocation());
}
}
}
private boolean mustForceSpawnAfterLogin(String worldName) {
return worldName != null && settings.getProperty(RestrictionSettings.FORCE_SPAWN_LOCATION_AFTER_LOGIN)
&& spawnOnLoginWorlds.contains(worldName);
}
private Location buildLocationFromAuth(Player player, PlayerAuth auth) {
World world = bukkitService.getWorld(auth.getWorld());
if (world == null) {
world = player.getWorld();
}
return new Location(world, auth.getQuitLocX(), auth.getQuitLocY(), auth.getQuitLocZ());
}
private void teleportBackFromSpawn(final Player player, final Location location) {
performTeleportation(player, new AuthMeTeleportEvent(player, location));
}
private void teleportToSpawn(final Player player, final boolean isAuthenticated) {
final Location spawnLoc = spawnLoader.getSpawnLocation(player);
performTeleportation(player, new SpawnTeleportEvent(player, spawnLoc, isAuthenticated));
}
/**
* Emits the teleportation event and performs teleportation according to it (potentially modified
* by external listeners). Note that not teleportation is performed if the event's location is empty.
*
* @param player the player to teleport
* @param event the event to emit and according to which to teleport
*/
private void performTeleportation(final Player player, final AbstractTeleportEvent event) {
bukkitService.scheduleSyncDelayedTask(new Runnable() {
@Override
public void run() {
bukkitService.callEvent(event);
if (player.isOnline() && isEventValid(event)) {
player.teleport(event.getTo());
}
}
});
}
private static boolean isEventValid(AbstractTeleportEvent event) {
return !event.isCancelled() && event.getTo() != null && event.getTo().getWorld() != null;
}
}
@@ -0,0 +1,202 @@
package fr.xephi.authme.service;
import com.github.authme.configme.properties.Property;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.geoip.GeoLiteAPI;
import fr.xephi.authme.initialization.Reloadable;
import fr.xephi.authme.output.MessageKey;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.permission.PlayerStatePermission;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.EmailSettings;
import fr.xephi.authme.settings.properties.ProtectionSettings;
import fr.xephi.authme.settings.properties.RestrictionSettings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import fr.xephi.authme.util.CollectionUtils;
import fr.xephi.authme.util.Utils;
import org.bukkit.command.CommandSender;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
/**
* Validation service.
*/
public class ValidationService implements Reloadable {
@Inject
private Settings settings;
@Inject
private DataSource dataSource;
@Inject
private PermissionsManager permissionsManager;
@Inject
private GeoLiteAPI geoLiteApi;
private Pattern passwordRegex;
private Set<String> unrestrictedNames;
ValidationService() { }
@PostConstruct
@Override
public void reload() {
passwordRegex = Utils.safePatternCompile(settings.getProperty(RestrictionSettings.ALLOWED_PASSWORD_REGEX));
// Use Set for more efficient contains() lookup
unrestrictedNames = new HashSet<>(settings.getProperty(RestrictionSettings.UNRESTRICTED_NAMES));
}
/**
* Verifies whether a password is valid according to the plugin settings.
*
* @param password the password to verify
* @param username the username the password is associated with
* @return the validation result
*/
public ValidationResult validatePassword(String password, String username) {
String passLow = password.toLowerCase();
if (!passwordRegex.matcher(passLow).matches()) {
return new ValidationResult(MessageKey.PASSWORD_CHARACTERS_ERROR, passwordRegex.pattern());
} else if (passLow.equalsIgnoreCase(username)) {
return new ValidationResult(MessageKey.PASSWORD_IS_USERNAME_ERROR);
} else if (password.length() < settings.getProperty(SecuritySettings.MIN_PASSWORD_LENGTH)
|| password.length() > settings.getProperty(SecuritySettings.MAX_PASSWORD_LENGTH)) {
return new ValidationResult(MessageKey.INVALID_PASSWORD_LENGTH);
} else if (settings.getProperty(SecuritySettings.UNSAFE_PASSWORDS).contains(passLow)) {
return new ValidationResult(MessageKey.PASSWORD_UNSAFE_ERROR);
}
return new ValidationResult();
}
/**
* Verifies whether the email is valid and admitted for use according to the plugin settings.
*
* @param email the email to verify
* @return true if the email is valid, false otherwise
*/
public boolean validateEmail(String email) {
if (!email.contains("@") || "your@email.com".equalsIgnoreCase(email)) {
return false;
}
final String emailDomain = email.split("@")[1];
return validateWhitelistAndBlacklist(
emailDomain, EmailSettings.DOMAIN_WHITELIST, EmailSettings.DOMAIN_BLACKLIST);
}
/**
* Queries the database whether the email is still free for registration, i.e. whether the given
* command sender may use the email to register a new account (as defined by settings and permissions).
*
* @param email the email to verify
* @param sender the command sender
* @return true if the email may be used, false otherwise (registration threshold has been exceeded)
*/
public boolean isEmailFreeForRegistration(String email, CommandSender sender) {
return permissionsManager.hasPermission(sender, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS)
|| dataSource.countAuthsByEmail(email) < settings.getProperty(EmailSettings.MAX_REG_PER_EMAIL);
}
/**
* Checks whether the player's country is allowed to join the server, based on the given IP address
* and the configured country whitelist or blacklist.
*
* @param hostAddress the IP address to verify
* @return true if the IP address' country is allowed, false otherwise
*/
public boolean isCountryAdmitted(String hostAddress) {
// Check if we have restrictions on country, if not return true and avoid the country lookup
if (settings.getProperty(ProtectionSettings.COUNTRIES_WHITELIST).isEmpty()
&& settings.getProperty(ProtectionSettings.COUNTRIES_BLACKLIST).isEmpty()) {
return true;
}
String countryCode = geoLiteApi.getCountryCode(hostAddress);
return validateWhitelistAndBlacklist(countryCode,
ProtectionSettings.COUNTRIES_WHITELIST,
ProtectionSettings.COUNTRIES_BLACKLIST);
}
/**
* Checks if the name is unrestricted according to the configured settings.
*
* @param name the name to verify
* @return true if unrestricted, false otherwise
*/
public boolean isUnrestricted(String name) {
return unrestrictedNames.contains(name.toLowerCase());
}
/**
* Verifies whether the given value is allowed according to the given whitelist and blacklist settings.
* Whitelist has precedence over blacklist: if a whitelist is set, the value is rejected if not present
* in the whitelist.
*
* @param value the value to verify
* @param whitelist the whitelist property
* @param blacklist the blacklist property
* @return true if the value is admitted by the lists, false otherwise
*/
private boolean validateWhitelistAndBlacklist(String value, Property<List<String>> whitelist,
Property<List<String>> blacklist) {
List<String> whitelistValue = settings.getProperty(whitelist);
if (!CollectionUtils.isEmpty(whitelistValue)) {
return containsIgnoreCase(whitelistValue, value);
}
List<String> blacklistValue = settings.getProperty(blacklist);
return CollectionUtils.isEmpty(blacklistValue) || !containsIgnoreCase(blacklistValue, value);
}
private static boolean containsIgnoreCase(Collection<String> coll, String needle) {
for (String entry : coll) {
if (entry.equalsIgnoreCase(needle)) {
return true;
}
}
return false;
}
public static final class ValidationResult {
private final MessageKey messageKey;
private final String[] args;
/**
* Constructor for a successful validation.
*/
public ValidationResult() {
this.messageKey = null;
this.args = null;
}
/**
* Constructor for a failed validation.
*
* @param messageKey message key of the validation error
* @param args arguments for the message key
*/
public ValidationResult(MessageKey messageKey, String... args) {
this.messageKey = messageKey;
this.args = args;
}
/**
* Returns whether an error was found during the validation, i.e. whether the validation failed.
*
* @return true if there is an error, false if the validation was successful
*/
public boolean hasError() {
return messageKey != null;
}
public MessageKey getMessageKey() {
return messageKey;
}
public String[] getArgs() {
return args;
}
}
}