Install Plugin & module-configuartion from Taboolib

This commit is contained in:
MC~蛟龙 2024-07-10 20:22:34 +08:00
parent 72bada0fca
commit 0ceb38e7a3
479 changed files with 47707 additions and 5 deletions

View File

@ -1,2 +1,2 @@
group=fr.xephi
group=fr.xephi.authme
version=5.6.0-FORK-b50

View File

@ -20,6 +20,8 @@ subprojects {
}
dependencies {
implementation(project(":project:module-util"))
implementation(project(":project:module-configuration"))
// Spigot API, https://www.spigotmc.org/
compileOnly("org.spigotmc:spigot-api:1.20.6-R0.1-SNAPSHOT")
// Java Libraries

View File

@ -0,0 +1,5 @@
description = "Fork of the first authentication plugin for the Bukkit API!"
dependencies {
}

View File

@ -0,0 +1,474 @@
package fr.xephi.authme;
import ch.jalu.injector.Injector;
import ch.jalu.injector.InjectorBuilder;
import com.alessiodp.libby.BukkitLibraryManager;
import com.github.Anon8281.universalScheduler.UniversalScheduler;
import com.github.Anon8281.universalScheduler.scheduling.schedulers.TaskScheduler;
import fr.xephi.authme.api.v3.AuthMeApi;
import fr.xephi.authme.command.CommandHandler;
import fr.xephi.authme.command.TabCompleteHandler;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.initialization.DataSourceProvider;
import fr.xephi.authme.initialization.OnShutdownPlayerSaver;
import fr.xephi.authme.initialization.OnStartupTasks;
import fr.xephi.authme.initialization.SettingsProvider;
import fr.xephi.authme.initialization.TaskCloser;
import fr.xephi.authme.listener.AdvancedShulkerFixListener;
import fr.xephi.authme.listener.BedrockAutoLoginListener;
import fr.xephi.authme.listener.BlockListener;
import fr.xephi.authme.listener.DoubleLoginFixListener;
import fr.xephi.authme.listener.EntityListener;
import fr.xephi.authme.listener.LoginLocationFixListener;
import fr.xephi.authme.listener.PlayerListener;
import fr.xephi.authme.listener.PlayerListener111;
import fr.xephi.authme.listener.PlayerListener19;
import fr.xephi.authme.listener.PlayerListener19Spigot;
import fr.xephi.authme.listener.PlayerListenerHigherThan18;
import fr.xephi.authme.listener.PurgeListener;
import fr.xephi.authme.listener.ServerListener;
import fr.xephi.authme.mail.EmailService;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.security.crypts.Sha256;
import fr.xephi.authme.service.BackupService;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.MigrationService;
import fr.xephi.authme.service.bungeecord.BungeeReceiver;
import fr.xephi.authme.service.velocity.VelocityReceiver;
import fr.xephi.authme.service.yaml.YamlParseException;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.SettingsWarner;
import fr.xephi.authme.settings.properties.EmailSettings;
import fr.xephi.authme.settings.properties.HooksSettings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import fr.xephi.authme.task.CleanupTask;
import fr.xephi.authme.task.Updater;
import fr.xephi.authme.task.purge.PurgeService;
import fr.xephi.authme.util.ExceptionUtils;
import org.bukkit.Server;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import javax.inject.Inject;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Objects;
import java.util.function.Consumer;
import static fr.xephi.authme.service.BukkitService.TICKS_PER_MINUTE;
import static fr.xephi.authme.util.Utils.isClassLoaded;
/**
* The AuthMe main class.
*/
public class AuthMe extends JavaPlugin {
// Constants
private static final String PLUGIN_NAME = "AuthMeReloaded";
private static final String LOG_FILENAME = "authme.log";
private static final int CLEANUP_INTERVAL = 5 * TICKS_PER_MINUTE;
// Version and build number values
private static String pluginVersion = "5.7.0-Fork";
private static final String pluginBuild = "b";
private static String pluginBuildNumber = "51";
// Private instances
private EmailService emailService;
private CommandHandler commandHandler;
private static TaskScheduler scheduler;
@Inject
public static Settings settings;
private DataSource database;
private BukkitService bukkitService;
private Injector injector;
private BackupService backupService;
public static ConsoleLogger logger;
/**
* Constructor.
*/
public AuthMe() {
}
/**
* Get the plugin's build
*
* @return The plugin's build
*/
public static String getPluginBuild() {
return pluginBuild;
}
/**
* Get the plugin's name.
*
* @return The plugin's name.
*/
public static String getPluginName() {
return PLUGIN_NAME;
}
/**
* Get the plugin's version.
*
* @return The plugin's version.
*/
public static String getPluginVersion() {
return pluginVersion;
}
/**
* Get the plugin's build number.
*
* @return The plugin's build number.
*/
public static String getPluginBuildNumber() {
return pluginBuildNumber;
}
/**
* Get the scheduler
*/
public static TaskScheduler getScheduler() {
return scheduler;
}
/**
* The library manager
*/
public static BukkitLibraryManager libraryManager;
/**
* Method called when the server enables the plugin.
*/
@Override
public void onEnable() {
// Load the plugin version data from the plugin description file
loadPluginInfo(getDescription().getVersion());
scheduler = UniversalScheduler.getScheduler(this);
libraryManager = new BukkitLibraryManager(this);
// Set the Logger instance and log file path
ConsoleLogger.initialize(getLogger(), new File(getDataFolder(), LOG_FILENAME));
logger = ConsoleLoggerFactory.get(AuthMe.class);
logger.info("You are running an unofficial fork version of AuthMe!");
// Check server version
if (!isClassLoaded("org.spigotmc.event.player.PlayerSpawnLocationEvent")
|| !isClassLoaded("org.bukkit.event.player.PlayerInteractAtEntityEvent")) {
logger.warning("You are running an unsupported server version (" + getServerNameVersionSafe() + "). "
+ "AuthMe requires Spigot 1.8.X or later!");
stopOrUnload();
return;
}
// Prevent running AuthMeBridge due to major exploit issues
if (getServer().getPluginManager().isPluginEnabled("AuthMeBridge")) {
logger.warning("Detected AuthMeBridge, support for it has been dropped as it was "
+ "causing exploit issues, please use AuthMeBungee instead! Aborting!");
stopOrUnload();
return;
}
// Initialize the plugin
try {
initialize();
} catch (Throwable th) {
YamlParseException yamlParseException = ExceptionUtils.findThrowableInCause(YamlParseException.class, th);
if (yamlParseException == null) {
logger.logException("Aborting initialization of AuthMe:", th);
th.printStackTrace();
} else {
logger.logException("File '" + yamlParseException.getFile() + "' contains invalid YAML. "
+ "Please run its contents through http://yamllint.com", yamlParseException);
}
stopOrUnload();
return;
}
// Show settings warnings
injector.getSingleton(SettingsWarner.class).logWarningsForMisconfigurations();
// Schedule clean up task
CleanupTask cleanupTask = injector.getSingleton(CleanupTask.class);
cleanupTask.runTaskTimerAsynchronously(this, CLEANUP_INTERVAL, CLEANUP_INTERVAL);
// Do a backup on start
backupService.doBackup(BackupService.BackupCause.START);
// Set up Metrics
OnStartupTasks.sendMetrics(this, settings);
if (settings.getProperty(SecuritySettings.SHOW_STARTUP_BANNER)) {
logger.info("\n" + " ___ __ __ __ ___ \n" +
" / | __ __/ /_/ /_ / |/ /__ \n" +
" / /| |/ / / / __/ __ \\/ /|_/ / _ \\\n" +
" / ___ / /_/ / /_/ / / / / / / __/\n" +
"/_/ |_\\__,_/\\__/_/ /_/_/ /_/\\___/ \n" +
" ");
}
//detect server brand with classloader
checkServerType();
try {
Objects.requireNonNull(getCommand("register")).setTabCompleter(new TabCompleteHandler());
Objects.requireNonNull(getCommand("login")).setTabCompleter(new TabCompleteHandler());
} catch (NullPointerException ignored) {
}
logger.info("AuthMeReReloaded is enabled successfully!");
// Purge on start if enabled
PurgeService purgeService = injector.getSingleton(PurgeService.class);
purgeService.runAutoPurge();
logger.info("GitHub: https://github.com/HaHaWTH/AuthMeReReloaded/");
if (settings.getProperty(SecuritySettings.CHECK_FOR_UPDATES)) {
checkForUpdates();
}
}
/**
* Load the version and build number of the plugin from the description file.
*
* @param versionRaw the version as given by the plugin description file
*/
private static void loadPluginInfo(String versionRaw) {
int index = versionRaw.lastIndexOf("-");
if (index != -1) {
pluginVersion = versionRaw.substring(0, index);
pluginBuildNumber = versionRaw.substring(index + 1);
if (pluginBuildNumber.startsWith("b")) {
pluginBuildNumber = pluginBuildNumber.substring(1);
}
}
}
/**
* Initialize the plugin and all the services.
*/
private void initialize() {
// Create plugin folder
getDataFolder().mkdir();
// Create injector, provide elements from the Bukkit environment and register providers
injector = new InjectorBuilder()
.addDefaultHandlers("fr.xephi.authme")
.create();
injector.register(AuthMe.class, this);
injector.register(Server.class, getServer());
injector.register(PluginManager.class, getServer().getPluginManager());
injector.provide(DataFolder.class, getDataFolder());
injector.registerProvider(Settings.class, SettingsProvider.class);
injector.registerProvider(DataSource.class, DataSourceProvider.class);
// Get settings and set up logger
settings = injector.getSingleton(Settings.class);
ConsoleLoggerFactory.reloadSettings(settings);
OnStartupTasks.setupConsoleFilter(getLogger());
// Set all service fields on the AuthMe class
instantiateServices(injector);
// Convert deprecated PLAINTEXT hash entries
MigrationService.changePlainTextToSha256(settings, database, new Sha256());
// If the server is empty (fresh start) just set all the players as unlogged
if (bukkitService.getOnlinePlayers().isEmpty()) {
database.purgeLogged();
}
// Register event listeners
registerEventListeners(injector);
// Start Email recall task if needed
OnStartupTasks onStartupTasks = injector.newInstance(OnStartupTasks.class);
onStartupTasks.scheduleRecallEmailTask();
}
/**
* Instantiates all services.
*
* @param injector the injector
*/
void instantiateServices(Injector injector) {
database = injector.getSingleton(DataSource.class);
bukkitService = injector.getSingleton(BukkitService.class);
commandHandler = injector.getSingleton(CommandHandler.class);
emailService = injector.getSingleton(EmailService.class);
backupService = injector.getSingleton(BackupService.class);
// Trigger instantiation (class not used elsewhere)
injector.getSingleton(BungeeReceiver.class);
injector.getSingleton(VelocityReceiver.class);
// Trigger construction of API classes; they will keep track of the singleton
injector.getSingleton(AuthMeApi.class);
}
/**
* Registers all event listeners.
*
* @param injector the injector
*/
void registerEventListeners(Injector injector) {
// Get the plugin manager instance
PluginManager pluginManager = getServer().getPluginManager();
// Register event listeners
pluginManager.registerEvents(injector.getSingleton(PlayerListener.class), this);
pluginManager.registerEvents(injector.getSingleton(BlockListener.class), this);
pluginManager.registerEvents(injector.getSingleton(EntityListener.class), this);
pluginManager.registerEvents(injector.getSingleton(ServerListener.class), this);
// Try to register 1.8+ player listeners
if (isClassLoaded("org.bukkit.event.entity.EntityPickupItemEvent") && isClassLoaded("org.bukkit.event.player.PlayerSwapHandItemsEvent")) {
pluginManager.registerEvents(injector.getSingleton(PlayerListenerHigherThan18.class), this);
} else if (isClassLoaded("org.bukkit.event.player.PlayerSwapHandItemsEvent")) {
pluginManager.registerEvents(injector.getSingleton(PlayerListener19.class), this);
}
// Try to register 1.9 player listeners(Moved to else-if)
// if (isClassLoaded("org.bukkit.event.player.PlayerSwapHandItemsEvent")) {
// pluginManager.registerEvents(injector.getSingleton(PlayerListener19.class), this);
// }
// Try to register 1.9 spigot player listeners
if (isClassLoaded("org.spigotmc.event.player.PlayerSpawnLocationEvent")) {
pluginManager.registerEvents(injector.getSingleton(PlayerListener19Spigot.class), this);
}
// Register listener for 1.11 events if available
if (isClassLoaded("org.bukkit.event.entity.EntityAirChangeEvent")) {
pluginManager.registerEvents(injector.getSingleton(PlayerListener111.class), this);
}
//Register 3rd party listeners
if (settings.getProperty(SecuritySettings.FORCE_LOGIN_BEDROCK) && settings.getProperty(HooksSettings.HOOK_FLOODGATE_PLAYER) && getServer().getPluginManager().getPlugin("floodgate") != null) {
pluginManager.registerEvents(injector.getSingleton(BedrockAutoLoginListener.class), this);
} else if (settings.getProperty(SecuritySettings.FORCE_LOGIN_BEDROCK) && (!settings.getProperty(HooksSettings.HOOK_FLOODGATE_PLAYER) || getServer().getPluginManager().getPlugin("floodgate") == null)) {
logger.warning("Failed to enable BedrockAutoLogin, ensure hookFloodgate: true and floodgate is loaded.");
}
if (settings.getProperty(SecuritySettings.LOGIN_LOC_FIX_SUB_UNDERGROUND) || settings.getProperty(SecuritySettings.LOGIN_LOC_FIX_SUB_PORTAL)) {
pluginManager.registerEvents(injector.getSingleton(LoginLocationFixListener.class), this);
}
if (settings.getProperty(SecuritySettings.ANTI_GHOST_PLAYERS)) {
pluginManager.registerEvents(injector.getSingleton(DoubleLoginFixListener.class), this);
}
if (settings.getProperty(SecuritySettings.ADVANCED_SHULKER_FIX) && !isClassLoaded("org.bukkit.event.player.PlayerCommandSendEvent")) {
pluginManager.registerEvents(injector.getSingleton(AdvancedShulkerFixListener.class), this);
} else if (settings.getProperty(SecuritySettings.ADVANCED_SHULKER_FIX) && isClassLoaded("org.bukkit.event.player.PlayerCommandSendEvent")) {
logger.warning("You are running an 1.13+ minecraft server, AdvancedShulkerFix won't enable.");
}
if (settings.getProperty(SecuritySettings.PURGE_DATA_ON_QUIT)) {
pluginManager.registerEvents(injector.getSingleton(PurgeListener.class), this);
}
}
/**
* Stops the server or disables the plugin, as defined in the configuration.
*/
public void stopOrUnload() {
if (settings == null || settings.getProperty(SecuritySettings.STOP_SERVER_ON_PROBLEM)) {
getLogger().warning("THE SERVER IS GOING TO SHUT DOWN AS DEFINED IN THE CONFIGURATION!");
setEnabled(false);
getServer().shutdown();
} else {
setEnabled(false);
}
}
@Override
public void onDisable() {
// onDisable is also called when we prematurely abort, so any field may be null
OnShutdownPlayerSaver onShutdownPlayerSaver = injector == null
? null
: injector.createIfHasDependencies(OnShutdownPlayerSaver.class);
if (onShutdownPlayerSaver != null) {
onShutdownPlayerSaver.saveAllPlayers();
}
if (settings != null && settings.getProperty(EmailSettings.SHUTDOWN_MAIL)) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy'.'MM'.'dd'.' HH:mm:ss");
Date date = new Date(System.currentTimeMillis());
emailService.sendShutDown(settings.getProperty(EmailSettings.SHUTDOWN_MAIL_ADDRESS),dateFormat.format(date));
}
// Do backup on stop if enabled
if (backupService != null) {
backupService.doBackup(BackupService.BackupCause.STOP);
}
// Wait for tasks and close data source
new TaskCloser(database).run();
// Disabled correctly
Consumer<String> infoLogMethod = logger == null ? getLogger()::info : logger::info;
infoLogMethod.accept("AuthMe " + this.getDescription().getVersion() + " is unloaded successfully!");
ConsoleLogger.closeFileWriter();
}
private void checkForUpdates() {
logger.info("Checking for updates...");
Updater updater = new Updater(pluginBuild + pluginBuildNumber);
bukkitService.runTaskAsynchronously(() -> {
if (updater.isUpdateAvailable()) {
String message = "New version available! Latest:" + updater.getLatestVersion() + " Current:" + pluginBuild + pluginBuildNumber;
logger.warning(message);
logger.warning("Download from here: https://modrinth.com/plugin/authmerereloaded");
} else {
logger.info("You are running the latest version.");
}
});
}
private void checkServerType() {
if (isClassLoaded("io.papermc.paper.threadedregions.RegionizedServer")) {
logger.info("AuthMeReReloaded is running on Folia");
} else if (isClassLoaded("com.destroystokyo.paper.PaperConfig")) {
logger.info("AuthMeReReloaded is running on Paper");
} else if (isClassLoaded("catserver.server.CatServerConfig")) {
logger.info("AuthMeReReloaded is running on CatServer");
} else if (isClassLoaded("org.spigotmc.SpigotConfig")) {
logger.info("AuthMeReReloaded is running on Spigot");
} else if (isClassLoaded("org.bukkit.craftbukkit.CraftServer")) {
logger.info("AuthMeReReloaded is running on Bukkit");
} else {
logger.info("AuthMeReReloaded is running on Unknown*");
}
}
/**
* Handle Bukkit commands.
*
* @param sender The command sender (Bukkit).
* @param cmd The command (Bukkit).
* @param commandLabel The command label (Bukkit).
* @param args The command arguments (Bukkit).
* @return True if the command was executed, false otherwise.
*/
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command cmd,
@NotNull String commandLabel, String[] args) {
// Make sure the command handler has been initialized
if (commandHandler == null) {
getLogger().severe("AuthMe command handler is not available");
return false;
}
// Handle the command
return commandHandler.processCommand(sender, commandLabel, args);
}
private String getServerNameVersionSafe() {
try {
Server server = getServer();
return server.getName() + " v. " + server.getVersion();
} catch (Throwable ignore) {
return "-";
}
}
}

View File

@ -0,0 +1,287 @@
package fr.xephi.authme;
import com.google.common.base.Throwables;
import fr.xephi.authme.output.LogLevel;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.PluginSettings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import fr.xephi.authme.util.ExceptionUtils;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* AuthMe logger.
*/
public final class ConsoleLogger {
private static final String NEW_LINE = System.getProperty("line.separator");
/** Formatter which formats dates to something like "[08-16 21:18:46]" for any given LocalDateTime. */
private static final DateTimeFormatter DATE_FORMAT = new DateTimeFormatterBuilder()
.appendLiteral('[')
.appendPattern("MM-dd HH:mm:ss")
.appendLiteral(']')
.toFormatter();
// Outside references
private static File logFile;
private static Logger logger;
// Shared state
private static OutputStreamWriter fileWriter;
// Individual state
private final String name;
private LogLevel logLevel = LogLevel.INFO;
/**
* Constructor.
*
* @param name the name of this logger (the fully qualified class name using it)
*/
public ConsoleLogger(String name) {
this.name = name;
}
// --------
// Configurations
// --------
public static void initialize(Logger logger, File logFile) {
ConsoleLogger.logger = logger;
ConsoleLogger.logFile = logFile;
}
/**
* Sets logging settings which are shared by all logger instances.
*
* @param settings the settings to read from
*/
public static void initializeSharedSettings(Settings settings) {
boolean useLogging = settings.getProperty(SecuritySettings.USE_LOGGING);
if (useLogging) {
initializeFileWriter();
} else {
closeFileWriter();
}
}
/**
* Sets logging settings which are individual to all loggers.
*
* @param settings the settings to read from
*/
public void initializeSettings(Settings settings) {
this.logLevel = settings.getProperty(PluginSettings.LOG_LEVEL);
}
public LogLevel getLogLevel() {
return logLevel;
}
public String getName() {
return name;
}
// --------
// Logging methods
// --------
/**
* Log a WARN message.
*
* @param message The message to log
*/
public void warning(String message) {
logger.warning(message);
writeLog("[WARN] " + message);
}
/**
* Log a Throwable with the provided message on WARNING level
* and save the stack trace to the log file.
*
* @param message The message to accompany the exception
* @param th The Throwable to log
*/
public void logException(String message, Throwable th) {
warning(message + " " + ExceptionUtils.formatException(th));
writeLog(Throwables.getStackTraceAsString(th));
}
/**
* Log an INFO message.
*
* @param message The message to log
*/
public void info(String message) {
logger.info(message);
writeLog("[INFO] " + message);
}
/**
* Log a FINE message if enabled.
* <p>
* Implementation note: this logs a message on INFO level because
* levels below INFO are disabled by Bukkit/Spigot.
*
* @param message The message to log
*/
public void fine(String message) {
if (logLevel.includes(LogLevel.FINE)) {
logger.info(message);
writeLog("[INFO:FINE] " + message);
}
}
// --------
// Debug log methods
// --------
/**
* Log a DEBUG message if enabled.
* <p>
* Implementation note: this logs a message on INFO level and prefixes it with "DEBUG" because
* levels below INFO are disabled by Bukkit/Spigot.
*
* @param message The message to log
*/
public void debug(String message) {
if (logLevel.includes(LogLevel.DEBUG)) {
logAndWriteWithDebugPrefix(message);
}
}
/**
* Log the DEBUG message.
*
* @param message the message
* @param param1 parameter to replace in the message
*/
// Avoids array creation if DEBUG level is disabled
public void debug(String message, Object param1) {
if (logLevel.includes(LogLevel.DEBUG)) {
debug(message, new Object[]{param1});
}
}
/**
* Log the DEBUG message.
*
* @param message the message
* @param param1 first param to replace in message
* @param param2 second param to replace in message
*/
// Avoids array creation if DEBUG level is disabled
public void debug(String message, Object param1, Object param2) {
if (logLevel.includes(LogLevel.DEBUG)) {
debug(message, new Object[]{param1, param2});
}
}
/**
* Log the DEBUG message.
*
* @param message the message
* @param params the params to replace in the message
*/
public void debug(String message, Object... params) {
if (logLevel.includes(LogLevel.DEBUG)) {
logAndWriteWithDebugPrefix(MessageFormat.format(message, params));
}
}
/**
* Log the DEBUG message from the supplier if enabled.
*
* @param msgSupplier the message supplier
*/
public void debug(Supplier<String> msgSupplier) {
if (logLevel.includes(LogLevel.DEBUG)) {
logAndWriteWithDebugPrefix(msgSupplier.get());
}
}
private void logAndWriteWithDebugPrefix(String message) {
String debugMessage = "[INFO:DEBUG] " + message;
logger.info(debugMessage);
writeLog(debugMessage);
}
// --------
// Helpers
// --------
/**
* Closes the file writer.
*/
public static void closeFileWriter() {
if (fileWriter != null) {
try {
fileWriter.flush();
} catch (IOException ignored) {
} finally {
closeSafely(fileWriter);
fileWriter = null;
}
}
}
/**
* Write a message into the log file with a TimeStamp if enabled.
*
* @param message The message to write to the log
*/
private static void writeLog(String message) {
if (fileWriter != null) {
String dateTime = DATE_FORMAT.format(LocalDateTime.now());
try {
fileWriter.write(dateTime);
fileWriter.write(": ");
fileWriter.write(message);
fileWriter.write(NEW_LINE);
fileWriter.flush();
} catch (IOException ignored) {
}
}
}
private static void closeSafely(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
logger.log(Level.SEVERE, "Failed to close resource", e);
}
}
}
/**
* Populates the {@link #fileWriter} field if it is null, handling any exceptions that might
* arise during its creation.
*/
private static void initializeFileWriter() {
if (fileWriter == null) {
FileOutputStream fos = null;
try {
fos = new FileOutputStream(logFile, true);
fileWriter = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
} catch (Exception e) {
closeSafely(fos);
logger.log(Level.SEVERE, "Failed to create writer to AuthMe log file", e);
}
}
}
}

View File

@ -0,0 +1,397 @@
package fr.xephi.authme.api.v3;
import fr.xephi.authme.AuthMe;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.process.Management;
import fr.xephi.authme.process.register.executors.ApiPasswordRegisterParams;
import fr.xephi.authme.process.register.executors.RegistrationMethod;
import fr.xephi.authme.security.PasswordSecurity;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.service.GeoIpService;
import fr.xephi.authme.service.ValidationService;
import fr.xephi.authme.util.PlayerUtils;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryView;
import javax.inject.Inject;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import static fr.xephi.authme.listener.PlayerListener.PENDING_INVENTORIES;
/**
* The current API of AuthMe.
*
* Recommended method of retrieving the AuthMeApi object:
* <code>
* AuthMeApi authmeApi = AuthMeApi.getInstance();
* </code>
*/
@SuppressWarnings("unused")
public class AuthMeApi {
private static AuthMeApi singleton;
private final AuthMe plugin;
private final DataSource dataSource;
private final PasswordSecurity passwordSecurity;
private final Management management;
private final ValidationService validationService;
private final PlayerCache playerCache;
private final GeoIpService geoIpService;
/*
* Constructor for AuthMeApi.
*/
@Inject
AuthMeApi(AuthMe plugin, DataSource dataSource, PlayerCache playerCache, PasswordSecurity passwordSecurity,
Management management, ValidationService validationService, GeoIpService geoIpService) {
this.plugin = plugin;
this.dataSource = dataSource;
this.passwordSecurity = passwordSecurity;
this.management = management;
this.validationService = validationService;
this.playerCache = playerCache;
this.geoIpService = geoIpService;
AuthMeApi.singleton = this;
}
/**
* Get the AuthMeApi object for AuthMe.
*
* @return The AuthMeApi object, or null if the AuthMe plugin is not enabled or not fully initialized yet
*/
public static AuthMeApi getInstance() {
return singleton;
}
/**
* Return the plugin instance.
*
* @return The AuthMe instance
*/
public AuthMe getPlugin() {
return plugin;
}
/**
* Gather the version number of the plugin.
* This can be used to determine whether certain AuthMeApi features are available or not.
*
* @return Plugin version identifier as a string.
*/
public String getPluginVersion() {
return AuthMe.getPluginVersion();
}
/**
* Return whether the given player is authenticated.
*
* @param player The player to verify
* @return true if the player is authenticated
*/
public boolean isAuthenticated(Player player) {
return playerCache.isAuthenticated(player.getName());
}
/**
* Check whether the given player is an NPC.
*
* @param player The player to verify
* @return true if the player is an npc
*/
public boolean isNpc(Player player) {
return PlayerUtils.isNpc(player);
}
/**
* Check whether the given player is unrestricted. For such players, AuthMe will not require
* them to authenticate.
*
* @param player The player to verify
* @return true if the player is unrestricted
* @see fr.xephi.authme.settings.properties.RestrictionSettings#UNRESTRICTED_NAMES
*/
public boolean isUnrestricted(Player player) {
return validationService.isUnrestricted(player.getName());
}
/**
* Get the last location of an online player.
*
* @param player The player to process
* @return The location of the player
*/
public Location getLastLocation(Player player) {
PlayerAuth auth = playerCache.getAuth(player.getName());
if (auth != null) {
return new Location(Bukkit.getWorld(auth.getWorld()),
auth.getQuitLocX(), auth.getQuitLocY(), auth.getQuitLocZ(), auth.getYaw(), auth.getPitch());
}
return null;
}
/**
* Returns the AuthMe info of the given player's name, or empty optional if the player doesn't exist.
*
* @param playerName The player name to look up
* @return AuthMe player info, or empty optional if the player doesn't exist
*/
public Optional<AuthMePlayer> getPlayerInfo(String playerName) {
PlayerAuth auth = playerCache.getAuth(playerName);
if (auth == null) {
auth = dataSource.getAuth(playerName);
}
return AuthMePlayerImpl.fromPlayerAuth(auth);
}
/**
* Get the last ip address of a player.
*
* @param playerName The name of the player to process
* @return The last ip address of the player
*/
public String getLastIp(String playerName) {
PlayerAuth auth = playerCache.getAuth(playerName);
if (auth == null) {
auth = dataSource.getAuth(playerName);
}
if (auth != null) {
return auth.getLastIp();
}
return null;
}
/**
* Get user names by ip.
*
* @param address The ip address to process
* @return The list of user names related to the ip address
*/
public List<String> getNamesByIp(String address) {
return dataSource.getAllAuthsByIp(address);
}
/**
* Get the last (AuthMe) login date of a player.
*
* @param playerName The name of the player to process
* @return The date of the last login, or null if the player doesn't exist or has never logged in
* @deprecated Use Java 8's Instant method {@link #getLastLoginTime(String)}
*/
@Deprecated
public Date getLastLogin(String playerName) {
Long lastLogin = getLastLoginMillis(playerName);
return lastLogin == null ? null : new Date(lastLogin);
}
/**
* Get the last (AuthMe) login timestamp of a player.
*
* @param playerName The name of the player to process
*
* @return The timestamp of the last login, or null if the player doesn't exist or has never logged in
*/
public Instant getLastLoginTime(String playerName) {
Long lastLogin = getLastLoginMillis(playerName);
return lastLogin == null ? null : Instant.ofEpochMilli(lastLogin);
}
private Long getLastLoginMillis(String playerName) {
PlayerAuth auth = playerCache.getAuth(playerName);
if (auth == null) {
auth = dataSource.getAuth(playerName);
}
if (auth != null) {
return auth.getLastLogin();
}
return null;
}
/**
* Return whether the player is registered.
*
* @param playerName The player name to check
* @return true if player is registered, false otherwise
*/
public boolean isRegistered(String playerName) {
String player = playerName.toLowerCase(Locale.ROOT);
return dataSource.isAuthAvailable(player);
}
/**
* Check the password for the given player.
*
* @param playerName The player to check the password for
* @param passwordToCheck The password to check
* @return true if the password is correct, false otherwise
*/
public boolean checkPassword(String playerName, String passwordToCheck) {
return passwordSecurity.comparePassword(passwordToCheck, playerName);
}
/**
* Register an OFFLINE/ONLINE player with the given password.
*
* @param playerName The player to register
* @param password The password to register the player with
*
* @return true if the player was registered successfully
*/
public boolean registerPlayer(String playerName, String password) {
String name = playerName.toLowerCase(Locale.ROOT);
if (isRegistered(name)) {
return false;
}
HashedPassword result = passwordSecurity.computeHash(password, name);
PlayerAuth auth = PlayerAuth.builder()
.name(name)
.password(result)
.realName(playerName)
.registrationDate(System.currentTimeMillis())
.build();
return dataSource.saveAuth(auth);
}
/**
* Open an inventory for the given player at any time.
*
* @param player The player to open the inventory for
* @param inventory The inventory to open
* @return The inventory view
*/
public InventoryView openInventory(Player player, Inventory inventory) {
PENDING_INVENTORIES.add(inventory);
return player.openInventory(inventory);
}
/**
* Force a player to login, i.e. the player is logged in without needing his password.
*
* @param player The player to log in
*/
public void forceLogin(Player player) {
management.forceLogin(player);
}
/**
* Force a player to login, i.e. the player is logged in without needing his password.
*
* @param player The player to log in
* @param quiet Whether to suppress the login message
*/
public void forceLogin(Player player, boolean quiet) {
management.forceLogin(player, quiet);
}
/**
* Force a player to logout.
*
* @param player The player to log out
*/
public void forceLogout(Player player) {
management.performLogout(player);
}
/**
* Force an ONLINE player to register.
*
* @param player The player to register
* @param password The password to use
* @param autoLogin Should the player be authenticated automatically after the registration?
*/
public void forceRegister(Player player, String password, boolean autoLogin) {
management.performRegister(RegistrationMethod.API_REGISTRATION,
ApiPasswordRegisterParams.of(player, password, autoLogin));
}
/**
* Register an ONLINE player with the given password.
*
* @param player The player to register
* @param password The password to use
*/
public void forceRegister(Player player, String password) {
forceRegister(player, password, true);
}
/**
* Unregister a player from AuthMe.
*
* @param player The player to unregister
*/
public void forceUnregister(Player player) {
management.performUnregisterByAdmin(null, player.getName(), player);
}
/**
* Unregister a player from AuthMe by name.
*
* @param name the name of the player (case-insensitive)
*/
public void forceUnregister(String name) {
management.performUnregisterByAdmin(null, name, Bukkit.getPlayer(name));
}
/**
* Change a user's password
*
* @param name the user name
* @param newPassword the new password
*/
public void changePassword(String name, String newPassword) {
management.performPasswordChangeAsAdmin(null, name, newPassword);
}
/**
* Get all the registered names (lowercase)
*
* @return registered names
*/
public List<String> getRegisteredNames() {
List<String> registeredNames = new ArrayList<>();
dataSource.getAllAuths().forEach(auth -> registeredNames.add(auth.getNickname()));
return registeredNames;
}
/**
* Get all the registered real-names (original case)
*
* @return registered real-names
*/
public List<String> getRegisteredRealNames() {
List<String> registeredNames = new ArrayList<>();
dataSource.getAllAuths().forEach(auth -> registeredNames.add(auth.getRealName()));
return registeredNames;
}
/**
* 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.
*/
public String getCountryCode(String ip) {
return geoIpService.getCountryCode(ip);
}
/**
* Get the country name of the given IP address.
*
* @param ip textual IP address to lookup.
*
* @return The name of the country.
*/
public String getCountryName(String ip) {
return geoIpService.getCountryName(ip);
}
}

