Remove all files

This commit is contained in:
HaHaWTH
2023-09-20 02:10:19 +08:00
parent 62f27f8231
commit ed320196fe
478 changed files with 0 additions and 94591 deletions
@@ -1,49 +0,0 @@
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();
}
}
@@ -1,75 +0,0 @@
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();
}
}
@@ -1,138 +0,0 @@
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.scheduleSyncDelayedTask(() -> {
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());
}
}
@@ -1,194 +0,0 @@
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();
}
}
@@ -1,373 +0,0 @@
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;
}
}
}
@@ -1,74 +0,0 @@
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;
}
}
@@ -1,94 +0,0 @@
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();
}
}
@@ -1,38 +0,0 @@
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);
}
@@ -1,95 +0,0 @@
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();
}
}
@@ -1,66 +0,0 @@
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();
}
}
@@ -1,66 +0,0 @@
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);
}
}
@@ -1,114 +0,0 @@
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));
}
}
@@ -1,17 +0,0 @@
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
}
@@ -1,11 +0,0 @@
package fr.xephi.authme.data.limbo;
public enum LimboMessageType {
REGISTER,
LOG_IN,
TOTP_CODE
}
@@ -1,137 +0,0 @@
package fr.xephi.authme.data.limbo;
import fr.xephi.authme.task.MessageTask;
import org.bukkit.Location;
import org.bukkit.scheduler.BukkitTask;
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 BukkitTask 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 BukkitTask 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(BukkitTask 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;
}
}
@@ -1,9 +0,0 @@
package fr.xephi.authme.data.limbo;
public enum LimboPlayerState {
PASSWORD_REQUIRED,
TOTP_REQUIRED
}
@@ -1,117 +0,0 @@
package fr.xephi.authme.data.limbo;
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 org.bukkit.scheduler.BukkitTask;
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, 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);
BukkitTask 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;
}
}
}
@@ -1,188 +0,0 @@
package fr.xephi.authme.data.limbo;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.persistence.LimboPersistence;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.SpawnLoader;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import static fr.xephi.authme.settings.properties.LimboSettings.RESTORE_ALLOW_FLIGHT;
import static fr.xephi.authme.settings.properties.LimboSettings.RESTORE_FLY_SPEED;
import static fr.xephi.authme.settings.properties.LimboSettings.RESTORE_WALK_SPEED;
/**
* Service for managing players that are in "limbo," a temporary state players are
* put in which have joined but not yet logged in.
*/
public class LimboService {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(LimboService.class);
private final Map<String, LimboPlayer> entries = new ConcurrentHashMap<>();
@Inject
private Settings settings;
@Inject
private LimboPlayerTaskManager taskManager;
@Inject
private LimboServiceHelper helper;
@Inject
private LimboPersistence persistence;
@Inject
private AuthGroupHandler authGroupHandler;
@Inject
private SpawnLoader spawnLoader;
LimboService() {
}
/**
* Creates a LimboPlayer for the given player and revokes all "limbo data" from the player.
*
* @param player the player to process
* @param isRegistered whether or not the player is registered
*/
public void createLimboPlayer(Player player, boolean isRegistered) {
final String name = player.getName().toLowerCase(Locale.ROOT);
LimboPlayer limboFromDisk = persistence.getLimboPlayer(player);
if (limboFromDisk != null) {
logger.debug("LimboPlayer for `{0}` already exists on disk", name);
}
LimboPlayer existingLimbo = entries.remove(name);
if (existingLimbo != null) {
existingLimbo.clearTasks();
logger.debug("LimboPlayer for `{0}` already present in memory", name);
}
Location location = spawnLoader.getPlayerLocationOrSpawn(player);
LimboPlayer limboPlayer = helper.merge(existingLimbo, limboFromDisk);
limboPlayer = helper.merge(helper.createLimboPlayer(player, isRegistered, location), limboPlayer);
taskManager.registerMessageTask(player, limboPlayer,
isRegistered ? LimboMessageType.LOG_IN : LimboMessageType.REGISTER);
taskManager.registerTimeoutTask(player, limboPlayer);
helper.revokeLimboStates(player);
authGroupHandler.setGroup(player, limboPlayer,
isRegistered ? AuthGroupType.REGISTERED_UNAUTHENTICATED : AuthGroupType.UNREGISTERED);
entries.put(name, limboPlayer);
persistence.saveLimboPlayer(player, limboPlayer);
}
/**
* Returns the limbo player for the given name, or null otherwise.
*
* @param name the name to retrieve the data for
* @return the associated limbo player, or null if none available
*/
public LimboPlayer getLimboPlayer(String name) {
return entries.get(name.toLowerCase(Locale.ROOT));
}
/**
* Returns whether there is a limbo player for the given name.
*
* @param name the name to check
* @return true if present, false otherwise
*/
public boolean hasLimboPlayer(String name) {
return entries.containsKey(name.toLowerCase(Locale.ROOT));
}
/**
* Restores the limbo data and subsequently deletes the entry.
* <p>
* Note that teleportation on the player is performed by {@link fr.xephi.authme.service.TeleportationService} and
* changing the permission group is handled by {@link fr.xephi.authme.data.limbo.AuthGroupHandler}.
*
* @param player the player whose data should be restored
*/
public void restoreData(Player player) {
String lowerName = player.getName().toLowerCase(Locale.ROOT);
LimboPlayer limbo = entries.remove(lowerName);
if (limbo == null) {
logger.debug("No LimboPlayer found for `{0}` - cannot restore", lowerName);
} else {
player.setOp(limbo.isOperator());
settings.getProperty(RESTORE_ALLOW_FLIGHT).restoreAllowFlight(player, limbo);
settings.getProperty(RESTORE_FLY_SPEED).restoreFlySpeed(player, limbo);
settings.getProperty(RESTORE_WALK_SPEED).restoreWalkSpeed(player, limbo);
limbo.clearTasks();
logger.debug("Restored LimboPlayer stats for `{0}`", lowerName);
persistence.removeLimboPlayer(player);
}
authGroupHandler.setGroup(player, limbo, AuthGroupType.LOGGED_IN);
}
/**
* Creates new tasks for the given player and cancels the old ones for a newly registered player.
* This resets his time to log in (TimeoutTask) and updates the message he is shown (MessageTask).
*
* @param player the player to reset the tasks for
*/
public void replaceTasksAfterRegistration(Player player) {
Optional<LimboPlayer> limboPlayer = getLimboOrLogError(player, "reset tasks");
limboPlayer.ifPresent(limbo -> {
taskManager.registerTimeoutTask(player, limbo);
taskManager.registerMessageTask(player, limbo, LimboMessageType.LOG_IN);
});
authGroupHandler.setGroup(player, limboPlayer.orElse(null), AuthGroupType.REGISTERED_UNAUTHENTICATED);
}
/**
* Resets the message task associated with the player's LimboPlayer.
*
* @param player the player to set a new message task for
* @param messageType the message to show for the limbo player
*/
public void resetMessageTask(Player player, LimboMessageType messageType) {
getLimboOrLogError(player, "reset message task")
.ifPresent(limbo -> taskManager.registerMessageTask(player, limbo, messageType));
}
/**
* @param player the player whose message task should be muted
*/
public void muteMessageTask(Player player) {
getLimboOrLogError(player, "mute message task")
.ifPresent(limbo -> LimboPlayerTaskManager.setMuted(limbo.getMessageTask(), true));
}
/**
* @param player the player whose message task should be unmuted
*/
public void unmuteMessageTask(Player player) {
getLimboOrLogError(player, "unmute message task")
.ifPresent(limbo -> LimboPlayerTaskManager.setMuted(limbo.getMessageTask(), false));
}
/**
* Returns the limbo player for the given player or logs an error.
*
* @param player the player to retrieve the limbo player for
* @param context the action for which the limbo player is being retrieved (for logging)
* @return Optional with the limbo player
*/
private Optional<LimboPlayer> getLimboOrLogError(Player player, String context) {
LimboPlayer limbo = entries.get(player.getName().toLowerCase(Locale.ROOT));
if (limbo == null) {
logger.debug("No LimboPlayer found for `{0}`. Action: {1}", player.getName(), context);
}
return Optional.ofNullable(limbo);
}
}
@@ -1,115 +0,0 @@
package fr.xephi.authme.data.limbo;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.LimboSettings;
import fr.xephi.authme.settings.properties.RestrictionSettings;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import static fr.xephi.authme.util.Utils.isCollectionEmpty;
import static java.util.stream.Collectors.toList;
/**
* Helper class for the LimboService.
*/
class LimboServiceHelper {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(LimboServiceHelper.class);
@Inject
private PermissionsManager permissionsManager;
@Inject
private Settings settings;
/**
* Creates a LimboPlayer with the given player's details.
*
* @param player the player to process
* @param isRegistered whether the player is registered
* @param location the player location
* @return limbo player with the player's data
*/
LimboPlayer createLimboPlayer(Player player, boolean isRegistered, Location location) {
// For safety reasons an unregistered player should not have OP status after registration
boolean isOperator = isRegistered && player.isOp();
boolean flyEnabled = player.getAllowFlight();
float walkSpeed = player.getWalkSpeed();
float flySpeed = player.getFlySpeed();
Collection<UserGroup> playerGroups = permissionsManager.hasGroupSupport()
? permissionsManager.getGroups(player) : Collections.emptyList();
List<String> groupNames = playerGroups.stream()
.map(UserGroup::getGroupName)
.collect(toList());
logger.debug("Player `{0}` has groups `{1}`", player.getName(), String.join(", ", groupNames));
return new LimboPlayer(location, isOperator, playerGroups, flyEnabled, walkSpeed, flySpeed);
}
/**
* Removes the data that is saved in a LimboPlayer from the player.
* <p>
* Note that teleportation on the player is performed by {@link fr.xephi.authme.service.TeleportationService} and
* changing the permission group is handled by {@link fr.xephi.authme.data.limbo.AuthGroupHandler}.
*
* @param player the player to set defaults to
*/
void revokeLimboStates(Player player) {
player.setOp(false);
settings.getProperty(LimboSettings.RESTORE_ALLOW_FLIGHT)
.processPlayer(player);
if (!settings.getProperty(RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT)) {
player.setFlySpeed(0.0f);
player.setWalkSpeed(0.0f);
}
}
/**
* Merges two existing LimboPlayer instances of a player. Merging is done the following way:
* <ul>
* <li><code>isOperator, allowFlight</code>: true if either limbo has true</li>
* <li><code>flySpeed, walkSpeed</code>: maximum value of either limbo player</li>
* <li><code>groups, location</code>: from old limbo if not empty/null, otherwise from new limbo</li>
* </ul>
*
* @param newLimbo the new limbo player
* @param oldLimbo the old limbo player
* @return merged limbo player if both arguments are not null, otherwise the first non-null argument
*/
LimboPlayer merge(LimboPlayer newLimbo, LimboPlayer oldLimbo) {
if (newLimbo == null) {
return oldLimbo;
} else if (oldLimbo == null) {
return newLimbo;
}
boolean isOperator = newLimbo.isOperator() || oldLimbo.isOperator();
boolean canFly = newLimbo.isCanFly() || oldLimbo.isCanFly();
float flySpeed = Math.max(newLimbo.getFlySpeed(), oldLimbo.getFlySpeed());
float walkSpeed = Math.max(newLimbo.getWalkSpeed(), oldLimbo.getWalkSpeed());
Collection<UserGroup> groups = getLimboGroups(oldLimbo.getGroups(), newLimbo.getGroups());
Location location = firstNotNull(oldLimbo.getLocation(), newLimbo.getLocation());
return new LimboPlayer(location, isOperator, groups, canFly, walkSpeed, flySpeed);
}
private static Location firstNotNull(Location first, Location second) {
return first == null ? second : first;
}
private Collection<UserGroup> getLimboGroups(Collection<UserGroup> oldLimboGroups,
Collection<UserGroup> newLimboGroups) {
logger.debug("Limbo merge: new and old groups are `{0}` and `{1}`", newLimboGroups, oldLimboGroups);
return isCollectionEmpty(oldLimboGroups) ? newLimboGroups : oldLimboGroups;
}
}
@@ -1,45 +0,0 @@
package fr.xephi.authme.data.limbo;
import java.util.Map;
import java.util.Objects;
public class UserGroup {
private String groupName;
private Map<String, String> contextMap;
public UserGroup(String groupName) {
this.groupName = groupName;
}
public UserGroup(String groupName, Map<String, String> contextMap) {
this.groupName = groupName;
this.contextMap = contextMap;
}
public String getGroupName() {
return groupName;
}
public Map<String, String> getContextMap() {
return contextMap;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
UserGroup userGroup = (UserGroup) o;
return Objects.equals(groupName, userGroup.groupName)
&& Objects.equals(contextMap, userGroup.contextMap);
}
@Override
public int hashCode() {
return Objects.hash(groupName, contextMap);
}
}
@@ -1,123 +0,0 @@
package fr.xephi.authme.data.limbo;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import org.bukkit.entity.Player;
/**
* Possible types to restore the walk and fly speed from LimboPlayer
* back to Bukkit Player.
*/
public enum WalkFlySpeedRestoreType {
/**
* Restores from LimboPlayer to Player.
*/
RESTORE {
@Override
public void restoreFlySpeed(Player player, LimboPlayer limbo) {
logger.debug(() -> "Restoring fly speed for LimboPlayer " + player.getName() + " to "
+ limbo.getFlySpeed() + " (RESTORE mode)");
player.setFlySpeed(limbo.getFlySpeed());
}
@Override
public void restoreWalkSpeed(Player player, LimboPlayer limbo) {
logger.debug(() -> "Restoring walk speed for LimboPlayer " + player.getName() + " to "
+ limbo.getWalkSpeed() + " (RESTORE mode)");
player.setWalkSpeed(limbo.getWalkSpeed());
}
},
/**
* Restores from LimboPlayer, using the default speed if the speed on LimboPlayer is 0.
*/
RESTORE_NO_ZERO {
@Override
public void restoreFlySpeed(Player player, LimboPlayer limbo) {
float limboFlySpeed = limbo.getFlySpeed();
if (limboFlySpeed > 0.01f) {
logger.debug(() -> "Restoring fly speed for LimboPlayer " + player.getName() + " to "
+ limboFlySpeed + " (RESTORE_NO_ZERO mode)");
player.setFlySpeed(limboFlySpeed);
} else {
logger.debug(() -> "Restoring fly speed for LimboPlayer " + player.getName()
+ " to DEFAULT, it was 0! (RESTORE_NO_ZERO mode)");
player.setFlySpeed(LimboPlayer.DEFAULT_FLY_SPEED);
}
}
@Override
public void restoreWalkSpeed(Player player, LimboPlayer limbo) {
float limboWalkSpeed = limbo.getWalkSpeed();
if (limboWalkSpeed > 0.01f) {
logger.debug(() -> "Restoring walk speed for LimboPlayer " + player.getName() + " to "
+ limboWalkSpeed + " (RESTORE_NO_ZERO mode)");
player.setWalkSpeed(limboWalkSpeed);
} else {
logger.debug(() -> "Restoring walk speed for LimboPlayer " + player.getName() + ""
+ " to DEFAULT, it was 0! (RESTORE_NO_ZERO mode)");
player.setWalkSpeed(LimboPlayer.DEFAULT_WALK_SPEED);
}
}
},
/**
* Uses the max speed of Player (current speed) and the LimboPlayer.
*/
MAX_RESTORE {
@Override
public void restoreFlySpeed(Player player, LimboPlayer limbo) {
float newSpeed = Math.max(player.getFlySpeed(), limbo.getFlySpeed());
logger.debug(() -> "Restoring fly speed for LimboPlayer " + player.getName() + " to " + newSpeed
+ " (Current: " + player.getFlySpeed() + ", Limbo: " + limbo.getFlySpeed() + ") (MAX_RESTORE mode)");
player.setFlySpeed(newSpeed);
}
@Override
public void restoreWalkSpeed(Player player, LimboPlayer limbo) {
float newSpeed = Math.max(player.getWalkSpeed(), limbo.getWalkSpeed());
logger.debug(() -> "Restoring walk speed for LimboPlayer " + player.getName() + " to " + newSpeed
+ " (Current: " + player.getWalkSpeed() + ", Limbo: " + limbo.getWalkSpeed() + ") (MAX_RESTORE mode)");
player.setWalkSpeed(newSpeed);
}
},
/**
* Always sets the default speed to the player.
*/
DEFAULT {
@Override
public void restoreFlySpeed(Player player, LimboPlayer limbo) {
logger.debug(() -> "Restoring fly speed for LimboPlayer " + player.getName()
+ " to DEFAULT (DEFAULT mode)");
player.setFlySpeed(LimboPlayer.DEFAULT_FLY_SPEED);
}
@Override
public void restoreWalkSpeed(Player player, LimboPlayer limbo) {
logger.debug(() -> "Restoring walk speed for LimboPlayer " + player.getName()
+ " to DEFAULT (DEFAULT mode)");
player.setWalkSpeed(LimboPlayer.DEFAULT_WALK_SPEED);
}
};
private static final ConsoleLogger logger = ConsoleLoggerFactory.get(WalkFlySpeedRestoreType.class);
/**
* Restores the fly speed from Limbo to Player according to the restoration type.
*
* @param player the player to modify
* @param limbo the limbo player to read from
*/
public abstract void restoreFlySpeed(Player player, LimboPlayer limbo);
/**
* Restores the walk speed from Limbo to Player according to the restoration type.
*
* @param player the player to modify
* @param limbo the limbo player to read from
*/
public abstract void restoreWalkSpeed(Player player, LimboPlayer limbo);
}
@@ -1,228 +0,0 @@
package fr.xephi.authme.data.limbo.persistence;
import com.google.common.io.Files;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.LimboPlayer;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.LimboSettings;
import fr.xephi.authme.util.FileUtils;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.io.File;
import java.io.FileWriter;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Persistence handler for LimboPlayer objects by distributing the objects to store
* in various segments (buckets) based on the start of the player's UUID.
*/
class DistributedFilesPersistenceHandler implements LimboPersistenceHandler {
private static final Type LIMBO_MAP_TYPE = new TypeToken<Map<String, LimboPlayer>>(){}.getType();
private final ConsoleLogger logger = ConsoleLoggerFactory.get(DistributedFilesPersistenceHandler.class);
private final File cacheFolder;
private final Gson gson;
private final SegmentNameBuilder segmentNameBuilder;
@Inject
DistributedFilesPersistenceHandler(@DataFolder File dataFolder, BukkitService bukkitService, Settings settings) {
cacheFolder = new File(dataFolder, "playerdata");
FileUtils.createDirectory(cacheFolder);
gson = new GsonBuilder()
.registerTypeAdapter(LimboPlayer.class, new LimboPlayerSerializer())
.registerTypeAdapter(LimboPlayer.class, new LimboPlayerDeserializer(bukkitService))
.setPrettyPrinting()
.create();
segmentNameBuilder = new SegmentNameBuilder(settings.getProperty(LimboSettings.DISTRIBUTION_SIZE));
convertOldDataToCurrentSegmentScheme();
deleteEmptyFiles();
}
@Override
public LimboPlayer getLimboPlayer(Player player) {
String uuid = player.getUniqueId().toString();
File file = getPlayerSegmentFile(uuid);
Map<String, LimboPlayer> entries = readLimboPlayers(file);
return entries == null ? null : entries.get(uuid);
}
@Override
public void saveLimboPlayer(Player player, LimboPlayer limbo) {
String uuid = player.getUniqueId().toString();
File file = getPlayerSegmentFile(uuid);
Map<String, LimboPlayer> entries = null;
if (file.exists()) {
entries = readLimboPlayers(file);
} else {
FileUtils.create(file);
}
/* intentionally separate if */
if (entries == null) {
entries = new HashMap<>();
}
entries.put(uuid, limbo);
saveEntries(entries, file);
}
@Override
public void removeLimboPlayer(Player player) {
String uuid = player.getUniqueId().toString();
File file = getPlayerSegmentFile(uuid);
if (file.exists()) {
Map<String, LimboPlayer> entries = readLimboPlayers(file);
if (entries != null && entries.remove(uuid) != null) {
saveEntries(entries, file);
}
}
}
@Override
public LimboPersistenceType getType() {
return LimboPersistenceType.DISTRIBUTED_FILES;
}
private void saveEntries(Map<String, LimboPlayer> entries, File file) {
try (FileWriter fw = new FileWriter(file)) {
gson.toJson(entries, fw);
} catch (Exception e) {
logger.logException("Could not write to '" + file + "':", e);
}
}
private Map<String, LimboPlayer> readLimboPlayers(File file) {
if (!file.exists()) {
return null;
}
try {
return gson.fromJson(Files.asCharSource(file, StandardCharsets.UTF_8).read(), LIMBO_MAP_TYPE);
} catch (Exception e) {
logger.logException("Failed reading '" + file + "':", e);
}
return null;
}
private File getPlayerSegmentFile(String uuid) {
String segment = segmentNameBuilder.createSegmentName(uuid);
return getSegmentFile(segment);
}
private File getSegmentFile(String segmentId) {
return new File(cacheFolder, segmentId + "-limbo.json");
}
/**
* Loads segment files in the cache folder that don't correspond to the current segmenting scheme
* and migrates the data into files of the current segments. This allows a player to change the
* segment size without any loss of data.
*/
private void convertOldDataToCurrentSegmentScheme() {
String currentPrefix = segmentNameBuilder.getPrefix();
File[] files = listFiles(cacheFolder);
Map<String, LimboPlayer> allLimboPlayers = new HashMap<>();
List<File> migratedFiles = new ArrayList<>();
for (File file : files) {
if (isLimboJsonFile(file) && !file.getName().startsWith(currentPrefix)) {
Map<String, LimboPlayer> data = readLimboPlayers(file);
if (data != null) {
allLimboPlayers.putAll(data);
migratedFiles.add(file);
}
}
}
if (!allLimboPlayers.isEmpty()) {
saveToNewSegments(allLimboPlayers);
migratedFiles.forEach(FileUtils::delete);
}
}
/**
* Saves the LimboPlayer data read from old segmenting schemes into the current segmenting scheme.
*
* @param limbosFromOldSegments the limbo players to store into the current segment files
*/
private void saveToNewSegments(Map<String, LimboPlayer> limbosFromOldSegments) {
Map<String, Map<String, LimboPlayer>> limboBySegment = groupBySegment(limbosFromOldSegments);
logger.info("Saving " + limbosFromOldSegments.size() + " LimboPlayers from old segments into "
+ limboBySegment.size() + " current segments");
for (Map.Entry<String, Map<String, LimboPlayer>> entry : limboBySegment.entrySet()) {
File file = getSegmentFile(entry.getKey());
Map<String, LimboPlayer> limbosToSave = Optional.ofNullable(readLimboPlayers(file))
.orElseGet(HashMap::new);
limbosToSave.putAll(entry.getValue());
saveEntries(limbosToSave, file);
}
}
/**
* Converts a Map of UUID to LimboPlayers to a 2-dimensional Map of LimboPlayers by segment ID and UUID.
* {@code Map(uuid -> LimboPlayer) to Map(segment -> Map(uuid -> LimboPlayer))}
*
* @param readLimboPlayers the limbo players to order by segment
* @return limbo players ordered by segment ID and associated player UUID
*/
private Map<String, Map<String, LimboPlayer>> groupBySegment(Map<String, LimboPlayer> readLimboPlayers) {
Map<String, Map<String, LimboPlayer>> limboBySegment = new HashMap<>();
for (Map.Entry<String, LimboPlayer> entry : readLimboPlayers.entrySet()) {
String segmentId = segmentNameBuilder.createSegmentName(entry.getKey());
limboBySegment.computeIfAbsent(segmentId, s -> new HashMap<>())
.put(entry.getKey(), entry.getValue());
}
return limboBySegment;
}
/**
* Deletes segment files that are empty.
*/
private void deleteEmptyFiles() {
File[] files = listFiles(cacheFolder);
long deletedFiles = Arrays.stream(files)
// typically the size is 2 because there's an empty JSON map: {}
.filter(f -> isLimboJsonFile(f) && f.length() < 3)
.peek(FileUtils::delete)
.count();
logger.debug("Limbo: Deleted {0} empty segment files", deletedFiles);
}
/**
* @param file the file to check
* @return true if it is a segment file storing Limbo data, false otherwise
*/
private static boolean isLimboJsonFile(File file) {
String name = file.getName();
return name.startsWith("seg") && name.endsWith("-limbo.json");
}
private File[] listFiles(File folder) {
File[] files = folder.listFiles();
if (files == null) {
logger.warning("Could not get files of '" + folder + "'");
return new File[0];
}
return files;
}
}
@@ -1,92 +0,0 @@
package fr.xephi.authme.data.limbo.persistence;
import com.google.common.io.Files;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.LimboPlayer;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.util.FileUtils;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* Saves LimboPlayer objects as JSON into individual files.
*/
class IndividualFilesPersistenceHandler implements LimboPersistenceHandler {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(IndividualFilesPersistenceHandler.class);
private final Gson gson;
private final File cacheDir;
@Inject
IndividualFilesPersistenceHandler(@DataFolder File dataFolder, BukkitService bukkitService) {
cacheDir = new File(dataFolder, "playerdata");
if (!cacheDir.exists() && !cacheDir.isDirectory() && !cacheDir.mkdir()) {
logger.warning("Failed to create playerdata directory '" + cacheDir + "'");
}
gson = new GsonBuilder()
.registerTypeAdapter(LimboPlayer.class, new LimboPlayerSerializer())
.registerTypeAdapter(LimboPlayer.class, new LimboPlayerDeserializer(bukkitService))
.setPrettyPrinting()
.create();
}
@Override
public LimboPlayer getLimboPlayer(Player player) {
String id = player.getUniqueId().toString();
File file = new File(cacheDir, id + File.separator + "data.json");
if (!file.exists()) {
return null;
}
try {
String str = Files.asCharSource(file, StandardCharsets.UTF_8).read();
return gson.fromJson(str, LimboPlayer.class);
} catch (IOException e) {
logger.logException("Could not read player data on disk for '" + player.getName() + "'", e);
return null;
}
}
@Override
public void saveLimboPlayer(Player player, LimboPlayer limboPlayer) {
String id = player.getUniqueId().toString();
try {
File file = new File(cacheDir, id + File.separator + "data.json");
Files.createParentDirs(file);
Files.touch(file);
Files.write(gson.toJson(limboPlayer), file, StandardCharsets.UTF_8);
} catch (IOException e) {
logger.logException("Failed to write " + player.getName() + " data:", e);
}
}
/**
* Removes the LimboPlayer. This will delete the
* "playerdata/&lt;uuid or name&gt;/" folder from disk.
*
* @param player player to remove
*/
@Override
public void removeLimboPlayer(Player player) {
String id = player.getUniqueId().toString();
File file = new File(cacheDir, id);
if (file.exists()) {
FileUtils.purgeDirectory(file);
FileUtils.delete(file);
}
}
@Override
public LimboPersistenceType getType() {
return LimboPersistenceType.INDIVIDUAL_FILES;
}
}
@@ -1,82 +0,0 @@
package fr.xephi.authme.data.limbo.persistence;
import ch.jalu.injector.factory.Factory;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.LimboPlayer;
import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.output.ConsoleLoggerFactory;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.LimboSettings;
import org.bukkit.entity.Player;
import javax.inject.Inject;
/**
* Handles the persistence of LimboPlayers.
*/
public class LimboPersistence implements SettingsDependent {
private final ConsoleLogger logger = ConsoleLoggerFactory.get(LimboPersistence.class);
private final Factory<LimboPersistenceHandler> handlerFactory;
private LimboPersistenceHandler handler;
@Inject
LimboPersistence(Settings settings, Factory<LimboPersistenceHandler> handlerFactory) {
this.handlerFactory = handlerFactory;
reload(settings);
}
/**
* Retrieves the LimboPlayer for the given player if available.
*
* @param player the player to retrieve the LimboPlayer for
* @return the player's limbo player, or null if not available
*/
public LimboPlayer getLimboPlayer(Player player) {
try {
return handler.getLimboPlayer(player);
} catch (Exception e) {
logger.logException("Could not get LimboPlayer for '" + player.getName() + "'", e);
}
return null;
}
/**
* Saves the given LimboPlayer for the provided player.
*
* @param player the player to save the LimboPlayer for
* @param limbo the limbo player to save
*/
public void saveLimboPlayer(Player player, LimboPlayer limbo) {
try {
handler.saveLimboPlayer(player, limbo);
} catch (Exception e) {
logger.logException("Could not save LimboPlayer for '" + player.getName() + "'", e);
}
}
/**
* Removes the LimboPlayer for the given player.
*
* @param player the player whose LimboPlayer should be removed
*/
public void removeLimboPlayer(Player player) {
try {
handler.removeLimboPlayer(player);
} catch (Exception e) {
logger.logException("Could not remove LimboPlayer for '" + player.getName() + "'", e);
}
}
@Override
public void reload(Settings settings) {
LimboPersistenceType persistenceType = settings.getProperty(LimboSettings.LIMBO_PERSISTENCE_TYPE);
// If we're changing from an existing handler, output a quick hint that nothing is converted.
if (handler != null && handler.getType() != persistenceType) {
logger.info("Limbo persistence type has changed! Note that the data is not converted.");
}
handler = handlerFactory.newInstance(persistenceType.getImplementationClass());
}
}
@@ -1,39 +0,0 @@
package fr.xephi.authme.data.limbo.persistence;
import fr.xephi.authme.data.limbo.LimboPlayer;
import org.bukkit.entity.Player;
/**
* Handles I/O for storing LimboPlayer objects.
*/
interface LimboPersistenceHandler {
/**
* Returns the limbo player for the given player if it exists.
*
* @param player the player
* @return the stored limbo player, or null if not available
*/
LimboPlayer getLimboPlayer(Player player);
/**
* Saves the given limbo player for the given player to the disk.
*
* @param player the player to save the limbo player for
* @param limbo the limbo player to save
*/
void saveLimboPlayer(Player player, LimboPlayer limbo);
/**
* Removes the limbo player from the disk.
*
* @param player the player whose limbo player should be removed
*/
void removeLimboPlayer(Player player);
/**
* @return the type of the limbo persistence implementation
*/
LimboPersistenceType getType();
}
@@ -1,34 +0,0 @@
package fr.xephi.authme.data.limbo.persistence;
/**
* Types of persistence for LimboPlayer objects.
*/
public enum LimboPersistenceType {
/** Store each LimboPlayer in a separate file. */
INDIVIDUAL_FILES(IndividualFilesPersistenceHandler.class),
/** Store LimboPlayers distributed in a configured number of files. */
DISTRIBUTED_FILES(DistributedFilesPersistenceHandler.class),
/** No persistence to disk. */
DISABLED(NoOpPersistenceHandler.class);
private final Class<? extends LimboPersistenceHandler> implementationClass;
/**
* Constructor.
*
* @param implementationClass the implementation class
*/
LimboPersistenceType(Class<? extends LimboPersistenceHandler> implementationClass) {
this.implementationClass= implementationClass;
}
/**
* @return class implementing the persistence type
*/
public Class<? extends LimboPersistenceHandler> getImplementationClass() {
return implementationClass;
}
}
@@ -1,163 +0,0 @@
package fr.xephi.authme.data.limbo.persistence;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import fr.xephi.authme.data.limbo.LimboPlayer;
import fr.xephi.authme.data.limbo.UserGroup;
import fr.xephi.authme.service.BukkitService;
import org.bukkit.Location;
import org.bukkit.World;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.CAN_FLY;
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.FLY_SPEED;
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.GROUPS;
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.IS_OP;
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOCATION;
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_PITCH;
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_WORLD;
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_X;
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_Y;
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_YAW;
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_Z;
import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.WALK_SPEED;
import static java.util.Optional.ofNullable;
/**
* Converts a JsonElement to a LimboPlayer.
*/
class LimboPlayerDeserializer implements JsonDeserializer<LimboPlayer> {
private static final String GROUP_LEGACY = "group";
private static final String CONTEXT_MAP = "contextMap";
private static final String GROUP_NAME = "groupName";
private BukkitService bukkitService;
LimboPlayerDeserializer(BukkitService bukkitService) {
this.bukkitService = bukkitService;
}
@Override
public LimboPlayer deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext context) {
JsonObject jsonObject = jsonElement.getAsJsonObject();
if (jsonObject == null) {
return null;
}
Location loc = deserializeLocation(jsonObject);
boolean operator = getBoolean(jsonObject, IS_OP);
Collection<UserGroup> groups = getLimboGroups(jsonObject);
boolean canFly = getBoolean(jsonObject, CAN_FLY);
float walkSpeed = getFloat(jsonObject, WALK_SPEED, LimboPlayer.DEFAULT_WALK_SPEED);
float flySpeed = getFloat(jsonObject, FLY_SPEED, LimboPlayer.DEFAULT_FLY_SPEED);
return new LimboPlayer(loc, operator, groups, canFly, walkSpeed, flySpeed);
}
private Location deserializeLocation(JsonObject jsonObject) {
JsonElement e;
if ((e = jsonObject.getAsJsonObject(LOCATION)) != null) {
JsonObject locationObject = e.getAsJsonObject();
World world = bukkitService.getWorld(getString(locationObject, LOC_WORLD));
if (world != null) {
double x = getDouble(locationObject, LOC_X);
double y = getDouble(locationObject, LOC_Y);
double z = getDouble(locationObject, LOC_Z);
float yaw = getFloat(locationObject, LOC_YAW);
float pitch = getFloat(locationObject, LOC_PITCH);
return new Location(world, x, y, z, yaw, pitch);
}
}
return null;
}
private static String getString(JsonObject jsonObject, String memberName) {
JsonElement element = jsonObject.get(memberName);
return element != null ? element.getAsString() : "";
}
/**
* @param jsonObject LimboPlayer represented as JSON
* @return The list of UserGroups create from JSON
*/
private static List<UserGroup> getLimboGroups(JsonObject jsonObject) {
JsonElement element = jsonObject.get(GROUPS);
if (element == null) {
String legacyGroup = ofNullable(jsonObject.get(GROUP_LEGACY)).map(JsonElement::getAsString).orElse(null);
return legacyGroup == null ? Collections.emptyList() :
Collections.singletonList(new UserGroup(legacyGroup, null));
}
List<UserGroup> result = new ArrayList<>();
JsonArray jsonArray = element.getAsJsonArray();
for (JsonElement arrayElement : jsonArray) {
if (!arrayElement.isJsonObject()) {
result.add(new UserGroup(arrayElement.getAsString(), null));
} else {
JsonObject jsonGroup = arrayElement.getAsJsonObject();
Map<String, String> contextMap = null;
if (jsonGroup.has(CONTEXT_MAP)) {
JsonElement contextMapJson = jsonGroup.get("contextMap");
Type type = new TypeToken<Map<String, String>>() {
}.getType();
contextMap = new Gson().fromJson(contextMapJson.getAsString(), type);
}
String groupName = jsonGroup.get(GROUP_NAME).getAsString();
result.add(new UserGroup(groupName, contextMap));
}
}
return result;
}
private static boolean getBoolean(JsonObject jsonObject, String memberName) {
JsonElement element = jsonObject.get(memberName);
return element != null && element.getAsBoolean();
}
private static float getFloat(JsonObject jsonObject, String memberName) {
return getNumberFromElement(jsonObject.get(memberName), JsonElement::getAsFloat, 0.0f);
}
private static float getFloat(JsonObject jsonObject, String memberName, float defaultValue) {
return getNumberFromElement(jsonObject.get(memberName), JsonElement::getAsFloat, defaultValue);
}
private static double getDouble(JsonObject jsonObject, String memberName) {
return getNumberFromElement(jsonObject.get(memberName), JsonElement::getAsDouble, 0.0);
}
/**
* Gets a number from the given JsonElement safely.
*
* @param jsonElement the element to retrieve the number from
* @param numberFunction the function to get the number from the element
* @param defaultValue the value to return if the element is null or the number cannot be retrieved
* @param <N> the number type
* @return the number from the given JSON element, or the default value
*/
private static <N extends Number> N getNumberFromElement(JsonElement jsonElement,
Function<JsonElement, N> numberFunction,
N defaultValue) {
if (jsonElement != null) {
try {
return numberFunction.apply(jsonElement);
} catch (NumberFormatException ignore) {
}
}
return defaultValue;
}
}
@@ -1,71 +0,0 @@
package fr.xephi.authme.data.limbo.persistence;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import fr.xephi.authme.data.limbo.LimboPlayer;
import org.bukkit.Location;
import java.lang.reflect.Type;
import java.util.List;
import java.util.stream.Collectors;
/**
* Converts a LimboPlayer to a JsonElement.
*/
class LimboPlayerSerializer implements JsonSerializer<LimboPlayer> {
static final String LOCATION = "location";
static final String LOC_WORLD = "world";
static final String LOC_X = "x";
static final String LOC_Y = "y";
static final String LOC_Z = "z";
static final String LOC_YAW = "yaw";
static final String LOC_PITCH = "pitch";
static final String GROUPS = "groups";
static final String IS_OP = "operator";
static final String CAN_FLY = "can-fly";
static final String WALK_SPEED = "walk-speed";
static final String FLY_SPEED = "fly-speed";
private static final Gson GSON = new Gson();
@Override
public JsonElement serialize(LimboPlayer limboPlayer, Type type, JsonSerializationContext context) {
Location loc = limboPlayer.getLocation();
JsonObject locationObject = new JsonObject();
locationObject.addProperty(LOC_WORLD, loc.getWorld().getName());
locationObject.addProperty(LOC_X, loc.getX());
locationObject.addProperty(LOC_Y, loc.getY());
locationObject.addProperty(LOC_Z, loc.getZ());
locationObject.addProperty(LOC_YAW, loc.getYaw());
locationObject.addProperty(LOC_PITCH, loc.getPitch());
JsonObject obj = new JsonObject();
obj.add(LOCATION, locationObject);
List<JsonObject> groups = limboPlayer.getGroups().stream().map(g -> {
JsonObject jsonGroup = new JsonObject();
jsonGroup.addProperty("groupName", g.getGroupName());
if (g.getContextMap() != null) {
jsonGroup.addProperty("contextMap", GSON.toJson(g.getContextMap()));
}
return jsonGroup;
}).collect(Collectors.toList());
JsonArray jsonGroups = new JsonArray();
groups.forEach(jsonGroups::add);
obj.add(GROUPS, jsonGroups);
obj.addProperty(IS_OP, limboPlayer.isOperator());
obj.addProperty(CAN_FLY, limboPlayer.isCanFly());
obj.addProperty(WALK_SPEED, limboPlayer.getWalkSpeed());
obj.addProperty(FLY_SPEED, limboPlayer.getFlySpeed());
return obj;
}
}
@@ -1,30 +0,0 @@
package fr.xephi.authme.data.limbo.persistence;
import fr.xephi.authme.data.limbo.LimboPlayer;
import org.bukkit.entity.Player;
/**
* Limbo player persistence implementation that does nothing.
*/
class NoOpPersistenceHandler implements LimboPersistenceHandler {
@Override
public LimboPlayer getLimboPlayer(Player player) {
return null;
}
@Override
public void saveLimboPlayer(Player player, LimboPlayer limbo) {
// noop
}
@Override
public void removeLimboPlayer(Player player) {
// noop
}
@Override
public LimboPersistenceType getType() {
return LimboPersistenceType.DISABLED;
}
}
@@ -1,73 +0,0 @@
package fr.xephi.authme.data.limbo.persistence;
import java.util.HashMap;
import java.util.Map;
/**
* Creates segment names for {@link DistributedFilesPersistenceHandler}.
*/
class SegmentNameBuilder {
private final int length;
private final int distribution;
private final String prefix;
private final Map<Character, Character> charToSegmentChar;
/**
* Constructor.
*
* @param partition the segment configuration
*/
SegmentNameBuilder(SegmentSize partition) {
this.length = partition.getLength();
this.distribution = partition.getDistribution();
this.prefix = "seg" + partition.getTotalSegments() + "-";
this.charToSegmentChar = buildCharMap(distribution);
}
/**
* Returns the segment ID for the given UUID.
*
* @param uuid the player's uuid to get the segment for
* @return id the uuid belongs to
*/
String createSegmentName(String uuid) {
if (distribution == 16) {
return prefix + uuid.substring(0, length);
} else {
return prefix + buildSegmentName(uuid.substring(0, length).toCharArray());
}
}
/**
* @return the prefix used for the current segment configuration
*/
String getPrefix() {
return prefix;
}
private String buildSegmentName(char[] chars) {
if (chars.length == 1) {
return String.valueOf(charToSegmentChar.get(chars[0]));
}
StringBuilder sb = new StringBuilder(chars.length);
for (char chr : chars) {
sb.append(charToSegmentChar.get(chr));
}
return sb.toString();
}
private static Map<Character, Character> buildCharMap(int distribution) {
final char[] hexChars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
final int divisor = 16 / distribution;
Map<Character, Character> charToSegmentChar = new HashMap<>();
for (int i = 0; i < hexChars.length; ++i) {
int mappedChar = i / divisor;
charToSegmentChar.put(hexChars[i], hexChars[mappedChar]);
}
return charToSegmentChar;
}
}
@@ -1,94 +0,0 @@
package fr.xephi.authme.data.limbo.persistence;
/**
* Configuration for the total number of segments to use.
* <p>
* The {@link DistributedFilesPersistenceHandler} reduces the number of files by assigning each UUID
* to a segment. This enum allows to define how many segments the UUIDs should be distributed in.
* <p>
* Segments are defined by a <b>distribution</b> and a <b>length.</b> The distribution defines
* to how many outputs a single hexadecimal characters should be mapped. So e.g. a distribution
* of 3 means that all hexadecimal characters 0-f should be distributed over three different
* outputs evenly. The {@link SegmentNameBuilder} simply uses hexadecimal characters as outputs,
* so e.g. with a distribution of 3 all hex characters 0-f are mapped to 0, 1, or 2.
* <p>
* To ensure an even distribution the segments must be powers of 2. Trivially, to implement a
* distribution of 16, the same character may be returned as was input (since 0-f make up 16
* characters). A distribution of 1, on the other hand, means that the same output is returned
* regardless of the input character.
* <p>
* The <b>length</b> parameter defines how many characters of a player's UUID should be used to
* create the segment ID. In other words, with a distribution of 2 and a length of 3, the first
* three characters of the UUID are taken into consideration, each mapped to one of two possible
* characters. For instance, a UUID starting with "0f5c9321" may yield the segment ID "010."
* Such a segment ID defines in which file the given UUID can be found and stored.
* <p>
* The number of segments such a configuration yields is computed as {@code distribution ^ length},
* since distribution defines how many outputs there are per digit, and length defines the number
* of digits. For instance, a distribution of 2 and a length of 3 will yield segment IDs 000, 001,
* 010, 011, 100, 101, 110 and 111 (i.e. all binary numbers from 0 to 7).
* <p>
* There are multiple possibilities to achieve certain segment totals, e.g. 8 different segments
* may be created by setting distribution to 8 and length to 1, or distr. to 2 and length to 3.
* Where possible, prefer a length of 1 (no string concatenation required) or a distribution of
* 16 (no remapping of the characters required).
*/
public enum SegmentSize {
/** 1. */
ONE(1, 1),
// /** 2. */
// TWO(2, 1),
/** 4. */
FOUR(4, 1),
/** 8. */
EIGHT(8, 1),
/** 16. */
SIXTEEN(16, 1),
/** 32. */
THIRTY_TWO(2, 5),
/** 64. */
SIXTY_FOUR(4, 3),
/** 128. */
ONE_TWENTY(2, 7),
/** 256. */
TWO_FIFTY(16, 2);
private final int distribution;
private final int length;
SegmentSize(int distribution, int length) {
this.distribution = distribution;
this.length = length;
}
/**
* @return the distribution size per character, i.e. how many possible outputs there are
* for any hexadecimal character
*/
public int getDistribution() {
return distribution;
}
/**
* @return number of characters from a UUID that should be used to create a segment ID
*/
public int getLength() {
return length;
}
/**
* @return number of segments to which this configuration will distribute all UUIDs
*/
public int getTotalSegments() {
return (int) Math.pow(distribution, length);
}
}