diff --git a/src/main/java/fr/xephi/authme/manager/CaptchaManager.java b/src/main/java/fr/xephi/authme/manager/CaptchaManager.java new file mode 100644 index 00000000..e89e9d49 --- /dev/null +++ b/src/main/java/fr/xephi/authme/manager/CaptchaManager.java @@ -0,0 +1,81 @@ +package fr.xephi.authme.manager; + +import fr.xephi.authme.security.RandomString; +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.properties.SecuritySettings; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * Manager for the handling of captchas. + */ +public class CaptchaManager { + + private final int threshold; + private final int captchaLength; + private final ConcurrentHashMap playerCounts; + private final ConcurrentHashMap captchaCodes; + + public CaptchaManager(NewSetting settings) { + this.playerCounts = new ConcurrentHashMap<>(); + this.captchaCodes = new ConcurrentHashMap<>(); + this.threshold = settings.getProperty(SecuritySettings.MAX_LOGIN_TRIES_BEFORE_CAPTCHA); + this.captchaLength = settings.getProperty(SecuritySettings.CAPTCHA_LENGTH); + } + + public void increaseCount(String player) { + String playerLower = player.toLowerCase(); + Integer currentCount = playerCounts.get(playerLower); + if (currentCount == null) { + playerCounts.put(playerLower, 1); + } else { + playerCounts.put(playerLower, currentCount + 1); + } + } + + /** + * Return whether the given player is required to solve a captcha. + * + * @param player The player to verify + * @return True if the player has to solve a captcha, false otherwise + */ + public boolean isCaptchaRequired(String player) { + Integer count = playerCounts.get(player.toLowerCase()); + return count != null && count >= threshold; + } + + /** + * Return the captcha code for the player. Creates one if none present, so call only after + * checking with {@link #isCaptchaRequired}. + * + * @param player The player + * @return The code required for the player + */ + public String getCaptchaCode(String player) { + String code = captchaCodes.get(player.toLowerCase()); + if (code == null) { + code = RandomString.generate(captchaLength); + captchaCodes.put(player.toLowerCase(), code); + } + return code; + } + + /** + * Return whether the supplied code is correct for the given player. + * + * @param player 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 player, String code) { + String savedCode = captchaCodes.get(player.toLowerCase()); + if (savedCode == null) { + return true; + } else if (savedCode.equalsIgnoreCase(code)) { + captchaCodes.remove(player.toLowerCase()); + return true; + } + return false; + } + +} diff --git a/src/main/java/fr/xephi/authme/manager/IpAddressManager.java b/src/main/java/fr/xephi/authme/manager/IpAddressManager.java index b8be8db3..951e3c8f 100644 --- a/src/main/java/fr/xephi/authme/manager/IpAddressManager.java +++ b/src/main/java/fr/xephi/authme/manager/IpAddressManager.java @@ -60,7 +60,7 @@ public class IpAddressManager { ipCache.remove(player.toLowerCase()); } - // returns null if IP could not be looked up --> expect that it won't be cached + // returns null if IP could not be looked up private String getVeryGamesIp(final String plainIp, final int port) { final String sUrl = String.format("http://monitor-1.verygames.net/api/?action=ipclean-real-ip" + "&out=raw&ip=%s&port=%d", plainIp, port); diff --git a/src/test/java/fr/xephi/authme/manager/CaptchaManagerTest.java b/src/test/java/fr/xephi/authme/manager/CaptchaManagerTest.java new file mode 100644 index 00000000..62fee9bb --- /dev/null +++ b/src/test/java/fr/xephi/authme/manager/CaptchaManagerTest.java @@ -0,0 +1,63 @@ +package fr.xephi.authme.manager; + +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.properties.SecuritySettings; +import org.junit.Test; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Test for {@link CaptchaManager}. + */ +public class CaptchaManagerTest { + + @Test + public void shouldAddCounts() { + // given + NewSetting settings = mockSettings(3, 4); + CaptchaManager manager = new CaptchaManager(settings); + String player = "tester"; + + // when + for (int i = 0; i < 2; ++i) { + manager.increaseCount(player); + } + + // then + assertThat(manager.isCaptchaRequired(player), equalTo(false)); + manager.increaseCount(player); + assertThat(manager.isCaptchaRequired(player.toUpperCase()), equalTo(true)); + assertThat(manager.isCaptchaRequired("otherPlayer"), equalTo(false)); + } + + @Test + public void shouldCreateAndCheckCaptcha() { + // given + String player = "Miner"; + NewSetting settings = mockSettings(1, 4); + CaptchaManager manager = new CaptchaManager(settings); + String captchaCode = manager.getCaptchaCode(player); + + // when + boolean badResult = manager.checkCode(player, "wrong_code"); + boolean goodResult = manager.checkCode(player, captchaCode); + + // then + assertThat(captchaCode.length(), equalTo(4)); + assertThat(badResult, equalTo(false)); + assertThat(goodResult, equalTo(true)); + // Supplying correct code should clear the entry, and any code should be valid if no entry is present + assertThat(manager.checkCode(player, "bogus"), equalTo(true)); + } + + + private static NewSetting mockSettings(int maxTries, int captchaLength) { + NewSetting settings = mock(NewSetting.class); + given(settings.getProperty(SecuritySettings.MAX_LOGIN_TRIES_BEFORE_CAPTCHA)).willReturn(maxTries); + given(settings.getProperty(SecuritySettings.CAPTCHA_LENGTH)).willReturn(captchaLength); + return settings; + } +} diff --git a/src/test/java/fr/xephi/authme/manager/IpAddressManagerTest.java b/src/test/java/fr/xephi/authme/manager/IpAddressManagerTest.java new file mode 100644 index 00000000..b5a1d743 --- /dev/null +++ b/src/test/java/fr/xephi/authme/manager/IpAddressManagerTest.java @@ -0,0 +1,64 @@ +package fr.xephi.authme.manager; + +import fr.xephi.authme.settings.NewSetting; +import fr.xephi.authme.settings.properties.HooksSettings; +import org.bukkit.entity.Player; +import org.junit.Test; + +import java.net.InetAddress; +import java.net.InetSocketAddress; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Test for {@link IpAddressManager}. + */ +public class IpAddressManagerTest { + + @Test + public void shouldRetrieveFromCache() { + // given + IpAddressManager ipAddressManager = new IpAddressManager(mockSettings(false)); + ipAddressManager.addCache("Test", "my test IP"); + + // when + String result = ipAddressManager.getPlayerIp(mockPlayer("test", "123.123.123.123")); + + // then + assertThat(result, equalTo("my test IP")); + } + + @Test + public void shouldReturnPlainIp() { + // given + IpAddressManager ipAddressManager = new IpAddressManager(mockSettings(false)); + + // when + String result = ipAddressManager.getPlayerIp(mockPlayer("bobby", "8.8.8.8")); + + // then + assertThat(result, equalTo("8.8.8.8")); + } + + + + private static NewSetting mockSettings(boolean useVeryGames) { + NewSetting settings = mock(NewSetting.class); + given(settings.getProperty(HooksSettings.ENABLE_VERYGAMES_IP_CHECK)).willReturn(useVeryGames); + return settings; + } + + private static Player mockPlayer(String name, String ip) { + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + InetAddress inetAddress = mock(InetAddress.class); + given(inetAddress.getHostAddress()).willReturn(ip); + InetSocketAddress inetSocketAddress = new InetSocketAddress(inetAddress, 8093); + given(player.getAddress()).willReturn(inetSocketAddress); + return player; + } + +}