View File

@ -0,0 +1,65 @@
package fr.xephi.authme.api.v3;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
/**
* Read-only player info exposed in the AuthMe API. The data in this object is copied from the
* database and not updated afterwards. As such, it may become outdated if the player data changes
* in AuthMe.
*
* @see AuthMeApi#getPlayerInfo
*/
public interface AuthMePlayer {
/**
* @return the case-sensitive name of the player, e.g. "thePlayer3030" - never null
*/
String getName();
/**
* Returns the UUID of the player as given by the server (may be offline UUID or not).
* The UUID is not present if AuthMe is configured not to store the UUID or if the data is not
* present (e.g. older record).
*
* @return player uuid, or empty optional if not available
*/
Optional<UUID> getUuid();
/**
* Returns the email address associated with this player, or an empty optional if not available.
*
* @return player's email or empty optional
*/
Optional<String> getEmail();
/**
* @return the registration date of the player's account - never null
*/
Instant getRegistrationDate();
/**
* Returns the IP address with which the player's account was registered. Returns an empty optional
* for older accounts, or if the account was registered by someone else (e.g. by an admin).
*
* @return the ip address used during the registration of the account, or empty optional
*/
Optional<String> getRegistrationIpAddress();
/**
* Returns the last login date of the player. An empty optional is returned if the player never logged in.
*
* @return date the player last logged in successfully, or empty optional if not applicable
*/
Optional<Instant> getLastLoginDate();
/**
* Returns the IP address the player last logged in with successfully. Returns an empty optional if the
* player never logged in.
*
* @return ip address the player last logged in with successfully, or empty optional if not applicable
*/
Optional<String> getLastLoginIpAddress();
}

View File

@ -0,0 +1,93 @@
package fr.xephi.authme.api.v3;
import fr.xephi.authme.data.auth.PlayerAuth;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
/**
* Implementation of {@link AuthMePlayer}. This implementation is not part of the API and
* may have breaking changes in subsequent releases.
*/
class AuthMePlayerImpl implements AuthMePlayer {
private String name;
private UUID uuid;
private String email;
private Instant registrationDate;
private String registrationIpAddress;
private Instant lastLoginDate;
private String lastLoginIpAddress;
AuthMePlayerImpl() {
}
/**
* Maps the given player auth to an AuthMePlayer instance. Returns an empty optional if
* the player auth is null.
*
* @param playerAuth the player auth or null
* @return the mapped player auth, or empty optional if the argument was null
*/
static Optional<AuthMePlayer> fromPlayerAuth(PlayerAuth playerAuth) {
if (playerAuth == null) {
return Optional.empty();
}
AuthMePlayerImpl authMeUser = new AuthMePlayerImpl();
authMeUser.name = playerAuth.getRealName();
authMeUser.uuid = playerAuth.getUuid();
authMeUser.email = nullIfDefault(playerAuth.getEmail(), PlayerAuth.DB_EMAIL_DEFAULT);
Long lastLoginMillis = nullIfDefault(playerAuth.getLastLogin(), PlayerAuth.DB_LAST_LOGIN_DEFAULT);
authMeUser.registrationDate = toInstant(playerAuth.getRegistrationDate());
authMeUser.registrationIpAddress = playerAuth.getRegistrationIp();
authMeUser.lastLoginDate = toInstant(lastLoginMillis);
authMeUser.lastLoginIpAddress = nullIfDefault(playerAuth.getLastIp(), PlayerAuth.DB_LAST_IP_DEFAULT);
return Optional.of(authMeUser);
}
@Override
public String getName() {
return name;
}
public Optional<UUID> getUuid() {
return Optional.ofNullable(uuid);
}
@Override
public Optional<String> getEmail() {
return Optional.ofNullable(email);
}
@Override
public Instant getRegistrationDate() {
return registrationDate;
}
@Override
public Optional<String> getRegistrationIpAddress() {
return Optional.ofNullable(registrationIpAddress);
}
@Override
public Optional<Instant> getLastLoginDate() {
return Optional.ofNullable( lastLoginDate);
}
@Override
public Optional<String> getLastLoginIpAddress() {
return Optional.ofNullable(lastLoginIpAddress);
}
private static Instant toInstant(Long epochMillis) {
return epochMillis == null ? null : Instant.ofEpochMilli(epochMillis);
}
private static <T> T nullIfDefault(T value, T defaultValue) {
return defaultValue.equals(value) ? null : value;
}
}

View File

@ -0,0 +1,61 @@
package fr.xephi.authme.command;
/**
* Wrapper for the description of a command argument.
*/
public class CommandArgumentDescription {
/**
* Argument name (one-word description of the argument).
*/
private final String name;
/**
* Argument description.
*/
private final String description;
/**
* Defines whether the argument is optional.
*/
private final boolean isOptional;
/**
* Constructor.
*
* @param name The argument name.
* @param description The argument description.
* @param isOptional True if the argument is optional, false otherwise.
*/
public CommandArgumentDescription(String name, String description, boolean isOptional) {
this.name = name;
this.description = description;
this.isOptional = isOptional;
}
/**
* Get the argument name.
*
* @return Argument name.
*/
public String getName() {
return this.name;
}
/**
* Get the argument description.
*
* @return Argument description.
*/
public String getDescription() {
return description;
}
/**
* Return whether the argument is optional.
*
* @return True if the argument is optional, false otherwise.
*/
public boolean isOptional() {
return isOptional;
}
}

View File

@ -0,0 +1,294 @@
package fr.xephi.authme.command;
import fr.xephi.authme.permission.PermissionNode;
import fr.xephi.authme.util.StringUtils;
import fr.xephi.authme.util.Utils;
import java.util.ArrayList;
import java.util.List;
import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Arrays.asList;
/**
* Command description defines which labels ("names") will lead to a command and points to the
* {@link ExecutableCommand} implementation that executes the logic of the command.
* <p>
* CommandDescription instances are built hierarchically: they have one parent, or {@code null} for base commands
* (main commands such as {@code /authme}), and may have multiple children extending the mapping of the parent: e.g. if
* {@code /authme} has a child whose label is {@code "register"}, then {@code /authme register} is the command that
* the child defines.
*/
@SuppressWarnings("checkstyle:FinalClass") // Justification: class is mocked in multiple tests
public class CommandDescription {
/**
* Defines the labels to execute the command. For example, if labels are "register" and "r" and the parent is
* the command for "/authme", then both "/authme register" and "/authme r" will be handled by this command.
*/
private List<String> labels;
/**
* Short description of the command.
*/
private String description;
/**
* Detailed description of what the command does.
*/
private String detailedDescription;
/**
* The class implementing the command described by this object.
*/
private Class<? extends ExecutableCommand> executableCommand;
/**
* The parent command.
*/
private CommandDescription parent;
/**
* The child commands that extend this command.
*/
private List<CommandDescription> children = new ArrayList<>();
/**
* The arguments the command takes.
*/
private List<CommandArgumentDescription> arguments;
/**
* Permission node required to execute this command.
*/
private PermissionNode permission;
/**
* Private constructor.
* <p>
* Note for developers: Instances should be created with {@link CommandBuilder#register()} to be properly
* registered in the command tree.
*
* @param labels command labels
* @param description description of the command
* @param detailedDescription detailed command description
* @param executableCommand class of the command implementation
* @param parent parent command
* @param arguments command arguments
* @param permission permission node required to execute this command
*/
private CommandDescription(List<String> labels, String description, String detailedDescription,
Class<? extends ExecutableCommand> executableCommand, CommandDescription parent,
List<CommandArgumentDescription> arguments, PermissionNode permission) {
this.labels = labels;
this.description = description;
this.detailedDescription = detailedDescription;
this.executableCommand = executableCommand;
this.parent = parent;
this.arguments = arguments;
this.permission = permission;
}
/**
* Return all relative labels of this command. For example, if this object describes {@code /authme register} and
* {@code /authme r}, then it will return a list with {@code register} and {@code r}. The parent label
* {@code authme} is not returned.
*
* @return All labels of the command description.
*/
public List<String> getLabels() {
return labels;
}
/**
* Check whether this command description has the given label.
*
* @param commandLabel The label to check for.
*
* @return {@code true} if this command contains the given label, {@code false} otherwise.
*/
public boolean hasLabel(String commandLabel) {
for (String label : labels) {
if (label.equalsIgnoreCase(commandLabel)) {
return true;
}
}
return false;
}
/**
* Return the {@link ExecutableCommand} class implementing this command.
*
* @return The executable command class
*/
public Class<? extends ExecutableCommand> getExecutableCommand() {
return executableCommand;
}
/**
* Return the parent.
*
* @return The parent command, or null for base commands
*/
public CommandDescription getParent() {
return parent;
}
/**
* Return the number of labels necessary to get to this command. This corresponds to the number of parents + 1.
*
* @return The number of labels, e.g. for "/authme abc def" the label count is 3
*/
public int getLabelCount() {
if (parent == null) {
return 1;
}
return parent.getLabelCount() + 1;
}
/**
* Return all command children.
*
* @return Command children.
*/
public List<CommandDescription> getChildren() {
return children;
}
/**
* Return all arguments the command takes.
*
* @return Command arguments.
*/
public List<CommandArgumentDescription> getArguments() {
return arguments;
}
/**
* Return a short description of the command.
*
* @return Command description.
*/
public String getDescription() {
return description;
}
/**
* Return a detailed description of the command.
*
* @return Detailed description.
*/
public String getDetailedDescription() {
return detailedDescription;
}
/**
* Return the permission node required to execute the command.
*
* @return The permission node, or null if none are required to execute the command.
*/
public PermissionNode getPermission() {
return permission;
}
/**
* Return a builder instance to create a new command description.
*
* @return The builder
*/
public static CommandBuilder builder() {
return new CommandBuilder();
}
/**
* Builder for initializing CommandDescription objects.
*/
public static final class CommandBuilder {
private List<String> labels;
private String description;
private String detailedDescription;
private Class<? extends ExecutableCommand> executableCommand;
private CommandDescription parent;
private List<CommandArgumentDescription> arguments = new ArrayList<>();
private PermissionNode permission;
/**
* Build a CommandDescription and register it onto the parent if available.
*
* @return The generated CommandDescription object
*/
public CommandDescription register() {
CommandDescription command = build();
if (command.parent != null) {
command.parent.children.add(command);
}
return command;
}
/**
* Build a CommandDescription (without registering it on the parent).
*
* @return The generated CommandDescription object
*/
public CommandDescription build() {
checkArgument(!Utils.isCollectionEmpty(labels), "Labels may not be empty");
checkArgument(!StringUtils.isBlank(description), "Description may not be empty");
checkArgument(!StringUtils.isBlank(detailedDescription), "Detailed description may not be empty");
checkArgument(executableCommand != null, "Executable command must be set");
// parents and permissions may be null; arguments may be empty
return new CommandDescription(labels, description, detailedDescription, executableCommand,
parent, arguments, permission);
}
public CommandBuilder labels(List<String> labels) {
this.labels = labels;
return this;
}
public CommandBuilder labels(String... labels) {
return labels(asList(labels));
}
public CommandBuilder description(String description) {
this.description = description;
return this;
}
public CommandBuilder detailedDescription(String detailedDescription) {
this.detailedDescription = detailedDescription;
return this;
}
public CommandBuilder executableCommand(Class<? extends ExecutableCommand> executableCommand) {
this.executableCommand = executableCommand;
return this;
}
public CommandBuilder parent(CommandDescription parent) {
this.parent = parent;
return this;
}
/**
* Add an argument that the command description requires. This method can be called multiples times to add
* multiple arguments.
*
* @param label The label of the argument (single word name of the argument)
* @param description The description of the argument
* @param isOptional True if the argument is optional, false if it is mandatory
*
* @return The builder
*/
public CommandBuilder withArgument(String label, String description, boolean isOptional) {
arguments.add(new CommandArgumentDescription(label, description, isOptional));
return this;
}
/**
* Add a permission node that a user must have to execute the command.
*
* @param permission The PermissionNode to add
* @return The builder
*/
public CommandBuilder permission(PermissionNode permission) {
this.permission = permission;
return this;
}
}
}

View File

@ -0,0 +1,186 @@
package fr.xephi.authme.command;
import ch.jalu.injector.factory.Factory;
import fr.xephi.authme.AuthMe;
import fr.xephi.authme.command.help.HelpProvider;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.util.StringUtils;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* The AuthMe command handler, responsible for invoking the correct {@link ExecutableCommand} based on incoming
* command labels or for displaying a help message for unknown command labels.
*/
public class CommandHandler {
/**
* The threshold for suggesting a similar command. If the difference is below this value, we will
* ask the player whether he meant the similar command.
*/
private static final double SUGGEST_COMMAND_THRESHOLD = 0.75;
private final CommandMapper commandMapper;
private final PermissionsManager permissionsManager;
private final Messages messages;
private final HelpProvider helpProvider;
/**
* Map with ExecutableCommand children. The key is the type of the value.
*/
private Map<Class<? extends ExecutableCommand>, ExecutableCommand> commands = new HashMap<>();
@Inject
CommandHandler(Factory<ExecutableCommand> commandFactory, CommandMapper commandMapper,
PermissionsManager permissionsManager, Messages messages, HelpProvider helpProvider) {
this.commandMapper = commandMapper;
this.permissionsManager = permissionsManager;
this.messages = messages;
this.helpProvider = helpProvider;
initializeCommands(commandFactory, commandMapper.getCommandClasses());
}
/**
* Map a command that was invoked to the proper {@link CommandDescription} or return a useful error
* message upon failure.
*
* @param sender The command sender.
* @param bukkitCommandLabel The command label (Bukkit).
* @param bukkitArgs The command arguments (Bukkit).
*
* @return True if the command was executed, false otherwise.
*/
public boolean processCommand(CommandSender sender, String bukkitCommandLabel, String[] bukkitArgs) {
// Add the Bukkit command label to the front so we get a list like [authme, register, bobby, mysecret]
List<String> parts = skipEmptyArguments(bukkitArgs);
parts.add(0, bukkitCommandLabel);
FoundCommandResult result = commandMapper.mapPartsToCommand(sender, parts);
handleCommandResult(sender, result);
return !FoundResultStatus.MISSING_BASE_COMMAND.equals(result.getResultStatus());
}
/**
* Processes the given {@link FoundCommandResult} for the provided command sender.
*
* @param sender the command sender who executed the command
* @param result the command mapping result
*/
private void handleCommandResult(CommandSender sender, FoundCommandResult result) {
switch (result.getResultStatus()) {
case SUCCESS:
executeCommand(sender, result);
break;
case MISSING_BASE_COMMAND:
sender.sendMessage(ChatColor.DARK_RED + "Failed to parse " + AuthMe.getPluginName() + " command!");
break;
case INCORRECT_ARGUMENTS:
sendImproperArgumentsMessage(sender, result);
break;
case UNKNOWN_LABEL:
sendUnknownCommandMessage(sender, result);
break;
case NO_PERMISSION:
messages.send(sender, MessageKey.NO_PERMISSION);
break;
default:
throw new IllegalStateException("Unknown result status '" + result.getResultStatus() + "'");
}
}
/**
* Initialize all required ExecutableCommand objects.
*
* @param commandFactory factory to create command objects
* @param commandClasses the classes to instantiate
*/
private void initializeCommands(Factory<ExecutableCommand> commandFactory,
Set<Class<? extends ExecutableCommand>> commandClasses) {
for (Class<? extends ExecutableCommand> clazz : commandClasses) {
commands.put(clazz, commandFactory.newInstance(clazz));
}
}
/**
* Execute the command for the given command sender.
*
* @param sender The sender which initiated the command
* @param result The mapped result
*/
private void executeCommand(CommandSender sender, FoundCommandResult result) {
ExecutableCommand executableCommand = commands.get(result.getCommandDescription().getExecutableCommand());
List<String> arguments = result.getArguments();
executableCommand.executeCommand(sender, arguments);
}
/**
* Skip all entries of the given array that are simply whitespace.
*
* @param args The array to process
* @return List of the items that are not empty
*/
private static List<String> skipEmptyArguments(String[] args) {
List<String> cleanArguments = new ArrayList<>();
for (String argument : args) {
if (!StringUtils.isBlank(argument)) {
cleanArguments.add(argument);
}
}
return cleanArguments;
}
/**
* Show an "unknown command" message to the user and suggest an existing command if its similarity is within
* the defined threshold.
*
* @param sender The command sender
* @param result The command that was found during the mapping process
*/
private static void sendUnknownCommandMessage(CommandSender sender, FoundCommandResult result) {
sender.sendMessage(ChatColor.DARK_RED + "Unknown command!");
// Show a command suggestion if available and the difference isn't too big
if (result.getDifference() <= SUGGEST_COMMAND_THRESHOLD && result.getCommandDescription() != null) {
sender.sendMessage(ChatColor.YELLOW + "Did you mean " + ChatColor.GOLD
+ CommandUtils.constructCommandPath(result.getCommandDescription()) + ChatColor.YELLOW + "?");
}
sender.sendMessage(ChatColor.YELLOW + "Use the command " + ChatColor.GOLD + "/" + result.getLabels().get(0)
+ " help" + ChatColor.YELLOW + " to view help.");
}
private void sendImproperArgumentsMessage(CommandSender sender, FoundCommandResult result) {
CommandDescription command = result.getCommandDescription();
if (!permissionsManager.hasPermission(sender, command.getPermission())) {
messages.send(sender, MessageKey.NO_PERMISSION);
return;
}
ExecutableCommand executableCommand = commands.get(command.getExecutableCommand());
MessageKey usageMessage = executableCommand.getArgumentsMismatchMessage();
if (usageMessage == null) {
showHelpForCommand(sender, result);
} else {
messages.send(sender, usageMessage);
}
}
private void showHelpForCommand(CommandSender sender, FoundCommandResult result) {
sender.sendMessage(ChatColor.DARK_RED + "Incorrect command arguments!");
helpProvider.outputHelp(sender, result, HelpProvider.SHOW_ARGUMENTS);
List<String> labels = result.getLabels();
String childLabel = labels.size() >= 2 ? labels.get(1) : "";
sender.sendMessage(ChatColor.GOLD + "Detailed help: " + ChatColor.WHITE
+ "/" + labels.get(0) + " help " + childLabel);
}
}

View File

@ -0,0 +1,652 @@
package fr.xephi.authme.command;
import com.google.common.collect.ImmutableList;
import fr.xephi.authme.command.executable.HelpCommand;
import fr.xephi.authme.command.executable.authme.AccountsCommand;
import fr.xephi.authme.command.executable.authme.AuthMeCommand;
import fr.xephi.authme.command.executable.authme.BackupCommand;
import fr.xephi.authme.command.executable.authme.ChangePasswordAdminCommand;
import fr.xephi.authme.command.executable.authme.ConverterCommand;
import fr.xephi.authme.command.executable.authme.FirstSpawnCommand;
import fr.xephi.authme.command.executable.authme.ForceLoginCommand;
import fr.xephi.authme.command.executable.authme.GetEmailCommand;
import fr.xephi.authme.command.executable.authme.GetIpCommand;
import fr.xephi.authme.command.executable.authme.LastLoginCommand;
import fr.xephi.authme.command.executable.authme.PurgeBannedPlayersCommand;
import fr.xephi.authme.command.executable.authme.PurgeCommand;
import fr.xephi.authme.command.executable.authme.PurgeLastPositionCommand;
import fr.xephi.authme.command.executable.authme.PurgePlayerCommand;
import fr.xephi.authme.command.executable.authme.RecentPlayersCommand;
import fr.xephi.authme.command.executable.authme.RegisterAdminCommand;
import fr.xephi.authme.command.executable.authme.ReloadCommand;
import fr.xephi.authme.command.executable.authme.SetEmailCommand;
import fr.xephi.authme.command.executable.authme.SetFirstSpawnCommand;
import fr.xephi.authme.command.executable.authme.SetSpawnCommand;
import fr.xephi.authme.command.executable.authme.SpawnCommand;
import fr.xephi.authme.command.executable.authme.SwitchAntiBotCommand;
import fr.xephi.authme.command.executable.authme.TotpDisableAdminCommand;
import fr.xephi.authme.command.executable.authme.TotpViewStatusCommand;
import fr.xephi.authme.command.executable.authme.UnregisterAdminCommand;
import fr.xephi.authme.command.executable.authme.UpdateHelpMessagesCommand;
import fr.xephi.authme.command.executable.authme.VersionCommand;
import fr.xephi.authme.command.executable.authme.debug.DebugCommand;
import fr.xephi.authme.command.executable.captcha.CaptchaCommand;
import fr.xephi.authme.command.executable.changepassword.ChangePasswordCommand;
import fr.xephi.authme.command.executable.email.AddEmailCommand;
import fr.xephi.authme.command.executable.email.ChangeEmailCommand;
import fr.xephi.authme.command.executable.email.EmailBaseCommand;
import fr.xephi.authme.command.executable.email.EmailSetPasswordCommand;
import fr.xephi.authme.command.executable.email.ProcessCodeCommand;
import fr.xephi.authme.command.executable.email.RecoverEmailCommand;
import fr.xephi.authme.command.executable.email.ShowEmailCommand;
import fr.xephi.authme.command.executable.login.LoginCommand;
import fr.xephi.authme.command.executable.logout.LogoutCommand;
import fr.xephi.authme.command.executable.register.RegisterCommand;
import fr.xephi.authme.command.executable.totp.AddTotpCommand;
import fr.xephi.authme.command.executable.totp.ConfirmTotpCommand;
import fr.xephi.authme.command.executable.totp.RemoveTotpCommand;
import fr.xephi.authme.command.executable.totp.TotpBaseCommand;
import fr.xephi.authme.command.executable.totp.TotpCodeCommand;
import fr.xephi.authme.command.executable.unregister.UnregisterCommand;
import fr.xephi.authme.command.executable.verification.VerificationCommand;
import fr.xephi.authme.permission.AdminPermission;
import fr.xephi.authme.permission.DebugSectionPermissions;
import fr.xephi.authme.permission.PlayerPermission;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
* Initializes all available AuthMe commands.
*/
public class CommandInitializer {
private static final boolean OPTIONAL = true;
private static final boolean MANDATORY = false;
private List<CommandDescription> commands;
public CommandInitializer() {
buildCommands();
}
/**
* Returns the description of all AuthMe commands.
*
* @return the command descriptions
*/
public List<CommandDescription> getCommands() {
return commands;
}
/**
* Builds the command description objects for all available AuthMe commands.
*/
private void buildCommands() {
// Register /authme and /email commands
CommandDescription authMeBase = buildAuthMeBaseCommand();
CommandDescription emailBase = buildEmailBaseCommand();
// Register the base login command
CommandDescription loginBase = CommandDescription.builder()
.parent(null)
.labels("login", "l", "log")
.description("Login command")
.detailedDescription("Command to log in using AuthMeReloaded.")
.withArgument("password", "Login password", MANDATORY)
.permission(PlayerPermission.LOGIN)
.executableCommand(LoginCommand.class)
.register();
// Register the base logout command
CommandDescription logoutBase = CommandDescription.builder()
.parent(null)
.labels("logout")
.description("Logout command")
.detailedDescription("Command to logout using AuthMeReloaded.")
.permission(PlayerPermission.LOGOUT)
.executableCommand(LogoutCommand.class)
.register();
// Register the base register command
CommandDescription registerBase = CommandDescription.builder()
.parent(null)
.labels("register", "reg")
.description("Register an account")
.detailedDescription("Command to register using AuthMeReloaded.")
.withArgument("password", "Password", OPTIONAL)
.withArgument("verifyPassword", "Verify password", OPTIONAL)
.permission(PlayerPermission.REGISTER)
.executableCommand(RegisterCommand.class)
.register();
// Register the base unregister command
CommandDescription unregisterBase = CommandDescription.builder()
.parent(null)
.labels("unregister", "unreg")
.description("Unregister an account")
.detailedDescription("Command to unregister using AuthMeReloaded.")
.withArgument("password", "Password", MANDATORY)
.permission(PlayerPermission.UNREGISTER)
.executableCommand(UnregisterCommand.class)
.register();
// Register the base changepassword command
CommandDescription changePasswordBase = CommandDescription.builder()
.parent(null)
.labels("changepassword", "changepass", "cp")
.description("Change password of an account")
.detailedDescription("Command to change your password using AuthMeReloaded.")
.withArgument("oldPassword", "Old password", MANDATORY)
.withArgument("newPassword", "New password", MANDATORY)
.permission(PlayerPermission.CHANGE_PASSWORD)
.executableCommand(ChangePasswordCommand.class)
.register();
// Create totp base command
CommandDescription totpBase = buildTotpBaseCommand();
// Register the base captcha command
CommandDescription captchaBase = CommandDescription.builder()
.parent(null)
.labels("captcha")
.description("Captcha command")
.detailedDescription("Captcha command for AuthMeReloaded.")
.withArgument("captcha", "The Captcha", MANDATORY)
.permission(PlayerPermission.CAPTCHA)
.executableCommand(CaptchaCommand.class)
.register();
// Register the base verification code command
CommandDescription verificationBase = CommandDescription.builder()
.parent(null)
.labels("verification")
.description("Verification command")
.detailedDescription("Command to complete the verification process for AuthMeReloaded.")
.withArgument("code", "The code", MANDATORY)
.permission(PlayerPermission.VERIFICATION_CODE)
.executableCommand(VerificationCommand.class)
.register();
List<CommandDescription> baseCommands = ImmutableList.of(authMeBase, emailBase, loginBase, logoutBase,
registerBase, unregisterBase, changePasswordBase, totpBase, captchaBase, verificationBase);
setHelpOnAllBases(baseCommands);
commands = baseCommands;
}
/**
* Creates a command description object for {@code /authme} including its children.
*
* @return the authme base command description
*/
private CommandDescription buildAuthMeBaseCommand() {
// Register the base AuthMe Reloaded command
CommandDescription authmeBase = CommandDescription.builder()
.labels("authme")
.description("AuthMe op commands")
.detailedDescription("The main AuthMeReloaded command. The root for all admin commands.")
.executableCommand(AuthMeCommand.class)
.register();
// Register the register command
CommandDescription.builder()
.parent(authmeBase)
.labels("register", "reg", "r")
.description("Register a player")
.detailedDescription("Register the specified player with the specified password.")
.withArgument("player", "Player name", MANDATORY)
.withArgument("password", "Password", MANDATORY)
.permission(AdminPermission.REGISTER)
.executableCommand(RegisterAdminCommand.class)
.register();
// Register the unregister command
CommandDescription.builder()
.parent(authmeBase)
.labels("unregister", "unreg", "unr")
.description("Unregister a player")
.detailedDescription("Unregister the specified player.")
.withArgument("player", "Player name", MANDATORY)
.permission(AdminPermission.UNREGISTER)
.executableCommand(UnregisterAdminCommand.class)
.register();
// Register the forcelogin command
CommandDescription.builder()
.parent(authmeBase)
.labels("forcelogin", "login")
.description("Enforce login player")
.detailedDescription("Enforce the specified player to login.")
.withArgument("player", "Online player name", OPTIONAL)
.permission(AdminPermission.FORCE_LOGIN)
.executableCommand(ForceLoginCommand.class)
.register();
// Register the changepassword command
CommandDescription.builder()
.parent(authmeBase)
.labels("password", "changepassword", "changepass", "cp")
.description("Change a player's password")
.detailedDescription("Change the password of a player.")
.withArgument("player", "Player name", MANDATORY)
.withArgument("pwd", "New password", MANDATORY)
.permission(AdminPermission.CHANGE_PASSWORD)
.executableCommand(ChangePasswordAdminCommand.class)
.register();
// Register the last login command
CommandDescription.builder()
.parent(authmeBase)
.labels("lastlogin", "ll")
.description("Player's last login")
.detailedDescription("View the date of the specified players last login.")
.withArgument("player", "Player name", OPTIONAL)
.permission(AdminPermission.LAST_LOGIN)
.executableCommand(LastLoginCommand.class)
.register();
// Register the accounts command
CommandDescription.builder()
.parent(authmeBase)
.labels("accounts", "account")
.description("Display player accounts")
.detailedDescription("Display all accounts of a player by his player name or IP.")
.withArgument("player", "Player name or IP", OPTIONAL)
.permission(AdminPermission.ACCOUNTS)
.executableCommand(AccountsCommand.class)
.register();
// Register the getemail command
CommandDescription.builder()
.parent(authmeBase)
.labels("email", "mail", "getemail", "getmail")
.description("Display player's email")
.detailedDescription("Display the email address of the specified player if set.")
.withArgument("player", "Player name", OPTIONAL)
.permission(AdminPermission.GET_EMAIL)
.executableCommand(GetEmailCommand.class)
.register();
// Register the setemail command
CommandDescription.builder()
.parent(authmeBase)
.labels("setemail", "setmail", "chgemail", "chgmail")
.description("Change player's email")
.detailedDescription("Change the email address of the specified player.")
.withArgument("player", "Player name", MANDATORY)
.withArgument("email", "Player email", MANDATORY)
.permission(AdminPermission.CHANGE_EMAIL)
.executableCommand(SetEmailCommand.class)
.register();
// Register the getip command
CommandDescription.builder()
.parent(authmeBase)
.labels("getip", "ip")
.description("Get player's IP")
.detailedDescription("Get the IP address of the specified online player.")
.withArgument("player", "Player name", MANDATORY)
.permission(AdminPermission.GET_IP)
.executableCommand(GetIpCommand.class)
.register();
// Register totp command
CommandDescription.builder()
.parent(authmeBase)
.labels("totp", "2fa")
.description("See if a player has enabled TOTP")
.detailedDescription("Returns whether the specified player has enabled two-factor authentication.")
.withArgument("player", "Player name", MANDATORY)
.permission(AdminPermission.VIEW_TOTP_STATUS)
.executableCommand(TotpViewStatusCommand.class)
.register();
// Register disable totp command
CommandDescription.builder()
.parent(authmeBase)
.labels("disabletotp", "disable2fa", "deletetotp", "delete2fa")
.description("Delete TOTP token of a player")
.detailedDescription("Disable two-factor authentication for a player.")
.withArgument("player", "Player name", MANDATORY)
.permission(AdminPermission.DISABLE_TOTP)
.executableCommand(TotpDisableAdminCommand.class)
.register();
// Register the spawn command
CommandDescription.builder()
.parent(authmeBase)
.labels("spawn", "home")
.description("Teleport to spawn")
.detailedDescription("Teleport to the spawn.")
.permission(AdminPermission.SPAWN)
.executableCommand(SpawnCommand.class)
.register();
// Register the setspawn command
CommandDescription.builder()
.parent(authmeBase)
.labels("setspawn", "chgspawn")
.description("Change the spawn")
.detailedDescription("Change the player's spawn to your current position.")
.permission(AdminPermission.SET_SPAWN)
.executableCommand(SetSpawnCommand.class)
.register();
// Register the firstspawn command
CommandDescription.builder()
.parent(authmeBase)
.labels("firstspawn", "firsthome")
.description("Teleport to first spawn")
.detailedDescription("Teleport to the first spawn.")
.permission(AdminPermission.FIRST_SPAWN)
.executableCommand(FirstSpawnCommand.class)
.register();
// Register the setfirstspawn command
CommandDescription.builder()
.parent(authmeBase)
.labels("setfirstspawn", "chgfirstspawn")
.description("Change the first spawn")
.detailedDescription("Change the first player's spawn to your current position.")
.permission(AdminPermission.SET_FIRST_SPAWN)
.executableCommand(SetFirstSpawnCommand.class)
.register();
// Register the purge command
CommandDescription.builder()
.parent(authmeBase)
.labels("purge", "delete")
.description("Purge old data")
.detailedDescription("Purge old AuthMeReloaded data longer than the specified number of days ago.")
.withArgument("days", "Number of days", MANDATORY)
.permission(AdminPermission.PURGE)
.executableCommand(PurgeCommand.class)
.register();
// Purge player command
CommandDescription.builder()
.parent(authmeBase)
.labels("purgeplayer")
.description("Purges the data of one player")
.detailedDescription("Purges data of the given player.")
.withArgument("player", "The player to purge", MANDATORY)
.withArgument("options", "'force' to run without checking if player is registered", OPTIONAL)
.permission(AdminPermission.PURGE_PLAYER)
.executableCommand(PurgePlayerCommand.class)
.register();
// Backup command
CommandDescription.builder()
.parent(authmeBase)
.labels("backup")
.description("Perform a backup")
.detailedDescription("Creates a backup of the registered users.")
.permission(AdminPermission.BACKUP)
.executableCommand(BackupCommand.class)
.register();
// Register the purgelastposition command
CommandDescription.builder()
.parent(authmeBase)
.labels("resetpos", "purgelastposition", "purgelastpos", "resetposition",
"resetlastposition", "resetlastpos")
.description("Purge player's last position")
.detailedDescription("Purge the last know position of the specified player or all of them.")
.withArgument("player/*", "Player name or * for all players", MANDATORY)
.permission(AdminPermission.PURGE_LAST_POSITION)
.executableCommand(PurgeLastPositionCommand.class)
.register();
// Register the purgebannedplayers command
CommandDescription.builder()
.parent(authmeBase)
.labels("purgebannedplayers", "purgebannedplayer", "deletebannedplayers", "deletebannedplayer")
.description("Purge banned players data")
.detailedDescription("Purge all AuthMeReloaded data for banned players.")
.permission(AdminPermission.PURGE_BANNED_PLAYERS)
.executableCommand(PurgeBannedPlayersCommand.class)
.register();
// Register the switchantibot command
CommandDescription.builder()
.parent(authmeBase)
.labels("switchantibot", "toggleantibot", "antibot")
.description("Switch AntiBot mode")
.detailedDescription("Switch or toggle the AntiBot mode to the specified state.")
.withArgument("mode", "ON / OFF", OPTIONAL)
.permission(AdminPermission.SWITCH_ANTIBOT)
.executableCommand(SwitchAntiBotCommand.class)
.register();
// Register the reload command
CommandDescription.builder()
.parent(authmeBase)
.labels("reload", "rld")
.description("Reload plugin")
.detailedDescription("Reload the AuthMeReloaded plugin.")
.permission(AdminPermission.RELOAD)
.executableCommand(ReloadCommand.class)
.register();
// Register the version command
CommandDescription.builder()
.parent(authmeBase)
.labels("version", "ver", "v", "about", "info")
.description("Version info")
.detailedDescription("Show detailed information about the installed AuthMeReloaded version, the "
+ "developers, contributors, and license.")
.executableCommand(VersionCommand.class)
.register();
CommandDescription.builder()
.parent(authmeBase)
.labels("converter", "convert", "conv")
.description("Converter command")
.detailedDescription("Converter command for AuthMeReloaded.")
.withArgument("job", "Conversion job: xauth / crazylogin / rakamak / "
+ "royalauth / vauth / sqliteToSql / mysqlToSqlite / loginsecurity", OPTIONAL)
.permission(AdminPermission.CONVERTER)
.executableCommand(ConverterCommand.class)
.register();
CommandDescription.builder()
.parent(authmeBase)
.labels("messages", "msg")
.description("Add missing help messages")
.detailedDescription("Adds missing texts to the current help messages file.")
.permission(AdminPermission.UPDATE_MESSAGES)
.executableCommand(UpdateHelpMessagesCommand.class)
.register();
CommandDescription.builder()
.parent(authmeBase)
.labels("recent")
.description("See players who have recently logged in")
.detailedDescription("Shows the last players that have logged in.")
.permission(AdminPermission.SEE_RECENT_PLAYERS)
.executableCommand(RecentPlayersCommand.class)
.register();
CommandDescription.builder()
.parent(authmeBase)
.labels("debug", "dbg")
.description("Debug features")
.detailedDescription("Allows various operations for debugging.")
.withArgument("child", "The child to execute", OPTIONAL)
.withArgument("arg", "argument (depends on debug section)", OPTIONAL)
.withArgument("arg", "argument (depends on debug section)", OPTIONAL)
.permission(DebugSectionPermissions.DEBUG_COMMAND)
.executableCommand(DebugCommand.class)
.register();
return authmeBase;
}
/**
* Creates a command description for {@code /email} including its children.
*
* @return the email base command description
*/
private CommandDescription buildEmailBaseCommand() {
// Register the base Email command
CommandDescription emailBase = CommandDescription.builder()
.parent(null)
.labels("email")
.description("Add email or recover password")
.detailedDescription("The AuthMeReloaded email command base.")
.executableCommand(EmailBaseCommand.class)
.register();
// Register the show command
CommandDescription.builder()
.parent(emailBase)
.labels("show", "myemail")
.description("Show Email")
.detailedDescription("Show your current email address.")
.permission(PlayerPermission.SEE_EMAIL)
.executableCommand(ShowEmailCommand.class)
.register();
// Register the add command
CommandDescription.builder()
.parent(emailBase)
.labels("add", "addemail", "addmail")
.description("Add Email")
.detailedDescription("Add a new email address to your account.")
.withArgument("email", "Email address", MANDATORY)
.withArgument("verifyEmail", "Email address verification", MANDATORY)
.permission(PlayerPermission.ADD_EMAIL)
.executableCommand(AddEmailCommand.class)
.register();
// Register the change command
CommandDescription.builder()
.parent(emailBase)
.labels("change", "changeemail", "changemail")
.description("Change Email")
.detailedDescription("Change an email address of your account.")
.withArgument("oldEmail", "Old email address", MANDATORY)
.withArgument("newEmail", "New email address", MANDATORY)
.permission(PlayerPermission.CHANGE_EMAIL)
.executableCommand(ChangeEmailCommand.class)
.register();
// Register the recover command
CommandDescription.builder()
.parent(emailBase)
.labels("recover", "recovery", "recoveremail", "recovermail")
.description("Recover password using email")
.detailedDescription("Recover your account using an Email address by sending a mail containing "
+ "a new password.")
.withArgument("email", "Email address", MANDATORY)
.permission(PlayerPermission.RECOVER_EMAIL)
.executableCommand(RecoverEmailCommand.class)
.register();
// Register the process recovery code command
CommandDescription.builder()
.parent(emailBase)
.labels("code")
.description("Submit code to recover password")
.detailedDescription("Recover your account by submitting a code delivered to your email.")
.withArgument("code", "Recovery code", MANDATORY)
.permission(PlayerPermission.RECOVER_EMAIL)
.executableCommand(ProcessCodeCommand.class)
.register();
// Register the change password after recovery command
CommandDescription.builder()
.parent(emailBase)
.labels("setpassword")
.description("Set new password after recovery")
.detailedDescription("Set a new password after successfully recovering your account.")
.withArgument("password", "New password", MANDATORY)
.permission(PlayerPermission.RECOVER_EMAIL)
.executableCommand(EmailSetPasswordCommand.class)
.register();
return emailBase;
}
/**
* Creates a command description object for {@code /totp} including its children.
*
* @return the totp base command description
*/
private CommandDescription buildTotpBaseCommand() {
// Register the base totp command
CommandDescription totpBase = CommandDescription.builder()
.parent(null)
.labels("totp", "2fa")
.description("TOTP commands")
.detailedDescription("Performs actions related to two-factor authentication.")
.executableCommand(TotpBaseCommand.class)
.register();
// Register the base totp code
CommandDescription.builder()
.parent(totpBase)
.labels("code", "c")
.description("Command for logging in")
.detailedDescription("Processes the two-factor authentication code during login.")
.withArgument("code", "The TOTP code to use to log in", MANDATORY)
.executableCommand(TotpCodeCommand.class)
.register();
// Register totp add
CommandDescription.builder()
.parent(totpBase)
.labels("add")
.description("Enables TOTP")
.detailedDescription("Enables two-factor authentication for your account.")
.permission(PlayerPermission.ENABLE_TWO_FACTOR_AUTH)
.executableCommand(AddTotpCommand.class)
.register();
// Register totp confirm
CommandDescription.builder()
.parent(totpBase)
.labels("confirm")
.description("Enables TOTP after successful code")
.detailedDescription("Saves the generated TOTP secret after confirmation.")
.withArgument("code", "Code from the given secret from /totp add", MANDATORY)
.permission(PlayerPermission.ENABLE_TWO_FACTOR_AUTH)
.executableCommand(ConfirmTotpCommand.class)
.register();
// Register totp remove
CommandDescription.builder()
.parent(totpBase)
.labels("remove")
.description("Removes TOTP")
.detailedDescription("Disables two-factor authentication for your account.")
.withArgument("code", "Current 2FA code", MANDATORY)
.permission(PlayerPermission.DISABLE_TWO_FACTOR_AUTH)
.executableCommand(RemoveTotpCommand.class)
.register();
return totpBase;
}
/**
* Sets the help command on all base commands, e.g. to register /authme help or /register help.
*
* @param commands the list of base commands to register a help child command on
*/
private void setHelpOnAllBases(Collection<CommandDescription> commands) {
final List<String> helpCommandLabels = Arrays.asList("help", "hlp", "h", "sos", "?");
for (CommandDescription base : commands) {
CommandDescription.builder()
.parent(base)
.labels(helpCommandLabels)
.description("View help")
.detailedDescription("View detailed help for /" + base.getLabels().get(0) + " commands.")
.withArgument("query", "The command or query to view help for.", OPTIONAL)
.executableCommand(HelpCommand.class)
.register();
}
}
}

