reupload files
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
package fr.xephi.authme.data;
|
||||
|
||||
import fr.xephi.authme.initialization.HasCleanup;
|
||||
import fr.xephi.authme.util.expiring.ExpiringSet;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class ProxySessionManager implements HasCleanup {
|
||||
|
||||
private final ExpiringSet<String> activeProxySessions;
|
||||
|
||||
@Inject
|
||||
public ProxySessionManager() {
|
||||
long countTimeout = 5;
|
||||
activeProxySessions = new ExpiringSet<>(countTimeout, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the player in the set
|
||||
* @param name the player's name
|
||||
*/
|
||||
private void setActiveSession(String name) {
|
||||
activeProxySessions.add(name.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a proxy session message from AuthMeBungee
|
||||
* @param name the player to process
|
||||
*/
|
||||
public void processProxySessionMessage(String name) {
|
||||
setActiveSession(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the player should be logged in or not
|
||||
* @param name the name of the player to check
|
||||
* @return true if player has to be logged in, false otherwise
|
||||
*/
|
||||
public boolean shouldResumeSession(String name) {
|
||||
return activeProxySessions.contains(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performCleanup() {
|
||||
activeProxySessions.removeExpiredEntries();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package fr.xephi.authme.data;
|
||||
|
||||
import fr.xephi.authme.initialization.HasCleanup;
|
||||
import fr.xephi.authme.initialization.SettingsDependent;
|
||||
import fr.xephi.authme.permission.PermissionsManager;
|
||||
import fr.xephi.authme.permission.PlayerPermission;
|
||||
import fr.xephi.authme.settings.Settings;
|
||||
import fr.xephi.authme.settings.properties.ProtectionSettings;
|
||||
import fr.xephi.authme.util.expiring.ExpiringSet;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class QuickCommandsProtectionManager implements SettingsDependent, HasCleanup {
|
||||
|
||||
private final PermissionsManager permissionsManager;
|
||||
|
||||
private final ExpiringSet<String> latestJoin;
|
||||
|
||||
@Inject
|
||||
public QuickCommandsProtectionManager(Settings settings, PermissionsManager permissionsManager) {
|
||||
this.permissionsManager = permissionsManager;
|
||||
long countTimeout = settings.getProperty(ProtectionSettings.QUICK_COMMANDS_DENIED_BEFORE_MILLISECONDS);
|
||||
latestJoin = new ExpiringSet<>(countTimeout, TimeUnit.MILLISECONDS);
|
||||
reload(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the player in the set
|
||||
* @param name the player's name
|
||||
*/
|
||||
private void setJoin(String name) {
|
||||
latestJoin.add(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given player has the permission and should be saved in the set
|
||||
* @param player the player to check
|
||||
* @return true if the player has the permission, false otherwise
|
||||
*/
|
||||
private boolean shouldSavePlayer(Player player) {
|
||||
return permissionsManager.hasPermission(player, PlayerPermission.QUICK_COMMANDS_PROTECTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the player join
|
||||
* @param player the player to process
|
||||
*/
|
||||
public void processJoin(Player player) {
|
||||
if (shouldSavePlayer(player)) {
|
||||
setJoin(player.getName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given player is able to perform the command
|
||||
* @param name the name of the player to check
|
||||
* @return true if the player is not in the set (so it's allowed to perform the command), false otherwise
|
||||
*/
|
||||
public boolean isAllowed(String name) {
|
||||
return !latestJoin.contains(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reload(Settings settings) {
|
||||
long countTimeout = settings.getProperty(ProtectionSettings.QUICK_COMMANDS_DENIED_BEFORE_MILLISECONDS);
|
||||
latestJoin.setExpiration(countTimeout, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performCleanup() {
|
||||
latestJoin.removeExpiredEntries();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package fr.xephi.authme.data;
|
||||
|
||||
import fr.xephi.authme.initialization.HasCleanup;
|
||||
import fr.xephi.authme.initialization.SettingsDependent;
|
||||
import fr.xephi.authme.message.MessageKey;
|
||||
import fr.xephi.authme.message.Messages;
|
||||
import fr.xephi.authme.service.BukkitService;
|
||||
import fr.xephi.authme.settings.Settings;
|
||||
import fr.xephi.authme.settings.properties.SecuritySettings;
|
||||
import fr.xephi.authme.util.PlayerUtils;
|
||||
import fr.xephi.authme.util.expiring.TimedCounter;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static fr.xephi.authme.util.Utils.MILLIS_PER_MINUTE;
|
||||
|
||||
/**
|
||||
* Manager for handling temporary bans.
|
||||
*/
|
||||
public class TempbanManager implements SettingsDependent, HasCleanup {
|
||||
|
||||
private final Map<String, TimedCounter<String>> ipLoginFailureCounts;
|
||||
private final BukkitService bukkitService;
|
||||
private final Messages messages;
|
||||
|
||||
private boolean isEnabled;
|
||||
private int threshold;
|
||||
private int length;
|
||||
private long resetThreshold;
|
||||
private String customCommand;
|
||||
|
||||
@Inject
|
||||
TempbanManager(BukkitService bukkitService, Messages messages, Settings settings) {
|
||||
this.ipLoginFailureCounts = new ConcurrentHashMap<>();
|
||||
this.bukkitService = bukkitService;
|
||||
this.messages = messages;
|
||||
reload(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases the failure count for the given IP address/username combination.
|
||||
*
|
||||
* @param address The player's IP address
|
||||
* @param name The username
|
||||
*/
|
||||
public void increaseCount(String address, String name) {
|
||||
if (isEnabled) {
|
||||
TimedCounter<String> countsByName = ipLoginFailureCounts.computeIfAbsent(
|
||||
address, k -> new TimedCounter<>(resetThreshold, TimeUnit.MINUTES));
|
||||
countsByName.increment(name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the failure count for a given IP address / username combination to 0.
|
||||
*
|
||||
* @param address The IP address
|
||||
* @param name The username
|
||||
*/
|
||||
public void resetCount(String address, String name) {
|
||||
if (isEnabled) {
|
||||
TimedCounter<String> counter = ipLoginFailureCounts.get(address);
|
||||
if (counter != null) {
|
||||
counter.remove(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the IP address should be tempbanned.
|
||||
*
|
||||
* @param address The player's IP address
|
||||
* @return True if the IP should be tempbanned
|
||||
*/
|
||||
public boolean shouldTempban(String address) {
|
||||
if (isEnabled) {
|
||||
TimedCounter<String> countsByName = ipLoginFailureCounts.get(address);
|
||||
if (countsByName != null) {
|
||||
return countsByName.total() >= threshold;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tempban a player's IP address for failing to log in too many times.
|
||||
* This calculates the expire time based on the time the method was called.
|
||||
*
|
||||
* @param player The player to tempban
|
||||
*/
|
||||
public void tempbanPlayer(final Player player) {
|
||||
if (isEnabled) {
|
||||
final String name = player.getName();
|
||||
final String ip = PlayerUtils.getPlayerIp(player);
|
||||
final String reason = messages.retrieveSingle(player, MessageKey.TEMPBAN_MAX_LOGINS);
|
||||
|
||||
final Date expires = new Date();
|
||||
long newTime = expires.getTime() + (length * MILLIS_PER_MINUTE);
|
||||
expires.setTime(newTime);
|
||||
|
||||
bukkitService.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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package fr.xephi.authme.data;
|
||||
|
||||
import ch.jalu.datasourcecolumns.data.DataSourceValue;
|
||||
import fr.xephi.authme.datasource.DataSource;
|
||||
import fr.xephi.authme.initialization.HasCleanup;
|
||||
import fr.xephi.authme.initialization.SettingsDependent;
|
||||
import fr.xephi.authme.mail.EmailService;
|
||||
import fr.xephi.authme.permission.PermissionsManager;
|
||||
import fr.xephi.authme.permission.PlayerPermission;
|
||||
import fr.xephi.authme.settings.Settings;
|
||||
import fr.xephi.authme.settings.properties.SecuritySettings;
|
||||
import fr.xephi.authme.util.RandomStringUtils;
|
||||
import fr.xephi.authme.util.Utils;
|
||||
import fr.xephi.authme.util.expiring.ExpiringMap;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class VerificationCodeManager implements SettingsDependent, HasCleanup {
|
||||
|
||||
private final EmailService emailService;
|
||||
private final DataSource dataSource;
|
||||
private final PermissionsManager permissionsManager;
|
||||
|
||||
private final ExpiringMap<String, String> verificationCodes;
|
||||
private final Set<String> verifiedPlayers;
|
||||
|
||||
private boolean canSendMail;
|
||||
|
||||
@Inject
|
||||
VerificationCodeManager(Settings settings, DataSource dataSource, EmailService emailService,
|
||||
PermissionsManager permissionsManager) {
|
||||
this.emailService = emailService;
|
||||
this.dataSource = dataSource;
|
||||
this.permissionsManager = permissionsManager;
|
||||
verifiedPlayers = new HashSet<>();
|
||||
long countTimeout = settings.getProperty(SecuritySettings.VERIFICATION_CODE_EXPIRATION_MINUTES);
|
||||
verificationCodes = new ExpiringMap<>(countTimeout, TimeUnit.MINUTES);
|
||||
reload(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if it is possible to send emails
|
||||
*
|
||||
* @return true if the service is enabled, false otherwise
|
||||
*/
|
||||
public boolean canSendMail() {
|
||||
return canSendMail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given player is able to verify his identity
|
||||
*
|
||||
* @param player the player to verify
|
||||
* @return true if the player has not been verified yet, false otherwise
|
||||
*/
|
||||
public boolean isVerificationRequired(Player player) {
|
||||
final String name = player.getName();
|
||||
return canSendMail
|
||||
&& !isPlayerVerified(name)
|
||||
&& permissionsManager.hasPermission(player, PlayerPermission.VERIFICATION_CODE)
|
||||
&& hasEmail(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given player is required to verify his identity through a command
|
||||
*
|
||||
* @param name the name of the player to verify
|
||||
* @return true if the player has an existing code and has not been verified yet, false otherwise
|
||||
*/
|
||||
public boolean isCodeRequired(String name) {
|
||||
return canSendMail && hasCode(name) && !isPlayerVerified(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given player has been verified or not
|
||||
*
|
||||
* @param name the name of the player to verify
|
||||
* @return true if the player has been verified, false otherwise
|
||||
*/
|
||||
private boolean isPlayerVerified(String name) {
|
||||
return verifiedPlayers.contains(name.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if a code exists for the player
|
||||
*
|
||||
* @param name the name of the player to verify
|
||||
* @return true if the code exists, false otherwise
|
||||
*/
|
||||
public boolean hasCode(String name) {
|
||||
return (verificationCodes.get(name.toLowerCase(Locale.ROOT)) != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given player is able to receive emails
|
||||
*
|
||||
* @param name the name of the player to verify
|
||||
* @return true if the player is able to receive emails, false otherwise
|
||||
*/
|
||||
public boolean hasEmail(String name) {
|
||||
boolean result = false;
|
||||
DataSourceValue<String> emailResult = dataSource.getEmail(name);
|
||||
if (emailResult.rowExists()) {
|
||||
final String email = emailResult.getValue();
|
||||
if (!Utils.isEmailEmpty(email)) {
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a code exists for the player or generates and saves a new one.
|
||||
*
|
||||
* @param name the player's name
|
||||
*/
|
||||
public void codeExistOrGenerateNew(String name) {
|
||||
if (!hasCode(name)) {
|
||||
generateCode(name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a code for the player and returns it.
|
||||
*
|
||||
* @param name the name of the player to generate a code for
|
||||
*/
|
||||
private void generateCode(String name) {
|
||||
DataSourceValue<String> emailResult = dataSource.getEmail(name);
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy'年'MM'月'dd'日' HH:mm:ss");
|
||||
Date date = new Date(System.currentTimeMillis());
|
||||
if (emailResult.rowExists()) {
|
||||
final String email = emailResult.getValue();
|
||||
if (!Utils.isEmailEmpty(email)) {
|
||||
String code = RandomStringUtils.generateNum(6); // 6 digits code
|
||||
verificationCodes.put(name.toLowerCase(Locale.ROOT), code);
|
||||
emailService.sendVerificationMail(name, email, code, dateFormat.format(date));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the given code against the existing one.
|
||||
*
|
||||
* @param name the name of the player to check
|
||||
* @param code the supplied code
|
||||
* @return true if the code matches, false otherwise
|
||||
*/
|
||||
public boolean checkCode(String name, String code) {
|
||||
boolean correct = false;
|
||||
if (code.equals(verificationCodes.get(name.toLowerCase(Locale.ROOT)))) {
|
||||
verify(name);
|
||||
correct = true;
|
||||
}
|
||||
return correct;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the user to the set of verified users
|
||||
*
|
||||
* @param name the name of the player to generate a code for
|
||||
*/
|
||||
public void verify(String name) {
|
||||
verifiedPlayers.add(name.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the user from the set of verified users
|
||||
*
|
||||
* @param name the name of the player to generate a code for
|
||||
*/
|
||||
public void unverify(String name){
|
||||
verifiedPlayers.remove(name.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reload(Settings settings) {
|
||||
canSendMail = emailService.hasAllInformation();
|
||||
long countTimeout = settings.getProperty(SecuritySettings.VERIFICATION_CODE_EXPIRATION_MINUTES);
|
||||
verificationCodes.setExpiration(countTimeout, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performCleanup() {
|
||||
verificationCodes.removeExpiredEntries();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
package fr.xephi.authme.data.auth;
|
||||
|
||||
import fr.xephi.authme.security.crypts.HashedPassword;
|
||||
import org.bukkit.Location;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
|
||||
/**
|
||||
* AuthMe player data.
|
||||
*/
|
||||
@SuppressWarnings("checkstyle:FinalClass") // Justification: class is mocked in multiple tests
|
||||
public class PlayerAuth {
|
||||
|
||||
/** Default email used in the database if the email column is defined to be NOT NULL. */
|
||||
public static final String DB_EMAIL_DEFAULT = "your@email.com";
|
||||
/** Default last login value used in the database if the last login column is NOT NULL. */
|
||||
public static final long DB_LAST_LOGIN_DEFAULT = 0;
|
||||
/** Default last ip value used in the database if the last IP column is NOT NULL. */
|
||||
public static final String DB_LAST_IP_DEFAULT = "127.0.0.1";
|
||||
|
||||
/** The player's name in lowercase, e.g. "xephi". */
|
||||
private String nickname;
|
||||
/** The player's name in the correct casing, e.g. "Xephi". */
|
||||
private String realName;
|
||||
private HashedPassword password;
|
||||
private String totpKey;
|
||||
private String email;
|
||||
private String lastIp;
|
||||
private int groupId;
|
||||
private Long lastLogin;
|
||||
private String registrationIp;
|
||||
private long registrationDate;
|
||||
// Fields storing the player's quit location
|
||||
private double x;
|
||||
private double y;
|
||||
private double z;
|
||||
private String world;
|
||||
private float yaw;
|
||||
private float pitch;
|
||||
private UUID uuid;
|
||||
|
||||
/**
|
||||
* Hidden constructor.
|
||||
*
|
||||
* @see #builder()
|
||||
*/
|
||||
private PlayerAuth() {
|
||||
}
|
||||
|
||||
|
||||
public void setNickname(String nickname) {
|
||||
this.nickname = nickname.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
public String getNickname() {
|
||||
return nickname;
|
||||
}
|
||||
|
||||
public String getRealName() {
|
||||
return realName;
|
||||
}
|
||||
|
||||
public void setRealName(String realName) {
|
||||
this.realName = realName;
|
||||
}
|
||||
|
||||
public int getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public void setQuitLocation(Location location) {
|
||||
x = location.getBlockX();
|
||||
y = location.getBlockY();
|
||||
z = location.getBlockZ();
|
||||
world = location.getWorld().getName();
|
||||
}
|
||||
|
||||
public double getQuitLocX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public void setQuitLocX(double d) {
|
||||
this.x = d;
|
||||
}
|
||||
|
||||
public double getQuitLocY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public void setQuitLocY(double d) {
|
||||
this.y = d;
|
||||
}
|
||||
|
||||
public double getQuitLocZ() {
|
||||
return z;
|
||||
}
|
||||
|
||||
public void setQuitLocZ(double d) {
|
||||
this.z = d;
|
||||
}
|
||||
|
||||
public String getWorld() {
|
||||
return world;
|
||||
}
|
||||
|
||||
public void setWorld(String world) {
|
||||
this.world = world;
|
||||
}
|
||||
|
||||
public float getYaw() {
|
||||
return yaw;
|
||||
}
|
||||
|
||||
public float getPitch() {
|
||||
return pitch;
|
||||
}
|
||||
|
||||
public String getLastIp() {
|
||||
return lastIp;
|
||||
}
|
||||
|
||||
public void setLastIp(String lastIp) {
|
||||
this.lastIp = lastIp;
|
||||
}
|
||||
|
||||
public Long getLastLogin() {
|
||||
return lastLogin;
|
||||
}
|
||||
|
||||
public void setLastLogin(long lastLogin) {
|
||||
this.lastLogin = lastLogin;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public HashedPassword getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(HashedPassword password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getRegistrationIp() {
|
||||
return registrationIp;
|
||||
}
|
||||
|
||||
public long getRegistrationDate() {
|
||||
return registrationDate;
|
||||
}
|
||||
|
||||
public void setRegistrationDate(long registrationDate) {
|
||||
this.registrationDate = registrationDate;
|
||||
}
|
||||
|
||||
public String getTotpKey() {
|
||||
return totpKey;
|
||||
}
|
||||
|
||||
public void setTotpKey(String totpKey) {
|
||||
this.totpKey = totpKey;
|
||||
}
|
||||
|
||||
public UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public void setUuid(UUID uuid) {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (!(obj instanceof PlayerAuth)) {
|
||||
return false;
|
||||
}
|
||||
PlayerAuth other = (PlayerAuth) obj;
|
||||
return Objects.equals(other.lastIp, this.lastIp) && Objects.equals(other.nickname, this.nickname);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hashCode = 7;
|
||||
hashCode = 71 * hashCode + (this.nickname != null ? this.nickname.hashCode() : 0);
|
||||
hashCode = 71 * hashCode + (this.lastIp != null ? this.lastIp.hashCode() : 0);
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Player : " + nickname + " | " + realName
|
||||
+ " ! IP : " + lastIp
|
||||
+ " ! LastLogin : " + lastLogin
|
||||
+ " ! LastPosition : " + x + "," + y + "," + z + "," + world
|
||||
+ " ! Email : " + email
|
||||
+ " ! Password : {" + password.getHash() + ", " + password.getSalt() + "}"
|
||||
+ " ! UUID : " + uuid;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private String name;
|
||||
private String realName;
|
||||
private HashedPassword password;
|
||||
private String totpKey;
|
||||
private String lastIp;
|
||||
private String email;
|
||||
private int groupId = -1;
|
||||
private Long lastLogin;
|
||||
private String registrationIp;
|
||||
private Long registrationDate;
|
||||
|
||||
private double x;
|
||||
private double y;
|
||||
private double z;
|
||||
private String world;
|
||||
private float yaw;
|
||||
private float pitch;
|
||||
private UUID uuid;
|
||||
|
||||
/**
|
||||
* Creates a PlayerAuth object.
|
||||
*
|
||||
* @return the generated PlayerAuth
|
||||
*/
|
||||
public PlayerAuth build() {
|
||||
PlayerAuth auth = new PlayerAuth();
|
||||
auth.nickname = checkNotNull(name).toLowerCase(Locale.ROOT);
|
||||
auth.realName = Optional.ofNullable(realName).orElse("Player");
|
||||
auth.password = Optional.ofNullable(password).orElse(new HashedPassword(""));
|
||||
auth.totpKey = totpKey;
|
||||
auth.email = DB_EMAIL_DEFAULT.equals(email) ? null : email;
|
||||
auth.lastIp = lastIp; // Don't check against default value 127.0.0.1 as it may be a legit value
|
||||
auth.groupId = groupId;
|
||||
auth.lastLogin = isEqualTo(lastLogin, DB_LAST_LOGIN_DEFAULT) ? null : lastLogin;
|
||||
auth.registrationIp = registrationIp;
|
||||
auth.registrationDate = registrationDate == null ? System.currentTimeMillis() : registrationDate;
|
||||
|
||||
auth.x = x;
|
||||
auth.y = y;
|
||||
auth.z = z;
|
||||
auth.world = Optional.ofNullable(world).orElse("world");
|
||||
auth.yaw = yaw;
|
||||
auth.pitch = pitch;
|
||||
auth.uuid = uuid;
|
||||
return auth;
|
||||
}
|
||||
|
||||
private static boolean isEqualTo(Long value, long defaultValue) {
|
||||
return value != null && defaultValue == value;
|
||||
}
|
||||
|
||||
public Builder name(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder realName(String realName) {
|
||||
this.realName = realName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder password(HashedPassword password) {
|
||||
this.password = password;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder password(String hash, String salt) {
|
||||
return password(new HashedPassword(hash, salt));
|
||||
}
|
||||
|
||||
public Builder totpKey(String totpKey) {
|
||||
this.totpKey = totpKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder lastIp(String lastIp) {
|
||||
this.lastIp = lastIp;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the location info based on the argument.
|
||||
*
|
||||
* @param location the location info to set
|
||||
* @return this builder instance
|
||||
*/
|
||||
public Builder location(Location location) {
|
||||
this.x = location.getX();
|
||||
this.y = location.getY();
|
||||
this.z = location.getZ();
|
||||
this.world = location.getWorld().getName();
|
||||
this.yaw = location.getYaw();
|
||||
this.pitch = location.getPitch();
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder locX(double x) {
|
||||
this.x = x;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder locY(double y) {
|
||||
this.y = y;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder locZ(double z) {
|
||||
this.z = z;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder locWorld(String world) {
|
||||
this.world = world;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder locYaw(float yaw) {
|
||||
this.yaw = yaw;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder locPitch(float pitch) {
|
||||
this.pitch = pitch;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder lastLogin(Long lastLogin) {
|
||||
this.lastLogin = lastLogin;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder groupId(int groupId) {
|
||||
this.groupId = groupId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder email(String email) {
|
||||
this.email = email;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder registrationIp(String ip) {
|
||||
this.registrationIp = ip;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder registrationDate(long date) {
|
||||
this.registrationDate = date;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder uuid(UUID uuid) {
|
||||
this.uuid = uuid;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package fr.xephi.authme.data.auth;
|
||||
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Used to manage player's Authenticated status
|
||||
*/
|
||||
public class PlayerCache {
|
||||
|
||||
private final Map<String, PlayerAuth> cache = new ConcurrentHashMap<>();
|
||||
|
||||
PlayerCache() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given auth object to the player cache (for the name defined in the PlayerAuth).
|
||||
*
|
||||
* @param auth the player auth object to save
|
||||
*/
|
||||
public void updatePlayer(PlayerAuth auth) {
|
||||
cache.put(auth.getNickname().toLowerCase(Locale.ROOT), auth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a player from the player cache.
|
||||
*
|
||||
* @param user name of the player to remove
|
||||
*/
|
||||
public void removePlayer(String user) {
|
||||
cache.remove(user.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether a player is authenticated (i.e. whether he is present in the player cache).
|
||||
*
|
||||
* @param user player's name
|
||||
*
|
||||
* @return true if player is logged in, false otherwise.
|
||||
*/
|
||||
public boolean isAuthenticated(String user) {
|
||||
return cache.containsKey(user.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the PlayerAuth associated with the given user, if available.
|
||||
*
|
||||
* @param user name of the player
|
||||
*
|
||||
* @return the associated auth object, or null if not available
|
||||
*/
|
||||
public PlayerAuth getAuth(String user) {
|
||||
return cache.get(user.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return number of logged in players
|
||||
*/
|
||||
public int getLogged() {
|
||||
return cache.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player cache data.
|
||||
*
|
||||
* @return all player auths inside the player cache
|
||||
*/
|
||||
public Map<String, PlayerAuth> getCache() {
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package fr.xephi.authme.data.captcha;
|
||||
|
||||
import fr.xephi.authme.util.RandomStringUtils;
|
||||
import fr.xephi.authme.util.expiring.ExpiringMap;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Primitive service for storing captcha codes.
|
||||
*/
|
||||
public class CaptchaCodeStorage {
|
||||
|
||||
/** Map of captcha codes (with player name as key, case-insensitive). */
|
||||
private ExpiringMap<String, String> captchaCodes;
|
||||
/** Number of characters newly generated captcha codes should have. */
|
||||
private int captchaLength;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param expirationInMinutes minutes after which a saved captcha code expires
|
||||
* @param captchaLength the number of characters a captcha code should have
|
||||
*/
|
||||
public CaptchaCodeStorage(long expirationInMinutes, int captchaLength) {
|
||||
this.captchaCodes = new ExpiringMap<>(expirationInMinutes, TimeUnit.MINUTES);
|
||||
this.captchaLength = captchaLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the expiration of captcha codes.
|
||||
*
|
||||
* @param expirationInMinutes minutes after which a saved captcha code expires
|
||||
*/
|
||||
public void setExpirationInMinutes(long expirationInMinutes) {
|
||||
captchaCodes.setExpiration(expirationInMinutes, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the captcha length.
|
||||
*
|
||||
* @param captchaLength number of characters a captcha code should have
|
||||
*/
|
||||
public void setCaptchaLength(int captchaLength) {
|
||||
this.captchaLength = captchaLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stored captcha for the player or generates and saves a new one.
|
||||
*
|
||||
* @param name the player's name
|
||||
* @return the code the player is required to enter
|
||||
*/
|
||||
public String getCodeOrGenerateNew(String name) {
|
||||
String code = captchaCodes.get(name.toLowerCase(Locale.ROOT));
|
||||
return code == null ? generateCode(name) : code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a code for the player and returns it.
|
||||
*
|
||||
* @param name the name of the player to generate a code for
|
||||
* @return the generated code
|
||||
*/
|
||||
private String generateCode(String name) {
|
||||
String code = RandomStringUtils.generate(captchaLength);
|
||||
captchaCodes.put(name.toLowerCase(Locale.ROOT), code);
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the given code against the existing one. Upon success, the saved captcha code is removed from storage.
|
||||
* Upon failure, a new code is generated.
|
||||
*
|
||||
* @param name the name of the player to check
|
||||
* @param code the supplied code
|
||||
* @return true if the code matches, false otherwise
|
||||
*/
|
||||
public boolean checkCode(String name, String code) {
|
||||
String nameLowerCase = name.toLowerCase(Locale.ROOT);
|
||||
String savedCode = captchaCodes.get(nameLowerCase);
|
||||
if (savedCode != null && savedCode.equalsIgnoreCase(code)) {
|
||||
captchaCodes.remove(nameLowerCase);
|
||||
return true;
|
||||
} else {
|
||||
generateCode(name);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void removeExpiredEntries() {
|
||||
captchaCodes.removeExpiredEntries();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package fr.xephi.authme.data.captcha;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Manages captcha codes.
|
||||
*/
|
||||
public interface CaptchaManager {
|
||||
|
||||
/**
|
||||
* Returns whether the given player is required to solve a captcha.
|
||||
*
|
||||
* @param name the name of the player to verify
|
||||
* @return true if the player has to solve a captcha, false otherwise
|
||||
*/
|
||||
boolean isCaptchaRequired(String name);
|
||||
|
||||
/**
|
||||
* Returns the stored captcha for the player or generates and saves a new one.
|
||||
*
|
||||
* @param name the player's name
|
||||
* @return the code the player is required to enter
|
||||
*/
|
||||
String getCaptchaCodeOrGenerateNew(String name);
|
||||
|
||||
/**
|
||||
* Checks the given code against the existing one. This method is not reentrant, i.e. it performs additional
|
||||
* state changes on success or failure, such as modifying some counter or setting a player as verified.
|
||||
* <p>
|
||||
* On success, the code associated with the player is cleared; on failure, a new code is generated.
|
||||
*
|
||||
* @param player the player to check
|
||||
* @param code the supplied code
|
||||
* @return true if the code matches, false otherwise
|
||||
*/
|
||||
boolean checkCode(Player player, String code);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package fr.xephi.authme.data.captcha;
|
||||
|
||||
import fr.xephi.authme.initialization.HasCleanup;
|
||||
import fr.xephi.authme.initialization.SettingsDependent;
|
||||
import fr.xephi.authme.settings.Settings;
|
||||
import fr.xephi.authme.settings.properties.SecuritySettings;
|
||||
import fr.xephi.authme.util.expiring.TimedCounter;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Manager for the handling of captchas after too many failed login attempts.
|
||||
*/
|
||||
public class LoginCaptchaManager implements CaptchaManager, SettingsDependent, HasCleanup {
|
||||
|
||||
private final TimedCounter<String> playerCounts;
|
||||
private final CaptchaCodeStorage captchaCodeStorage;
|
||||
|
||||
private boolean isEnabled;
|
||||
private int threshold;
|
||||
|
||||
@Inject
|
||||
LoginCaptchaManager(Settings settings) {
|
||||
// Note: Proper values are set in reload()
|
||||
this.captchaCodeStorage = new CaptchaCodeStorage(30, 4);
|
||||
this.playerCounts = new TimedCounter<>(9, TimeUnit.MINUTES);
|
||||
reload(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases the failure count for the given player.
|
||||
*
|
||||
* @param name the player's name
|
||||
*/
|
||||
public void increaseLoginFailureCount(String name) {
|
||||
if (isEnabled) {
|
||||
String playerLower = name.toLowerCase(Locale.ROOT);
|
||||
playerCounts.increment(playerLower);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCaptchaRequired(String playerName) {
|
||||
return isEnabled && playerCounts.get(playerName.toLowerCase(Locale.ROOT)) >= threshold;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCaptchaCodeOrGenerateNew(String name) {
|
||||
return captchaCodeStorage.getCodeOrGenerateNew(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkCode(Player player, String code) {
|
||||
String nameLower = player.getName().toLowerCase(Locale.ROOT);
|
||||
boolean isCodeCorrect = captchaCodeStorage.checkCode(nameLower, code);
|
||||
if (isCodeCorrect) {
|
||||
playerCounts.remove(nameLower);
|
||||
}
|
||||
return isCodeCorrect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the login count of the given player to 0.
|
||||
*
|
||||
* @param name the player's name
|
||||
*/
|
||||
public void resetLoginFailureCount(String name) {
|
||||
if (isEnabled) {
|
||||
playerCounts.remove(name.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reload(Settings settings) {
|
||||
int expirationInMinutes = settings.getProperty(SecuritySettings.CAPTCHA_COUNT_MINUTES_BEFORE_RESET);
|
||||
captchaCodeStorage.setExpirationInMinutes(expirationInMinutes);
|
||||
int captchaLength = settings.getProperty(SecuritySettings.CAPTCHA_LENGTH);
|
||||
captchaCodeStorage.setCaptchaLength(captchaLength);
|
||||
|
||||
int countTimeout = settings.getProperty(SecuritySettings.CAPTCHA_COUNT_MINUTES_BEFORE_RESET);
|
||||
playerCounts.setExpiration(countTimeout, TimeUnit.MINUTES);
|
||||
|
||||
isEnabled = settings.getProperty(SecuritySettings.ENABLE_LOGIN_FAILURE_CAPTCHA);
|
||||
threshold = settings.getProperty(SecuritySettings.MAX_LOGIN_TRIES_BEFORE_CAPTCHA);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performCleanup() {
|
||||
playerCounts.removeExpiredEntries();
|
||||
captchaCodeStorage.removeExpiredEntries();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package fr.xephi.authme.data.captcha;
|
||||
|
||||
import fr.xephi.authme.initialization.HasCleanup;
|
||||
import fr.xephi.authme.initialization.SettingsDependent;
|
||||
import fr.xephi.authme.settings.Settings;
|
||||
import fr.xephi.authme.settings.properties.SecuritySettings;
|
||||
import fr.xephi.authme.util.expiring.ExpiringSet;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Captcha manager for registration.
|
||||
*/
|
||||
public class RegistrationCaptchaManager implements CaptchaManager, SettingsDependent, HasCleanup {
|
||||
|
||||
private static final int MINUTES_VALID_FOR_REGISTRATION = 30;
|
||||
|
||||
private final ExpiringSet<String> verifiedNamesForRegistration;
|
||||
private final CaptchaCodeStorage captchaCodeStorage;
|
||||
private boolean isEnabled;
|
||||
|
||||
@Inject
|
||||
RegistrationCaptchaManager(Settings settings) {
|
||||
// NOTE: proper captcha length is set in reload()
|
||||
this.captchaCodeStorage = new CaptchaCodeStorage(MINUTES_VALID_FOR_REGISTRATION, 4);
|
||||
this.verifiedNamesForRegistration = new ExpiringSet<>(MINUTES_VALID_FOR_REGISTRATION, TimeUnit.MINUTES);
|
||||
reload(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCaptchaRequired(String name) {
|
||||
return isEnabled && !verifiedNamesForRegistration.contains(name.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCaptchaCodeOrGenerateNew(String name) {
|
||||
return captchaCodeStorage.getCodeOrGenerateNew(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkCode(Player player, String code) {
|
||||
String nameLower = player.getName().toLowerCase(Locale.ROOT);
|
||||
boolean isCodeCorrect = captchaCodeStorage.checkCode(nameLower, code);
|
||||
if (isCodeCorrect) {
|
||||
verifiedNamesForRegistration.add(nameLower);
|
||||
}
|
||||
return isCodeCorrect;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reload(Settings settings) {
|
||||
int captchaLength = settings.getProperty(SecuritySettings.CAPTCHA_LENGTH);
|
||||
captchaCodeStorage.setCaptchaLength(captchaLength);
|
||||
|
||||
isEnabled = settings.getProperty(SecuritySettings.ENABLE_CAPTCHA_FOR_REGISTRATION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performCleanup() {
|
||||
verifiedNamesForRegistration.removeExpiredEntries();
|
||||
captchaCodeStorage.removeExpiredEntries();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package fr.xephi.authme.data.limbo;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Possible types to restore the "allow flight" property
|
||||
* from LimboPlayer to Bukkit Player.
|
||||
*/
|
||||
public enum AllowFlightRestoreType {
|
||||
|
||||
/** Set value from LimboPlayer to Player. */
|
||||
RESTORE {
|
||||
@Override
|
||||
public void restoreAllowFlight(Player player, LimboPlayer limbo) {
|
||||
player.setAllowFlight(limbo.isCanFly());
|
||||
}
|
||||
},
|
||||
|
||||
/** Always set flight enabled to true. */
|
||||
ENABLE {
|
||||
@Override
|
||||
public void restoreAllowFlight(Player player, LimboPlayer limbo) {
|
||||
player.setAllowFlight(true);
|
||||
}
|
||||
},
|
||||
|
||||
/** Always set flight enabled to false. */
|
||||
DISABLE {
|
||||
@Override
|
||||
public void restoreAllowFlight(Player player, LimboPlayer limbo) {
|
||||
player.setAllowFlight(false);
|
||||
}
|
||||
},
|
||||
|
||||
/** The user's flight handling is not modified. */
|
||||
NOTHING {
|
||||
@Override
|
||||
public void restoreAllowFlight(Player player, LimboPlayer limbo) {
|
||||
// noop
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processPlayer(Player player) {
|
||||
// noop
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Restores the "allow flight" property from the LimboPlayer to the Player.
|
||||
* This method behaves differently for each restoration type.
|
||||
*
|
||||
* @param player the player to modify
|
||||
* @param limbo the limbo player to read from
|
||||
*/
|
||||
public abstract void restoreAllowFlight(Player player, LimboPlayer limbo);
|
||||
|
||||
/**
|
||||
* Processes the player when a LimboPlayer instance is created based on him. Typically this
|
||||
* method revokes the {@code allowFlight} property to be restored again later.
|
||||
*
|
||||
* @param player the player to process
|
||||
*/
|
||||
public void processPlayer(Player player) {
|
||||
player.setAllowFlight(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package fr.xephi.authme.data.limbo;
|
||||
|
||||
import fr.xephi.authme.ConsoleLogger;
|
||||
import fr.xephi.authme.initialization.Reloadable;
|
||||
import fr.xephi.authme.output.ConsoleLoggerFactory;
|
||||
import fr.xephi.authme.permission.PermissionsManager;
|
||||
import fr.xephi.authme.settings.Settings;
|
||||
import fr.xephi.authme.settings.properties.PluginSettings;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.inject.Inject;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* Changes the permission group according to the auth status of the player and the configuration.
|
||||
* <p>
|
||||
* If this feature is enabled, the <i>primary permissions group</i> of a player is replaced until he has
|
||||
* logged in. Some permission plugins have a notion of a primary group; for other permission plugins the
|
||||
* first group is simply taken.
|
||||
* <p>
|
||||
* The groups that are used as replacement until the player logs in is configurable and depends on if
|
||||
* the player is registered or not. Note that some (all?) permission systems require the group to actually
|
||||
* exist for the replacement to take place. Furthermore, since some permission groups require that players
|
||||
* be in at least one group, this will mean that the player is not removed from his primary group.
|
||||
*/
|
||||
class AuthGroupHandler implements Reloadable {
|
||||
|
||||
private final ConsoleLogger logger = ConsoleLoggerFactory.get(AuthGroupHandler.class);
|
||||
|
||||
@Inject
|
||||
private PermissionsManager permissionsManager;
|
||||
|
||||
@Inject
|
||||
private Settings settings;
|
||||
|
||||
private UserGroup unregisteredGroup;
|
||||
private UserGroup registeredGroup;
|
||||
|
||||
AuthGroupHandler() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the group of a player by its authentication status.
|
||||
*
|
||||
* @param player the player
|
||||
* @param limbo the associated limbo player (nullable)
|
||||
* @param groupType the group type
|
||||
*/
|
||||
void setGroup(Player player, LimboPlayer limbo, AuthGroupType groupType) {
|
||||
if (!useAuthGroups()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Collection<UserGroup> previousGroups = limbo == null ? Collections.emptyList() : limbo.getGroups();
|
||||
|
||||
switch (groupType) {
|
||||
// Implementation note: some permission systems don't support players not being in any group,
|
||||
// so add the new group before removing the old ones
|
||||
case UNREGISTERED:
|
||||
permissionsManager.addGroup(player, unregisteredGroup);
|
||||
permissionsManager.removeGroup(player, registeredGroup);
|
||||
permissionsManager.removeGroups(player, previousGroups);
|
||||
break;
|
||||
|
||||
case REGISTERED_UNAUTHENTICATED:
|
||||
permissionsManager.addGroup(player, registeredGroup);
|
||||
permissionsManager.removeGroup(player, unregisteredGroup);
|
||||
permissionsManager.removeGroups(player, previousGroups);
|
||||
|
||||
break;
|
||||
|
||||
case LOGGED_IN:
|
||||
permissionsManager.addGroups(player, previousGroups);
|
||||
permissionsManager.removeGroup(player, unregisteredGroup);
|
||||
permissionsManager.removeGroup(player, registeredGroup);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException("Encountered unhandled auth group type '" + groupType + "'");
|
||||
}
|
||||
|
||||
logger.debug(() -> player.getName() + " changed to "
|
||||
+ groupType + ": has groups " + permissionsManager.getGroups(player));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the auth permissions group function should be used.
|
||||
*
|
||||
* @return true if should be used, false otherwise
|
||||
*/
|
||||
private boolean useAuthGroups() {
|
||||
// Check whether the permissions check is enabled
|
||||
if (!settings.getProperty(PluginSettings.ENABLE_PERMISSION_CHECK)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure group support is available
|
||||
if (!permissionsManager.hasGroupSupport()) {
|
||||
logger.warning("The current permissions system doesn't have group support, unable to set group!");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@PostConstruct
|
||||
public void reload() {
|
||||
unregisteredGroup = new UserGroup(settings.getProperty(PluginSettings.UNREGISTERED_GROUP));
|
||||
registeredGroup = new UserGroup(settings.getProperty(PluginSettings.REGISTERED_GROUP));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package fr.xephi.authme.data.limbo;
|
||||
|
||||
/**
|
||||
* Represents the group type based on the user's auth status.
|
||||
*/
|
||||
enum AuthGroupType {
|
||||
|
||||
/** Player does not have an account. */
|
||||
UNREGISTERED,
|
||||
|
||||
/** Player is registered but not logged in. */
|
||||
REGISTERED_UNAUTHENTICATED,
|
||||
|
||||
/** Player is logged in. */
|
||||
LOGGED_IN
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package fr.xephi.authme.data.limbo;
|
||||
|
||||
public enum LimboMessageType {
|
||||
|
||||
REGISTER,
|
||||
|
||||
LOG_IN,
|
||||
|
||||
TOTP_CODE
|
||||
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package fr.xephi.authme.data.limbo;
|
||||
|
||||
public enum LimboPlayerState {
|
||||
|
||||
PASSWORD_REQUIRED,
|
||||
|
||||
TOTP_REQUIRED
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
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);
|
||||
|
||||
}
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
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/<uuid or name>/" 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
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();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user