#930 Create registration captcha manager

- Introduce registration captcha manager, rename login captcha manager accordingly
- Integrate reg. captcha manager into /register command

Open points:
- Refactor common captcha functionality into abstract superclass
- If captcha before /register necessary, show appropriate message to player immediately
- Unit tests
This commit is contained in:
ljacqu
2017-12-01 21:12:35 +01:00
parent 67a6a42dfe
commit 33904c09e9
11 changed files with 220 additions and 69 deletions
@@ -12,9 +12,9 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* Manager for the handling of captchas.
* Manager for the handling of captchas after too many failed login attempts.
*/
public class CaptchaManager implements SettingsDependent, HasCleanup {
public class LoginCaptchaManager implements SettingsDependent, HasCleanup {
private final TimedCounter<String> playerCounts;
private final ConcurrentHashMap<String, String> captchaCodes;
@@ -24,7 +24,7 @@ public class CaptchaManager implements SettingsDependent, HasCleanup {
private int captchaLength;
@Inject
CaptchaManager(Settings settings) {
LoginCaptchaManager(Settings settings) {
this.captchaCodes = new ConcurrentHashMap<>();
long countTimeout = settings.getProperty(SecuritySettings.CAPTCHA_COUNT_MINUTES_BEFORE_RESET);
this.playerCounts = new TimedCounter<>(countTimeout, TimeUnit.MINUTES);
@@ -36,7 +36,7 @@ public class CaptchaManager implements SettingsDependent, HasCleanup {
*
* @param name the player's name
*/
public void increaseCount(String name) {
public void increaseLoginFailureCount(String name) {
if (isEnabled) {
String playerLower = name.toLowerCase();
playerCounts.increment(playerLower);
@@ -44,7 +44,7 @@ public class CaptchaManager implements SettingsDependent, HasCleanup {
}
/**
* Returns whether the given player is required to solve a captcha.
* Returns whether the given player is required to solve a captcha before he can use /login again.
*
* @param name the name of the player to verify
* @return true if the player has to solve a captcha, false otherwise
@@ -84,12 +84,13 @@ public class CaptchaManager implements SettingsDependent, HasCleanup {
* @return true if the code matches or if no captcha is required for the player, false otherwise
*/
public boolean checkCode(String name, String code) {
String savedCode = captchaCodes.get(name.toLowerCase());
final String nameLowerCase = name.toLowerCase();
String savedCode = captchaCodes.get(nameLowerCase);
if (savedCode == null) {
return true;
} else if (savedCode.equalsIgnoreCase(code)) {
captchaCodes.remove(name.toLowerCase());
playerCounts.remove(name.toLowerCase());
captchaCodes.remove(nameLowerCase);
playerCounts.remove(nameLowerCase);
return true;
}
return false;
@@ -100,7 +101,7 @@ public class CaptchaManager implements SettingsDependent, HasCleanup {
*
* @param name the player's name
*/
public void resetCounts(String name) {
public void resetLoginFailureCount(String name) {
if (isEnabled) {
captchaCodes.remove(name.toLowerCase());
playerCounts.remove(name.toLowerCase());
@@ -109,7 +110,7 @@ public class CaptchaManager implements SettingsDependent, HasCleanup {
@Override
public void reload(Settings settings) {
this.isEnabled = settings.getProperty(SecuritySettings.USE_CAPTCHA);
this.isEnabled = settings.getProperty(SecuritySettings.ENABLE_LOGIN_FAILURE_CAPTCHA);
this.threshold = settings.getProperty(SecuritySettings.MAX_LOGIN_TRIES_BEFORE_CAPTCHA);
this.captchaLength = settings.getProperty(SecuritySettings.CAPTCHA_LENGTH);
long countTimeout = settings.getProperty(SecuritySettings.CAPTCHA_COUNT_MINUTES_BEFORE_RESET);
@@ -0,0 +1,98 @@
package fr.xephi.authme.data;
import fr.xephi.authme.initialization.HasCleanup;
import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import fr.xephi.authme.util.RandomStringUtils;
import fr.xephi.authme.util.expiring.ExpiringSet;
import javax.inject.Inject;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* Captcha handler for registration.
*/
public class RegistrationCaptchaManager implements SettingsDependent, HasCleanup {
private static final int MINUTES_VALID_FOR_REGISTRATION = 30;
private final Map<String, String> captchaCodes;
private final ExpiringSet<String> verifiedNamesForRegistration;
private boolean isEnabledForRegistration;
private int captchaLength;
@Inject
RegistrationCaptchaManager(Settings settings) {
this.captchaCodes = new ConcurrentHashMap<>();
this.verifiedNamesForRegistration = new ExpiringSet<>(MINUTES_VALID_FOR_REGISTRATION, TimeUnit.MINUTES);
reload(settings);
}
/**
* Returns whether the given player is required to solve a captcha before he can register.
*
* @param name the name of the player to verify
* @return true if the player has to solve a captcha, false otherwise
*/
public boolean isCaptchaRequired(String name) {
return isEnabledForRegistration && !verifiedNamesForRegistration.contains(name.toLowerCase());
}
/**
* 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 getCaptchaCodeOrGenerateNew(String name) {
String code = captchaCodes.get(name.toLowerCase());
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
*/
public String generateCode(String name) {
String code = RandomStringUtils.generate(captchaLength);
captchaCodes.put(name.toLowerCase(), code);
return code;
}
/**
* Checks the given code against the existing one and resets the player's auth failure count upon success.
*
* @param name the name of the player to check
* @param code the supplied code
* @return true if the code matches or if no captcha is required for the player, false otherwise
*/
public boolean checkCode(String name, String code) {
final String nameLowerCase = name.toLowerCase();
String savedCode = captchaCodes.get(nameLowerCase);
if (savedCode == null) {
return true;
} else if (savedCode.equalsIgnoreCase(code)) {
captchaCodes.remove(nameLowerCase);
verifiedNamesForRegistration.add(nameLowerCase);
return true;
}
return false;
}
@Override
public void reload(Settings settings) {
this.isEnabledForRegistration = settings.getProperty(SecuritySettings.ENABLE_CAPTCHA_FOR_REGISTRATION);
this.captchaLength = settings.getProperty(SecuritySettings.CAPTCHA_LENGTH);
}
@Override
public void performCleanup() {
verifiedNamesForRegistration.removeExpiredEntries();
}
}