View File

@ -0,0 +1,207 @@
package fr.xephi.authme.command;
import fr.xephi.authme.command.executable.HelpCommand;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.util.StringUtils;
import fr.xephi.authme.util.Utils;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import static fr.xephi.authme.command.FoundResultStatus.INCORRECT_ARGUMENTS;
import static fr.xephi.authme.command.FoundResultStatus.MISSING_BASE_COMMAND;
import static fr.xephi.authme.command.FoundResultStatus.UNKNOWN_LABEL;
/**
* Maps incoming command parts to the correct {@link CommandDescription}.
*/
public class CommandMapper {
/**
* The class of the help command, to which the base label should also be passed in the arguments.
*/
private static final Class<? extends ExecutableCommand> HELP_COMMAND_CLASS = HelpCommand.class;
private final Collection<CommandDescription> baseCommands;
private final PermissionsManager permissionsManager;
@Inject
public CommandMapper(CommandInitializer commandInitializer, PermissionsManager permissionsManager) {
this.baseCommands = commandInitializer.getCommands();
this.permissionsManager = permissionsManager;
}
/**
* Map incoming command parts to a command. This processes all parts and distinguishes the labels from arguments.
*
* @param sender The command sender (null if none applicable)
* @param parts The parts to map to commands and arguments
* @return The generated {@link FoundCommandResult}
*/
public FoundCommandResult mapPartsToCommand(CommandSender sender, List<String> parts) {
if (Utils.isCollectionEmpty(parts)) {
return new FoundCommandResult(null, parts, null, 0.0, MISSING_BASE_COMMAND);
}
CommandDescription base = getBaseCommand(parts.get(0));
if (base == null) {
return new FoundCommandResult(null, parts, null, 0.0, MISSING_BASE_COMMAND);
}
// Prefer labels: /register help goes to "Help command", not "Register command" with argument 'help'
List<String> remainingParts = parts.subList(1, parts.size());
CommandDescription childCommand = getSuitableChild(base, remainingParts);
if (childCommand != null) {
FoundResultStatus status = getPermissionAwareStatus(sender, childCommand);
FoundCommandResult result = new FoundCommandResult(
childCommand, parts.subList(0, 2), parts.subList(2, parts.size()), 0.0, status);
return transformResultForHelp(result);
} else if (hasSuitableArgumentCount(base, remainingParts.size())) {
FoundResultStatus status = getPermissionAwareStatus(sender, base);
return new FoundCommandResult(base, parts.subList(0, 1), parts.subList(1, parts.size()), 0.0, status);
}
return getCommandWithSmallestDifference(base, parts);
}
/**
* Return all {@link ExecutableCommand} classes referenced in {@link CommandDescription} objects.
*
* @return all classes
* @see CommandInitializer#getCommands
*/
public Set<Class<? extends ExecutableCommand>> getCommandClasses() {
Set<Class<? extends ExecutableCommand>> classes = new HashSet<>(50);
for (CommandDescription command : baseCommands) {
classes.add(command.getExecutableCommand());
for (CommandDescription child : command.getChildren()) {
classes.add(child.getExecutableCommand());
}
}
return classes;
}
/**
* Return the command whose label matches the given parts the best. This method is called when
* a successful mapping could not be performed.
*
* @param base the base command
* @param parts the command parts
* @return the closest result
*/
private static FoundCommandResult getCommandWithSmallestDifference(CommandDescription base, List<String> parts) {
// Return the base command with incorrect arg count error if we only have one part
if (parts.size() <= 1) {
return new FoundCommandResult(base, parts, new ArrayList<>(), 0.0, INCORRECT_ARGUMENTS);
}
final String childLabel = parts.get(1);
double minDifference = Double.POSITIVE_INFINITY;
CommandDescription closestCommand = null;
for (CommandDescription child : base.getChildren()) {
double difference = getLabelDifference(child, childLabel);
if (difference < minDifference) {
minDifference = difference;
closestCommand = child;
}
}
// base command may have no children, in which case we return the base command with incorrect arguments error
if (closestCommand == null) {
return new FoundCommandResult(
base, parts.subList(0, 1), parts.subList(1, parts.size()), 0.0, INCORRECT_ARGUMENTS);
}
FoundResultStatus status = (minDifference == 0.0) ? INCORRECT_ARGUMENTS : UNKNOWN_LABEL;
final int partsSize = parts.size();
List<String> labels = parts.subList(0, Math.min(closestCommand.getLabelCount(), partsSize));
List<String> arguments = (labels.size() == partsSize)
? new ArrayList<>()
: parts.subList(labels.size(), partsSize);
return new FoundCommandResult(closestCommand, labels, arguments, minDifference, status);
}
private CommandDescription getBaseCommand(String label) {
String baseLabel = label.toLowerCase(Locale.ROOT);
if (baseLabel.startsWith("authme:")) {
baseLabel = baseLabel.substring("authme:".length());
}
for (CommandDescription command : baseCommands) {
if (command.hasLabel(baseLabel)) {
return command;
}
}
return null;
}
/**
* Return a child from a base command if the label and the argument count match.
*
* @param baseCommand The base command whose children should be checked
* @param parts The command parts received from the invocation; the first item is the potential label and any
* other items are command arguments. The first initial part that led to the base command should not
* be present.
*
* @return A command if there was a complete match (including proper argument count), null otherwise
*/
private static CommandDescription getSuitableChild(CommandDescription baseCommand, List<String> parts) {
if (Utils.isCollectionEmpty(parts)) {
return null;
}
final String label = parts.get(0).toLowerCase(Locale.ROOT);
final int argumentCount = parts.size() - 1;
for (CommandDescription child : baseCommand.getChildren()) {
if (child.hasLabel(label) && hasSuitableArgumentCount(child, argumentCount)) {
return child;
}
}
return null;
}
private static FoundCommandResult transformResultForHelp(FoundCommandResult result) {
if (result.getCommandDescription() != null
&& HELP_COMMAND_CLASS == result.getCommandDescription().getExecutableCommand()) {
// For "/authme help register" we have labels = [authme, help] and arguments = [register]
// But for the help command we want labels = [authme, help] and arguments = [authme, register],
// so we can use the arguments as the labels to the command to show help for
List<String> arguments = new ArrayList<>(result.getArguments());
arguments.add(0, result.getLabels().get(0));
return new FoundCommandResult(result.getCommandDescription(), result.getLabels(),
arguments, result.getDifference(), result.getResultStatus());
}
return result;
}
private FoundResultStatus getPermissionAwareStatus(CommandSender sender, CommandDescription command) {
if (sender != null && !permissionsManager.hasPermission(sender, command.getPermission())) {
return FoundResultStatus.NO_PERMISSION;
}
return FoundResultStatus.SUCCESS;
}
private static boolean hasSuitableArgumentCount(CommandDescription command, int argumentCount) {
int minArgs = CommandUtils.getMinNumberOfArguments(command);
int maxArgs = CommandUtils.getMaxNumberOfArguments(command);
return argumentCount >= minArgs && argumentCount <= maxArgs;
}
private static double getLabelDifference(CommandDescription command, String givenLabel) {
return command.getLabels().stream()
.map(label -> StringUtils.getDifference(label, givenLabel))
.min(Double::compareTo)
.orElseThrow(() -> new IllegalStateException("Command does not have any labels set"));
}
}

View File

@ -0,0 +1,109 @@
package fr.xephi.authme.command;
import com.google.common.collect.Lists;
import org.bukkit.ChatColor;
import java.util.ArrayList;
import java.util.List;
/**
* Utility functions for {@link CommandDescription} objects.
*/
public final class CommandUtils {
private CommandUtils() {
}
/**
* Returns the minimum number of arguments required for running the command (= number of mandatory arguments).
*
* @param command the command to process
* @return min number of arguments required by the command
*/
public static int getMinNumberOfArguments(CommandDescription command) {
int mandatoryArguments = 0;
for (CommandArgumentDescription argument : command.getArguments()) {
if (!argument.isOptional()) {
++mandatoryArguments;
}
}
return mandatoryArguments;
}
/**
* Returns the maximum number of arguments the command accepts.
*
* @param command the command to process
* @return max number of arguments that may be passed to the command
*/
public static int getMaxNumberOfArguments(CommandDescription command) {
return command.getArguments().size();
}
/**
* Constructs a hierarchical list of commands for the given command. The commands are in order:
* the parents of the given command precede the provided command. For example, given the command
* for {@code /authme register}, a list with {@code [{authme}, {authme register}]} is returned.
*
* @param command the command to build a parent list for
* @return the parent list
*/
public static List<CommandDescription> constructParentList(CommandDescription command) {
List<CommandDescription> commands = new ArrayList<>();
CommandDescription currentCommand = command;
while (currentCommand != null) {
commands.add(currentCommand);
currentCommand = currentCommand.getParent();
}
return Lists.reverse(commands);
}
/**
* Returns a textual representation of the command, e.g. {@code /authme register}.
*
* @param command the command to create the path for
* @return the command string
*/
public static String constructCommandPath(CommandDescription command) {
StringBuilder sb = new StringBuilder();
String prefix = "/";
for (CommandDescription ancestor : constructParentList(command)) {
sb.append(prefix).append(ancestor.getLabels().get(0));
prefix = " ";
}
return sb.toString();
}
/**
* Constructs a command path with color formatting, based on the supplied labels. This includes
* the command's arguments, as defined in the provided command description. The list of labels
* must contain all labels to be used.
*
* @param command the command to read arguments from
* @param correctLabels the labels to use (must be complete)
* @return formatted command syntax incl. arguments
*/
public static String buildSyntax(CommandDescription command, List<String> correctLabels) {
StringBuilder commandSyntax = new StringBuilder(ChatColor.WHITE + "/" + correctLabels.get(0) + ChatColor.YELLOW);
for (int i = 1; i < correctLabels.size(); ++i) {
commandSyntax.append(" ").append(correctLabels.get(i));
}
for (CommandArgumentDescription argument : command.getArguments()) {
commandSyntax.append(" ").append(formatArgument(argument));
}
return commandSyntax.toString();
}
/**
* Formats a command argument with the proper type of brackets.
*
* @param argument the argument to format
* @return the formatted argument
*/
public static String formatArgument(CommandArgumentDescription argument) {
if (argument.isOptional()) {
return "[" + argument.getName() + "]";
}
return "<" + argument.getName() + ">";
}
}

View File

@ -0,0 +1,31 @@
package fr.xephi.authme.command;
import fr.xephi.authme.message.MessageKey;
import org.bukkit.command.CommandSender;
import java.util.List;
/**
* Base class for AuthMe commands that can be executed.
*/
public interface ExecutableCommand {
/**
* Executes the command with the given arguments.
*
* @param sender the command sender (initiator of the command)
* @param arguments the arguments
*/
void executeCommand(CommandSender sender, List<String> arguments);
/**
* Returns the message to show to the user if the command is used with the wrong arguments.
* If null is returned, the standard help (/<i>command</i> help) output is shown.
*
* @return the message explaining the command's usage, or {@code null} for default behavior
*/
default MessageKey getArgumentsMismatchMessage() {
return null;
}
}

View File

@ -0,0 +1,79 @@
package fr.xephi.authme.command;
import java.util.List;
/**
* Result of a command mapping by {@link CommandHandler}. An object of this class represents a successful mapping
* as well as erroneous ones, as communicated with {@link FoundResultStatus}.
* <p>
* Fields other than {@link FoundResultStatus} are available depending, among other factors, on the status:
* <ul>
* <li>{@link FoundResultStatus#SUCCESS} entails that mapping the input to a command was successful. Therefore,
* the command description, labels and arguments are set. The difference is 0.0.</li>
* <li>{@link FoundResultStatus#INCORRECT_ARGUMENTS}: The received parts could be mapped to a command but the argument
* count doesn't match. Guarantees that the command description field is not null; difference is 0.0</li>
* <li>{@link FoundResultStatus#UNKNOWN_LABEL}: The labels could not be mapped to a command. The command description
* may be set to the most similar command, or it may be null. Difference is above 0.0.</li>
* <li>{@link FoundResultStatus#NO_PERMISSION}: The command could be matched properly but the sender does not have
* permission to execute it.</li>
* <li>{@link FoundResultStatus#MISSING_BASE_COMMAND} should never occur. All other fields may be null and any further
* processing of the object should be aborted.</li>
* </ul>
*/
public class FoundCommandResult {
/**
* The command description instance.
*/
private final CommandDescription commandDescription;
/**
* The labels used to invoke the command. This may be different for the same {@link ExecutableCommand} instance
* if multiple labels have been defined, e.g. "/authme register" and "/authme reg".
*/
private final List<String> labels;
/** The command arguments. */
private final List<String> arguments;
/** The difference between the matched command and the supplied labels. */
private final double difference;
/** The status of the result (see class description). */
private final FoundResultStatus resultStatus;
/**
* Constructor.
*
* @param commandDescription The command description.
* @param labels The labels used to access the command.
* @param arguments The command arguments.
* @param difference The difference between the supplied labels and the matched command.
* @param resultStatus The status of the result.
*/
public FoundCommandResult(CommandDescription commandDescription, List<String> labels, List<String> arguments,
double difference, FoundResultStatus resultStatus) {
this.commandDescription = commandDescription;
this.labels = labels;
this.arguments = arguments;
this.difference = difference;
this.resultStatus = resultStatus;
}
public CommandDescription getCommandDescription() {
return this.commandDescription;
}
public List<String> getArguments() {
return this.arguments;
}
public List<String> getLabels() {
return this.labels;
}
public double getDifference() {
return difference;
}
public FoundResultStatus getResultStatus() {
return resultStatus;
}
}

View File

@ -0,0 +1,18 @@
package fr.xephi.authme.command;
/**
* Result status for mapping command parts. See {@link FoundCommandResult} for a detailed description of the states.
*/
public enum FoundResultStatus {
SUCCESS,
INCORRECT_ARGUMENTS,
UNKNOWN_LABEL,
NO_PERMISSION,
MISSING_BASE_COMMAND
}

View File

@ -0,0 +1,45 @@
package fr.xephi.authme.command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.List;
/**
* Common base type for player-only commands, handling the verification that the command sender is indeed a player.
*/
public abstract class PlayerCommand implements ExecutableCommand {
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
if (sender instanceof Player) {
runCommand((Player) sender, arguments);
} else {
String alternative = getAlternativeCommand();
if (alternative != null) {
sender.sendMessage("Player only! Please use " + alternative + " instead.");
} else {
sender.sendMessage("This command is only for players.");
}
}
}
/**
* Runs the command with the given player and arguments.
*
* @param player the player who initiated the command
* @param arguments the arguments supplied with the command
*/
protected abstract void runCommand(Player player, List<String> arguments);
/**
* Returns an alternative command (textual representation) that is not restricted to players only.
* Example: {@code "/authme register <playerName> <password>"}
*
* @return Alternative command not restricted to players, or null if not applicable
*/
protected String getAlternativeCommand() {
return null;
}
}

View File

@ -0,0 +1,17 @@
package fr.xephi.authme.command;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
public class TabCompleteHandler implements TabCompleter {
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) {
return new ArrayList<>();
}
}

View File

@ -0,0 +1,64 @@
package fr.xephi.authme.command.executable;
import fr.xephi.authme.command.CommandMapper;
import fr.xephi.authme.command.CommandUtils;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.command.FoundCommandResult;
import fr.xephi.authme.command.FoundResultStatus;
import fr.xephi.authme.command.help.HelpProvider;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.List;
import static fr.xephi.authme.command.FoundResultStatus.MISSING_BASE_COMMAND;
import static fr.xephi.authme.command.FoundResultStatus.UNKNOWN_LABEL;
import static fr.xephi.authme.command.help.HelpProvider.ALL_OPTIONS;
import static fr.xephi.authme.command.help.HelpProvider.SHOW_ALTERNATIVES;
import static fr.xephi.authme.command.help.HelpProvider.SHOW_CHILDREN;
import static fr.xephi.authme.command.help.HelpProvider.SHOW_COMMAND;
import static fr.xephi.authme.command.help.HelpProvider.SHOW_DESCRIPTION;
/**
* Displays help information to a user.
*/
public class HelpCommand implements ExecutableCommand {
@Inject
private CommandMapper commandMapper;
@Inject
private HelpProvider helpProvider;
// Convention: arguments is not the actual invoked arguments but the command that was invoked,
// e.g. "/authme help register" would typically be arguments = [register], but here we pass [authme, register]
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
FoundCommandResult result = commandMapper.mapPartsToCommand(sender, arguments);
FoundResultStatus resultStatus = result.getResultStatus();
if (MISSING_BASE_COMMAND.equals(resultStatus)) {
sender.sendMessage(ChatColor.DARK_RED + "Could not get base command");
return;
} else if (UNKNOWN_LABEL.equals(resultStatus)) {
if (result.getCommandDescription() == null) {
sender.sendMessage(ChatColor.DARK_RED + "Unknown command");
return;
} else {
sender.sendMessage(ChatColor.GOLD + "Assuming " + ChatColor.WHITE
+ CommandUtils.constructCommandPath(result.getCommandDescription()));
}
}
int mappedCommandLevel = result.getCommandDescription().getLabelCount();
if (mappedCommandLevel == 1) {
helpProvider.outputHelp(sender, result,
SHOW_COMMAND | SHOW_DESCRIPTION | SHOW_CHILDREN | SHOW_ALTERNATIVES);
} else {
helpProvider.outputHelp(sender, result, ALL_OPTIONS);
}
}
}

View File

@ -0,0 +1,74 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.CommonService;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.List;
import java.util.Locale;
/**
* Shows all accounts registered by the same IP address for the given player name or IP address.
*/
public class AccountsCommand implements ExecutableCommand {
@Inject
private DataSource dataSource;
@Inject
private BukkitService bukkitService;
@Inject
private CommonService commonService;
@Override
public void executeCommand(final CommandSender sender, List<String> arguments) {
// TODO #1366: last IP vs. registration IP?
final String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0);
// Assumption: a player name cannot contain '.'
if (playerName.contains(".")) {
bukkitService.runTaskAsynchronously(() -> {
List<String> accountList = dataSource.getAllAuthsByIp(playerName);
if (accountList.isEmpty()) {
sender.sendMessage("[AuthMe] This IP does not exist in the database.");
} else if (accountList.size() == 1) {
sender.sendMessage("[AuthMe] " + playerName + " is a single account player");
} else {
outputAccountsList(sender, playerName, accountList);
}
});
} else {
bukkitService.runTaskAsynchronously(() -> {
PlayerAuth auth = dataSource.getAuth(playerName.toLowerCase(Locale.ROOT));
if (auth == null) {
commonService.send(sender, MessageKey.UNKNOWN_USER);
return;
} else if (auth.getLastIp() == null) {
sender.sendMessage("No known last IP address for player");
return;
}
List<String> accountList = dataSource.getAllAuthsByIp(auth.getLastIp());
if (accountList.isEmpty()) {
commonService.send(sender, MessageKey.UNKNOWN_USER);
} else if (accountList.size() == 1) {
sender.sendMessage("[AuthMe] " + playerName + " is a single account player");
} else {
outputAccountsList(sender, playerName, accountList);
}
});
}
}
private static void outputAccountsList(CommandSender sender, String playerName, List<String> accountList) {
sender.sendMessage("[AuthMe] " + playerName + " has " + accountList.size() + " accounts.");
String message = "[AuthMe] " + String.join(", ", accountList) + ".";
sender.sendMessage(message);
}
}

View File

@ -0,0 +1,24 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.AuthMe;
import fr.xephi.authme.command.ExecutableCommand;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import java.util.List;
/**
* AuthMe base command; shows the version and some command pointers.
*/
public class AuthMeCommand implements ExecutableCommand {
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
sender.sendMessage(ChatColor.GREEN + "This server is running " + AuthMe.getPluginName() + " v"
+ AuthMe.getPluginVersion() + " b" + AuthMe.getPluginBuildNumber()+ "! " + ChatColor.RED + "<3");
sender.sendMessage(ChatColor.YELLOW + "Use the command " + ChatColor.GOLD + "/authme help" + ChatColor.YELLOW
+ " to view help.");
sender.sendMessage(ChatColor.YELLOW + "Use the command " + ChatColor.GOLD + "/authme about" + ChatColor.YELLOW
+ " to view about.");
}
}

View File

@ -0,0 +1,23 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.service.BackupService;
import fr.xephi.authme.service.BackupService.BackupCause;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.List;
/**
* Command to perform a backup.
*/
public class BackupCommand implements ExecutableCommand {
@Inject
private BackupService backupService;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
backupService.doBackup(BackupCause.COMMAND, sender);
}
}

View File

@ -0,0 +1,41 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.process.Management;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.service.ValidationService;
import fr.xephi.authme.service.ValidationService.ValidationResult;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.List;
/**
* Admin command for changing a player's password.
*/
public class ChangePasswordAdminCommand implements ExecutableCommand {
@Inject
private ValidationService validationService;
@Inject
private CommonService commonService;
@Inject
private Management management;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
// Get the player and password
final String playerName = arguments.get(0);
final String playerPass = arguments.get(1);
// Validate the password
ValidationResult validationResult = validationService.validatePassword(playerPass, playerName);
if (validationResult.hasError()) {
commonService.send(sender, validationResult.getMessageKey(), validationResult.getArgs());
} else {
management.performPasswordChangeAsAdmin(sender, playerName, playerPass);
}
}
}

View File

@ -0,0 +1,95 @@
package fr.xephi.authme.command.executable.authme;
import ch.jalu.injector.factory.Factory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSortedMap;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.datasource.converter.Converter;
import fr.xephi.authme.datasource.converter.CrazyLoginConverter;
import fr.xephi.authme.datasource.converter.H2ToSqlite;
import fr.xephi.authme.datasource.converter.LoginSecurityConverter;
import fr.xephi.authme.datasource.converter.MySqlToSqlite;
import fr.xephi.authme.datasource.converter.RoyalAuthConverter;
import fr.xephi.authme.datasource.converter.SqliteToH2;
import fr.xephi.authme.datasource.converter.SqliteToSql;
import fr.xephi.authme.datasource.converter.XAuthConverter;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.CommonService;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* Converter command: launches conversion based on its parameters.
*/
public class ConverterCommand implements ExecutableCommand {
@VisibleForTesting
static final Map<String, Class<? extends Converter>> CONVERTERS = getConverters();
private final ConsoleLogger logger = ConsoleLoggerFactory.get(ConverterCommand.class);
@Inject
private CommonService commonService;
@Inject
private BukkitService bukkitService;
@Inject
private Factory<Converter> converterFactory;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
Class<? extends Converter> converterClass = getConverterClassFromArgs(arguments);
if (converterClass == null) {
sender.sendMessage("Converters: " + String.join(", ", CONVERTERS.keySet()));
return;
}
// Get the proper converter instance
final Converter converter = converterFactory.newInstance(converterClass);
// Run the convert job
bukkitService.runTaskAsynchronously(() -> {
try {
converter.execute(sender);
} catch (Exception e) {
commonService.send(sender, MessageKey.ERROR);
logger.logException("Error during conversion:", e);
}
});
// Show a status message
sender.sendMessage("[AuthMe] Successfully started " + arguments.get(0));
}
private static Class<? extends Converter> getConverterClassFromArgs(List<String> arguments) {
return arguments.isEmpty()
? null
: CONVERTERS.get(arguments.get(0).toLowerCase(Locale.ROOT));
}
/**
* Initializes a map with all available converters.
*
* @return map with all available converters
*/
private static Map<String, Class<? extends Converter>> getConverters() {
return ImmutableSortedMap.<String, Class<? extends Converter>>naturalOrder()
.put("xauth", XAuthConverter.class)
.put("crazylogin", CrazyLoginConverter.class)
.put("royalauth", RoyalAuthConverter.class)
.put("sqlitetosql", SqliteToSql.class)
.put("mysqltosqlite", MySqlToSqlite.class)
.put("sqlitetoh2", SqliteToH2.class)
.put("h2tosqlite", H2ToSqlite.class)
.put("loginsecurity", LoginSecurityConverter.class)
.build();
}
}

