Sync repo files to b22
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
package fr.xephi.authme.service;
|
||||
|
||||
import fr.xephi.authme.initialization.SettingsDependent;
|
||||
import fr.xephi.authme.message.MessageKey;
|
||||
import fr.xephi.authme.message.Messages;
|
||||
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.AtomicIntervalCounter;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
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.
|
||||
*/
|
||||
public class AntiBotService implements SettingsDependent {
|
||||
|
||||
// Instances
|
||||
private final Messages messages;
|
||||
private final PermissionsManager permissionsManager;
|
||||
private final BukkitService bukkitService;
|
||||
private final CopyOnWriteArrayList<String> antibotKicked = new CopyOnWriteArrayList<>();
|
||||
// Settings
|
||||
private int duration;
|
||||
// Service status
|
||||
private AntiBotStatus antiBotStatus;
|
||||
private boolean startup;
|
||||
private BukkitTask disableTask;
|
||||
private AtomicIntervalCounter flaggedCounter;
|
||||
|
||||
@Inject
|
||||
AntiBotService(Settings settings, Messages messages, PermissionsManager permissionsManager,
|
||||
BukkitService bukkitService) {
|
||||
// Instances
|
||||
this.messages = messages;
|
||||
this.permissionsManager = permissionsManager;
|
||||
this.bukkitService = bukkitService;
|
||||
// Initial status
|
||||
disableTask = null;
|
||||
antiBotStatus = AntiBotStatus.DISABLED;
|
||||
startup = true;
|
||||
// Load settings and start if required
|
||||
reload(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reload(Settings settings) {
|
||||
// Load settings
|
||||
duration = settings.getProperty(ProtectionSettings.ANTIBOT_DURATION);
|
||||
int sensibility = settings.getProperty(ProtectionSettings.ANTIBOT_SENSIBILITY);
|
||||
int interval = settings.getProperty(ProtectionSettings.ANTIBOT_INTERVAL);
|
||||
flaggedCounter = new AtomicIntervalCounter(sensibility, interval * 1000);
|
||||
|
||||
// Stop existing protection
|
||||
stopProtection();
|
||||
antiBotStatus = AntiBotStatus.DISABLED;
|
||||
|
||||
// If antibot is disabled, just stop
|
||||
if (!settings.getProperty(ProtectionSettings.ENABLE_ANTIBOT)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Bot activation task
|
||||
Runnable enableTask = () -> antiBotStatus = AntiBotStatus.LISTENING;
|
||||
|
||||
// Delay the schedule on first start
|
||||
if (startup) {
|
||||
int delay = settings.getProperty(ProtectionSettings.ANTIBOT_DELAY);
|
||||
bukkitService.scheduleSyncDelayedTask(enableTask, delay * TICKS_PER_SECOND);
|
||||
startup = false;
|
||||
} else {
|
||||
enableTask.run();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transitions the anti bot service to an active status.
|
||||
*/
|
||||
private void startProtection() {
|
||||
if (antiBotStatus == AntiBotStatus.ACTIVE) {
|
||||
return; // Already activating/active
|
||||
}
|
||||
if (disableTask != null) {
|
||||
disableTask.cancel();
|
||||
}
|
||||
// Schedule auto-disable
|
||||
disableTask = bukkitService.runTaskLater(this::stopProtection, duration * TICKS_PER_MINUTE);
|
||||
antiBotStatus = AntiBotStatus.ACTIVE;
|
||||
bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(() -> {
|
||||
// Inform admins
|
||||
bukkitService.getOnlinePlayers().stream()
|
||||
.filter(player -> permissionsManager.hasPermission(player, AdminPermission.ANTIBOT_MESSAGES))
|
||||
.forEach(player -> messages.send(player, MessageKey.ANTIBOT_AUTO_ENABLED_MESSAGE));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Transitions the anti bot service from active status back to listening.
|
||||
*/
|
||||
private void stopProtection() {
|
||||
if (antiBotStatus != AntiBotStatus.ACTIVE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Change status
|
||||
antiBotStatus = AntiBotStatus.LISTENING;
|
||||
flaggedCounter.reset();
|
||||
antibotKicked.clear();
|
||||
|
||||
// Cancel auto-disable task
|
||||
disableTask.cancel();
|
||||
disableTask = null;
|
||||
|
||||
// Inform admins
|
||||
String durationString = Integer.toString(duration);
|
||||
bukkitService.getOnlinePlayers().stream()
|
||||
.filter(player -> permissionsManager.hasPermission(player, AdminPermission.ANTIBOT_MESSAGES))
|
||||
.forEach(player -> messages.send(player, MessageKey.ANTIBOT_AUTO_DISABLED_MESSAGE, durationString));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the status of the AntiBot service.
|
||||
*
|
||||
* @return status of the antibot service
|
||||
*/
|
||||
public AntiBotStatus getAntiBotStatus() {
|
||||
return antiBotStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to override the status of the protection.
|
||||
*
|
||||
* @param started the new protection status
|
||||
*/
|
||||
public void overrideAntiBotStatus(boolean started) {
|
||||
if (antiBotStatus != AntiBotStatus.DISABLED) {
|
||||
if (started) {
|
||||
startProtection();
|
||||
} else {
|
||||
stopProtection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if a player should be kicked due to antibot service.
|
||||
*
|
||||
* @return if the player should be kicked
|
||||
*/
|
||||
public boolean shouldKick() {
|
||||
if (antiBotStatus == AntiBotStatus.DISABLED) {
|
||||
return false;
|
||||
} else if (antiBotStatus == AntiBotStatus.ACTIVE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (flaggedCounter.handle()) {
|
||||
startProtection();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the player was kicked because of activated antibot. The list is reset
|
||||
* when antibot is deactivated.
|
||||
*
|
||||
* @param name the name to check
|
||||
*
|
||||
* @return true if the given name has been kicked because of Antibot
|
||||
*/
|
||||
public boolean wasPlayerKicked(String name) {
|
||||
return antibotKicked.contains(name.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a name to the list of players kicked by antibot. Should only be used when a player
|
||||
* is determined to be kicked because of failed antibot verification.
|
||||
*
|
||||
* @param name the name to add
|
||||
*/
|
||||
public void addPlayerKick(String name) {
|
||||
antibotKicked.addIfAbsent(name.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
public enum AntiBotStatus {
|
||||
LISTENING,
|
||||
DISABLED,
|
||||
ACTIVE
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package fr.xephi.authme.service;
|
||||
|
||||
import fr.xephi.authme.ConsoleLogger;
|
||||
import fr.xephi.authme.datasource.DataSourceType;
|
||||
import fr.xephi.authme.initialization.DataFolder;
|
||||
import fr.xephi.authme.mail.EmailService;
|
||||
import fr.xephi.authme.output.ConsoleLoggerFactory;
|
||||
import fr.xephi.authme.settings.Settings;
|
||||
import fr.xephi.authme.settings.properties.BackupSettings;
|
||||
import fr.xephi.authme.settings.properties.DatabaseSettings;
|
||||
import fr.xephi.authme.util.FileUtils;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import javax.inject.Inject;
|
||||
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.util.Locale;
|
||||
|
||||
import static fr.xephi.authme.util.Utils.logAndSendMessage;
|
||||
import static fr.xephi.authme.util.Utils.logAndSendWarning;
|
||||
|
||||
/**
|
||||
* Performs a backup of the data source.
|
||||
*/
|
||||
public class BackupService {
|
||||
|
||||
private final ConsoleLogger logger = ConsoleLoggerFactory.get(EmailService.class);
|
||||
|
||||
private final File dataFolder;
|
||||
private final File backupFolder;
|
||||
private final Settings settings;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param dataFolder the data folder
|
||||
* @param settings the plugin settings
|
||||
*/
|
||||
@Inject
|
||||
public BackupService(@DataFolder File dataFolder, Settings settings) {
|
||||
this.dataFolder = dataFolder;
|
||||
this.backupFolder = new File(dataFolder, "backups");
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a backup for the given reason.
|
||||
*
|
||||
* @param cause backup reason
|
||||
*/
|
||||
public void doBackup(BackupCause cause) {
|
||||
doBackup(cause, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a backup for the given reason.
|
||||
*
|
||||
* @param cause backup reason
|
||||
* @param sender the command sender (nullable)
|
||||
*/
|
||||
public void doBackup(BackupCause cause, CommandSender sender) {
|
||||
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) {
|
||||
logAndSendWarning(sender,
|
||||
"Can't perform a backup: disabled in configuration. Cause of the backup: " + cause.name());
|
||||
}
|
||||
return;
|
||||
} else if (BackupCause.START == cause && !settings.getProperty(BackupSettings.ON_SERVER_START)
|
||||
|| BackupCause.STOP == cause && !settings.getProperty(BackupSettings.ON_SERVER_STOP)) {
|
||||
// Don't perform backup on start or stop if so configured
|
||||
return;
|
||||
}
|
||||
|
||||
// Do backup and check return value!
|
||||
if (doBackup()) {
|
||||
logAndSendMessage(sender,
|
||||
"A backup has been performed successfully. Cause of the backup: " + cause.name());
|
||||
} else {
|
||||
logAndSendWarning(sender, "Error while performing a backup! Cause of the backup: " + cause.name());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean doBackup() {
|
||||
DataSourceType dataSourceType = settings.getProperty(DatabaseSettings.BACKEND);
|
||||
switch (dataSourceType) {
|
||||
case MYSQL:
|
||||
return performMySqlBackup();
|
||||
case SQLITE:
|
||||
String dbName = settings.getProperty(DatabaseSettings.MYSQL_DATABASE);
|
||||
return performFileBackup(dbName + ".db");
|
||||
default:
|
||||
logger.warning("Unknown data source type '" + dataSourceType + "' for backup");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a backup for the MySQL data source.
|
||||
*
|
||||
* @return true if successful, false otherwise
|
||||
*/
|
||||
private boolean performMySqlBackup() {
|
||||
FileUtils.createDirectory(backupFolder);
|
||||
File sqlBackupFile = constructBackupFile("sql");
|
||||
|
||||
String backupWindowsPath = settings.getProperty(BackupSettings.MYSQL_WINDOWS_PATH);
|
||||
boolean isUsingWindows = useWindowsCommand(backupWindowsPath);
|
||||
String backupCommand = isUsingWindows
|
||||
? backupWindowsPath + "\\bin\\mysqldump.exe" + buildMysqlDumpArguments(sqlBackupFile)
|
||||
: "mysqldump" + buildMysqlDumpArguments(sqlBackupFile);
|
||||
|
||||
try {
|
||||
Process runtimeProcess = Runtime.getRuntime().exec(backupCommand);
|
||||
int processComplete = runtimeProcess.waitFor();
|
||||
if (processComplete == 0) {
|
||||
logger.info("Backup created successfully. (Using Windows = " + isUsingWindows + ")");
|
||||
return true;
|
||||
} else {
|
||||
logger.warning("Could not create the backup! (Using Windows = " + isUsingWindows + ")");
|
||||
}
|
||||
} catch (IOException | InterruptedException e) {
|
||||
logger.logException("Error during backup (using Windows = " + isUsingWindows + "):", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean performFileBackup(String filename) {
|
||||
FileUtils.createDirectory(backupFolder);
|
||||
File backupFile = constructBackupFile("db");
|
||||
|
||||
try {
|
||||
copy(new File(dataFolder, filename), backupFile);
|
||||
return true;
|
||||
} catch (IOException ex) {
|
||||
logger.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 boolean useWindowsCommand(String windowsPath) {
|
||||
String isWin = System.getProperty("os.name").toLowerCase(Locale.ROOT);
|
||||
if (isWin.contains("win")) {
|
||||
if (new File(windowsPath + "\\bin\\mysqldump.exe").exists()) {
|
||||
return true;
|
||||
} else {
|
||||
logger.warning("Mysql Windows Path is incorrect. Please check it");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the command line arguments to pass along when running the {@code mysqldump} command.
|
||||
*
|
||||
* @param sqlBackupFile the file to back up to
|
||||
* @return the mysqldump command line arguments
|
||||
*/
|
||||
private String buildMysqlDumpArguments(File sqlBackupFile) {
|
||||
String dbUsername = settings.getProperty(DatabaseSettings.MYSQL_USERNAME);
|
||||
String dbPassword = settings.getProperty(DatabaseSettings.MYSQL_PASSWORD);
|
||||
String dbName = settings.getProperty(DatabaseSettings.MYSQL_DATABASE);
|
||||
String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE);
|
||||
|
||||
return " -u " + dbUsername + " -p" + dbPassword + " " + dbName
|
||||
+ " --tables " + tableName + " -r " + sqlBackupFile.getPath() + ".sql";
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the file name to back up the data source to.
|
||||
*
|
||||
* @param fileExtension the file extension to use (e.g. sql)
|
||||
* @return the file to back up the data to
|
||||
*/
|
||||
private File constructBackupFile(String fileExtension) {
|
||||
String dateString = FileUtils.createCurrentTimeString();
|
||||
return new File(backupFolder, "backup" + dateString + "." + fileExtension);
|
||||
}
|
||||
|
||||
private static void copy(File src, File dst) throws IOException {
|
||||
try (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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Possible backup causes.
|
||||
*/
|
||||
public enum BackupCause {
|
||||
START,
|
||||
STOP,
|
||||
COMMAND,
|
||||
OTHER
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
package fr.xephi.authme.service;
|
||||
|
||||
import fr.xephi.authme.AuthMe;
|
||||
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.command.CommandSender;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* 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 boolean useAsyncTasks;
|
||||
|
||||
@Inject
|
||||
BukkitService(AuthMe authMe, Settings settings) {
|
||||
this.authMe = authMe;
|
||||
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 we are currently on a async thread; 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 (Bukkit.isPrimaryThread()) {
|
||||
task.run();
|
||||
} else {
|
||||
scheduleSyncDelayedTask(task);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @throws IllegalStateException if this was already scheduled
|
||||
*/
|
||||
public BukkitTask runTaskTimerAsynchronously(BukkitRunnable task, long delay, long period) {
|
||||
return task.runTaskTimerAsynchronously(authMe, 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
|
||||
*/
|
||||
public BukkitTask runTaskTimer(BukkitRunnable task, long delay, long period) {
|
||||
return task.runTaskTimer(authMe, delay, period);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 the player by the given name, regardless if they are offline or
|
||||
* online.
|
||||
* <p>
|
||||
* This method may involve a blocking web request to get the UUID for the
|
||||
* given name.
|
||||
* <p>
|
||||
* This will return an object even if the player does not exist. To this
|
||||
* method, all players will exist.
|
||||
*
|
||||
* @param name the name the player to retrieve
|
||||
* @return an offline player
|
||||
*/
|
||||
public OfflinePlayer getOfflinePlayer(String name) {
|
||||
return authMe.getServer().getOfflinePlayer(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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a view of all currently online players.
|
||||
*
|
||||
* @return collection of online players
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public Collection<Player> getOnlinePlayers() {
|
||||
return (Collection<Player>) Bukkit.getOnlinePlayers();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event with the provided function and emits it.
|
||||
*
|
||||
* @param eventSupplier the event supplier: function taking a boolean specifying whether AuthMe is configured
|
||||
* in async mode or not
|
||||
* @param <E> the event type
|
||||
* @return the event that was created and emitted
|
||||
*/
|
||||
public <E extends Event> E createAndCallEvent(Function<Boolean, E> eventSupplier) {
|
||||
E event = eventSupplier.apply(useAsyncTasks);
|
||||
callEvent(event);
|
||||
return 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a command on this server, and executes it if found.
|
||||
*
|
||||
* @param sender the apparent sender of the command
|
||||
* @param commandLine the command + arguments. Example: <code>test abc 123</code>
|
||||
* @return returns false if no target is found
|
||||
*/
|
||||
public boolean dispatchCommand(CommandSender sender, String commandLine) {
|
||||
return Bukkit.dispatchCommand(sender, commandLine);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a command to be run as console user on this server, and executes it if found.
|
||||
*
|
||||
* @param commandLine the command + arguments. Example: <code>test abc 123</code>
|
||||
* @return returns false if no target is found
|
||||
*/
|
||||
public boolean dispatchConsoleCommand(String commandLine) {
|
||||
return Bukkit.dispatchCommand(Bukkit.getConsoleSender(), commandLine);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reload(Settings settings) {
|
||||
useAsyncTasks = settings.getProperty(PluginSettings.USE_ASYNC_TASKS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the specified bytes to bungeecord using the specified player connection.
|
||||
*
|
||||
* @param player the player
|
||||
* @param bytes the message
|
||||
*/
|
||||
public void sendBungeeMessage(Player player, byte[] bytes) {
|
||||
player.sendPluginMessage(authMe, "BungeeCord", bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a ban to the 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an optional with a boolean indicating whether bungeecord is enabled or not if the
|
||||
* server implementation is Spigot. Otherwise returns an empty optional.
|
||||
*
|
||||
* @return Optional with configuration value for Spigot, empty optional otherwise
|
||||
*/
|
||||
public Optional<Boolean> isBungeeCordConfiguredForSpigot() {
|
||||
try {
|
||||
YamlConfiguration spigotConfig = Bukkit.spigot().getConfig();
|
||||
return Optional.of(spigotConfig.getBoolean("settings.bungeecord"));
|
||||
} catch (NoSuchMethodError e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the IP string that this server is bound to, otherwise empty string
|
||||
*/
|
||||
public String getIp() {
|
||||
return Bukkit.getServer().getIp();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package fr.xephi.authme.service;
|
||||
|
||||
import ch.jalu.configme.properties.Property;
|
||||
import fr.xephi.authme.message.MessageKey;
|
||||
import fr.xephi.authme.message.Messages;
|
||||
import fr.xephi.authme.permission.PermissionNode;
|
||||
import fr.xephi.authme.permission.PermissionsManager;
|
||||
import fr.xephi.authme.settings.Settings;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
* Service for the most common operations regarding settings, messages and permissions.
|
||||
*/
|
||||
public class CommonService {
|
||||
|
||||
@Inject
|
||||
private Settings settings;
|
||||
|
||||
@Inject
|
||||
private Messages messages;
|
||||
|
||||
@Inject
|
||||
private PermissionsManager permissionsManager;
|
||||
|
||||
CommonService() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a property's value.
|
||||
*
|
||||
* @param property the property to retrieve
|
||||
* @param <T> the property type
|
||||
* @return the property's value
|
||||
*/
|
||||
public <T> T getProperty(Property<T> property) {
|
||||
return settings.getProperty(property);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the command sender.
|
||||
*
|
||||
* @param sender the command sender
|
||||
* @param key the message key
|
||||
*/
|
||||
public void send(CommandSender sender, MessageKey key) {
|
||||
messages.send(sender, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the command sender with the given replacements.
|
||||
*
|
||||
* @param sender the command sender
|
||||
* @param key the message key
|
||||
* @param replacements the replacements to apply to the message
|
||||
*/
|
||||
public void send(CommandSender sender, MessageKey key, String... replacements) {
|
||||
messages.send(sender, key, replacements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a message in one piece.
|
||||
*
|
||||
* @param sender The entity to send the message to
|
||||
* @param key the key of the message
|
||||
* @return the message
|
||||
*/
|
||||
public String retrieveSingleMessage(CommandSender sender, MessageKey key) {
|
||||
return messages.retrieveSingle(sender, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the player has the given permission.
|
||||
*
|
||||
* @param player the player
|
||||
* @param node the permission node to check
|
||||
* @return true if player has permission, false otherwise
|
||||
*/
|
||||
public boolean hasPermission(Player player, PermissionNode node) {
|
||||
return permissionsManager.hasPermission(player, node);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package fr.xephi.authme.service;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.hash.HashFunction;
|
||||
import com.google.common.hash.Hashing;
|
||||
import com.maxmind.db.GeoIp2Provider;
|
||||
import com.maxmind.db.Reader;
|
||||
import com.maxmind.db.Reader.FileMode;
|
||||
import com.maxmind.db.cache.CHMCache;
|
||||
import com.maxmind.db.model.Country;
|
||||
import com.maxmind.db.model.CountryResponse;
|
||||
import fr.xephi.authme.ConsoleLogger;
|
||||
import fr.xephi.authme.initialization.DataFolder;
|
||||
import fr.xephi.authme.output.ConsoleLoggerFactory;
|
||||
import fr.xephi.authme.settings.Settings;
|
||||
import fr.xephi.authme.settings.properties.ProtectionSettings;
|
||||
import fr.xephi.authme.util.FileUtils;
|
||||
import fr.xephi.authme.util.InternetProtocolUtils;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URL;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Base64;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
public class GeoIpService {
|
||||
|
||||
//private static final String LICENSE =
|
||||
//"[LICENSE] This product includes GeoLite2 data created by MaxMind, available at https://www.maxmind.com";
|
||||
|
||||
private static final String DATABASE_NAME = "GeoLite2-Country";
|
||||
private static final String DATABASE_FILE = DATABASE_NAME + ".mmdb";
|
||||
|
||||
|
||||
private final ConsoleLogger logger = ConsoleLoggerFactory.get(GeoIpService.class);
|
||||
private final Path dataFile;
|
||||
|
||||
private GeoIp2Provider databaseReader;
|
||||
private volatile boolean downloading;
|
||||
|
||||
@Inject
|
||||
GeoIpService(@DataFolder File dataFolder){
|
||||
this.dataFile = dataFolder.toPath().resolve(DATABASE_FILE);
|
||||
|
||||
// Fires download of recent data or the initialization of the look up service
|
||||
isDataAvailable();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
GeoIpService(@DataFolder File dataFolder, GeoIp2Provider reader) {
|
||||
this.dataFile = dataFolder.toPath().resolve(DATABASE_FILE);
|
||||
|
||||
this.databaseReader = reader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download (if absent or old) the GeoIpLite data file and then try to load it.
|
||||
*
|
||||
* @return True if the data is available, false otherwise.
|
||||
*/
|
||||
private synchronized boolean isDataAvailable() {
|
||||
if (downloading) {
|
||||
// we are currently downloading the database
|
||||
return false;
|
||||
}
|
||||
|
||||
if (databaseReader != null) {
|
||||
// everything is initialized
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Files.exists(dataFile)) {
|
||||
try {
|
||||
startReading();
|
||||
return true;
|
||||
} catch (IOException ioEx) {
|
||||
logger.logException("Failed to load GeoLiteAPI database", ioEx);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// File is outdated or doesn't exist - let's try to download the data file!
|
||||
// use bukkit's cached threads
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
||||
private void startReading() throws IOException {
|
||||
databaseReader = new Reader(dataFile.toFile(), FileMode.MEMORY, new CHMCache());
|
||||
|
||||
// clear downloading flag, because we now have working reader instance
|
||||
downloading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the archive to the destination file if it's newer than the locally version.
|
||||
*
|
||||
* @param lastModified modification timestamp of the already present file
|
||||
* @param destination save file
|
||||
* @return null if no updates were found, the MD5 hash of the downloaded archive if successful
|
||||
* @throws IOException if failed during downloading and writing to destination file
|
||||
*/
|
||||
|
||||
/**
|
||||
* Downloads the archive to the destination file if it's newer than the locally version.
|
||||
*
|
||||
* @param destination save file
|
||||
* @return null if no updates were found, the MD5 hash of the downloaded archive if successful
|
||||
* @throws IOException if failed during downloading and writing to destination file
|
||||
*/
|
||||
|
||||
/**
|
||||
* Verify if the expected checksum is equal to the checksum of the given file.
|
||||
*
|
||||
* @param function the checksum function like MD5, SHA256 used to generate the checksum from the file
|
||||
* @param file the file we want to calculate the checksum from
|
||||
* @param expectedChecksum the expected checksum
|
||||
* @throws IOException on I/O error reading the file or the checksum verification failed
|
||||
*/
|
||||
|
||||
/**
|
||||
* Extract the database from gzipped data. Existing outputFile will be replaced if it already exists.
|
||||
*
|
||||
* @param inputFile gzipped database input file
|
||||
* @param outputFile destination file for the database
|
||||
* @throws IOException on I/O error reading the archive, or writing the output
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the country code of the given IP address.
|
||||
*
|
||||
* @param ip textual IP address to lookup.
|
||||
* @return two-character ISO 3166-1 alpha code for the country, "LOCALHOST" for local addresses
|
||||
* or "--" if it cannot be fetched.
|
||||
*/
|
||||
public String getCountryCode(String ip) {
|
||||
if (InternetProtocolUtils.isLocalAddress(ip)) {
|
||||
return "LOCALHOST";
|
||||
}
|
||||
return getCountry(ip).map(Country::getIsoCode).orElse("--");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the country name of the given IP address.
|
||||
*
|
||||
* @param ip textual IP address to lookup.
|
||||
* @return The name of the country, "LocalHost" for local addresses, or "N/A" if it cannot be fetched.
|
||||
*/
|
||||
public String getCountryName(String ip) {
|
||||
if (InternetProtocolUtils.isLocalAddress(ip)) {
|
||||
return "LocalHost";
|
||||
}
|
||||
return getCountry(ip).map(Country::getName).orElse("N/A");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the country of the given IP address
|
||||
*
|
||||
* @param ip textual IP address to lookup
|
||||
* @return the wrapped Country model or {@link Optional#empty()} if
|
||||
* <ul>
|
||||
* <li>Database reader isn't initialized</li>
|
||||
* <li>MaxMind has no record about this IP address</li>
|
||||
* <li>IP address is local</li>
|
||||
* <li>Textual representation is not a valid IP address</li>
|
||||
* </ul>
|
||||
*/
|
||||
private Optional<Country> getCountry(String ip) {
|
||||
if (ip == null || ip.isEmpty() || !isDataAvailable()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
InetAddress address = InetAddress.getByName(ip);
|
||||
|
||||
// Reader.getCountry() can be null for unknown addresses
|
||||
return Optional.ofNullable(databaseReader.getCountry(address)).map(CountryResponse::getCountry);
|
||||
} catch (UnknownHostException e) {
|
||||
// Ignore invalid ip addresses
|
||||
// Legacy GEO IP Database returned a unknown country object with Country-Code: '--' and Country-Name: 'N/A'
|
||||
} catch (IOException ioEx) {
|
||||
logger.logException("Cannot lookup country for " + ip + " at GEO IP database", ioEx);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package fr.xephi.authme.service;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import fr.xephi.authme.command.CommandArgumentDescription;
|
||||
import fr.xephi.authme.command.CommandDescription;
|
||||
import fr.xephi.authme.command.CommandInitializer;
|
||||
import fr.xephi.authme.command.help.HelpMessage;
|
||||
import fr.xephi.authme.command.help.HelpMessagesService;
|
||||
import fr.xephi.authme.command.help.HelpSection;
|
||||
import fr.xephi.authme.initialization.DataFolder;
|
||||
import fr.xephi.authme.message.MessagePathHelper;
|
||||
import fr.xephi.authme.permission.DefaultPermission;
|
||||
import fr.xephi.authme.settings.Settings;
|
||||
import fr.xephi.authme.settings.properties.PluginSettings;
|
||||
import org.yaml.snakeyaml.DumperOptions;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Generates the full command structure for the help translation and saves it to the current help file,
|
||||
* preserving already existing entries.
|
||||
*/
|
||||
public class HelpTranslationGenerator {
|
||||
|
||||
@Inject
|
||||
private CommandInitializer commandInitializer;
|
||||
|
||||
@Inject
|
||||
private HelpMessagesService helpMessagesService;
|
||||
|
||||
@Inject
|
||||
private Settings settings;
|
||||
|
||||
@DataFolder
|
||||
@Inject
|
||||
private File dataFolder;
|
||||
|
||||
/**
|
||||
* Updates the help file to contain entries for all commands.
|
||||
*
|
||||
* @return the help file that has been updated
|
||||
* @throws IOException if the help file cannot be written to
|
||||
*/
|
||||
public File updateHelpFile() throws IOException {
|
||||
String languageCode = settings.getProperty(PluginSettings.MESSAGES_LANGUAGE);
|
||||
File helpFile = new File(dataFolder, MessagePathHelper.createHelpMessageFilePath(languageCode));
|
||||
Map<String, Object> helpEntries = generateHelpMessageEntries();
|
||||
|
||||
String helpEntriesYaml = exportToYaml(helpEntries);
|
||||
Files.write(helpFile.toPath(), helpEntriesYaml.getBytes(), StandardOpenOption.TRUNCATE_EXISTING);
|
||||
return helpFile;
|
||||
}
|
||||
|
||||
private static String exportToYaml(Map<String, Object> helpEntries) {
|
||||
DumperOptions options = new DumperOptions();
|
||||
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
|
||||
options.setAllowUnicode(true);
|
||||
return new Yaml(options).dump(helpEntries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates entries for a complete help text file.
|
||||
*
|
||||
* @return help text entries to save
|
||||
*/
|
||||
private Map<String, Object> generateHelpMessageEntries() {
|
||||
Map<String, Object> messageEntries = new LinkedHashMap<>(HelpMessage.values().length);
|
||||
for (HelpMessage message : HelpMessage.values()) {
|
||||
messageEntries.put(message.getEntryKey(), helpMessagesService.getMessage(message));
|
||||
}
|
||||
|
||||
Map<String, String> defaultPermissions = new LinkedHashMap<>();
|
||||
for (DefaultPermission defaultPermission : DefaultPermission.values()) {
|
||||
defaultPermissions.put(HelpMessagesService.getDefaultPermissionsSubPath(defaultPermission),
|
||||
helpMessagesService.getMessage(defaultPermission));
|
||||
}
|
||||
messageEntries.put("defaultPermissions", defaultPermissions);
|
||||
|
||||
Map<String, String> sectionEntries = new LinkedHashMap<>(HelpSection.values().length);
|
||||
for (HelpSection section : HelpSection.values()) {
|
||||
sectionEntries.put(section.getEntryKey(), helpMessagesService.getMessage(section));
|
||||
}
|
||||
|
||||
Map<String, Object> commandEntries = new LinkedHashMap<>();
|
||||
for (CommandDescription command : commandInitializer.getCommands()) {
|
||||
generateCommandEntries(command, commandEntries);
|
||||
}
|
||||
|
||||
return ImmutableMap.of(
|
||||
"common", messageEntries,
|
||||
"section", sectionEntries,
|
||||
"commands", commandEntries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds YAML entries for the provided command its children to the given map.
|
||||
*
|
||||
* @param command the command to process (including its children)
|
||||
* @param commandEntries the map to add the generated entries to
|
||||
*/
|
||||
private void generateCommandEntries(CommandDescription command, Map<String, Object> commandEntries) {
|
||||
CommandDescription translatedCommand = helpMessagesService.buildLocalizedDescription(command);
|
||||
Map<String, Object> commandData = new LinkedHashMap<>();
|
||||
commandData.put("description", translatedCommand.getDescription());
|
||||
commandData.put("detailedDescription", translatedCommand.getDetailedDescription());
|
||||
|
||||
int i = 1;
|
||||
for (CommandArgumentDescription argument : translatedCommand.getArguments()) {
|
||||
Map<String, String> argumentData = new LinkedHashMap<>(2);
|
||||
argumentData.put("label", argument.getName());
|
||||
argumentData.put("description", argument.getDescription());
|
||||
commandData.put("arg" + i, argumentData);
|
||||
++i;
|
||||
}
|
||||
|
||||
commandEntries.put(HelpMessagesService.getCommandSubPath(translatedCommand), commandData);
|
||||
translatedCommand.getChildren().forEach(child -> generateCommandEntries(child, commandEntries));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package fr.xephi.authme.service;
|
||||
|
||||
import fr.xephi.authme.util.StringUtils;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* The JoinMessageService class.
|
||||
*/
|
||||
public class JoinMessageService {
|
||||
|
||||
private BukkitService bukkitService;
|
||||
|
||||
private Map<String, String> joinMessages;
|
||||
|
||||
@Inject
|
||||
JoinMessageService(BukkitService bukkitService) {
|
||||
this.bukkitService = bukkitService;
|
||||
joinMessages = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a join message.
|
||||
*
|
||||
* @param playerName the player name
|
||||
* @param string the join message
|
||||
*/
|
||||
public void putMessage(String playerName, String string) {
|
||||
joinMessages.put(playerName, string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast the join message of the specified player.
|
||||
*
|
||||
* @param playerName the player name
|
||||
*/
|
||||
public void sendMessage(String playerName) {
|
||||
String joinMessage = joinMessages.remove(playerName);
|
||||
if (!StringUtils.isBlank(joinMessage)) {
|
||||
bukkitService.broadcastMessage(joinMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package fr.xephi.authme.service;
|
||||
|
||||
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 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.SecuritySettings;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Migrations to perform during the initialization of AuthMe.
|
||||
*/
|
||||
public final class MigrationService {
|
||||
|
||||
private static ConsoleLogger logger = ConsoleLoggerFactory.get(MigrationService.class);
|
||||
|
||||
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)) {
|
||||
logger.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");
|
||||
logger.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$")) {
|
||||
logger.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();
|
||||
logger.info("Migrated " + allAuths.size() + " accounts from plaintext to SHA256");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package fr.xephi.authme.service;
|
||||
|
||||
import fr.xephi.authme.ConsoleLogger;
|
||||
import fr.xephi.authme.datasource.DataSource;
|
||||
import fr.xephi.authme.initialization.HasCleanup;
|
||||
import fr.xephi.authme.initialization.Reloadable;
|
||||
import fr.xephi.authme.mail.EmailService;
|
||||
import fr.xephi.authme.message.MessageKey;
|
||||
import fr.xephi.authme.message.Messages;
|
||||
import fr.xephi.authme.output.ConsoleLoggerFactory;
|
||||
import fr.xephi.authme.security.PasswordSecurity;
|
||||
import fr.xephi.authme.security.crypts.HashedPassword;
|
||||
import fr.xephi.authme.settings.properties.SecuritySettings;
|
||||
import fr.xephi.authme.util.PlayerUtils;
|
||||
import fr.xephi.authme.util.RandomStringUtils;
|
||||
import fr.xephi.authme.util.expiring.Duration;
|
||||
import fr.xephi.authme.util.expiring.ExpiringMap;
|
||||
import fr.xephi.authme.util.expiring.ExpiringSet;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.inject.Inject;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static fr.xephi.authme.settings.properties.EmailSettings.RECOVERY_PASSWORD_LENGTH;
|
||||
|
||||
/**
|
||||
* Manager for password recovery.
|
||||
*/
|
||||
public class PasswordRecoveryService implements Reloadable, HasCleanup {
|
||||
|
||||
private final ConsoleLogger logger = ConsoleLoggerFactory.get(PasswordRecoveryService.class);
|
||||
|
||||
@Inject
|
||||
private CommonService commonService;
|
||||
|
||||
@Inject
|
||||
private DataSource dataSource;
|
||||
|
||||
@Inject
|
||||
private EmailService emailService;
|
||||
|
||||
@Inject
|
||||
private PasswordSecurity passwordSecurity;
|
||||
|
||||
@Inject
|
||||
private RecoveryCodeService recoveryCodeService;
|
||||
|
||||
@Inject
|
||||
private Messages messages;
|
||||
|
||||
private ExpiringSet<String> emailCooldown;
|
||||
private ExpiringMap<String, String> successfulRecovers;
|
||||
|
||||
@PostConstruct
|
||||
private void initEmailCooldownSet() {
|
||||
emailCooldown = new ExpiringSet<>(
|
||||
commonService.getProperty(SecuritySettings.EMAIL_RECOVERY_COOLDOWN_SECONDS), TimeUnit.SECONDS);
|
||||
successfulRecovers = new ExpiringMap<>(
|
||||
commonService.getProperty(SecuritySettings.PASSWORD_CHANGE_TIMEOUT), TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new recovery code and send it to the player
|
||||
* via email.
|
||||
*
|
||||
* @param player The player getting the code.
|
||||
* @param email The email to send the code to.
|
||||
*/
|
||||
public void createAndSendRecoveryCode(Player player, String email) {
|
||||
if (!checkEmailCooldown(player)) {
|
||||
return;
|
||||
}
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy'年'MM'月'dd'日' HH:mm:ss");
|
||||
Date date = new Date(System.currentTimeMillis());
|
||||
String recoveryCode = recoveryCodeService.generateCode(player.getName());
|
||||
boolean couldSendMail = emailService.sendRecoveryCode(player.getName(), email, recoveryCode, dateFormat.format(date));
|
||||
if (couldSendMail) {
|
||||
commonService.send(player, MessageKey.RECOVERY_CODE_SENT);
|
||||
emailCooldown.add(player.getName().toLowerCase(Locale.ROOT));
|
||||
} else {
|
||||
commonService.send(player, MessageKey.EMAIL_SEND_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new password and send it to the player via
|
||||
* email. This will update the database with the new password.
|
||||
*
|
||||
* @param player The player recovering their password.
|
||||
* @param email The email to send the password to.
|
||||
*/
|
||||
public void generateAndSendNewPassword(Player player, String email) {
|
||||
if (!checkEmailCooldown(player)) {
|
||||
return;
|
||||
}
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy'年'MM'月'dd'日' HH:mm:ss");
|
||||
Date date = new Date(System.currentTimeMillis());
|
||||
|
||||
String name = player.getName();
|
||||
String thePass = RandomStringUtils.generate(commonService.getProperty(RECOVERY_PASSWORD_LENGTH));
|
||||
HashedPassword hashNew = passwordSecurity.computeHash(thePass, name);
|
||||
|
||||
logger.info("Generating new password for '" + name + "'");
|
||||
|
||||
dataSource.updatePassword(name, hashNew);
|
||||
boolean couldSendMail = emailService.sendPasswordMail(name, email, thePass, dateFormat.format(date));
|
||||
if (couldSendMail) {
|
||||
commonService.send(player, MessageKey.RECOVERY_EMAIL_SENT_MESSAGE);
|
||||
emailCooldown.add(player.getName().toLowerCase(Locale.ROOT));
|
||||
} else {
|
||||
commonService.send(player, MessageKey.EMAIL_SEND_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows a player to change their password after
|
||||
* correctly entering a recovery code.
|
||||
*
|
||||
* @param player The player recovering their password.
|
||||
*/
|
||||
public void addSuccessfulRecovery(Player player) {
|
||||
String name = player.getName();
|
||||
String address = PlayerUtils.getPlayerIp(player);
|
||||
|
||||
successfulRecovers.put(name, address);
|
||||
commonService.send(player, MessageKey.RECOVERY_CHANGE_PASSWORD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a player from the list of successful recovers so that he can
|
||||
* no longer use the /email setpassword command.
|
||||
*
|
||||
* @param player The player to remove.
|
||||
*/
|
||||
public void removeFromSuccessfulRecovery(Player player) {
|
||||
successfulRecovers.remove(player.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a player is able to have emails sent.
|
||||
*
|
||||
* @param player The player to check.
|
||||
* @return True if the player is not on cooldown.
|
||||
*/
|
||||
private boolean checkEmailCooldown(Player player) {
|
||||
Duration waitDuration = emailCooldown.getExpiration(player.getName().toLowerCase(Locale.ROOT));
|
||||
if (waitDuration.getDuration() > 0) {
|
||||
String durationText = messages.formatDuration(waitDuration);
|
||||
messages.send(player, MessageKey.EMAIL_COOLDOWN_ERROR, durationText);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a player can change their password after recovery
|
||||
* using the /email setpassword command.
|
||||
*
|
||||
* @param player The player to check.
|
||||
* @return True if the player can change their password.
|
||||
*/
|
||||
public boolean canChangePassword(Player player) {
|
||||
String name = player.getName();
|
||||
String playerAddress = PlayerUtils.getPlayerIp(player);
|
||||
String storedAddress = successfulRecovers.get(name);
|
||||
|
||||
return storedAddress != null && playerAddress.equals(storedAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reload() {
|
||||
emailCooldown.setExpiration(
|
||||
commonService.getProperty(SecuritySettings.EMAIL_RECOVERY_COOLDOWN_SECONDS), TimeUnit.SECONDS);
|
||||
successfulRecovers.setExpiration(
|
||||
commonService.getProperty(SecuritySettings.PASSWORD_CHANGE_TIMEOUT), TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performCleanup() {
|
||||
emailCooldown.removeExpiredEntries();
|
||||
successfulRecovers.removeExpiredEntries();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package fr.xephi.authme.service;
|
||||
|
||||
import ch.jalu.injector.annotations.NoFieldScan;
|
||||
import com.earth2me.essentials.Essentials;
|
||||
import com.onarandombox.MultiverseCore.MultiverseCore;
|
||||
import com.onarandombox.MultiverseCore.api.MVWorldManager;
|
||||
import fr.xephi.authme.ConsoleLogger;
|
||||
import fr.xephi.authme.output.ConsoleLoggerFactory;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.plugin.PluginManager;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Hooks into third-party plugins and allows to perform actions on them.
|
||||
*/
|
||||
@NoFieldScan
|
||||
public class PluginHookService {
|
||||
|
||||
private final ConsoleLogger logger = ConsoleLoggerFactory.get(PluginHookService.class);
|
||||
private final PluginManager pluginManager;
|
||||
private Essentials essentials;
|
||||
private Plugin cmi;
|
||||
private MultiverseCore multiverse;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param pluginManager The server's plugin manager
|
||||
*/
|
||||
@Inject
|
||||
public PluginHookService(PluginManager pluginManager) {
|
||||
this.pluginManager = pluginManager;
|
||||
tryHookToEssentials();
|
||||
tryHookToCmi();
|
||||
tryHookToMultiverse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable the social spy status of the given user if Essentials is available.
|
||||
*
|
||||
* @param player The player to modify
|
||||
* @param socialSpyStatus The social spy status (enabled/disabled) to set
|
||||
*/
|
||||
public void setEssentialsSocialSpyStatus(Player player, boolean socialSpyStatus) {
|
||||
if (essentials != null) {
|
||||
essentials.getUser(player).setSocialSpyEnabled(socialSpyStatus);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If Essentials is hooked into, return Essentials' data folder.
|
||||
*
|
||||
* @return The Essentials data folder, or null if unavailable
|
||||
*/
|
||||
public File getEssentialsDataFolder() {
|
||||
if (essentials != null) {
|
||||
return essentials.getDataFolder();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* If CMI is hooked into, return CMI' data folder.
|
||||
*
|
||||
* @return The CMI data folder, or null if unavailable
|
||||
*/
|
||||
public File getCmiDataFolder() {
|
||||
Plugin plugin = pluginManager.getPlugin("CMI");
|
||||
if (plugin == null) {
|
||||
return null;
|
||||
}
|
||||
return plugin.getDataFolder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the spawn of the given world as defined by Multiverse (if available).
|
||||
*
|
||||
* @param world The world to get the Multiverse spawn for
|
||||
* @return The spawn location from Multiverse, or null if unavailable
|
||||
*/
|
||||
public Location getMultiverseSpawn(World world) {
|
||||
if (multiverse != null) {
|
||||
MVWorldManager manager = multiverse.getMVWorldManager();
|
||||
if (manager.isMVWorld(world)) {
|
||||
return manager.getMVWorld(world).getSpawnLocation();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ------
|
||||
// "Is plugin available" methods
|
||||
// ------
|
||||
|
||||
/**
|
||||
* @return true if we have a hook to Essentials, false otherwise
|
||||
*/
|
||||
public boolean isEssentialsAvailable() {
|
||||
return essentials != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if we have a hook to CMI, false otherwise
|
||||
*/
|
||||
public boolean isCmiAvailable() {
|
||||
return cmi != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if we have a hook to Multiverse, false otherwise
|
||||
*/
|
||||
public boolean isMultiverseAvailable() {
|
||||
return multiverse != null;
|
||||
}
|
||||
|
||||
// ------
|
||||
// Hook methods
|
||||
// ------
|
||||
|
||||
/**
|
||||
* Attempts to create a hook into Essentials.
|
||||
*/
|
||||
public void tryHookToEssentials() {
|
||||
try {
|
||||
essentials = getPlugin(pluginManager, "Essentials", Essentials.class);
|
||||
} catch (Exception | NoClassDefFoundError ignored) {
|
||||
essentials = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to create a hook into CMI.
|
||||
*/
|
||||
public void tryHookToCmi() {
|
||||
try {
|
||||
cmi = getPlugin(pluginManager, "CMI", Plugin.class);
|
||||
} catch (Exception | NoClassDefFoundError ignored) {
|
||||
cmi = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to create a hook into Multiverse.
|
||||
*/
|
||||
public void tryHookToMultiverse() {
|
||||
try {
|
||||
multiverse = getPlugin(pluginManager, "Multiverse-Core", MultiverseCore.class);
|
||||
} catch (Exception | NoClassDefFoundError ignored) {
|
||||
multiverse = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ------
|
||||
// Unhook methods
|
||||
// ------
|
||||
|
||||
/**
|
||||
* Unhooks from Essentials.
|
||||
*/
|
||||
public void unhookEssentials() {
|
||||
essentials = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unhooks from CMI.
|
||||
*/
|
||||
public void unhookCmi() {
|
||||
cmi = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unhooks from Multiverse.
|
||||
*/
|
||||
public void unhookMultiverse() {
|
||||
multiverse = null;
|
||||
}
|
||||
|
||||
// ------
|
||||
// Helpers
|
||||
// ------
|
||||
|
||||
private <T extends Plugin> T getPlugin(PluginManager pluginManager, String name, Class<T> clazz)
|
||||
throws Exception, NoClassDefFoundError {
|
||||
if (pluginManager.isPluginEnabled(name)) {
|
||||
T plugin = clazz.cast(pluginManager.getPlugin(name));
|
||||
logger.info("Hooked successfully into " + name);
|
||||
return plugin;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package fr.xephi.authme.service;
|
||||
|
||||
import fr.xephi.authme.initialization.HasCleanup;
|
||||
import fr.xephi.authme.initialization.SettingsDependent;
|
||||
import fr.xephi.authme.settings.Settings;
|
||||
import fr.xephi.authme.settings.properties.SecuritySettings;
|
||||
import fr.xephi.authme.util.RandomStringUtils;
|
||||
import fr.xephi.authme.util.expiring.ExpiringMap;
|
||||
import fr.xephi.authme.util.expiring.TimedCounter;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Manager for recovery codes.
|
||||
*/
|
||||
public class RecoveryCodeService implements SettingsDependent, HasCleanup {
|
||||
|
||||
private final ExpiringMap<String, String> recoveryCodes;
|
||||
private final TimedCounter<String> playerTries;
|
||||
private int recoveryCodeLength;
|
||||
private int recoveryCodeExpiration;
|
||||
private int recoveryCodeMaxTries;
|
||||
|
||||
@Inject
|
||||
RecoveryCodeService(Settings settings) {
|
||||
recoveryCodeLength = settings.getProperty(SecuritySettings.RECOVERY_CODE_LENGTH);
|
||||
recoveryCodeExpiration = settings.getProperty(SecuritySettings.RECOVERY_CODE_HOURS_VALID);
|
||||
recoveryCodeMaxTries = settings.getProperty(SecuritySettings.RECOVERY_CODE_MAX_TRIES);
|
||||
recoveryCodes = new ExpiringMap<>(recoveryCodeExpiration, TimeUnit.HOURS);
|
||||
playerTries = new TimedCounter<>(recoveryCodeExpiration, TimeUnit.HOURS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether recovery codes are enabled or not
|
||||
*/
|
||||
public boolean isRecoveryCodeNeeded() {
|
||||
return recoveryCodeLength > 0 && recoveryCodeExpiration > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the recovery code for the given player.
|
||||
*
|
||||
* @param player the player to generate a code for
|
||||
* @return the generated code
|
||||
*/
|
||||
public String generateCode(String player) {
|
||||
String code = RandomStringUtils.generateHex(recoveryCodeLength);
|
||||
|
||||
playerTries.put(player, recoveryCodeMaxTries);
|
||||
recoveryCodes.put(player, code);
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the supplied code is valid for the given player.
|
||||
*
|
||||
* @param player the player to check for
|
||||
* @param code the code to check
|
||||
* @return true if the code matches and has not expired, false otherwise
|
||||
*/
|
||||
public boolean isCodeValid(String player, String code) {
|
||||
String storedCode = recoveryCodes.get(player);
|
||||
playerTries.decrement(player);
|
||||
return storedCode != null && storedCode.equals(code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a player has tries remaining to enter a code.
|
||||
*
|
||||
* @param player The player to check for.
|
||||
* @return True if the player has tries left.
|
||||
*/
|
||||
public boolean hasTriesLeft(String player) {
|
||||
return playerTries.get(player) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of attempts a player has to enter a code.
|
||||
*
|
||||
* @param player The player to check for.
|
||||
* @return The number of tries left.
|
||||
*/
|
||||
public int getTriesLeft(String player) {
|
||||
return playerTries.get(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the player's recovery code if present.
|
||||
*
|
||||
* @param player the player
|
||||
*/
|
||||
public void removeCode(String player) {
|
||||
recoveryCodes.remove(player);
|
||||
playerTries.remove(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reload(Settings settings) {
|
||||
recoveryCodeLength = settings.getProperty(SecuritySettings.RECOVERY_CODE_LENGTH);
|
||||
recoveryCodeExpiration = settings.getProperty(SecuritySettings.RECOVERY_CODE_HOURS_VALID);
|
||||
recoveryCodeMaxTries = settings.getProperty(SecuritySettings.RECOVERY_CODE_MAX_TRIES);
|
||||
recoveryCodes.setExpiration(recoveryCodeExpiration, TimeUnit.HOURS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performCleanup() {
|
||||
recoveryCodes.removeExpiredEntries();
|
||||
playerTries.removeExpiredEntries();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package fr.xephi.authme.service;
|
||||
|
||||
import fr.xephi.authme.ConsoleLogger;
|
||||
import fr.xephi.authme.data.auth.PlayerAuth;
|
||||
import fr.xephi.authme.datasource.DataSource;
|
||||
import fr.xephi.authme.events.RestoreSessionEvent;
|
||||
import fr.xephi.authme.initialization.Reloadable;
|
||||
import fr.xephi.authme.message.MessageKey;
|
||||
import fr.xephi.authme.output.ConsoleLoggerFactory;
|
||||
import fr.xephi.authme.settings.properties.PluginSettings;
|
||||
import fr.xephi.authme.util.PlayerUtils;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static fr.xephi.authme.util.Utils.MILLIS_PER_MINUTE;
|
||||
|
||||
/**
|
||||
* Handles the user sessions.
|
||||
*/
|
||||
public class SessionService implements Reloadable {
|
||||
|
||||
private final ConsoleLogger logger = ConsoleLoggerFactory.get(SessionService.class);
|
||||
private final CommonService service;
|
||||
private final BukkitService bukkitService;
|
||||
private final DataSource database;
|
||||
|
||||
private boolean isEnabled;
|
||||
|
||||
@Inject
|
||||
SessionService(CommonService service, BukkitService bukkitService, DataSource database) {
|
||||
this.service = service;
|
||||
this.bukkitService = bukkitService;
|
||||
this.database = database;
|
||||
reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the player has a session he can resume.
|
||||
*
|
||||
* @param player the player to check
|
||||
* @return true if there is a current session, false otherwise
|
||||
*/
|
||||
public boolean canResumeSession(Player player) {
|
||||
final String name = player.getName();
|
||||
if (isEnabled && database.hasSession(name)) {
|
||||
database.setUnlogged(name);
|
||||
database.revokeSession(name);
|
||||
PlayerAuth auth = database.getAuth(name);
|
||||
|
||||
SessionState state = fetchSessionStatus(auth, player);
|
||||
if (state.equals(SessionState.VALID)) {
|
||||
RestoreSessionEvent event = bukkitService.createAndCallEvent(
|
||||
isAsync -> new RestoreSessionEvent(player, isAsync));
|
||||
return !event.isCancelled();
|
||||
} else if (state.equals(SessionState.IP_CHANGED)) {
|
||||
service.send(player, MessageKey.SESSION_EXPIRED);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given Player has a current session by comparing its properties
|
||||
* with the given PlayerAuth's.
|
||||
*
|
||||
* @param auth the player auth
|
||||
* @param player the associated player
|
||||
* @return SessionState based on the state of the session (VALID, NOT_VALID, OUTDATED, IP_CHANGED)
|
||||
*/
|
||||
private SessionState fetchSessionStatus(PlayerAuth auth, Player player) {
|
||||
if (auth == null) {
|
||||
logger.warning("No PlayerAuth in database for '" + player.getName() + "' during session check");
|
||||
return SessionState.NOT_VALID;
|
||||
} else if (auth.getLastLogin() == null) {
|
||||
return SessionState.NOT_VALID;
|
||||
}
|
||||
long timeSinceLastLogin = System.currentTimeMillis() - auth.getLastLogin();
|
||||
|
||||
if (timeSinceLastLogin > 0
|
||||
&& timeSinceLastLogin < service.getProperty(PluginSettings.SESSIONS_TIMEOUT) * MILLIS_PER_MINUTE) {
|
||||
if (PlayerUtils.getPlayerIp(player).equals(auth.getLastIp())) {
|
||||
return SessionState.VALID;
|
||||
} else {
|
||||
return SessionState.IP_CHANGED;
|
||||
}
|
||||
}
|
||||
return SessionState.OUTDATED;
|
||||
}
|
||||
|
||||
public void grantSession(String name) {
|
||||
if (isEnabled) {
|
||||
database.grantSession(name);
|
||||
}
|
||||
}
|
||||
|
||||
public void revokeSession(String name) {
|
||||
database.revokeSession(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reload() {
|
||||
this.isEnabled = service.getProperty(PluginSettings.SESSIONS_ENABLED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package fr.xephi.authme.service;
|
||||
|
||||
public enum SessionState {
|
||||
|
||||
VALID,
|
||||
|
||||
NOT_VALID,
|
||||
|
||||
OUTDATED,
|
||||
|
||||
IP_CHANGED
|
||||
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package fr.xephi.authme.service;
|
||||
|
||||
import fr.xephi.authme.ConsoleLogger;
|
||||
import fr.xephi.authme.data.auth.PlayerAuth;
|
||||
import fr.xephi.authme.data.auth.PlayerCache;
|
||||
import fr.xephi.authme.data.limbo.LimboPlayer;
|
||||
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.output.ConsoleLoggerFactory;
|
||||
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 {
|
||||
|
||||
private final ConsoleLogger logger = ConsoleLoggerFactory.get(TeleportationService.class);
|
||||
|
||||
@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.
|
||||
*
|
||||
* @param player the player to process
|
||||
*/
|
||||
public void teleportOnJoin(final Player player) {
|
||||
if (!settings.getProperty(RestrictionSettings.NO_TELEPORT)
|
||||
&& settings.getProperty(TELEPORT_UNAUTHED_TO_SPAWN)) {
|
||||
logger.debug("Teleport on join for player `{0}`", player.getName());
|
||||
teleportToSpawn(player, playerCache.isAuthenticated(player.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's custom on join location.
|
||||
*
|
||||
* @param player the player to process
|
||||
*
|
||||
* @return the custom spawn location, null if the player should spawn at the original location
|
||||
*/
|
||||
public Location prepareOnJoinSpawnLocation(final Player player) {
|
||||
if (!settings.getProperty(RestrictionSettings.NO_TELEPORT)
|
||||
&& settings.getProperty(TELEPORT_UNAUTHED_TO_SPAWN)) {
|
||||
final Location location = spawnLoader.getSpawnLocation(player);
|
||||
|
||||
SpawnTeleportEvent event = new SpawnTeleportEvent(player, location,
|
||||
playerCache.isAuthenticated(player.getName()));
|
||||
bukkitService.callEvent(event);
|
||||
if (!isEventValid(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.debug("Returning custom location for >1.9 join event for player `{0}`", player.getName());
|
||||
return location;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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())) {
|
||||
logger.debug("Attempting to teleport player `{0}` to first spawn", 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 LimboPlayer object
|
||||
*/
|
||||
public void teleportOnLogin(final Player player, PlayerAuth auth, LimboPlayer limbo) {
|
||||
if (settings.getProperty(RestrictionSettings.NO_TELEPORT)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// #856: If LimboPlayer 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 LimboPlayer is from where the player comes, before any teleportation by AuthMe
|
||||
if (mustForceSpawnAfterLogin(worldName)) {
|
||||
logger.debug("Teleporting `{0}` to spawn because of 'force-spawn after login'", player.getName());
|
||||
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);
|
||||
logger.debug("Teleporting `{0}` after login, based on the player auth", player.getName());
|
||||
teleportBackFromSpawn(player, location);
|
||||
} else if (limbo != null && limbo.getLocation() != null) {
|
||||
logger.debug("Teleporting `{0}` after login, based on the limbo player", player.getName());
|
||||
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(),
|
||||
auth.getYaw(), auth.getPitch());
|
||||
}
|
||||
|
||||
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 no 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.scheduleSyncTaskFromOptionallyAsyncTask(() -> {
|
||||
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,343 @@
|
||||
package fr.xephi.authme.service;
|
||||
|
||||
import ch.jalu.configme.properties.Property;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.HashMultimap;
|
||||
import com.google.common.collect.Multimap;
|
||||
import fr.xephi.authme.ConsoleLogger;
|
||||
import fr.xephi.authme.datasource.DataSource;
|
||||
import fr.xephi.authme.initialization.Reloadable;
|
||||
import fr.xephi.authme.message.MessageKey;
|
||||
import fr.xephi.authme.output.ConsoleLoggerFactory;
|
||||
import fr.xephi.authme.permission.PermissionsManager;
|
||||
import fr.xephi.authme.permission.PlayerStatePermission;
|
||||
import fr.xephi.authme.security.HashUtils;
|
||||
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.PlayerUtils;
|
||||
import fr.xephi.authme.util.Utils;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.inject.Inject;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static fr.xephi.authme.util.StringUtils.isInsideString;
|
||||
|
||||
/**
|
||||
* Validation service.
|
||||
*/
|
||||
public class ValidationService implements Reloadable {
|
||||
|
||||
private final ConsoleLogger logger = ConsoleLoggerFactory.get(ValidationService.class);
|
||||
|
||||
@Inject
|
||||
private Settings settings;
|
||||
@Inject
|
||||
private DataSource dataSource;
|
||||
@Inject
|
||||
private PermissionsManager permissionsManager;
|
||||
@Inject
|
||||
private GeoIpService geoIpService;
|
||||
|
||||
private Pattern passwordRegex;
|
||||
private Pattern emailRegex;
|
||||
private Multimap<String, String> restrictedNames;
|
||||
|
||||
ValidationService() {
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
@Override
|
||||
public void reload() {
|
||||
passwordRegex = Utils.safePatternCompile(settings.getProperty(RestrictionSettings.ALLOWED_PASSWORD_REGEX));
|
||||
restrictedNames = settings.getProperty(RestrictionSettings.ENABLE_RESTRICTED_USERS)
|
||||
? loadNameRestrictions(settings.getProperty(RestrictionSettings.RESTRICTED_USERS))
|
||||
: HashMultimap.create();
|
||||
|
||||
emailRegex = Utils.safePatternCompile(settings.getProperty(RestrictionSettings.ALLOWED_EMAIL_REGEX));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(Locale.ROOT);
|
||||
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);
|
||||
} else if (settings.getProperty(SecuritySettings.HAVE_I_BEEN_PWNED_CHECK)) {
|
||||
HaveIBeenPwnedResults results = validatePasswordHaveIBeenPwned(password);
|
||||
if (results != null
|
||||
&& results.isPwned()
|
||||
&& results.getPwnCount() > settings.getProperty(SecuritySettings.HAVE_I_BEEN_PWNED_LIMIT)) {
|
||||
return new ValidationResult(MessageKey.PASSWORD_PWNED_ERROR, String.valueOf(results.getPwnCount()));
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return emailRegex.matcher(email).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = geoIpService.getCountryCode(hostAddress);
|
||||
boolean isCountryAllowed = validateWhitelistAndBlacklist(countryCode,
|
||||
ProtectionSettings.COUNTRIES_WHITELIST, ProtectionSettings.COUNTRIES_BLACKLIST);
|
||||
logger.debug("Country code `{0}` for `{1}` is allowed: {2}", countryCode, hostAddress, isCountryAllowed);
|
||||
return isCountryAllowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 settings.getProperty(RestrictionSettings.UNRESTRICTED_NAMES).contains(name.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the player meets any name restriction if present (IP/domain-based).
|
||||
*
|
||||
* @param player the player to check
|
||||
* @return true if the player may join, false if the player does not satisfy the name restrictions
|
||||
*/
|
||||
public boolean fulfillsNameRestrictions(Player player) {
|
||||
Collection<String> restrictions = restrictedNames.get(player.getName().toLowerCase(Locale.ROOT));
|
||||
if (Utils.isCollectionEmpty(restrictions)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String ip = PlayerUtils.getPlayerIp(player);
|
||||
String domain = getHostName(player.getAddress());
|
||||
for (String restriction : restrictions) {
|
||||
if (restriction.startsWith("regex:")) {
|
||||
restriction = restriction.replace("regex:", "");
|
||||
} else {
|
||||
restriction = restriction.replace("*", "(.*)");
|
||||
}
|
||||
if (ip.matches(restriction)) {
|
||||
return true;
|
||||
}
|
||||
if (domain.matches(restriction)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected String getHostName(InetSocketAddress inetSocketAddr) {
|
||||
return inetSocketAddr.getHostName();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (!Utils.isCollectionEmpty(whitelistValue)) {
|
||||
return containsIgnoreCase(whitelistValue, value);
|
||||
}
|
||||
List<String> blacklistValue = settings.getProperty(blacklist);
|
||||
return Utils.isCollectionEmpty(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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the configured name restrictions into a Multimap by player name (all-lowercase).
|
||||
*
|
||||
* @param configuredRestrictions the restriction rules to convert to a map
|
||||
* @return map of allowed IPs/domain names by player name
|
||||
*/
|
||||
private Multimap<String, String> loadNameRestrictions(Set<String> configuredRestrictions) {
|
||||
Multimap<String, String> restrictions = HashMultimap.create();
|
||||
for (String restriction : configuredRestrictions) {
|
||||
if (isInsideString(';', restriction)) {
|
||||
String[] data = restriction.split(";");
|
||||
restrictions.put(data[0].toLowerCase(Locale.ROOT), data[1]);
|
||||
} else {
|
||||
logger.warning("Restricted user rule must have a ';' separating name from restriction,"
|
||||
+ " but found: '" + restriction + "'");
|
||||
}
|
||||
}
|
||||
return restrictions;
|
||||
}
|
||||
/**
|
||||
* Check haveibeenpwned.com for the given password.
|
||||
*
|
||||
* @param password password to check for
|
||||
* @return Results of the check
|
||||
*/
|
||||
public HaveIBeenPwnedResults validatePasswordHaveIBeenPwned(String password) {
|
||||
String hash = HashUtils.sha1(password);
|
||||
|
||||
String hashPrefix = hash.substring(0, 5);
|
||||
|
||||
try {
|
||||
String url = String.format("https://api.pwnedpasswords.com/range/%s", hashPrefix);
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
connection.setRequestMethod("GET");
|
||||
connection.setRequestProperty("User-Agent", "AuthMeReloaded");
|
||||
connection.setConnectTimeout(5000);
|
||||
connection.setReadTimeout(5000);
|
||||
connection.setDoInput(true);
|
||||
StringBuilder outStr = new StringBuilder();
|
||||
|
||||
try (DataInputStream input = new DataInputStream(connection.getInputStream())) {
|
||||
for (int c = input.read(); c != -1; c = input.read())
|
||||
outStr.append((char) c);
|
||||
}
|
||||
|
||||
String[] hashes = outStr.toString().split("\n");
|
||||
for (String hashSuffix : hashes) {
|
||||
String[] hashSuffixParts = hashSuffix.trim().split(":");
|
||||
if (hashSuffixParts[0].equalsIgnoreCase(hash.substring(5))) {
|
||||
return new HaveIBeenPwnedResults(true, Integer.parseInt(hashSuffixParts[1]));
|
||||
}
|
||||
}
|
||||
return new HaveIBeenPwnedResults(false, 0);
|
||||
} catch (java.io.IOException e) {
|
||||
logger.warning("验证密码时出现错误,这可能是由于网络问题,如果无法解决,请关闭HaveIBeenPwned检查");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class HaveIBeenPwnedResults {
|
||||
private final boolean isPwned;
|
||||
private final int pwnCount;
|
||||
|
||||
public HaveIBeenPwnedResults(boolean isPwned, int pwnCount) {
|
||||
this.isPwned = isPwned;
|
||||
this.pwnCount = pwnCount;
|
||||
}
|
||||
|
||||
public boolean isPwned() {
|
||||
return isPwned;
|
||||
}
|
||||
|
||||
public int getPwnCount() {
|
||||
return pwnCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package fr.xephi.authme.service.bungeecord;
|
||||
|
||||
import com.google.common.io.ByteArrayDataInput;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import fr.xephi.authme.AuthMe;
|
||||
import fr.xephi.authme.ConsoleLogger;
|
||||
import fr.xephi.authme.data.ProxySessionManager;
|
||||
import fr.xephi.authme.initialization.SettingsDependent;
|
||||
import fr.xephi.authme.output.ConsoleLoggerFactory;
|
||||
import fr.xephi.authme.process.Management;
|
||||
import fr.xephi.authme.service.BukkitService;
|
||||
import fr.xephi.authme.settings.Settings;
|
||||
import fr.xephi.authme.settings.properties.HooksSettings;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.messaging.Messenger;
|
||||
import org.bukkit.plugin.messaging.PluginMessageListener;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Optional;
|
||||
|
||||
public class BungeeReceiver implements PluginMessageListener, SettingsDependent {
|
||||
|
||||
private final ConsoleLogger logger = ConsoleLoggerFactory.get(BungeeReceiver.class);
|
||||
|
||||
private final AuthMe plugin;
|
||||
private final BukkitService bukkitService;
|
||||
private final ProxySessionManager proxySessionManager;
|
||||
private final Management management;
|
||||
|
||||
private boolean isEnabled;
|
||||
|
||||
@Inject
|
||||
BungeeReceiver(AuthMe plugin, BukkitService bukkitService, ProxySessionManager proxySessionManager,
|
||||
Management management, Settings settings) {
|
||||
this.plugin = plugin;
|
||||
this.bukkitService = bukkitService;
|
||||
this.proxySessionManager = proxySessionManager;
|
||||
this.management = management;
|
||||
reload(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reload(Settings settings) {
|
||||
this.isEnabled = settings.getProperty(HooksSettings.BUNGEECORD);
|
||||
if (this.isEnabled) {
|
||||
this.isEnabled = bukkitService.isBungeeCordConfiguredForSpigot().orElse(false);
|
||||
}
|
||||
if (this.isEnabled) {
|
||||
final Messenger messenger = plugin.getServer().getMessenger();
|
||||
if (!messenger.isIncomingChannelRegistered(plugin, "BungeeCord")) {
|
||||
messenger.registerIncomingPluginChannel(plugin, "BungeeCord", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the given data input and attempts to translate it to a message for the "AuthMe.v2.Broadcast" channel.
|
||||
*
|
||||
* @param in the input to handle
|
||||
*/
|
||||
private void handleBroadcast(ByteArrayDataInput in) {
|
||||
// Read data byte array
|
||||
short dataLength = in.readShort();
|
||||
byte[] dataBytes = new byte[dataLength];
|
||||
in.readFully(dataBytes);
|
||||
ByteArrayDataInput dataIn = ByteStreams.newDataInput(dataBytes);
|
||||
|
||||
// Parse type
|
||||
String typeId = dataIn.readUTF();
|
||||
Optional<MessageType> type = MessageType.fromId(typeId);
|
||||
if (!type.isPresent()) {
|
||||
logger.debug("Received unsupported forwarded bungeecord message type! ({0})", typeId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse argument
|
||||
String argument;
|
||||
try {
|
||||
argument = dataIn.readUTF();
|
||||
} catch (IllegalStateException e) {
|
||||
logger.warning("Received invalid forwarded plugin message of type " + type.get().name()
|
||||
+ ": argument is missing!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle type
|
||||
switch (type.get()) {
|
||||
case LOGIN:
|
||||
case LOGOUT:
|
||||
// TODO: unused
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the given data input and attempts to translate it to a message for the "AuthMe.v2" channel.
|
||||
*
|
||||
* @param in the input to handle
|
||||
*/
|
||||
private void handle(ByteArrayDataInput in) {
|
||||
// Parse type
|
||||
String typeId = in.readUTF();
|
||||
Optional<MessageType> type = MessageType.fromId(typeId);
|
||||
if (!type.isPresent()) {
|
||||
logger.debug("Received unsupported bungeecord message type! ({0})", typeId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse argument
|
||||
String argument;
|
||||
try {
|
||||
argument = in.readUTF();
|
||||
} catch (IllegalStateException e) {
|
||||
logger.warning("Received invalid plugin message of type " + type.get().name()
|
||||
+ ": argument is missing!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle type
|
||||
switch (type.get()) {
|
||||
case PERFORM_LOGIN:
|
||||
performLogin(argument);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPluginMessageReceived(String channel, Player player, byte[] data) {
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ByteArrayDataInput in = ByteStreams.newDataInput(data);
|
||||
|
||||
// Check subchannel
|
||||
String subChannel = in.readUTF();
|
||||
if ("AuthMe.v2.Broadcast".equals(subChannel)) {
|
||||
handleBroadcast(in);
|
||||
} else if ("AuthMe.v2".equals(subChannel)) {
|
||||
handle(in);
|
||||
}
|
||||
}
|
||||
|
||||
private void performLogin(String name) {
|
||||
Player player = bukkitService.getPlayerExact(name);
|
||||
if (player != null && player.isOnline()) {
|
||||
management.forceLogin(player, true);
|
||||
logger.info("The user " + player.getName() + " has been automatically logged in, "
|
||||
+ "as requested via plugin messaging.");
|
||||
} else {
|
||||
proxySessionManager.processProxySessionMessage(name);
|
||||
logger.info("The user " + name + " should be automatically logged in, "
|
||||
+ "as requested via plugin messaging but has not been detected, nickname has been"
|
||||
+ " added to autologin queue.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package fr.xephi.authme.service.bungeecord;
|
||||
|
||||
import com.google.common.io.ByteArrayDataOutput;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import fr.xephi.authme.AuthMe;
|
||||
import fr.xephi.authme.ConsoleLogger;
|
||||
import fr.xephi.authme.initialization.SettingsDependent;
|
||||
import fr.xephi.authme.output.ConsoleLoggerFactory;
|
||||
import fr.xephi.authme.service.BukkitService;
|
||||
import fr.xephi.authme.settings.Settings;
|
||||
import fr.xephi.authme.settings.properties.HooksSettings;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.messaging.Messenger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Locale;
|
||||
|
||||
public class BungeeSender implements SettingsDependent {
|
||||
|
||||
private final ConsoleLogger logger = ConsoleLoggerFactory.get(BungeeSender.class);
|
||||
private final AuthMe plugin;
|
||||
private final BukkitService bukkitService;
|
||||
|
||||
private boolean isEnabled;
|
||||
private String destinationServerOnLogin;
|
||||
|
||||
/*
|
||||
* Constructor.
|
||||
*/
|
||||
@Inject
|
||||
BungeeSender(AuthMe plugin, BukkitService bukkitService, Settings settings) {
|
||||
this.plugin = plugin;
|
||||
this.bukkitService = bukkitService;
|
||||
reload(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reload(Settings settings) {
|
||||
this.isEnabled = settings.getProperty(HooksSettings.BUNGEECORD);
|
||||
this.destinationServerOnLogin = settings.getProperty(HooksSettings.BUNGEECORD_SERVER);
|
||||
|
||||
if (this.isEnabled) {
|
||||
Messenger messenger = plugin.getServer().getMessenger();
|
||||
if (!messenger.isOutgoingChannelRegistered(plugin, "BungeeCord")) {
|
||||
messenger.registerOutgoingPluginChannel(plugin, "BungeeCord");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return isEnabled;
|
||||
}
|
||||
|
||||
private void sendBungeecordMessage(Player player, String... data) {
|
||||
ByteArrayDataOutput out = ByteStreams.newDataOutput();
|
||||
for (String element : data) {
|
||||
out.writeUTF(element);
|
||||
}
|
||||
bukkitService.sendBungeeMessage(player, out.toByteArray());
|
||||
}
|
||||
|
||||
private void sendForwardedBungeecordMessage(Player player, String subChannel, String... data) {
|
||||
ByteArrayDataOutput out = ByteStreams.newDataOutput();
|
||||
out.writeUTF("Forward");
|
||||
out.writeUTF("ONLINE");
|
||||
out.writeUTF(subChannel);
|
||||
ByteArrayDataOutput dataOut = ByteStreams.newDataOutput();
|
||||
for (String element : data) {
|
||||
dataOut.writeUTF(element);
|
||||
}
|
||||
byte[] dataBytes = dataOut.toByteArray();
|
||||
out.writeShort(dataBytes.length);
|
||||
out.write(dataBytes);
|
||||
bukkitService.sendBungeeMessage(player, out.toByteArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a player to a specified server. If no server is configured, this will
|
||||
* do nothing.
|
||||
*
|
||||
* @param player The player to send.
|
||||
*/
|
||||
public void connectPlayerOnLogin(Player player) {
|
||||
if (!isEnabled || destinationServerOnLogin.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// Add a small delay, just in case...
|
||||
bukkitService.scheduleSyncDelayedTask(() ->
|
||||
sendBungeecordMessage(player, "Connect", destinationServerOnLogin), 10L);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the AuthMe plugin messaging channel, if enabled.
|
||||
*
|
||||
* @param player The player related to the message
|
||||
* @param type The message type, See {@link MessageType}
|
||||
*/
|
||||
public void sendAuthMeBungeecordMessage(Player player, MessageType type) {
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
if (!plugin.isEnabled()) {
|
||||
logger.debug("Tried to send a " + type + " bungeecord message but the plugin was disabled!");
|
||||
return;
|
||||
}
|
||||
if (type.isBroadcast()) {
|
||||
sendForwardedBungeecordMessage(player, "AuthMe.v2.Broadcast", type.getId(), player.getName().toLowerCase(Locale.ROOT));
|
||||
} else {
|
||||
sendBungeecordMessage(player, "AuthMe.v2", type.getId(), player.getName().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package fr.xephi.authme.service.bungeecord;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public enum MessageType {
|
||||
LOGIN("login", true),
|
||||
LOGOUT("logout", true),
|
||||
PERFORM_LOGIN("perform.login", false);
|
||||
|
||||
private final String id;
|
||||
private final boolean broadcast;
|
||||
|
||||
MessageType(String id, boolean broadcast) {
|
||||
this.id = id;
|
||||
this.broadcast = broadcast;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public boolean isBroadcast() {
|
||||
return broadcast;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the MessageType with the given ID.
|
||||
*
|
||||
* @param id the message type id.
|
||||
*
|
||||
* @return the MessageType with the given id, empty if invalid.
|
||||
*/
|
||||
public static Optional<MessageType> fromId(String id) {
|
||||
for (MessageType current : values()) {
|
||||
if (current.getId().equals(id)) {
|
||||
return Optional.of(current);
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package fr.xephi.authme.service.yaml;
|
||||
|
||||
import ch.jalu.configme.exception.ConfigMeException;
|
||||
import ch.jalu.configme.resource.PropertyReader;
|
||||
import ch.jalu.configme.resource.YamlFileResource;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Creates {@link YamlFileResource} objects.
|
||||
*/
|
||||
public final class YamlFileResourceProvider {
|
||||
|
||||
private YamlFileResourceProvider() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link YamlFileResource} instance for the given file. Wraps SnakeYAML's parse exception
|
||||
* thrown when a reader is created into an AuthMe exception.
|
||||
*
|
||||
* @param file the file to load
|
||||
* @return the generated resource
|
||||
*/
|
||||
public static YamlFileResource loadFromFile(File file) {
|
||||
return new AuthMeYamlFileResource(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension of {@link YamlFileResource} which wraps SnakeYAML's parse exception into a custom
|
||||
* exception when a reader is created.
|
||||
*/
|
||||
private static final class AuthMeYamlFileResource extends YamlFileResource {
|
||||
|
||||
AuthMeYamlFileResource(File file) {
|
||||
super(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PropertyReader createReader() {
|
||||
try {
|
||||
return super.createReader();
|
||||
} catch (ConfigMeException e) {
|
||||
throw new YamlParseException(getFile().getPath(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package fr.xephi.authme.service.yaml;
|
||||
|
||||
import ch.jalu.configme.exception.ConfigMeException;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Exception when a YAML file could not be parsed.
|
||||
*/
|
||||
public class YamlParseException extends RuntimeException {
|
||||
|
||||
private final String file;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param file the file a parsing exception occurred with
|
||||
* @param configMeException the caught exception from ConfigMe
|
||||
*/
|
||||
public YamlParseException(String file, ConfigMeException configMeException) {
|
||||
super(Optional.ofNullable(configMeException.getCause()).orElse(configMeException));
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
public String getFile() {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user