reupload files

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