View File

@ -0,0 +1,35 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.SpawnLoader;
import fr.xephi.authme.util.TeleportUtils;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Teleports the player to the first spawn.
*/
public class FirstSpawnCommand extends PlayerCommand {
@Inject
private Settings settings;
@Inject
private SpawnLoader spawnLoader;
@Inject
private BukkitService bukkitService;
@Override
public void runCommand(Player player, List<String> arguments) {
if (spawnLoader.getFirstSpawn() == null) {
player.sendMessage("[AuthMe] First spawn has failed, please try to define the first spawn");
} else {
//String name= player.getName();
bukkitService.runTaskIfFolia(player, () -> {
TeleportUtils.teleport(player, spawnLoader.getFirstSpawn());
});
//player.teleport(spawnLoader.getFirstSpawn());
}
}
}

View File

@ -0,0 +1,44 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.process.Management;
import fr.xephi.authme.service.BukkitService;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
import static fr.xephi.authme.permission.PlayerPermission.CAN_LOGIN_BE_FORCED;
/**
* Forces the login of a player, i.e. logs the player in without the need of a (correct) password.
*/
public class ForceLoginCommand implements ExecutableCommand {
@Inject
private PermissionsManager permissionsManager;
@Inject
private Management management;
@Inject
private BukkitService bukkitService;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
// Get the player query
String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0);
Player player = bukkitService.getPlayerExact(playerName);
if (player == null || !player.isOnline()) {
sender.sendMessage("Player needs to be online!");
} else if (!permissionsManager.hasPermission(player, CAN_LOGIN_BE_FORCED)) {
sender.sendMessage("You cannot force login the player " + playerName + "!");
} else {
management.forceLogin(player);
sender.sendMessage("Force login for " + playerName + " performed!");
}
}
}

View File

@ -0,0 +1,35 @@
package fr.xephi.authme.command.executable.authme;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.service.CommonService;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.List;
/**
* Returns a player's email.
*/
public class GetEmailCommand implements ExecutableCommand {
@Inject
private DataSource dataSource;
@Inject
private CommonService commonService;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0);
DataSourceValue<String> email = dataSource.getEmail(playerName);
if (email.rowExists()) {
sender.sendMessage("[AuthMe] " + playerName + "'s email: " + email.getValue());
} else {
commonService.send(sender, MessageKey.UNKNOWN_USER);
}
}
}

View File

@ -0,0 +1,41 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.util.PlayerUtils;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
public class GetIpCommand implements ExecutableCommand {
@Inject
private BukkitService bukkitService;
@Inject
private DataSource dataSource;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
String playerName = arguments.get(0);
Player player = bukkitService.getPlayerExact(playerName);
PlayerAuth auth = dataSource.getAuth(playerName);
if (player != null) {
sender.sendMessage("Current IP of " + player.getName() + " is " + PlayerUtils.getPlayerIp(player)
+ ":" + player.getAddress().getPort());
}
if (auth == null) {
String displayName = player == null ? playerName : player.getName();
sender.sendMessage(displayName + " is not registered in the database");
} else {
sender.sendMessage("Database: last IP: " + auth.getLastIp() + ", registration IP: "
+ auth.getRegistrationIp());
}
}
}

View File

@ -0,0 +1,56 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.service.CommonService;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.Date;
import java.util.List;
/**
* Returns the last login date of the given user.
*/
public class LastLoginCommand implements ExecutableCommand {
@Inject
private DataSource dataSource;
@Inject
private CommonService commonService;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
// Get the player
String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0);
PlayerAuth auth = dataSource.getAuth(playerName);
if (auth == null) {
commonService.send(sender, MessageKey.UNKNOWN_USER);
return;
}
// Get the last login date
final Long lastLogin = auth.getLastLogin();
final String lastLoginDate = lastLogin == null ? "never" : new Date(lastLogin).toString();
// Show the player status
sender.sendMessage("[AuthMe] " + playerName + " last login: " + lastLoginDate);
if (lastLogin != null) {
sender.sendMessage("[AuthMe] The player " + playerName + " last logged in "
+ createLastLoginIntervalMessage(lastLogin) + " ago");
}
sender.sendMessage("[AuthMe] Last player's IP: " + auth.getLastIp());
}
private static String createLastLoginIntervalMessage(long lastLogin) {
final long diff = System.currentTimeMillis() - lastLogin;
return (int) (diff / 86400000) + " days "
+ (int) (diff / 3600000 % 24) + " hours "
+ (int) (diff / 60000 % 60) + " mins "
+ (int) (diff / 1000 % 60) + " secs";
}
}

View File

@ -0,0 +1,38 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.task.purge.PurgeService;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
/**
* Command for purging data of banned players. Depending on the settings
* it purges (deletes) data from third-party plugins as well.
*/
public class PurgeBannedPlayersCommand implements ExecutableCommand {
@Inject
private PurgeService purgeService;
@Inject
private BukkitService bukkitService;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
// Get the list of banned players
Set<OfflinePlayer> bannedPlayers = bukkitService.getBannedPlayers();
Set<String> namedBanned = new HashSet<>(bannedPlayers.size());
for (OfflinePlayer offlinePlayer : bannedPlayers) {
namedBanned.add(offlinePlayer.getName().toLowerCase(Locale.ROOT));
}
purgeService.purgePlayers(sender, namedBanned, bannedPlayers.toArray(new OfflinePlayer[bannedPlayers.size()]));
}
}

View File

@ -0,0 +1,51 @@
package fr.xephi.authme.command.executable.authme;
import com.google.common.primitives.Ints;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.task.purge.PurgeService;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.Calendar;
import java.util.List;
/**
* Command for purging the data of players which have not been online for a given number
* of days. Depending on the settings, this removes player data in third-party plugins as well.
*/
public class PurgeCommand implements ExecutableCommand {
private static final int MINIMUM_LAST_SEEN_DAYS = 30;
@Inject
private PurgeService purgeService;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
// Get the days parameter
String daysStr = arguments.get(0);
// Convert the days string to an integer value, and make sure it's valid
Integer days = Ints.tryParse(daysStr);
if (days == null) {
sender.sendMessage(ChatColor.RED + "The value you've entered is invalid!");
return;
}
// Validate the value
if (days < MINIMUM_LAST_SEEN_DAYS) {
sender.sendMessage(ChatColor.RED + "You can only purge data older than "
+ MINIMUM_LAST_SEEN_DAYS + " days");
return;
}
// Create a calender instance to determine the date
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DATE, -days);
long until = calendar.getTimeInMillis();
// Run the purge
purgeService.runPurge(sender, until);
}
}

View File

@ -0,0 +1,56 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.service.CommonService;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.List;
/**
* Removes the stored last position of a user or of all.
*/
public class PurgeLastPositionCommand implements ExecutableCommand {
@Inject
private DataSource dataSource;
@Inject
private CommonService commonService;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0);
if ("*".equals(playerName)) {
for (PlayerAuth auth : dataSource.getAllAuths()) {
resetLastPosition(auth);
dataSource.updateQuitLoc(auth);
// TODO: send an update when a messaging service will be implemented (QUITLOC)
}
sender.sendMessage("All players last position locations are now reset");
} else {
// Get the user auth and make sure the user exists
PlayerAuth auth = dataSource.getAuth(playerName);
if (auth == null) {
commonService.send(sender, MessageKey.UNKNOWN_USER);
return;
}
resetLastPosition(auth);
dataSource.updateQuitLoc(auth);
// TODO: send an update when a messaging service will be implemented (QUITLOC)
sender.sendMessage(playerName + "'s last position location is now reset");
}
}
private static void resetLastPosition(PlayerAuth auth) {
auth.setQuitLocX(0d);
auth.setQuitLocY(0d);
auth.setQuitLocZ(0d);
auth.setWorld("world");
}
}

View File

@ -0,0 +1,47 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.task.purge.PurgeExecutor;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.List;
import java.util.Locale;
import static java.util.Collections.singletonList;
/**
* Command to purge a player.
*/
public class PurgePlayerCommand implements ExecutableCommand {
@Inject
private PurgeExecutor purgeExecutor;
@Inject
private BukkitService bukkitService;
@Inject
private DataSource dataSource;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
String option = arguments.size() > 1 ? arguments.get(1) : null;
bukkitService.runTaskAsynchronously(
() -> executeCommand(sender, arguments.get(0), option));
}
private void executeCommand(CommandSender sender, String name, String option) {
if ("force".equals(option) || !dataSource.isAuthAvailable(name)) {
OfflinePlayer offlinePlayer = bukkitService.getOfflinePlayer(name);
purgeExecutor.executePurge(singletonList(offlinePlayer), singletonList(name.toLowerCase(Locale.ROOT)));
sender.sendMessage("Purged data for player " + name);
} else {
sender.sendMessage("This player is still registered! Are you sure you want to proceed? "
+ "Use '/authme purgeplayer " + name + " force' to run the command anyway");
}
}
}

View File

@ -0,0 +1,55 @@
package fr.xephi.authme.command.executable.authme;
import com.google.common.annotations.VisibleForTesting;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import static java.time.Instant.ofEpochMilli;
/**
* Command showing the most recent logged in players.
*/
public class RecentPlayersCommand implements ExecutableCommand {
/** DateTime formatter, producing Strings such as "10:42 AM, 11 Jul". */
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("hh:mm a, dd MMM");
@Inject
private DataSource dataSource;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
List<PlayerAuth> recentPlayers = dataSource.getRecentlyLoggedInPlayers();
sender.sendMessage(ChatColor.BLUE + "[AuthMe] Recently logged in players");
for (PlayerAuth auth : recentPlayers) {
sender.sendMessage(formatPlayerMessage(auth));
}
}
@VisibleForTesting
ZoneId getZoneId() {
return ZoneId.systemDefault();
}
private String formatPlayerMessage(PlayerAuth auth) {
String lastLoginText;
if (auth.getLastLogin() == null) {
lastLoginText = "never";
} else {
LocalDateTime lastLogin = LocalDateTime.ofInstant(ofEpochMilli(auth.getLastLogin()), getZoneId());
lastLoginText = DATE_FORMAT.format(lastLogin);
}
return "- " + auth.getRealName() + " (" + lastLoginText + " with IP " + auth.getLastIp() + ")";
}
}

View File

@ -0,0 +1,86 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.security.PasswordSecurity;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.service.ValidationService;
import fr.xephi.authme.service.ValidationService.ValidationResult;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
import java.util.Locale;
/**
* Admin command to register a user.
*/
public class RegisterAdminCommand implements ExecutableCommand {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(RegisterAdminCommand.class);
@Inject
private PasswordSecurity passwordSecurity;
@Inject
private CommonService commonService;
@Inject
private DataSource dataSource;
@Inject
private BukkitService bukkitService;
@Inject
private ValidationService validationService;
@Override
public void executeCommand(final CommandSender sender, List<String> arguments) {
// Get the player name and password
final String playerName = arguments.get(0);
final String playerPass = arguments.get(1);
final String playerNameLowerCase = playerName.toLowerCase(Locale.ROOT);
// Command logic
ValidationResult passwordValidation = validationService.validatePassword(playerPass, playerName);
if (passwordValidation.hasError()) {
commonService.send(sender, passwordValidation.getMessageKey(), passwordValidation.getArgs());
return;
}
bukkitService.runTaskOptionallyAsync(() -> {
if (dataSource.isAuthAvailable(playerNameLowerCase)) {
commonService.send(sender, MessageKey.NAME_ALREADY_REGISTERED);
return;
}
HashedPassword hashedPassword = passwordSecurity.computeHash(playerPass, playerNameLowerCase);
PlayerAuth auth = PlayerAuth.builder()
.name(playerNameLowerCase)
.realName(playerName)
.password(hashedPassword)
.registrationDate(System.currentTimeMillis())
.build();
if (!dataSource.saveAuth(auth)) {
commonService.send(sender, MessageKey.ERROR);
return;
}
commonService.send(sender, MessageKey.REGISTER_SUCCESS);
logger.info(sender.getName() + " registered " + playerName);
final Player player = bukkitService.getPlayerExact(playerName);
if (player != null) {
bukkitService.scheduleSyncTaskFromOptionallyAsyncTask(() ->
// AuthMeReReloaded - Folia compatibility
bukkitService.runTaskIfFolia(player, () -> player.kickPlayer(commonService.retrieveSingleMessage(player, MessageKey.KICK_FOR_ADMIN_REGISTER))));
}
});
}
}

View File

@ -0,0 +1,77 @@
package fr.xephi.authme.command.executable.authme;
import ch.jalu.injector.factory.SingletonStore;
import fr.xephi.authme.AuthMe;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.initialization.Reloadable;
import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.SettingsWarner;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import fr.xephi.authme.util.Utils;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.List;
/**
* The reload command.
*/
public class ReloadCommand implements ExecutableCommand {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(ReloadCommand.class);
@Inject
private AuthMe plugin;
@Inject
private Settings settings;
@Inject
private DataSource dataSource;
@Inject
private CommonService commonService;
@Inject
private SettingsWarner settingsWarner;
@Inject
private SingletonStore<Reloadable> reloadableStore;
@Inject
private SingletonStore<SettingsDependent> settingsDependentStore;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
try {
settings.reload();
ConsoleLoggerFactory.reloadSettings(settings);
settingsWarner.logWarningsForMisconfigurations();
// We do not change database type for consistency issues, but we'll output a note in the logs
if (!settings.getProperty(DatabaseSettings.BACKEND).equals(dataSource.getType())) {
Utils.logAndSendMessage(sender, "Note: cannot change database type during /authme reload");
}
performReloadOnServices();
commonService.send(sender, MessageKey.CONFIG_RELOAD_SUCCESS);
} catch (Exception e) {
sender.sendMessage("Error occurred during reload of AuthMe: aborting");
logger.logException("Aborting! Encountered exception during reload of AuthMe:", e);
plugin.stopOrUnload();
}
}
private void performReloadOnServices() {
reloadableStore.retrieveAllOfType()
.forEach(r -> r.reload());
settingsDependentStore.retrieveAllOfType()
.forEach(s -> s.reload(settings));
}
}

View File

@ -0,0 +1,75 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.service.ValidationService;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.List;
/**
* Admin command for setting an email to an account.
*/
public class SetEmailCommand implements ExecutableCommand {
@Inject
private DataSource dataSource;
@Inject
private CommonService commonService;
@Inject
private PlayerCache playerCache;
@Inject
private BukkitService bukkitService;
@Inject
private ValidationService validationService;
@Override
public void executeCommand(final CommandSender sender, List<String> arguments) {
// Get the player name and email address
final String playerName = arguments.get(0);
final String playerEmail = arguments.get(1);
// Validate the email address
if (!validationService.validateEmail(playerEmail)) {
commonService.send(sender, MessageKey.INVALID_EMAIL);
return;
}
bukkitService.runTaskOptionallyAsync(() -> { // AuthMeReReloaded - Folia compatibility
// Validate the user
PlayerAuth auth = dataSource.getAuth(playerName);
if (auth == null) {
commonService.send(sender, MessageKey.UNKNOWN_USER);
return;
} else if (!validationService.isEmailFreeForRegistration(playerEmail, sender)) {
commonService.send(sender, MessageKey.EMAIL_ALREADY_USED_ERROR);
return;
}
// Set the email address
auth.setEmail(playerEmail);
if (!dataSource.updateEmail(auth)) {
commonService.send(sender, MessageKey.ERROR);
return;
}
// Update the player cache
if (playerCache.getAuth(playerName) != null) {
playerCache.updatePlayer(auth);
}
// Show a status message
commonService.send(sender, MessageKey.EMAIL_CHANGED_SUCCESS);
});
}
}

View File

@ -0,0 +1,23 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.settings.SpawnLoader;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
public class SetFirstSpawnCommand extends PlayerCommand {
@Inject
private SpawnLoader spawnLoader;
@Override
public void runCommand(Player player, List<String> arguments) {
if (spawnLoader.setFirstSpawn(player.getLocation())) {
player.sendMessage("[AuthMe] Correctly defined new first spawn point");
} else {
player.sendMessage("[AuthMe] SetFirstSpawn has failed, please retry");
}
}
}

View File

@ -0,0 +1,23 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.settings.SpawnLoader;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
public class SetSpawnCommand extends PlayerCommand {
@Inject
private SpawnLoader spawnLoader;
@Override
public void runCommand(Player player, List<String> arguments) {
if (spawnLoader.setSpawn(player.getLocation())) {
player.sendMessage("[AuthMe] Correctly defined new spawn point");
} else {
player.sendMessage("[AuthMe] SetSpawn has failed, please retry");
}
}
}

View File

@ -0,0 +1,27 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.settings.SpawnLoader;
import fr.xephi.authme.util.TeleportUtils;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
public class SpawnCommand extends PlayerCommand {
@Inject
private SpawnLoader spawnLoader;
@Inject
private BukkitService bukkitService;
@Override
public void runCommand(Player player, List<String> arguments) {
if (spawnLoader.getSpawn() == null) {
player.sendMessage("[AuthMe] Spawn has failed, please try to define the spawn");
} else {
bukkitService.runTaskIfFolia(player, () -> TeleportUtils.teleport(player, spawnLoader.getSpawn()));
}
}
}

View File

@ -0,0 +1,52 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.command.CommandMapper;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.command.FoundCommandResult;
import fr.xephi.authme.command.help.HelpProvider;
import fr.xephi.authme.service.AntiBotService;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.Arrays;
import java.util.List;
/**
* Display or change the status of the antibot mod.
*/
public class SwitchAntiBotCommand implements ExecutableCommand {
@Inject
private AntiBotService antiBotService;
@Inject
private CommandMapper commandMapper;
@Inject
private HelpProvider helpProvider;
@Override
public void executeCommand(final CommandSender sender, List<String> arguments) {
if (arguments.isEmpty()) {
sender.sendMessage("[AuthMe] AntiBot status: " + antiBotService.getAntiBotStatus().name());
return;
}
String newState = arguments.get(0);
// Enable or disable the mod
if ("ON".equalsIgnoreCase(newState)) {
antiBotService.overrideAntiBotStatus(true);
sender.sendMessage("[AuthMe] AntiBot Manual Override: enabled!");
} else if ("OFF".equalsIgnoreCase(newState)) {
antiBotService.overrideAntiBotStatus(false);
sender.sendMessage("[AuthMe] AntiBot Manual Override: disabled!");
} else {
sender.sendMessage(ChatColor.DARK_RED + "Invalid AntiBot mode!");
FoundCommandResult result = commandMapper.mapPartsToCommand(sender, Arrays.asList("authme", "antibot"));
helpProvider.outputHelp(sender, result, HelpProvider.SHOW_ARGUMENTS);
sender.sendMessage(ChatColor.GOLD + "Detailed help: " + ChatColor.WHITE + "/authme help antibot");
}
}
}

View File

@ -0,0 +1,61 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.service.BukkitService;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Command to disable two-factor authentication for a user.
*/
public class TotpDisableAdminCommand implements ExecutableCommand {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(TotpDisableAdminCommand.class);
@Inject
private DataSource dataSource;
@Inject
private Messages messages;
@Inject
private BukkitService bukkitService;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
String player = arguments.get(0);
PlayerAuth auth = dataSource.getAuth(player);
if (auth == null) {
messages.send(sender, MessageKey.UNKNOWN_USER);
} else if (auth.getTotpKey() == null) {
sender.sendMessage(ChatColor.RED + "Player '" + player + "' does not have two-factor auth enabled");
} else {
removeTotpKey(sender, player);
}
}
private void removeTotpKey(CommandSender sender, String player) {
if (dataSource.removeTotpKey(player)) {
sender.sendMessage("Disabled two-factor authentication successfully for '" + player + "'");
logger.info(sender.getName() + " disable two-factor authentication for '" + player + "'");
Player onlinePlayer = bukkitService.getPlayerExact(player);
if (onlinePlayer != null) {
messages.send(onlinePlayer, MessageKey.TWO_FACTOR_REMOVED_SUCCESS);
}
} else {
messages.send(sender, MessageKey.ERROR);
}
}
}

View File

@ -0,0 +1,38 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.message.Messages;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.List;
/**
* Command to see whether a user has enabled two-factor authentication.
*/
public class TotpViewStatusCommand implements ExecutableCommand {
@Inject
private DataSource dataSource;
@Inject
private Messages messages;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
String player = arguments.get(0);
PlayerAuth auth = dataSource.getAuth(player);
if (auth == null) {
messages.send(sender, MessageKey.UNKNOWN_USER);
} else if (auth.getTotpKey() == null) {
sender.sendMessage(ChatColor.RED + "Player '" + player + "' does NOT have two-factor auth enabled");
} else {
sender.sendMessage(ChatColor.DARK_GREEN + "Player '" + player + "' has enabled two-factor authentication");
}
}
}

View File

@ -0,0 +1,49 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.process.Management;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.CommonService;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Admin command to unregister a player.
*/
public class UnregisterAdminCommand implements ExecutableCommand {
@Inject
private DataSource dataSource;
@Inject
private CommonService commonService;
@Inject
private BukkitService bukkitService;
@Inject
private Management management;
UnregisterAdminCommand() {
}
@Override
public void executeCommand(final CommandSender sender, List<String> arguments) {
String playerName = arguments.get(0);
// Make sure the user exists
if (!dataSource.isAuthAvailable(playerName)) {
commonService.send(sender, MessageKey.UNKNOWN_USER);
return;
}
// Get the player from the server and perform unregister
Player target = bukkitService.getPlayerExact(playerName);
management.performUnregisterByAdmin(sender, playerName, target);
}
}

View File

@ -0,0 +1,39 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.command.help.HelpMessagesService;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.service.HelpTranslationGenerator;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.util.List;
/**
* Messages command, updates the user's help messages file with any missing files
* from the provided file in the JAR.
*/
public class UpdateHelpMessagesCommand implements ExecutableCommand {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(UpdateHelpMessagesCommand.class);
@Inject
private HelpTranslationGenerator helpTranslationGenerator;
@Inject
private HelpMessagesService helpMessagesService;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
try {
File updatedFile = helpTranslationGenerator.updateHelpFile();
sender.sendMessage("Successfully updated the help file '" + updatedFile.getName() + "'");
helpMessagesService.reloadMessagesFile();
} catch (IOException e) {
sender.sendMessage("Could not update help file: " + e.getMessage());
logger.logException("Could not update help file:", e);
}
}
}

View File

@ -0,0 +1,99 @@
package fr.xephi.authme.command.executable.authme;
import fr.xephi.authme.AuthMe;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.List;
public class VersionCommand implements ExecutableCommand {
@Inject
private BukkitService bukkitService;
@Inject
private Settings settings;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
// Show some version info
sender.sendMessage(ChatColor.GOLD + "==========[ " + AuthMe.getPluginName() + " ABOUT ]==========");
sender.sendMessage(ChatColor.GOLD + "Version: " + ChatColor.WHITE + AuthMe.getPluginName()
+ " v" + AuthMe.getPluginVersion() + ChatColor.GRAY + " (build: " + AuthMe.getPluginBuildNumber() + ")");
sender.sendMessage(ChatColor.GOLD + "Database Implementation: " + ChatColor.WHITE + settings.getProperty(DatabaseSettings.BACKEND).toString());
sender.sendMessage(ChatColor.GOLD + "Authors:");
Collection<Player> onlinePlayers = bukkitService.getOnlinePlayers();
printDeveloper(sender, "Gabriele C.", "sgdc3", "Project manager, Contributor", onlinePlayers);
printDeveloper(sender, "Lucas J.", "ljacqu", "Main Developer", onlinePlayers);
printDeveloper(sender, "games647", "games647", "Developer", onlinePlayers);
printDeveloper(sender, "Hex3l", "Hex3l", "Developer", onlinePlayers);
printDeveloper(sender, "krusic22", "krusic22", "Support", onlinePlayers);
sender.sendMessage(ChatColor.GOLD + "Retired authors:");
printDeveloper(sender, "Alexandre Vanhecke", "xephi59", "Original Author", onlinePlayers);
printDeveloper(sender, "Gnat008", "gnat008", "Developer, Retired", onlinePlayers);
printDeveloper(sender, "DNx5", "DNx5", "Developer, Retired", onlinePlayers);
printDeveloper(sender, "Tim Visee", "timvisee", "Developer, Retired", onlinePlayers);
sender.sendMessage(ChatColor.GOLD + "Website: " + ChatColor.WHITE
+ "https://github.com/AuthMe/AuthMeReloaded");
sender.sendMessage(ChatColor.GOLD + "License: " + ChatColor.WHITE + "GNU GPL v3.0"
+ ChatColor.GRAY + ChatColor.ITALIC + " (See LICENSE file)");
sender.sendMessage(ChatColor.GOLD + "Copyright: " + ChatColor.WHITE
+ "Copyright (c) AuthMe-Team " + new SimpleDateFormat("yyyy").format(new Date())
+ ". Released under GPL v3 License.");
}
/**
* Print a developer with proper styling.
*
* @param sender The command sender
* @param name The display name of the developer
* @param minecraftName The Minecraft username of the developer, if available
* @param function The function of the developer
* @param onlinePlayers The list of online players
*/
private static void printDeveloper(CommandSender sender, String name, String minecraftName, String function,
Collection<Player> onlinePlayers) {
// Print the name
StringBuilder msg = new StringBuilder();
msg.append(" ")
.append(ChatColor.WHITE)
.append(name);
// Append the Minecraft name
msg.append(ChatColor.GRAY).append(" // ").append(ChatColor.WHITE).append(minecraftName);
msg.append(ChatColor.GRAY).append(ChatColor.ITALIC).append(" (").append(function).append(")");
// Show the online status
if (isPlayerOnline(minecraftName, onlinePlayers)) {
msg.append(ChatColor.GREEN).append(ChatColor.ITALIC).append(" (In-Game)");
}
// Print the message
sender.sendMessage(msg.toString());
}
/**
* Check whether a player is online.
*
* @param minecraftName The Minecraft player name
* @param onlinePlayers List of online players
*
* @return True if the player is online, false otherwise
*/
private static boolean isPlayerOnline(String minecraftName, Collection<Player> onlinePlayers) {
for (Player player : onlinePlayers) {
if (player.getName().equalsIgnoreCase(minecraftName)) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,88 @@
package fr.xephi.authme.command.executable.authme.debug;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.permission.DebugSectionPermissions;
import fr.xephi.authme.permission.PermissionNode;
import fr.xephi.authme.service.GeoIpService;
import fr.xephi.authme.service.ValidationService;
import fr.xephi.authme.settings.properties.ProtectionSettings;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.List;
import java.util.regex.Pattern;
/**
* Shows the GeoIP information as returned by the geoIpService.
*/
class CountryLookup implements DebugSection {
private static final Pattern IS_IP_ADDR = Pattern.compile("(\\d{1,3}\\.){3}\\d{1,3}");
@Inject
private GeoIpService geoIpService;
@Inject
private DataSource dataSource;
@Inject
private ValidationService validationService;
@Override
public String getName() {
return "cty";
}
@Override
public String getDescription() {
return "Check country protection / country data";
}
@Override
public void execute(CommandSender sender, List<String> arguments) {
sender.sendMessage(ChatColor.BLUE + "AuthMe country lookup");
if (arguments.isEmpty()) {
sender.sendMessage("Check player: /authme debug cty Bobby");
sender.sendMessage("Check IP address: /authme debug cty 127.123.45.67");
return;
}
String argument = arguments.get(0);
if (IS_IP_ADDR.matcher(argument).matches()) {
outputInfoForIpAddr(sender, argument);
} else {
outputInfoForPlayer(sender, argument);
}
}
@Override
public PermissionNode getRequiredPermission() {
return DebugSectionPermissions.COUNTRY_LOOKUP;
}
private void outputInfoForIpAddr(CommandSender sender, String ipAddr) {
sender.sendMessage("IP '" + ipAddr + "' maps to country '" + geoIpService.getCountryCode(ipAddr)
+ "' (" + geoIpService.getCountryName(ipAddr) + ")");
if (validationService.isCountryAdmitted(ipAddr)) {
sender.sendMessage(ChatColor.DARK_GREEN + "This IP address' country is not blocked");
} else {
sender.sendMessage(ChatColor.DARK_RED + "This IP address' country is blocked from the server");
}
sender.sendMessage("Note: if " + ProtectionSettings.ENABLE_PROTECTION + " is false no country is blocked");
}
// TODO #1366: Extend with registration IP?
private void outputInfoForPlayer(CommandSender sender, String name) {
PlayerAuth auth = dataSource.getAuth(name);
if (auth == null) {
sender.sendMessage("No player with name '" + name + "'");
} else if (auth.getLastIp() == null) {
sender.sendMessage("No last IP address known for '" + name + "'");
} else {
sender.sendMessage("Player '" + name + "' has IP address " + auth.getLastIp());
outputInfoForIpAddr(sender, auth.getLastIp());
}
}
}

View File

@ -0,0 +1,81 @@
package fr.xephi.authme.command.executable.authme.debug;
import ch.jalu.injector.factory.SingletonStore;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.datasource.CacheDataSource;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.initialization.HasCleanup;
import fr.xephi.authme.initialization.Reloadable;
import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.permission.DebugSectionPermissions;
import fr.xephi.authme.permission.PermissionNode;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.List;
import java.util.Map;
import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.applyToLimboPlayersMap;
/**
* Fetches various statistics, particularly regarding in-memory data that is stored.
*/
class DataStatistics implements DebugSection {
@Inject
private PlayerCache playerCache;
@Inject
private LimboService limboService;
@Inject
private DataSource dataSource;
@Inject
private SingletonStore<Object> singletonStore;
@Override
public String getName() {
return "stats";
}
@Override
public String getDescription() {
return "Outputs general data statistics";
}
@Override
public void execute(CommandSender sender, List<String> arguments) {
sender.sendMessage(ChatColor.BLUE + "AuthMe statistics");
sender.sendMessage("LimboPlayers in memory: " + applyToLimboPlayersMap(limboService, Map::size));
sender.sendMessage("PlayerCache size: " + playerCache.getLogged() + " (= logged in players)");
outputDatabaseStats(sender);
outputInjectorStats(sender);
sender.sendMessage("Total logger instances: " + ConsoleLoggerFactory.getTotalLoggers());
}
@Override
public PermissionNode getRequiredPermission() {
return DebugSectionPermissions.DATA_STATISTICS;
}
private void outputDatabaseStats(CommandSender sender) {
sender.sendMessage("Total players in DB: " + dataSource.getAccountsRegistered());
if (dataSource instanceof CacheDataSource) {
CacheDataSource cacheDataSource = (CacheDataSource) this.dataSource;
sender.sendMessage("Cached PlayerAuth objects: " + cacheDataSource.getCachedAuths().size());
}
}
private void outputInjectorStats(CommandSender sender) {
sender.sendMessage("Singleton Java classes: " + singletonStore.retrieveAllOfType().size());
sender.sendMessage(String.format("(Reloadable: %d / SettingsDependent: %d / HasCleanup: %d)",
singletonStore.retrieveAllOfType(Reloadable.class).size(),
singletonStore.retrieveAllOfType(SettingsDependent.class).size(),
singletonStore.retrieveAllOfType(HasCleanup.class).size()));
}
}

View File

@ -0,0 +1,85 @@
package fr.xephi.authme.command.executable.authme.debug;
import ch.jalu.injector.factory.Factory;
import com.google.common.collect.ImmutableSet;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.permission.PermissionsManager;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* Debug command main.
*/
public class DebugCommand implements ExecutableCommand {
private static final Set<Class<? extends DebugSection>> SECTION_CLASSES = ImmutableSet.of(
PermissionGroups.class, DataStatistics.class, CountryLookup.class, PlayerAuthViewer.class, InputValidator.class,
LimboPlayerViewer.class, CountryLookup.class, HasPermissionChecker.class, TestEmailSender.class,
SpawnLocationViewer.class, MySqlDefaultChanger.class);
@Inject
private Factory<DebugSection> debugSectionFactory;
@Inject
private PermissionsManager permissionsManager;
private Map<String, DebugSection> sections;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
DebugSection debugSection = findDebugSection(arguments);
if (debugSection == null) {
sendAvailableSections(sender);
} else {
executeSection(debugSection, sender, arguments);
}
}
private DebugSection findDebugSection(List<String> arguments) {
if (arguments.isEmpty()) {
return null;
}
return getSections().get(arguments.get(0).toLowerCase(Locale.ROOT));
}
private void sendAvailableSections(CommandSender sender) {
sender.sendMessage(ChatColor.BLUE + "AuthMe debug utils");
sender.sendMessage("Sections available to you:");
long availableSections = getSections().values().stream()
.filter(section -> permissionsManager.hasPermission(sender, section.getRequiredPermission()))
.peek(e -> sender.sendMessage("- " + e.getName() + ": " + e.getDescription()))
.count();
if (availableSections == 0) {
sender.sendMessage(ChatColor.RED + "You don't have permission to view any debug section");
}
}
private void executeSection(DebugSection section, CommandSender sender, List<String> arguments) {
if (permissionsManager.hasPermission(sender, section.getRequiredPermission())) {
section.execute(sender, arguments.subList(1, arguments.size()));
} else {
sender.sendMessage(ChatColor.RED + "You don't have permission for this section. See /authme debug");
}
}
// Lazy getter
private Map<String, DebugSection> getSections() {
if (sections == null) {
Map<String, DebugSection> sections = new TreeMap<>();
for (Class<? extends DebugSection> sectionClass : SECTION_CLASSES) {
DebugSection section = debugSectionFactory.newInstance(sectionClass);
sections.put(section.getName(), section);
}
this.sections = sections;
}
return sections;
}
}

View File

@ -0,0 +1,36 @@
package fr.xephi.authme.command.executable.authme.debug;
import fr.xephi.authme.permission.PermissionNode;
import org.bukkit.command.CommandSender;
import java.util.List;
/**
* A debug section: "child" command of the debug command.
*/
interface DebugSection {
/**
* @return the name to get to this child command
*/
String getName();
/**
* @return short description of the child command
*/
String getDescription();
/**
* Executes the debug child command.
*
* @param sender the sender executing the command
* @param arguments the arguments, without the label of the child command
*/
void execute(CommandSender sender, List<String> arguments);
/**
* @return permission required to run this section
*/
PermissionNode getRequiredPermission();
}

View File

@ -0,0 +1,130 @@
package fr.xephi.authme.command.executable.authme.debug;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.datasource.CacheDataSource;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import org.bukkit.Location;
import java.lang.reflect.Field;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
import java.util.Map;
import java.util.function.Function;
/**
* Utilities used within the DebugSection implementations.
*/
final class DebugSectionUtils {
private static ConsoleLogger logger = ConsoleLoggerFactory.get(DebugSectionUtils.class);
private static Field limboEntriesField;
private DebugSectionUtils() {
}
/**
* Formats the given location in a human readable way. Null-safe.
*
* @param location the location to format
* @return the formatted location
*/
static String formatLocation(Location location) {
if (location == null) {
return "null";
}
String worldName = location.getWorld() == null ? "null" : location.getWorld().getName();
return formatLocation(location.getX(), location.getY(), location.getZ(), worldName);
}
/**
* Formats the given location in a human readable way.
*
* @param x the x coordinate
* @param y the y coordinate
* @param z the z coordinate
* @param world the world name
* @return the formatted location
*/
static String formatLocation(double x, double y, double z, String world) {
return "(" + round(x) + ", " + round(y) + ", " + round(z) + ") in '" + world + "'";
}
/**
* Rounds the given number to two decimals.
*
* @param number the number to round
* @return the rounded number
*/
private static String round(double number) {
DecimalFormat df = new DecimalFormat("#.##");
df.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.US));
df.setRoundingMode(RoundingMode.HALF_UP);
return df.format(number);
}
private static Field getLimboPlayerEntriesField() {
if (limboEntriesField == null) {
try {
Field field = LimboService.class.getDeclaredField("entries");
field.setAccessible(true);
limboEntriesField = field;
} catch (Exception e) {
logger.logException("Could not retrieve LimboService entries field:", e);
}
}
return limboEntriesField;
}
/**
* Applies the given function to the map in LimboService containing the LimboPlayers.
* As we don't want to expose this information in non-debug settings, this is done with reflection.
* Exceptions are generously caught and {@code null} is returned on failure.
*
* @param limboService the limbo service instance to get the map from
* @param function the function to apply to the map
* @param <U> the result type of the function
*
* @return the value of the function applied to the map, or null upon error
*/
static <U> U applyToLimboPlayersMap(LimboService limboService, Function<Map, U> function) {
Field limboPlayerEntriesField = getLimboPlayerEntriesField();
if (limboPlayerEntriesField != null) {
try {
return function.apply((Map) limboEntriesField.get(limboService));
} catch (Exception e) {
logger.logException("Could not retrieve LimboService values:", e);
}
}
return null;
}
static <T> T castToTypeOrNull(Object object, Class<T> clazz) {
return clazz.isInstance(object) ? clazz.cast(object) : null;
}
/**
* Unwraps the "cache data source" and returns the underlying source. Returns the
* same as the input argument otherwise.
*
* @param dataSource the data source to unwrap if applicable
* @return the non-cache data source
*/
static DataSource unwrapSourceFromCacheDataSource(DataSource dataSource) {
if (dataSource instanceof CacheDataSource) {
try {
Field source = CacheDataSource.class.getDeclaredField("source");
source.setAccessible(true);
return (DataSource) source.get(dataSource);
} catch (NoSuchFieldException | IllegalAccessException e) {
logger.logException("Could not get source of CacheDataSource:", e);
return null;
}
}
return dataSource;
}
}

View File

@ -0,0 +1,138 @@
package fr.xephi.authme.command.executable.authme.debug;
import com.google.common.collect.ImmutableList;
import fr.xephi.authme.permission.AdminPermission;
import fr.xephi.authme.permission.DebugSectionPermissions;
import fr.xephi.authme.permission.DefaultPermission;
import fr.xephi.authme.permission.PermissionNode;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.permission.PlayerPermission;
import fr.xephi.authme.permission.PlayerStatePermission;
import fr.xephi.authme.service.BukkitService;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.BiFunction;
/**
* Checks if a player has a given permission, as checked by AuthMe.
*/
class HasPermissionChecker implements DebugSection {
static final List<Class<? extends PermissionNode>> PERMISSION_NODE_CLASSES = ImmutableList.of(
AdminPermission.class, PlayerPermission.class, PlayerStatePermission.class, DebugSectionPermissions.class);
@Inject
private PermissionsManager permissionsManager;
@Inject
private BukkitService bukkitService;
@Override
public String getName() {
return "perm";
}
@Override
public String getDescription() {
return "Checks if a player has a given permission";
}
@Override
public void execute(CommandSender sender, List<String> arguments) {
sender.sendMessage(ChatColor.BLUE + "AuthMe permission check");
if (arguments.size() < 2) {
sender.sendMessage("Check if a player has permission:");
sender.sendMessage("Example: /authme debug perm bobby my.perm.node");
sender.sendMessage("Permission system type used: " + permissionsManager.getPermissionSystem());
return;
}
final String playerName = arguments.get(0);
final String permissionNode = arguments.get(1);
Player player = bukkitService.getPlayerExact(playerName);
if (player == null) {
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerName);
if (offlinePlayer == null) {
sender.sendMessage(ChatColor.DARK_RED + "Player '" + playerName + "' does not exist");
} else {
sender.sendMessage("Player '" + playerName + "' not online; checking with offline player");
performPermissionCheck(offlinePlayer, permissionNode, permissionsManager::hasPermissionOffline, sender);
}
} else {
performPermissionCheck(player, permissionNode, permissionsManager::hasPermission, sender);
}
}
@Override
public PermissionNode getRequiredPermission() {
return DebugSectionPermissions.HAS_PERMISSION_CHECK;
}
/**
* Performs a permission check and informs the given sender of the result. {@code permissionChecker} is the
* permission check to perform with the given {@code node} and the {@code player}.
*
* @param player the player to check a permission for
* @param node the node of the permission to check
* @param permissionChecker permission checking function
* @param sender the sender to inform of the result
* @param <P> the player type
*/
private static <P extends OfflinePlayer> void performPermissionCheck(
P player, String node, BiFunction<P, PermissionNode, Boolean> permissionChecker, CommandSender sender) {
PermissionNode permNode = getPermissionNode(sender, node);
if (permissionChecker.apply(player, permNode)) {
sender.sendMessage(ChatColor.DARK_GREEN + "Success: player '" + player.getName()
+ "' has permission '" + node + "'");
} else {
sender.sendMessage(ChatColor.DARK_RED + "Check failed: player '" + player.getName()
+ "' does NOT have permission '" + node + "'");
}
}
/**
* Based on the given permission node (String), tries to find the according AuthMe {@link PermissionNode}
* instance, or creates a new one if not available.
*
* @param sender the sender (used to inform him if no AuthMe PermissionNode can be matched)
* @param node the node to search for
* @return the node as {@link PermissionNode} object
*/
private static PermissionNode getPermissionNode(CommandSender sender, String node) {
Optional<? extends PermissionNode> permNode = PERMISSION_NODE_CLASSES.stream()
.map(Class::getEnumConstants)
.flatMap(Arrays::stream)
.filter(perm -> perm.getNode().equals(node))
.findFirst();
if (permNode.isPresent()) {
return permNode.get();
} else {
sender.sendMessage("Did not detect AuthMe permission; using default permission = DENIED");
return createPermNode(node);
}
}
private static PermissionNode createPermNode(String node) {
return new PermissionNode() {
@Override
public String getNode() {
return node;
}
@Override
public DefaultPermission getDefaultPermission() {
return DefaultPermission.NOT_ALLOWED;
}
};
}
}

View File

@ -0,0 +1,124 @@
package fr.xephi.authme.command.executable.authme.debug;
import fr.xephi.authme.listener.FailedVerificationException;
import fr.xephi.authme.listener.OnJoinVerifier;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.permission.DebugSectionPermissions;
import fr.xephi.authme.permission.PermissionNode;
import fr.xephi.authme.service.ValidationService;
import fr.xephi.authme.service.ValidationService.ValidationResult;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.Arrays;
import java.util.List;
import static fr.xephi.authme.command.executable.authme.debug.InputValidator.ValidationObject.MAIL;
import static fr.xephi.authme.command.executable.authme.debug.InputValidator.ValidationObject.NAME;
import static fr.xephi.authme.command.executable.authme.debug.InputValidator.ValidationObject.PASS;
/**
* Checks if a sample username, email or password is valid according to the AuthMe settings.
*/
class InputValidator implements DebugSection {
@Inject
private ValidationService validationService;
@Inject
private Messages messages;
@Inject
private OnJoinVerifier onJoinVerifier;
@Override
public String getName() {
return "valid";
}
@Override
public String getDescription() {
return "Checks if your config.yml allows a password / email";
}
@Override
public void execute(CommandSender sender, List<String> arguments) {
if (arguments.size() < 2 || !ValidationObject.matchesAny(arguments.get(0))) {
displayUsageHint(sender);
} else if (PASS.matches(arguments.get(0))) {
validatePassword(sender, arguments.get(1));
} else if (MAIL.matches(arguments.get(0))) {
validateEmail(sender, arguments.get(1));
} else if (NAME.matches(arguments.get(0))) {
validateUsername(sender, arguments.get(1));
} else {
throw new IllegalStateException("Unexpected validation object with arg[0] = '" + arguments.get(0) + "'");
}
}
@Override
public PermissionNode getRequiredPermission() {
return DebugSectionPermissions.INPUT_VALIDATOR;
}
private void displayUsageHint(CommandSender sender) {
sender.sendMessage(ChatColor.BLUE + "Validation tests");
sender.sendMessage("You can define forbidden emails and passwords in your config.yml."
+ " You can test your settings with this command.");
final String command = ChatColor.GOLD + "/authme debug valid";
sender.sendMessage(" Use " + command + " pass <pass>" + ChatColor.RESET + " to check a password");
sender.sendMessage(" Use " + command + " mail <mail>" + ChatColor.RESET + " to check an email");
sender.sendMessage(" Use " + command + " name <name>" + ChatColor.RESET + " to check a username");
}
private void validatePassword(CommandSender sender, String password) {
ValidationResult validationResult = validationService.validatePassword(password, "");
sender.sendMessage(ChatColor.BLUE + "Validation of password '" + password + "'");
if (validationResult.hasError()) {
messages.send(sender, validationResult.getMessageKey(), validationResult.getArgs());
} else {
sender.sendMessage(ChatColor.DARK_GREEN + "Valid password!");
}
}
private void validateEmail(CommandSender sender, String email) {
boolean isValidEmail = validationService.validateEmail(email);
sender.sendMessage(ChatColor.BLUE + "Validation of email '" + email + "'");
if (isValidEmail) {
sender.sendMessage(ChatColor.DARK_GREEN + "Valid email!");
} else {
sender.sendMessage(ChatColor.DARK_RED + "Email is not valid!");
}
}
private void validateUsername(CommandSender sender, String username) {
sender.sendMessage(ChatColor.BLUE + "Validation of username '" + username + "'");
try {
onJoinVerifier.checkIsValidName(username);
sender.sendMessage("Valid username!");
} catch (FailedVerificationException failedVerificationEx) {
messages.send(sender, failedVerificationEx.getReason(), failedVerificationEx.getArgs());
}
}
enum ValidationObject {
PASS, MAIL, NAME;
static boolean matchesAny(String arg) {
return Arrays.stream(values()).anyMatch(vo -> vo.matches(arg));
}
boolean matches(String arg) {
return name().equalsIgnoreCase(arg);
}
}
}

View File

@ -0,0 +1,143 @@
package fr.xephi.authme.command.executable.authme.debug;
import fr.xephi.authme.data.limbo.LimboPlayer;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.data.limbo.persistence.LimboPersistence;
import fr.xephi.authme.permission.DebugSectionPermissions;
import fr.xephi.authme.permission.PermissionNode;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.service.BukkitService;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.applyToLimboPlayersMap;
import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.formatLocation;
/**
* Shows the data stored in LimboPlayers and the equivalent properties on online players.
*/
class LimboPlayerViewer implements DebugSection {
@Inject
private LimboService limboService;
@Inject
private LimboPersistence limboPersistence;
@Inject
private BukkitService bukkitService;
@Inject
private PermissionsManager permissionsManager;
@Override
public String getName() {
return "limbo";
}
@Override
public String getDescription() {
return "View LimboPlayers and player's \"limbo stats\"";
}
@Override
public void execute(CommandSender sender, List<String> arguments) {
if (arguments.isEmpty()) {
sender.sendMessage(ChatColor.BLUE + "AuthMe limbo viewer");
sender.sendMessage("/authme debug limbo <player>: show a player's limbo info");
sender.sendMessage("Available limbo records: " + applyToLimboPlayersMap(limboService, Map::keySet));
return;
}
LimboPlayer memoryLimbo = limboService.getLimboPlayer(arguments.get(0));
Player player = bukkitService.getPlayerExact(arguments.get(0));
LimboPlayer diskLimbo = player != null ? limboPersistence.getLimboPlayer(player) : null;
if (memoryLimbo == null && player == null) {
sender.sendMessage(ChatColor.BLUE + "No AuthMe limbo data");
sender.sendMessage("No limbo data and no player online with name '" + arguments.get(0) + "'");
return;
}
sender.sendMessage(ChatColor.BLUE + "Player / limbo / disk limbo info for '" + arguments.get(0) + "'");
new InfoDisplayer(sender, player, memoryLimbo, diskLimbo)
.sendEntry("Is op", Player::isOp, LimboPlayer::isOperator)
.sendEntry("Walk speed", Player::getWalkSpeed, LimboPlayer::getWalkSpeed)
.sendEntry("Can fly", Player::getAllowFlight, LimboPlayer::isCanFly)
.sendEntry("Fly speed", Player::getFlySpeed, LimboPlayer::getFlySpeed)
.sendEntry("Location", p -> formatLocation(p.getLocation()), l -> formatLocation(l.getLocation()))
.sendEntry("Prim. group",
p -> permissionsManager.hasGroupSupport() ? permissionsManager.getPrimaryGroup(p) : "N/A",
LimboPlayer::getGroups);
}
@Override
public PermissionNode getRequiredPermission() {
return DebugSectionPermissions.LIMBO_PLAYER_VIEWER;
}
/**
* Displays the info for the given LimboPlayer and Player to the provided CommandSender.
*/
private static final class InfoDisplayer {
private final CommandSender sender;
private final Optional<Player> player;
private final Optional<LimboPlayer> memoryLimbo;
private final Optional<LimboPlayer> diskLimbo;
/**
* Constructor.
*
* @param sender command sender to send the information to
* @param player the player to get data from
* @param memoryLimbo the limbo player to get data from
*/
InfoDisplayer(CommandSender sender, Player player, LimboPlayer memoryLimbo, LimboPlayer diskLimbo) {
this.sender = sender;
this.player = Optional.ofNullable(player);
this.memoryLimbo = Optional.ofNullable(memoryLimbo);
this.diskLimbo = Optional.ofNullable(diskLimbo);
if (memoryLimbo == null) {
sender.sendMessage("Note: no Limbo information available");
}
if (player == null) {
sender.sendMessage("Note: player is not online");
} else if (diskLimbo == null) {
sender.sendMessage("Note: no Limbo on disk available");
}
}
/**
* Displays a piece of information to the command sender.
*
* @param title the designation of the piece of information
* @param playerGetter getter for data retrieval on Player
* @param limboGetter getter for data retrieval on the LimboPlayer
* @param <T> the data type
* @return this instance (for chaining)
*/
<T> InfoDisplayer sendEntry(String title,
Function<Player, T> playerGetter,
Function<LimboPlayer, T> limboGetter) {
sender.sendMessage(
title + ": "
+ getData(player, playerGetter)
+ " / "
+ getData(memoryLimbo, limboGetter)
+ " / "
+ getData(diskLimbo, limboGetter));
return this;
}
static <E, T> String getData(Optional<E> entity, Function<E, T> getter) {
return entity.map(getter).map(String::valueOf).orElse(" -- ");
}
}
}

View File

@ -0,0 +1,327 @@
package fr.xephi.authme.command.executable.authme.debug;
import ch.jalu.configme.properties.Property;
import com.google.common.annotations.VisibleForTesting;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.datasource.MySQL;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.permission.DebugSectionPermissions;
import fr.xephi.authme.permission.PermissionNode;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.castToTypeOrNull;
import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.unwrapSourceFromCacheDataSource;
import static fr.xephi.authme.data.auth.PlayerAuth.DB_EMAIL_DEFAULT;
import static fr.xephi.authme.data.auth.PlayerAuth.DB_LAST_IP_DEFAULT;
import static fr.xephi.authme.data.auth.PlayerAuth.DB_LAST_LOGIN_DEFAULT;
import static fr.xephi.authme.datasource.SqlDataSourceUtils.getColumnDefaultValue;
import static fr.xephi.authme.datasource.SqlDataSourceUtils.isNotNullColumn;
import static java.lang.String.format;
/**
* Convenience command to add or remove the default value of a column and its nullable status
* in the MySQL data source.
*/
class MySqlDefaultChanger implements DebugSection {
private static final String NOT_NULL_SUFFIX = ChatColor.DARK_AQUA + "@" + ChatColor.RESET;
private static final String DEFAULT_VALUE_SUFFIX = ChatColor.GOLD + "#" + ChatColor.RESET;
private ConsoleLogger logger = ConsoleLoggerFactory.get(MySqlDefaultChanger.class);
@Inject
private Settings settings;
@Inject
private DataSource dataSource;
private MySQL mySql;
@PostConstruct
void setMySqlField() {
this.mySql = castToTypeOrNull(unwrapSourceFromCacheDataSource(this.dataSource), MySQL.class);
}
@Override
public String getName() {
return "mysqldef";
}
@Override
public String getDescription() {
return "Add or remove the default value of MySQL columns";
}
@Override
public PermissionNode getRequiredPermission() {
return DebugSectionPermissions.MYSQL_DEFAULT_CHANGER;
}
@Override
public void execute(CommandSender sender, List<String> arguments) {
if (mySql == null) {
sender.sendMessage("Defaults can be changed for the MySQL data source only.");
return;
}
Operation operation = matchToEnum(arguments, 0, Operation.class);
Columns column = matchToEnum(arguments, 1, Columns.class);
if (operation == Operation.DETAILS) {
showColumnDetails(sender);
} else if (operation == null || column == null) {
displayUsageHints(sender);
} else {
sender.sendMessage(ChatColor.BLUE + "[AuthMe] MySQL change '" + column + "'");
try (Connection con = getConnection(mySql)) {
switch (operation) {
case ADD:
changeColumnToNotNullWithDefault(sender, column, con);
break;
case REMOVE:
removeNotNullAndDefault(sender, column, con);
break;
default:
throw new IllegalStateException("Unknown operation '" + operation + "'");
}
} catch (SQLException | IllegalStateException e) {
logger.logException("Failed to perform MySQL default altering operation:", e);
}
}
}
/**
* Adds a default value to the column definition and adds a {@code NOT NULL} constraint for
* the specified column.
*
* @param sender the command sender initiation the action
* @param column the column to modify
* @param con connection to the database
* @throws SQLException .
*/
private void changeColumnToNotNullWithDefault(CommandSender sender, Columns column,
Connection con) throws SQLException {
final String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE);
final String columnName = settings.getProperty(column.getColumnNameProperty());
// Replace NULLs with future default value
String sql = format("UPDATE %s SET %s = ? WHERE %s IS NULL;", tableName, columnName, columnName);
int updatedRows;
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setObject(1, column.getDefaultValue());
updatedRows = pst.executeUpdate();
}
sender.sendMessage("Replaced NULLs with default value ('" + column.getDefaultValue()
+ "'), modifying " + updatedRows + " entries");
// Change column definition to NOT NULL version
try (Statement st = con.createStatement()) {
st.execute(format("ALTER TABLE %s MODIFY %s %s", tableName, columnName, column.getNotNullDefinition()));
sender.sendMessage("Changed column '" + columnName + "' to have NOT NULL constraint");
}
// Log success message
logger.info("Changed MySQL column '" + columnName + "' to be NOT NULL, as initiated by '"
+ sender.getName() + "'");
}
/**
* Removes the {@code NOT NULL} constraint of a column definition and replaces rows with the
* default value to {@code NULL}.
*
* @param sender the command sender initiation the action
* @param column the column to modify
* @param con connection to the database
* @throws SQLException .
*/
private void removeNotNullAndDefault(CommandSender sender, Columns column, Connection con) throws SQLException {
final String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE);
final String columnName = settings.getProperty(column.getColumnNameProperty());
// Change column definition to nullable version
try (Statement st = con.createStatement()) {
st.execute(format("ALTER TABLE %s MODIFY %s %s", tableName, columnName, column.getNullableDefinition()));
sender.sendMessage("Changed column '" + columnName + "' to allow nulls");
}
// Replace old default value with NULL
String sql = format("UPDATE %s SET %s = NULL WHERE %s = ?;", tableName, columnName, columnName);
int updatedRows;
try (PreparedStatement pst = con.prepareStatement(sql)) {
pst.setObject(1, column.getDefaultValue());
updatedRows = pst.executeUpdate();
}
sender.sendMessage("Replaced default value ('" + column.getDefaultValue()
+ "') to be NULL, modifying " + updatedRows + " entries");
// Log success message
logger.info("Changed MySQL column '" + columnName + "' to allow NULL, as initiated by '"
+ sender.getName() + "'");
}
/**
* Outputs the current definitions of all {@link Columns} which can be migrated.
*
* @param sender command sender to output the data to
*/
private void showColumnDetails(CommandSender sender) {
sender.sendMessage(ChatColor.BLUE + "MySQL column details");
final String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE);
try (Connection con = getConnection(mySql)) {
final DatabaseMetaData metaData = con.getMetaData();
for (Columns col : Columns.values()) {
String columnName = settings.getProperty(col.getColumnNameProperty());
String isNullText = isNotNullColumn(metaData, tableName, columnName) ? "NOT NULL" : "nullable";
Object defaultValue = getColumnDefaultValue(metaData, tableName, columnName);
String defaultText = defaultValue == null ? "no default" : "default: '" + defaultValue + "'";
sender.sendMessage(formatColumnWithMetadata(col, metaData, tableName)
+ " (" + columnName + "): " + isNullText + ", " + defaultText);
}
} catch (SQLException e) {
logger.logException("Failed while showing column details:", e);
sender.sendMessage("Failed while showing column details. See log for info");
}
}
/**
* Displays sample commands and the list of columns that can be changed.
*
* @param sender the sender issuing the command
*/
private void displayUsageHints(CommandSender sender) {
sender.sendMessage(ChatColor.BLUE + "MySQL column changer");
sender.sendMessage("Adds or removes a NOT NULL constraint for a column.");
sender.sendMessage("Examples: add a NOT NULL constraint with");
sender.sendMessage(" /authme debug mysqldef add <column>");
sender.sendMessage("Remove one with /authme debug mysqldef remove <column>");
sender.sendMessage("Available columns: " + constructColumnListWithMetadata());
sender.sendMessage(" " + NOT_NULL_SUFFIX + ": not-null, " + DEFAULT_VALUE_SUFFIX
+ ": has default. See /authme debug mysqldef details");
}
/**
* @return list of {@link Columns} we can toggle with suffixes indicating their NOT NULL and default value status
*/
private String constructColumnListWithMetadata() {
try (Connection con = getConnection(mySql)) {
final DatabaseMetaData metaData = con.getMetaData();
final String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE);
List<String> formattedColumns = new ArrayList<>(Columns.values().length);
for (Columns col : Columns.values()) {
formattedColumns.add(formatColumnWithMetadata(col, metaData, tableName));
}
return String.join(ChatColor.RESET + ", ", formattedColumns);
} catch (SQLException e) {
logger.logException("Failed to construct column list:", e);
return ChatColor.RED + "An error occurred! Please see the console for details.";
}
}
private String formatColumnWithMetadata(Columns column, DatabaseMetaData metaData,
String tableName) throws SQLException {
String columnName = settings.getProperty(column.getColumnNameProperty());
boolean isNotNull = isNotNullColumn(metaData, tableName, columnName);
boolean hasDefaultValue = getColumnDefaultValue(metaData, tableName, columnName) != null;
return column.name()
+ (isNotNull ? NOT_NULL_SUFFIX : "")
+ (hasDefaultValue ? DEFAULT_VALUE_SUFFIX : "");
}
/**
* Gets the Connection object from the MySQL data source.
*
* @param mySql the MySQL data source to get the connection from
* @return the connection
*/
@VisibleForTesting
Connection getConnection(MySQL mySql) {
try {
Method method = MySQL.class.getDeclaredMethod("getConnection");
method.setAccessible(true);
return (Connection) method.invoke(mySql);
} catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
throw new IllegalStateException("Could not get MySQL connection", e);
}
}
private static <E extends Enum<E>> E matchToEnum(List<String> arguments, int index, Class<E> clazz) {
if (arguments.size() <= index) {
return null;
}
String str = arguments.get(index);
return Arrays.stream(clazz.getEnumConstants())
.filter(e -> e.name().equalsIgnoreCase(str))
.findFirst().orElse(null);
}
private enum Operation {
ADD, REMOVE, DETAILS
}
/** MySQL columns which can be toggled between being NOT NULL and allowing NULL values. */
enum Columns {
LASTLOGIN(DatabaseSettings.MYSQL_COL_LASTLOGIN,
"BIGINT", "BIGINT NOT NULL DEFAULT 0", DB_LAST_LOGIN_DEFAULT),
LASTIP(DatabaseSettings.MYSQL_COL_LAST_IP,
"VARCHAR(40) CHARACTER SET ascii COLLATE ascii_bin",
"VARCHAR(40) CHARACTER SET ascii COLLATE ascii_bin NOT NULL DEFAULT '127.0.0.1'",
DB_LAST_IP_DEFAULT),
EMAIL(DatabaseSettings.MYSQL_COL_EMAIL,
"VARCHAR(255)", "VARCHAR(255) NOT NULL DEFAULT 'your@email.com'", DB_EMAIL_DEFAULT);
private final Property<String> columnNameProperty;
private final String nullableDefinition;
private final String notNullDefinition;
private final Object defaultValue;
Columns(Property<String> columnNameProperty, String nullableDefinition,
String notNullDefinition, Object defaultValue) {
this.columnNameProperty = columnNameProperty;
this.nullableDefinition = nullableDefinition;
this.notNullDefinition = notNullDefinition;
this.defaultValue = defaultValue;
}
/** @return property defining the column name in the database */
Property<String> getColumnNameProperty() {
return columnNameProperty;
}
/** @return SQL definition of the column allowing NULL values */
String getNullableDefinition() {
return nullableDefinition;
}
/** @return SQL definition of the column with a NOT NULL constraint */
String getNotNullDefinition() {
return notNullDefinition;
}
/** @return the default value used in {@link #notNullDefinition} */
Object getDefaultValue() {
return defaultValue;
}
}
}

View File

@ -0,0 +1,56 @@
package fr.xephi.authme.command.executable.authme.debug;
import fr.xephi.authme.data.limbo.UserGroup;
import fr.xephi.authme.permission.DebugSectionPermissions;
import fr.xephi.authme.permission.PermissionNode;
import fr.xephi.authme.permission.PermissionsManager;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
import static java.util.stream.Collectors.toList;
/**
* Outputs the permission groups of a player.
*/
class PermissionGroups implements DebugSection {
@Inject
private PermissionsManager permissionsManager;
@Override
public String getName() {
return "groups";
}
@Override
public String getDescription() {
return "Show permission groups a player belongs to";
}
@Override
public void execute(CommandSender sender, List<String> arguments) {
sender.sendMessage(ChatColor.BLUE + "AuthMe permission groups");
String name = arguments.isEmpty() ? sender.getName() : arguments.get(0);
Player player = Bukkit.getPlayer(name);
if (player == null) {
sender.sendMessage("Player " + name + " could not be found");
} else {
List<String> groupNames = permissionsManager.getGroups(player).stream()
.map(UserGroup::getGroupName)
.collect(toList());
sender.sendMessage("Player " + name + " has permission groups: " + String.join(", ", groupNames));
sender.sendMessage("Primary group is: " + permissionsManager.getGroups(player));
}
}
@Override
public PermissionNode getRequiredPermission() {
return DebugSectionPermissions.PERM_GROUPS;
}
}

View File

@ -0,0 +1,117 @@
package fr.xephi.authme.command.executable.authme.debug;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.permission.DebugSectionPermissions;
import fr.xephi.authme.permission.PermissionNode;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.util.StringUtils;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.formatLocation;
/**
* Allows to view the data of a PlayerAuth in the database.
*/
class PlayerAuthViewer implements DebugSection {
@Inject
private DataSource dataSource;
@Override
public String getName() {
return "db";
}
@Override
public String getDescription() {
return "View player's data in the database";
}
@Override
public void execute(CommandSender sender, List<String> arguments) {
if (arguments.isEmpty()) {
sender.sendMessage(ChatColor.BLUE + "AuthMe database viewer");
sender.sendMessage("Enter player name to view his data in the database.");
sender.sendMessage("Example: /authme debug db Bobby");
return;
}
PlayerAuth auth = dataSource.getAuth(arguments.get(0));
if (auth == null) {
sender.sendMessage(ChatColor.BLUE + "AuthMe database viewer");
sender.sendMessage("No record exists for '" + arguments.get(0) + "'");
} else {
displayAuthToSender(auth, sender);
}
}
@Override
public PermissionNode getRequiredPermission() {
return DebugSectionPermissions.PLAYER_AUTH_VIEWER;
}
/**
* Outputs the PlayerAuth information to the given sender.
*
* @param auth the PlayerAuth to display
* @param sender the sender to send the messages to
*/
private void displayAuthToSender(PlayerAuth auth, CommandSender sender) {
sender.sendMessage(ChatColor.BLUE + "[AuthMe] Player " + auth.getNickname() + " / " + auth.getRealName());
sender.sendMessage("Email: " + auth.getEmail() + ". IP: " + auth.getLastIp() + ". Group: " + auth.getGroupId());
sender.sendMessage("Quit location: "
+ formatLocation(auth.getQuitLocX(), auth.getQuitLocY(), auth.getQuitLocZ(), auth.getWorld()));
sender.sendMessage("Last login: " + formatDate(auth.getLastLogin()));
sender.sendMessage("Registration: " + formatDate(auth.getRegistrationDate())
+ " with IP " + auth.getRegistrationIp());
HashedPassword hashedPass = auth.getPassword();
sender.sendMessage("Hash / salt (partial): '" + safeSubstring(hashedPass.getHash(), 6)
+ "' / '" + safeSubstring(hashedPass.getSalt(), 4) + "'");
sender.sendMessage("TOTP code (partial): '" + safeSubstring(auth.getTotpKey(), 3) + "'");
}
/**
* Fail-safe substring method. Guarantees not to show the entire String.
*
* @param str the string to transform
* @param length number of characters to show from the start of the String
* @return the first <code>length</code> characters of the string, or half of the string if it is shorter,
* or empty string if the string is null or empty
*/
private static String safeSubstring(String str, int length) {
if (StringUtils.isBlank(str)) {
return "";
} else if (str.length() < length) {
return str.substring(0, str.length() / 2) + "...";
} else {
return str.substring(0, length) + "...";
}
}
/**
* Formats the given timestamp to a human readable date.
*
* @param timestamp the timestamp to format (nullable)
* @return the formatted timestamp
*/
private static String formatDate(Long timestamp) {
if (timestamp == null) {
return "Not available (null)";
} else if (timestamp == 0) {
return "Not available (0)";
} else {
LocalDateTime date = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault());
return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(date);
}
}
}

View File

@ -0,0 +1,87 @@
package fr.xephi.authme.command.executable.authme.debug;
import fr.xephi.authme.permission.DebugSectionPermissions;
import fr.xephi.authme.permission.PermissionNode;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.SpawnLoader;
import fr.xephi.authme.settings.properties.RestrictionSettings;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.formatLocation;
/**
* Shows the spawn location that AuthMe is configured to use.
*/
class SpawnLocationViewer implements DebugSection {
@Inject
private SpawnLoader spawnLoader;
@Inject
private Settings settings;
@Inject
private BukkitService bukkitService;
@Override
public String getName() {
return "spawn";
}
@Override
public String getDescription() {
return "Shows the spawn location that AuthMe will use";
}
@Override
public void execute(CommandSender sender, List<String> arguments) {
sender.sendMessage(ChatColor.BLUE + "AuthMe spawn location viewer");
if (arguments.isEmpty()) {
showGeneralInfo(sender);
} else if ("?".equals(arguments.get(0))) {
showHelp(sender);
} else {
showPlayerSpawn(sender, arguments.get(0));
}
}
@Override
public PermissionNode getRequiredPermission() {
return DebugSectionPermissions.SPAWN_LOCATION;
}
private void showGeneralInfo(CommandSender sender) {
sender.sendMessage("Spawn priority: "
+ String.join(", ", settings.getProperty(RestrictionSettings.SPAWN_PRIORITY)));
sender.sendMessage("AuthMe spawn location: " + formatLocation(spawnLoader.getSpawn()));
sender.sendMessage("AuthMe first spawn location: " + formatLocation(spawnLoader.getFirstSpawn()));
sender.sendMessage("AuthMe (first)spawn are only used depending on the configured priority!");
sender.sendMessage("Use '/authme debug spawn ?' for further help");
}
private void showHelp(CommandSender sender) {
sender.sendMessage("Use /authme spawn and /authme firstspawn to teleport to the spawns.");
sender.sendMessage("/authme set(first)spawn sets the (first) spawn to your current location.");
sender.sendMessage("Use /authme debug spawn <player> to view where a player would be teleported to.");
sender.sendMessage("Read more at https://github.com/AuthMe/AuthMeReloaded/wiki/Spawn-Handling");
}
private void showPlayerSpawn(CommandSender sender, String playerName) {
Player player = bukkitService.getPlayerExact(playerName);
if (player == null) {
sender.sendMessage("Player '" + playerName + "' is not online!");
} else {
Location spawn = spawnLoader.getSpawnLocation(player);
sender.sendMessage("Player '" + playerName + "' has spawn location: " + formatLocation(spawn));
sender.sendMessage("Note: this check excludes the AuthMe firstspawn.");
}
}
}

View File

@ -0,0 +1,125 @@
package fr.xephi.authme.command.executable.authme.debug;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.mail.SendMailSsl;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.permission.DebugSectionPermissions;
import fr.xephi.authme.permission.PermissionNode;
import fr.xephi.authme.util.StringUtils;
import fr.xephi.authme.util.Utils;
import org.apache.commons.mail.EmailException;
import org.apache.commons.mail.HtmlEmail;
import org.bukkit.ChatColor;
import org.bukkit.Server;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.List;
/**
* Sends out a test email.
*/
class TestEmailSender implements DebugSection {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(TestEmailSender.class);
@Inject
private DataSource dataSource;
@Inject
private SendMailSsl sendMailSsl;
@Inject
private Server server;
@Override
public String getName() {
return "mail";
}
@Override
public String getDescription() {
return "Sends out a test email";
}
@Override
public void execute(CommandSender sender, List<String> arguments) {
sender.sendMessage(ChatColor.BLUE + "AuthMe test email sender");
if (!sendMailSsl.hasAllInformation()) {
sender.sendMessage(ChatColor.RED + "You haven't set all required configurations in config.yml "
+ "for sending emails. Please check your config.yml");
return;
}
String email = getEmail(sender, arguments);
// getEmail() takes care of informing the sender of the error if email == null
if (email != null) {
boolean sendMail = sendTestEmail(email);
if (sendMail) {
sender.sendMessage("Test email sent to " + email + " with success");
} else {
sender.sendMessage(ChatColor.RED + "Failed to send test mail to " + email + "; please check your logs");
}
}
}
@Override
public PermissionNode getRequiredPermission() {
return DebugSectionPermissions.TEST_EMAIL;
}
/**
* Gets the email address to use based on the sender and the arguments. If the arguments are empty,
* we attempt to retrieve the email from the sender. If there is an argument, we verify that it is
* an email address.
* {@code null} is returned if no email address could be found. This method informs the sender of
* the specific error in such cases.
*
* @param sender the command sender
* @param arguments the provided arguments
* @return the email to use, or null if none found
*/
private String getEmail(CommandSender sender, List<String> arguments) {
if (arguments.isEmpty()) {
DataSourceValue<String> emailResult = dataSource.getEmail(sender.getName());
if (!emailResult.rowExists()) {
sender.sendMessage(ChatColor.RED + "Please provide an email address, "
+ "e.g. /authme debug mail test@example.com");
return null;
}
final String email = emailResult.getValue();
if (Utils.isEmailEmpty(email)) {
sender.sendMessage(ChatColor.RED + "No email set for your account!"
+ " Please use /authme debug mail <email>");
return null;
}
return email;
} else {
String email = arguments.get(0);
if (StringUtils.isInsideString('@', email)) {
return email;
}
sender.sendMessage(ChatColor.RED + "Invalid email! Usage: /authme debug mail test@example.com");
return null;
}
}
private boolean sendTestEmail(String email) {
HtmlEmail htmlEmail;
try {
htmlEmail = sendMailSsl.initializeMail(email);
} catch (EmailException e) {
logger.logException("Failed to create email for sample email:", e);
return false;
}
htmlEmail.setSubject("AuthMe test email");
String message = "Hello there!<br />This is a sample email sent to you from a Minecraft server ("
+ server.getName() + ") via /authme debug mail. If you're seeing this, sending emails should be fine.";
return sendMailSsl.sendEmail(message, htmlEmail);
}
}

View File

@ -0,0 +1,86 @@
package fr.xephi.authme.command.executable.captcha;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.data.captcha.LoginCaptchaManager;
import fr.xephi.authme.data.captcha.RegistrationCaptchaManager;
import fr.xephi.authme.data.limbo.LimboMessageType;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.service.CommonService;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Captcha command, allowing a player to solve a captcha.
*/
public class CaptchaCommand extends PlayerCommand {
@Inject
private PlayerCache playerCache;
@Inject
private LoginCaptchaManager loginCaptchaManager;
@Inject
private RegistrationCaptchaManager registrationCaptchaManager;
@Inject
private CommonService commonService;
@Inject
private LimboService limboService;
@Inject
private DataSource dataSource;
@Override
public void runCommand(Player player, List<String> arguments) {
final String name = player.getName();
if (playerCache.isAuthenticated(name)) {
// No captcha is relevant if the player is logged in
commonService.send(player, MessageKey.ALREADY_LOGGED_IN_ERROR);
return;
}
if (loginCaptchaManager.isCaptchaRequired(name)) {
checkLoginCaptcha(player, arguments.get(0));
} else {
final boolean isPlayerRegistered = dataSource.isAuthAvailable(name);
if (!isPlayerRegistered && registrationCaptchaManager.isCaptchaRequired(name)) {
checkRegisterCaptcha(player, arguments.get(0));
} else {
MessageKey errorMessage = isPlayerRegistered ? MessageKey.USAGE_LOGIN : MessageKey.USAGE_REGISTER;
commonService.send(player, errorMessage);
}
}
}
private void checkLoginCaptcha(Player player, String captchaCode) {
final boolean isCorrectCode = loginCaptchaManager.checkCode(player, captchaCode);
if (isCorrectCode) {
commonService.send(player, MessageKey.CAPTCHA_SUCCESS);
commonService.send(player, MessageKey.LOGIN_MESSAGE);
limboService.unmuteMessageTask(player);
} else {
String newCode = loginCaptchaManager.getCaptchaCodeOrGenerateNew(player.getName());
commonService.send(player, MessageKey.CAPTCHA_WRONG_ERROR, newCode);
}
}
private void checkRegisterCaptcha(Player player, String captchaCode) {
final boolean isCorrectCode = registrationCaptchaManager.checkCode(player, captchaCode);
if (isCorrectCode) {
commonService.send(player, MessageKey.REGISTER_CAPTCHA_SUCCESS);
commonService.send(player, MessageKey.REGISTER_MESSAGE);
} else {
String newCode = registrationCaptchaManager.getCaptchaCodeOrGenerateNew(player.getName());
commonService.send(player, MessageKey.CAPTCHA_WRONG_ERROR, newCode);
}
limboService.resetMessageTask(player, LimboMessageType.REGISTER);
}
}

View File

@ -0,0 +1,74 @@
package fr.xephi.authme.command.executable.changepassword;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.VerificationCodeManager;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.process.Management;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.service.ValidationService;
import fr.xephi.authme.service.ValidationService.ValidationResult;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
import java.util.Locale;
/**
* The command for a player to change his password with.
*/
public class ChangePasswordCommand extends PlayerCommand {
@Inject
private CommonService commonService;
@Inject
private PlayerCache playerCache;
@Inject
private ValidationService validationService;
@Inject
private Management management;
@Inject
private VerificationCodeManager codeManager;
@Override
public void runCommand(Player player, List<String> arguments) {
String name = player.getName().toLowerCase(Locale.ROOT);
if (!playerCache.isAuthenticated(name)) {
commonService.send(player, MessageKey.NOT_LOGGED_IN);
return;
}
// Check if the user has been verified or not
if (codeManager.isVerificationRequired(player)) {
codeManager.codeExistOrGenerateNew(name);
commonService.send(player, MessageKey.VERIFICATION_CODE_REQUIRED);
return;
}
String oldPassword = arguments.get(0);
String newPassword = arguments.get(1);
// Make sure the password is allowed
ValidationResult passwordValidation = validationService.validatePassword(newPassword, name);
if (passwordValidation.hasError()) {
commonService.send(player, passwordValidation.getMessageKey(), passwordValidation.getArgs());
return;
}
management.performPasswordChange(player, oldPassword, newPassword);
}
@Override
protected String getAlternativeCommand() {
return "/authme password <playername> <password>";
}
@Override
public MessageKey getArgumentsMismatchMessage() {
return MessageKey.USAGE_CHANGE_PASSWORD;
}
}

View File

@ -0,0 +1,40 @@
package fr.xephi.authme.command.executable.email;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.process.Management;
import fr.xephi.authme.service.CommonService;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Command for setting an email to an account.
*/
public class AddEmailCommand extends PlayerCommand {
@Inject
private Management management;
@Inject
private CommonService commonService;
@Override
public void runCommand(Player player, List<String> arguments) {
String email = arguments.get(0);
String emailConfirmation = arguments.get(1);
if (email.equals(emailConfirmation)) {
// Closer inspection of the mail address handled by the async task
management.performAddEmail(player, email);
} else {
commonService.send(player, MessageKey.CONFIRM_EMAIL_MESSAGE);
}
}
@Override
public MessageKey getArgumentsMismatchMessage() {
return MessageKey.USAGE_ADD_EMAIL;
}
}

View File

@ -0,0 +1,46 @@
package fr.xephi.authme.command.executable.email;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.VerificationCodeManager;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.process.Management;
import fr.xephi.authme.service.CommonService;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Change email command.
*/
public class ChangeEmailCommand extends PlayerCommand {
@Inject
private Management management;
@Inject
private CommonService commonService;
@Inject
private VerificationCodeManager codeManager;
@Override
public void runCommand(Player player, List<String> arguments) {
final String playerName = player.getName();
// Check if the user has been verified or not
if (codeManager.isVerificationRequired(player)) {
codeManager.codeExistOrGenerateNew(playerName);
commonService.send(player, MessageKey.VERIFICATION_CODE_REQUIRED);
return;
}
String playerMailOld = arguments.get(0);
String playerMailNew = arguments.get(1);
management.performChangeEmail(player, playerMailOld, playerMailNew);
}
@Override
public MessageKey getArgumentsMismatchMessage() {
return MessageKey.USAGE_CHANGE_EMAIL;
}
}

View File

@ -0,0 +1,29 @@
package fr.xephi.authme.command.executable.email;
import fr.xephi.authme.command.CommandMapper;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.command.FoundCommandResult;
import fr.xephi.authme.command.help.HelpProvider;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.Collections;
import java.util.List;
/**
* Base command for /email, showing information about the child commands.
*/
public class EmailBaseCommand implements ExecutableCommand {
@Inject
private CommandMapper commandMapper;
@Inject
private HelpProvider helpProvider;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
FoundCommandResult result = commandMapper.mapPartsToCommand(sender, Collections.singletonList("email"));
helpProvider.outputHelp(sender, result, HelpProvider.SHOW_CHILDREN);
}
}

View File

@ -0,0 +1,61 @@
package fr.xephi.authme.command.executable.email;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.security.PasswordSecurity;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.service.PasswordRecoveryService;
import fr.xephi.authme.service.ValidationService;
import fr.xephi.authme.service.ValidationService.ValidationResult;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Command for changing password following successful recovery.
*/
public class EmailSetPasswordCommand extends PlayerCommand {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(EmailSetPasswordCommand.class);
@Inject
private DataSource dataSource;
@Inject
private CommonService commonService;
@Inject
private PasswordRecoveryService recoveryService;
@Inject
private PasswordSecurity passwordSecurity;
@Inject
private ValidationService validationService;
@Override
protected void runCommand(Player player, List<String> arguments) {
if (recoveryService.canChangePassword(player)) {
String name = player.getName();
String password = arguments.get(0);
ValidationResult result = validationService.validatePassword(password, name);
if (!result.hasError()) {
HashedPassword hashedPassword = passwordSecurity.computeHash(password, name);
dataSource.updatePassword(name, hashedPassword);
recoveryService.removeFromSuccessfulRecovery(player);
logger.info("Player '" + name + "' has changed their password from recovery");
commonService.send(player, MessageKey.PASSWORD_CHANGED_SUCCESS);
} else {
commonService.send(player, result.getMessageKey(), result.getArgs());
}
} else {
commonService.send(player, MessageKey.CHANGE_PASSWORD_EXPIRED);
}
}
}

View File

@ -0,0 +1,46 @@
package fr.xephi.authme.command.executable.email;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.service.PasswordRecoveryService;
import fr.xephi.authme.service.RecoveryCodeService;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Command for submitting email recovery code.
*/
public class ProcessCodeCommand extends PlayerCommand {
@Inject
private CommonService commonService;
@Inject
private RecoveryCodeService codeService;
@Inject
private PasswordRecoveryService recoveryService;
@Override
protected void runCommand(Player player, List<String> arguments) {
String name = player.getName();
String code = arguments.get(0);
if (codeService.hasTriesLeft(name)) {
if (codeService.isCodeValid(name, code)) {
commonService.send(player, MessageKey.RECOVERY_CODE_CORRECT);
recoveryService.addSuccessfulRecovery(player);
codeService.removeCode(name);
} else {
commonService.send(player, MessageKey.INCORRECT_RECOVERY_CODE,
Integer.toString(codeService.getTriesLeft(name)));
}
} else {
codeService.removeCode(name);
commonService.send(player, MessageKey.RECOVERY_TRIES_EXCEEDED);
}
}
}

View File

@ -0,0 +1,91 @@
package fr.xephi.authme.command.executable.email;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.mail.EmailService;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.service.PasswordRecoveryService;
import fr.xephi.authme.service.RecoveryCodeService;
import fr.xephi.authme.util.Utils;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Command for password recovery by email.
*/
public class RecoverEmailCommand extends PlayerCommand {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(RecoverEmailCommand.class);
@Inject
private CommonService commonService;
@Inject
private DataSource dataSource;
@Inject
private PlayerCache playerCache;
@Inject
private EmailService emailService;
@Inject
private PasswordRecoveryService recoveryService;
@Inject
private RecoveryCodeService recoveryCodeService;
@Inject
private BukkitService bukkitService;
@Override
protected void runCommand(Player player, List<String> arguments) {
final String playerMail = arguments.get(0);
final String playerName = player.getName();
if (!emailService.hasAllInformation()) {
logger.warning("Mail API is not set");
commonService.send(player, MessageKey.INCOMPLETE_EMAIL_SETTINGS);
return;
}
if (playerCache.isAuthenticated(playerName)) {
commonService.send(player, MessageKey.ALREADY_LOGGED_IN_ERROR);
return;
}
DataSourceValue<String> emailResult = dataSource.getEmail(playerName);
if (!emailResult.rowExists()) {
commonService.send(player, MessageKey.USAGE_REGISTER);
return;
}
final String email = emailResult.getValue();
if (Utils.isEmailEmpty(email) || !email.equalsIgnoreCase(playerMail)) {
commonService.send(player, MessageKey.INVALID_EMAIL);
return;
}
bukkitService.runTaskAsynchronously(() -> {
if (recoveryCodeService.isRecoveryCodeNeeded()) {
// Recovery code is needed; generate and send one
recoveryService.createAndSendRecoveryCode(player, email);
} else {
// Code not needed, just send them a new password
recoveryService.generateAndSendNewPassword(player, email);
}
});
}
@Override
public MessageKey getArgumentsMismatchMessage() {
return MessageKey.USAGE_RECOVER_EMAIL;
}
}

View File

@ -0,0 +1,48 @@
package fr.xephi.authme.command.executable.email;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.settings.properties.SecuritySettings;
import fr.xephi.authme.util.Utils;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Show email command.
*/
public class ShowEmailCommand extends PlayerCommand {
@Inject
private CommonService commonService;
@Inject
private PlayerCache playerCache;
@Override
public void runCommand(Player player, List<String> arguments) {
PlayerAuth auth = playerCache.getAuth(player.getName());
if (auth != null && !Utils.isEmailEmpty(auth.getEmail())) {
if (commonService.getProperty(SecuritySettings.USE_EMAIL_MASKING)){
commonService.send(player, MessageKey.EMAIL_SHOW, emailMask(auth.getEmail()));
} else {
commonService.send(player, MessageKey.EMAIL_SHOW, auth.getEmail());
}
} else {
commonService.send(player, MessageKey.SHOW_NO_EMAIL);
}
}
private String emailMask(String email){
String[] frag = email.split("@"); //Split id and domain
int sid = frag[0].length() / 3 + 1; //Define the id view (required length >= 1)
int sdomain = frag[1].length() / 3; //Define the domain view (required length >= 0)
String id = frag[0].substring(0, sid) + "***"; //Build the id
String domain = "***" + frag[1].substring(sdomain); //Build the domain
return id + "@" + domain;
}
}

View File

@ -0,0 +1,34 @@
package fr.xephi.authme.command.executable.login;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.process.Management;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Login command.
*/
public class LoginCommand extends PlayerCommand {
@Inject
private Management management;
@Override
public void runCommand(Player player, List<String> arguments) {
String password = arguments.get(0);
management.performLogin(player, password);
}
@Override
public MessageKey getArgumentsMismatchMessage() {
return MessageKey.USAGE_LOGIN;
}
@Override
protected String getAlternativeCommand() {
return "/authme forcelogin <player>";
}
}

View File

@ -0,0 +1,22 @@
package fr.xephi.authme.command.executable.logout;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.process.Management;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Logout command.
*/
public class LogoutCommand extends PlayerCommand {
@Inject
private Management management;
@Override
public void runCommand(Player player, List<String> arguments) {
management.performLogout(player);
}
}

View File

@ -0,0 +1,222 @@
package fr.xephi.authme.command.executable.register;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.captcha.RegistrationCaptchaManager;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.mail.EmailService;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.process.Management;
import fr.xephi.authme.process.register.RegisterSecondaryArgument;
import fr.xephi.authme.process.register.RegistrationType;
import fr.xephi.authme.process.register.executors.EmailRegisterParams;
import fr.xephi.authme.process.register.executors.PasswordRegisterParams;
import fr.xephi.authme.process.register.executors.RegistrationMethod;
import fr.xephi.authme.process.register.executors.TwoFactorRegisterParams;
import fr.xephi.authme.security.HashAlgorithm;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.service.ValidationService;
import fr.xephi.authme.settings.properties.EmailSettings;
import fr.xephi.authme.settings.properties.RegistrationSettings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
import static fr.xephi.authme.process.register.RegisterSecondaryArgument.CONFIRMATION;
import static fr.xephi.authme.process.register.RegisterSecondaryArgument.EMAIL_MANDATORY;
import static fr.xephi.authme.process.register.RegisterSecondaryArgument.EMAIL_OPTIONAL;
import static fr.xephi.authme.process.register.RegisterSecondaryArgument.NONE;
import static fr.xephi.authme.settings.properties.RegistrationSettings.REGISTER_SECOND_ARGUMENT;
/**
* Command for /register.
*/
public class RegisterCommand extends PlayerCommand {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(RegisterCommand.class);
@Inject
private Management management;
@Inject
private CommonService commonService;
@Inject
private BukkitService bukkitService;
@Inject
private DataSource dataSource;
@Inject
private EmailService emailService;
@Inject
private ValidationService validationService;
@Inject
private RegistrationCaptchaManager registrationCaptchaManager;
@Override
public void runCommand(Player player, List<String> arguments) {
if (!isCaptchaFulfilled(player)) {
return; // isCaptchaFulfilled handles informing the player on failure
}
if (commonService.getProperty(SecuritySettings.PASSWORD_HASH) == HashAlgorithm.TWO_FACTOR) {
//for two factor auth we don't need to check the usage
management.performRegister(RegistrationMethod.TWO_FACTOR_REGISTRATION,
TwoFactorRegisterParams.of(player));
return;
} else if (arguments.size() < 1) {
commonService.send(player, MessageKey.USAGE_REGISTER);
return;
}
RegistrationType registrationType = commonService.getProperty(RegistrationSettings.REGISTRATION_TYPE);
if (registrationType == RegistrationType.PASSWORD) {
handlePasswordRegistration(player, arguments);
} else if (registrationType == RegistrationType.EMAIL) {
handleEmailRegistration(player, arguments);
} else {
throw new IllegalStateException("Unknown registration type '" + registrationType + "'");
}
}
@Override
protected String getAlternativeCommand() {
return "/authme register <playername> <password>";
}
@Override
public MessageKey getArgumentsMismatchMessage() {
return MessageKey.USAGE_REGISTER;
}
private boolean isCaptchaFulfilled(Player player) {
if (registrationCaptchaManager.isCaptchaRequired(player.getName())) {
String code = registrationCaptchaManager.getCaptchaCodeOrGenerateNew(player.getName());
commonService.send(player, MessageKey.CAPTCHA_FOR_REGISTRATION_REQUIRED, code);
return false;
}
return true;
}
private void handlePasswordRegistration(Player player, List<String> arguments) {
if (isSecondArgValidForPasswordRegistration(player, arguments)) {
final String password = arguments.get(0);
final String email = getEmailIfAvailable(arguments);
management.performRegister(RegistrationMethod.PASSWORD_REGISTRATION,
PasswordRegisterParams.of(player, password, email));
}
}
private String getEmailIfAvailable(List<String> arguments) {
if (arguments.size() >= 2) {
RegisterSecondaryArgument secondArgType = commonService.getProperty(REGISTER_SECOND_ARGUMENT);
if (secondArgType == EMAIL_MANDATORY || secondArgType == EMAIL_OPTIONAL) {
return arguments.get(1);
}
}
return null;
}
/**
* Verifies that the second argument is valid (based on the configuration)
* to perform a password registration. The player is informed if the check
* is unsuccessful.
*
* @param player the player to register
* @param arguments the provided arguments
* @return true if valid, false otherwise
*/
private boolean isSecondArgValidForPasswordRegistration(Player player, List<String> arguments) {
RegisterSecondaryArgument secondArgType = commonService.getProperty(REGISTER_SECOND_ARGUMENT);
// cases where args.size < 2
if (secondArgType == NONE || secondArgType == EMAIL_OPTIONAL && arguments.size() < 2) {
return true;
} else if (arguments.size() < 2) {
commonService.send(player, MessageKey.USAGE_REGISTER);
return false;
}
if (secondArgType == CONFIRMATION) {
if (arguments.get(0).equals(arguments.get(1))) {
return true;
} else {
commonService.send(player, MessageKey.PASSWORD_MATCH_ERROR);
return false;
}
} else if (secondArgType == EMAIL_MANDATORY || secondArgType == EMAIL_OPTIONAL) {
if (validationService.validateEmail(arguments.get(1))) {
return true;
} else {
commonService.send(player, MessageKey.INVALID_EMAIL);
return false;
}
} else {
throw new IllegalStateException("Unknown secondary argument type '" + secondArgType + "'");
}
}
private void handleEmailRegistration(Player player, List<String> arguments) {
if (!emailService.hasAllInformation()) {
commonService.send(player, MessageKey.INCOMPLETE_EMAIL_SETTINGS);
logger.warning("Cannot register player '" + player.getName() + "': no email or password is set "
+ "to send emails from. Please adjust your config at " + EmailSettings.MAIL_ACCOUNT.getPath());
return;
}
final String email = arguments.get(0);
if (!validationService.validateEmail(email)) {
commonService.send(player, MessageKey.INVALID_EMAIL);
} else if (isSecondArgValidForEmailRegistration(player, arguments)) {
management.performRegister(RegistrationMethod.EMAIL_REGISTRATION,
EmailRegisterParams.of(player, email));
if (commonService.getProperty(RegistrationSettings.UNREGISTER_ON_EMAIL_VERIFICATION_FAILURE) && commonService.getProperty(RegistrationSettings.UNREGISTER_AFTER_MINUTES) > 0) {
bukkitService.runTaskLater(player, () -> {
if (dataSource.getAuth(player.getName()) != null) {
if (dataSource.getAuth(player.getName()).getLastLogin() == null) {
management.performUnregisterByAdmin(null, player.getName(), player);
}
}
}, 60 * 20 * commonService.getProperty(RegistrationSettings.UNREGISTER_AFTER_MINUTES));
}
}
}
/**
* Verifies that the second argument is valid (based on the configuration)
* to perform an email registration. The player is informed if the check
* is unsuccessful.
*
* @param player the player to register
* @param arguments the provided arguments
* @return true if valid, false otherwise
*/
private boolean isSecondArgValidForEmailRegistration(Player player, List<String> arguments) {
RegisterSecondaryArgument secondArgType = commonService.getProperty(REGISTER_SECOND_ARGUMENT);
// cases where args.size < 2
if (secondArgType == NONE || secondArgType == EMAIL_OPTIONAL && arguments.size() < 2) {
return true;
} else if (arguments.size() < 2) {
commonService.send(player, MessageKey.USAGE_REGISTER);
return false;
}
if (secondArgType == EMAIL_OPTIONAL || secondArgType == EMAIL_MANDATORY || secondArgType == CONFIRMATION) {
if (arguments.get(0).equals(arguments.get(1))) {
return true;
} else {
commonService.send(player, MessageKey.USAGE_REGISTER);
return false;
}
} else {
throw new IllegalStateException("Unknown secondary argument type '" + secondArgType + "'");
}
}
}

View File

@ -0,0 +1,43 @@
package fr.xephi.authme.command.executable.totp;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.security.totp.GenerateTotpService;
import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Command for a player to enable TOTP.
*/
public class AddTotpCommand extends PlayerCommand {
@Inject
private GenerateTotpService generateTotpService;
@Inject
private PlayerCache playerCache;
@Inject
private Messages messages;
@Override
protected void runCommand(Player player, List<String> arguments) {
PlayerAuth auth = playerCache.getAuth(player.getName());
if (auth == null) {
messages.send(player, MessageKey.NOT_LOGGED_IN);
} else if (auth.getTotpKey() == null) {
TotpGenerationResult createdTotpInfo = generateTotpService.generateTotpKey(player);
messages.send(player, MessageKey.TWO_FACTOR_CREATE,
createdTotpInfo.getTotpKey(), createdTotpInfo.getAuthenticatorQrCodeUrl());
messages.send(player, MessageKey.TWO_FACTOR_CREATE_CONFIRMATION_REQUIRED);
} else {
messages.send(player, MessageKey.TWO_FACTOR_ALREADY_ENABLED);
}
}
}

View File

@ -0,0 +1,74 @@
package fr.xephi.authme.command.executable.totp;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.security.totp.GenerateTotpService;
import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Command to enable TOTP by supplying the proper code as confirmation.
*/
public class ConfirmTotpCommand extends PlayerCommand {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(ConfirmTotpCommand.class);
@Inject
private GenerateTotpService generateTotpService;
@Inject
private PlayerCache playerCache;
@Inject
private DataSource dataSource;
@Inject
private Messages messages;
@Override
protected void runCommand(Player player, List<String> arguments) {
PlayerAuth auth = playerCache.getAuth(player.getName());
if (auth == null) {
messages.send(player, MessageKey.NOT_LOGGED_IN);
} else if (auth.getTotpKey() != null) {
messages.send(player, MessageKey.TWO_FACTOR_ALREADY_ENABLED);
} else {
verifyTotpCodeConfirmation(player, auth, arguments.get(0));
}
}
private void verifyTotpCodeConfirmation(Player player, PlayerAuth auth, String inputTotpCode) {
final TotpGenerationResult totpDetails = generateTotpService.getGeneratedTotpKey(player);
if (totpDetails == null) {
messages.send(player, MessageKey.TWO_FACTOR_ENABLE_ERROR_NO_CODE);
} else {
boolean isCodeValid = generateTotpService.isTotpCodeCorrectForGeneratedTotpKey(player, inputTotpCode);
if (isCodeValid) {
generateTotpService.removeGenerateTotpKey(player);
insertTotpKeyIntoDatabase(player, auth, totpDetails);
} else {
messages.send(player, MessageKey.TWO_FACTOR_ENABLE_ERROR_WRONG_CODE);
}
}
}
private void insertTotpKeyIntoDatabase(Player player, PlayerAuth auth, TotpGenerationResult totpDetails) {
if (dataSource.setTotpKey(player.getName(), totpDetails.getTotpKey())) {
messages.send(player, MessageKey.TWO_FACTOR_ENABLE_SUCCESS);
auth.setTotpKey(totpDetails.getTotpKey());
playerCache.updatePlayer(auth);
logger.info("Player '" + player.getName() + "' has successfully added a TOTP key to their account");
} else {
messages.send(player, MessageKey.ERROR);
}
}
}

View File

@ -0,0 +1,62 @@
package fr.xephi.authme.command.executable.totp;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.security.totp.TotpAuthenticator;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Command for a player to remove 2FA authentication.
*/
public class RemoveTotpCommand extends PlayerCommand {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(RemoveTotpCommand.class);
@Inject
private DataSource dataSource;
@Inject
private PlayerCache playerCache;
@Inject
private TotpAuthenticator totpAuthenticator;
@Inject
private Messages messages;
@Override
protected void runCommand(Player player, List<String> arguments) {
PlayerAuth auth = playerCache.getAuth(player.getName());
if (auth == null) {
messages.send(player, MessageKey.NOT_LOGGED_IN);
} else if (auth.getTotpKey() == null) {
messages.send(player, MessageKey.TWO_FACTOR_NOT_ENABLED_ERROR);
} else {
if (totpAuthenticator.checkCode(auth, arguments.get(0))) {
removeTotpKeyFromDatabase(player, auth);
} else {
messages.send(player, MessageKey.TWO_FACTOR_INVALID_CODE);
}
}
}
private void removeTotpKeyFromDatabase(Player player, PlayerAuth auth) {
if (dataSource.removeTotpKey(auth.getNickname())) {
auth.setTotpKey(null);
playerCache.updatePlayer(auth);
messages.send(player, MessageKey.TWO_FACTOR_REMOVED_SUCCESS);
logger.info("Player '" + player.getName() + "' removed their TOTP key");
} else {
messages.send(player, MessageKey.ERROR);
}
}
}

View File

@ -0,0 +1,29 @@
package fr.xephi.authme.command.executable.totp;
import fr.xephi.authme.command.CommandMapper;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.command.FoundCommandResult;
import fr.xephi.authme.command.help.HelpProvider;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.Collections;
import java.util.List;
/**
* Base command for /totp.
*/
public class TotpBaseCommand implements ExecutableCommand {
@Inject
private CommandMapper commandMapper;
@Inject
private HelpProvider helpProvider;
@Override
public void executeCommand(CommandSender sender, List<String> arguments) {
FoundCommandResult result = commandMapper.mapPartsToCommand(sender, Collections.singletonList("totp"));
helpProvider.outputHelp(sender, result, HelpProvider.SHOW_CHILDREN);
}
}

View File

@ -0,0 +1,79 @@
package fr.xephi.authme.command.executable.totp;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.command.PlayerCommand;
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.data.limbo.LimboPlayerState;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.process.login.AsynchronousLogin;
import fr.xephi.authme.security.totp.TotpAuthenticator;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* TOTP code command for processing the 2FA code during the login process.
*/
public class TotpCodeCommand extends PlayerCommand {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(TotpCodeCommand.class);
@Inject
private LimboService limboService;
@Inject
private PlayerCache playerCache;
@Inject
private Messages messages;
@Inject
private TotpAuthenticator totpAuthenticator;
@Inject
private DataSource dataSource;
@Inject
private AsynchronousLogin asynchronousLogin;
@Override
protected void runCommand(Player player, List<String> arguments) {
if (playerCache.isAuthenticated(player.getName())) {
messages.send(player, MessageKey.ALREADY_LOGGED_IN_ERROR);
return;
}
PlayerAuth auth = dataSource.getAuth(player.getName());
if (auth == null) {
messages.send(player, MessageKey.REGISTER_MESSAGE);
return;
}
LimboPlayer limbo = limboService.getLimboPlayer(player.getName());
if (limbo != null && limbo.getState() == LimboPlayerState.TOTP_REQUIRED) {
processCode(player, auth, arguments.get(0));
} else {
logger.debug(() -> "Aborting TOTP check for player '" + player.getName()
+ "'. Invalid limbo state: " + (limbo == null ? "no limbo" : limbo.getState()));
messages.send(player, MessageKey.LOGIN_MESSAGE);
}
}
private void processCode(Player player, PlayerAuth auth, String inputCode) {
boolean isCodeValid = totpAuthenticator.checkCode(auth, inputCode);
if (isCodeValid) {
logger.debug("Successfully checked TOTP code for `{0}`", player.getName());
asynchronousLogin.performLogin(player, auth);
} else {
logger.debug("Input TOTP code was invalid for player `{0}`", player.getName());
messages.send(player, MessageKey.TWO_FACTOR_INVALID_CODE);
}
}
}

View File

@ -0,0 +1,62 @@
package fr.xephi.authme.command.executable.unregister;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.VerificationCodeManager;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.process.Management;
import fr.xephi.authme.service.CommonService;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Command for a player to unregister himself.
*/
public class UnregisterCommand extends PlayerCommand {
@Inject
private Management management;
@Inject
private CommonService commonService;
@Inject
private PlayerCache playerCache;
@Inject
private VerificationCodeManager codeManager;
@Override
public void runCommand(Player player, List<String> arguments) {
String playerPass = arguments.get(0);
String playerName = player.getName();
// Make sure the player is authenticated
if (!playerCache.isAuthenticated(playerName)) {
commonService.send(player, MessageKey.NOT_LOGGED_IN);
return;
}
// Check if the user has been verified or not
if (codeManager.isVerificationRequired(player)) {
codeManager.codeExistOrGenerateNew(playerName);
commonService.send(player, MessageKey.VERIFICATION_CODE_REQUIRED);
return;
}
// Unregister the player
management.performUnregister(player, playerPass);
}
@Override
public MessageKey getArgumentsMismatchMessage() {
return MessageKey.USAGE_UNREGISTER;
}
@Override
protected String getAlternativeCommand() {
return "/authme unregister <player>";
}
}

View File

@ -0,0 +1,61 @@
package fr.xephi.authme.command.executable.verification;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.VerificationCodeManager;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.service.CommonService;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.List;
/**
* Used to complete the email verification process.
*/
public class VerificationCommand extends PlayerCommand {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(VerificationCommand.class);
@Inject
private CommonService commonService;
@Inject
private VerificationCodeManager codeManager;
@Override
public void runCommand(Player player, List<String> arguments) {
final String playerName = player.getName();
if (!codeManager.canSendMail()) {
logger.warning("Mail API is not set");
commonService.send(player, MessageKey.INCOMPLETE_EMAIL_SETTINGS);
return;
}
if (codeManager.isVerificationRequired(player)) {
if (codeManager.isCodeRequired(playerName)) {
if (codeManager.checkCode(playerName, arguments.get(0))) {
commonService.send(player, MessageKey.VERIFICATION_CODE_VERIFIED);
} else {
commonService.send(player, MessageKey.INCORRECT_VERIFICATION_CODE);
}
} else {
commonService.send(player, MessageKey.VERIFICATION_CODE_EXPIRED);
}
} else {
if (codeManager.hasEmail(playerName)) {
commonService.send(player, MessageKey.VERIFICATION_CODE_ALREADY_VERIFIED);
} else {
commonService.send(player, MessageKey.VERIFICATION_CODE_EMAIL_NEEDED);
commonService.send(player, MessageKey.ADD_EMAIL_MESSAGE);
}
}
}
@Override
public MessageKey getArgumentsMismatchMessage() {
return MessageKey.USAGE_VERIFICATION_CODE;
}
}

View File

@ -0,0 +1,43 @@
package fr.xephi.authme.command.help;
/**
* Common, non-generic keys for messages used when showing command help.
* All keys are prefixed with {@code common}.
*/
public enum HelpMessage {
HEADER("header"),
OPTIONAL("optional"),
HAS_PERMISSION("hasPermission"),
NO_PERMISSION("noPermission"),
DEFAULT("default"),
RESULT("result");
private static final String PREFIX = "common.";
private final String key;
/**
* Constructor.
*
* @param key the message key
*/
HelpMessage(String key) {
this.key = PREFIX + key;
}
/** @return the message key */
public String getKey() {
return key;
}
/** @return the key without the common prefix */
public String getEntryKey() {
// Note ljacqu 20171008: #getKey is called more often than this method, so we optimize for the former method
return key.substring(PREFIX.length());
}
}

View File

@ -0,0 +1,117 @@
package fr.xephi.authme.command.help;
import com.google.common.base.CaseFormat;
import fr.xephi.authme.command.CommandArgumentDescription;
import fr.xephi.authme.command.CommandDescription;
import fr.xephi.authme.command.CommandUtils;
import fr.xephi.authme.message.HelpMessagesFileHandler;
import fr.xephi.authme.permission.DefaultPermission;
import javax.inject.Inject;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* Manages translatable help messages.
*/
public class HelpMessagesService {
private static final String COMMAND_PREFIX = "commands.";
private static final String DESCRIPTION_SUFFIX = ".description";
private static final String DETAILED_DESCRIPTION_SUFFIX = ".detailedDescription";
private static final String DEFAULT_PERMISSIONS_PATH = "common.defaultPermissions.";
private final HelpMessagesFileHandler helpMessagesFileHandler;
@Inject
HelpMessagesService(HelpMessagesFileHandler helpMessagesFileHandler) {
this.helpMessagesFileHandler = helpMessagesFileHandler;
}
/**
* Creates a copy of the supplied command description with localized messages where present.
*
* @param command the command to build a localized version of
* @return the localized description
*/
public CommandDescription buildLocalizedDescription(CommandDescription command) {
final String path = COMMAND_PREFIX + getCommandSubPath(command);
if (!helpMessagesFileHandler.hasSection(path)) {
// Messages file does not have a section for this command - return the provided command
return command;
}
CommandDescription.CommandBuilder builder = CommandDescription.builder()
.description(getText(path + DESCRIPTION_SUFFIX, command::getDescription))
.detailedDescription(getText(path + DETAILED_DESCRIPTION_SUFFIX, command::getDetailedDescription))
.executableCommand(command.getExecutableCommand())
.parent(command.getParent())
.labels(command.getLabels())
.permission(command.getPermission());
int i = 1;
for (CommandArgumentDescription argument : command.getArguments()) {
String argPath = path + ".arg" + i;
String label = getText(argPath + ".label", argument::getName);
String description = getText(argPath + ".description", argument::getDescription);
builder.withArgument(label, description, argument.isOptional());
++i;
}
CommandDescription localCommand = builder.build();
localCommand.getChildren().addAll(command.getChildren());
return localCommand;
}
public String getDescription(CommandDescription command) {
return getText(COMMAND_PREFIX + getCommandSubPath(command) + DESCRIPTION_SUFFIX, command::getDescription);
}
public String getMessage(HelpMessage message) {
return helpMessagesFileHandler.getMessage(message.getKey());
}
public String getMessage(HelpSection section) {
return helpMessagesFileHandler.getMessage(section.getKey());
}
public String getMessage(DefaultPermission defaultPermission) {
// e.g. {default_permissions_path}.opOnly for DefaultPermission.OP_ONLY
String path = DEFAULT_PERMISSIONS_PATH + getDefaultPermissionsSubPath(defaultPermission);
return helpMessagesFileHandler.getMessage(path);
}
public static String getDefaultPermissionsSubPath(DefaultPermission defaultPermission) {
return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, defaultPermission.name());
}
private String getText(String path, Supplier<String> defaultTextGetter) {
String message = helpMessagesFileHandler.getMessageIfExists(path);
return message == null
? defaultTextGetter.get()
: message;
}
/**
* Triggers a reload of the help messages file. Note that this method is not needed
* to be called for /authme reload.
*/
public void reloadMessagesFile() {
helpMessagesFileHandler.reload();
}
/**
* Returns the command subpath for the given command (i.e. the path to the translations for the given
* command under "commands").
*
* @param command the command to process
* @return the subpath for the command's texts
*/
public static String getCommandSubPath(CommandDescription command) {
return CommandUtils.constructParentList(command)
.stream()
.map(cmd -> cmd.getLabels().get(0))
.collect(Collectors.joining("."));
}
}

View File

@ -0,0 +1,328 @@
package fr.xephi.authme.command.help;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import fr.xephi.authme.command.CommandArgumentDescription;
import fr.xephi.authme.command.CommandDescription;
import fr.xephi.authme.command.CommandUtils;
import fr.xephi.authme.command.FoundCommandResult;
import fr.xephi.authme.initialization.Reloadable;
import fr.xephi.authme.permission.DefaultPermission;
import fr.xephi.authme.permission.PermissionNode;
import fr.xephi.authme.permission.PermissionsManager;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import static fr.xephi.authme.command.help.HelpSection.DETAILED_DESCRIPTION;
import static fr.xephi.authme.command.help.HelpSection.SHORT_DESCRIPTION;
import static java.util.Collections.singletonList;
/**
* Help syntax generator for AuthMe commands.
*/
public class HelpProvider implements Reloadable {
// --- Bit flags ---
/** Set to show a command overview. */
public static final int SHOW_COMMAND = 0x001;
/** Set to show the description of the command. */
public static final int SHOW_DESCRIPTION = 0x002;
/** Set to show the detailed description of the command. */
public static final int SHOW_LONG_DESCRIPTION = 0x004;
/** Set to include the arguments the command takes. */
public static final int SHOW_ARGUMENTS = 0x008;
/** Set to show the permissions required to execute the command. */
public static final int SHOW_PERMISSIONS = 0x010;
/** Set to show alternative labels for the command. */
public static final int SHOW_ALTERNATIVES = 0x020;
/** Set to show the child commands of the command. */
public static final int SHOW_CHILDREN = 0x040;
/** Shortcut for setting all options. */
public static final int ALL_OPTIONS = ~0;
private final PermissionsManager permissionsManager;
private final HelpMessagesService helpMessagesService;
/** int with bit flags set corresponding to the above constants for enabled sections. */
private Integer enabledSections;
@Inject
HelpProvider(PermissionsManager permissionsManager, HelpMessagesService helpMessagesService) {
this.permissionsManager = permissionsManager;
this.helpMessagesService = helpMessagesService;
}
/**
* Builds the help messages based on the provided arguments.
*
* @param sender the sender to evaluate permissions with
* @param result the command result to create help for
* @param options output options
* @return the generated help messages
*/
private List<String> buildHelpOutput(CommandSender sender, FoundCommandResult result, int options) {
if (result.getCommandDescription() == null) {
return singletonList(ChatColor.DARK_RED + "Failed to retrieve any help information!");
}
List<String> lines = new ArrayList<>();
options = filterDisabledSections(options);
if (options == 0) {
// Return directly if no options are enabled so we don't include the help header
return lines;
}
String header = helpMessagesService.getMessage(HelpMessage.HEADER);
if (!header.isEmpty()) {
lines.add(ChatColor.GOLD + header);
}
CommandDescription command = helpMessagesService.buildLocalizedDescription(result.getCommandDescription());
List<String> correctLabels = ImmutableList.copyOf(filterCorrectLabels(command, result.getLabels()));
if (hasFlag(SHOW_COMMAND, options)) {
lines.add(ChatColor.GOLD + helpMessagesService.getMessage(HelpSection.COMMAND) + ": "
+ CommandUtils.buildSyntax(command, correctLabels));
}
if (hasFlag(SHOW_DESCRIPTION, options)) {
lines.add(ChatColor.GOLD + helpMessagesService.getMessage(SHORT_DESCRIPTION) + ": "
+ ChatColor.WHITE + command.getDescription());
}
if (hasFlag(SHOW_LONG_DESCRIPTION, options)) {
lines.add(ChatColor.GOLD + helpMessagesService.getMessage(DETAILED_DESCRIPTION) + ":");
lines.add(ChatColor.WHITE + " " + command.getDetailedDescription());
}
if (hasFlag(SHOW_ARGUMENTS, options)) {
addArgumentsInfo(command, lines);
}
if (hasFlag(SHOW_PERMISSIONS, options) && sender != null) {
addPermissionsInfo(command, sender, lines);
}
if (hasFlag(SHOW_ALTERNATIVES, options)) {
addAlternativesInfo(command, correctLabels, lines);
}
if (hasFlag(SHOW_CHILDREN, options)) {
addChildrenInfo(command, correctLabels, lines);
}
return lines;
}
/**
* Outputs the help for a given command.
*
* @param sender the sender to output the help to
* @param result the result to output information about
* @param options output options
*/
public void outputHelp(CommandSender sender, FoundCommandResult result, int options) {
List<String> lines = buildHelpOutput(sender, result, options);
for (String line : lines) {
sender.sendMessage(line);
}
}
@Override
public void reload() {
// We don't know about the reloading order of the classes, i.e. we cannot assume that HelpMessagesService
// has already been reloaded. So set the enabledSections flag to null and redefine it first time needed.
enabledSections = null;
}
/**
* Removes any disabled sections from the options. Sections are considered disabled
* if the translated text for the section is empty.
*
* @param options the options to process
* @return the options without any disabled sections
*/
@SuppressWarnings("checkstyle:BooleanExpressionComplexity")
private int filterDisabledSections(int options) {
if (enabledSections == null) {
enabledSections = flagFor(HelpSection.COMMAND, SHOW_COMMAND)
| flagFor(HelpSection.SHORT_DESCRIPTION, SHOW_DESCRIPTION)
| flagFor(HelpSection.DETAILED_DESCRIPTION, SHOW_LONG_DESCRIPTION)
| flagFor(HelpSection.ARGUMENTS, SHOW_ARGUMENTS)
| flagFor(HelpSection.PERMISSIONS, SHOW_PERMISSIONS)
| flagFor(HelpSection.ALTERNATIVES, SHOW_ALTERNATIVES)
| flagFor(HelpSection.CHILDREN, SHOW_CHILDREN);
}
return options & enabledSections;
}
private int flagFor(HelpSection section, int flag) {
return helpMessagesService.getMessage(section).isEmpty() ? 0 : flag;
}
/**
* Adds help info about the given command's arguments into the provided list.
*
* @param command the command to generate arguments info for
* @param lines the output collection to add the info to
*/
private void addArgumentsInfo(CommandDescription command, List<String> lines) {
if (command.getArguments().isEmpty()) {
return;
}
lines.add(ChatColor.GOLD + helpMessagesService.getMessage(HelpSection.ARGUMENTS) + ":");
StringBuilder argString = new StringBuilder();
String optionalText = " (" + helpMessagesService.getMessage(HelpMessage.OPTIONAL) + ")";
for (CommandArgumentDescription argument : command.getArguments()) {
argString.setLength(0);
argString.append(" ").append(ChatColor.YELLOW).append(ChatColor.ITALIC).append(argument.getName())
.append(": ").append(ChatColor.WHITE).append(argument.getDescription());
if (argument.isOptional()) {
argString.append(ChatColor.GRAY).append(ChatColor.ITALIC).append(optionalText);
}
lines.add(argString.toString());
}
}
/**
* Adds help info about the given command's alternative labels into the provided list.
*
* @param command the command for which to generate info about its labels
* @param correctLabels labels used to access the command (sanitized)
* @param lines the output collection to add the info to
*/
private void addAlternativesInfo(CommandDescription command, List<String> correctLabels, List<String> lines) {
if (command.getLabels().size() <= 1) {
return;
}
lines.add(ChatColor.GOLD + helpMessagesService.getMessage(HelpSection.ALTERNATIVES) + ":");
// Label with which the command was called -> don't show it as an alternative
final String usedLabel;
// Takes alternative label and constructs list of labels, e.g. "reg" -> [authme, reg]
final Function<String, List<String>> commandLabelsFn;
if (correctLabels.size() == 1) {
usedLabel = correctLabels.get(0);
commandLabelsFn = label -> singletonList(label);
} else {
usedLabel = correctLabels.get(1);
commandLabelsFn = label -> Arrays.asList(correctLabels.get(0), label);
}
// Create a list of alternatives
for (String label : command.getLabels()) {
if (!label.equalsIgnoreCase(usedLabel)) {
lines.add(" " + CommandUtils.buildSyntax(command, commandLabelsFn.apply(label)));
}
}
}
/**
* Adds help info about the given command's permissions into the provided list.
*
* @param command the command to generate permissions info for
* @param sender the command sender, used to evaluate permissions
* @param lines the output collection to add the info to
*/
private void addPermissionsInfo(CommandDescription command, CommandSender sender, List<String> lines) {
PermissionNode permission = command.getPermission();
if (permission == null) {
return;
}
lines.add(ChatColor.GOLD + helpMessagesService.getMessage(HelpSection.PERMISSIONS) + ":");
boolean hasPermission = permissionsManager.hasPermission(sender, permission);
lines.add(String.format(" " + ChatColor.YELLOW + ChatColor.ITALIC + "%s" + ChatColor.GRAY + " (%s)",
permission.getNode(), getLocalPermissionText(hasPermission)));
// Addendum to the line to specify whether the sender has permission or not when default is OP_ONLY
final DefaultPermission defaultPermission = permission.getDefaultPermission();
String addendum = "";
if (DefaultPermission.OP_ONLY.equals(defaultPermission)) {
addendum = " (" + getLocalPermissionText(defaultPermission.evaluate(sender)) + ")";
}
lines.add(ChatColor.GOLD + helpMessagesService.getMessage(HelpMessage.DEFAULT) + ": "
+ ChatColor.GRAY + ChatColor.ITALIC + helpMessagesService.getMessage(defaultPermission) + addendum);
// Evaluate if the sender has permission to the command
ChatColor permissionColor;
String permissionText;
if (permissionsManager.hasPermission(sender, command.getPermission())) {
permissionColor = ChatColor.GREEN;
permissionText = getLocalPermissionText(true);
} else {
permissionColor = ChatColor.DARK_RED;
permissionText = getLocalPermissionText(false);
}
lines.add(String.format(ChatColor.GOLD + " %s: %s" + ChatColor.ITALIC + "%s",
helpMessagesService.getMessage(HelpMessage.RESULT), permissionColor, permissionText));
}
private String getLocalPermissionText(boolean hasPermission) {
if (hasPermission) {
return helpMessagesService.getMessage(HelpMessage.HAS_PERMISSION);
}
return helpMessagesService.getMessage(HelpMessage.NO_PERMISSION);
}
/**
* Adds help info about the given command's child command into the provided list.
*
* @param command the command for which to generate info about its child commands
* @param correctLabels the labels used to access the given command (sanitized)
* @param lines the output collection to add the info to
*/
private void addChildrenInfo(CommandDescription command, List<String> correctLabels, List<String> lines) {
if (command.getChildren().isEmpty()) {
return;
}
lines.add(ChatColor.GOLD + helpMessagesService.getMessage(HelpSection.CHILDREN) + ":");
String parentCommandPath = String.join(" ", correctLabels);
for (CommandDescription child : command.getChildren()) {
lines.add(" /" + parentCommandPath + " " + child.getLabels().get(0)
+ ChatColor.GRAY + ChatColor.ITALIC + ": " + helpMessagesService.getDescription(child));
}
}
private static boolean hasFlag(int flag, int options) {
return (flag & options) != 0;
}
/**
* Returns a list of labels for the given command, using the labels from the provided labels list
* as long as they are correct.
* <p>
* Background: commands may have multiple labels (e.g. /authme register vs. /authme reg). It is interesting
* for us to keep with which label the user requested the command. At the same time, when a user inputs a
* non-existent label, we try to find the most similar one. This method keeps all labels that exists and will
* default to the command's first label when an invalid label is encountered.
* <p>
* Examples:
* command = "authme register", labels = {authme, egister}. Output: {authme, register}
* command = "authme register", labels = {authme, reg}. Output: {authme, reg}
*
* @param command the command to compare the labels against
* @param labels the labels as input by the user
* @return list of correct labels, keeping the user's input where possible
*/
@VisibleForTesting
static List<String> filterCorrectLabels(CommandDescription command, List<String> labels) {
List<CommandDescription> commands = CommandUtils.constructParentList(command);
List<String> correctLabels = new ArrayList<>();
boolean foundIncorrectLabel = false;
for (int i = 0; i < commands.size(); ++i) {
if (!foundIncorrectLabel && i < labels.size() && commands.get(i).hasLabel(labels.get(i))) {
correctLabels.add(labels.get(i));
} else {
foundIncorrectLabel = true;
correctLabels.add(commands.get(i).getLabels().get(0));
}
}
return correctLabels;
}
}

View File

@ -0,0 +1,44 @@
package fr.xephi.authme.command.help;
/**
* Translatable sections. Message keys are prefixed by {@code section}.
*/
public enum HelpSection {
COMMAND("command"),
SHORT_DESCRIPTION("description"),
DETAILED_DESCRIPTION("detailedDescription"),
ARGUMENTS("arguments"),
ALTERNATIVES("alternatives"),
PERMISSIONS("permissions"),
CHILDREN("children");
private static final String PREFIX = "section.";
private final String key;
/**
* Constructor.
*
* @param key the message key
*/
HelpSection(String key) {
this.key = PREFIX + key;
}
/** @return the message key */
public String getKey() {
return key;
}
/** @return the key without the common prefix */
public String getEntryKey() {
// Note ljacqu 20171008: #getKey is called more often than this method, so we optimize for the former method
return key.substring(PREFIX.length());
}
}

View File

@ -0,0 +1,49 @@
package fr.xephi.authme.data;
import fr.xephi.authme.initialization.HasCleanup;
import fr.xephi.authme.util.expiring.ExpiringSet;
import javax.inject.Inject;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
public class ProxySessionManager implements HasCleanup {
private final ExpiringSet<String> activeProxySessions;
@Inject
public ProxySessionManager() {
long countTimeout = 5;
activeProxySessions = new ExpiringSet<>(countTimeout, TimeUnit.SECONDS);
}
/**
* Saves the player in the set
* @param name the player's name
*/
private void setActiveSession(String name) {
activeProxySessions.add(name.toLowerCase(Locale.ROOT));
}
/**
* Process a proxy session message from AuthMeBungee
* @param name the player to process
*/
public void processProxySessionMessage(String name) {
setActiveSession(name);
}
/**
* Returns if the player should be logged in or not
* @param name the name of the player to check
* @return true if player has to be logged in, false otherwise
*/
public boolean shouldResumeSession(String name) {
return activeProxySessions.contains(name);
}
@Override
public void performCleanup() {
activeProxySessions.removeExpiredEntries();
}
}

View File

@ -0,0 +1,75 @@
package fr.xephi.authme.data;
import fr.xephi.authme.initialization.HasCleanup;
import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.permission.PlayerPermission;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.ProtectionSettings;
import fr.xephi.authme.util.expiring.ExpiringSet;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.concurrent.TimeUnit;
public class QuickCommandsProtectionManager implements SettingsDependent, HasCleanup {
private final PermissionsManager permissionsManager;
private final ExpiringSet<String> latestJoin;
@Inject
public QuickCommandsProtectionManager(Settings settings, PermissionsManager permissionsManager) {
this.permissionsManager = permissionsManager;
long countTimeout = settings.getProperty(ProtectionSettings.QUICK_COMMANDS_DENIED_BEFORE_MILLISECONDS);
latestJoin = new ExpiringSet<>(countTimeout, TimeUnit.MILLISECONDS);
reload(settings);
}
/**
* Save the player in the set
* @param name the player's name
*/
private void setJoin(String name) {
latestJoin.add(name);
}
/**
* Returns whether the given player has the permission and should be saved in the set
* @param player the player to check
* @return true if the player has the permission, false otherwise
*/
private boolean shouldSavePlayer(Player player) {
return permissionsManager.hasPermission(player, PlayerPermission.QUICK_COMMANDS_PROTECTION);
}
/**
* Process the player join
* @param player the player to process
*/
public void processJoin(Player player) {
if (shouldSavePlayer(player)) {
setJoin(player.getName());
}
}
/**
* Returns whether the given player is able to perform the command
* @param name the name of the player to check
* @return true if the player is not in the set (so it's allowed to perform the command), false otherwise
*/
public boolean isAllowed(String name) {
return !latestJoin.contains(name);
}
@Override
public void reload(Settings settings) {
long countTimeout = settings.getProperty(ProtectionSettings.QUICK_COMMANDS_DENIED_BEFORE_MILLISECONDS);
latestJoin.setExpiration(countTimeout, TimeUnit.MILLISECONDS);
}
@Override
public void performCleanup() {
latestJoin.removeExpiredEntries();
}
}

View File

@ -0,0 +1,138 @@
package fr.xephi.authme.data;
import fr.xephi.authme.initialization.HasCleanup;
import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import fr.xephi.authme.util.PlayerUtils;
import fr.xephi.authme.util.expiring.TimedCounter;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import static fr.xephi.authme.util.Utils.MILLIS_PER_MINUTE;
/**
* Manager for handling temporary bans.
*/
public class TempbanManager implements SettingsDependent, HasCleanup {
private final Map<String, TimedCounter<String>> ipLoginFailureCounts;
private final BukkitService bukkitService;
private final Messages messages;
private boolean isEnabled;
private int threshold;
private int length;
private long resetThreshold;
private String customCommand;
@Inject
TempbanManager(BukkitService bukkitService, Messages messages, Settings settings) {
this.ipLoginFailureCounts = new ConcurrentHashMap<>();
this.bukkitService = bukkitService;
this.messages = messages;
reload(settings);
}
/**
* Increases the failure count for the given IP address/username combination.
*
* @param address The player's IP address
* @param name The username
*/
public void increaseCount(String address, String name) {
if (isEnabled) {
TimedCounter<String> countsByName = ipLoginFailureCounts.computeIfAbsent(
address, k -> new TimedCounter<>(resetThreshold, TimeUnit.MINUTES));
countsByName.increment(name);
}
}
/**
* Set the failure count for a given IP address / username combination to 0.
*
* @param address The IP address
* @param name The username
*/
public void resetCount(String address, String name) {
if (isEnabled) {
TimedCounter<String> counter = ipLoginFailureCounts.get(address);
if (counter != null) {
counter.remove(name);
}
}
}
/**
* Return whether the IP address should be tempbanned.
*
* @param address The player's IP address
* @return True if the IP should be tempbanned
*/
public boolean shouldTempban(String address) {
if (isEnabled) {
TimedCounter<String> countsByName = ipLoginFailureCounts.get(address);
if (countsByName != null) {
return countsByName.total() >= threshold;
}
}
return false;
}
/**
* Tempban a player's IP address for failing to log in too many times.
* This calculates the expire time based on the time the method was called.
*
* @param player The player to tempban
*/
public void tempbanPlayer(final Player player) {
if (isEnabled) {
final String name = player.getName();
final String ip = PlayerUtils.getPlayerIp(player);
final String reason = messages.retrieveSingle(player, MessageKey.TEMPBAN_MAX_LOGINS);
final Date expires = new Date();
long newTime = expires.getTime() + (length * MILLIS_PER_MINUTE);
expires.setTime(newTime);
bukkitService.runTask(player,() -> { // AuthMeReReloaded - Folia compatibility
if (customCommand.isEmpty()) {
bukkitService.banIp(ip, reason, expires, "AuthMe");
player.kickPlayer(reason);
} else {
String command = customCommand
.replace("%player%", name)
.replace("%ip%", ip);
bukkitService.dispatchConsoleCommand(command);
}
});
ipLoginFailureCounts.remove(ip);
}
}
@Override
public void reload(Settings settings) {
this.isEnabled = settings.getProperty(SecuritySettings.TEMPBAN_ON_MAX_LOGINS);
this.threshold = settings.getProperty(SecuritySettings.MAX_LOGIN_TEMPBAN);
this.length = settings.getProperty(SecuritySettings.TEMPBAN_LENGTH);
this.resetThreshold = settings.getProperty(SecuritySettings.TEMPBAN_MINUTES_BEFORE_RESET);
this.customCommand = settings.getProperty(SecuritySettings.TEMPBAN_CUSTOM_COMMAND);
}
@Override
public void performCleanup() {
for (TimedCounter<String> countsByIp : ipLoginFailureCounts.values()) {
countsByIp.removeExpiredEntries();
}
ipLoginFailureCounts.entrySet().removeIf(e -> e.getValue().isEmpty());
}
}

View File

@ -0,0 +1,194 @@
package fr.xephi.authme.data;
import ch.jalu.datasourcecolumns.data.DataSourceValue;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.initialization.HasCleanup;
import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.mail.EmailService;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.permission.PlayerPermission;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import fr.xephi.authme.util.RandomStringUtils;
import fr.xephi.authme.util.Utils;
import fr.xephi.authme.util.expiring.ExpiringMap;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public class VerificationCodeManager implements SettingsDependent, HasCleanup {
private final EmailService emailService;
private final DataSource dataSource;
private final PermissionsManager permissionsManager;
private final ExpiringMap<String, String> verificationCodes;
private final Set<String> verifiedPlayers;
private boolean canSendMail;
@Inject
VerificationCodeManager(Settings settings, DataSource dataSource, EmailService emailService,
PermissionsManager permissionsManager) {
this.emailService = emailService;
this.dataSource = dataSource;
this.permissionsManager = permissionsManager;
verifiedPlayers = new HashSet<>();
long countTimeout = settings.getProperty(SecuritySettings.VERIFICATION_CODE_EXPIRATION_MINUTES);
verificationCodes = new ExpiringMap<>(countTimeout, TimeUnit.MINUTES);
reload(settings);
}
/**
* Returns if it is possible to send emails
*
* @return true if the service is enabled, false otherwise
*/
public boolean canSendMail() {
return canSendMail;
}
/**
* Returns whether the given player is able to verify his identity
*
* @param player the player to verify
* @return true if the player has not been verified yet, false otherwise
*/
public boolean isVerificationRequired(Player player) {
final String name = player.getName();
return canSendMail
&& !isPlayerVerified(name)
&& permissionsManager.hasPermission(player, PlayerPermission.VERIFICATION_CODE)
&& hasEmail(name);
}
/**
* Returns whether the given player is required to verify his identity through a command
*
* @param name the name of the player to verify
* @return true if the player has an existing code and has not been verified yet, false otherwise
*/
public boolean isCodeRequired(String name) {
return canSendMail && hasCode(name) && !isPlayerVerified(name);
}
/**
* Returns whether the given player has been verified or not
*
* @param name the name of the player to verify
* @return true if the player has been verified, false otherwise
*/
private boolean isPlayerVerified(String name) {
return verifiedPlayers.contains(name.toLowerCase(Locale.ROOT));
}
/**
* Returns if a code exists for the player
*
* @param name the name of the player to verify
* @return true if the code exists, false otherwise
*/
public boolean hasCode(String name) {
return (verificationCodes.get(name.toLowerCase(Locale.ROOT)) != null);
}
/**
* Returns whether the given player is able to receive emails
*
* @param name the name of the player to verify
* @return true if the player is able to receive emails, false otherwise
*/
public boolean hasEmail(String name) {
boolean result = false;
DataSourceValue<String> emailResult = dataSource.getEmail(name);
if (emailResult.rowExists()) {
final String email = emailResult.getValue();
if (!Utils.isEmailEmpty(email)) {
result = true;
}
}
return result;
}
/**
* Check if a code exists for the player or generates and saves a new one.
*
* @param name the player's name
*/
public void codeExistOrGenerateNew(String name) {
if (!hasCode(name)) {
generateCode(name);
}
}
/**
* Generates a code for the player and returns it.
*
* @param name the name of the player to generate a code for
*/
private void generateCode(String name) {
DataSourceValue<String> emailResult = dataSource.getEmail(name);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy'年'MM'月'dd'日' HH:mm:ss");
Date date = new Date(System.currentTimeMillis());
if (emailResult.rowExists()) {
final String email = emailResult.getValue();
if (!Utils.isEmailEmpty(email)) {
String code = RandomStringUtils.generateNum(6); // 6 digits code
verificationCodes.put(name.toLowerCase(Locale.ROOT), code);
emailService.sendVerificationMail(name, email, code, dateFormat.format(date));
}
}
}
/**
* Checks the given code against the existing one.
*
* @param name the name of the player to check
* @param code the supplied code
* @return true if the code matches, false otherwise
*/
public boolean checkCode(String name, String code) {
boolean correct = false;
if (code.equals(verificationCodes.get(name.toLowerCase(Locale.ROOT)))) {
verify(name);
correct = true;
}
return correct;
}
/**
* Add the user to the set of verified users
*
* @param name the name of the player to generate a code for
*/
public void verify(String name) {
verifiedPlayers.add(name.toLowerCase(Locale.ROOT));
}
/**
* Remove the user from the set of verified users
*
* @param name the name of the player to generate a code for
*/
public void unverify(String name){
verifiedPlayers.remove(name.toLowerCase(Locale.ROOT));
}
@Override
public void reload(Settings settings) {
canSendMail = emailService.hasAllInformation();
long countTimeout = settings.getProperty(SecuritySettings.VERIFICATION_CODE_EXPIRATION_MINUTES);
verificationCodes.setExpiration(countTimeout, TimeUnit.MINUTES);
}
@Override
public void performCleanup() {
verificationCodes.removeExpiredEntries();
}
}

View File

@ -0,0 +1,373 @@
package fr.xephi.authme.data.auth;
import fr.xephi.authme.security.crypts.HashedPassword;
import org.bukkit.Location;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* AuthMe player data.
*/
@SuppressWarnings("checkstyle:FinalClass") // Justification: class is mocked in multiple tests
public class PlayerAuth {
/** Default email used in the database if the email column is defined to be NOT NULL. */
public static final String DB_EMAIL_DEFAULT = "your@email.com";
/** Default last login value used in the database if the last login column is NOT NULL. */
public static final long DB_LAST_LOGIN_DEFAULT = 0;
/** Default last ip value used in the database if the last IP column is NOT NULL. */
public static final String DB_LAST_IP_DEFAULT = "127.0.0.1";
/** The player's name in lowercase, e.g. "xephi". */
private String nickname;
/** The player's name in the correct casing, e.g. "Xephi". */
private String realName;
private HashedPassword password;
private String totpKey;
private String email;
private String lastIp;
private int groupId;
private Long lastLogin;
private String registrationIp;
private long registrationDate;
// Fields storing the player's quit location
private double x;
private double y;
private double z;
private String world;
private float yaw;
private float pitch;
private UUID uuid;
/**
* Hidden constructor.
*
* @see #builder()
*/
private PlayerAuth() {
}
public void setNickname(String nickname) {
this.nickname = nickname.toLowerCase(Locale.ROOT);
}
public String getNickname() {
return nickname;
}
public String getRealName() {
return realName;
}
public void setRealName(String realName) {
this.realName = realName;
}
public int getGroupId() {
return groupId;
}
public void setQuitLocation(Location location) {
x = location.getBlockX();
y = location.getBlockY();
z = location.getBlockZ();
world = location.getWorld().getName();
}
public double getQuitLocX() {
return x;
}
public void setQuitLocX(double d) {
this.x = d;
}
public double getQuitLocY() {
return y;
}
public void setQuitLocY(double d) {
this.y = d;
}
public double getQuitLocZ() {
return z;
}
public void setQuitLocZ(double d) {
this.z = d;
}
public String getWorld() {
return world;
}
public void setWorld(String world) {
this.world = world;
}
public float getYaw() {
return yaw;
}
public float getPitch() {
return pitch;
}
public String getLastIp() {
return lastIp;
}
public void setLastIp(String lastIp) {
this.lastIp = lastIp;
}
public Long getLastLogin() {
return lastLogin;
}
public void setLastLogin(long lastLogin) {
this.lastLogin = lastLogin;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public HashedPassword getPassword() {
return password;
}
public void setPassword(HashedPassword password) {
this.password = password;
}
public String getRegistrationIp() {
return registrationIp;
}
public long getRegistrationDate() {
return registrationDate;
}
public void setRegistrationDate(long registrationDate) {
this.registrationDate = registrationDate;
}
public String getTotpKey() {
return totpKey;
}
public void setTotpKey(String totpKey) {
this.totpKey = totpKey;
}
public UUID getUuid() {
return uuid;
}
public void setUuid(UUID uuid) {
this.uuid = uuid;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof PlayerAuth)) {
return false;
}
PlayerAuth other = (PlayerAuth) obj;
return Objects.equals(other.lastIp, this.lastIp) && Objects.equals(other.nickname, this.nickname);
}
@Override
public int hashCode() {
int hashCode = 7;
hashCode = 71 * hashCode + (this.nickname != null ? this.nickname.hashCode() : 0);
hashCode = 71 * hashCode + (this.lastIp != null ? this.lastIp.hashCode() : 0);
return hashCode;
}
@Override
public String toString() {
return "Player : " + nickname + " | " + realName
+ " ! IP : " + lastIp
+ " ! LastLogin : " + lastLogin
+ " ! LastPosition : " + x + "," + y + "," + z + "," + world
+ " ! Email : " + email
+ " ! Password : {" + password.getHash() + ", " + password.getSalt() + "}"
+ " ! UUID : " + uuid;
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private String name;
private String realName;
private HashedPassword password;
private String totpKey;
private String lastIp;
private String email;
private int groupId = -1;
private Long lastLogin;
private String registrationIp;
private Long registrationDate;
private double x;
private double y;
private double z;
private String world;
private float yaw;
private float pitch;
private UUID uuid;
/**
* Creates a PlayerAuth object.
*
* @return the generated PlayerAuth
*/
public PlayerAuth build() {
PlayerAuth auth = new PlayerAuth();
auth.nickname = checkNotNull(name).toLowerCase(Locale.ROOT);
auth.realName = Optional.ofNullable(realName).orElse("Player");
auth.password = Optional.ofNullable(password).orElse(new HashedPassword(""));
auth.totpKey = totpKey;
auth.email = DB_EMAIL_DEFAULT.equals(email) ? null : email;
auth.lastIp = lastIp; // Don't check against default value 127.0.0.1 as it may be a legit value
auth.groupId = groupId;
auth.lastLogin = isEqualTo(lastLogin, DB_LAST_LOGIN_DEFAULT) ? null : lastLogin;
auth.registrationIp = registrationIp;
auth.registrationDate = registrationDate == null ? System.currentTimeMillis() : registrationDate;
auth.x = x;
auth.y = y;
auth.z = z;
auth.world = Optional.ofNullable(world).orElse("world");
auth.yaw = yaw;
auth.pitch = pitch;
auth.uuid = uuid;
return auth;
}
private static boolean isEqualTo(Long value, long defaultValue) {
return value != null && defaultValue == value;
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder realName(String realName) {
this.realName = realName;
return this;
}
public Builder password(HashedPassword password) {
this.password = password;
return this;
}
public Builder password(String hash, String salt) {
return password(new HashedPassword(hash, salt));
}
public Builder totpKey(String totpKey) {
this.totpKey = totpKey;
return this;
}
public Builder lastIp(String lastIp) {
this.lastIp = lastIp;
return this;
}
/**
* Sets the location info based on the argument.
*
* @param location the location info to set
* @return this builder instance
*/
public Builder location(Location location) {
this.x = location.getX();
this.y = location.getY();
this.z = location.getZ();
this.world = location.getWorld().getName();
this.yaw = location.getYaw();
this.pitch = location.getPitch();
return this;
}
public Builder locX(double x) {
this.x = x;
return this;
}
public Builder locY(double y) {
this.y = y;
return this;
}
public Builder locZ(double z) {
this.z = z;
return this;
}
public Builder locWorld(String world) {
this.world = world;
return this;
}
public Builder locYaw(float yaw) {
this.yaw = yaw;
return this;
}
public Builder locPitch(float pitch) {
this.pitch = pitch;
return this;
}
public Builder lastLogin(Long lastLogin) {
this.lastLogin = lastLogin;
return this;
}
public Builder groupId(int groupId) {
this.groupId = groupId;
return this;
}
public Builder email(String email) {
this.email = email;
return this;
}
public Builder registrationIp(String ip) {
this.registrationIp = ip;
return this;
}
public Builder registrationDate(long date) {
this.registrationDate = date;
return this;
}
public Builder uuid(UUID uuid) {
this.uuid = uuid;
return this;
}
}
}

View File

@ -0,0 +1,74 @@
package fr.xephi.authme.data.auth;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Used to manage player's Authenticated status
*/
public class PlayerCache {
private final Map<String, PlayerAuth> cache = new ConcurrentHashMap<>();
PlayerCache() {
}
/**
* Adds the given auth object to the player cache (for the name defined in the PlayerAuth).
*
* @param auth the player auth object to save
*/
public void updatePlayer(PlayerAuth auth) {
cache.put(auth.getNickname().toLowerCase(Locale.ROOT), auth);
}
/**
* Removes a player from the player cache.
*
* @param user name of the player to remove
*/
public void removePlayer(String user) {
cache.remove(user.toLowerCase(Locale.ROOT));
}
/**
* Get whether a player is authenticated (i.e. whether he is present in the player cache).
*
* @param user player's name
*
* @return true if player is logged in, false otherwise.
*/
public boolean isAuthenticated(String user) {
return cache.containsKey(user.toLowerCase(Locale.ROOT));
}
/**
* Returns the PlayerAuth associated with the given user, if available.
*
* @param user name of the player
*
* @return the associated auth object, or null if not available
*/
public PlayerAuth getAuth(String user) {
return cache.get(user.toLowerCase(Locale.ROOT));
}
/**
* @return number of logged in players
*/
public int getLogged() {
return cache.size();
}
/**
* Returns the player cache data.
*
* @return all player auths inside the player cache
*/
public Map<String, PlayerAuth> getCache() {
return this.cache;
}
}

View File

@ -0,0 +1,94 @@
package fr.xephi.authme.data.captcha;
import fr.xephi.authme.util.RandomStringUtils;
import fr.xephi.authme.util.expiring.ExpiringMap;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
/**
* Primitive service for storing captcha codes.
*/
public class CaptchaCodeStorage {
/** Map of captcha codes (with player name as key, case-insensitive). */
private ExpiringMap<String, String> captchaCodes;
/** Number of characters newly generated captcha codes should have. */
private int captchaLength;
/**
* Constructor.
*
* @param expirationInMinutes minutes after which a saved captcha code expires
* @param captchaLength the number of characters a captcha code should have
*/
public CaptchaCodeStorage(long expirationInMinutes, int captchaLength) {
this.captchaCodes = new ExpiringMap<>(expirationInMinutes, TimeUnit.MINUTES);
this.captchaLength = captchaLength;
}
/**
* Sets the expiration of captcha codes.
*
* @param expirationInMinutes minutes after which a saved captcha code expires
*/
public void setExpirationInMinutes(long expirationInMinutes) {
captchaCodes.setExpiration(expirationInMinutes, TimeUnit.MINUTES);
}
/**
* Sets the captcha length.
*
* @param captchaLength number of characters a captcha code should have
*/
public void setCaptchaLength(int captchaLength) {
this.captchaLength = captchaLength;
}
/**
* Returns the stored captcha for the player or generates and saves a new one.
*
* @param name the player's name
* @return the code the player is required to enter
*/
public String getCodeOrGenerateNew(String name) {
String code = captchaCodes.get(name.toLowerCase(Locale.ROOT));
return code == null ? generateCode(name) : code;
}
/**
* Generates a code for the player and returns it.
*
* @param name the name of the player to generate a code for
* @return the generated code
*/
private String generateCode(String name) {
String code = RandomStringUtils.generate(captchaLength);
captchaCodes.put(name.toLowerCase(Locale.ROOT), code);
return code;
}
/**
* Checks the given code against the existing one. Upon success, the saved captcha code is removed from storage.
* Upon failure, a new code is generated.
*
* @param name the name of the player to check
* @param code the supplied code
* @return true if the code matches, false otherwise
*/
public boolean checkCode(String name, String code) {
String nameLowerCase = name.toLowerCase(Locale.ROOT);
String savedCode = captchaCodes.get(nameLowerCase);
if (savedCode != null && savedCode.equalsIgnoreCase(code)) {
captchaCodes.remove(nameLowerCase);
return true;
} else {
generateCode(name);
}
return false;
}
public void removeExpiredEntries() {
captchaCodes.removeExpiredEntries();
}
}

View File

@ -0,0 +1,38 @@
package fr.xephi.authme.data.captcha;
import org.bukkit.entity.Player;
/**
* Manages captcha codes.
*/
public interface CaptchaManager {
/**
* Returns whether the given player is required to solve a captcha.
*
* @param name the name of the player to verify
* @return true if the player has to solve a captcha, false otherwise
*/
boolean isCaptchaRequired(String name);
/**
* Returns the stored captcha for the player or generates and saves a new one.
*
* @param name the player's name
* @return the code the player is required to enter
*/
String getCaptchaCodeOrGenerateNew(String name);
/**
* Checks the given code against the existing one. This method is not reentrant, i.e. it performs additional
* state changes on success or failure, such as modifying some counter or setting a player as verified.
* <p>
* On success, the code associated with the player is cleared; on failure, a new code is generated.
*
* @param player the player to check
* @param code the supplied code
* @return true if the code matches, false otherwise
*/
boolean checkCode(Player player, String code);
}

View File

@ -0,0 +1,95 @@
package fr.xephi.authme.data.captcha;
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.expiring.TimedCounter;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
/**
* Manager for the handling of captchas after too many failed login attempts.
*/
public class LoginCaptchaManager implements CaptchaManager, SettingsDependent, HasCleanup {
private final TimedCounter<String> playerCounts;
private final CaptchaCodeStorage captchaCodeStorage;
private boolean isEnabled;
private int threshold;
@Inject
LoginCaptchaManager(Settings settings) {
// Note: Proper values are set in reload()
this.captchaCodeStorage = new CaptchaCodeStorage(30, 4);
this.playerCounts = new TimedCounter<>(9, TimeUnit.MINUTES);
reload(settings);
}
/**
* Increases the failure count for the given player.
*
* @param name the player's name
*/
public void increaseLoginFailureCount(String name) {
if (isEnabled) {
String playerLower = name.toLowerCase(Locale.ROOT);
playerCounts.increment(playerLower);
}
}
@Override
public boolean isCaptchaRequired(String playerName) {
return isEnabled && playerCounts.get(playerName.toLowerCase(Locale.ROOT)) >= threshold;
}
@Override
public String getCaptchaCodeOrGenerateNew(String name) {
return captchaCodeStorage.getCodeOrGenerateNew(name);
}
@Override
public boolean checkCode(Player player, String code) {
String nameLower = player.getName().toLowerCase(Locale.ROOT);
boolean isCodeCorrect = captchaCodeStorage.checkCode(nameLower, code);
if (isCodeCorrect) {
playerCounts.remove(nameLower);
}
return isCodeCorrect;
}
/**
* Resets the login count of the given player to 0.
*
* @param name the player's name
*/
public void resetLoginFailureCount(String name) {
if (isEnabled) {
playerCounts.remove(name.toLowerCase(Locale.ROOT));
}
}
@Override
public void reload(Settings settings) {
int expirationInMinutes = settings.getProperty(SecuritySettings.CAPTCHA_COUNT_MINUTES_BEFORE_RESET);
captchaCodeStorage.setExpirationInMinutes(expirationInMinutes);
int captchaLength = settings.getProperty(SecuritySettings.CAPTCHA_LENGTH);
captchaCodeStorage.setCaptchaLength(captchaLength);
int countTimeout = settings.getProperty(SecuritySettings.CAPTCHA_COUNT_MINUTES_BEFORE_RESET);
playerCounts.setExpiration(countTimeout, TimeUnit.MINUTES);
isEnabled = settings.getProperty(SecuritySettings.ENABLE_LOGIN_FAILURE_CAPTCHA);
threshold = settings.getProperty(SecuritySettings.MAX_LOGIN_TRIES_BEFORE_CAPTCHA);
}
@Override
public void performCleanup() {
playerCounts.removeExpiredEntries();
captchaCodeStorage.removeExpiredEntries();
}
}

View File

@ -0,0 +1,66 @@
package fr.xephi.authme.data.captcha;
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.expiring.ExpiringSet;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
/**
* Captcha manager for registration.
*/
public class RegistrationCaptchaManager implements CaptchaManager, SettingsDependent, HasCleanup {
private static final int MINUTES_VALID_FOR_REGISTRATION = 30;
private final ExpiringSet<String> verifiedNamesForRegistration;
private final CaptchaCodeStorage captchaCodeStorage;
private boolean isEnabled;
@Inject
RegistrationCaptchaManager(Settings settings) {
// NOTE: proper captcha length is set in reload()
this.captchaCodeStorage = new CaptchaCodeStorage(MINUTES_VALID_FOR_REGISTRATION, 4);
this.verifiedNamesForRegistration = new ExpiringSet<>(MINUTES_VALID_FOR_REGISTRATION, TimeUnit.MINUTES);
reload(settings);
}
@Override
public boolean isCaptchaRequired(String name) {
return isEnabled && !verifiedNamesForRegistration.contains(name.toLowerCase(Locale.ROOT));
}
@Override
public String getCaptchaCodeOrGenerateNew(String name) {
return captchaCodeStorage.getCodeOrGenerateNew(name);
}
@Override
public boolean checkCode(Player player, String code) {
String nameLower = player.getName().toLowerCase(Locale.ROOT);
boolean isCodeCorrect = captchaCodeStorage.checkCode(nameLower, code);
if (isCodeCorrect) {
verifiedNamesForRegistration.add(nameLower);
}
return isCodeCorrect;
}
@Override
public void reload(Settings settings) {
int captchaLength = settings.getProperty(SecuritySettings.CAPTCHA_LENGTH);
captchaCodeStorage.setCaptchaLength(captchaLength);
isEnabled = settings.getProperty(SecuritySettings.ENABLE_CAPTCHA_FOR_REGISTRATION);
}
@Override
public void performCleanup() {
verifiedNamesForRegistration.removeExpiredEntries();
captchaCodeStorage.removeExpiredEntries();
}
}

View File

@ -0,0 +1,66 @@
package fr.xephi.authme.data.limbo;
import org.bukkit.entity.Player;
/**
* Possible types to restore the "allow flight" property
* from LimboPlayer to Bukkit Player.
*/
public enum AllowFlightRestoreType {
/** Set value from LimboPlayer to Player. */
RESTORE {
@Override
public void restoreAllowFlight(Player player, LimboPlayer limbo) {
player.setAllowFlight(limbo.isCanFly());
}
},
/** Always set flight enabled to true. */
ENABLE {
@Override
public void restoreAllowFlight(Player player, LimboPlayer limbo) {
player.setAllowFlight(true);
}
},
/** Always set flight enabled to false. */
DISABLE {
@Override
public void restoreAllowFlight(Player player, LimboPlayer limbo) {
player.setAllowFlight(false);
}
},
/** The user's flight handling is not modified. */
NOTHING {
@Override
public void restoreAllowFlight(Player player, LimboPlayer limbo) {
// noop
}
@Override
public void processPlayer(Player player) {
// noop
}
};
/**
* Restores the "allow flight" property from the LimboPlayer to the Player.
* This method behaves differently for each restoration type.
*
* @param player the player to modify
* @param limbo the limbo player to read from
*/
public abstract void restoreAllowFlight(Player player, LimboPlayer limbo);
/**
* Processes the player when a LimboPlayer instance is created based on him. Typically this
* method revokes the {@code allowFlight} property to be restored again later.
*
* @param player the player to process
*/
public void processPlayer(Player player) {
player.setAllowFlight(false);
}
}

View File

@ -0,0 +1,114 @@
package fr.xephi.authme.data.limbo;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.initialization.Reloadable;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.PluginSettings;
import org.bukkit.entity.Player;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.util.Collection;
import java.util.Collections;
/**
* Changes the permission group according to the auth status of the player and the configuration.
* <p>
* If this feature is enabled, the <i>primary permissions group</i> of a player is replaced until he has
* logged in. Some permission plugins have a notion of a primary group; for other permission plugins the
* first group is simply taken.
* <p>
* The groups that are used as replacement until the player logs in is configurable and depends on if
* the player is registered or not. Note that some (all?) permission systems require the group to actually
* exist for the replacement to take place. Furthermore, since some permission groups require that players
* be in at least one group, this will mean that the player is not removed from his primary group.
*/
class AuthGroupHandler implements Reloadable {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(AuthGroupHandler.class);
@Inject
private PermissionsManager permissionsManager;
@Inject
private Settings settings;
private UserGroup unregisteredGroup;
private UserGroup registeredGroup;
AuthGroupHandler() {
}
/**
* Sets the group of a player by its authentication status.
*
* @param player the player
* @param limbo the associated limbo player (nullable)
* @param groupType the group type
*/
void setGroup(Player player, LimboPlayer limbo, AuthGroupType groupType) {
if (!useAuthGroups()) {
return;
}
Collection<UserGroup> previousGroups = limbo == null ? Collections.emptyList() : limbo.getGroups();
switch (groupType) {
// Implementation note: some permission systems don't support players not being in any group,
// so add the new group before removing the old ones
case UNREGISTERED:
permissionsManager.addGroup(player, unregisteredGroup);
permissionsManager.removeGroup(player, registeredGroup);
permissionsManager.removeGroups(player, previousGroups);
break;
case REGISTERED_UNAUTHENTICATED:
permissionsManager.addGroup(player, registeredGroup);
permissionsManager.removeGroup(player, unregisteredGroup);
permissionsManager.removeGroups(player, previousGroups);
break;
case LOGGED_IN:
permissionsManager.addGroups(player, previousGroups);
permissionsManager.removeGroup(player, unregisteredGroup);
permissionsManager.removeGroup(player, registeredGroup);
break;
default:
throw new IllegalStateException("Encountered unhandled auth group type '" + groupType + "'");
}
logger.debug(() -> player.getName() + " changed to "
+ groupType + ": has groups " + permissionsManager.getGroups(player));
}
/**
* Returns whether the auth permissions group function should be used.
*
* @return true if should be used, false otherwise
*/
private boolean useAuthGroups() {
// Check whether the permissions check is enabled
if (!settings.getProperty(PluginSettings.ENABLE_PERMISSION_CHECK)) {
return false;
}
// Make sure group support is available
if (!permissionsManager.hasGroupSupport()) {
logger.warning("The current permissions system doesn't have group support, unable to set group!");
return false;
}
return true;
}
@Override
@PostConstruct
public void reload() {
unregisteredGroup = new UserGroup(settings.getProperty(PluginSettings.UNREGISTERED_GROUP));
registeredGroup = new UserGroup(settings.getProperty(PluginSettings.REGISTERED_GROUP));
}
}

View File

@ -0,0 +1,17 @@
package fr.xephi.authme.data.limbo;
/**
* Represents the group type based on the user's auth status.
*/
enum AuthGroupType {
/** Player does not have an account. */
UNREGISTERED,
/** Player is registered but not logged in. */
REGISTERED_UNAUTHENTICATED,
/** Player is logged in. */
LOGGED_IN
}

View File

@ -0,0 +1,11 @@
package fr.xephi.authme.data.limbo;
public enum LimboMessageType {
REGISTER,
LOG_IN,
TOTP_CODE
}

View File

@ -0,0 +1,138 @@
package fr.xephi.authme.data.limbo;
import com.github.Anon8281.universalScheduler.scheduling.tasks.MyScheduledTask;
import fr.xephi.authme.task.MessageTask;
import org.bukkit.Location;
import java.util.ArrayList;
import java.util.Collection;
/**
* Represents a player which is not logged in and keeps track of certain states (like OP status, flying)
* which may be revoked from the player until he has logged in or registered.
*/
public class LimboPlayer {
public static final float DEFAULT_WALK_SPEED = 0.2f;
public static final float DEFAULT_FLY_SPEED = 0.1f;
private final boolean canFly;
private final boolean operator;
private final Collection<UserGroup> groups;
private final Location loc;
private final float walkSpeed;
private final float flySpeed;
private MyScheduledTask timeoutTask = null;
private MessageTask messageTask = null;
private LimboPlayerState state = LimboPlayerState.PASSWORD_REQUIRED;
public LimboPlayer(Location loc, boolean operator, Collection<UserGroup> groups, boolean fly, float walkSpeed,
float flySpeed) {
this.loc = loc;
this.operator = operator;
this.groups = new ArrayList<>(groups); // prevent bug #2413
this.canFly = fly;
this.walkSpeed = walkSpeed;
this.flySpeed = flySpeed;
}
/**
* Return the player's original location.
*
* @return The player's location
*/
public Location getLocation() {
return loc;
}
/**
* Return whether the player is an operator or not (i.e. whether he is an OP).
*
* @return True if the player has OP status, false otherwise
*/
public boolean isOperator() {
return operator;
}
/**
* Return the player's permissions groups.
*
* @return The permissions groups the player belongs to
*/
public Collection<UserGroup> getGroups() {
return groups;
}
public boolean isCanFly() {
return canFly;
}
public float getWalkSpeed() {
return walkSpeed;
}
public float getFlySpeed() {
return flySpeed;
}
/**
* Return the timeout task, which kicks the player if he hasn't registered or logged in
* after a configurable amount of time.
*
* @return The timeout task associated to the player
*/
public MyScheduledTask getTimeoutTask() {
return timeoutTask;
}
/**
* Set the timeout task of the player. The timeout task kicks the player after a configurable
* amount of time if he hasn't logged in or registered.
*
* @param timeoutTask The task to set
*/
public void setTimeoutTask(MyScheduledTask timeoutTask) {
if (this.timeoutTask != null) {
this.timeoutTask.cancel();
}
this.timeoutTask = timeoutTask;
}
/**
* Return the message task reminding the player to log in or register.
*
* @return The task responsible for sending the message regularly
*/
public MessageTask getMessageTask() {
return messageTask;
}
/**
* Set the messages task responsible for telling the player to log in or register.
*
* @param messageTask The message task to set
*/
public void setMessageTask(MessageTask messageTask) {
if (this.messageTask != null) {
this.messageTask.cancel();
}
this.messageTask = messageTask;
}
/**
* Clears all tasks associated to the player.
*/
public void clearTasks() {
setMessageTask(null);
setTimeoutTask(null);
}
public LimboPlayerState getState() {
return state;
}
public void setState(LimboPlayerState state) {
this.state = state;
}
}

View File

@ -0,0 +1,9 @@
package fr.xephi.authme.data.limbo;
public enum LimboPlayerState {
PASSWORD_REQUIRED,
TOTP_REQUIRED
}

View File

@ -0,0 +1,117 @@
package fr.xephi.authme.data.limbo;
import com.github.Anon8281.universalScheduler.scheduling.tasks.MyScheduledTask;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.data.captcha.RegistrationCaptchaManager;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.message.Messages;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.RegistrationSettings;
import fr.xephi.authme.settings.properties.RestrictionSettings;
import fr.xephi.authme.task.MessageTask;
import fr.xephi.authme.task.TimeoutTask;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import static fr.xephi.authme.service.BukkitService.TICKS_PER_SECOND;
/**
* Registers tasks associated with a LimboPlayer.
*/
class LimboPlayerTaskManager {
@Inject
private Messages messages;
@Inject
private Settings settings;
@Inject
private BukkitService bukkitService;
@Inject
private PlayerCache playerCache;
@Inject
private RegistrationCaptchaManager registrationCaptchaManager;
LimboPlayerTaskManager() {
}
/**
* Registers a {@link MessageTask} for the given player name.
*
* @param player the player
* @param limbo the associated limbo player of the player
* @param messageType message type
*/
void registerMessageTask(Player player, LimboPlayer limbo, LimboMessageType messageType) {
int interval = settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL);
MessageResult result = getMessageKey(player.getName(), messageType);
if (interval > 0) {
String[] joinMessage = messages.retrieveSingle(player, result.messageKey, result.args).split("\n");
MessageTask messageTask = new MessageTask(player, joinMessage);
bukkitService.runTaskTimer(messageTask, 2 * TICKS_PER_SECOND, (long) interval * TICKS_PER_SECOND);
limbo.setMessageTask(messageTask);
}
}
/**
* Registers a {@link TimeoutTask} for the given player according to the configuration.
*
* @param player the player to register a timeout task for
* @param limbo the associated limbo player
*/
void registerTimeoutTask(Player player, LimboPlayer limbo) {
final int timeout = settings.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND;
if (timeout > 0) {
String message = messages.retrieveSingle(player, MessageKey.LOGIN_TIMEOUT_ERROR);
MyScheduledTask task = bukkitService.runTaskLater(new TimeoutTask(player, message, playerCache), timeout);
limbo.setTimeoutTask(task);
}
}
/**
* Null-safe method to set the muted flag on a message task.
*
* @param task the task to modify (or null)
* @param isMuted the value to set if task is not null
*/
static void setMuted(MessageTask task, boolean isMuted) {
if (task != null) {
task.setMuted(isMuted);
}
}
/**
* Returns the appropriate message key according to the registration status and settings.
*
* @param name the player's name
* @param messageType the message to show
* @return the message key to display to the user
*/
private MessageResult getMessageKey(String name, LimboMessageType messageType) {
if (messageType == LimboMessageType.LOG_IN) {
return new MessageResult(MessageKey.LOGIN_MESSAGE);
} else if (messageType == LimboMessageType.TOTP_CODE) {
return new MessageResult(MessageKey.TWO_FACTOR_CODE_REQUIRED);
} else if (registrationCaptchaManager.isCaptchaRequired(name)) {
final String captchaCode = registrationCaptchaManager.getCaptchaCodeOrGenerateNew(name);
return new MessageResult(MessageKey.CAPTCHA_FOR_REGISTRATION_REQUIRED, captchaCode);
} else {
return new MessageResult(MessageKey.REGISTER_MESSAGE);
}
}
private static final class MessageResult {
private final MessageKey messageKey;
private final String[] args;
MessageResult(MessageKey messageKey, String... args) {
this.messageKey = messageKey;
this.args = args;
}
}
}

Some files were not shown because too many files have changed in this diff Show More