From 9954c82cb6cbf11ec24a7fe21359eca3ba9bac07 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 5 Mar 2018 19:50:58 +0100 Subject: [PATCH 01/63] #1141 Add TOTP key field to database and PlayerAuth - Add new field for storing TOTP key - Implement data source methods for manipulation of its value --- .../fr/xephi/authme/data/auth/PlayerAuth.java | 12 +++++++ .../authme/datasource/CacheDataSource.java | 9 ++++++ .../fr/xephi/authme/datasource/Columns.java | 2 ++ .../xephi/authme/datasource/DataSource.java | 19 ++++++++++++ .../fr/xephi/authme/datasource/FlatFile.java | 5 +++ .../fr/xephi/authme/datasource/MySQL.java | 20 ++++++++++++ .../fr/xephi/authme/datasource/SQLite.java | 21 +++++++++++++ .../settings/properties/DatabaseSettings.java | 4 +++ .../AbstractDataSourceIntegrationTest.java | 31 +++++++++++++++++++ .../authme/datasource/sql-initialize.sql | 5 +-- 10 files changed, 126 insertions(+), 2 deletions(-) diff --git a/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java b/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java index 4c8b8ee3..534c0c01 100644 --- a/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java +++ b/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java @@ -26,6 +26,7 @@ public class PlayerAuth { /** 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; @@ -160,6 +161,10 @@ public class PlayerAuth { this.registrationDate = registrationDate; } + public String getTotpKey() { + return totpKey; + } + @Override public boolean equals(Object obj) { if (!(obj instanceof PlayerAuth)) { @@ -195,6 +200,7 @@ public class PlayerAuth { private String name; private String realName; private HashedPassword password; + private String totpKey; private String lastIp; private String email; private int groupId = -1; @@ -219,6 +225,7 @@ public class PlayerAuth { auth.nickname = checkNotNull(name).toLowerCase(); auth.realName = firstNonNull(realName, "Player"); auth.password = firstNonNull(password, 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; @@ -258,6 +265,11 @@ public class PlayerAuth { 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; diff --git a/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java b/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java index 39f04a53..0926def8 100644 --- a/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java +++ b/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java @@ -268,6 +268,15 @@ public class CacheDataSource implements DataSource { return source.getRecentlyLoggedInPlayers(); } + @Override + public boolean setTotpKey(String user, String totpKey) { + boolean result = source.setTotpKey(user, totpKey); + if (result) { + cachedAuths.refresh(user); + } + return result; + } + @Override public void invalidateCache(String playerName) { cachedAuths.invalidate(playerName); diff --git a/src/main/java/fr/xephi/authme/datasource/Columns.java b/src/main/java/fr/xephi/authme/datasource/Columns.java index 946c33de..0d372a23 100644 --- a/src/main/java/fr/xephi/authme/datasource/Columns.java +++ b/src/main/java/fr/xephi/authme/datasource/Columns.java @@ -14,6 +14,7 @@ public final class Columns { public final String REAL_NAME; public final String PASSWORD; public final String SALT; + public final String TOTP_KEY; public final String LAST_IP; public final String LAST_LOGIN; public final String GROUP; @@ -35,6 +36,7 @@ public final class Columns { REAL_NAME = settings.getProperty(DatabaseSettings.MYSQL_COL_REALNAME); PASSWORD = settings.getProperty(DatabaseSettings.MYSQL_COL_PASSWORD); SALT = settings.getProperty(DatabaseSettings.MYSQL_COL_SALT); + TOTP_KEY = settings.getProperty(DatabaseSettings.MYSQL_COL_TOTP_KEY); LAST_IP = settings.getProperty(DatabaseSettings.MYSQL_COL_LAST_IP); LAST_LOGIN = settings.getProperty(DatabaseSettings.MYSQL_COL_LASTLOGIN); GROUP = settings.getProperty(DatabaseSettings.MYSQL_COL_GROUP); diff --git a/src/main/java/fr/xephi/authme/datasource/DataSource.java b/src/main/java/fr/xephi/authme/datasource/DataSource.java index 6f97951d..b6e09fff 100644 --- a/src/main/java/fr/xephi/authme/datasource/DataSource.java +++ b/src/main/java/fr/xephi/authme/datasource/DataSource.java @@ -232,6 +232,25 @@ public interface DataSource extends Reloadable { */ List getRecentlyLoggedInPlayers(); + /** + * Sets the given TOTP key to the player's account. + * + * @param user the name of the player to modify + * @param totpKey the totp key to set + * @return True upon success, false upon failure + */ + boolean setTotpKey(String user, String totpKey); + + /** + * Removes the TOTP key if present of the given player's account. + * + * @param user the name of the player to modify + * @return True upon success, false upon failure + */ + default boolean removeTotpKey(String user) { + return setTotpKey(user, null); + } + /** * Reload the data source. */ diff --git a/src/main/java/fr/xephi/authme/datasource/FlatFile.java b/src/main/java/fr/xephi/authme/datasource/FlatFile.java index d234da55..f70b0376 100644 --- a/src/main/java/fr/xephi/authme/datasource/FlatFile.java +++ b/src/main/java/fr/xephi/authme/datasource/FlatFile.java @@ -398,6 +398,11 @@ public class FlatFile implements DataSource { throw new UnsupportedOperationException("Flat file no longer supported"); } + @Override + public boolean setTotpKey(String user, String totpKey) { + throw new UnsupportedOperationException("Flat file no longer supported"); + } + /** * Creates a PlayerAuth object from the read data. * diff --git a/src/main/java/fr/xephi/authme/datasource/MySQL.java b/src/main/java/fr/xephi/authme/datasource/MySQL.java index 01a240fe..93014edc 100644 --- a/src/main/java/fr/xephi/authme/datasource/MySQL.java +++ b/src/main/java/fr/xephi/authme/datasource/MySQL.java @@ -248,6 +248,11 @@ public class MySQL implements DataSource { st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.HAS_SESSION + " SMALLINT NOT NULL DEFAULT '0' AFTER " + col.IS_LOGGED); } + + if (isColumnMissing(md, col.TOTP_KEY)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.TOTP_KEY + " VARCHAR(16);"); + } } ConsoleLogger.info("MySQL setup finished"); } @@ -728,6 +733,20 @@ public class MySQL implements DataSource { return players; } + @Override + public boolean setTotpKey(String user, String totpKey) { + String sql = "UPDATE " + tableName + " SET " + col.TOTP_KEY + " = ? WHERE " + col.NAME + " = ?"; + try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { + pst.setString(1, totpKey); + pst.setString(2, user.toLowerCase()); + pst.executeUpdate(); + return true; + } catch (SQLException e) { + logSqlException(e); + } + return false; + } + private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException { String salt = col.SALT.isEmpty() ? null : row.getString(col.SALT); int group = col.GROUP.isEmpty() ? -1 : row.getInt(col.GROUP); @@ -735,6 +754,7 @@ public class MySQL implements DataSource { .name(row.getString(col.NAME)) .realName(row.getString(col.REAL_NAME)) .password(row.getString(col.PASSWORD), salt) + .totpKey(row.getString(col.TOTP_KEY)) .lastLogin(getNullableLong(row, col.LAST_LOGIN)) .lastIp(row.getString(col.LAST_IP)) .email(row.getString(col.EMAIL)) diff --git a/src/main/java/fr/xephi/authme/datasource/SQLite.java b/src/main/java/fr/xephi/authme/datasource/SQLite.java index 422d57b1..df46f930 100644 --- a/src/main/java/fr/xephi/authme/datasource/SQLite.java +++ b/src/main/java/fr/xephi/authme/datasource/SQLite.java @@ -171,6 +171,11 @@ public class SQLite implements DataSource { st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.HAS_SESSION + " INT NOT NULL DEFAULT '0';"); } + + if (isColumnMissing(md, col.TOTP_KEY)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.TOTP_KEY + " VARCHAR(16);"); + } } ConsoleLogger.info("SQLite Setup finished"); } @@ -654,6 +659,21 @@ public class SQLite implements DataSource { return players; } + + @Override + public boolean setTotpKey(String user, String totpKey) { + String sql = "UPDATE " + tableName + " SET " + col.TOTP_KEY + " = ? WHERE " + col.NAME + " = ?"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setString(1, totpKey); + pst.setString(2, user.toLowerCase()); + pst.executeUpdate(); + return true; + } catch (SQLException e) { + logSqlException(e); + } + return false; + } + private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException { String salt = !col.SALT.isEmpty() ? row.getString(col.SALT) : null; @@ -662,6 +682,7 @@ public class SQLite implements DataSource { .email(row.getString(col.EMAIL)) .realName(row.getString(col.REAL_NAME)) .password(row.getString(col.PASSWORD), salt) + .totpKey(row.getString(col.TOTP_KEY)) .lastLogin(getNullableLong(row, col.LAST_LOGIN)) .lastIp(row.getString(col.LAST_IP)) .registrationDate(row.getLong(col.REGISTRATION_DATE)) diff --git a/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java b/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java index 66ddd3cd..8ebccf94 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java @@ -79,6 +79,10 @@ public final class DatabaseSettings implements SettingsHolder { public static final Property MYSQL_COL_HASSESSION = newProperty("DataSource.mySQLColumnHasSession", "hasSession"); + @Comment("Column for storing a player's TOTP key (for two-factor authentication)") + public static final Property MYSQL_COL_TOTP_KEY = + newProperty("DataSource.mySQLtotpKey", "totp"); + @Comment("Column for storing the player's last IP") public static final Property MYSQL_COL_LAST_IP = newProperty("DataSource.mySQLColumnIp", "ip"); diff --git a/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java b/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java index 530ab56b..0e809482 100644 --- a/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java @@ -103,12 +103,14 @@ public abstract class AbstractDataSourceIntegrationTest { assertThat(bobbyAuth, hasRegistrationInfo("127.0.4.22", 1436778723L)); assertThat(bobbyAuth.getLastLogin(), equalTo(1449136800L)); assertThat(bobbyAuth.getPassword(), equalToHash("$SHA$11aa0706173d7272$dbba966")); + assertThat(bobbyAuth.getTotpKey(), equalTo("JBSWY3DPEHPK3PXP")); assertThat(userAuth, hasAuthBasicData("user", "user", "user@example.org", "34.56.78.90")); assertThat(userAuth, hasAuthLocation(124.1, 76.3, -127.8, "nether", 0.23f, 4.88f)); assertThat(userAuth, hasRegistrationInfo(null, 0)); assertThat(userAuth.getLastLogin(), equalTo(1453242857L)); assertThat(userAuth.getPassword(), equalToHash("b28c32f624a4eb161d6adc9acb5bfc5b", "f750ba32")); + assertThat(userAuth.getTotpKey(), nullValue()); } @Test @@ -494,4 +496,33 @@ public abstract class AbstractDataSourceIntegrationTest { contains("user24", "user20", "user22", "user29", "user28", "user16", "user18", "user12", "user14", "user11")); } + + @Test + public void shouldSetTotpKey() { + // given + DataSource dataSource = getDataSource(); + String newTotpKey = "My new TOTP key"; + + // when + dataSource.setTotpKey("BObBy", newTotpKey); + dataSource.setTotpKey("does-not-exist", "bogus"); + + // then + assertThat(dataSource.getAuth("bobby").getTotpKey(), equalTo(newTotpKey)); + } + + @Test + public void shouldRemoveTotpKey() { + // given + DataSource dataSource = getDataSource(); + + // when + dataSource.removeTotpKey("BoBBy"); + dataSource.removeTotpKey("user"); + dataSource.removeTotpKey("does-not-exist"); + + // then + assertThat(dataSource.getAuth("bobby").getTotpKey(), nullValue()); + assertThat(dataSource.getAuth("user").getTotpKey(), nullValue()); + } } diff --git a/src/test/resources/fr/xephi/authme/datasource/sql-initialize.sql b/src/test/resources/fr/xephi/authme/datasource/sql-initialize.sql index 306df476..f8153d0d 100644 --- a/src/test/resources/fr/xephi/authme/datasource/sql-initialize.sql +++ b/src/test/resources/fr/xephi/authme/datasource/sql-initialize.sql @@ -4,6 +4,7 @@ CREATE TABLE authme ( id INTEGER AUTO_INCREMENT, username VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, + totp VARCHAR(16), ip VARCHAR(40), lastlogin BIGINT, regdate BIGINT NOT NULL, @@ -22,7 +23,7 @@ CREATE TABLE authme ( CONSTRAINT table_const_prim PRIMARY KEY (id) ); -INSERT INTO authme (id, username, password, ip, lastlogin, x, y, z, world, yaw, pitch, email, isLogged, realname, salt, regdate, regip) -VALUES (1,'bobby','$SHA$11aa0706173d7272$dbba966','123.45.67.89',1449136800,1.05,2.1,4.2,'world',-0.44,2.77,'your@email.com',0,'Bobby',NULL,1436778723,'127.0.4.22'); +INSERT INTO authme (id, username, password, ip, lastlogin, x, y, z, world, yaw, pitch, email, isLogged, realname, salt, regdate, regip, totp) +VALUES (1,'bobby','$SHA$11aa0706173d7272$dbba966','123.45.67.89',1449136800,1.05,2.1,4.2,'world',-0.44,2.77,'your@email.com',0,'Bobby',NULL,1436778723,'127.0.4.22','JBSWY3DPEHPK3PXP'); INSERT INTO authme (id, username, password, ip, lastlogin, x, y, z, world, yaw, pitch, email, isLogged, realname, salt, regdate) VALUES (NULL,'user','b28c32f624a4eb161d6adc9acb5bfc5b','34.56.78.90',1453242857,124.1,76.3,-127.8,'nether',0.23,4.88,'user@example.org',0,'user','f750ba32',0); From c3cf9e3ee0ebd9b56c3f08bf5dd100cf0d1ba7c2 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Wed, 7 Mar 2018 20:11:53 +0100 Subject: [PATCH 02/63] #1141 Rough version of TOTP commands to add and remove a code for a player --- pom.xml | 11 ++ .../authme/command/CommandInitializer.java | 69 +++++++++-- .../executable/totp/AddTotpCommand.java | 41 +++++++ .../executable/totp/ConfirmTotpCommand.java | 39 ++++++ .../executable/totp/RemoveTotpCommand.java | 37 ++++++ .../executable/totp/TotpBaseCommand.java | 29 +++++ .../authme/permission/PlayerPermission.java | 7 +- .../fr/xephi/authme/security/TotpService.java | 113 ++++++++++++++++++ .../xephi/authme/service/BukkitService.java | 7 ++ src/main/resources/plugin.yml | 9 ++ .../command/CommandInitializerTest.java | 2 +- .../authme/security/TotpServiceTest.java | 51 ++++++++ .../authme/service/BukkitServiceTest.java | 13 ++ 13 files changed, 416 insertions(+), 12 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java create mode 100644 src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java create mode 100644 src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java create mode 100644 src/main/java/fr/xephi/authme/command/executable/totp/TotpBaseCommand.java create mode 100644 src/main/java/fr/xephi/authme/security/TotpService.java create mode 100644 src/test/java/fr/xephi/authme/security/TotpServiceTest.java diff --git a/pom.xml b/pom.xml index 56ef2940..2d8b1287 100644 --- a/pom.xml +++ b/pom.xml @@ -286,6 +286,10 @@ org.bstats fr.xephi.authme.libs.org.bstats + + com.warrenstrange + fr.xephi.authme.libs.com.warrenstrange + @@ -461,6 +465,13 @@ true + + + com.warrenstrange + googleauth + 1.1.2 + + org.spigotmc diff --git a/src/main/java/fr/xephi/authme/command/CommandInitializer.java b/src/main/java/fr/xephi/authme/command/CommandInitializer.java index dc5f740f..7f0d33e7 100644 --- a/src/main/java/fr/xephi/authme/command/CommandInitializer.java +++ b/src/main/java/fr/xephi/authme/command/CommandInitializer.java @@ -40,6 +40,10 @@ import fr.xephi.authme.command.executable.email.ShowEmailCommand; import fr.xephi.authme.command.executable.login.LoginCommand; import fr.xephi.authme.command.executable.logout.LogoutCommand; import fr.xephi.authme.command.executable.register.RegisterCommand; +import fr.xephi.authme.command.executable.totp.AddTotpCommand; +import fr.xephi.authme.command.executable.totp.ConfirmTotpCommand; +import fr.xephi.authme.command.executable.totp.RemoveTotpCommand; +import fr.xephi.authme.command.executable.totp.TotpBaseCommand; import fr.xephi.authme.command.executable.unregister.UnregisterCommand; import fr.xephi.authme.command.executable.verification.VerificationCommand; import fr.xephi.authme.permission.AdminPermission; @@ -134,6 +138,9 @@ public class CommandInitializer { .executableCommand(ChangePasswordCommand.class) .register(); + // Create totp base command + CommandDescription totpBase = buildTotpBaseCommand(); + // Register the base captcha command CommandDescription captchaBase = CommandDescription.builder() .parent(null) @@ -156,16 +163,8 @@ public class CommandInitializer { .executableCommand(VerificationCommand.class) .register(); - List baseCommands = ImmutableList.of( - authMeBase, - emailBase, - loginBase, - logoutBase, - registerBase, - unregisterBase, - changePasswordBase, - captchaBase, - verificationBase); + List baseCommands = ImmutableList.of(authMeBase, emailBase, loginBase, logoutBase, + registerBase, unregisterBase, changePasswordBase, totpBase, captchaBase, verificationBase); setHelpOnAllBases(baseCommands); commands = baseCommands; @@ -543,6 +542,56 @@ public class CommandInitializer { return emailBase; } + /** + * Creates a command description object for {@code /totp} including its children. + * + * @return the totp base command description + */ + private CommandDescription buildTotpBaseCommand() { + // Register the base totp command + CommandDescription totpBase = CommandDescription.builder() + .parent(null) + .labels("totp", "2fa") + .description("TOTP commands") + .detailedDescription("Performs actions related to two-factor authentication.") + .executableCommand(TotpBaseCommand.class) + .register(); + + // Register totp add + CommandDescription.builder() + .parent(totpBase) + .labels("add") + .description("Enables TOTP") + .detailedDescription("Enables two-factor authentication for your account.") + .permission(PlayerPermission.TOGGLE_TOTP_STATUS) + .executableCommand(AddTotpCommand.class) + .register(); + + // Register totp confirm + CommandDescription.builder() + .parent(totpBase) + .labels("confirm") + .description("Enables TOTP after successful code") + .detailedDescription("Saves the generated TOTP secret after confirmation.") + .withArgument("code", "Code from the given secret from /totp add", false) + .permission(PlayerPermission.TOGGLE_TOTP_STATUS) + .executableCommand(ConfirmTotpCommand.class) + .register(); + + // Register totp remove + CommandDescription.builder() + .parent(totpBase) + .labels("remove") + .description("Removes TOTP") + .detailedDescription("Disables two-factor authentication for your account.") + .withArgument("code", "Current 2FA code", false) + .permission(PlayerPermission.TOGGLE_TOTP_STATUS) + .executableCommand(RemoveTotpCommand.class) + .register(); + + return totpBase; + } + /** * Sets the help command on all base commands, e.g. to register /authme help or /register help. * diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java new file mode 100644 index 00000000..44eef5d8 --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java @@ -0,0 +1,41 @@ +package fr.xephi.authme.command.executable.totp; + +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.security.TotpService; +import fr.xephi.authme.security.TotpService.TotpGenerationResult; +import fr.xephi.authme.service.CommonService; +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * Command for a player to enable TOTP. + */ +public class AddTotpCommand extends PlayerCommand { + + @Inject + private TotpService totpService; + + @Inject + private DataSource dataSource; + + @Inject + private CommonService commonService; + + @Override + protected void runCommand(Player player, List arguments) { + PlayerAuth auth = dataSource.getAuth(player.getName()); + if (auth.getTotpKey() == null) { + TotpGenerationResult createdTotpInfo = totpService.generateTotpKey(player); + commonService.send(player, MessageKey.TWO_FACTOR_CREATE, + createdTotpInfo.getTotpKey(), createdTotpInfo.getAuthenticatorQrCodeUrl()); + } else { + player.sendMessage(ChatColor.RED + "Two-factor authentication is already enabled for your account!"); + } + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java new file mode 100644 index 00000000..1bf59f7d --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java @@ -0,0 +1,39 @@ +package fr.xephi.authme.command.executable.totp; + +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.security.TotpService; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * Command to enable TOTP by supplying the proper code as confirmation. + */ +public class ConfirmTotpCommand extends PlayerCommand { + + @Inject + private TotpService totpService; + + @Inject + private DataSource dataSource; + + @Override + protected void runCommand(Player player, List arguments) { + // TODO #1141: Check if player already has TOTP + + final String totpKey = totpService.retrieveGeneratedSecret(player); + if (totpKey == null) { + player.sendMessage("No TOTP key has been generated for you or it has expired. Please run /totp add"); + } else { + boolean isTotpCodeValid = totpService.confirmCodeForGeneratedTotpKey(player, arguments.get(0)); + if (isTotpCodeValid) { + dataSource.setTotpKey(player.getName(), totpKey); + player.sendMessage("Successfully enabled two-factor authentication for your account"); + } else { + player.sendMessage("Wrong code or code has expired. Please use /totp add again"); + } + } + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java new file mode 100644 index 00000000..abf0b79c --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java @@ -0,0 +1,37 @@ +package fr.xephi.authme.command.executable.totp; + +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.security.TotpService; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * Command for a player to remove 2FA authentication. + */ +public class RemoveTotpCommand extends PlayerCommand { + + @Inject + private DataSource dataSource; + + @Inject + private TotpService totpService; + + @Override + protected void runCommand(Player player, List arguments) { + PlayerAuth auth = dataSource.getAuth(player.getName()); + if (auth.getTotpKey() == null) { + player.sendMessage("Two-factor authentication is not enabled for your account!"); + } else { + if (totpService.verifyCode(auth, arguments.get(0))) { + dataSource.removeTotpKey(auth.getNickname()); + player.sendMessage("Successfully removed two-factor authentication from your account"); + } else { + player.sendMessage("Invalid code!"); + } + } + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/TotpBaseCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/TotpBaseCommand.java new file mode 100644 index 00000000..2b170a03 --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/totp/TotpBaseCommand.java @@ -0,0 +1,29 @@ +package fr.xephi.authme.command.executable.totp; + +import fr.xephi.authme.command.CommandMapper; +import fr.xephi.authme.command.ExecutableCommand; +import fr.xephi.authme.command.FoundCommandResult; +import fr.xephi.authme.command.help.HelpProvider; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.util.Collections; +import java.util.List; + +/** + * Base command for /totp. + */ +public class TotpBaseCommand implements ExecutableCommand { + + @Inject + private CommandMapper commandMapper; + + @Inject + private HelpProvider helpProvider; + + @Override + public void executeCommand(CommandSender sender, List arguments) { + FoundCommandResult result = commandMapper.mapPartsToCommand(sender, Collections.singletonList("totp")); + helpProvider.outputHelp(sender, result, HelpProvider.SHOW_CHILDREN); + } +} diff --git a/src/main/java/fr/xephi/authme/permission/PlayerPermission.java b/src/main/java/fr/xephi/authme/permission/PlayerPermission.java index 1a89490f..12a4a422 100644 --- a/src/main/java/fr/xephi/authme/permission/PlayerPermission.java +++ b/src/main/java/fr/xephi/authme/permission/PlayerPermission.java @@ -68,7 +68,12 @@ public enum PlayerPermission implements PermissionNode { /** * Permission to use the email verification codes feature. */ - VERIFICATION_CODE("authme.player.security.verificationcode"); + VERIFICATION_CODE("authme.player.security.verificationcode"), + + /** + * Permission to enable and disable TOTP. + */ + TOGGLE_TOTP_STATUS("authme.player.totp"); /** * The permission node. diff --git a/src/main/java/fr/xephi/authme/security/TotpService.java b/src/main/java/fr/xephi/authme/security/TotpService.java new file mode 100644 index 00000000..dd8257b6 --- /dev/null +++ b/src/main/java/fr/xephi/authme/security/TotpService.java @@ -0,0 +1,113 @@ +package fr.xephi.authme.security; + +import com.warrenstrange.googleauth.GoogleAuthenticator; +import com.warrenstrange.googleauth.GoogleAuthenticatorKey; +import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.initialization.HasCleanup; +import fr.xephi.authme.service.BukkitService; +import fr.xephi.authme.util.expiring.ExpiringMap; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.concurrent.TimeUnit; + +/** + * Service for TOTP actions. + */ +public class TotpService implements HasCleanup { + + private static final int NEW_TOTP_KEY_EXPIRATION_MINUTES = 5; + + private final ExpiringMap totpKeys; + private final GoogleAuthenticator authenticator; + private final BukkitService bukkitService; + + @Inject + TotpService(BukkitService bukkitService) { + this.bukkitService = bukkitService; + this.totpKeys = new ExpiringMap<>(NEW_TOTP_KEY_EXPIRATION_MINUTES, TimeUnit.MINUTES); + this.authenticator = new GoogleAuthenticator(); + } + + /** + * Generates a new TOTP key and returns the corresponding QR code. The generated key is saved temporarily + * for the user and can be later retrieved with a confirmation code from {@link #confirmCodeForGeneratedTotpKey}. + * + * @param player the player to save the TOTP key for + * @return TOTP generation result + */ + public TotpGenerationResult generateTotpKey(Player player) { + GoogleAuthenticatorKey credentials = authenticator.createCredentials(); + totpKeys.put(player.getName().toLowerCase(), credentials.getKey()); + String qrCodeUrl = GoogleAuthenticatorQRGenerator.getOtpAuthURL( + bukkitService.getIp(), player.getName(), credentials); + return new TotpGenerationResult(credentials.getKey(), qrCodeUrl); + } + + /** + * Returns the generated TOTP secret of a player, if available and not yet expired. + * + * @param player the player to retrieve the TOTP key for + * @return the totp secret + */ + public String retrieveGeneratedSecret(Player player) { + return totpKeys.get(player.getName().toLowerCase()); + } + + /** + * Returns if the new totp code matches the newly generated totp key. + * + * @param player the player to retrieve the code for + * @param totpCodeConfirmation the input code confirmation + * @return the TOTP secret that was generated for the player, or null if not available or if the code is incorrect + */ + // Maybe by allowing to retrieve without confirmation and exposing verifyCode(String, String) + public boolean confirmCodeForGeneratedTotpKey(Player player, String totpCodeConfirmation) { + String totpSecret = totpKeys.get(player.getName().toLowerCase()); + if (totpSecret != null) { + if (checkCode(totpSecret, totpCodeConfirmation)) { + totpKeys.remove(player.getName().toLowerCase()); + return true; + } + } + return false; + } + + public boolean verifyCode(PlayerAuth auth, String totpCode) { + return checkCode(auth.getTotpKey(), totpCode); + } + + private boolean checkCode(String totpKey, String inputCode) { + try { + Integer totpCode = Integer.valueOf(inputCode); + return authenticator.authorize(totpKey, totpCode); + } catch (NumberFormatException e) { + // ignore + } + return false; + } + + @Override + public void performCleanup() { + totpKeys.removeExpiredEntries(); + } + + public static final class TotpGenerationResult { + private final String totpKey; + private final String authenticatorQrCodeUrl; + + TotpGenerationResult(String totpKey, String authenticatorQrCodeUrl) { + this.totpKey = totpKey; + this.authenticatorQrCodeUrl = authenticatorQrCodeUrl; + } + + public String getTotpKey() { + return totpKey; + } + + public String getAuthenticatorQrCodeUrl() { + return authenticatorQrCodeUrl; + } + } +} diff --git a/src/main/java/fr/xephi/authme/service/BukkitService.java b/src/main/java/fr/xephi/authme/service/BukkitService.java index 6c9cd0cb..f7808098 100644 --- a/src/main/java/fr/xephi/authme/service/BukkitService.java +++ b/src/main/java/fr/xephi/authme/service/BukkitService.java @@ -376,4 +376,11 @@ public class BukkitService implements SettingsDependent { public BanEntry banIp(String ip, String reason, Date expires, String source) { return Bukkit.getServer().getBanList(BanList.Type.IP).addBan(ip, reason, expires, source); } + + /** + * @return the IP string that this server is bound to, otherwise empty string + */ + public String getIp() { + return Bukkit.getServer().getIp(); + } } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 2bae573d..e60721de 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -46,6 +46,11 @@ commands: aliases: - changepass - cp + totp: + description: TOTP commands + usage: /totp add|remove + aliases: + - 2fa captcha: description: Captcha command usage: /captcha @@ -233,6 +238,7 @@ permissions: authme.player.register: true authme.player.security.verificationcode: true authme.player.seeownaccounts: true + authme.player.totp: true authme.player.unregister: true authme.player.canbeforced: description: Permission for users a login can be forced to. @@ -277,6 +283,9 @@ permissions: authme.player.seeownaccounts: description: Permission to use to see own other accounts. default: true + authme.player.totp: + description: Permission to enable and disable TOTP. + default: true authme.player.unregister: description: Command permission to unregister. default: true diff --git a/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java b/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java index 2b8215ba..133d0194 100644 --- a/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandInitializerTest.java @@ -44,7 +44,7 @@ public class CommandInitializerTest { // It obviously doesn't make sense to test much of the concrete data // that is being initialized; we just want to guarantee with this test // that data is indeed being initialized and we take a few "probes" - assertThat(commands, hasSize(9)); + assertThat(commands, hasSize(10)); assertThat(commandsIncludeLabel(commands, "authme"), equalTo(true)); assertThat(commandsIncludeLabel(commands, "register"), equalTo(true)); assertThat(commandsIncludeLabel(commands, "help"), equalTo(false)); diff --git a/src/test/java/fr/xephi/authme/security/TotpServiceTest.java b/src/test/java/fr/xephi/authme/security/TotpServiceTest.java new file mode 100644 index 00000000..6350b4c4 --- /dev/null +++ b/src/test/java/fr/xephi/authme/security/TotpServiceTest.java @@ -0,0 +1,51 @@ +package fr.xephi.authme.security; + +import fr.xephi.authme.security.TotpService.TotpGenerationResult; +import fr.xephi.authme.service.BukkitService; +import org.bukkit.entity.Player; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static fr.xephi.authme.AuthMeMatchers.stringWithLength; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Test for {@link TotpService}. + */ +@RunWith(MockitoJUnitRunner.class) +public class TotpServiceTest { + + @InjectMocks + private TotpService totpService; + + @Mock + private BukkitService bukkitService; + + @Test + public void shouldGenerateTotpKey() { + // given + Player player = mock(Player.class); + given(player.getName()).willReturn("Bobby"); + given(bukkitService.getIp()).willReturn("127.48.44.4"); + + // when + TotpGenerationResult key1 = totpService.generateTotpKey(player); + TotpGenerationResult key2 = totpService.generateTotpKey(player); + + // then + assertThat(key1.getTotpKey(), stringWithLength(16)); + assertThat(key2.getTotpKey(), stringWithLength(16)); + assertThat(key1.getAuthenticatorQrCodeUrl(), startsWith("https://chart.googleapis.com/chart?chs=200x200")); + assertThat(key2.getAuthenticatorQrCodeUrl(), startsWith("https://chart.googleapis.com/chart?chs=200x200")); + assertThat(key1.getTotpKey(), not(equalTo(key2.getTotpKey()))); + } + +} diff --git a/src/test/java/fr/xephi/authme/service/BukkitServiceTest.java b/src/test/java/fr/xephi/authme/service/BukkitServiceTest.java index 232d11ce..e6787c10 100644 --- a/src/test/java/fr/xephi/authme/service/BukkitServiceTest.java +++ b/src/test/java/fr/xephi/authme/service/BukkitServiceTest.java @@ -332,6 +332,19 @@ public class BukkitServiceTest { assertThat(event.getPlayer(), equalTo(player)); } + @Test + public void shouldReturnServerIp() { + // given + String ip = "99.99.99.99"; + given(server.getIp()).willReturn(ip); + + // when + String result = bukkitService.getIp(); + + // then + assertThat(result, equalTo(ip)); + } + // Note: This method is used through reflections public static Player[] onlinePlayersImpl() { return new Player[]{ From e72d5d5e819f496c39e41af8b9080c64f376e1c1 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Fri, 9 Mar 2018 18:37:01 +0100 Subject: [PATCH 03/63] #1141 Require TOTP code to be passed with /login (temporary) - Temporarily require the TOTP code to be provided with /login - Future implementation should require it as a second step --- .../authme/command/CommandInitializer.java | 1 + .../authme/debug/PlayerAuthViewer.java | 1 + .../executable/login/LoginCommand.java | 5 +- .../fr/xephi/authme/process/Management.java | 4 +- .../process/login/AsynchronousLogin.java | 23 +++- .../authme/debug/PlayerAuthViewerTest.java | 106 ++++++++++++++++++ .../executable/login/LoginCommandTest.java | 16 ++- 7 files changed, 147 insertions(+), 9 deletions(-) create mode 100644 src/test/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewerTest.java diff --git a/src/main/java/fr/xephi/authme/command/CommandInitializer.java b/src/main/java/fr/xephi/authme/command/CommandInitializer.java index 7f0d33e7..dd3cc37b 100644 --- a/src/main/java/fr/xephi/authme/command/CommandInitializer.java +++ b/src/main/java/fr/xephi/authme/command/CommandInitializer.java @@ -89,6 +89,7 @@ public class CommandInitializer { .description("Login command") .detailedDescription("Command to log in using AuthMeReloaded.") .withArgument("password", "Login password", false) + .withArgument("2facode", "TOTP code", true) .permission(PlayerPermission.LOGIN) .executableCommand(LoginCommand.class) .register(); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewer.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewer.java index 07e160a4..ae314e09 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewer.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewer.java @@ -77,6 +77,7 @@ class PlayerAuthViewer implements DebugSection { HashedPassword hashedPass = auth.getPassword(); sender.sendMessage("Hash / salt (partial): '" + safeSubstring(hashedPass.getHash(), 6) + "' / '" + safeSubstring(hashedPass.getSalt(), 4) + "'"); + sender.sendMessage("TOTP code (partial): '" + safeSubstring(auth.getTotpKey(), 3) + "'"); } /** diff --git a/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java b/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java index 32a4d87d..e2878271 100644 --- a/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java @@ -18,8 +18,9 @@ public class LoginCommand extends PlayerCommand { @Override public void runCommand(Player player, List arguments) { - final String password = arguments.get(0); - management.performLogin(player, password); + String password = arguments.get(0); + String totpCode = arguments.size() > 1 ? arguments.get(1) : null; + management.performLogin(player, password, totpCode); } @Override diff --git a/src/main/java/fr/xephi/authme/process/Management.java b/src/main/java/fr/xephi/authme/process/Management.java index 5260fed1..a5a879e0 100644 --- a/src/main/java/fr/xephi/authme/process/Management.java +++ b/src/main/java/fr/xephi/authme/process/Management.java @@ -49,8 +49,8 @@ public class Management { } - public void performLogin(Player player, String password) { - runTask(() -> asynchronousLogin.login(player, password)); + public void performLogin(Player player, String password, String totpCode) { + runTask(() -> asynchronousLogin.login(player, password, totpCode)); } public void forceLogin(Player player) { diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index c52202ef..6cadf820 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -18,6 +18,7 @@ import fr.xephi.authme.permission.PlayerStatePermission; import fr.xephi.authme.process.AsynchronousProcess; import fr.xephi.authme.process.SyncProcessManager; import fr.xephi.authme.security.PasswordSecurity; +import fr.xephi.authme.security.TotpService; import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.CommonService; import fr.xephi.authme.service.SessionService; @@ -78,6 +79,9 @@ public class AsynchronousLogin implements AsynchronousProcess { @Inject private BungeeSender bungeeSender; + @Inject + private TotpService totpService; + AsynchronousLogin() { } @@ -86,10 +90,11 @@ public class AsynchronousLogin implements AsynchronousProcess { * * @param player the player to log in * @param password the password to log in with + * @param totpCode the totp code (nullable) */ - public void login(Player player, String password) { + public void login(Player player, String password, String totpCode) { PlayerAuth auth = getPlayerAuth(player); - if (auth != null && checkPlayerInfo(player, auth, password)) { + if (auth != null && checkPlayerInfo(player, auth, password, totpCode)) { performLogin(player, auth); } } @@ -156,10 +161,11 @@ public class AsynchronousLogin implements AsynchronousProcess { * @param player the player requesting to log in * @param auth the PlayerAuth object of the player * @param password the password supplied by the player + * @param totpCode the input totp code (nullable) * @return true if the password matches and all other conditions are met (e.g. no captcha required), * false otherwise */ - private boolean checkPlayerInfo(Player player, PlayerAuth auth, String password) { + private boolean checkPlayerInfo(Player player, PlayerAuth auth, String password, String totpCode) { final String name = player.getName().toLowerCase(); // If captcha is required send a message to the player and deny to log in @@ -174,6 +180,17 @@ public class AsynchronousLogin implements AsynchronousProcess { loginCaptchaManager.increaseLoginFailureCount(name); tempbanManager.increaseCount(ip, name); + if (auth.getTotpKey() != null) { + if (totpCode == null) { + player.sendMessage( + "You have two-factor authentication enabled. Please provide it: /login <2faCode>"); + return false; + } else if (!totpService.verifyCode(auth, totpCode)) { + player.sendMessage("Invalid code for two-factor authentication. Please try again"); + return false; + } + } + if (passwordSecurity.comparePassword(password, auth.getPassword(), player.getName())) { return true; } else { diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewerTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewerTest.java new file mode 100644 index 00000000..8f34bcbf --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewerTest.java @@ -0,0 +1,106 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import org.bukkit.command.CommandSender; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Collections; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItem; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.hamcrest.MockitoHamcrest.argThat; + +/** + * Test for {@link PlayerAuthViewer}. + */ +@RunWith(MockitoJUnitRunner.class) +public class PlayerAuthViewerTest { + + @InjectMocks + private PlayerAuthViewer authViewer; + + @Mock + private DataSource dataSource; + + @Test + public void shouldMakeExample() { + // given + CommandSender sender = mock(CommandSender.class); + + // when + authViewer.execute(sender, Collections.emptyList()); + + // then + verify(sender).sendMessage(argThat(containsString("Example: /authme debug db Bobby"))); + } + + @Test + public void shouldHandleMissingPlayer() { + // given + CommandSender sender = mock(CommandSender.class); + + // when + authViewer.execute(sender, Collections.singletonList("bogus")); + + // then + verify(dataSource).getAuth("bogus"); + verify(sender).sendMessage(argThat(containsString("No record exists for 'bogus'"))); + } + + @Test + public void shouldDisplayAuthInfo() { + // given + CommandSender sender = mock(CommandSender.class); + PlayerAuth auth = PlayerAuth.builder().name("george").realName("George") + .password("abcdefghijkl", "mnopqrst") + .lastIp("127.1.2.7").registrationDate(1111140000000L) + .totpKey("SECRET1321") + .build(); + given(dataSource.getAuth("George")).willReturn(auth); + + // when + authViewer.execute(sender, Collections.singletonList("George")); + + // then + ArgumentCaptor textCaptor = ArgumentCaptor.forClass(String.class); + verify(sender, atLeastOnce()).sendMessage(textCaptor.capture()); + assertThat(textCaptor.getAllValues(), hasItem(containsString("Player george / George"))); + assertThat(textCaptor.getAllValues(), hasItem(containsString("Registration: 2005-03-18T"))); + assertThat(textCaptor.getAllValues(), hasItem(containsString("Hash / salt (partial): 'abcdef...' / 'mnop...'"))); + assertThat(textCaptor.getAllValues(), hasItem(containsString("TOTP code (partial): 'SEC...'"))); + } + + @Test + public void shouldHandleCornerCases() { + // given + CommandSender sender = mock(CommandSender.class); + PlayerAuth auth = PlayerAuth.builder().name("tar") + .password("abcd", null) + .lastIp("127.1.2.7").registrationDate(0L) + .build(); + given(dataSource.getAuth("Tar")).willReturn(auth); + + // when + authViewer.execute(sender, Collections.singletonList("Tar")); + + // then + ArgumentCaptor textCaptor = ArgumentCaptor.forClass(String.class); + verify(sender, atLeastOnce()).sendMessage(textCaptor.capture()); + assertThat(textCaptor.getAllValues(), hasItem(containsString("Player tar / Player"))); + assertThat(textCaptor.getAllValues(), hasItem(containsString("Registration: Not available (0)"))); + assertThat(textCaptor.getAllValues(), hasItem(containsString("Last login: Not available (null)"))); + assertThat(textCaptor.getAllValues(), hasItem(containsString("Hash / salt (partial): 'ab...' / ''"))); + assertThat(textCaptor.getAllValues(), hasItem(containsString("TOTP code (partial): ''"))); + } +} diff --git a/src/test/java/fr/xephi/authme/command/executable/login/LoginCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/login/LoginCommandTest.java index 38307f93..1335ca6f 100644 --- a/src/test/java/fr/xephi/authme/command/executable/login/LoginCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/login/LoginCommandTest.java @@ -11,12 +11,12 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import java.util.Arrays; import java.util.Collections; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; @@ -57,7 +57,19 @@ public class LoginCommandTest { command.executeCommand(sender, Collections.singletonList("password")); // then - verify(management).performLogin(eq(sender), eq("password")); + verify(management).performLogin(sender, "password", null); + } + + @Test + public void shouldCallManagementForPasswordAndTotpCode() { + // given + Player sender = mock(Player.class); + + // when + command.executeCommand(sender, Arrays.asList("pwd", "12345")); + + // then + verify(management).performLogin(sender, "pwd", "12345"); } @Test From eb9cd31a65e064acaf607e66fc44cfacec9513ea Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 10 Mar 2018 16:21:53 +0100 Subject: [PATCH 04/63] #1141 Split TOTP permissions for add/remove, refactor TOTP services - Split TotpService further into GenerateTotpService and TotpAuthenticator, which wraps the GoogleAuthenticator impl - Add missing tests for the services - Change GenerateTotpService's interface to behave like a collection for more intuitive method behavior --- .../authme/command/CommandInitializer.java | 89 +++++++------- .../executable/totp/AddTotpCommand.java | 8 +- .../executable/totp/ConfirmTotpCommand.java | 16 +-- .../executable/totp/RemoveTotpCommand.java | 2 +- .../authme/permission/PlayerPermission.java | 9 +- .../process/login/AsynchronousLogin.java | 2 +- .../fr/xephi/authme/security/TotpService.java | 113 ------------------ .../security/totp/GenerateTotpService.java | 69 +++++++++++ .../security/totp/TotpAuthenticator.java | 67 +++++++++++ .../authme/security/totp/TotpService.java | 18 +++ src/main/resources/plugin.yml | 14 ++- .../authme/security/TotpServiceTest.java | 51 -------- .../totp/GenerateTotpServiceTest.java | 113 ++++++++++++++++++ .../security/totp/TotpAuthenticatorTest.java | 84 +++++++++++++ .../authme/security/totp/TotpServiceTest.java | 46 +++++++ ...lpTranslationGeneratorIntegrationTest.java | 2 +- .../fr/xephi/authme/message/help_test.yml | 3 + 17 files changed, 478 insertions(+), 228 deletions(-) delete mode 100644 src/main/java/fr/xephi/authme/security/TotpService.java create mode 100644 src/main/java/fr/xephi/authme/security/totp/GenerateTotpService.java create mode 100644 src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java create mode 100644 src/main/java/fr/xephi/authme/security/totp/TotpService.java delete mode 100644 src/test/java/fr/xephi/authme/security/TotpServiceTest.java create mode 100644 src/test/java/fr/xephi/authme/security/totp/GenerateTotpServiceTest.java create mode 100644 src/test/java/fr/xephi/authme/security/totp/TotpAuthenticatorTest.java create mode 100644 src/test/java/fr/xephi/authme/security/totp/TotpServiceTest.java diff --git a/src/main/java/fr/xephi/authme/command/CommandInitializer.java b/src/main/java/fr/xephi/authme/command/CommandInitializer.java index dd3cc37b..e1890626 100644 --- a/src/main/java/fr/xephi/authme/command/CommandInitializer.java +++ b/src/main/java/fr/xephi/authme/command/CommandInitializer.java @@ -59,6 +59,9 @@ import java.util.List; */ public class CommandInitializer { + private static final boolean OPTIONAL = true; + private static final boolean MANDATORY = false; + private List commands; public CommandInitializer() { @@ -88,8 +91,8 @@ public class CommandInitializer { .labels("login", "l", "log") .description("Login command") .detailedDescription("Command to log in using AuthMeReloaded.") - .withArgument("password", "Login password", false) - .withArgument("2facode", "TOTP code", true) + .withArgument("password", "Login password", MANDATORY) + .withArgument("2facode", "TOTP code", OPTIONAL) .permission(PlayerPermission.LOGIN) .executableCommand(LoginCommand.class) .register(); @@ -110,8 +113,8 @@ public class CommandInitializer { .labels("register", "reg") .description("Register an account") .detailedDescription("Command to register using AuthMeReloaded.") - .withArgument("password", "Password", true) - .withArgument("verifyPassword", "Verify password", true) + .withArgument("password", "Password", OPTIONAL) + .withArgument("verifyPassword", "Verify password", OPTIONAL) .permission(PlayerPermission.REGISTER) .executableCommand(RegisterCommand.class) .register(); @@ -122,7 +125,7 @@ public class CommandInitializer { .labels("unregister", "unreg") .description("Unregister an account") .detailedDescription("Command to unregister using AuthMeReloaded.") - .withArgument("password", "Password", false) + .withArgument("password", "Password", MANDATORY) .permission(PlayerPermission.UNREGISTER) .executableCommand(UnregisterCommand.class) .register(); @@ -133,8 +136,8 @@ public class CommandInitializer { .labels("changepassword", "changepass", "cp") .description("Change password of an account") .detailedDescription("Command to change your password using AuthMeReloaded.") - .withArgument("oldPassword", "Old password", false) - .withArgument("newPassword", "New password", false) + .withArgument("oldPassword", "Old password", MANDATORY) + .withArgument("newPassword", "New password", MANDATORY) .permission(PlayerPermission.CHANGE_PASSWORD) .executableCommand(ChangePasswordCommand.class) .register(); @@ -148,7 +151,7 @@ public class CommandInitializer { .labels("captcha") .description("Captcha command") .detailedDescription("Captcha command for AuthMeReloaded.") - .withArgument("captcha", "The Captcha", false) + .withArgument("captcha", "The Captcha", MANDATORY) .permission(PlayerPermission.CAPTCHA) .executableCommand(CaptchaCommand.class) .register(); @@ -159,7 +162,7 @@ public class CommandInitializer { .labels("verification") .description("Verification command") .detailedDescription("Command to complete the verification process for AuthMeReloaded.") - .withArgument("code", "The code", false) + .withArgument("code", "The code", MANDATORY) .permission(PlayerPermission.VERIFICATION_CODE) .executableCommand(VerificationCommand.class) .register(); @@ -191,8 +194,8 @@ public class CommandInitializer { .labels("register", "reg", "r") .description("Register a player") .detailedDescription("Register the specified player with the specified password.") - .withArgument("player", "Player name", false) - .withArgument("password", "Password", false) + .withArgument("player", "Player name", MANDATORY) + .withArgument("password", "Password", MANDATORY) .permission(AdminPermission.REGISTER) .executableCommand(RegisterAdminCommand.class) .register(); @@ -203,7 +206,7 @@ public class CommandInitializer { .labels("unregister", "unreg", "unr") .description("Unregister a player") .detailedDescription("Unregister the specified player.") - .withArgument("player", "Player name", false) + .withArgument("player", "Player name", MANDATORY) .permission(AdminPermission.UNREGISTER) .executableCommand(UnregisterAdminCommand.class) .register(); @@ -214,7 +217,7 @@ public class CommandInitializer { .labels("forcelogin", "login") .description("Enforce login player") .detailedDescription("Enforce the specified player to login.") - .withArgument("player", "Online player name", true) + .withArgument("player", "Online player name", OPTIONAL) .permission(AdminPermission.FORCE_LOGIN) .executableCommand(ForceLoginCommand.class) .register(); @@ -225,8 +228,8 @@ public class CommandInitializer { .labels("password", "changepassword", "changepass", "cp") .description("Change a player's password") .detailedDescription("Change the password of a player.") - .withArgument("player", "Player name", false) - .withArgument("pwd", "New password", false) + .withArgument("player", "Player name", MANDATORY) + .withArgument("pwd", "New password", MANDATORY) .permission(AdminPermission.CHANGE_PASSWORD) .executableCommand(ChangePasswordAdminCommand.class) .register(); @@ -237,7 +240,7 @@ public class CommandInitializer { .labels("lastlogin", "ll") .description("Player's last login") .detailedDescription("View the date of the specified players last login.") - .withArgument("player", "Player name", true) + .withArgument("player", "Player name", OPTIONAL) .permission(AdminPermission.LAST_LOGIN) .executableCommand(LastLoginCommand.class) .register(); @@ -248,7 +251,7 @@ public class CommandInitializer { .labels("accounts", "account") .description("Display player accounts") .detailedDescription("Display all accounts of a player by his player name or IP.") - .withArgument("player", "Player name or IP", true) + .withArgument("player", "Player name or IP", OPTIONAL) .permission(AdminPermission.ACCOUNTS) .executableCommand(AccountsCommand.class) .register(); @@ -259,7 +262,7 @@ public class CommandInitializer { .labels("email", "mail", "getemail", "getmail") .description("Display player's email") .detailedDescription("Display the email address of the specified player if set.") - .withArgument("player", "Player name", true) + .withArgument("player", "Player name", OPTIONAL) .permission(AdminPermission.GET_EMAIL) .executableCommand(GetEmailCommand.class) .register(); @@ -270,8 +273,8 @@ public class CommandInitializer { .labels("setemail", "setmail", "chgemail", "chgmail") .description("Change player's email") .detailedDescription("Change the email address of the specified player.") - .withArgument("player", "Player name", false) - .withArgument("email", "Player email", false) + .withArgument("player", "Player name", MANDATORY) + .withArgument("email", "Player email", MANDATORY) .permission(AdminPermission.CHANGE_EMAIL) .executableCommand(SetEmailCommand.class) .register(); @@ -282,7 +285,7 @@ public class CommandInitializer { .labels("getip", "ip") .description("Get player's IP") .detailedDescription("Get the IP address of the specified online player.") - .withArgument("player", "Player name", false) + .withArgument("player", "Player name", MANDATORY) .permission(AdminPermission.GET_IP) .executableCommand(GetIpCommand.class) .register(); @@ -333,7 +336,7 @@ public class CommandInitializer { .labels("purge", "delete") .description("Purge old data") .detailedDescription("Purge old AuthMeReloaded data longer than the specified number of days ago.") - .withArgument("days", "Number of days", false) + .withArgument("days", "Number of days", MANDATORY) .permission(AdminPermission.PURGE) .executableCommand(PurgeCommand.class) .register(); @@ -344,8 +347,8 @@ public class CommandInitializer { .labels("purgeplayer") .description("Purges the data of one player") .detailedDescription("Purges data of the given player.") - .withArgument("player", "The player to purge", false) - .withArgument("options", "'force' to run without checking if player is registered", true) + .withArgument("player", "The player to purge", MANDATORY) + .withArgument("options", "'force' to run without checking if player is registered", OPTIONAL) .permission(AdminPermission.PURGE_PLAYER) .executableCommand(PurgePlayerCommand.class) .register(); @@ -367,7 +370,7 @@ public class CommandInitializer { "resetlastposition", "resetlastpos") .description("Purge player's last position") .detailedDescription("Purge the last know position of the specified player or all of them.") - .withArgument("player/*", "Player name or * for all players", false) + .withArgument("player/*", "Player name or * for all players", MANDATORY) .permission(AdminPermission.PURGE_LAST_POSITION) .executableCommand(PurgeLastPositionCommand.class) .register(); @@ -388,7 +391,7 @@ public class CommandInitializer { .labels("switchantibot", "toggleantibot", "antibot") .description("Switch AntiBot mode") .detailedDescription("Switch or toggle the AntiBot mode to the specified state.") - .withArgument("mode", "ON / OFF", true) + .withArgument("mode", "ON / OFF", OPTIONAL) .permission(AdminPermission.SWITCH_ANTIBOT) .executableCommand(SwitchAntiBotCommand.class) .register(); @@ -419,7 +422,7 @@ public class CommandInitializer { .description("Converter command") .detailedDescription("Converter command for AuthMeReloaded.") .withArgument("job", "Conversion job: xauth / crazylogin / rakamak / " - + "royalauth / vauth / sqliteToSql / mysqlToSqlite / loginsecurity", true) + + "royalauth / vauth / sqliteToSql / mysqlToSqlite / loginsecurity", OPTIONAL) .permission(AdminPermission.CONVERTER) .executableCommand(ConverterCommand.class) .register(); @@ -447,9 +450,9 @@ public class CommandInitializer { .labels("debug", "dbg") .description("Debug features") .detailedDescription("Allows various operations for debugging.") - .withArgument("child", "The child to execute", true) - .withArgument("arg", "argument (depends on debug section)", true) - .withArgument("arg", "argument (depends on debug section)", true) + .withArgument("child", "The child to execute", OPTIONAL) + .withArgument("arg", "argument (depends on debug section)", OPTIONAL) + .withArgument("arg", "argument (depends on debug section)", OPTIONAL) .permission(DebugSectionPermissions.DEBUG_COMMAND) .executableCommand(DebugCommand.class) .register(); @@ -488,8 +491,8 @@ public class CommandInitializer { .labels("add", "addemail", "addmail") .description("Add Email") .detailedDescription("Add a new email address to your account.") - .withArgument("email", "Email address", false) - .withArgument("verifyEmail", "Email address verification", false) + .withArgument("email", "Email address", MANDATORY) + .withArgument("verifyEmail", "Email address verification", MANDATORY) .permission(PlayerPermission.ADD_EMAIL) .executableCommand(AddEmailCommand.class) .register(); @@ -500,8 +503,8 @@ public class CommandInitializer { .labels("change", "changeemail", "changemail") .description("Change Email") .detailedDescription("Change an email address of your account.") - .withArgument("oldEmail", "Old email address", false) - .withArgument("newEmail", "New email address", false) + .withArgument("oldEmail", "Old email address", MANDATORY) + .withArgument("newEmail", "New email address", MANDATORY) .permission(PlayerPermission.CHANGE_EMAIL) .executableCommand(ChangeEmailCommand.class) .register(); @@ -513,7 +516,7 @@ public class CommandInitializer { .description("Recover password using email") .detailedDescription("Recover your account using an Email address by sending a mail containing " + "a new password.") - .withArgument("email", "Email address", false) + .withArgument("email", "Email address", MANDATORY) .permission(PlayerPermission.RECOVER_EMAIL) .executableCommand(RecoverEmailCommand.class) .register(); @@ -524,7 +527,7 @@ public class CommandInitializer { .labels("code") .description("Submit code to recover password") .detailedDescription("Recover your account by submitting a code delivered to your email.") - .withArgument("code", "Recovery code", false) + .withArgument("code", "Recovery code", MANDATORY) .permission(PlayerPermission.RECOVER_EMAIL) .executableCommand(ProcessCodeCommand.class) .register(); @@ -535,7 +538,7 @@ public class CommandInitializer { .labels("setpassword") .description("Set new password after recovery") .detailedDescription("Set a new password after successfully recovering your account.") - .withArgument("password", "New password", false) + .withArgument("password", "New password", MANDATORY) .permission(PlayerPermission.RECOVER_EMAIL) .executableCommand(SetPasswordCommand.class) .register(); @@ -564,7 +567,7 @@ public class CommandInitializer { .labels("add") .description("Enables TOTP") .detailedDescription("Enables two-factor authentication for your account.") - .permission(PlayerPermission.TOGGLE_TOTP_STATUS) + .permission(PlayerPermission.ENABLE_TWO_FACTOR_AUTH) .executableCommand(AddTotpCommand.class) .register(); @@ -574,8 +577,8 @@ public class CommandInitializer { .labels("confirm") .description("Enables TOTP after successful code") .detailedDescription("Saves the generated TOTP secret after confirmation.") - .withArgument("code", "Code from the given secret from /totp add", false) - .permission(PlayerPermission.TOGGLE_TOTP_STATUS) + .withArgument("code", "Code from the given secret from /totp add", MANDATORY) + .permission(PlayerPermission.ENABLE_TWO_FACTOR_AUTH) .executableCommand(ConfirmTotpCommand.class) .register(); @@ -585,8 +588,8 @@ public class CommandInitializer { .labels("remove") .description("Removes TOTP") .detailedDescription("Disables two-factor authentication for your account.") - .withArgument("code", "Current 2FA code", false) - .permission(PlayerPermission.TOGGLE_TOTP_STATUS) + .withArgument("code", "Current 2FA code", MANDATORY) + .permission(PlayerPermission.DISABLE_TWO_FACTOR_AUTH) .executableCommand(RemoveTotpCommand.class) .register(); @@ -607,7 +610,7 @@ public class CommandInitializer { .labels(helpCommandLabels) .description("View help") .detailedDescription("View detailed help for /" + base.getLabels().get(0) + " commands.") - .withArgument("query", "The command or query to view help for.", true) + .withArgument("query", "The command or query to view help for.", OPTIONAL) .executableCommand(HelpCommand.class) .register(); } diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java index 44eef5d8..8fc9aaf1 100644 --- a/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java @@ -4,8 +4,8 @@ import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.message.MessageKey; -import fr.xephi.authme.security.TotpService; -import fr.xephi.authme.security.TotpService.TotpGenerationResult; +import fr.xephi.authme.security.totp.GenerateTotpService; +import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult; import fr.xephi.authme.service.CommonService; import org.bukkit.ChatColor; import org.bukkit.entity.Player; @@ -19,7 +19,7 @@ import java.util.List; public class AddTotpCommand extends PlayerCommand { @Inject - private TotpService totpService; + private GenerateTotpService generateTotpService; @Inject private DataSource dataSource; @@ -31,7 +31,7 @@ public class AddTotpCommand extends PlayerCommand { protected void runCommand(Player player, List arguments) { PlayerAuth auth = dataSource.getAuth(player.getName()); if (auth.getTotpKey() == null) { - TotpGenerationResult createdTotpInfo = totpService.generateTotpKey(player); + TotpGenerationResult createdTotpInfo = generateTotpService.generateTotpKey(player); commonService.send(player, MessageKey.TWO_FACTOR_CREATE, createdTotpInfo.getTotpKey(), createdTotpInfo.getAuthenticatorQrCodeUrl()); } else { diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java index 1bf59f7d..34bdf56d 100644 --- a/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java @@ -2,7 +2,8 @@ package fr.xephi.authme.command.executable.totp; import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.security.TotpService; +import fr.xephi.authme.security.totp.GenerateTotpService; +import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult; import org.bukkit.entity.Player; import javax.inject.Inject; @@ -14,7 +15,7 @@ import java.util.List; public class ConfirmTotpCommand extends PlayerCommand { @Inject - private TotpService totpService; + private GenerateTotpService generateTotpService; @Inject private DataSource dataSource; @@ -23,13 +24,14 @@ public class ConfirmTotpCommand extends PlayerCommand { protected void runCommand(Player player, List arguments) { // TODO #1141: Check if player already has TOTP - final String totpKey = totpService.retrieveGeneratedSecret(player); - if (totpKey == null) { + final TotpGenerationResult totpDetails = generateTotpService.getGeneratedTotpKey(player); + if (totpDetails == null) { player.sendMessage("No TOTP key has been generated for you or it has expired. Please run /totp add"); } else { - boolean isTotpCodeValid = totpService.confirmCodeForGeneratedTotpKey(player, arguments.get(0)); - if (isTotpCodeValid) { - dataSource.setTotpKey(player.getName(), totpKey); + boolean isCodeValid = generateTotpService.isTotpCodeCorrectForGeneratedTotpKey(player, arguments.get(0)); + if (isCodeValid) { + generateTotpService.removeGenerateTotpKey(player); + dataSource.setTotpKey(player.getName(), totpDetails.getTotpKey()); player.sendMessage("Successfully enabled two-factor authentication for your account"); } else { player.sendMessage("Wrong code or code has expired. Please use /totp add again"); diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java index abf0b79c..0a2a371d 100644 --- a/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java @@ -3,7 +3,7 @@ package fr.xephi.authme.command.executable.totp; import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.security.TotpService; +import fr.xephi.authme.security.totp.TotpService; import org.bukkit.entity.Player; import javax.inject.Inject; diff --git a/src/main/java/fr/xephi/authme/permission/PlayerPermission.java b/src/main/java/fr/xephi/authme/permission/PlayerPermission.java index 12a4a422..33b5fd1e 100644 --- a/src/main/java/fr/xephi/authme/permission/PlayerPermission.java +++ b/src/main/java/fr/xephi/authme/permission/PlayerPermission.java @@ -71,9 +71,14 @@ public enum PlayerPermission implements PermissionNode { VERIFICATION_CODE("authme.player.security.verificationcode"), /** - * Permission to enable and disable TOTP. + * Permission to enable two-factor authentication. */ - TOGGLE_TOTP_STATUS("authme.player.totp"); + ENABLE_TWO_FACTOR_AUTH("authme.player.totpadd"), + + /** + * Permission to disable two-factor authentication. + */ + DISABLE_TWO_FACTOR_AUTH("authme.player.totpremove"); /** * The permission node. diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index 6cadf820..629ce1aa 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -18,7 +18,7 @@ import fr.xephi.authme.permission.PlayerStatePermission; import fr.xephi.authme.process.AsynchronousProcess; import fr.xephi.authme.process.SyncProcessManager; import fr.xephi.authme.security.PasswordSecurity; -import fr.xephi.authme.security.TotpService; +import fr.xephi.authme.security.totp.TotpService; import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.CommonService; import fr.xephi.authme.service.SessionService; diff --git a/src/main/java/fr/xephi/authme/security/TotpService.java b/src/main/java/fr/xephi/authme/security/TotpService.java deleted file mode 100644 index dd8257b6..00000000 --- a/src/main/java/fr/xephi/authme/security/TotpService.java +++ /dev/null @@ -1,113 +0,0 @@ -package fr.xephi.authme.security; - -import com.warrenstrange.googleauth.GoogleAuthenticator; -import com.warrenstrange.googleauth.GoogleAuthenticatorKey; -import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator; -import fr.xephi.authme.data.auth.PlayerAuth; -import fr.xephi.authme.initialization.HasCleanup; -import fr.xephi.authme.service.BukkitService; -import fr.xephi.authme.util.expiring.ExpiringMap; -import org.bukkit.entity.Player; - -import javax.inject.Inject; -import java.util.concurrent.TimeUnit; - -/** - * Service for TOTP actions. - */ -public class TotpService implements HasCleanup { - - private static final int NEW_TOTP_KEY_EXPIRATION_MINUTES = 5; - - private final ExpiringMap totpKeys; - private final GoogleAuthenticator authenticator; - private final BukkitService bukkitService; - - @Inject - TotpService(BukkitService bukkitService) { - this.bukkitService = bukkitService; - this.totpKeys = new ExpiringMap<>(NEW_TOTP_KEY_EXPIRATION_MINUTES, TimeUnit.MINUTES); - this.authenticator = new GoogleAuthenticator(); - } - - /** - * Generates a new TOTP key and returns the corresponding QR code. The generated key is saved temporarily - * for the user and can be later retrieved with a confirmation code from {@link #confirmCodeForGeneratedTotpKey}. - * - * @param player the player to save the TOTP key for - * @return TOTP generation result - */ - public TotpGenerationResult generateTotpKey(Player player) { - GoogleAuthenticatorKey credentials = authenticator.createCredentials(); - totpKeys.put(player.getName().toLowerCase(), credentials.getKey()); - String qrCodeUrl = GoogleAuthenticatorQRGenerator.getOtpAuthURL( - bukkitService.getIp(), player.getName(), credentials); - return new TotpGenerationResult(credentials.getKey(), qrCodeUrl); - } - - /** - * Returns the generated TOTP secret of a player, if available and not yet expired. - * - * @param player the player to retrieve the TOTP key for - * @return the totp secret - */ - public String retrieveGeneratedSecret(Player player) { - return totpKeys.get(player.getName().toLowerCase()); - } - - /** - * Returns if the new totp code matches the newly generated totp key. - * - * @param player the player to retrieve the code for - * @param totpCodeConfirmation the input code confirmation - * @return the TOTP secret that was generated for the player, or null if not available or if the code is incorrect - */ - // Maybe by allowing to retrieve without confirmation and exposing verifyCode(String, String) - public boolean confirmCodeForGeneratedTotpKey(Player player, String totpCodeConfirmation) { - String totpSecret = totpKeys.get(player.getName().toLowerCase()); - if (totpSecret != null) { - if (checkCode(totpSecret, totpCodeConfirmation)) { - totpKeys.remove(player.getName().toLowerCase()); - return true; - } - } - return false; - } - - public boolean verifyCode(PlayerAuth auth, String totpCode) { - return checkCode(auth.getTotpKey(), totpCode); - } - - private boolean checkCode(String totpKey, String inputCode) { - try { - Integer totpCode = Integer.valueOf(inputCode); - return authenticator.authorize(totpKey, totpCode); - } catch (NumberFormatException e) { - // ignore - } - return false; - } - - @Override - public void performCleanup() { - totpKeys.removeExpiredEntries(); - } - - public static final class TotpGenerationResult { - private final String totpKey; - private final String authenticatorQrCodeUrl; - - TotpGenerationResult(String totpKey, String authenticatorQrCodeUrl) { - this.totpKey = totpKey; - this.authenticatorQrCodeUrl = authenticatorQrCodeUrl; - } - - public String getTotpKey() { - return totpKey; - } - - public String getAuthenticatorQrCodeUrl() { - return authenticatorQrCodeUrl; - } - } -} diff --git a/src/main/java/fr/xephi/authme/security/totp/GenerateTotpService.java b/src/main/java/fr/xephi/authme/security/totp/GenerateTotpService.java new file mode 100644 index 00000000..14a6a6bb --- /dev/null +++ b/src/main/java/fr/xephi/authme/security/totp/GenerateTotpService.java @@ -0,0 +1,69 @@ +package fr.xephi.authme.security.totp; + +import fr.xephi.authme.initialization.HasCleanup; +import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult; +import fr.xephi.authme.util.expiring.ExpiringMap; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.concurrent.TimeUnit; + +/** + * Handles the generation of new TOTP secrets for players. + */ +public class GenerateTotpService implements HasCleanup { + + private static final int NEW_TOTP_KEY_EXPIRATION_MINUTES = 5; + + private final ExpiringMap totpKeys; + + @Inject + private TotpAuthenticator totpAuthenticator; + + GenerateTotpService() { + this.totpKeys = new ExpiringMap<>(NEW_TOTP_KEY_EXPIRATION_MINUTES, TimeUnit.MINUTES); + } + + /** + * Generates a new TOTP key and returns the corresponding QR code. + * + * @param player the player to save the TOTP key for + * @return TOTP generation result + */ + public TotpGenerationResult generateTotpKey(Player player) { + TotpGenerationResult credentials = totpAuthenticator.generateTotpKey(player); + totpKeys.put(player.getName().toLowerCase(), credentials); + return credentials; + } + + /** + * Returns the generated TOTP secret of a player, if available and not yet expired. + * + * @param player the player to retrieve the TOTP key for + * @return TOTP generation result + */ + public TotpGenerationResult getGeneratedTotpKey(Player player) { + return totpKeys.get(player.getName().toLowerCase()); + } + + public void removeGenerateTotpKey(Player player) { + totpKeys.remove(player.getName().toLowerCase()); + } + + /** + * Returns whether the given totp code is correct for the generated TOTP key of the player. + * + * @param player the player to verify the code for + * @param totpCode the totp code to verify with the generated secret + * @return true if the input code is correct, false if the code is invalid or no unexpired totp key is available + */ + public boolean isTotpCodeCorrectForGeneratedTotpKey(Player player, String totpCode) { + TotpGenerationResult totpDetails = totpKeys.get(player.getName().toLowerCase()); + return totpDetails != null && totpAuthenticator.checkCode(totpDetails.getTotpKey(), totpCode); + } + + @Override + public void performCleanup() { + totpKeys.removeExpiredEntries(); + } +} diff --git a/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java b/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java new file mode 100644 index 00000000..ed31da3a --- /dev/null +++ b/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java @@ -0,0 +1,67 @@ +package fr.xephi.authme.security.totp; + +import com.google.common.annotations.VisibleForTesting; +import com.warrenstrange.googleauth.GoogleAuthenticator; +import com.warrenstrange.googleauth.GoogleAuthenticatorKey; +import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator; +import com.warrenstrange.googleauth.IGoogleAuthenticator; +import fr.xephi.authme.service.BukkitService; +import org.bukkit.entity.Player; + +import javax.inject.Inject; + +/** + * Provides rudimentary TOTP functions (wraps third-party TOTP implementation). + */ +public class TotpAuthenticator { + + private final IGoogleAuthenticator authenticator; + private final BukkitService bukkitService; + + + @Inject + TotpAuthenticator(BukkitService bukkitService) { + this(new GoogleAuthenticator(), bukkitService); + } + + @VisibleForTesting + TotpAuthenticator(IGoogleAuthenticator authenticator, BukkitService bukkitService) { + this.authenticator = authenticator; + this.bukkitService = bukkitService; + } + + public boolean checkCode(String totpKey, String inputCode) { + try { + Integer totpCode = Integer.valueOf(inputCode); + return authenticator.authorize(totpKey, totpCode); + } catch (NumberFormatException e) { + // ignore + } + return false; + } + + public TotpGenerationResult generateTotpKey(Player player) { + GoogleAuthenticatorKey credentials = authenticator.createCredentials(); + String qrCodeUrl = GoogleAuthenticatorQRGenerator.getOtpAuthURL( + bukkitService.getIp(), player.getName(), credentials); + return new TotpGenerationResult(credentials.getKey(), qrCodeUrl); + } + + public static final class TotpGenerationResult { + private final String totpKey; + private final String authenticatorQrCodeUrl; + + TotpGenerationResult(String totpKey, String authenticatorQrCodeUrl) { + this.totpKey = totpKey; + this.authenticatorQrCodeUrl = authenticatorQrCodeUrl; + } + + public String getTotpKey() { + return totpKey; + } + + public String getAuthenticatorQrCodeUrl() { + return authenticatorQrCodeUrl; + } + } +} diff --git a/src/main/java/fr/xephi/authme/security/totp/TotpService.java b/src/main/java/fr/xephi/authme/security/totp/TotpService.java new file mode 100644 index 00000000..15e3381f --- /dev/null +++ b/src/main/java/fr/xephi/authme/security/totp/TotpService.java @@ -0,0 +1,18 @@ +package fr.xephi.authme.security.totp; + +import fr.xephi.authme.data.auth.PlayerAuth; + +import javax.inject.Inject; + +/** + * Service for TOTP actions. + */ +public class TotpService { + + @Inject + private TotpAuthenticator totpAuthenticator; + + public boolean verifyCode(PlayerAuth auth, String totpCode) { + return totpAuthenticator.checkCode(auth.getTotpKey(), totpCode); + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index e60721de..bee65078 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -23,7 +23,7 @@ commands: usage: /email show|add|change|recover|code|setpassword login: description: Login command - usage: /login + usage: /login [2facode] aliases: - l - log @@ -48,7 +48,7 @@ commands: - cp totp: description: TOTP commands - usage: /totp add|remove + usage: /totp add|confirm|remove aliases: - 2fa captcha: @@ -238,7 +238,8 @@ permissions: authme.player.register: true authme.player.security.verificationcode: true authme.player.seeownaccounts: true - authme.player.totp: true + authme.player.totpadd: true + authme.player.totpremove: true authme.player.unregister: true authme.player.canbeforced: description: Permission for users a login can be forced to. @@ -283,8 +284,11 @@ permissions: authme.player.seeownaccounts: description: Permission to use to see own other accounts. default: true - authme.player.totp: - description: Permission to enable and disable TOTP. + authme.player.totpadd: + description: Permission to enable two-factor authentication. + default: true + authme.player.totpremove: + description: Permission to disable two-factor authentication. default: true authme.player.unregister: description: Command permission to unregister. diff --git a/src/test/java/fr/xephi/authme/security/TotpServiceTest.java b/src/test/java/fr/xephi/authme/security/TotpServiceTest.java deleted file mode 100644 index 6350b4c4..00000000 --- a/src/test/java/fr/xephi/authme/security/TotpServiceTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package fr.xephi.authme.security; - -import fr.xephi.authme.security.TotpService.TotpGenerationResult; -import fr.xephi.authme.service.BukkitService; -import org.bukkit.entity.Player; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import static fr.xephi.authme.AuthMeMatchers.stringWithLength; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.startsWith; -import static org.junit.Assert.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - -/** - * Test for {@link TotpService}. - */ -@RunWith(MockitoJUnitRunner.class) -public class TotpServiceTest { - - @InjectMocks - private TotpService totpService; - - @Mock - private BukkitService bukkitService; - - @Test - public void shouldGenerateTotpKey() { - // given - Player player = mock(Player.class); - given(player.getName()).willReturn("Bobby"); - given(bukkitService.getIp()).willReturn("127.48.44.4"); - - // when - TotpGenerationResult key1 = totpService.generateTotpKey(player); - TotpGenerationResult key2 = totpService.generateTotpKey(player); - - // then - assertThat(key1.getTotpKey(), stringWithLength(16)); - assertThat(key2.getTotpKey(), stringWithLength(16)); - assertThat(key1.getAuthenticatorQrCodeUrl(), startsWith("https://chart.googleapis.com/chart?chs=200x200")); - assertThat(key2.getAuthenticatorQrCodeUrl(), startsWith("https://chart.googleapis.com/chart?chs=200x200")); - assertThat(key1.getTotpKey(), not(equalTo(key2.getTotpKey()))); - } - -} diff --git a/src/test/java/fr/xephi/authme/security/totp/GenerateTotpServiceTest.java b/src/test/java/fr/xephi/authme/security/totp/GenerateTotpServiceTest.java new file mode 100644 index 00000000..1a22d26e --- /dev/null +++ b/src/test/java/fr/xephi/authme/security/totp/GenerateTotpServiceTest.java @@ -0,0 +1,113 @@ +package fr.xephi.authme.security.totp; + +import fr.xephi.authme.ReflectionTestUtils; +import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult; +import fr.xephi.authme.util.expiring.ExpiringMap; +import org.bukkit.entity.Player; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Test for {@link GenerateTotpService}. + */ +@RunWith(MockitoJUnitRunner.class) +public class GenerateTotpServiceTest { + + @InjectMocks + private GenerateTotpService generateTotpService; + + @Mock + private TotpAuthenticator totpAuthenticator; + + @Test + public void shouldGenerateTotpKey() { + // given + TotpGenerationResult givenGenerationResult = new TotpGenerationResult("1234", "http://example.com/link/to/chart"); + Player player = mockPlayerWithName("Spencer"); + given(totpAuthenticator.generateTotpKey(player)).willReturn(givenGenerationResult); + + // when + TotpGenerationResult result = generateTotpService.generateTotpKey(player); + + // then + assertThat(result, equalTo(givenGenerationResult)); + assertThat(generateTotpService.getGeneratedTotpKey(player), equalTo(givenGenerationResult)); + } + + @Test + public void shouldRemoveGeneratedTotpKey() { + // given + TotpGenerationResult givenGenerationResult = new TotpGenerationResult("1234", "http://example.com/link/to/chart"); + Player player = mockPlayerWithName("Hanna"); + given(totpAuthenticator.generateTotpKey(player)).willReturn(givenGenerationResult); + generateTotpService.generateTotpKey(player); + + // when + generateTotpService.removeGenerateTotpKey(player); + + // then + assertThat(generateTotpService.getGeneratedTotpKey(player), nullValue()); + } + + @Test + public void shouldCheckGeneratedTotpKey() { + // given + String generatedKey = "ASLO43KDF2J"; + TotpGenerationResult givenGenerationResult = new TotpGenerationResult(generatedKey, "url"); + Player player = mockPlayerWithName("Aria"); + given(totpAuthenticator.generateTotpKey(player)).willReturn(givenGenerationResult); + generateTotpService.generateTotpKey(player); + String validCode = "928374"; + given(totpAuthenticator.checkCode(generatedKey, validCode)).willReturn(true); + + // when + boolean invalidCodeResult = generateTotpService.isTotpCodeCorrectForGeneratedTotpKey(player, "000000"); + boolean validCodeResult = generateTotpService.isTotpCodeCorrectForGeneratedTotpKey(player, validCode); + boolean unknownPlayerResult = generateTotpService.isTotpCodeCorrectForGeneratedTotpKey(mockPlayerWithName("other"), "299874"); + + // then + assertThat(invalidCodeResult, equalTo(false)); + assertThat(validCodeResult, equalTo(true)); + assertThat(unknownPlayerResult, equalTo(false)); + verify(totpAuthenticator).checkCode(generatedKey, "000000"); + verify(totpAuthenticator).checkCode(generatedKey, validCode); + } + + @Test + public void shouldRemoveExpiredEntries() throws InterruptedException { + // given + TotpGenerationResult generationResult = new TotpGenerationResult("key", "url"); + ExpiringMap generatedKeys = + ReflectionTestUtils.getFieldValue(GenerateTotpService.class, generateTotpService, "totpKeys"); + generatedKeys.setExpiration(1, TimeUnit.MILLISECONDS); + generatedKeys.put("ghost", generationResult); + generatedKeys.setExpiration(5, TimeUnit.MINUTES); + generatedKeys.put("ezra", generationResult); + + // when + Thread.sleep(2L); + generateTotpService.performCleanup(); + + // then + assertThat(generateTotpService.getGeneratedTotpKey(mockPlayerWithName("Ezra")), equalTo(generationResult)); + assertThat(generateTotpService.getGeneratedTotpKey(mockPlayerWithName("ghost")), nullValue()); + } + + private static Player mockPlayerWithName(String name) { + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + return player; + } +} diff --git a/src/test/java/fr/xephi/authme/security/totp/TotpAuthenticatorTest.java b/src/test/java/fr/xephi/authme/security/totp/TotpAuthenticatorTest.java new file mode 100644 index 00000000..eb2746fa --- /dev/null +++ b/src/test/java/fr/xephi/authme/security/totp/TotpAuthenticatorTest.java @@ -0,0 +1,84 @@ +package fr.xephi.authme.security.totp; + +import com.warrenstrange.googleauth.IGoogleAuthenticator; +import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult; +import fr.xephi.authme.service.BukkitService; +import org.bukkit.entity.Player; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static fr.xephi.authme.AuthMeMatchers.stringWithLength; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link TotpAuthenticator}. + */ +@RunWith(MockitoJUnitRunner.class) +public class TotpAuthenticatorTest { + + @InjectMocks + private TotpAuthenticator totpAuthenticator; + + @Mock + private BukkitService bukkitService; + + @Mock + private IGoogleAuthenticator googleAuthenticator; + + @Test + public void shouldGenerateTotpKey() { + // given + // Use the GoogleAuthenticator instance the TotpAuthenticator normally creates to test its parameters + totpAuthenticator = new TotpAuthenticator(bukkitService); + + Player player = mock(Player.class); + given(player.getName()).willReturn("Bobby"); + given(bukkitService.getIp()).willReturn("127.48.44.4"); + + // when + TotpGenerationResult key1 = totpAuthenticator.generateTotpKey(player); + TotpGenerationResult key2 = totpAuthenticator.generateTotpKey(player); + + // then + assertThat(key1.getTotpKey(), stringWithLength(16)); + assertThat(key2.getTotpKey(), stringWithLength(16)); + assertThat(key1.getAuthenticatorQrCodeUrl(), startsWith("https://chart.googleapis.com/chart?chs=200x200")); + assertThat(key2.getAuthenticatorQrCodeUrl(), startsWith("https://chart.googleapis.com/chart?chs=200x200")); + assertThat(key1.getTotpKey(), not(equalTo(key2.getTotpKey()))); + } + + @Test + public void shouldCheckCode() { + // given + String secret = "the_secret"; + int code = 21398; + given(googleAuthenticator.authorize(secret, code)).willReturn(true); + + // when + boolean result = totpAuthenticator.checkCode(secret, Integer.toString(code)); + + // then + assertThat(result, equalTo(true)); + verify(googleAuthenticator).authorize(secret, code); + } + + @Test + public void shouldHandleInvalidNumberInput() { + // given / when + boolean result = totpAuthenticator.checkCode("Some_Secret", "123ZZ"); + + // then + assertThat(result, equalTo(false)); + verifyZeroInteractions(googleAuthenticator); + } +} diff --git a/src/test/java/fr/xephi/authme/security/totp/TotpServiceTest.java b/src/test/java/fr/xephi/authme/security/totp/TotpServiceTest.java new file mode 100644 index 00000000..7d321cf1 --- /dev/null +++ b/src/test/java/fr/xephi/authme/security/totp/TotpServiceTest.java @@ -0,0 +1,46 @@ +package fr.xephi.authme.security.totp; + +import fr.xephi.authme.data.auth.PlayerAuth; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +/** + * Test for {@link TotpService}. + */ +@RunWith(MockitoJUnitRunner.class) +public class TotpServiceTest { + + @InjectMocks + private TotpService totpService; + + @Mock + private TotpAuthenticator totpAuthenticator; + + @Test + public void shouldVerifyCode() { + // given + String totpKey = "ASLO43KDF2J"; + PlayerAuth auth = PlayerAuth.builder() + .name("Maya") + .totpKey(totpKey) + .build(); + String inputCode = "408435"; + given(totpAuthenticator.checkCode(totpKey, inputCode)).willReturn(true); + + // when + boolean result = totpService.verifyCode(auth, inputCode); + + // then + assertThat(result, equalTo(true)); + verify(totpAuthenticator).checkCode(totpKey, inputCode); + } + +} diff --git a/src/test/java/fr/xephi/authme/service/HelpTranslationGeneratorIntegrationTest.java b/src/test/java/fr/xephi/authme/service/HelpTranslationGeneratorIntegrationTest.java index 3e094cc2..9159b424 100644 --- a/src/test/java/fr/xephi/authme/service/HelpTranslationGeneratorIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/service/HelpTranslationGeneratorIntegrationTest.java @@ -126,7 +126,7 @@ public class HelpTranslationGeneratorIntegrationTest { // Check /login checkDescription(configuration.get("commands.login"), "Login command", "/login detailed desc."); - checkArgs(configuration.get("commands.login"), arg("loginArg", "Login password")); + checkArgs(configuration.get("commands.login"), arg("loginArg", "Login password"), arg("2faArg", "TOTP code")); // Check /unregister checkDescription(configuration.get("commands.unregister"), "unreg_desc", "unreg_detail_desc"); diff --git a/src/test/resources/fr/xephi/authme/message/help_test.yml b/src/test/resources/fr/xephi/authme/message/help_test.yml index d6970daa..9647ecd7 100644 --- a/src/test/resources/fr/xephi/authme/message/help_test.yml +++ b/src/test/resources/fr/xephi/authme/message/help_test.yml @@ -65,6 +65,9 @@ commands: arg1: label: loginArg nonExistent: does not exist + arg2: + label: 2faArg + rhetorical: false someProp: label: '''someProp'' does not exist' description: idk From 495cfc69a951a3b6b913d0b5c642a963069bc8ff Mon Sep 17 00:00:00 2001 From: ljacqu Date: Tue, 20 Mar 2018 23:06:08 +0100 Subject: [PATCH 05/63] #1141 Move TOTP code during login as separate step: /2fa code Rough version. - Introduces a limbo player state on the LimboPlayer, allowing us to add further mandatory actions between successful (password) authentication and the ability to play on the server --- .../authme/command/CommandInitializer.java | 11 ++- .../executable/captcha/CaptchaCommand.java | 3 +- .../executable/login/LoginCommand.java | 3 +- .../executable/totp/TotpCodeCommand.java | 72 +++++++++++++++++++ .../authme/data/limbo/LimboMessageType.java | 11 +++ .../xephi/authme/data/limbo/LimboPlayer.java | 9 +++ .../authme/data/limbo/LimboPlayerState.java | 11 +++ .../data/limbo/LimboPlayerTaskManager.java | 14 ++-- .../xephi/authme/data/limbo/LimboService.java | 11 +-- .../fr/xephi/authme/message/MessageKey.java | 3 + .../message/updater/MessageUpdater.java | 1 + .../fr/xephi/authme/process/Management.java | 4 +- .../process/login/AsynchronousLogin.java | 37 ++++------ .../properties/RestrictionSettings.java | 2 +- src/main/resources/messages/messages_en.yml | 3 + .../executable/login/LoginCommandTest.java | 15 +--- .../limbo/LimboPlayerTaskManagerTest.java | 8 +-- .../authme/data/limbo/LimboServiceTest.java | 6 +- 18 files changed, 162 insertions(+), 62 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/command/executable/totp/TotpCodeCommand.java create mode 100644 src/main/java/fr/xephi/authme/data/limbo/LimboMessageType.java create mode 100644 src/main/java/fr/xephi/authme/data/limbo/LimboPlayerState.java diff --git a/src/main/java/fr/xephi/authme/command/CommandInitializer.java b/src/main/java/fr/xephi/authme/command/CommandInitializer.java index e1890626..e2450ba8 100644 --- a/src/main/java/fr/xephi/authme/command/CommandInitializer.java +++ b/src/main/java/fr/xephi/authme/command/CommandInitializer.java @@ -44,6 +44,7 @@ import fr.xephi.authme.command.executable.totp.AddTotpCommand; import fr.xephi.authme.command.executable.totp.ConfirmTotpCommand; import fr.xephi.authme.command.executable.totp.RemoveTotpCommand; import fr.xephi.authme.command.executable.totp.TotpBaseCommand; +import fr.xephi.authme.command.executable.totp.TotpCodeCommand; import fr.xephi.authme.command.executable.unregister.UnregisterCommand; import fr.xephi.authme.command.executable.verification.VerificationCommand; import fr.xephi.authme.permission.AdminPermission; @@ -92,7 +93,6 @@ public class CommandInitializer { .description("Login command") .detailedDescription("Command to log in using AuthMeReloaded.") .withArgument("password", "Login password", MANDATORY) - .withArgument("2facode", "TOTP code", OPTIONAL) .permission(PlayerPermission.LOGIN) .executableCommand(LoginCommand.class) .register(); @@ -561,6 +561,15 @@ public class CommandInitializer { .executableCommand(TotpBaseCommand.class) .register(); + // Register the base totp code + CommandDescription.builder() + .parent(null) + .labels("code", "c") + .description("Command for logging in") + .detailedDescription("Processes the two-factor authentication code during login.") + .executableCommand(TotpCodeCommand.class) + .register(); + // Register totp add CommandDescription.builder() .parent(totpBase) diff --git a/src/main/java/fr/xephi/authme/command/executable/captcha/CaptchaCommand.java b/src/main/java/fr/xephi/authme/command/executable/captcha/CaptchaCommand.java index b0da817f..c486dd04 100644 --- a/src/main/java/fr/xephi/authme/command/executable/captcha/CaptchaCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/captcha/CaptchaCommand.java @@ -4,6 +4,7 @@ import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.data.captcha.LoginCaptchaManager; import fr.xephi.authme.data.captcha.RegistrationCaptchaManager; +import fr.xephi.authme.data.limbo.LimboMessageType; import fr.xephi.authme.data.limbo.LimboService; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.message.MessageKey; @@ -80,6 +81,6 @@ public class CaptchaCommand extends PlayerCommand { String newCode = registrationCaptchaManager.getCaptchaCodeOrGenerateNew(player.getName()); commonService.send(player, MessageKey.CAPTCHA_WRONG_ERROR, newCode); } - limboService.resetMessageTask(player, false); + limboService.resetMessageTask(player, LimboMessageType.REGISTER); } } diff --git a/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java b/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java index e2878271..57ae8639 100644 --- a/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/login/LoginCommand.java @@ -19,8 +19,7 @@ public class LoginCommand extends PlayerCommand { @Override public void runCommand(Player player, List arguments) { String password = arguments.get(0); - String totpCode = arguments.size() > 1 ? arguments.get(1) : null; - management.performLogin(player, password, totpCode); + management.performLogin(player, password); } @Override diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/TotpCodeCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/TotpCodeCommand.java new file mode 100644 index 00000000..74d85746 --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/totp/TotpCodeCommand.java @@ -0,0 +1,72 @@ +package fr.xephi.authme.command.executable.totp; + +import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.data.limbo.LimboPlayer; +import fr.xephi.authme.data.limbo.LimboPlayerState; +import fr.xephi.authme.data.limbo.LimboService; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.process.login.AsynchronousLogin; +import fr.xephi.authme.security.totp.TotpService; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.List; + +/** + * TOTP code command for processing the 2FA code during the login process. + */ +public class TotpCodeCommand extends PlayerCommand { + + @Inject + private LimboService limboService; + + @Inject + private PlayerCache playerCache; + + @Inject + private Messages messages; + + @Inject + private TotpService totpService; + + @Inject + private DataSource dataSource; + + @Inject + private AsynchronousLogin asynchronousLogin; + + @Override + protected void runCommand(Player player, List arguments) { + if (playerCache.isAuthenticated(player.getName())) { + messages.send(player, MessageKey.ALREADY_LOGGED_IN_ERROR); + return; + } + + PlayerAuth auth = dataSource.getAuth(player.getName()); + if (auth == null) { + messages.send(player, MessageKey.REGISTER_MESSAGE); + return; + } + + LimboPlayer limbo = limboService.getLimboPlayer(player.getName()); + if (limbo.getState() == LimboPlayerState.TOTP_REQUIRED) { + processCode(player, limbo, auth, arguments.get(0)); + } else { + messages.send(player, MessageKey.LOGIN_MESSAGE); + } + } + + private void processCode(Player player, LimboPlayer limbo, PlayerAuth auth, String inputCode) { + boolean isCodeValid = totpService.verifyCode(auth, inputCode); + if (isCodeValid) { + limbo.setState(LimboPlayerState.FINISHED); + asynchronousLogin.performLogin(player, auth); + } else { + player.sendMessage("Invalid code!"); + } + } +} diff --git a/src/main/java/fr/xephi/authme/data/limbo/LimboMessageType.java b/src/main/java/fr/xephi/authme/data/limbo/LimboMessageType.java new file mode 100644 index 00000000..4d0af3e5 --- /dev/null +++ b/src/main/java/fr/xephi/authme/data/limbo/LimboMessageType.java @@ -0,0 +1,11 @@ +package fr.xephi.authme.data.limbo; + +public enum LimboMessageType { + + REGISTER, + + LOG_IN, + + TOTP_CODE + +} diff --git a/src/main/java/fr/xephi/authme/data/limbo/LimboPlayer.java b/src/main/java/fr/xephi/authme/data/limbo/LimboPlayer.java index b7ea415c..03c3ea13 100644 --- a/src/main/java/fr/xephi/authme/data/limbo/LimboPlayer.java +++ b/src/main/java/fr/xephi/authme/data/limbo/LimboPlayer.java @@ -23,6 +23,7 @@ public class LimboPlayer { 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 groups, boolean fly, float walkSpeed, float flySpeed) { @@ -124,4 +125,12 @@ public class LimboPlayer { setMessageTask(null); setTimeoutTask(null); } + + public LimboPlayerState getState() { + return state; + } + + public void setState(LimboPlayerState state) { + this.state = state; + } } diff --git a/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerState.java b/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerState.java new file mode 100644 index 00000000..24d6cb0f --- /dev/null +++ b/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerState.java @@ -0,0 +1,11 @@ +package fr.xephi.authme.data.limbo; + +public enum LimboPlayerState { + + PASSWORD_REQUIRED, + + TOTP_REQUIRED, + + FINISHED + +} diff --git a/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManager.java b/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManager.java index e5f4f65d..3612e679 100644 --- a/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManager.java +++ b/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManager.java @@ -45,11 +45,11 @@ class LimboPlayerTaskManager { * * @param player the player * @param limbo the associated limbo player of the player - * @param isRegistered whether the player is registered or not (needed to determine the message in the task) + * @param messageType message type */ - void registerMessageTask(Player player, LimboPlayer limbo, boolean isRegistered) { + void registerMessageTask(Player player, LimboPlayer limbo, LimboMessageType messageType) { int interval = settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL); - MessageResult result = getMessageKey(player.getName(), isRegistered); + 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); @@ -89,12 +89,14 @@ class LimboPlayerTaskManager { * Returns the appropriate message key according to the registration status and settings. * * @param name the player's name - * @param isRegistered whether or not the username is registered + * @param messageType the message to show * @return the message key to display to the user */ - private MessageResult getMessageKey(String name, boolean isRegistered) { - if (isRegistered) { + 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); diff --git a/src/main/java/fr/xephi/authme/data/limbo/LimboService.java b/src/main/java/fr/xephi/authme/data/limbo/LimboService.java index 982f359a..fae9d44e 100644 --- a/src/main/java/fr/xephi/authme/data/limbo/LimboService.java +++ b/src/main/java/fr/xephi/authme/data/limbo/LimboService.java @@ -69,7 +69,8 @@ public class LimboService { LimboPlayer limboPlayer = helper.merge(existingLimbo, limboFromDisk); limboPlayer = helper.merge(helper.createLimboPlayer(player, isRegistered, location), limboPlayer); - taskManager.registerMessageTask(player, limboPlayer, isRegistered); + taskManager.registerMessageTask(player, limboPlayer, + isRegistered ? LimboMessageType.LOG_IN : LimboMessageType.REGISTER); taskManager.registerTimeoutTask(player, limboPlayer); helper.revokeLimboStates(player); authGroupHandler.setGroup(player, limboPlayer, @@ -134,7 +135,7 @@ public class LimboService { Optional limboPlayer = getLimboOrLogError(player, "reset tasks"); limboPlayer.ifPresent(limbo -> { taskManager.registerTimeoutTask(player, limbo); - taskManager.registerMessageTask(player, limbo, true); + taskManager.registerMessageTask(player, limbo, LimboMessageType.LOG_IN); }); authGroupHandler.setGroup(player, limboPlayer.orElse(null), AuthGroupType.REGISTERED_UNAUTHENTICATED); } @@ -143,11 +144,11 @@ public class LimboService { * Resets the message task associated with the player's LimboPlayer. * * @param player the player to set a new message task for - * @param isRegistered whether or not the player is registered + * @param messageType the message to show for the limbo player */ - public void resetMessageTask(Player player, boolean isRegistered) { + public void resetMessageTask(Player player, LimboMessageType messageType) { getLimboOrLogError(player, "reset message task") - .ifPresent(limbo -> taskManager.registerMessageTask(player, limbo, isRegistered)); + .ifPresent(limbo -> taskManager.registerMessageTask(player, limbo, messageType)); } /** diff --git a/src/main/java/fr/xephi/authme/message/MessageKey.java b/src/main/java/fr/xephi/authme/message/MessageKey.java index b6367c5b..c9f59934 100644 --- a/src/main/java/fr/xephi/authme/message/MessageKey.java +++ b/src/main/java/fr/xephi/authme/message/MessageKey.java @@ -197,6 +197,9 @@ public enum MessageKey { /** Your secret code is %code. You can scan it from here %url */ TWO_FACTOR_CREATE("misc.two_factor_create", "%code", "%url"), + /** Please submit your two-factor authentication code with /2fa code <code>. */ + TWO_FACTOR_CODE_REQUIRED("two_factor.code_required"), + /** You are not the owner of this account. Please choose another name! */ NOT_OWNER_ERROR("on_join_validation.not_owner_error"), diff --git a/src/main/java/fr/xephi/authme/message/updater/MessageUpdater.java b/src/main/java/fr/xephi/authme/message/updater/MessageUpdater.java index 3b429106..579968ee 100644 --- a/src/main/java/fr/xephi/authme/message/updater/MessageUpdater.java +++ b/src/main/java/fr/xephi/authme/message/updater/MessageUpdater.java @@ -131,6 +131,7 @@ public class MessageUpdater { .put("captcha", new String[]{"Captcha"}) .put("verification", new String[]{"Verification code"}) .put("time", new String[]{"Time units"}) + .put("two_factor", new String[]{"Two-factor authentication"}) .build(); Set addedKeys = new HashSet<>(); diff --git a/src/main/java/fr/xephi/authme/process/Management.java b/src/main/java/fr/xephi/authme/process/Management.java index a5a879e0..5260fed1 100644 --- a/src/main/java/fr/xephi/authme/process/Management.java +++ b/src/main/java/fr/xephi/authme/process/Management.java @@ -49,8 +49,8 @@ public class Management { } - public void performLogin(Player player, String password, String totpCode) { - runTask(() -> asynchronousLogin.login(player, password, totpCode)); + public void performLogin(Player player, String password) { + runTask(() -> asynchronousLogin.login(player, password)); } public void forceLogin(Player player) { diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index 0f8bec3f..22c46dd9 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -6,6 +6,8 @@ import fr.xephi.authme.data.TempbanManager; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.data.captcha.LoginCaptchaManager; +import fr.xephi.authme.data.limbo.LimboMessageType; +import fr.xephi.authme.data.limbo.LimboPlayerState; import fr.xephi.authme.data.limbo.LimboService; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.events.AuthMeAsyncPreLoginEvent; @@ -18,7 +20,6 @@ import fr.xephi.authme.permission.PlayerStatePermission; import fr.xephi.authme.process.AsynchronousProcess; import fr.xephi.authme.process.SyncProcessManager; import fr.xephi.authme.security.PasswordSecurity; -import fr.xephi.authme.security.totp.TotpService; import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.CommonService; import fr.xephi.authme.service.SessionService; @@ -79,9 +80,6 @@ public class AsynchronousLogin implements AsynchronousProcess { @Inject private BungeeSender bungeeSender; - @Inject - private TotpService totpService; - AsynchronousLogin() { } @@ -90,12 +88,17 @@ public class AsynchronousLogin implements AsynchronousProcess { * * @param player the player to log in * @param password the password to log in with - * @param totpCode the totp code (nullable) */ - public void login(Player player, String password, String totpCode) { + public void login(Player player, String password) { PlayerAuth auth = getPlayerAuth(player); - if (auth != null && checkPlayerInfo(player, auth, password, totpCode)) { - performLogin(player, auth); + if (auth != null && checkPlayerInfo(player, auth, password)) { + if (auth.getTotpKey() != null) { + limboService.resetMessageTask(player, LimboMessageType.TOTP_CODE); + limboService.getLimboPlayer(player.getName()).setState(LimboPlayerState.TOTP_REQUIRED); + // TODO #1141: Check if we should check limbo state before processing password + } else { + performLogin(player, auth); + } } } @@ -130,7 +133,7 @@ public class AsynchronousLogin implements AsynchronousProcess { if (auth == null) { service.send(player, MessageKey.UNKNOWN_USER); // Recreate the message task to immediately send the message again as response - limboService.resetMessageTask(player, false); + limboService.resetMessageTask(player, LimboMessageType.REGISTER); return null; } @@ -161,11 +164,10 @@ public class AsynchronousLogin implements AsynchronousProcess { * @param player the player requesting to log in * @param auth the PlayerAuth object of the player * @param password the password supplied by the player - * @param totpCode the input totp code (nullable) * @return true if the password matches and all other conditions are met (e.g. no captcha required), * false otherwise */ - private boolean checkPlayerInfo(Player player, PlayerAuth auth, String password, String totpCode) { + private boolean checkPlayerInfo(Player player, PlayerAuth auth, String password) { final String name = player.getName().toLowerCase(); // If captcha is required send a message to the player and deny to log in @@ -180,17 +182,6 @@ public class AsynchronousLogin implements AsynchronousProcess { loginCaptchaManager.increaseLoginFailureCount(name); tempbanManager.increaseCount(ip, name); - if (auth.getTotpKey() != null) { - if (totpCode == null) { - player.sendMessage( - "You have two-factor authentication enabled. Please provide it: /login <2faCode>"); - return false; - } else if (!totpService.verifyCode(auth, totpCode)) { - player.sendMessage("Invalid code for two-factor authentication. Please try again"); - return false; - } - } - if (passwordSecurity.comparePassword(password, auth.getPassword(), player.getName())) { return true; } else { @@ -235,7 +226,7 @@ public class AsynchronousLogin implements AsynchronousProcess { * @param player the player to log in * @param auth the associated PlayerAuth object */ - private void performLogin(Player player, PlayerAuth auth) { + public void performLogin(Player player, PlayerAuth auth) { if (player.isOnline()) { final boolean isFirstLogin = (auth.getLastLogin() == null); diff --git a/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java b/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java index 492af4e6..e769dd9b 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java @@ -26,7 +26,7 @@ public final class RestrictionSettings implements SettingsHolder { @Comment("Allowed commands for unauthenticated players") public static final Property> ALLOW_COMMANDS = newLowercaseListProperty("settings.restrictions.allowCommands", - "/login", "/register", "/l", "/reg", "/email", "/captcha"); + "/login", "/register", "/l", "/reg", "/email", "/captcha", "/2fa", "/totp"); @Comment({ "Max number of allowed registrations per IP", diff --git a/src/main/resources/messages/messages_en.yml b/src/main/resources/messages/messages_en.yml index e5e20055..9b586bb1 100644 --- a/src/main/resources/messages/messages_en.yml +++ b/src/main/resources/messages/messages_en.yml @@ -128,6 +128,9 @@ verification: code_expired: '&3Your code has expired! Execute another sensitive command to get a new code!' email_needed: '&3To verify your identity you need to link an email address with your account!!' +two_factor: + code_required: 'Please submit your two-factor authentication code with /2fa code ' + # Time units time: second: 'second' diff --git a/src/test/java/fr/xephi/authme/command/executable/login/LoginCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/login/LoginCommandTest.java index 1335ca6f..b5ce86a5 100644 --- a/src/test/java/fr/xephi/authme/command/executable/login/LoginCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/login/LoginCommandTest.java @@ -11,7 +11,6 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import java.util.Arrays; import java.util.Collections; import static org.hamcrest.Matchers.containsString; @@ -57,19 +56,7 @@ public class LoginCommandTest { command.executeCommand(sender, Collections.singletonList("password")); // then - verify(management).performLogin(sender, "password", null); - } - - @Test - public void shouldCallManagementForPasswordAndTotpCode() { - // given - Player sender = mock(Player.class); - - // when - command.executeCommand(sender, Arrays.asList("pwd", "12345")); - - // then - verify(management).performLogin(sender, "pwd", "12345"); + verify(management).performLogin(sender, "password" ); } @Test diff --git a/src/test/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManagerTest.java b/src/test/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManagerTest.java index f1507a29..ff074300 100644 --- a/src/test/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManagerTest.java +++ b/src/test/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManagerTest.java @@ -76,7 +76,7 @@ public class LimboPlayerTaskManagerTest { given(settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL)).willReturn(interval); // when - limboPlayerTaskManager.registerMessageTask(player, limboPlayer, false); + limboPlayerTaskManager.registerMessageTask(player, limboPlayer, LimboMessageType.REGISTER); // then verify(limboPlayer).setMessageTask(any(MessageTask.class)); @@ -94,7 +94,7 @@ public class LimboPlayerTaskManagerTest { given(settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL)).willReturn(0); // when - limboPlayerTaskManager.registerMessageTask(player, limboPlayer, true); + limboPlayerTaskManager.registerMessageTask(player, limboPlayer, LimboMessageType.LOG_IN); // then verifyZeroInteractions(limboPlayer, bukkitService); @@ -113,7 +113,7 @@ public class LimboPlayerTaskManagerTest { given(messages.retrieveSingle(player, MessageKey.REGISTER_MESSAGE)).willReturn("Please register!"); // when - limboPlayerTaskManager.registerMessageTask(player, limboPlayer, false); + limboPlayerTaskManager.registerMessageTask(player, limboPlayer, LimboMessageType.REGISTER); // then assertThat(limboPlayer.getMessageTask(), not(nullValue())); @@ -137,7 +137,7 @@ public class LimboPlayerTaskManagerTest { given(messages.retrieveSingle(player, MessageKey.CAPTCHA_FOR_REGISTRATION_REQUIRED, captcha)).willReturn("Need to use captcha"); // when - limboPlayerTaskManager.registerMessageTask(player, limboPlayer, false); + limboPlayerTaskManager.registerMessageTask(player, limboPlayer, LimboMessageType.REGISTER); // then assertThat(limboPlayer.getMessageTask(), not(nullValue())); diff --git a/src/test/java/fr/xephi/authme/data/limbo/LimboServiceTest.java b/src/test/java/fr/xephi/authme/data/limbo/LimboServiceTest.java index 81feaa8e..8bcfaff7 100644 --- a/src/test/java/fr/xephi/authme/data/limbo/LimboServiceTest.java +++ b/src/test/java/fr/xephi/authme/data/limbo/LimboServiceTest.java @@ -90,7 +90,7 @@ public class LimboServiceTest { limboService.createLimboPlayer(player, true); // then - verify(taskManager).registerMessageTask(eq(player), any(LimboPlayer.class), eq(true)); + verify(taskManager).registerMessageTask(eq(player), any(LimboPlayer.class), eq(LimboMessageType.LOG_IN)); verify(taskManager).registerTimeoutTask(eq(player), any(LimboPlayer.class)); verify(player).setAllowFlight(false); verify(player).setFlySpeed(0.0f); @@ -121,7 +121,7 @@ public class LimboServiceTest { limboService.createLimboPlayer(player, false); // then - verify(taskManager).registerMessageTask(eq(player), any(LimboPlayer.class), eq(false)); + verify(taskManager).registerMessageTask(eq(player), any(LimboPlayer.class), eq(LimboMessageType.REGISTER)); verify(taskManager).registerTimeoutTask(eq(player), any(LimboPlayer.class)); verify(permissionsManager, only()).hasGroupSupport(); verify(player).setAllowFlight(false); @@ -209,7 +209,7 @@ public class LimboServiceTest { // then verify(taskManager).registerTimeoutTask(player, limbo); - verify(taskManager).registerMessageTask(player, limbo, true); + verify(taskManager).registerMessageTask(player, limbo, LimboMessageType.LOG_IN); verify(authGroupHandler).setGroup(player, limbo, AuthGroupType.REGISTERED_UNAUTHENTICATED); } From 584a0bebbfb92c9e3496b9ea7dbe08f2bb9625c2 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Tue, 20 Mar 2018 23:13:48 +0100 Subject: [PATCH 06/63] Minor: Fix failing test after command change --- .../authme/service/HelpTranslationGeneratorIntegrationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/fr/xephi/authme/service/HelpTranslationGeneratorIntegrationTest.java b/src/test/java/fr/xephi/authme/service/HelpTranslationGeneratorIntegrationTest.java index 9159b424..3e094cc2 100644 --- a/src/test/java/fr/xephi/authme/service/HelpTranslationGeneratorIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/service/HelpTranslationGeneratorIntegrationTest.java @@ -126,7 +126,7 @@ public class HelpTranslationGeneratorIntegrationTest { // Check /login checkDescription(configuration.get("commands.login"), "Login command", "/login detailed desc."); - checkArgs(configuration.get("commands.login"), arg("loginArg", "Login password"), arg("2faArg", "TOTP code")); + checkArgs(configuration.get("commands.login"), arg("loginArg", "Login password")); // Check /unregister checkDescription(configuration.get("commands.unregister"), "unreg_desc", "unreg_detail_desc"); From e9ab82db6bea3f4c59095227208f4e426592bc0f Mon Sep 17 00:00:00 2001 From: ljacqu Date: Wed, 21 Mar 2018 23:56:13 +0100 Subject: [PATCH 07/63] #1141 Make 2fa messages translatable, various cleanups (null safety, ...) --- .../executable/totp/AddTotpCommand.java | 13 ++++++----- .../executable/totp/ConfirmTotpCommand.java | 19 +++++++++++---- .../executable/totp/RemoveTotpCommand.java | 11 ++++++--- .../executable/totp/TotpCodeCommand.java | 9 ++++---- .../authme/data/limbo/LimboPlayerState.java | 4 +--- .../fr/xephi/authme/message/MessageKey.java | 23 ++++++++++++++++++- .../security/totp/TotpAuthenticator.java | 7 ++++++ src/main/resources/messages/messages_en.yml | 9 +++++++- .../security/totp/TotpAuthenticatorTest.java | 8 +++++-- .../fr/xephi/authme/message/help_test.yml | 3 --- 10 files changed, 78 insertions(+), 28 deletions(-) diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java index 8fc9aaf1..9238c3b0 100644 --- a/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java @@ -4,10 +4,9 @@ import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; import fr.xephi.authme.security.totp.GenerateTotpService; import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult; -import fr.xephi.authme.service.CommonService; -import org.bukkit.ChatColor; import org.bukkit.entity.Player; import javax.inject.Inject; @@ -25,17 +24,19 @@ public class AddTotpCommand extends PlayerCommand { private DataSource dataSource; @Inject - private CommonService commonService; + private Messages messages; @Override protected void runCommand(Player player, List arguments) { PlayerAuth auth = dataSource.getAuth(player.getName()); - if (auth.getTotpKey() == null) { + if (auth == null) { + messages.send(player, MessageKey.REGISTER_MESSAGE); + } else if (auth.getTotpKey() == null) { TotpGenerationResult createdTotpInfo = generateTotpService.generateTotpKey(player); - commonService.send(player, MessageKey.TWO_FACTOR_CREATE, + messages.send(player, MessageKey.TWO_FACTOR_CREATE, createdTotpInfo.getTotpKey(), createdTotpInfo.getAuthenticatorQrCodeUrl()); } else { - player.sendMessage(ChatColor.RED + "Two-factor authentication is already enabled for your account!"); + messages.send(player, MessageKey.TWO_FACTOR_ALREADY_ENABLED); } } } diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java index 34bdf56d..52d1a980 100644 --- a/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java @@ -1,7 +1,10 @@ package fr.xephi.authme.command.executable.totp; import fr.xephi.authme.command.PlayerCommand; +import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; import fr.xephi.authme.security.totp.GenerateTotpService; import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult; import org.bukkit.entity.Player; @@ -20,21 +23,29 @@ public class ConfirmTotpCommand extends PlayerCommand { @Inject private DataSource dataSource; + @Inject + private Messages messages; + @Override protected void runCommand(Player player, List arguments) { - // TODO #1141: Check if player already has TOTP + PlayerAuth auth = dataSource.getAuth(player.getName()); + if (auth == null) { + messages.send(player, MessageKey.REGISTER_MESSAGE); + } else if (auth.getTotpKey() != null) { + messages.send(player, MessageKey.TWO_FACTOR_ALREADY_ENABLED); + } final TotpGenerationResult totpDetails = generateTotpService.getGeneratedTotpKey(player); if (totpDetails == null) { - player.sendMessage("No TOTP key has been generated for you or it has expired. Please run /totp add"); + messages.send(player, MessageKey.TWO_FACTOR_ENABLE_ERROR_NO_CODE); } else { boolean isCodeValid = generateTotpService.isTotpCodeCorrectForGeneratedTotpKey(player, arguments.get(0)); if (isCodeValid) { generateTotpService.removeGenerateTotpKey(player); dataSource.setTotpKey(player.getName(), totpDetails.getTotpKey()); - player.sendMessage("Successfully enabled two-factor authentication for your account"); + messages.send(player, MessageKey.TWO_FACTOR_ENABLE_SUCCESS); } else { - player.sendMessage("Wrong code or code has expired. Please use /totp add again"); + messages.send(player, MessageKey.TWO_FACTOR_ENABLE_ERROR_WRONG_CODE); } } } diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java index 0a2a371d..20ef4a1b 100644 --- a/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java @@ -3,6 +3,8 @@ package fr.xephi.authme.command.executable.totp; import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; import fr.xephi.authme.security.totp.TotpService; import org.bukkit.entity.Player; @@ -20,17 +22,20 @@ public class RemoveTotpCommand extends PlayerCommand { @Inject private TotpService totpService; + @Inject + private Messages messages; + @Override protected void runCommand(Player player, List arguments) { PlayerAuth auth = dataSource.getAuth(player.getName()); if (auth.getTotpKey() == null) { - player.sendMessage("Two-factor authentication is not enabled for your account!"); + messages.send(player, MessageKey.TWO_FACTOR_NOT_ENABLED_ERROR); } else { if (totpService.verifyCode(auth, arguments.get(0))) { dataSource.removeTotpKey(auth.getNickname()); - player.sendMessage("Successfully removed two-factor authentication from your account"); + messages.send(player, MessageKey.TWO_FACTOR_REMOVED_SUCCESS); } else { - player.sendMessage("Invalid code!"); + messages.send(player, MessageKey.TWO_FACTOR_INVALID_CODE); } } } diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/TotpCodeCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/TotpCodeCommand.java index 74d85746..d8b7a28f 100644 --- a/src/main/java/fr/xephi/authme/command/executable/totp/TotpCodeCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/totp/TotpCodeCommand.java @@ -53,20 +53,19 @@ public class TotpCodeCommand extends PlayerCommand { } LimboPlayer limbo = limboService.getLimboPlayer(player.getName()); - if (limbo.getState() == LimboPlayerState.TOTP_REQUIRED) { - processCode(player, limbo, auth, arguments.get(0)); + if (limbo != null && limbo.getState() == LimboPlayerState.TOTP_REQUIRED) { + processCode(player, auth, arguments.get(0)); } else { messages.send(player, MessageKey.LOGIN_MESSAGE); } } - private void processCode(Player player, LimboPlayer limbo, PlayerAuth auth, String inputCode) { + private void processCode(Player player, PlayerAuth auth, String inputCode) { boolean isCodeValid = totpService.verifyCode(auth, inputCode); if (isCodeValid) { - limbo.setState(LimboPlayerState.FINISHED); asynchronousLogin.performLogin(player, auth); } else { - player.sendMessage("Invalid code!"); + messages.send(player, MessageKey.TWO_FACTOR_INVALID_CODE); } } } diff --git a/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerState.java b/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerState.java index 24d6cb0f..5940ab20 100644 --- a/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerState.java +++ b/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerState.java @@ -4,8 +4,6 @@ public enum LimboPlayerState { PASSWORD_REQUIRED, - TOTP_REQUIRED, - - FINISHED + TOTP_REQUIRED } diff --git a/src/main/java/fr/xephi/authme/message/MessageKey.java b/src/main/java/fr/xephi/authme/message/MessageKey.java index 90206ce7..5d340964 100644 --- a/src/main/java/fr/xephi/authme/message/MessageKey.java +++ b/src/main/java/fr/xephi/authme/message/MessageKey.java @@ -195,11 +195,32 @@ public enum MessageKey { EMAIL_ALREADY_USED_ERROR("email.already_used"), /** Your secret code is %code. You can scan it from here %url */ - TWO_FACTOR_CREATE("misc.two_factor_create", "%code", "%url"), + TWO_FACTOR_CREATE("two_factor.code_created", "%code", "%url"), /** Please submit your two-factor authentication code with /2fa code <code>. */ TWO_FACTOR_CODE_REQUIRED("two_factor.code_required"), + /** Two-factor authentication is already enabled for your account! */ + TWO_FACTOR_ALREADY_ENABLED("two_factor.already_enabled"), + + /** No 2fa key has been generated for you or it has expired. Please run /2fa add */ + TWO_FACTOR_ENABLE_ERROR_NO_CODE("two_factor.enable_error_no_code"), + + /** Successfully enabled two-factor authentication for your account */ + TWO_FACTOR_ENABLE_SUCCESS("two_factor.enable_success"), + + /** Wrong code or code has expired. Please run /2fa add */ + TWO_FACTOR_ENABLE_ERROR_WRONG_CODE("two_factor.enable_error_wrong_code"), + + /** Two-factor authentication is not enabled for your account. Run /2fa add */ + TWO_FACTOR_NOT_ENABLED_ERROR("two_factor.not_enabled_error"), + + /** Successfully removed two-factor auth from your account */ + TWO_FACTOR_REMOVED_SUCCESS("two_factor.removed_success"), + + /** Invalid code! */ + TWO_FACTOR_INVALID_CODE("two_factor.invalid_code"), + /** You are not the owner of this account. Please choose another name! */ NOT_OWNER_ERROR("on_join_validation.not_owner_error"), diff --git a/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java b/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java index ed31da3a..d546df07 100644 --- a/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java +++ b/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java @@ -30,6 +30,13 @@ public class TotpAuthenticator { this.bukkitService = bukkitService; } + /** + * Returns whether the given input code matches for the provided TOTP key. + * + * @param totpKey the key to check with + * @param inputCode the input code to verify + * @return true if code is valid, false otherwise + */ public boolean checkCode(String totpKey, String inputCode) { try { Integer totpCode = Integer.valueOf(inputCode); diff --git a/src/main/resources/messages/messages_en.yml b/src/main/resources/messages/messages_en.yml index 1b5e0928..aca51fa7 100644 --- a/src/main/resources/messages/messages_en.yml +++ b/src/main/resources/messages/messages_en.yml @@ -56,7 +56,6 @@ unregister: misc: accounts_owned_self: 'You own %count accounts:' accounts_owned_other: 'The player %name has %count accounts:' - two_factor_create: '&2Your secret code is %code. You can scan it from here %url' account_not_activated: '&cYour account isn''t activated yet, please check your emails!' password_changed: '&2Password changed successfully!' logout: '&2Logged out successfully!' @@ -130,7 +129,15 @@ verification: email_needed: '&3To verify your identity you need to link an email address with your account!!' two_factor: + code_created: '&2Your secret code is %code. You can scan it from here %url' code_required: 'Please submit your two-factor authentication code with /2fa code ' + already_enabled: 'Two-factor authentication is already enabled for your account!' + enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + enable_success: 'Successfully enabled two-factor authentication for your account' + enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + removed_success: 'Successfully removed two-factor auth from your account' + invalid_code: 'Invalid code!' # Time units time: diff --git a/src/test/java/fr/xephi/authme/security/totp/TotpAuthenticatorTest.java b/src/test/java/fr/xephi/authme/security/totp/TotpAuthenticatorTest.java index eb2746fa..b675ef8a 100644 --- a/src/test/java/fr/xephi/authme/security/totp/TotpAuthenticatorTest.java +++ b/src/test/java/fr/xephi/authme/security/totp/TotpAuthenticatorTest.java @@ -4,9 +4,9 @@ import com.warrenstrange.googleauth.IGoogleAuthenticator; import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult; import fr.xephi.authme.service.BukkitService; import org.bukkit.entity.Player; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @@ -26,7 +26,6 @@ import static org.mockito.Mockito.verifyZeroInteractions; @RunWith(MockitoJUnitRunner.class) public class TotpAuthenticatorTest { - @InjectMocks private TotpAuthenticator totpAuthenticator; @Mock @@ -35,6 +34,11 @@ public class TotpAuthenticatorTest { @Mock private IGoogleAuthenticator googleAuthenticator; + @Before + public void initializeTotpAuthenticator() { + totpAuthenticator = new TotpAuthenticator(googleAuthenticator, bukkitService); + } + @Test public void shouldGenerateTotpKey() { // given diff --git a/src/test/resources/fr/xephi/authme/message/help_test.yml b/src/test/resources/fr/xephi/authme/message/help_test.yml index 9647ecd7..d6970daa 100644 --- a/src/test/resources/fr/xephi/authme/message/help_test.yml +++ b/src/test/resources/fr/xephi/authme/message/help_test.yml @@ -65,9 +65,6 @@ commands: arg1: label: loginArg nonExistent: does not exist - arg2: - label: 2faArg - rhetorical: false someProp: label: '''someProp'' does not exist' description: idk From 5a58f2c44f918b1378a6de4b5862f496b1a68219 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 24 Mar 2018 12:24:25 +0100 Subject: [PATCH 08/63] #1539 Integrate data source columns library - Create wrapper around SqlColumnsHandler for AuthMe-specific behavior - Integrate columns handler into first SQLite and MySQL method implementations --- pom.xml | 8 + .../authme/datasource/AuthMeColumns.java | 125 ++++++++++++++++ .../authme/datasource/ColumnContext.java | 20 +++ .../fr/xephi/authme/datasource/MySQL.java | 98 +++--------- .../fr/xephi/authme/datasource/SQLite.java | 89 +++-------- .../columnshandler/AuthMeColumnsHandler.java | 141 ++++++++++++++++++ .../xephi/authme/ClassesConsistencyTest.java | 3 +- .../AbstractDataSourceIntegrationTest.java | 17 ++- 8 files changed, 346 insertions(+), 155 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/datasource/AuthMeColumns.java create mode 100644 src/main/java/fr/xephi/authme/datasource/ColumnContext.java create mode 100644 src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java diff --git a/pom.xml b/pom.xml index 0ee1ecf6..f109d741 100644 --- a/pom.xml +++ b/pom.xml @@ -788,6 +788,14 @@ + + ch.jalu + datasourcecolumns + 0.1-SNAPSHOT + compile + true + + diff --git a/src/main/java/fr/xephi/authme/datasource/AuthMeColumns.java b/src/main/java/fr/xephi/authme/datasource/AuthMeColumns.java new file mode 100644 index 00000000..657084ae --- /dev/null +++ b/src/main/java/fr/xephi/authme/datasource/AuthMeColumns.java @@ -0,0 +1,125 @@ +package fr.xephi.authme.datasource; + +import ch.jalu.configme.properties.Property; +import ch.jalu.datasourcecolumns.ColumnType; +import ch.jalu.datasourcecolumns.DependentColumn; +import ch.jalu.datasourcecolumns.StandardTypes; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.settings.properties.DatabaseSettings; + +import java.util.function.Function; + +public final class AuthMeColumns implements DependentColumn { + + public static final AuthMeColumns NAME = createString( + DatabaseSettings.MYSQL_COL_NAME, PlayerAuth::getNickname); + + public static final AuthMeColumns NICK_NAME = createString( + DatabaseSettings.MYSQL_COL_REALNAME, PlayerAuth::getRealName); + + public static final AuthMeColumns PASSWORD = createString( + DatabaseSettings.MYSQL_COL_PASSWORD, auth -> auth.getPassword().getHash()); + + public static final AuthMeColumns SALT = new AuthMeColumns<>( + StandardTypes.STRING, DatabaseSettings.MYSQL_COL_SALT, auth -> auth.getPassword().getSalt(), true); + + public static final AuthMeColumns EMAIL = createString( + DatabaseSettings.MYSQL_COL_EMAIL, PlayerAuth::getEmail); + + public static final AuthMeColumns LAST_IP = createString( + DatabaseSettings.MYSQL_COL_LAST_IP, PlayerAuth::getLastIp); + + public static final AuthMeColumns LOCATION_X = createDouble( + DatabaseSettings.MYSQL_COL_LASTLOC_X, PlayerAuth::getQuitLocX); + + public static final AuthMeColumns LOCATION_Y = createDouble( + DatabaseSettings.MYSQL_COL_LASTLOC_Y, PlayerAuth::getQuitLocY); + + public static final AuthMeColumns LOCATION_Z = createDouble( + DatabaseSettings.MYSQL_COL_LASTLOC_Z, PlayerAuth::getQuitLocZ); + + public static final AuthMeColumns LOCATION_WORLD = createString( + DatabaseSettings.MYSQL_COL_LASTLOC_WORLD, PlayerAuth::getWorld); + + public static final AuthMeColumns LOCATION_YAW = createFloat( + DatabaseSettings.MYSQL_COL_LASTLOC_YAW, PlayerAuth::getYaw); + + public static final AuthMeColumns LOCATION_PITCH = createFloat( + DatabaseSettings.MYSQL_COL_LASTLOC_PITCH, PlayerAuth::getPitch); + + + private final ColumnType columnType; + private final Property nameProperty; + private final Function playerAuthGetter; + private final boolean isOptional; + + private AuthMeColumns(ColumnType type, Property nameProperty, Function playerAuthGetter, + boolean isOptional) { + this.columnType = type; + this.nameProperty = nameProperty; + this.playerAuthGetter = playerAuthGetter; + this.isOptional = isOptional; + } + + private static AuthMeColumns createString(Property nameProperty, + Function getter) { + return new AuthMeColumns<>(StandardTypes.STRING, nameProperty, getter, false); + } + + private static AuthMeColumns createDouble(Property nameProperty, + Function getter) { + return new AuthMeColumns<>(new DoubleType(), nameProperty, getter, false); + } + + private static AuthMeColumns createFloat(Property nameProperty, + Function getter) { + return new AuthMeColumns<>(new FloatType(), nameProperty, getter, false); + } + + + public Property getNameProperty() { + return nameProperty; + } + + @Override + public T getValueFromDependent(PlayerAuth playerAuth) { + return playerAuthGetter.apply(playerAuth); + } + + @Override + public String resolveName(ColumnContext columnContext) { + return columnContext.getName(this); + } + + @Override + public ColumnType getType() { + return columnType; + } + + @Override + public boolean isColumnUsed(ColumnContext columnContext) { + return !isOptional || !resolveName(columnContext).isEmpty(); + } + + @Override + public boolean useDefaultForNullValue(ColumnContext columnContext) { + return false; + } + + // TODO: Move this to the project... + private static final class DoubleType implements ColumnType { + + @Override + public Class getClazz() { + return Double.class; + } + } + + private static final class FloatType implements ColumnType { + + @Override + public Class getClazz() { + return Float.class; + } + } +} diff --git a/src/main/java/fr/xephi/authme/datasource/ColumnContext.java b/src/main/java/fr/xephi/authme/datasource/ColumnContext.java new file mode 100644 index 00000000..cbc743fa --- /dev/null +++ b/src/main/java/fr/xephi/authme/datasource/ColumnContext.java @@ -0,0 +1,20 @@ +package fr.xephi.authme.datasource; + +import fr.xephi.authme.settings.Settings; + +import java.util.HashMap; +import java.util.Map; + +public class ColumnContext { + + private final Settings settings; + private final Map, String> columnNames = new HashMap<>(); + + public ColumnContext(Settings settings) { + this.settings = settings; + } + + public String getName(AuthMeColumns column) { + return columnNames.computeIfAbsent(column, k -> settings.getProperty(k.getNameProperty())); + } +} diff --git a/src/main/java/fr/xephi/authme/datasource/MySQL.java b/src/main/java/fr/xephi/authme/datasource/MySQL.java index ce46baad..f098d468 100644 --- a/src/main/java/fr/xephi/authme/datasource/MySQL.java +++ b/src/main/java/fr/xephi/authme/datasource/MySQL.java @@ -1,10 +1,12 @@ package fr.xephi.authme.datasource; +import ch.jalu.datasourcecolumns.data.DataSourceValues; import com.google.common.annotations.VisibleForTesting; import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.pool.HikariPool.PoolInitializationException; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.columnshandler.AuthMeColumnsHandler; import fr.xephi.authme.datasource.mysqlextensions.MySqlExtension; import fr.xephi.authme.datasource.mysqlextensions.MySqlExtensionsFactory; import fr.xephi.authme.security.crypts.HashedPassword; @@ -25,6 +27,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import static ch.jalu.datasourcecolumns.data.UpdateValues.with; import static fr.xephi.authme.datasource.SqlDataSourceUtils.getNullableLong; import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException; @@ -45,6 +48,7 @@ public class MySQL implements DataSource { private int maxLifetime; private List columnOthers; private Columns col; + private AuthMeColumnsHandler columnsHandler; private MySqlExtension sqlExtension; private HikariDataSource ds; @@ -99,6 +103,8 @@ public class MySQL implements DataSource { this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); this.columnOthers = settings.getProperty(HooksSettings.MYSQL_OTHER_USERNAME_COLS); this.col = new Columns(settings); + this.columnsHandler = + AuthMeColumnsHandler.createForMySql(sql -> getConnection().prepareStatement(sql), settings); this.sqlExtension = extensionsFactory.buildExtension(col); this.poolSize = settings.getProperty(DatabaseSettings.MYSQL_POOL_SIZE); this.maxLifetime = settings.getProperty(DatabaseSettings.MYSQL_CONNECTION_MAX_LIFETIME); @@ -278,20 +284,13 @@ public class MySQL implements DataSource { @Override public HashedPassword getPassword(String user) { - boolean useSalt = !col.SALT.isEmpty(); - String sql = "SELECT " + col.PASSWORD - + (useSalt ? ", " + col.SALT : "") - + " FROM " + tableName + " WHERE " + col.NAME + "=?;"; - try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, user.toLowerCase()); - try (ResultSet rs = pst.executeQuery()) { - if (rs.next()) { - return new HashedPassword(rs.getString(col.PASSWORD), - useSalt ? rs.getString(col.SALT) : null); - } + try { + DataSourceValues passwordResult = columnsHandler.retrieve(user, AuthMeColumns.PASSWORD, AuthMeColumns.SALT); + if (passwordResult.rowExists()) { + return new HashedPassword(passwordResult.get(AuthMeColumns.PASSWORD), passwordResult.get(AuthMeColumns.SALT)); } - } catch (SQLException ex) { - logSqlException(ex); + } catch (SQLException e) { + logSqlException(e); } return null; } @@ -371,33 +370,9 @@ public class MySQL implements DataSource { @Override public boolean updatePassword(String user, HashedPassword password) { - user = user.toLowerCase(); - try (Connection con = getConnection()) { - boolean useSalt = !col.SALT.isEmpty(); - if (useSalt) { - String sql = String.format("UPDATE %s SET %s = ?, %s = ? WHERE %s = ?;", - tableName, col.PASSWORD, col.SALT, col.NAME); - try (PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, password.getHash()); - pst.setString(2, password.getSalt()); - pst.setString(3, user); - pst.executeUpdate(); - } - } else { - String sql = String.format("UPDATE %s SET %s = ? WHERE %s = ?;", - tableName, col.PASSWORD, col.NAME); - try (PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, password.getHash()); - pst.setString(2, user); - pst.executeUpdate(); - } - } - sqlExtension.changePassword(user, password, con); - return true; - } catch (SQLException ex) { - logSqlException(ex); - } - return false; + return columnsHandler.update(user, + with(AuthMeColumns.PASSWORD, password.getHash()) + .and(AuthMeColumns.SALT, password.getSalt()).build()); } @Override @@ -456,38 +431,14 @@ public class MySQL implements DataSource { @Override public boolean updateQuitLoc(PlayerAuth auth) { - String sql = "UPDATE " + tableName - + " SET " + col.LASTLOC_X + " =?, " + col.LASTLOC_Y + "=?, " + col.LASTLOC_Z + "=?, " - + col.LASTLOC_WORLD + "=?, " + col.LASTLOC_YAW + "=?, " + col.LASTLOC_PITCH + "=?" - + " WHERE " + col.NAME + "=?;"; - try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { - pst.setDouble(1, auth.getQuitLocX()); - pst.setDouble(2, auth.getQuitLocY()); - pst.setDouble(3, auth.getQuitLocZ()); - pst.setString(4, auth.getWorld()); - pst.setFloat(5, auth.getYaw()); - pst.setFloat(6, auth.getPitch()); - pst.setString(7, auth.getNickname()); - pst.executeUpdate(); - return true; - } catch (SQLException ex) { - logSqlException(ex); - } - return false; + return columnsHandler.update(auth, + AuthMeColumns.LOCATION_X, AuthMeColumns.LOCATION_Y, AuthMeColumns.LOCATION_Z, + AuthMeColumns.LOCATION_WORLD, AuthMeColumns.LOCATION_YAW, AuthMeColumns.LOCATION_PITCH); } @Override public boolean updateEmail(PlayerAuth auth) { - String sql = "UPDATE " + tableName + " SET " + col.EMAIL + " =? WHERE " + col.NAME + "=?;"; - try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, auth.getEmail()); - pst.setString(2, auth.getNickname()); - pst.executeUpdate(); - return true; - } catch (SQLException ex) { - logSqlException(ex); - } - return false; + return columnsHandler.update(auth, AuthMeColumns.EMAIL); } @Override @@ -654,16 +605,7 @@ public class MySQL implements DataSource { @Override public boolean updateRealName(String user, String realName) { - String sql = "UPDATE " + tableName + " SET " + col.REAL_NAME + "=? WHERE " + col.NAME + "=?;"; - try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, realName); - pst.setString(2, user); - pst.executeUpdate(); - return true; - } catch (SQLException ex) { - logSqlException(ex); - } - return false; + return columnsHandler.update(user, AuthMeColumns.NICK_NAME, realName); } @Override diff --git a/src/main/java/fr/xephi/authme/datasource/SQLite.java b/src/main/java/fr/xephi/authme/datasource/SQLite.java index ada94afb..57d12a6b 100644 --- a/src/main/java/fr/xephi/authme/datasource/SQLite.java +++ b/src/main/java/fr/xephi/authme/datasource/SQLite.java @@ -1,8 +1,10 @@ package fr.xephi.authme.datasource; +import ch.jalu.datasourcecolumns.data.DataSourceValues; import com.google.common.annotations.VisibleForTesting; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.columnshandler.AuthMeColumnsHandler; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.DatabaseSettings; @@ -22,6 +24,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import static ch.jalu.datasourcecolumns.data.UpdateValues.with; import static fr.xephi.authme.datasource.SqlDataSourceUtils.getNullableLong; import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException; @@ -37,6 +40,7 @@ public class SQLite implements DataSource { private final String tableName; private final Columns col; private Connection con; + private AuthMeColumnsHandler columnsHandler; /** * Constructor for SQLite. @@ -71,6 +75,7 @@ public class SQLite implements DataSource { this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); this.col = new Columns(settings); this.con = connection; + this.columnsHandler = AuthMeColumnsHandler.createForSqlite(con, settings); } /** @@ -220,20 +225,13 @@ public class SQLite implements DataSource { @Override public HashedPassword getPassword(String user) { - boolean useSalt = !col.SALT.isEmpty(); - String sql = "SELECT " + col.PASSWORD - + (useSalt ? ", " + col.SALT : "") - + " FROM " + tableName + " WHERE " + col.NAME + "=?"; - try (PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, user); - try (ResultSet rs = pst.executeQuery()) { - if (rs.next()) { - return new HashedPassword(rs.getString(col.PASSWORD), - useSalt ? rs.getString(col.SALT) : null); - } + try { + DataSourceValues values = columnsHandler.retrieve(user, AuthMeColumns.PASSWORD, AuthMeColumns.SALT); + if (values.rowExists()) { + return new HashedPassword(values.get(AuthMeColumns.PASSWORD), values.get(AuthMeColumns.SALT)); } - } catch (SQLException ex) { - logSqlException(ex); + } catch (SQLException e) { + logSqlException(e); } return null; } @@ -305,25 +303,9 @@ public class SQLite implements DataSource { @Override public boolean updatePassword(String user, HashedPassword password) { - user = user.toLowerCase(); - boolean useSalt = !col.SALT.isEmpty(); - String sql = "UPDATE " + tableName + " SET " + col.PASSWORD + " = ?" - + (useSalt ? ", " + col.SALT + " = ?" : "") - + " WHERE " + col.NAME + " = ?"; - try (PreparedStatement pst = con.prepareStatement(sql)){ - pst.setString(1, password.getHash()); - if (useSalt) { - pst.setString(2, password.getSalt()); - pst.setString(3, user); - } else { - pst.setString(2, user); - } - pst.executeUpdate(); - return true; - } catch (SQLException ex) { - logSqlException(ex); - } - return false; + return columnsHandler.update(user, + with(AuthMeColumns.PASSWORD, password.getHash()) + .and(AuthMeColumns.SALT, password.getSalt()).build()); } @Override @@ -392,38 +374,14 @@ public class SQLite implements DataSource { @Override public boolean updateQuitLoc(PlayerAuth auth) { - String sql = "UPDATE " + tableName + " SET " - + col.LASTLOC_X + "=?, " + col.LASTLOC_Y + "=?, " + col.LASTLOC_Z + "=?, " - + col.LASTLOC_WORLD + "=?, " + col.LASTLOC_YAW + "=?, " + col.LASTLOC_PITCH + "=? " - + "WHERE " + col.NAME + "=?;"; - try (PreparedStatement pst = con.prepareStatement(sql)) { - pst.setDouble(1, auth.getQuitLocX()); - pst.setDouble(2, auth.getQuitLocY()); - pst.setDouble(3, auth.getQuitLocZ()); - pst.setString(4, auth.getWorld()); - pst.setFloat(5, auth.getYaw()); - pst.setFloat(6, auth.getPitch()); - pst.setString(7, auth.getNickname()); - pst.executeUpdate(); - return true; - } catch (SQLException ex) { - logSqlException(ex); - } - return false; + return columnsHandler.update(auth, + AuthMeColumns.LOCATION_X, AuthMeColumns.LOCATION_Y, AuthMeColumns.LOCATION_Z, + AuthMeColumns.LOCATION_WORLD, AuthMeColumns.LOCATION_YAW, AuthMeColumns.LOCATION_PITCH); } @Override public boolean updateEmail(PlayerAuth auth) { - String sql = "UPDATE " + tableName + " SET " + col.EMAIL + "=? WHERE " + col.NAME + "=?;"; - try (PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, auth.getEmail()); - pst.setString(2, auth.getNickname()); - pst.executeUpdate(); - return true; - } catch (SQLException ex) { - logSqlException(ex); - } - return false; + return columnsHandler.update(auth, AuthMeColumns.EMAIL); } @Override @@ -583,16 +541,7 @@ public class SQLite implements DataSource { @Override public boolean updateRealName(String user, String realName) { - String sql = "UPDATE " + tableName + " SET " + col.REAL_NAME + "=? WHERE " + col.NAME + "=?;"; - try (PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, realName); - pst.setString(2, user); - pst.executeUpdate(); - return true; - } catch (SQLException ex) { - logSqlException(ex); - } - return false; + return columnsHandler.update(user, AuthMeColumns.NICK_NAME, realName); } @Override diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java new file mode 100644 index 00000000..b4c02dc9 --- /dev/null +++ b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java @@ -0,0 +1,141 @@ +package fr.xephi.authme.datasource.columnshandler; + +import ch.jalu.datasourcecolumns.data.DataSourceValue; +import ch.jalu.datasourcecolumns.data.DataSourceValues; +import ch.jalu.datasourcecolumns.data.UpdateValues; +import ch.jalu.datasourcecolumns.sqlimplementation.PredicateSqlGenerator; +import ch.jalu.datasourcecolumns.sqlimplementation.PreparedStatementGenerator; +import ch.jalu.datasourcecolumns.sqlimplementation.ResultSetValueRetriever; +import ch.jalu.datasourcecolumns.sqlimplementation.SqlColumnsHandler; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.AuthMeColumns; +import fr.xephi.authme.datasource.ColumnContext; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.DatabaseSettings; + +import java.sql.Connection; +import java.sql.SQLException; + +import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException; + +/** + * Wrapper of {@link SqlColumnsHandler} for the AuthMe data table. + * Wraps exceptions and provides better support for operations based on a {@link PlayerAuth} object. + */ +public final class AuthMeColumnsHandler { + + private final SqlColumnsHandler internalHandler; + + private AuthMeColumnsHandler(SqlColumnsHandler internalHandler) { + this.internalHandler = internalHandler; + } + + /** + * Creates a column handler for SQLite. + * + * @param connection the connection to the database + * @param settings plugin settings + * @return created column handler + */ + public static AuthMeColumnsHandler createForSqlite(Connection connection, Settings settings) { + ColumnContext columnContext = new ColumnContext(settings); + String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); + String nameColumn = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME); + + SqlColumnsHandler sqlColHandler = + new SqlColumnsHandler<>(connection, columnContext, tableName, nameColumn); + return new AuthMeColumnsHandler(sqlColHandler); + } + + /** + * Creates a column handler for MySQL. + * + * @param preparedStatementGenerator supplier of SQL prepared statements with a connection to the database + * @param settings plugin settings + * @return created column handler + */ + public static AuthMeColumnsHandler createForMySql(PreparedStatementGenerator preparedStatementGenerator, + Settings settings) { + ColumnContext columnContext = new ColumnContext(settings); + String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); + String nameColumn = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME); + + SqlColumnsHandler sqlColHandler = new SqlColumnsHandler<>(preparedStatementGenerator, + columnContext, tableName, nameColumn, new ResultSetValueRetriever<>(columnContext), + new PredicateSqlGenerator<>(columnContext)); + return new AuthMeColumnsHandler(sqlColHandler); + } + + /** + * Changes a column from a specific row to the given value. + * + * @param name name of the account to modify + * @param column the column to modify + * @param value the value to set the column to + * @param the column type + * @return true upon success, false otherwise + */ + public boolean update(String name, AuthMeColumns column, T value) { + try { + return internalHandler.update(name, column, value); + } catch (SQLException e) { + logSqlException(e); + return false; + } + } + + /** + * Updates a row to have the values as retrieved from the PlayerAuth object. + * + * @param auth the player auth object to modify and to get values from + * @param columns the columns to update in the row + * @return true upon success, false otherwise + */ + public boolean update(PlayerAuth auth, AuthMeColumns... columns) { + try { + return internalHandler.update(auth.getNickname(), auth, columns); + } catch (SQLException e) { + logSqlException(e); + return false; + } + } + + /** + * Updates a row to have the given values. + * + * @param name the name of the account to modify + * @param updateValues the values to set on the row + * @return true upon success, false otherwise + */ + public boolean update(String name, UpdateValues updateValues) { + try { + return internalHandler.update(name.toLowerCase(), updateValues); + } catch (SQLException e) { + logSqlException(e); + return false; + } + } + + /** + * Retrieves the given column from a given row. + * + * @param name the account name to look up + * @param column the column whose value should be retrieved + * @param the column type + * @return the result of the lookup + */ + public DataSourceValue retrieve(String name, AuthMeColumns column) throws SQLException { + return internalHandler.retrieve(name.toLowerCase(), column); + } + + /** + * Retrieves multiple values from a given row. + * + * @param name the account name to look up + * @param columns the columns to retrieve + * @return map-like object with the requested values + */ + public DataSourceValues retrieve(String name, AuthMeColumns... columns) throws SQLException { + return internalHandler.retrieve(name.toLowerCase(), columns); + } +} diff --git a/src/test/java/fr/xephi/authme/ClassesConsistencyTest.java b/src/test/java/fr/xephi/authme/ClassesConsistencyTest.java index 267f7d7c..4005dc79 100644 --- a/src/test/java/fr/xephi/authme/ClassesConsistencyTest.java +++ b/src/test/java/fr/xephi/authme/ClassesConsistencyTest.java @@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import fr.xephi.authme.data.captcha.CaptchaCodeStorage; +import fr.xephi.authme.datasource.AuthMeColumns; import fr.xephi.authme.datasource.Columns; import fr.xephi.authme.datasource.mysqlextensions.MySqlExtension; import fr.xephi.authme.initialization.HasCleanup; @@ -52,7 +53,7 @@ public class ClassesConsistencyTest { int.class, long.class, float.class, String.class, File.class, Enum.class, collectionsUnmodifiableList(), Charset.class, /* AuthMe */ - Property.class, RegistrationMethod.class, + Property.class, RegistrationMethod.class, AuthMeColumns.class, /* Guava */ ImmutableMap.class, ImmutableList.class); diff --git a/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java b/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java index 530ab56b..70df2d48 100644 --- a/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java @@ -61,7 +61,7 @@ public abstract class AbstractDataSourceIntegrationTest { // when HashedPassword bobbyPassword = dataSource.getPassword("bobby"); HashedPassword invalidPassword = dataSource.getPassword("doesNotExist"); - HashedPassword userPassword = dataSource.getPassword("user"); + HashedPassword userPassword = dataSource.getPassword("User"); // then assertThat(bobbyPassword, equalToHash("$SHA$11aa0706173d7272$dbba966")); @@ -160,7 +160,8 @@ public abstract class AbstractDataSourceIntegrationTest { boolean response2 = dataSource.updatePassword("non-existent-name", new HashedPassword("sd")); // then - assertThat(response1 && response2, equalTo(true)); + assertThat(response1, equalTo(true)); + assertThat(response2, equalTo(false)); // no record modified assertThat(dataSource.getPassword("user"), equalToHash(newHash)); } @@ -175,7 +176,8 @@ public abstract class AbstractDataSourceIntegrationTest { boolean response2 = dataSource.updatePassword("non-existent-name", new HashedPassword("asdfasdf", "a1f34ec")); // then - assertThat(response1 && response2, equalTo(true)); + assertThat(response1, equalTo(true)); + assertThat(response2, equalTo(false)); // no record modified assertThat(dataSource.getPassword("user"), equalToHash("new_hash")); } @@ -191,7 +193,8 @@ public abstract class AbstractDataSourceIntegrationTest { boolean response2 = dataSource.updatePassword(invalidAuth); // then - assertThat(response1 && response2, equalTo(true)); + assertThat(response1, equalTo(true)); + assertThat(response2, equalTo(false)); // no record modified assertThat(dataSource.getPassword("bobby"), equalToHash("tt", "cc")); } @@ -273,7 +276,8 @@ public abstract class AbstractDataSourceIntegrationTest { boolean response2 = dataSource.updateEmail(invalidAuth); // then - assertThat(response1 && response2, equalTo(true)); + assertThat(response1, equalTo(true)); + assertThat(response2, equalTo(false)); // no record modified assertThat(dataSource.getAllAuths(), hasItem(hasAuthBasicData("user", "user", email, "34.56.78.90"))); } @@ -328,7 +332,8 @@ public abstract class AbstractDataSourceIntegrationTest { boolean response2 = dataSource.updateRealName("notExists", "NOTEXISTS"); // then - assertThat(response1 && response2, equalTo(true)); + assertThat(response1, equalTo(true)); + assertThat(response2, equalTo(false)); // no record modified assertThat(dataSource.getAuth("bobby"), hasAuthBasicData("bobby", "BOBBY", null, "123.45.67.89")); } From 881ef6a640c86f27ac63da65ffc897124ef2f7ce Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 24 Mar 2018 21:16:43 +0100 Subject: [PATCH 09/63] #1539 DataSource columns: close MySQL connections, add missing columns, use newly built-in types, improve column initialization --- .../authme/datasource/ColumnContext.java | 20 ----- .../fr/xephi/authme/datasource/MySQL.java | 35 ++------- .../fr/xephi/authme/datasource/SQLite.java | 45 +---------- .../{ => columnshandler}/AuthMeColumns.java | 72 ++++++++---------- .../columnshandler/AuthMeColumnsFactory.java | 76 +++++++++++++++++++ .../columnshandler/AuthMeColumnsHandler.java | 34 ++++++--- .../columnshandler/ColumnContext.java | 35 +++++++++ .../columnshandler/ConnectionSupplier.java | 17 +++++ .../MySqlPreparedStatementGenerator.java | 44 +++++++++++ .../xephi/authme/ClassesConsistencyTest.java | 2 +- 10 files changed, 238 insertions(+), 142 deletions(-) delete mode 100644 src/main/java/fr/xephi/authme/datasource/ColumnContext.java rename src/main/java/fr/xephi/authme/datasource/{ => columnshandler}/AuthMeColumns.java (61%) create mode 100644 src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsFactory.java create mode 100644 src/main/java/fr/xephi/authme/datasource/columnshandler/ColumnContext.java create mode 100644 src/main/java/fr/xephi/authme/datasource/columnshandler/ConnectionSupplier.java create mode 100644 src/main/java/fr/xephi/authme/datasource/columnshandler/MySqlPreparedStatementGenerator.java diff --git a/src/main/java/fr/xephi/authme/datasource/ColumnContext.java b/src/main/java/fr/xephi/authme/datasource/ColumnContext.java deleted file mode 100644 index cbc743fa..00000000 --- a/src/main/java/fr/xephi/authme/datasource/ColumnContext.java +++ /dev/null @@ -1,20 +0,0 @@ -package fr.xephi.authme.datasource; - -import fr.xephi.authme.settings.Settings; - -import java.util.HashMap; -import java.util.Map; - -public class ColumnContext { - - private final Settings settings; - private final Map, String> columnNames = new HashMap<>(); - - public ColumnContext(Settings settings) { - this.settings = settings; - } - - public String getName(AuthMeColumns column) { - return columnNames.computeIfAbsent(column, k -> settings.getProperty(k.getNameProperty())); - } -} diff --git a/src/main/java/fr/xephi/authme/datasource/MySQL.java b/src/main/java/fr/xephi/authme/datasource/MySQL.java index f098d468..8e501ae0 100644 --- a/src/main/java/fr/xephi/authme/datasource/MySQL.java +++ b/src/main/java/fr/xephi/authme/datasource/MySQL.java @@ -6,6 +6,7 @@ import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.pool.HikariPool.PoolInitializationException; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.columnshandler.AuthMeColumns; import fr.xephi.authme.datasource.columnshandler.AuthMeColumnsHandler; import fr.xephi.authme.datasource.mysqlextensions.MySqlExtension; import fr.xephi.authme.datasource.mysqlextensions.MySqlExtensionsFactory; @@ -13,7 +14,6 @@ import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.DatabaseSettings; import fr.xephi.authme.settings.properties.HooksSettings; -import fr.xephi.authme.util.StringUtils; import java.sql.Connection; import java.sql.DatabaseMetaData; @@ -103,8 +103,7 @@ public class MySQL implements DataSource { this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); this.columnOthers = settings.getProperty(HooksSettings.MYSQL_OTHER_USERNAME_COLS); this.col = new Columns(settings); - this.columnsHandler = - AuthMeColumnsHandler.createForMySql(sql -> getConnection().prepareStatement(sql), settings); + this.columnsHandler = AuthMeColumnsHandler.createForMySql(this::getConnection, settings); this.sqlExtension = extensionsFactory.buildExtension(col); this.poolSize = settings.getProperty(DatabaseSettings.MYSQL_POOL_SIZE); this.maxLifetime = settings.getProperty(DatabaseSettings.MYSQL_CONNECTION_MAX_LIFETIME); @@ -317,33 +316,11 @@ public class MySQL implements DataSource { @Override public boolean saveAuth(PlayerAuth auth) { + columnsHandler.insert(auth, + AuthMeColumns.NAME, AuthMeColumns.NICK_NAME, AuthMeColumns.PASSWORD, AuthMeColumns.SALT, + AuthMeColumns.EMAIL, AuthMeColumns.REGISTRATION_DATE, AuthMeColumns.REGISTRATION_IP); + try (Connection con = getConnection()) { - // TODO ljacqu 20171104: Replace with generic columns util to clean this up - boolean useSalt = !col.SALT.isEmpty() || !StringUtils.isEmpty(auth.getPassword().getSalt()); - boolean hasEmail = auth.getEmail() != null; - String emailPlaceholder = hasEmail ? "?" : "DEFAULT"; - - String sql = "INSERT INTO " + tableName + "(" - + col.NAME + "," + col.PASSWORD + "," + col.REAL_NAME - + "," + col.EMAIL + "," + col.REGISTRATION_DATE + "," + col.REGISTRATION_IP - + (useSalt ? "," + col.SALT : "") - + ") VALUES (?,?,?," + emailPlaceholder + ",?,?" + (useSalt ? ",?" : "") + ");"; - try (PreparedStatement pst = con.prepareStatement(sql)) { - int index = 1; - pst.setString(index++, auth.getNickname()); - pst.setString(index++, auth.getPassword().getHash()); - pst.setString(index++, auth.getRealName()); - if (hasEmail) { - pst.setString(index++, auth.getEmail()); - } - pst.setObject(index++, auth.getRegistrationDate()); - pst.setString(index++, auth.getRegistrationIp()); - if (useSalt) { - pst.setString(index++, auth.getPassword().getSalt()); - } - pst.executeUpdate(); - } - if (!columnOthers.isEmpty()) { for (String column : columnOthers) { try (PreparedStatement pst = con.prepareStatement( diff --git a/src/main/java/fr/xephi/authme/datasource/SQLite.java b/src/main/java/fr/xephi/authme/datasource/SQLite.java index 57d12a6b..0b2361aa 100644 --- a/src/main/java/fr/xephi/authme/datasource/SQLite.java +++ b/src/main/java/fr/xephi/authme/datasource/SQLite.java @@ -4,11 +4,11 @@ import ch.jalu.datasourcecolumns.data.DataSourceValues; import com.google.common.annotations.VisibleForTesting; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.columnshandler.AuthMeColumns; import fr.xephi.authme.datasource.columnshandler.AuthMeColumnsHandler; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.DatabaseSettings; -import fr.xephi.authme.util.StringUtils; import java.io.File; import java.sql.Connection; @@ -254,46 +254,9 @@ public class SQLite implements DataSource { @Override public boolean saveAuth(PlayerAuth auth) { - PreparedStatement pst = null; - try { - HashedPassword password = auth.getPassword(); - if (col.SALT.isEmpty()) { - if (!StringUtils.isEmpty(auth.getPassword().getSalt())) { - ConsoleLogger.warning("Warning! Detected hashed password with separate salt but the salt column " - + "is not set in the config!"); - } - - pst = con.prepareStatement("INSERT INTO " + tableName + "(" + col.NAME + "," + col.PASSWORD - + "," + col.REAL_NAME + "," + col.EMAIL - + "," + col.REGISTRATION_DATE + "," + col.REGISTRATION_IP - + ") VALUES (?,?,?,?,?,?);"); - pst.setString(1, auth.getNickname()); - pst.setString(2, password.getHash()); - pst.setString(3, auth.getRealName()); - pst.setString(4, auth.getEmail()); - pst.setLong(5, auth.getRegistrationDate()); - pst.setString(6, auth.getRegistrationIp()); - pst.executeUpdate(); - } else { - pst = con.prepareStatement("INSERT INTO " + tableName + "(" + col.NAME + "," + col.PASSWORD - + "," + col.REAL_NAME + "," + col.EMAIL - + "," + col.REGISTRATION_DATE + "," + col.REGISTRATION_IP + "," + col.SALT - + ") VALUES (?,?,?,?,?,?,?);"); - pst.setString(1, auth.getNickname()); - pst.setString(2, password.getHash()); - pst.setString(3, auth.getRealName()); - pst.setString(4, auth.getEmail()); - pst.setLong(5, auth.getRegistrationDate()); - pst.setString(6, auth.getRegistrationIp()); - pst.setString(7, password.getSalt()); - pst.executeUpdate(); - } - } catch (SQLException ex) { - logSqlException(ex); - } finally { - close(pst); - } - return true; + return columnsHandler.insert(auth, + AuthMeColumns.NAME, AuthMeColumns.NICK_NAME, AuthMeColumns.PASSWORD, AuthMeColumns.SALT, + AuthMeColumns.EMAIL, AuthMeColumns.REGISTRATION_DATE, AuthMeColumns.REGISTRATION_IP); } @Override diff --git a/src/main/java/fr/xephi/authme/datasource/AuthMeColumns.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumns.java similarity index 61% rename from src/main/java/fr/xephi/authme/datasource/AuthMeColumns.java rename to src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumns.java index 657084ae..246bf558 100644 --- a/src/main/java/fr/xephi/authme/datasource/AuthMeColumns.java +++ b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumns.java @@ -1,14 +1,27 @@ -package fr.xephi.authme.datasource; +package fr.xephi.authme.datasource.columnshandler; import ch.jalu.configme.properties.Property; import ch.jalu.datasourcecolumns.ColumnType; import ch.jalu.datasourcecolumns.DependentColumn; -import ch.jalu.datasourcecolumns.StandardTypes; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.settings.properties.DatabaseSettings; import java.util.function.Function; +import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.ColumnOptions.DEFAULT_FOR_NULL; +import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.ColumnOptions.OPTIONAL; +import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createDouble; +import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createFloat; +import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createInteger; +import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createLong; +import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createString; + +/** + * Column definitions for the AuthMe table. + * + * @param the column type + * @see PlayerAuth + */ public final class AuthMeColumns implements DependentColumn { public static final AuthMeColumns NAME = createString( @@ -20,15 +33,24 @@ public final class AuthMeColumns implements DependentColumn PASSWORD = createString( DatabaseSettings.MYSQL_COL_PASSWORD, auth -> auth.getPassword().getHash()); - public static final AuthMeColumns SALT = new AuthMeColumns<>( - StandardTypes.STRING, DatabaseSettings.MYSQL_COL_SALT, auth -> auth.getPassword().getSalt(), true); + public static final AuthMeColumns SALT = createString( + DatabaseSettings.MYSQL_COL_SALT, auth -> auth.getPassword().getSalt(), OPTIONAL); public static final AuthMeColumns EMAIL = createString( - DatabaseSettings.MYSQL_COL_EMAIL, PlayerAuth::getEmail); + DatabaseSettings.MYSQL_COL_EMAIL, PlayerAuth::getEmail, DEFAULT_FOR_NULL); public static final AuthMeColumns LAST_IP = createString( DatabaseSettings.MYSQL_COL_LAST_IP, PlayerAuth::getLastIp); + public static final AuthMeColumns GROUP_ID = createInteger( + DatabaseSettings.MYSQL_COL_GROUP, PlayerAuth::getGroupId, OPTIONAL); + + public static final AuthMeColumns REGISTRATION_IP = createString( + DatabaseSettings.MYSQL_COL_REGISTER_IP, PlayerAuth::getRegistrationIp); + + public static final AuthMeColumns REGISTRATION_DATE = createLong( + DatabaseSettings.MYSQL_COL_REGISTER_DATE, PlayerAuth::getRegistrationDate); + public static final AuthMeColumns LOCATION_X = createDouble( DatabaseSettings.MYSQL_COL_LASTLOC_X, PlayerAuth::getQuitLocX); @@ -52,28 +74,15 @@ public final class AuthMeColumns implements DependentColumn nameProperty; private final Function playerAuthGetter; private final boolean isOptional; + private final boolean useDefaultForNull; - private AuthMeColumns(ColumnType type, Property nameProperty, Function playerAuthGetter, - boolean isOptional) { + AuthMeColumns(ColumnType type, Property nameProperty, Function playerAuthGetter, + boolean isOptional, boolean useDefaultForNull) { this.columnType = type; this.nameProperty = nameProperty; this.playerAuthGetter = playerAuthGetter; this.isOptional = isOptional; - } - - private static AuthMeColumns createString(Property nameProperty, - Function getter) { - return new AuthMeColumns<>(StandardTypes.STRING, nameProperty, getter, false); - } - - private static AuthMeColumns createDouble(Property nameProperty, - Function getter) { - return new AuthMeColumns<>(new DoubleType(), nameProperty, getter, false); - } - - private static AuthMeColumns createFloat(Property nameProperty, - Function getter) { - return new AuthMeColumns<>(new FloatType(), nameProperty, getter, false); + this.useDefaultForNull = useDefaultForNull; } @@ -103,23 +112,6 @@ public final class AuthMeColumns implements DependentColumn { - - @Override - public Class getClazz() { - return Double.class; - } - } - - private static final class FloatType implements ColumnType { - - @Override - public Class getClazz() { - return Float.class; - } + return useDefaultForNull && columnContext.hasDefaultSupport(); } } diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsFactory.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsFactory.java new file mode 100644 index 00000000..420a26bc --- /dev/null +++ b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsFactory.java @@ -0,0 +1,76 @@ +package fr.xephi.authme.datasource.columnshandler; + +import ch.jalu.configme.properties.Property; +import ch.jalu.datasourcecolumns.ColumnType; +import ch.jalu.datasourcecolumns.StandardTypes; +import fr.xephi.authme.data.auth.PlayerAuth; + +import java.util.function.Function; + +/** + * Util class for initializing {@link AuthMeColumns} constants. + */ +final class AuthMeColumnsFactory { + + private AuthMeColumnsFactory() { + } + + static AuthMeColumns createInteger(Property nameProperty, + Function playerAuthGetter, + ColumnOptions... options) { + return createInternal(StandardTypes.INTEGER, nameProperty, playerAuthGetter, options); + } + + static AuthMeColumns createLong(Property nameProperty, + Function playerAuthGetter, + ColumnOptions... options) { + return createInternal(StandardTypes.LONG, nameProperty, playerAuthGetter, options); + } + + static AuthMeColumns createString(Property nameProperty, + Function playerAuthGetter, + ColumnOptions... options) { + return createInternal(StandardTypes.STRING, nameProperty, playerAuthGetter, options); + } + + static AuthMeColumns createDouble(Property nameProperty, + Function playerAuthGetter, + ColumnOptions... options) { + return createInternal(StandardTypes.DOUBLE, nameProperty, playerAuthGetter, options); + } + + static AuthMeColumns createFloat(Property nameProperty, + Function playerAuthGetter, + ColumnOptions... options) { + return createInternal(StandardTypes.FLOAT, nameProperty, playerAuthGetter, options); + } + + private static AuthMeColumns createInternal(ColumnType type, Property nameProperty, + Function authGetter, ColumnOptions... options) { + return new AuthMeColumns<>(type, nameProperty, authGetter, isOptional(options), hasDefaultForNull(options)); + } + + private static boolean isOptional(ColumnOptions[] options) { + return containsInArray(ColumnOptions.OPTIONAL, options); + } + + private static boolean hasDefaultForNull(ColumnOptions[] options) { + return containsInArray(ColumnOptions.DEFAULT_FOR_NULL, options); + } + + private static boolean containsInArray(ColumnOptions needle, ColumnOptions[] haystack) { + for (ColumnOptions option : haystack) { + if (option == needle) { + return true; + } + } + return false; + } + + enum ColumnOptions { + + OPTIONAL, + + DEFAULT_FOR_NULL + } +} diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java index b4c02dc9..a35c8c97 100644 --- a/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java +++ b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java @@ -4,12 +4,9 @@ import ch.jalu.datasourcecolumns.data.DataSourceValue; import ch.jalu.datasourcecolumns.data.DataSourceValues; import ch.jalu.datasourcecolumns.data.UpdateValues; import ch.jalu.datasourcecolumns.sqlimplementation.PredicateSqlGenerator; -import ch.jalu.datasourcecolumns.sqlimplementation.PreparedStatementGenerator; import ch.jalu.datasourcecolumns.sqlimplementation.ResultSetValueRetriever; import ch.jalu.datasourcecolumns.sqlimplementation.SqlColumnsHandler; import fr.xephi.authme.data.auth.PlayerAuth; -import fr.xephi.authme.datasource.AuthMeColumns; -import fr.xephi.authme.datasource.ColumnContext; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.DatabaseSettings; @@ -38,7 +35,7 @@ public final class AuthMeColumnsHandler { * @return created column handler */ public static AuthMeColumnsHandler createForSqlite(Connection connection, Settings settings) { - ColumnContext columnContext = new ColumnContext(settings); + ColumnContext columnContext = new ColumnContext(settings, false); String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); String nameColumn = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME); @@ -50,19 +47,18 @@ public final class AuthMeColumnsHandler { /** * Creates a column handler for MySQL. * - * @param preparedStatementGenerator supplier of SQL prepared statements with a connection to the database + * @param connectionSupplier supplier of connections from the connection pool * @param settings plugin settings * @return created column handler */ - public static AuthMeColumnsHandler createForMySql(PreparedStatementGenerator preparedStatementGenerator, - Settings settings) { - ColumnContext columnContext = new ColumnContext(settings); + public static AuthMeColumnsHandler createForMySql(ConnectionSupplier connectionSupplier, Settings settings) { + ColumnContext columnContext = new ColumnContext(settings, true); String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); String nameColumn = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME); - SqlColumnsHandler sqlColHandler = new SqlColumnsHandler<>(preparedStatementGenerator, - columnContext, tableName, nameColumn, new ResultSetValueRetriever<>(columnContext), - new PredicateSqlGenerator<>(columnContext)); + SqlColumnsHandler sqlColHandler = new SqlColumnsHandler<>( + new MySqlPreparedStatementGenerator(connectionSupplier), columnContext, tableName, nameColumn, + new ResultSetValueRetriever<>(columnContext), new PredicateSqlGenerator<>(columnContext)); return new AuthMeColumnsHandler(sqlColHandler); } @@ -138,4 +134,20 @@ public final class AuthMeColumnsHandler { public DataSourceValues retrieve(String name, AuthMeColumns... columns) throws SQLException { return internalHandler.retrieve(name.toLowerCase(), columns); } + + /** + * Inserts the given values into a new row, as taken from the player auth. + * + * @param auth the player auth to get values from + * @param columns the columns to insert + * @return true upon success, false otherwise + */ + public boolean insert(PlayerAuth auth, AuthMeColumns... columns) { + try { + return internalHandler.insert(auth, columns); + } catch (SQLException e) { + logSqlException(e); + return false; + } + } } diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/ColumnContext.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/ColumnContext.java new file mode 100644 index 00000000..8759935d --- /dev/null +++ b/src/main/java/fr/xephi/authme/datasource/columnshandler/ColumnContext.java @@ -0,0 +1,35 @@ +package fr.xephi.authme.datasource.columnshandler; + +import fr.xephi.authme.settings.Settings; + +import java.util.HashMap; +import java.util.Map; + +/** + * Context for resolving the properties of {@link AuthMeColumns} entries. + */ +public class ColumnContext { + + private final Settings settings; + private final Map, String> columnNames = new HashMap<>(); + private final boolean hasDefaultSupport; + + /** + * Constructor. + * + * @param settings plugin settings + * @param hasDefaultSupport whether or not the underlying database has support for the {@code DEFAULT} keyword + */ + public ColumnContext(Settings settings, boolean hasDefaultSupport) { + this.settings = settings; + this.hasDefaultSupport = hasDefaultSupport; + } + + public String getName(AuthMeColumns column) { + return columnNames.computeIfAbsent(column, k -> settings.getProperty(k.getNameProperty())); + } + + public boolean hasDefaultSupport() { + return hasDefaultSupport; + } +} diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/ConnectionSupplier.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/ConnectionSupplier.java new file mode 100644 index 00000000..4d419a21 --- /dev/null +++ b/src/main/java/fr/xephi/authme/datasource/columnshandler/ConnectionSupplier.java @@ -0,0 +1,17 @@ +package fr.xephi.authme.datasource.columnshandler; + +import java.sql.Connection; +import java.sql.SQLException; + +/** + * Supplier of connections to a database. + */ +@FunctionalInterface +public interface ConnectionSupplier { + + /** + * @return connection object to the database + */ + Connection get() throws SQLException; + +} diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/MySqlPreparedStatementGenerator.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/MySqlPreparedStatementGenerator.java new file mode 100644 index 00000000..c20357ae --- /dev/null +++ b/src/main/java/fr/xephi/authme/datasource/columnshandler/MySqlPreparedStatementGenerator.java @@ -0,0 +1,44 @@ +package fr.xephi.authme.datasource.columnshandler; + +import ch.jalu.datasourcecolumns.sqlimplementation.PreparedStatementGenerator; +import ch.jalu.datasourcecolumns.sqlimplementation.PreparedStatementResult; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * Implementation of {@link PreparedStatementGenerator} for MySQL which ensures that the connection + * taken from the connection pool is also closed after the prepared statement has been executed. + */ +class MySqlPreparedStatementGenerator implements PreparedStatementGenerator { + + private final ConnectionSupplier connectionSupplier; + + MySqlPreparedStatementGenerator(ConnectionSupplier connectionSupplier) { + this.connectionSupplier = connectionSupplier; + } + + @Override + public PreparedStatementResult create(String sql) throws SQLException { + Connection connection = connectionSupplier.get(); + return new MySqlPreparedStatementResult(connection, connection.prepareStatement(sql)); + } + + /** Prepared statement result which also closes the associated connection. */ + private static final class MySqlPreparedStatementResult extends PreparedStatementResult { + + private final Connection connection; + + MySqlPreparedStatementResult(Connection connection, PreparedStatement preparedStatement) { + super(preparedStatement); + this.connection = connection; + } + + @Override + public void close() throws SQLException { + super.close(); + connection.close(); + } + } +} diff --git a/src/test/java/fr/xephi/authme/ClassesConsistencyTest.java b/src/test/java/fr/xephi/authme/ClassesConsistencyTest.java index 4005dc79..c7f09398 100644 --- a/src/test/java/fr/xephi/authme/ClassesConsistencyTest.java +++ b/src/test/java/fr/xephi/authme/ClassesConsistencyTest.java @@ -5,8 +5,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import fr.xephi.authme.data.captcha.CaptchaCodeStorage; -import fr.xephi.authme.datasource.AuthMeColumns; import fr.xephi.authme.datasource.Columns; +import fr.xephi.authme.datasource.columnshandler.AuthMeColumns; import fr.xephi.authme.datasource.mysqlextensions.MySqlExtension; import fr.xephi.authme.initialization.HasCleanup; import fr.xephi.authme.process.register.executors.RegistrationMethod; From 137fc3d50579cfc9c2db29dd86f19fc4cc202325 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sat, 24 Mar 2018 22:53:30 +0100 Subject: [PATCH 10/63] #1539 Use columns handler in more datasource methods; fix case-insensitivity for SQLite --- .../fr/xephi/authme/datasource/MySQL.java | 81 +++++-------------- .../fr/xephi/authme/datasource/SQLite.java | 81 ++++--------------- .../columnshandler/AuthMeColumns.java | 3 + .../columnshandler/AuthMeColumnsHandler.java | 35 +++++++- .../datasource/MySqlIntegrationTest.java | 4 +- 5 files changed, 75 insertions(+), 129 deletions(-) diff --git a/src/main/java/fr/xephi/authme/datasource/MySQL.java b/src/main/java/fr/xephi/authme/datasource/MySQL.java index 8e501ae0..e7739fb1 100644 --- a/src/main/java/fr/xephi/authme/datasource/MySQL.java +++ b/src/main/java/fr/xephi/authme/datasource/MySQL.java @@ -1,6 +1,7 @@ package fr.xephi.authme.datasource; import ch.jalu.datasourcecolumns.data.DataSourceValues; +import ch.jalu.datasourcecolumns.predicate.AlwaysTruePredicate; import com.google.common.annotations.VisibleForTesting; import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.pool.HikariPool.PoolInitializationException; @@ -23,11 +24,14 @@ import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import static ch.jalu.datasourcecolumns.data.UpdateValues.with; +import static ch.jalu.datasourcecolumns.predicate.StandardPredicates.eq; +import static ch.jalu.datasourcecolumns.predicate.StandardPredicates.eqIgnoreCase; import static fr.xephi.authme.datasource.SqlDataSourceUtils.getNullableLong; import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException; @@ -269,24 +273,20 @@ public class MySQL implements DataSource { @Override public boolean isAuthAvailable(String user) { - String sql = "SELECT " + col.NAME + " FROM " + tableName + " WHERE " + col.NAME + "=?;"; - try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, user.toLowerCase()); - try (ResultSet rs = pst.executeQuery()) { - return rs.next(); - } - } catch (SQLException ex) { - logSqlException(ex); + try { + return columnsHandler.retrieve(user, AuthMeColumns.NAME).rowExists(); + } catch (SQLException e) { + logSqlException(e); + return false; } - return false; } @Override public HashedPassword getPassword(String user) { try { - DataSourceValues passwordResult = columnsHandler.retrieve(user, AuthMeColumns.PASSWORD, AuthMeColumns.SALT); - if (passwordResult.rowExists()) { - return new HashedPassword(passwordResult.get(AuthMeColumns.PASSWORD), passwordResult.get(AuthMeColumns.SALT)); + DataSourceValues values = columnsHandler.retrieve(user, AuthMeColumns.PASSWORD, AuthMeColumns.SALT); + if (values.rowExists()) { + return new HashedPassword(values.get(AuthMeColumns.PASSWORD), values.get(AuthMeColumns.SALT)); } } catch (SQLException e) { logSqlException(e); @@ -354,19 +354,7 @@ public class MySQL implements DataSource { @Override public boolean updateSession(PlayerAuth auth) { - String sql = "UPDATE " + tableName + " SET " - + col.LAST_IP + "=?, " + col.LAST_LOGIN + "=?, " + col.REAL_NAME + "=? WHERE " + col.NAME + "=?;"; - try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, auth.getLastIp()); - pst.setObject(2, auth.getLastLogin()); - pst.setString(3, auth.getRealName()); - pst.setString(4, auth.getNickname()); - pst.executeUpdate(); - return true; - } catch (SQLException ex) { - logSqlException(ex); - } - return false; + return columnsHandler.update(auth, AuthMeColumns.LAST_IP, AuthMeColumns.LAST_LOGIN, AuthMeColumns.NICK_NAME); } @Override @@ -427,35 +415,17 @@ public class MySQL implements DataSource { @Override public List getAllAuthsByIp(String ip) { - List result = new ArrayList<>(); - String sql = "SELECT " + col.NAME + " FROM " + tableName + " WHERE " + col.LAST_IP + "=?;"; - try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, ip); - try (ResultSet rs = pst.executeQuery()) { - while (rs.next()) { - result.add(rs.getString(col.NAME)); - } - } - } catch (SQLException ex) { - logSqlException(ex); + try { + return columnsHandler.retrieve(eq(AuthMeColumns.LAST_IP, ip), AuthMeColumns.NAME); + } catch (SQLException e) { + logSqlException(e); + return Collections.emptyList(); } - return result; } @Override public int countAuthsByEmail(String email) { - String sql = "SELECT COUNT(1) FROM " + tableName + " WHERE UPPER(" + col.EMAIL + ") = UPPER(?)"; - try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, email); - try (ResultSet rs = pst.executeQuery()) { - if (rs.next()) { - return rs.getInt(1); - } - } - } catch (SQLException ex) { - logSqlException(ex); - } - return 0; + return columnsHandler.count(eqIgnoreCase(AuthMeColumns.EMAIL, email)); } @Override @@ -566,18 +536,7 @@ public class MySQL implements DataSource { @Override public int getAccountsRegistered() { - int result = 0; - String sql = "SELECT COUNT(*) FROM " + tableName; - try (Connection con = getConnection(); - Statement st = con.createStatement(); - ResultSet rs = st.executeQuery(sql)) { - if (rs.next()) { - result = rs.getInt(1); - } - } catch (SQLException ex) { - logSqlException(ex); - } - return result; + return columnsHandler.count(new AlwaysTruePredicate<>()); } @Override diff --git a/src/main/java/fr/xephi/authme/datasource/SQLite.java b/src/main/java/fr/xephi/authme/datasource/SQLite.java index 0b2361aa..01f13a50 100644 --- a/src/main/java/fr/xephi/authme/datasource/SQLite.java +++ b/src/main/java/fr/xephi/authme/datasource/SQLite.java @@ -1,6 +1,7 @@ package fr.xephi.authme.datasource; import ch.jalu.datasourcecolumns.data.DataSourceValues; +import ch.jalu.datasourcecolumns.predicate.AlwaysTruePredicate; import com.google.common.annotations.VisibleForTesting; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.auth.PlayerAuth; @@ -20,11 +21,14 @@ import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import static ch.jalu.datasourcecolumns.data.UpdateValues.with; +import static ch.jalu.datasourcecolumns.predicate.StandardPredicates.eq; +import static ch.jalu.datasourcecolumns.predicate.StandardPredicates.eqIgnoreCase; import static fr.xephi.authme.datasource.SqlDataSourceUtils.getNullableLong; import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException; @@ -211,14 +215,10 @@ public class SQLite implements DataSource { @Override public boolean isAuthAvailable(String user) { - String sql = "SELECT 1 FROM " + tableName + " WHERE LOWER(" + col.NAME + ")=LOWER(?);"; - try (PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, user); - try (ResultSet rs = pst.executeQuery()) { - return rs.next(); - } - } catch (SQLException ex) { - ConsoleLogger.warning(ex.getMessage()); + try { + return columnsHandler.retrieve(user, AuthMeColumns.NAME).rowExists(); + } catch (SQLException e) { + logSqlException(e); return false; } } @@ -273,19 +273,7 @@ public class SQLite implements DataSource { @Override public boolean updateSession(PlayerAuth auth) { - String sql = "UPDATE " + tableName + " SET " + col.LAST_IP + "=?, " + col.LAST_LOGIN + "=?, " - + col.REAL_NAME + "=? WHERE " + col.NAME + "=?;"; - try (PreparedStatement pst = con.prepareStatement(sql)){ - pst.setString(1, auth.getLastIp()); - pst.setObject(2, auth.getLastLogin()); - pst.setString(3, auth.getRealName()); - pst.setString(4, auth.getNickname()); - pst.executeUpdate(); - return true; - } catch (SQLException ex) { - logSqlException(ex); - } - return false; + return columnsHandler.update(auth, AuthMeColumns.LAST_IP, AuthMeColumns.LAST_LOGIN, AuthMeColumns.NICK_NAME); } @Override @@ -360,36 +348,17 @@ public class SQLite implements DataSource { @Override public List getAllAuthsByIp(String ip) { - List countIp = new ArrayList<>(); - String sql = "SELECT " + col.NAME + " FROM " + tableName + " WHERE " + col.LAST_IP + "=?;"; - try (PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, ip); - try (ResultSet rs = pst.executeQuery()) { - while (rs.next()) { - countIp.add(rs.getString(col.NAME)); - } - return countIp; - } - } catch (SQLException ex) { - logSqlException(ex); + try { + return columnsHandler.retrieve(eq(AuthMeColumns.LAST_IP, ip), AuthMeColumns.NAME); + } catch (SQLException e) { + logSqlException(e); + return Collections.emptyList(); } - return new ArrayList<>(); } @Override public int countAuthsByEmail(String email) { - String sql = "SELECT COUNT(1) FROM " + tableName + " WHERE " + col.EMAIL + " = ? COLLATE NOCASE;"; - try (PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, email); - try (ResultSet rs = pst.executeQuery()) { - if (rs.next()) { - return rs.getInt(1); - } - } - } catch (SQLException ex) { - logSqlException(ex); - } - return 0; + return columnsHandler.count(eqIgnoreCase(AuthMeColumns.EMAIL, email)); } @Override @@ -491,15 +460,7 @@ public class SQLite implements DataSource { @Override public int getAccountsRegistered() { - String sql = "SELECT COUNT(*) FROM " + tableName + ";"; - try (PreparedStatement pst = con.prepareStatement(sql); ResultSet rs = pst.executeQuery()) { - if (rs.next()) { - return rs.getInt(1); - } - } catch (SQLException ex) { - logSqlException(ex); - } - return 0; + return columnsHandler.count(new AlwaysTruePredicate<>()); } @Override @@ -607,16 +568,6 @@ public class SQLite implements DataSource { + currentTimestamp + ", to all " + updatedRows + " rows"); } - private static void close(Statement st) { - if (st != null) { - try { - st.close(); - } catch (SQLException ex) { - logSqlException(ex); - } - } - } - private static void close(Connection con) { if (con != null) { try { diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumns.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumns.java index 246bf558..109ea5a6 100644 --- a/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumns.java +++ b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumns.java @@ -45,6 +45,9 @@ public final class AuthMeColumns implements DependentColumn GROUP_ID = createInteger( DatabaseSettings.MYSQL_COL_GROUP, PlayerAuth::getGroupId, OPTIONAL); + public static final AuthMeColumns LAST_LOGIN = createLong( + DatabaseSettings.MYSQL_COL_LASTLOGIN, PlayerAuth::getLastLogin); + public static final AuthMeColumns REGISTRATION_IP = createString( DatabaseSettings.MYSQL_COL_REGISTER_IP, PlayerAuth::getRegistrationIp); diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java index a35c8c97..08202e39 100644 --- a/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java +++ b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java @@ -3,7 +3,9 @@ package fr.xephi.authme.datasource.columnshandler; import ch.jalu.datasourcecolumns.data.DataSourceValue; import ch.jalu.datasourcecolumns.data.DataSourceValues; import ch.jalu.datasourcecolumns.data.UpdateValues; +import ch.jalu.datasourcecolumns.predicate.Predicate; import ch.jalu.datasourcecolumns.sqlimplementation.PredicateSqlGenerator; +import ch.jalu.datasourcecolumns.sqlimplementation.PreparedStatementGenerator; import ch.jalu.datasourcecolumns.sqlimplementation.ResultSetValueRetriever; import ch.jalu.datasourcecolumns.sqlimplementation.SqlColumnsHandler; import fr.xephi.authme.data.auth.PlayerAuth; @@ -12,6 +14,7 @@ import fr.xephi.authme.settings.properties.DatabaseSettings; import java.sql.Connection; import java.sql.SQLException; +import java.util.List; import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException; @@ -39,8 +42,9 @@ public final class AuthMeColumnsHandler { String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); String nameColumn = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME); - SqlColumnsHandler sqlColHandler = - new SqlColumnsHandler<>(connection, columnContext, tableName, nameColumn); + SqlColumnsHandler sqlColHandler = new SqlColumnsHandler<>( + PreparedStatementGenerator.fromConnection(connection), columnContext, tableName, nameColumn, + new ResultSetValueRetriever<>(columnContext), new PredicateSqlGenerator<>(columnContext, true)); return new AuthMeColumnsHandler(sqlColHandler); } @@ -135,6 +139,18 @@ public final class AuthMeColumnsHandler { return internalHandler.retrieve(name.toLowerCase(), columns); } + /** + * Retrieves a column's value for all rows that satisfy the given predicate. + * + * @param predicate the predicate to fulfill + * @param column the column to retrieve from the matching rows + * @param the column's value type + * @return the values of the matching rows + */ + public List retrieve(Predicate predicate, AuthMeColumns column) throws SQLException { + return internalHandler.retrieve(predicate, column); + } + /** * Inserts the given values into a new row, as taken from the player auth. * @@ -150,4 +166,19 @@ public final class AuthMeColumnsHandler { return false; } } + + /** + * Returns the number of rows that match the provided predicate. + * + * @param predicate the predicate to test the rows for + * @return number of rows fulfilling the predicate + */ + public int count(Predicate predicate) { + try { + return internalHandler.count(predicate); + } catch (SQLException e) { + logSqlException(e); + return 0; + } + } } diff --git a/src/test/java/fr/xephi/authme/datasource/MySqlIntegrationTest.java b/src/test/java/fr/xephi/authme/datasource/MySqlIntegrationTest.java index eab6a4cc..02760823 100644 --- a/src/test/java/fr/xephi/authme/datasource/MySqlIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/datasource/MySqlIntegrationTest.java @@ -55,7 +55,9 @@ public class MySqlIntegrationTest extends AbstractDataSourceIntegrationTest { HikariConfig config = new HikariConfig(); config.setDataSourceClassName("org.h2.jdbcx.JdbcDataSource"); config.setConnectionTestQuery("VALUES 1"); - config.addDataSourceProperty("URL", "jdbc:h2:mem:test"); + // Note "ignorecase=true": H2 does not support `COLLATE NOCASE` for case-insensitive equals queries. + // MySQL is by default case-insensitive so this is OK to make as an assumption. + config.addDataSourceProperty("URL", "jdbc:h2:mem:test;ignorecase=true"); config.addDataSourceProperty("user", "sa"); config.addDataSourceProperty("password", "sa"); HikariDataSource ds = new HikariDataSource(config); From 4595a141914817b6764d8fa16847af9d4eaae5cd Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 25 Mar 2018 11:52:40 +0200 Subject: [PATCH 11/63] #1539 Add support for columns that are not on player auth (is_logged, has_session) --- .../executable/authme/GetEmailCommand.java | 6 +- .../authme/debug/TestEmailSender.java | 6 +- .../executable/email/RecoverEmailCommand.java | 6 +- .../authme/data/VerificationCodeManager.java | 10 +- .../authme/datasource/CacheDataSource.java | 8 +- .../xephi/authme/datasource/DataSource.java | 3 +- .../authme/datasource/DataSourceResult.java | 53 ---------- .../fr/xephi/authme/datasource/FlatFile.java | 3 +- .../fr/xephi/authme/datasource/MySQL.java | 95 +++++------------- .../fr/xephi/authme/datasource/SQLite.java | 64 ++++--------- .../columnshandler/AuthMeColumns.java | 96 ++++++------------- .../columnshandler/AuthMeColumnsFactory.java | 45 +++++---- .../columnshandler/AuthMeColumnsHandler.java | 30 ++++-- .../columnshandler/ColumnContext.java | 4 +- .../columnshandler/DataSourceColumn.java | 57 +++++++++++ .../columnshandler/PlayerAuthColumn.java | 32 +++++++ .../xephi/authme/ClassesConsistencyTest.java | 5 +- .../authme/GetEmailCommandTest.java | 6 +- .../email/RecoverEmailCommandTest.java | 12 +-- .../data/VerificationCodeManagerTest.java | 10 +- .../AbstractDataSourceIntegrationTest.java | 8 +- 21 files changed, 257 insertions(+), 302 deletions(-) delete mode 100644 src/main/java/fr/xephi/authme/datasource/DataSourceResult.java create mode 100644 src/main/java/fr/xephi/authme/datasource/columnshandler/DataSourceColumn.java create mode 100644 src/main/java/fr/xephi/authme/datasource/columnshandler/PlayerAuthColumn.java diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/GetEmailCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/GetEmailCommand.java index a7f327a1..b6691438 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/GetEmailCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/GetEmailCommand.java @@ -1,8 +1,8 @@ package fr.xephi.authme.command.executable.authme; +import ch.jalu.datasourcecolumns.data.DataSourceValue; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.datasource.DataSourceResult; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.service.CommonService; import org.bukkit.command.CommandSender; @@ -25,8 +25,8 @@ public class GetEmailCommand implements ExecutableCommand { public void executeCommand(CommandSender sender, List arguments) { String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0); - DataSourceResult email = dataSource.getEmail(playerName); - if (email.playerExists()) { + DataSourceValue email = dataSource.getEmail(playerName); + if (email.rowExists()) { sender.sendMessage("[AuthMe] " + playerName + "'s email: " + email.getValue()); } else { commonService.send(sender, MessageKey.UNKNOWN_USER); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/TestEmailSender.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/TestEmailSender.java index f3580d32..02bd4a21 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/debug/TestEmailSender.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/TestEmailSender.java @@ -1,8 +1,8 @@ package fr.xephi.authme.command.executable.authme.debug; +import ch.jalu.datasourcecolumns.data.DataSourceValue; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.datasource.DataSourceResult; import fr.xephi.authme.mail.SendMailSsl; import fr.xephi.authme.permission.DebugSectionPermissions; import fr.xephi.authme.permission.PermissionNode; @@ -82,8 +82,8 @@ class TestEmailSender implements DebugSection { */ private String getEmail(CommandSender sender, List arguments) { if (arguments.isEmpty()) { - DataSourceResult emailResult = dataSource.getEmail(sender.getName()); - if (!emailResult.playerExists()) { + DataSourceValue emailResult = dataSource.getEmail(sender.getName()); + if (!emailResult.rowExists()) { sender.sendMessage(ChatColor.RED + "Please provide an email address, " + "e.g. /authme debug mail test@example.com"); return null; diff --git a/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java b/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java index 339980a3..0a3a9694 100644 --- a/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/email/RecoverEmailCommand.java @@ -1,10 +1,10 @@ package fr.xephi.authme.command.executable.email; +import ch.jalu.datasourcecolumns.data.DataSourceValue; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.datasource.DataSourceResult; import fr.xephi.authme.mail.EmailService; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.service.BukkitService; @@ -58,8 +58,8 @@ public class RecoverEmailCommand extends PlayerCommand { return; } - DataSourceResult emailResult = dataSource.getEmail(playerName); - if (!emailResult.playerExists()) { + DataSourceValue emailResult = dataSource.getEmail(playerName); + if (!emailResult.rowExists()) { commonService.send(player, MessageKey.USAGE_REGISTER); return; } diff --git a/src/main/java/fr/xephi/authme/data/VerificationCodeManager.java b/src/main/java/fr/xephi/authme/data/VerificationCodeManager.java index a6ba75c6..c5c2d725 100644 --- a/src/main/java/fr/xephi/authme/data/VerificationCodeManager.java +++ b/src/main/java/fr/xephi/authme/data/VerificationCodeManager.java @@ -1,7 +1,7 @@ package fr.xephi.authme.data; +import ch.jalu.datasourcecolumns.data.DataSourceValue; import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.datasource.DataSourceResult; import fr.xephi.authme.initialization.HasCleanup; import fr.xephi.authme.initialization.SettingsDependent; import fr.xephi.authme.mail.EmailService; @@ -103,8 +103,8 @@ public class VerificationCodeManager implements SettingsDependent, HasCleanup { */ public boolean hasEmail(String name) { boolean result = false; - DataSourceResult emailResult = dataSource.getEmail(name); - if (emailResult.playerExists()) { + DataSourceValue emailResult = dataSource.getEmail(name); + if (emailResult.rowExists()) { final String email = emailResult.getValue(); if (!Utils.isEmailEmpty(email)) { result = true; @@ -130,8 +130,8 @@ public class VerificationCodeManager implements SettingsDependent, HasCleanup { * @param name the name of the player to generate a code for */ private void generateCode(String name) { - DataSourceResult emailResult = dataSource.getEmail(name); - if (emailResult.playerExists()) { + DataSourceValue emailResult = dataSource.getEmail(name); + if (emailResult.rowExists()) { final String email = emailResult.getValue(); if (!Utils.isEmailEmpty(email)) { String code = RandomStringUtils.generateNum(6); // 6 digits code diff --git a/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java b/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java index 39f04a53..5081cf09 100644 --- a/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java +++ b/src/main/java/fr/xephi/authme/datasource/CacheDataSource.java @@ -1,5 +1,7 @@ package fr.xephi.authme.datasource; +import ch.jalu.datasourcecolumns.data.DataSourceValue; +import ch.jalu.datasourcecolumns.data.DataSourceValueImpl; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; @@ -244,10 +246,10 @@ public class CacheDataSource implements DataSource { } @Override - public DataSourceResult getEmail(String user) { + public DataSourceValue getEmail(String user) { return cachedAuths.getUnchecked(user) - .map(auth -> DataSourceResult.of(auth.getEmail())) - .orElse(DataSourceResult.unknownPlayer()); + .map(auth -> DataSourceValueImpl.of(auth.getEmail())) + .orElse(DataSourceValueImpl.unknownRow()); } @Override diff --git a/src/main/java/fr/xephi/authme/datasource/DataSource.java b/src/main/java/fr/xephi/authme/datasource/DataSource.java index 6f97951d..a0a47ade 100644 --- a/src/main/java/fr/xephi/authme/datasource/DataSource.java +++ b/src/main/java/fr/xephi/authme/datasource/DataSource.java @@ -1,5 +1,6 @@ package fr.xephi.authme.datasource; +import ch.jalu.datasourcecolumns.data.DataSourceValue; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.initialization.Reloadable; import fr.xephi.authme.security.crypts.HashedPassword; @@ -216,7 +217,7 @@ public interface DataSource extends Reloadable { * @param user the user to retrieve an email for * @return the email saved for the user, or null if user or email is not present */ - DataSourceResult getEmail(String user); + DataSourceValue getEmail(String user); /** * Return all players of the database. diff --git a/src/main/java/fr/xephi/authme/datasource/DataSourceResult.java b/src/main/java/fr/xephi/authme/datasource/DataSourceResult.java deleted file mode 100644 index c005874e..00000000 --- a/src/main/java/fr/xephi/authme/datasource/DataSourceResult.java +++ /dev/null @@ -1,53 +0,0 @@ -package fr.xephi.authme.datasource; - -/** - * Wraps a value and allows to specify whether a value is missing or the player is not registered. - */ -public final class DataSourceResult { - - /** Instance used when a player does not exist. */ - private static final DataSourceResult UNKNOWN_PLAYER = new DataSourceResult<>(null); - private final T value; - - private DataSourceResult(T value) { - this.value = value; - } - - /** - * Returns a {@link DataSourceResult} for the given value. - * - * @param value the value to wrap - * @param the value's type - * @return DataSourceResult object for the given value - */ - public static DataSourceResult of(T value) { - return new DataSourceResult<>(value); - } - - /** - * Returns a {@link DataSourceResult} specifying that the player does not exist. - * - * @param the value type - * @return data source result for unknown player - */ - public static DataSourceResult unknownPlayer() { - return UNKNOWN_PLAYER; - } - - /** - * @return whether the player of the associated value exists - */ - public boolean playerExists() { - return this != UNKNOWN_PLAYER; - } - - /** - * Returns the value. It is {@code null} if the player is unknown. It is also {@code null} - * if the player exists but does not have the value defined. - * - * @return the value, or null - */ - public T getValue() { - return value; - } -} diff --git a/src/main/java/fr/xephi/authme/datasource/FlatFile.java b/src/main/java/fr/xephi/authme/datasource/FlatFile.java index d234da55..56db1e6e 100644 --- a/src/main/java/fr/xephi/authme/datasource/FlatFile.java +++ b/src/main/java/fr/xephi/authme/datasource/FlatFile.java @@ -1,5 +1,6 @@ package fr.xephi.authme.datasource; +import ch.jalu.datasourcecolumns.data.DataSourceValue; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.security.crypts.HashedPassword; @@ -366,7 +367,7 @@ public class FlatFile implements DataSource { } @Override - public DataSourceResult getEmail(String user) { + public DataSourceValue getEmail(String user) { throw new UnsupportedOperationException("Flat file no longer supported"); } diff --git a/src/main/java/fr/xephi/authme/datasource/MySQL.java b/src/main/java/fr/xephi/authme/datasource/MySQL.java index e7739fb1..b8e487e2 100644 --- a/src/main/java/fr/xephi/authme/datasource/MySQL.java +++ b/src/main/java/fr/xephi/authme/datasource/MySQL.java @@ -1,5 +1,7 @@ package fr.xephi.authme.datasource; +import ch.jalu.datasourcecolumns.data.DataSourceValue; +import ch.jalu.datasourcecolumns.data.DataSourceValueImpl; import ch.jalu.datasourcecolumns.data.DataSourceValues; import ch.jalu.datasourcecolumns.predicate.AlwaysTruePredicate; import com.google.common.annotations.VisibleForTesting; @@ -448,90 +450,49 @@ public class MySQL implements DataSource { @Override public boolean isLogged(String user) { - String sql = "SELECT " + col.IS_LOGGED + " FROM " + tableName + " WHERE " + col.NAME + "=?;"; - try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, user); - try (ResultSet rs = pst.executeQuery()) { - return rs.next() && (rs.getInt(col.IS_LOGGED) == 1); - } - } catch (SQLException ex) { - logSqlException(ex); + try { + DataSourceValue result = columnsHandler.retrieve(user, AuthMeColumns.IS_LOGGED); + return result.rowExists() && Integer.valueOf(1).equals(result.getValue()); + } catch (SQLException e) { + logSqlException(e); + return false; } - return false; } @Override public void setLogged(String user) { - String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE " + col.NAME + "=?;"; - try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { - pst.setInt(1, 1); - pst.setString(2, user.toLowerCase()); - pst.executeUpdate(); - } catch (SQLException ex) { - logSqlException(ex); - } + columnsHandler.update(user, AuthMeColumns.IS_LOGGED, 1); } @Override public void setUnlogged(String user) { - String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE " + col.NAME + "=?;"; - try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { - pst.setInt(1, 0); - pst.setString(2, user.toLowerCase()); - pst.executeUpdate(); - } catch (SQLException ex) { - logSqlException(ex); - } + columnsHandler.update(user, AuthMeColumns.IS_LOGGED, 0); } @Override public boolean hasSession(String user) { - String sql = "SELECT " + col.HAS_SESSION + " FROM " + tableName + " WHERE " + col.NAME + "=?;"; - try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, user.toLowerCase()); - try (ResultSet rs = pst.executeQuery()) { - return rs.next() && (rs.getInt(col.HAS_SESSION) == 1); - } - } catch (SQLException ex) { - logSqlException(ex); + try { + DataSourceValue result = columnsHandler.retrieve(user, AuthMeColumns.HAS_SESSION); + return result.rowExists() && Integer.valueOf(1).equals(result.getValue()); + } catch (SQLException e) { + logSqlException(e); + return false; } - return false; } @Override public void grantSession(String user) { - String sql = "UPDATE " + tableName + " SET " + col.HAS_SESSION + "=? WHERE " + col.NAME + "=?;"; - try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { - pst.setInt(1, 1); - pst.setString(2, user.toLowerCase()); - pst.executeUpdate(); - } catch (SQLException ex) { - logSqlException(ex); - } + columnsHandler.update(user, AuthMeColumns.HAS_SESSION, 1); } @Override public void revokeSession(String user) { - String sql = "UPDATE " + tableName + " SET " + col.HAS_SESSION + "=? WHERE " + col.NAME + "=?;"; - try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { - pst.setInt(1, 0); - pst.setString(2, user.toLowerCase()); - pst.executeUpdate(); - } catch (SQLException ex) { - logSqlException(ex); - } + columnsHandler.update(user, AuthMeColumns.HAS_SESSION, 0); } @Override public void purgeLogged() { - String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE " + col.IS_LOGGED + "=?;"; - try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { - pst.setInt(1, 0); - pst.setInt(2, 1); - pst.executeUpdate(); - } catch (SQLException ex) { - logSqlException(ex); - } + columnsHandler.update(eq(AuthMeColumns.IS_LOGGED, 1), AuthMeColumns.IS_LOGGED, 0); } @Override @@ -545,19 +506,13 @@ public class MySQL implements DataSource { } @Override - public DataSourceResult getEmail(String user) { - String sql = "SELECT " + col.EMAIL + " FROM " + tableName + " WHERE " + col.NAME + "=?;"; - try (Connection con = getConnection(); PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, user); - try (ResultSet rs = pst.executeQuery()) { - if (rs.next()) { - return DataSourceResult.of(rs.getString(1)); - } - } - } catch (SQLException ex) { - logSqlException(ex); + public DataSourceValue getEmail(String user) { + try { + return columnsHandler.retrieve(user, AuthMeColumns.EMAIL); + } catch (SQLException e) { + logSqlException(e); + return DataSourceValueImpl.unknownRow(); } - return DataSourceResult.unknownPlayer(); } @Override diff --git a/src/main/java/fr/xephi/authme/datasource/SQLite.java b/src/main/java/fr/xephi/authme/datasource/SQLite.java index 01f13a50..5c78e0bb 100644 --- a/src/main/java/fr/xephi/authme/datasource/SQLite.java +++ b/src/main/java/fr/xephi/authme/datasource/SQLite.java @@ -1,5 +1,7 @@ package fr.xephi.authme.datasource; +import ch.jalu.datasourcecolumns.data.DataSourceValue; +import ch.jalu.datasourcecolumns.data.DataSourceValueImpl; import ch.jalu.datasourcecolumns.data.DataSourceValues; import ch.jalu.datasourcecolumns.predicate.AlwaysTruePredicate; import com.google.common.annotations.VisibleForTesting; @@ -408,54 +410,28 @@ public class SQLite implements DataSource { @Override public boolean hasSession(String user) { - String sql = "SELECT " + col.HAS_SESSION + " FROM " + tableName + " WHERE LOWER(" + col.NAME + ")=?;"; - try (PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, user.toLowerCase()); - try (ResultSet rs = pst.executeQuery()) { - if (rs.next()) { - return rs.getInt(col.HAS_SESSION) == 1; - } - } - } catch (SQLException ex) { - logSqlException(ex); + try { + DataSourceValue result = columnsHandler.retrieve(user, AuthMeColumns.HAS_SESSION); + return result.rowExists() && Integer.valueOf(1).equals(result.getValue()); + } catch (SQLException e) { + logSqlException(e); + return false; } - return false; } @Override public void grantSession(String user) { - String sql = "UPDATE " + tableName + " SET " + col.HAS_SESSION + "=? WHERE LOWER(" + col.NAME + ")=?;"; - try (PreparedStatement pst = con.prepareStatement(sql)) { - pst.setInt(1, 1); - pst.setString(2, user.toLowerCase()); - pst.executeUpdate(); - } catch (SQLException ex) { - logSqlException(ex); - } + columnsHandler.update(user, AuthMeColumns.HAS_SESSION, 1); } @Override public void revokeSession(String user) { - String sql = "UPDATE " + tableName + " SET " + col.HAS_SESSION + "=? WHERE LOWER(" + col.NAME + ")=?;"; - try (PreparedStatement pst = con.prepareStatement(sql)) { - pst.setInt(1, 0); - pst.setString(2, user.toLowerCase()); - pst.executeUpdate(); - } catch (SQLException ex) { - logSqlException(ex); - } + columnsHandler.update(user, AuthMeColumns.HAS_SESSION, 0); } @Override public void purgeLogged() { - String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE " + col.IS_LOGGED + "=?;"; - try (PreparedStatement pst = con.prepareStatement(sql)) { - pst.setInt(1, 0); - pst.setInt(2, 1); - pst.executeUpdate(); - } catch (SQLException ex) { - logSqlException(ex); - } + columnsHandler.update(eq(AuthMeColumns.IS_LOGGED, 1), AuthMeColumns.IS_LOGGED, 0); } @Override @@ -469,19 +445,13 @@ public class SQLite implements DataSource { } @Override - public DataSourceResult getEmail(String user) { - String sql = "SELECT " + col.EMAIL + " FROM " + tableName + " WHERE " + col.NAME + "=?;"; - try (PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, user); - try (ResultSet rs = pst.executeQuery()) { - if (rs.next()) { - return DataSourceResult.of(rs.getString(1)); - } - } - } catch (SQLException ex) { - logSqlException(ex); + public DataSourceValue getEmail(String user) { + try { + return columnsHandler.retrieve(user, AuthMeColumns.EMAIL); + } catch (SQLException e) { + logSqlException(e); + return DataSourceValueImpl.unknownRow(); } - return DataSourceResult.unknownPlayer(); } @Override diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumns.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumns.java index 109ea5a6..5c235095 100644 --- a/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumns.java +++ b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumns.java @@ -1,13 +1,8 @@ package fr.xephi.authme.datasource.columnshandler; -import ch.jalu.configme.properties.Property; -import ch.jalu.datasourcecolumns.ColumnType; -import ch.jalu.datasourcecolumns.DependentColumn; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.settings.properties.DatabaseSettings; -import java.util.function.Function; - import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.ColumnOptions.DEFAULT_FOR_NULL; import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.ColumnOptions.OPTIONAL; import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createDouble; @@ -17,104 +12,71 @@ import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.cre import static fr.xephi.authme.datasource.columnshandler.AuthMeColumnsFactory.createString; /** - * Column definitions for the AuthMe table. - * - * @param the column type - * @see PlayerAuth + * Contains column definitions for the AuthMe table. */ -public final class AuthMeColumns implements DependentColumn { +public final class AuthMeColumns { - public static final AuthMeColumns NAME = createString( + public static final PlayerAuthColumn NAME = createString( DatabaseSettings.MYSQL_COL_NAME, PlayerAuth::getNickname); - public static final AuthMeColumns NICK_NAME = createString( + public static final PlayerAuthColumn NICK_NAME = createString( DatabaseSettings.MYSQL_COL_REALNAME, PlayerAuth::getRealName); - public static final AuthMeColumns PASSWORD = createString( + public static final PlayerAuthColumn PASSWORD = createString( DatabaseSettings.MYSQL_COL_PASSWORD, auth -> auth.getPassword().getHash()); - public static final AuthMeColumns SALT = createString( + public static final PlayerAuthColumn SALT = createString( DatabaseSettings.MYSQL_COL_SALT, auth -> auth.getPassword().getSalt(), OPTIONAL); - public static final AuthMeColumns EMAIL = createString( + public static final PlayerAuthColumn EMAIL = createString( DatabaseSettings.MYSQL_COL_EMAIL, PlayerAuth::getEmail, DEFAULT_FOR_NULL); - public static final AuthMeColumns LAST_IP = createString( + public static final PlayerAuthColumn LAST_IP = createString( DatabaseSettings.MYSQL_COL_LAST_IP, PlayerAuth::getLastIp); - public static final AuthMeColumns GROUP_ID = createInteger( + public static final PlayerAuthColumn GROUP_ID = createInteger( DatabaseSettings.MYSQL_COL_GROUP, PlayerAuth::getGroupId, OPTIONAL); - public static final AuthMeColumns LAST_LOGIN = createLong( + public static final PlayerAuthColumn LAST_LOGIN = createLong( DatabaseSettings.MYSQL_COL_LASTLOGIN, PlayerAuth::getLastLogin); - public static final AuthMeColumns REGISTRATION_IP = createString( + public static final PlayerAuthColumn REGISTRATION_IP = createString( DatabaseSettings.MYSQL_COL_REGISTER_IP, PlayerAuth::getRegistrationIp); - public static final AuthMeColumns REGISTRATION_DATE = createLong( + public static final PlayerAuthColumn REGISTRATION_DATE = createLong( DatabaseSettings.MYSQL_COL_REGISTER_DATE, PlayerAuth::getRegistrationDate); - public static final AuthMeColumns LOCATION_X = createDouble( + // -------- + // Location columns + // -------- + public static final PlayerAuthColumn LOCATION_X = createDouble( DatabaseSettings.MYSQL_COL_LASTLOC_X, PlayerAuth::getQuitLocX); - public static final AuthMeColumns LOCATION_Y = createDouble( + public static final PlayerAuthColumn LOCATION_Y = createDouble( DatabaseSettings.MYSQL_COL_LASTLOC_Y, PlayerAuth::getQuitLocY); - public static final AuthMeColumns LOCATION_Z = createDouble( + public static final PlayerAuthColumn LOCATION_Z = createDouble( DatabaseSettings.MYSQL_COL_LASTLOC_Z, PlayerAuth::getQuitLocZ); - public static final AuthMeColumns LOCATION_WORLD = createString( + public static final PlayerAuthColumn LOCATION_WORLD = createString( DatabaseSettings.MYSQL_COL_LASTLOC_WORLD, PlayerAuth::getWorld); - public static final AuthMeColumns LOCATION_YAW = createFloat( + public static final PlayerAuthColumn LOCATION_YAW = createFloat( DatabaseSettings.MYSQL_COL_LASTLOC_YAW, PlayerAuth::getYaw); - public static final AuthMeColumns LOCATION_PITCH = createFloat( + public static final PlayerAuthColumn LOCATION_PITCH = createFloat( DatabaseSettings.MYSQL_COL_LASTLOC_PITCH, PlayerAuth::getPitch); + // -------- + // Columns not on PlayerAuth + // -------- + public static final DataSourceColumn IS_LOGGED = createInteger( + DatabaseSettings.MYSQL_COL_ISLOGGED); - private final ColumnType columnType; - private final Property nameProperty; - private final Function playerAuthGetter; - private final boolean isOptional; - private final boolean useDefaultForNull; - - AuthMeColumns(ColumnType type, Property nameProperty, Function playerAuthGetter, - boolean isOptional, boolean useDefaultForNull) { - this.columnType = type; - this.nameProperty = nameProperty; - this.playerAuthGetter = playerAuthGetter; - this.isOptional = isOptional; - this.useDefaultForNull = useDefaultForNull; - } + public static final DataSourceColumn HAS_SESSION = createInteger( + DatabaseSettings.MYSQL_COL_HASSESSION); - public Property getNameProperty() { - return nameProperty; - } - - @Override - public T getValueFromDependent(PlayerAuth playerAuth) { - return playerAuthGetter.apply(playerAuth); - } - - @Override - public String resolveName(ColumnContext columnContext) { - return columnContext.getName(this); - } - - @Override - public ColumnType getType() { - return columnType; - } - - @Override - public boolean isColumnUsed(ColumnContext columnContext) { - return !isOptional || !resolveName(columnContext).isEmpty(); - } - - @Override - public boolean useDefaultForNullValue(ColumnContext columnContext) { - return useDefaultForNull && columnContext.hasDefaultSupport(); + private AuthMeColumns() { } } diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsFactory.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsFactory.java index 420a26bc..3400f76c 100644 --- a/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsFactory.java +++ b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsFactory.java @@ -8,46 +8,53 @@ import fr.xephi.authme.data.auth.PlayerAuth; import java.util.function.Function; /** - * Util class for initializing {@link AuthMeColumns} constants. + * Util class for initializing {@link DataSourceColumn} objects. */ final class AuthMeColumnsFactory { private AuthMeColumnsFactory() { } - static AuthMeColumns createInteger(Property nameProperty, - Function playerAuthGetter, - ColumnOptions... options) { + static DataSourceColumn createInteger(Property nameProperty, + ColumnOptions... options) { + return new DataSourceColumn<>(StandardTypes.INTEGER, nameProperty, + isOptional(options), hasDefaultForNull(options)); + } + + static PlayerAuthColumn createInteger(Property nameProperty, + Function playerAuthGetter, + ColumnOptions... options) { return createInternal(StandardTypes.INTEGER, nameProperty, playerAuthGetter, options); } - static AuthMeColumns createLong(Property nameProperty, - Function playerAuthGetter, - ColumnOptions... options) { + static PlayerAuthColumn createLong(Property nameProperty, + Function playerAuthGetter, + ColumnOptions... options) { return createInternal(StandardTypes.LONG, nameProperty, playerAuthGetter, options); } - static AuthMeColumns createString(Property nameProperty, - Function playerAuthGetter, - ColumnOptions... options) { + static PlayerAuthColumn createString(Property nameProperty, + Function playerAuthGetter, + ColumnOptions... options) { return createInternal(StandardTypes.STRING, nameProperty, playerAuthGetter, options); } - static AuthMeColumns createDouble(Property nameProperty, - Function playerAuthGetter, - ColumnOptions... options) { + static PlayerAuthColumn createDouble(Property nameProperty, + Function playerAuthGetter, + ColumnOptions... options) { return createInternal(StandardTypes.DOUBLE, nameProperty, playerAuthGetter, options); } - static AuthMeColumns createFloat(Property nameProperty, - Function playerAuthGetter, - ColumnOptions... options) { + static PlayerAuthColumn createFloat(Property nameProperty, + Function playerAuthGetter, + ColumnOptions... options) { return createInternal(StandardTypes.FLOAT, nameProperty, playerAuthGetter, options); } - private static AuthMeColumns createInternal(ColumnType type, Property nameProperty, - Function authGetter, ColumnOptions... options) { - return new AuthMeColumns<>(type, nameProperty, authGetter, isOptional(options), hasDefaultForNull(options)); + private static PlayerAuthColumn createInternal(ColumnType type, Property nameProperty, + Function authGetter, + ColumnOptions... options) { + return new PlayerAuthColumn<>(type, nameProperty, isOptional(options), hasDefaultForNull(options), authGetter); } private static boolean isOptional(ColumnOptions[] options) { diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java index 08202e39..7b665712 100644 --- a/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java +++ b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java @@ -75,7 +75,7 @@ public final class AuthMeColumnsHandler { * @param the column type * @return true upon success, false otherwise */ - public boolean update(String name, AuthMeColumns column, T value) { + public boolean update(String name, DataSourceColumn column, T value) { try { return internalHandler.update(name, column, value); } catch (SQLException e) { @@ -91,7 +91,7 @@ public final class AuthMeColumnsHandler { * @param columns the columns to update in the row * @return true upon success, false otherwise */ - public boolean update(PlayerAuth auth, AuthMeColumns... columns) { + public boolean update(PlayerAuth auth, PlayerAuthColumn... columns) { try { return internalHandler.update(auth.getNickname(), auth, columns); } catch (SQLException e) { @@ -116,6 +116,24 @@ public final class AuthMeColumnsHandler { } } + /** + * Sets the given value to the provided column for all rows which match the predicate. + * + * @param predicate the predicate to filter rows by + * @param column the column to modify on the matched rows + * @param value the new value to set + * @param the column type + * @return number of modified rows + */ + public int update(Predicate predicate, DataSourceColumn column, T value) { + try { + return internalHandler.update(predicate, column, value); + } catch (SQLException e) { + logSqlException(e); + return 0; + } + } + /** * Retrieves the given column from a given row. * @@ -124,7 +142,7 @@ public final class AuthMeColumnsHandler { * @param the column type * @return the result of the lookup */ - public DataSourceValue retrieve(String name, AuthMeColumns column) throws SQLException { + public DataSourceValue retrieve(String name, DataSourceColumn column) throws SQLException { return internalHandler.retrieve(name.toLowerCase(), column); } @@ -135,7 +153,7 @@ public final class AuthMeColumnsHandler { * @param columns the columns to retrieve * @return map-like object with the requested values */ - public DataSourceValues retrieve(String name, AuthMeColumns... columns) throws SQLException { + public DataSourceValues retrieve(String name, DataSourceColumn... columns) throws SQLException { return internalHandler.retrieve(name.toLowerCase(), columns); } @@ -147,7 +165,7 @@ public final class AuthMeColumnsHandler { * @param the column's value type * @return the values of the matching rows */ - public List retrieve(Predicate predicate, AuthMeColumns column) throws SQLException { + public List retrieve(Predicate predicate, DataSourceColumn column) throws SQLException { return internalHandler.retrieve(predicate, column); } @@ -158,7 +176,7 @@ public final class AuthMeColumnsHandler { * @param columns the columns to insert * @return true upon success, false otherwise */ - public boolean insert(PlayerAuth auth, AuthMeColumns... columns) { + public boolean insert(PlayerAuth auth, PlayerAuthColumn... columns) { try { return internalHandler.insert(auth, columns); } catch (SQLException e) { diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/ColumnContext.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/ColumnContext.java index 8759935d..554266a0 100644 --- a/src/main/java/fr/xephi/authme/datasource/columnshandler/ColumnContext.java +++ b/src/main/java/fr/xephi/authme/datasource/columnshandler/ColumnContext.java @@ -11,7 +11,7 @@ import java.util.Map; public class ColumnContext { private final Settings settings; - private final Map, String> columnNames = new HashMap<>(); + private final Map, String> columnNames = new HashMap<>(); private final boolean hasDefaultSupport; /** @@ -25,7 +25,7 @@ public class ColumnContext { this.hasDefaultSupport = hasDefaultSupport; } - public String getName(AuthMeColumns column) { + public String getName(DataSourceColumn column) { return columnNames.computeIfAbsent(column, k -> settings.getProperty(k.getNameProperty())); } diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/DataSourceColumn.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/DataSourceColumn.java new file mode 100644 index 00000000..4b7fa4ca --- /dev/null +++ b/src/main/java/fr/xephi/authme/datasource/columnshandler/DataSourceColumn.java @@ -0,0 +1,57 @@ +package fr.xephi.authme.datasource.columnshandler; + +import ch.jalu.configme.properties.Property; +import ch.jalu.datasourcecolumns.Column; +import ch.jalu.datasourcecolumns.ColumnType; + +/** + * Basic {@link Column} implementation for AuthMe. + * + * @param column type + */ +public class DataSourceColumn implements Column { + + private final ColumnType columnType; + private final Property nameProperty; + private final boolean isOptional; + private final boolean useDefaultForNull; + + /** + * Constructor. + * + * @param type type of the column + * @param nameProperty property defining the column name + * @param isOptional whether or not the column can be skipped (if name is configured to empty string) + * @param useDefaultForNull whether SQL DEFAULT should be used for null values (if supported by the database) + */ + DataSourceColumn(ColumnType type, Property nameProperty, boolean isOptional, boolean useDefaultForNull) { + this.columnType = type; + this.nameProperty = nameProperty; + this.isOptional = isOptional; + this.useDefaultForNull = useDefaultForNull; + } + + public Property getNameProperty() { + return nameProperty; + } + + @Override + public String resolveName(ColumnContext columnContext) { + return columnContext.getName(this); + } + + @Override + public ColumnType getType() { + return columnType; + } + + @Override + public boolean isColumnUsed(ColumnContext columnContext) { + return !isOptional || !resolveName(columnContext).isEmpty(); + } + + @Override + public boolean useDefaultForNullValue(ColumnContext columnContext) { + return useDefaultForNull && columnContext.hasDefaultSupport(); + } +} diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/PlayerAuthColumn.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/PlayerAuthColumn.java new file mode 100644 index 00000000..43d022b1 --- /dev/null +++ b/src/main/java/fr/xephi/authme/datasource/columnshandler/PlayerAuthColumn.java @@ -0,0 +1,32 @@ +package fr.xephi.authme.datasource.columnshandler; + +import ch.jalu.configme.properties.Property; +import ch.jalu.datasourcecolumns.ColumnType; +import ch.jalu.datasourcecolumns.DependentColumn; +import fr.xephi.authme.data.auth.PlayerAuth; + +import java.util.function.Function; + +/** + * Implementation for columns which can also be retrieved from a {@link PlayerAuth} object. + * + * @param column type + */ +public class PlayerAuthColumn extends DataSourceColumn implements DependentColumn { + + private final Function playerAuthGetter; + + /* + * Constructor. See parent class for details. + */ + PlayerAuthColumn(ColumnType type, Property nameProperty, boolean isOptional, boolean useDefaultForNull, + Function playerAuthGetter) { + super(type, nameProperty, isOptional, useDefaultForNull); + this.playerAuthGetter = playerAuthGetter; + } + + @Override + public T getValueFromDependent(PlayerAuth auth) { + return playerAuthGetter.apply(auth); + } +} diff --git a/src/test/java/fr/xephi/authme/ClassesConsistencyTest.java b/src/test/java/fr/xephi/authme/ClassesConsistencyTest.java index c7f09398..eb00d14e 100644 --- a/src/test/java/fr/xephi/authme/ClassesConsistencyTest.java +++ b/src/test/java/fr/xephi/authme/ClassesConsistencyTest.java @@ -6,7 +6,8 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import fr.xephi.authme.data.captcha.CaptchaCodeStorage; import fr.xephi.authme.datasource.Columns; -import fr.xephi.authme.datasource.columnshandler.AuthMeColumns; +import fr.xephi.authme.datasource.columnshandler.DataSourceColumn; +import fr.xephi.authme.datasource.columnshandler.PlayerAuthColumn; import fr.xephi.authme.datasource.mysqlextensions.MySqlExtension; import fr.xephi.authme.initialization.HasCleanup; import fr.xephi.authme.process.register.executors.RegistrationMethod; @@ -53,7 +54,7 @@ public class ClassesConsistencyTest { int.class, long.class, float.class, String.class, File.class, Enum.class, collectionsUnmodifiableList(), Charset.class, /* AuthMe */ - Property.class, RegistrationMethod.class, AuthMeColumns.class, + Property.class, RegistrationMethod.class, DataSourceColumn.class, PlayerAuthColumn.class, /* Guava */ ImmutableMap.class, ImmutableList.class); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/GetEmailCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/GetEmailCommandTest.java index 07c92d13..dc3fda40 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/GetEmailCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/GetEmailCommandTest.java @@ -1,7 +1,7 @@ package fr.xephi.authme.command.executable.authme; +import ch.jalu.datasourcecolumns.data.DataSourceValueImpl; import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.datasource.DataSourceResult; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.service.CommonService; import org.bukkit.command.CommandSender; @@ -38,7 +38,7 @@ public class GetEmailCommandTest { public void shouldReportUnknownUser() { // given String user = "myTestUser"; - given(dataSource.getEmail(user)).willReturn(DataSourceResult.unknownPlayer()); + given(dataSource.getEmail(user)).willReturn(DataSourceValueImpl.unknownRow()); CommandSender sender = mock(CommandSender.class); // when @@ -53,7 +53,7 @@ public class GetEmailCommandTest { // given String user = "userToView"; String email = "user.email@example.org"; - given(dataSource.getEmail(user)).willReturn(DataSourceResult.of(email)); + given(dataSource.getEmail(user)).willReturn(DataSourceValueImpl.of(email)); CommandSender sender = mock(CommandSender.class); // when diff --git a/src/test/java/fr/xephi/authme/command/executable/email/RecoverEmailCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/email/RecoverEmailCommandTest.java index 416649e0..5842ec30 100644 --- a/src/test/java/fr/xephi/authme/command/executable/email/RecoverEmailCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/email/RecoverEmailCommandTest.java @@ -1,12 +1,12 @@ package fr.xephi.authme.command.executable.email; +import ch.jalu.datasourcecolumns.data.DataSourceValueImpl; import ch.jalu.injector.testing.BeforeInjecting; import ch.jalu.injector.testing.DelayedInjectionRunner; import ch.jalu.injector.testing.InjectDelayed; import fr.xephi.authme.TestHelper; import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.datasource.DataSourceResult; import fr.xephi.authme.mail.EmailService; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.security.PasswordSecurity; @@ -118,7 +118,7 @@ public class RecoverEmailCommandTest { given(sender.getName()).willReturn(name); given(emailService.hasAllInformation()).willReturn(true); given(playerCache.isAuthenticated(name)).willReturn(false); - given(dataSource.getEmail(name)).willReturn(DataSourceResult.unknownPlayer()); + given(dataSource.getEmail(name)).willReturn(DataSourceValueImpl.unknownRow()); // when command.executeCommand(sender, Collections.singletonList("someone@example.com")); @@ -138,7 +138,7 @@ public class RecoverEmailCommandTest { given(sender.getName()).willReturn(name); given(emailService.hasAllInformation()).willReturn(true); given(playerCache.isAuthenticated(name)).willReturn(false); - given(dataSource.getEmail(name)).willReturn(DataSourceResult.of(DEFAULT_EMAIL)); + given(dataSource.getEmail(name)).willReturn(DataSourceValueImpl.of(DEFAULT_EMAIL)); // when command.executeCommand(sender, Collections.singletonList(DEFAULT_EMAIL)); @@ -158,7 +158,7 @@ public class RecoverEmailCommandTest { given(sender.getName()).willReturn(name); given(emailService.hasAllInformation()).willReturn(true); given(playerCache.isAuthenticated(name)).willReturn(false); - given(dataSource.getEmail(name)).willReturn(DataSourceResult.of("raptor@example.org")); + given(dataSource.getEmail(name)).willReturn(DataSourceValueImpl.of("raptor@example.org")); // when command.executeCommand(sender, Collections.singletonList("wrong-email@example.com")); @@ -180,7 +180,7 @@ public class RecoverEmailCommandTest { given(emailService.sendRecoveryCode(anyString(), anyString(), anyString())).willReturn(true); given(playerCache.isAuthenticated(name)).willReturn(false); String email = "v@example.com"; - given(dataSource.getEmail(name)).willReturn(DataSourceResult.of(email)); + given(dataSource.getEmail(name)).willReturn(DataSourceValueImpl.of(email)); String code = "a94f37"; given(recoveryCodeService.isRecoveryCodeNeeded()).willReturn(true); given(recoveryCodeService.generateCode(name)).willReturn(code); @@ -205,7 +205,7 @@ public class RecoverEmailCommandTest { given(emailService.sendPasswordMail(anyString(), anyString(), anyString())).willReturn(true); given(playerCache.isAuthenticated(name)).willReturn(false); String email = "vulture@example.com"; - given(dataSource.getEmail(name)).willReturn(DataSourceResult.of(email)); + given(dataSource.getEmail(name)).willReturn(DataSourceValueImpl.of(email)); given(recoveryCodeService.isRecoveryCodeNeeded()).willReturn(false); setBukkitServiceToRunTaskAsynchronously(bukkitService); diff --git a/src/test/java/fr/xephi/authme/data/VerificationCodeManagerTest.java b/src/test/java/fr/xephi/authme/data/VerificationCodeManagerTest.java index b99d6a37..732b1ad7 100644 --- a/src/test/java/fr/xephi/authme/data/VerificationCodeManagerTest.java +++ b/src/test/java/fr/xephi/authme/data/VerificationCodeManagerTest.java @@ -1,7 +1,7 @@ package fr.xephi.authme.data; +import ch.jalu.datasourcecolumns.data.DataSourceValueImpl; import fr.xephi.authme.datasource.DataSource; -import fr.xephi.authme.datasource.DataSourceResult; import fr.xephi.authme.mail.EmailService; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.permission.PlayerPermission; @@ -51,7 +51,7 @@ public class VerificationCodeManagerTest { // given String name1 = "ILoveTests"; Player player1 = mockPlayerWithName(name1); - given(dataSource.getEmail(name1)).willReturn(DataSourceResult.of("ilovetests@test.com")); + given(dataSource.getEmail(name1)).willReturn(DataSourceValueImpl.of("ilovetests@test.com")); given(permissionsManager.hasPermission(player1, PlayerPermission.VERIFICATION_CODE)).willReturn(true); String name2 = "StillLovingTests"; Player player2 = mockPlayerWithName(name2); @@ -106,7 +106,7 @@ public class VerificationCodeManagerTest { // given String player = "ILoveTests"; String email = "ilovetests@test.com"; - given(dataSource.getEmail(player)).willReturn(DataSourceResult.of(email)); + given(dataSource.getEmail(player)).willReturn(DataSourceValueImpl.of(email)); VerificationCodeManager codeManager1 = createCodeManager(); VerificationCodeManager codeManager2 = createCodeManager(); codeManager2.codeExistOrGenerateNew(player); @@ -125,7 +125,7 @@ public class VerificationCodeManagerTest { // given String player = "ILoveTests"; String email = "ilovetests@test.com"; - given(dataSource.getEmail(player)).willReturn(DataSourceResult.of(email)); + given(dataSource.getEmail(player)).willReturn(DataSourceValueImpl.of(email)); VerificationCodeManager codeManager1 = createCodeManager(); VerificationCodeManager codeManager2 = createCodeManager(); codeManager2.codeExistOrGenerateNew(player); @@ -145,7 +145,7 @@ public class VerificationCodeManagerTest { String player = "ILoveTests"; String code = "193458"; String email = "ilovetests@test.com"; - given(dataSource.getEmail(player)).willReturn(DataSourceResult.of(email)); + given(dataSource.getEmail(player)).willReturn(DataSourceValueImpl.of(email)); VerificationCodeManager codeManager1 = createCodeManager(); VerificationCodeManager codeManager2 = createCodeManager(); codeManager1.codeExistOrGenerateNew(player); diff --git a/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java b/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java index 70df2d48..8efee23b 100644 --- a/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/datasource/AbstractDataSourceIntegrationTest.java @@ -1,5 +1,7 @@ package fr.xephi.authme.datasource; +import ch.jalu.datasourcecolumns.data.DataSourceValue; +import ch.jalu.datasourcecolumns.data.DataSourceValueImpl; import com.google.common.collect.Lists; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.security.crypts.HashedPassword; @@ -420,12 +422,12 @@ public abstract class AbstractDataSourceIntegrationTest { DataSource dataSource = getDataSource(); // when - DataSourceResult email1 = dataSource.getEmail(user1); - DataSourceResult email2 = dataSource.getEmail(user2); + DataSourceValue email1 = dataSource.getEmail(user1); + DataSourceValue email2 = dataSource.getEmail(user2); // then assertThat(email1.getValue(), equalTo("user@example.org")); - assertThat(email2, is(DataSourceResult.unknownPlayer())); + assertThat(email2, is(DataSourceValueImpl.unknownRow())); } @Test From 26472b6be32e924813f61841b90087ae46a2ea66 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 25 Mar 2018 22:27:44 +0200 Subject: [PATCH 12/63] #1539 Create common parent for SQLite and MySQL --- .../datasource/AbstractSqlDataSource.java | 169 +++++++++++++++++ .../fr/xephi/authme/datasource/MySQL.java | 151 +-------------- .../fr/xephi/authme/datasource/SQLite.java | 174 +----------------- .../xephi/authme/ClassesConsistencyTest.java | 2 + 4 files changed, 175 insertions(+), 321 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/datasource/AbstractSqlDataSource.java diff --git a/src/main/java/fr/xephi/authme/datasource/AbstractSqlDataSource.java b/src/main/java/fr/xephi/authme/datasource/AbstractSqlDataSource.java new file mode 100644 index 00000000..0c40c479 --- /dev/null +++ b/src/main/java/fr/xephi/authme/datasource/AbstractSqlDataSource.java @@ -0,0 +1,169 @@ +package fr.xephi.authme.datasource; + +import ch.jalu.datasourcecolumns.data.DataSourceValue; +import ch.jalu.datasourcecolumns.data.DataSourceValueImpl; +import ch.jalu.datasourcecolumns.data.DataSourceValues; +import ch.jalu.datasourcecolumns.predicate.AlwaysTruePredicate; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.columnshandler.AuthMeColumns; +import fr.xephi.authme.datasource.columnshandler.AuthMeColumnsHandler; +import fr.xephi.authme.security.crypts.HashedPassword; + +import java.sql.SQLException; +import java.util.Collections; +import java.util.List; + +import static ch.jalu.datasourcecolumns.data.UpdateValues.with; +import static ch.jalu.datasourcecolumns.predicate.StandardPredicates.eq; +import static ch.jalu.datasourcecolumns.predicate.StandardPredicates.eqIgnoreCase; +import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException; + +/** + * Common type for SQL-based data sources. Classes implementing this + * must ensure that {@link #columnsHandler} is initialized on creation. + */ +public abstract class AbstractSqlDataSource implements DataSource { + + protected AuthMeColumnsHandler columnsHandler; + + @Override + public boolean isAuthAvailable(String user) { + try { + return columnsHandler.retrieve(user, AuthMeColumns.NAME).rowExists(); + } catch (SQLException e) { + logSqlException(e); + return false; + } + } + + @Override + public HashedPassword getPassword(String user) { + try { + DataSourceValues values = columnsHandler.retrieve(user, AuthMeColumns.PASSWORD, AuthMeColumns.SALT); + if (values.rowExists()) { + return new HashedPassword(values.get(AuthMeColumns.PASSWORD), values.get(AuthMeColumns.SALT)); + } + } catch (SQLException e) { + logSqlException(e); + } + return null; + } + + @Override + public boolean saveAuth(PlayerAuth auth) { + return columnsHandler.insert(auth, + AuthMeColumns.NAME, AuthMeColumns.NICK_NAME, AuthMeColumns.PASSWORD, AuthMeColumns.SALT, + AuthMeColumns.EMAIL, AuthMeColumns.REGISTRATION_DATE, AuthMeColumns.REGISTRATION_IP); + } + + @Override + public boolean hasSession(String user) { + try { + DataSourceValue result = columnsHandler.retrieve(user, AuthMeColumns.HAS_SESSION); + return result.rowExists() && Integer.valueOf(1).equals(result.getValue()); + } catch (SQLException e) { + logSqlException(e); + return false; + } + } + + @Override + public boolean updateSession(PlayerAuth auth) { + return columnsHandler.update(auth, AuthMeColumns.LAST_IP, AuthMeColumns.LAST_LOGIN, AuthMeColumns.NICK_NAME); + } + + @Override + public boolean updatePassword(PlayerAuth auth) { + return updatePassword(auth.getNickname(), auth.getPassword()); + } + + @Override + public boolean updatePassword(String user, HashedPassword password) { + return columnsHandler.update(user, + with(AuthMeColumns.PASSWORD, password.getHash()) + .and(AuthMeColumns.SALT, password.getSalt()).build()); + } + + @Override + public boolean updateQuitLoc(PlayerAuth auth) { + return columnsHandler.update(auth, + AuthMeColumns.LOCATION_X, AuthMeColumns.LOCATION_Y, AuthMeColumns.LOCATION_Z, + AuthMeColumns.LOCATION_WORLD, AuthMeColumns.LOCATION_YAW, AuthMeColumns.LOCATION_PITCH); + } + + @Override + public List getAllAuthsByIp(String ip) { + try { + return columnsHandler.retrieve(eq(AuthMeColumns.LAST_IP, ip), AuthMeColumns.NAME); + } catch (SQLException e) { + logSqlException(e); + return Collections.emptyList(); + } + } + + @Override + public int countAuthsByEmail(String email) { + return columnsHandler.count(eqIgnoreCase(AuthMeColumns.EMAIL, email)); + } + + @Override + public boolean updateEmail(PlayerAuth auth) { + return columnsHandler.update(auth, AuthMeColumns.EMAIL); + } + + @Override + public boolean isLogged(String user) { + try { + DataSourceValue result = columnsHandler.retrieve(user, AuthMeColumns.IS_LOGGED); + return result.rowExists() && Integer.valueOf(1).equals(result.getValue()); + } catch (SQLException e) { + logSqlException(e); + return false; + } + } + + @Override + public void setLogged(String user) { + columnsHandler.update(user, AuthMeColumns.IS_LOGGED, 1); + } + + @Override + public void setUnlogged(String user) { + columnsHandler.update(user, AuthMeColumns.IS_LOGGED, 0); + } + + @Override + public void grantSession(String user) { + columnsHandler.update(user, AuthMeColumns.HAS_SESSION, 1); + } + + @Override + public void revokeSession(String user) { + columnsHandler.update(user, AuthMeColumns.HAS_SESSION, 0); + } + + @Override + public void purgeLogged() { + columnsHandler.update(eq(AuthMeColumns.IS_LOGGED, 1), AuthMeColumns.IS_LOGGED, 0); + } + + @Override + public int getAccountsRegistered() { + return columnsHandler.count(new AlwaysTruePredicate<>()); + } + + @Override + public boolean updateRealName(String user, String realName) { + return columnsHandler.update(user, AuthMeColumns.NICK_NAME, realName); + } + + @Override + public DataSourceValue getEmail(String user) { + try { + return columnsHandler.retrieve(user, AuthMeColumns.EMAIL); + } catch (SQLException e) { + logSqlException(e); + return DataSourceValueImpl.unknownRow(); + } + } +} diff --git a/src/main/java/fr/xephi/authme/datasource/MySQL.java b/src/main/java/fr/xephi/authme/datasource/MySQL.java index b8e487e2..9f445615 100644 --- a/src/main/java/fr/xephi/authme/datasource/MySQL.java +++ b/src/main/java/fr/xephi/authme/datasource/MySQL.java @@ -1,19 +1,13 @@ package fr.xephi.authme.datasource; -import ch.jalu.datasourcecolumns.data.DataSourceValue; -import ch.jalu.datasourcecolumns.data.DataSourceValueImpl; -import ch.jalu.datasourcecolumns.data.DataSourceValues; -import ch.jalu.datasourcecolumns.predicate.AlwaysTruePredicate; import com.google.common.annotations.VisibleForTesting; import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.pool.HikariPool.PoolInitializationException; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.auth.PlayerAuth; -import fr.xephi.authme.datasource.columnshandler.AuthMeColumns; import fr.xephi.authme.datasource.columnshandler.AuthMeColumnsHandler; import fr.xephi.authme.datasource.mysqlextensions.MySqlExtension; import fr.xephi.authme.datasource.mysqlextensions.MySqlExtensionsFactory; -import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.DatabaseSettings; import fr.xephi.authme.settings.properties.HooksSettings; @@ -26,14 +20,10 @@ import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; -import static ch.jalu.datasourcecolumns.data.UpdateValues.with; -import static ch.jalu.datasourcecolumns.predicate.StandardPredicates.eq; -import static ch.jalu.datasourcecolumns.predicate.StandardPredicates.eqIgnoreCase; import static fr.xephi.authme.datasource.SqlDataSourceUtils.getNullableLong; import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException; @@ -41,7 +31,7 @@ import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException; * MySQL data source. */ @SuppressWarnings({"checkstyle:AbbreviationAsWordInName"}) // Justification: Class name cannot be changed anymore -public class MySQL implements DataSource { +public class MySQL extends AbstractSqlDataSource { private boolean useSsl; private String host; @@ -54,7 +44,6 @@ public class MySQL implements DataSource { private int maxLifetime; private List columnOthers; private Columns col; - private AuthMeColumnsHandler columnsHandler; private MySqlExtension sqlExtension; private HikariDataSource ds; @@ -273,29 +262,6 @@ public class MySQL implements DataSource { } } - @Override - public boolean isAuthAvailable(String user) { - try { - return columnsHandler.retrieve(user, AuthMeColumns.NAME).rowExists(); - } catch (SQLException e) { - logSqlException(e); - return false; - } - } - - @Override - public HashedPassword getPassword(String user) { - try { - DataSourceValues values = columnsHandler.retrieve(user, AuthMeColumns.PASSWORD, AuthMeColumns.SALT); - if (values.rowExists()) { - return new HashedPassword(values.get(AuthMeColumns.PASSWORD), values.get(AuthMeColumns.SALT)); - } - } catch (SQLException e) { - logSqlException(e); - } - return null; - } - @Override public PlayerAuth getAuth(String user) { String sql = "SELECT * FROM " + tableName + " WHERE " + col.NAME + "=?;"; @@ -318,9 +284,7 @@ public class MySQL implements DataSource { @Override public boolean saveAuth(PlayerAuth auth) { - columnsHandler.insert(auth, - AuthMeColumns.NAME, AuthMeColumns.NICK_NAME, AuthMeColumns.PASSWORD, AuthMeColumns.SALT, - AuthMeColumns.EMAIL, AuthMeColumns.REGISTRATION_DATE, AuthMeColumns.REGISTRATION_IP); + super.saveAuth(auth); try (Connection con = getConnection()) { if (!columnOthers.isEmpty()) { @@ -342,23 +306,6 @@ public class MySQL implements DataSource { return false; } - @Override - public boolean updatePassword(PlayerAuth auth) { - return updatePassword(auth.getNickname(), auth.getPassword()); - } - - @Override - public boolean updatePassword(String user, HashedPassword password) { - return columnsHandler.update(user, - with(AuthMeColumns.PASSWORD, password.getHash()) - .and(AuthMeColumns.SALT, password.getSalt()).build()); - } - - @Override - public boolean updateSession(PlayerAuth auth) { - return columnsHandler.update(auth, AuthMeColumns.LAST_IP, AuthMeColumns.LAST_LOGIN, AuthMeColumns.NICK_NAME); - } - @Override public Set getRecordsToPurge(long until) { Set list = new HashSet<>(); @@ -396,18 +343,6 @@ public class MySQL implements DataSource { return false; } - @Override - public boolean updateQuitLoc(PlayerAuth auth) { - return columnsHandler.update(auth, - AuthMeColumns.LOCATION_X, AuthMeColumns.LOCATION_Y, AuthMeColumns.LOCATION_Z, - AuthMeColumns.LOCATION_WORLD, AuthMeColumns.LOCATION_YAW, AuthMeColumns.LOCATION_PITCH); - } - - @Override - public boolean updateEmail(PlayerAuth auth) { - return columnsHandler.update(auth, AuthMeColumns.EMAIL); - } - @Override public void closeConnection() { if (ds != null && !ds.isClosed()) { @@ -415,21 +350,6 @@ public class MySQL implements DataSource { } } - @Override - public List getAllAuthsByIp(String ip) { - try { - return columnsHandler.retrieve(eq(AuthMeColumns.LAST_IP, ip), AuthMeColumns.NAME); - } catch (SQLException e) { - logSqlException(e); - return Collections.emptyList(); - } - } - - @Override - public int countAuthsByEmail(String email) { - return columnsHandler.count(eqIgnoreCase(AuthMeColumns.EMAIL, email)); - } - @Override public void purgeRecords(Collection toPurge) { String sql = "DELETE FROM " + tableName + " WHERE " + col.NAME + "=?;"; @@ -448,73 +368,6 @@ public class MySQL implements DataSource { return DataSourceType.MYSQL; } - @Override - public boolean isLogged(String user) { - try { - DataSourceValue result = columnsHandler.retrieve(user, AuthMeColumns.IS_LOGGED); - return result.rowExists() && Integer.valueOf(1).equals(result.getValue()); - } catch (SQLException e) { - logSqlException(e); - return false; - } - } - - @Override - public void setLogged(String user) { - columnsHandler.update(user, AuthMeColumns.IS_LOGGED, 1); - } - - @Override - public void setUnlogged(String user) { - columnsHandler.update(user, AuthMeColumns.IS_LOGGED, 0); - } - - @Override - public boolean hasSession(String user) { - try { - DataSourceValue result = columnsHandler.retrieve(user, AuthMeColumns.HAS_SESSION); - return result.rowExists() && Integer.valueOf(1).equals(result.getValue()); - } catch (SQLException e) { - logSqlException(e); - return false; - } - } - - @Override - public void grantSession(String user) { - columnsHandler.update(user, AuthMeColumns.HAS_SESSION, 1); - } - - @Override - public void revokeSession(String user) { - columnsHandler.update(user, AuthMeColumns.HAS_SESSION, 0); - } - - @Override - public void purgeLogged() { - columnsHandler.update(eq(AuthMeColumns.IS_LOGGED, 1), AuthMeColumns.IS_LOGGED, 0); - } - - @Override - public int getAccountsRegistered() { - return columnsHandler.count(new AlwaysTruePredicate<>()); - } - - @Override - public boolean updateRealName(String user, String realName) { - return columnsHandler.update(user, AuthMeColumns.NICK_NAME, realName); - } - - @Override - public DataSourceValue getEmail(String user) { - try { - return columnsHandler.retrieve(user, AuthMeColumns.EMAIL); - } catch (SQLException e) { - logSqlException(e); - return DataSourceValueImpl.unknownRow(); - } - } - @Override public List getAllAuths() { List auths = new ArrayList<>(); diff --git a/src/main/java/fr/xephi/authme/datasource/SQLite.java b/src/main/java/fr/xephi/authme/datasource/SQLite.java index 5c78e0bb..b2f639a3 100644 --- a/src/main/java/fr/xephi/authme/datasource/SQLite.java +++ b/src/main/java/fr/xephi/authme/datasource/SQLite.java @@ -1,15 +1,9 @@ package fr.xephi.authme.datasource; -import ch.jalu.datasourcecolumns.data.DataSourceValue; -import ch.jalu.datasourcecolumns.data.DataSourceValueImpl; -import ch.jalu.datasourcecolumns.data.DataSourceValues; -import ch.jalu.datasourcecolumns.predicate.AlwaysTruePredicate; import com.google.common.annotations.VisibleForTesting; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.auth.PlayerAuth; -import fr.xephi.authme.datasource.columnshandler.AuthMeColumns; import fr.xephi.authme.datasource.columnshandler.AuthMeColumnsHandler; -import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.DatabaseSettings; @@ -23,14 +17,10 @@ import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; -import static ch.jalu.datasourcecolumns.data.UpdateValues.with; -import static ch.jalu.datasourcecolumns.predicate.StandardPredicates.eq; -import static ch.jalu.datasourcecolumns.predicate.StandardPredicates.eqIgnoreCase; import static fr.xephi.authme.datasource.SqlDataSourceUtils.getNullableLong; import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException; @@ -38,7 +28,7 @@ import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException; * SQLite data source. */ @SuppressWarnings({"checkstyle:AbbreviationAsWordInName"}) // Justification: Class name cannot be changed anymore -public class SQLite implements DataSource { +public class SQLite extends AbstractSqlDataSource { private final Settings settings; private final File dataFolder; @@ -46,7 +36,6 @@ public class SQLite implements DataSource { private final String tableName; private final Columns col; private Connection con; - private AuthMeColumnsHandler columnsHandler; /** * Constructor for SQLite. @@ -71,6 +60,7 @@ public class SQLite implements DataSource { ConsoleLogger.logException("Error during SQLite initialization:", ex); throw ex; } + this.columnsHandler = AuthMeColumnsHandler.createForSqlite(con, settings); } @VisibleForTesting @@ -215,29 +205,6 @@ public class SQLite implements DataSource { } } - @Override - public boolean isAuthAvailable(String user) { - try { - return columnsHandler.retrieve(user, AuthMeColumns.NAME).rowExists(); - } catch (SQLException e) { - logSqlException(e); - return false; - } - } - - @Override - public HashedPassword getPassword(String user) { - try { - DataSourceValues values = columnsHandler.retrieve(user, AuthMeColumns.PASSWORD, AuthMeColumns.SALT); - if (values.rowExists()) { - return new HashedPassword(values.get(AuthMeColumns.PASSWORD), values.get(AuthMeColumns.SALT)); - } - } catch (SQLException e) { - logSqlException(e); - } - return null; - } - @Override public PlayerAuth getAuth(String user) { String sql = "SELECT * FROM " + tableName + " WHERE LOWER(" + col.NAME + ")=LOWER(?);"; @@ -254,30 +221,6 @@ public class SQLite implements DataSource { return null; } - @Override - public boolean saveAuth(PlayerAuth auth) { - return columnsHandler.insert(auth, - AuthMeColumns.NAME, AuthMeColumns.NICK_NAME, AuthMeColumns.PASSWORD, AuthMeColumns.SALT, - AuthMeColumns.EMAIL, AuthMeColumns.REGISTRATION_DATE, AuthMeColumns.REGISTRATION_IP); - } - - @Override - public boolean updatePassword(PlayerAuth auth) { - return updatePassword(auth.getNickname(), auth.getPassword()); - } - - @Override - public boolean updatePassword(String user, HashedPassword password) { - return columnsHandler.update(user, - with(AuthMeColumns.PASSWORD, password.getHash()) - .and(AuthMeColumns.SALT, password.getSalt()).build()); - } - - @Override - public boolean updateSession(PlayerAuth auth) { - return columnsHandler.update(auth, AuthMeColumns.LAST_IP, AuthMeColumns.LAST_LOGIN, AuthMeColumns.NICK_NAME); - } - @Override public Set getRecordsToPurge(long until) { Set list = new HashSet<>(); @@ -325,18 +268,6 @@ public class SQLite implements DataSource { return false; } - @Override - public boolean updateQuitLoc(PlayerAuth auth) { - return columnsHandler.update(auth, - AuthMeColumns.LOCATION_X, AuthMeColumns.LOCATION_Y, AuthMeColumns.LOCATION_Z, - AuthMeColumns.LOCATION_WORLD, AuthMeColumns.LOCATION_YAW, AuthMeColumns.LOCATION_PITCH); - } - - @Override - public boolean updateEmail(PlayerAuth auth) { - return columnsHandler.update(auth, AuthMeColumns.EMAIL); - } - @Override public void closeConnection() { try { @@ -348,112 +279,11 @@ public class SQLite implements DataSource { } } - @Override - public List getAllAuthsByIp(String ip) { - try { - return columnsHandler.retrieve(eq(AuthMeColumns.LAST_IP, ip), AuthMeColumns.NAME); - } catch (SQLException e) { - logSqlException(e); - return Collections.emptyList(); - } - } - - @Override - public int countAuthsByEmail(String email) { - return columnsHandler.count(eqIgnoreCase(AuthMeColumns.EMAIL, email)); - } - @Override public DataSourceType getType() { return DataSourceType.SQLITE; } - @Override - public boolean isLogged(String user) { - String sql = "SELECT " + col.IS_LOGGED + " FROM " + tableName + " WHERE LOWER(" + col.NAME + ")=?;"; - try (PreparedStatement pst = con.prepareStatement(sql)) { - pst.setString(1, user); - try (ResultSet rs = pst.executeQuery()) { - if (rs.next()) { - return rs.getInt(col.IS_LOGGED) == 1; - } - } - } catch (SQLException ex) { - logSqlException(ex); - } - return false; - } - - @Override - public void setLogged(String user) { - String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE LOWER(" + col.NAME + ")=?;"; - try (PreparedStatement pst = con.prepareStatement(sql)) { - pst.setInt(1, 1); - pst.setString(2, user); - pst.executeUpdate(); - } catch (SQLException ex) { - logSqlException(ex); - } - } - - @Override - public void setUnlogged(String user) { - String sql = "UPDATE " + tableName + " SET " + col.IS_LOGGED + "=? WHERE LOWER(" + col.NAME + ")=?;"; - try (PreparedStatement pst = con.prepareStatement(sql)) { - pst.setInt(1, 0); - pst.setString(2, user); - pst.executeUpdate(); - } catch (SQLException ex) { - logSqlException(ex); - } - } - - @Override - public boolean hasSession(String user) { - try { - DataSourceValue result = columnsHandler.retrieve(user, AuthMeColumns.HAS_SESSION); - return result.rowExists() && Integer.valueOf(1).equals(result.getValue()); - } catch (SQLException e) { - logSqlException(e); - return false; - } - } - - @Override - public void grantSession(String user) { - columnsHandler.update(user, AuthMeColumns.HAS_SESSION, 1); - } - - @Override - public void revokeSession(String user) { - columnsHandler.update(user, AuthMeColumns.HAS_SESSION, 0); - } - - @Override - public void purgeLogged() { - columnsHandler.update(eq(AuthMeColumns.IS_LOGGED, 1), AuthMeColumns.IS_LOGGED, 0); - } - - @Override - public int getAccountsRegistered() { - return columnsHandler.count(new AlwaysTruePredicate<>()); - } - - @Override - public boolean updateRealName(String user, String realName) { - return columnsHandler.update(user, AuthMeColumns.NICK_NAME, realName); - } - - @Override - public DataSourceValue getEmail(String user) { - try { - return columnsHandler.retrieve(user, AuthMeColumns.EMAIL); - } catch (SQLException e) { - logSqlException(e); - return DataSourceValueImpl.unknownRow(); - } - } - @Override public List getAllAuths() { List auths = new ArrayList<>(); diff --git a/src/test/java/fr/xephi/authme/ClassesConsistencyTest.java b/src/test/java/fr/xephi/authme/ClassesConsistencyTest.java index eb00d14e..f02b5dc9 100644 --- a/src/test/java/fr/xephi/authme/ClassesConsistencyTest.java +++ b/src/test/java/fr/xephi/authme/ClassesConsistencyTest.java @@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import fr.xephi.authme.data.captcha.CaptchaCodeStorage; +import fr.xephi.authme.datasource.AbstractSqlDataSource; import fr.xephi.authme.datasource.Columns; import fr.xephi.authme.datasource.columnshandler.DataSourceColumn; import fr.xephi.authme.datasource.columnshandler.PlayerAuthColumn; @@ -62,6 +63,7 @@ public class ClassesConsistencyTest { private static final Set> CLASSES_EXCLUDED_FROM_VISIBILITY_TEST = ImmutableSet.of( Whirlpool.class, // not our implementation, so we don't touch it MySqlExtension.class, // has immutable protected fields used by all children + AbstractSqlDataSource.class, // protected members for inheritance Columns.class // uses non-static String constants, which is safe ); From 68329a876172b53dabcff704be36692f601dadb9 Mon Sep 17 00:00:00 2001 From: RikoDEV Date: Fri, 30 Mar 2018 16:01:22 +0200 Subject: [PATCH 13/63] Update messages_pl.yml (#1548) --- src/main/resources/messages/messages_pl.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/messages/messages_pl.yml b/src/main/resources/messages/messages_pl.yml index 4d9061df..cc512acc 100644 --- a/src/main/resources/messages/messages_pl.yml +++ b/src/main/resources/messages/messages_pl.yml @@ -79,7 +79,7 @@ on_join_validation: country_banned: '&4Ten kraj jest zbanowany na tym serwerze' not_owner_error: '&cNie jesteś właścicielem tego konta, wybierz inny nick!' invalid_name_case: '&cPowinieneś dołączyć do serwera z nicku %valid, a nie %invalid.' - # TODO quick_command: 'You used a command too fast! Please, join the server again and wait more before using any command.' + quick_command: '&cUżyłeś komendy zbyt szybko! Ponownie dołącz do serwera i poczekaj chwilę, zanim użyjesz dowolnej komendy.' # Email email: From fc54c0311b4f8245c4ef1bcf1de5a60576c6504d Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 2 Apr 2018 23:19:13 +0200 Subject: [PATCH 14/63] #1539 Columns handler: finishing touches - Add relocation rule for shading of the library - Fix SQLite connection not being refreshed on reload --- pom.xml | 5 ++++- src/main/java/fr/xephi/authme/datasource/SQLite.java | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 01034dc6..a7653699 100644 --- a/pom.xml +++ b/pom.xml @@ -258,6 +258,10 @@ ch.jalu.configme fr.xephi.authme.libs.ch.jalu.configme + + ch.jalu.datasourcecolumns + fr.xephi.authme.libs.ch.jalu.datasourcecolumns + com.zaxxer.hikari fr.xephi.authme.libs.com.zaxxer.hikari @@ -792,7 +796,6 @@ ch.jalu datasourcecolumns 0.1-SNAPSHOT - compile true diff --git a/src/main/java/fr/xephi/authme/datasource/SQLite.java b/src/main/java/fr/xephi/authme/datasource/SQLite.java index b2f639a3..b7ed2083 100644 --- a/src/main/java/fr/xephi/authme/datasource/SQLite.java +++ b/src/main/java/fr/xephi/authme/datasource/SQLite.java @@ -60,7 +60,6 @@ public class SQLite extends AbstractSqlDataSource { ConsoleLogger.logException("Error during SQLite initialization:", ex); throw ex; } - this.columnsHandler = AuthMeColumnsHandler.createForSqlite(con, settings); } @VisibleForTesting @@ -86,6 +85,7 @@ public class SQLite extends AbstractSqlDataSource { ConsoleLogger.debug("SQLite driver loaded"); this.con = DriverManager.getConnection("jdbc:sqlite:plugins/AuthMe/" + database + ".db"); + this.columnsHandler = AuthMeColumnsHandler.createForSqlite(con, settings); } /** From 106dea161151a686c014a33ebd0c91e37c92dd35 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 2 Apr 2018 23:43:52 +0200 Subject: [PATCH 15/63] Minor: fix JavaDoc warnings --- .../datasource/columnshandler/AuthMeColumnsHandler.java | 3 +++ .../authme/datasource/columnshandler/ConnectionSupplier.java | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java index 7b665712..bb0d80b7 100644 --- a/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java +++ b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java @@ -141,6 +141,7 @@ public final class AuthMeColumnsHandler { * @param column the column whose value should be retrieved * @param the column type * @return the result of the lookup + * @throws SQLException . */ public DataSourceValue retrieve(String name, DataSourceColumn column) throws SQLException { return internalHandler.retrieve(name.toLowerCase(), column); @@ -152,6 +153,7 @@ public final class AuthMeColumnsHandler { * @param name the account name to look up * @param columns the columns to retrieve * @return map-like object with the requested values + * @throws SQLException . */ public DataSourceValues retrieve(String name, DataSourceColumn... columns) throws SQLException { return internalHandler.retrieve(name.toLowerCase(), columns); @@ -164,6 +166,7 @@ public final class AuthMeColumnsHandler { * @param column the column to retrieve from the matching rows * @param the column's value type * @return the values of the matching rows + * @throws SQLException . */ public List retrieve(Predicate predicate, DataSourceColumn column) throws SQLException { return internalHandler.retrieve(predicate, column); diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/ConnectionSupplier.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/ConnectionSupplier.java index 4d419a21..77fbe8f3 100644 --- a/src/main/java/fr/xephi/authme/datasource/columnshandler/ConnectionSupplier.java +++ b/src/main/java/fr/xephi/authme/datasource/columnshandler/ConnectionSupplier.java @@ -10,7 +10,10 @@ import java.sql.SQLException; public interface ConnectionSupplier { /** - * @return connection object to the database + * Returns a connection to the database. + * + * @return the connection + * @throws SQLException . */ Connection get() throws SQLException; From 9326094d9c1620845b5aa837a683583b0df092c7 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Tue, 3 Apr 2018 00:12:25 +0200 Subject: [PATCH 16/63] #1141 Fix review remarks by @games647 - Use SHA512 to generate keys instead of default SHA1 - Declare google authenticator dependency as optional and add relocation rule --- pom.xml | 17 +++++++--------- .../fr/xephi/authme/message/MessageKey.java | 2 +- .../security/totp/TotpAuthenticator.java | 20 ++++++++++++------- .../security/totp/TotpAuthenticatorTest.java | 14 ++++++++++++- 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/pom.xml b/pom.xml index 0cd2db1d..703b9fa6 100644 --- a/pom.xml +++ b/pom.xml @@ -251,16 +251,8 @@ fr.xephi.authme.libs.com.google - ch.jalu.injector - fr.xephi.authme.libs.jalu.injector - - - ch.jalu.configme - fr.xephi.authme.libs.ch.jalu.configme - - - ch.jalu.datasourcecolumns - fr.xephi.authme.libs.ch.jalu.datasourcecolumns + ch.jalu + fr.xephi.authme.libs.ch.jalu com.zaxxer.hikari @@ -290,6 +282,10 @@ de.mkammerer fr.xephi.authme.libs.de.mkammerer + + com.warrenstrange + fr.xephi.authme.libs.com.warrenstrange + javax.inject fr.xephi.authme.libs.javax.inject @@ -482,6 +478,7 @@ com.warrenstrange googleauth 1.1.2 + true diff --git a/src/main/java/fr/xephi/authme/message/MessageKey.java b/src/main/java/fr/xephi/authme/message/MessageKey.java index 5d340964..538b5259 100644 --- a/src/main/java/fr/xephi/authme/message/MessageKey.java +++ b/src/main/java/fr/xephi/authme/message/MessageKey.java @@ -197,7 +197,7 @@ public enum MessageKey { /** Your secret code is %code. You can scan it from here %url */ TWO_FACTOR_CREATE("two_factor.code_created", "%code", "%url"), - /** Please submit your two-factor authentication code with /2fa code <code>. */ + /** Please submit your two-factor authentication code with /2fa code <code>. */ TWO_FACTOR_CODE_REQUIRED("two_factor.code_required"), /** Two-factor authentication is already enabled for your account! */ diff --git a/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java b/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java index d546df07..cffc09cd 100644 --- a/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java +++ b/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java @@ -1,9 +1,11 @@ package fr.xephi.authme.security.totp; -import com.google.common.annotations.VisibleForTesting; import com.warrenstrange.googleauth.GoogleAuthenticator; +import com.warrenstrange.googleauth.GoogleAuthenticatorConfig; +import com.warrenstrange.googleauth.GoogleAuthenticatorConfig.GoogleAuthenticatorConfigBuilder; import com.warrenstrange.googleauth.GoogleAuthenticatorKey; import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator; +import com.warrenstrange.googleauth.HmacHashFunction; import com.warrenstrange.googleauth.IGoogleAuthenticator; import fr.xephi.authme.service.BukkitService; import org.bukkit.entity.Player; @@ -18,16 +20,20 @@ public class TotpAuthenticator { private final IGoogleAuthenticator authenticator; private final BukkitService bukkitService; - @Inject TotpAuthenticator(BukkitService bukkitService) { - this(new GoogleAuthenticator(), bukkitService); + this.authenticator = createGoogleAuthenticator(); + this.bukkitService = bukkitService; } - @VisibleForTesting - TotpAuthenticator(IGoogleAuthenticator authenticator, BukkitService bukkitService) { - this.authenticator = authenticator; - this.bukkitService = bukkitService; + /** + * @return new Google Authenticator instance + */ + protected IGoogleAuthenticator createGoogleAuthenticator() { + GoogleAuthenticatorConfig config = new GoogleAuthenticatorConfigBuilder() + .setHmacHashFunction(HmacHashFunction.HmacSHA512) + .build(); + return new GoogleAuthenticator(config); } /** diff --git a/src/test/java/fr/xephi/authme/security/totp/TotpAuthenticatorTest.java b/src/test/java/fr/xephi/authme/security/totp/TotpAuthenticatorTest.java index b675ef8a..27434cce 100644 --- a/src/test/java/fr/xephi/authme/security/totp/TotpAuthenticatorTest.java +++ b/src/test/java/fr/xephi/authme/security/totp/TotpAuthenticatorTest.java @@ -36,7 +36,7 @@ public class TotpAuthenticatorTest { @Before public void initializeTotpAuthenticator() { - totpAuthenticator = new TotpAuthenticator(googleAuthenticator, bukkitService); + totpAuthenticator = new TotpAuthenticatorTestImpl(bukkitService); } @Test @@ -85,4 +85,16 @@ public class TotpAuthenticatorTest { assertThat(result, equalTo(false)); verifyZeroInteractions(googleAuthenticator); } + + private final class TotpAuthenticatorTestImpl extends TotpAuthenticator { + + TotpAuthenticatorTestImpl(BukkitService bukkitService) { + super(bukkitService); + } + + @Override + protected IGoogleAuthenticator createGoogleAuthenticator() { + return googleAuthenticator; + } + } } From 1f9bf3875584516ef836239e9e56d65c1df48eb4 Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 3 Apr 2018 09:45:27 -0600 Subject: [PATCH 17/63] Added EmailChangedEvent (#1549) * Added EmailChangedEvent * Fix failing tests Silly. * Documented the EmailChangedEvent * Separate messages for cancelled email event * Added lang todos for all the languages I can't speak I wish I could though. * Checkstyle satisfaction * Changed log level to info for cancelled events --- .../authme/data/VerificationCodeManager.java | 2 +- .../authme/events/EmailChangedEvent.java | 87 +++++++++++++++++++ .../fr/xephi/authme/message/MessageKey.java | 6 ++ .../authme/process/email/AsyncAddEmail.java | 12 +++ .../process/email/AsyncChangeEmail.java | 18 +++- src/main/resources/messages/messages_bg.yml | 2 + src/main/resources/messages/messages_br.yml | 2 + src/main/resources/messages/messages_cz.yml | 2 + src/main/resources/messages/messages_de.yml | 2 + src/main/resources/messages/messages_en.yml | 2 + src/main/resources/messages/messages_eo.yml | 2 + src/main/resources/messages/messages_es.yml | 2 + src/main/resources/messages/messages_et.yml | 2 + src/main/resources/messages/messages_eu.yml | 2 + src/main/resources/messages/messages_fi.yml | 2 + src/main/resources/messages/messages_fr.yml | 2 + src/main/resources/messages/messages_gl.yml | 2 + src/main/resources/messages/messages_hu.yml | 2 + src/main/resources/messages/messages_id.yml | 2 + src/main/resources/messages/messages_it.yml | 2 + src/main/resources/messages/messages_ko.yml | 2 + src/main/resources/messages/messages_lt.yml | 2 + src/main/resources/messages/messages_nl.yml | 2 + src/main/resources/messages/messages_pl.yml | 2 + src/main/resources/messages/messages_pt.yml | 2 + src/main/resources/messages/messages_ro.yml | 2 + src/main/resources/messages/messages_ru.yml | 8 +- src/main/resources/messages/messages_sk.yml | 2 + src/main/resources/messages/messages_tr.yml | 2 + src/main/resources/messages/messages_uk.yml | 4 +- src/main/resources/messages/messages_vn.yml | 2 + src/main/resources/messages/messages_zhcn.yml | 2 + src/main/resources/messages/messages_zhhk.yml | 2 + src/main/resources/messages/messages_zhmc.yml | 2 + src/main/resources/messages/messages_zhtw.yml | 2 + .../process/email/AsyncAddEmailTest.java | 34 ++++++++ .../process/email/AsyncChangeEmailTest.java | 40 ++++++++- 37 files changed, 259 insertions(+), 8 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/events/EmailChangedEvent.java diff --git a/src/main/java/fr/xephi/authme/data/VerificationCodeManager.java b/src/main/java/fr/xephi/authme/data/VerificationCodeManager.java index c5c2d725..1cd17668 100644 --- a/src/main/java/fr/xephi/authme/data/VerificationCodeManager.java +++ b/src/main/java/fr/xephi/authme/data/VerificationCodeManager.java @@ -162,7 +162,7 @@ public class VerificationCodeManager implements SettingsDependent, HasCleanup { * * @param name the name of the player to generate a code for */ - public void verify(String name){ + public void verify(String name) { verifiedPlayers.add(name.toLowerCase()); } diff --git a/src/main/java/fr/xephi/authme/events/EmailChangedEvent.java b/src/main/java/fr/xephi/authme/events/EmailChangedEvent.java new file mode 100644 index 00000000..7d9468ca --- /dev/null +++ b/src/main/java/fr/xephi/authme/events/EmailChangedEvent.java @@ -0,0 +1,87 @@ +package fr.xephi.authme.events; + +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +import javax.annotation.Nullable; + +/** + * This event is called when a player adds or changes his email address. + */ +public class EmailChangedEvent extends CustomEvent implements Cancellable { + private static final HandlerList handlers = new HandlerList(); + private final Player player; + private final String oldEmail; + private final String newEmail; + private boolean isCancelled; + + /** + * Constructor + * + * @param player The player that changed email + * @param oldEmail Old email player had on file. Can be null when user adds an email + * @param newEmail New email that player tries to set. In case of adding email, this will contain + * the email is trying to set. + * @param isAsync should this event be called asynchronously? + */ + public EmailChangedEvent(Player player, @Nullable String oldEmail, String newEmail, boolean isAsync) { + super(isAsync); + this.player = player; + this.oldEmail = oldEmail; + this.newEmail = newEmail; + } + + @Override + public boolean isCancelled() { + return isCancelled; + } + + /** + * Gets the player who changes the email + * + * @return The player who changed the email + */ + public Player getPlayer() { + return player; + } + + /** + * Gets the old email in case user tries to change existing email. + * + * @return old email stored on file. Can be null when user never had an email and adds a new one. + */ + public @Nullable String getOldEmail() { + return this.oldEmail; + } + + /** + * Gets the new email. + * + * @return the email user is trying to set. If user adds email and never had one before, + * this is where such email can be found. + */ + public String getNewEmail() { + return this.newEmail; + } + + @Override + public void setCancelled(boolean cancelled) { + this.isCancelled = cancelled; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + /** + * Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link Event}. + * + * @return The list of handlers + */ + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/src/main/java/fr/xephi/authme/message/MessageKey.java b/src/main/java/fr/xephi/authme/message/MessageKey.java index 355f14a9..357e7d8c 100644 --- a/src/main/java/fr/xephi/authme/message/MessageKey.java +++ b/src/main/java/fr/xephi/authme/message/MessageKey.java @@ -167,12 +167,18 @@ public enum MessageKey { /** Email address successfully added to your account! */ EMAIL_ADDED_SUCCESS("email.added"), + /** Adding email was not allowed */ + EMAIL_ADD_NOT_ALLOWED("email.add_not_allowed"), + /** Please confirm your email address! */ CONFIRM_EMAIL_MESSAGE("email.request_confirmation"), /** Email address changed correctly! */ EMAIL_CHANGED_SUCCESS("email.changed"), + /** Changing email was not allowed */ + EMAIL_CHANGE_NOT_ALLOWED("email.change_not_allowed"), + /** Your current email address is: %email */ EMAIL_SHOW("email.email_show", "%email"), diff --git a/src/main/java/fr/xephi/authme/process/email/AsyncAddEmail.java b/src/main/java/fr/xephi/authme/process/email/AsyncAddEmail.java index 016d6169..1896bfd3 100644 --- a/src/main/java/fr/xephi/authme/process/email/AsyncAddEmail.java +++ b/src/main/java/fr/xephi/authme/process/email/AsyncAddEmail.java @@ -4,8 +4,10 @@ import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.events.EmailChangedEvent; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.process.AsynchronousProcess; +import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.CommonService; import fr.xephi.authme.service.ValidationService; import fr.xephi.authme.service.bungeecord.BungeeSender; @@ -35,6 +37,9 @@ public class AsyncAddEmail implements AsynchronousProcess { @Inject private BungeeSender bungeeSender; + @Inject + private BukkitService bukkitService; + AsyncAddEmail() { } /** @@ -57,6 +62,13 @@ public class AsyncAddEmail implements AsynchronousProcess { } else if (!validationService.isEmailFreeForRegistration(email, player)) { service.send(player, MessageKey.EMAIL_ALREADY_USED_ERROR); } else { + EmailChangedEvent event = bukkitService.createAndCallEvent(isAsync + -> new EmailChangedEvent(player, null, email, isAsync)); + if (event.isCancelled()) { + ConsoleLogger.info("Could not add email to player '" + player + "' – event was cancelled"); + service.send(player, MessageKey.EMAIL_ADD_NOT_ALLOWED); + return; + } auth.setEmail(email); if (dataSource.updateEmail(auth)) { playerCache.updatePlayer(auth); diff --git a/src/main/java/fr/xephi/authme/process/email/AsyncChangeEmail.java b/src/main/java/fr/xephi/authme/process/email/AsyncChangeEmail.java index 8edd9496..26a5da9e 100644 --- a/src/main/java/fr/xephi/authme/process/email/AsyncChangeEmail.java +++ b/src/main/java/fr/xephi/authme/process/email/AsyncChangeEmail.java @@ -1,10 +1,13 @@ package fr.xephi.authme.process.email; +import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.events.EmailChangedEvent; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.process.AsynchronousProcess; +import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.CommonService; import fr.xephi.authme.service.ValidationService; import fr.xephi.authme.service.bungeecord.BungeeSender; @@ -32,6 +35,9 @@ public class AsyncChangeEmail implements AsynchronousProcess { @Inject private BungeeSender bungeeSender; + + @Inject + private BukkitService bukkitService; AsyncChangeEmail() { } @@ -57,14 +63,22 @@ public class AsyncChangeEmail implements AsynchronousProcess { } else if (!validationService.isEmailFreeForRegistration(newEmail, player)) { service.send(player, MessageKey.EMAIL_ALREADY_USED_ERROR); } else { - saveNewEmail(auth, player, newEmail); + saveNewEmail(auth, player, oldEmail, newEmail); } } else { outputUnloggedMessage(player); } } - private void saveNewEmail(PlayerAuth auth, Player player, String newEmail) { + private void saveNewEmail(PlayerAuth auth, Player player, String oldEmail, String newEmail) { + EmailChangedEvent event = bukkitService.createAndCallEvent(isAsync + -> new EmailChangedEvent(player, oldEmail, newEmail, isAsync)); + if (event.isCancelled()) { + ConsoleLogger.info("Could not change email for player '" + player + "' – event was cancelled"); + service.send(player, MessageKey.EMAIL_CHANGE_NOT_ALLOWED); + return; + } + auth.setEmail(newEmail); if (dataSource.updateEmail(auth)) { playerCache.updatePlayer(auth); diff --git a/src/main/resources/messages/messages_bg.yml b/src/main/resources/messages/messages_bg.yml index 8b073bd9..3a6f3494 100644 --- a/src/main/resources/messages/messages_bg.yml +++ b/src/main/resources/messages/messages_bg.yml @@ -99,6 +99,8 @@ email: send_failure: 'Съобщението не беше изпратено. Моля свържете се с администратора.' # TODO change_password_expired: 'You cannot change your password using this command anymore.' email_cooldown_error: '&cВече е бил изпратен имейл адрес. Трябва а изчакаш %time преди да пратиш нов.' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_br.yml b/src/main/resources/messages/messages_br.yml index 69f5e395..69062b23 100644 --- a/src/main/resources/messages/messages_br.yml +++ b/src/main/resources/messages/messages_br.yml @@ -102,6 +102,8 @@ email: send_failure: '&cO e-mail não pôde ser enviado, reporte isso a um administrador!' change_password_expired: 'Você não pode mais usar esse comando de recuperação de senha!' email_cooldown_error: '&cUm e-mail já foi enviado, espere mais %time antes de enviar novamente!' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_cz.yml b/src/main/resources/messages/messages_cz.yml index 5ed2ceb4..e3bd5c35 100644 --- a/src/main/resources/messages/messages_cz.yml +++ b/src/main/resources/messages/messages_cz.yml @@ -99,6 +99,8 @@ email: send_failure: 'Email nemohl být odeslán. Kontaktujte prosím admina.' change_password_expired: 'Nemůžeš si změnit heslo pomocí toho příkazu.' email_cooldown_error: '&cEmail už byl nedávno odeslán. Musíš čekat %time před odesláním nového.' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_de.yml b/src/main/resources/messages/messages_de.yml index 84679c48..97639a9a 100644 --- a/src/main/resources/messages/messages_de.yml +++ b/src/main/resources/messages/messages_de.yml @@ -99,6 +99,8 @@ email: send_failure: 'Die E-Mail konnte nicht gesendet werden. Bitte kontaktiere einen Administrator.' change_password_expired: 'Mit diesem Befehl kannst du dein Passwort nicht mehr ändern.' email_cooldown_error: '&cEine E-Mail wurde erst kürzlich versendet. Du musst %time warten, bevor du eine neue anfordern kannst.' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_en.yml b/src/main/resources/messages/messages_en.yml index 6d9d2879..971945af 100644 --- a/src/main/resources/messages/messages_en.yml +++ b/src/main/resources/messages/messages_en.yml @@ -98,6 +98,8 @@ email: add_email_request: '&3Please add your email to your account with the command: /email add ' change_password_expired: 'You cannot change your password using this command anymore.' email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' + add_not_allowed: '&cAdding email was not allowed' + change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_eo.yml b/src/main/resources/messages/messages_eo.yml index c5783c83..390acdca 100644 --- a/src/main/resources/messages/messages_eo.yml +++ b/src/main/resources/messages/messages_eo.yml @@ -99,6 +99,8 @@ email: send_failure: 'La retpoŝto ne estis sendita. Bonvolu kontakti administranto.' change_password_expired: 'Vi ne povas ŝanĝi vian pasvorton per tiu ĉi komando plu.' email_cooldown_error: '&cRetmesaĝon jam sendita lastatempe. Vi devas atendi %time antaŭ vi povas sendi novan.' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_es.yml b/src/main/resources/messages/messages_es.yml index 50363ead..bde1f29e 100644 --- a/src/main/resources/messages/messages_es.yml +++ b/src/main/resources/messages/messages_es.yml @@ -100,6 +100,8 @@ email: send_failure: 'No se ha podido enviar el correo electrónico. Por favor, contacta con un administrador.' change_password_expired: 'No puedes cambiar la contraseña utilizando este comando.' email_cooldown_error: '&cEl correo ha sido enviado recientemente. Debes esperar %time antes de volver a enviar uno nuevo.' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_et.yml b/src/main/resources/messages/messages_et.yml index 68ce520f..ba5e0b15 100644 --- a/src/main/resources/messages/messages_et.yml +++ b/src/main/resources/messages/messages_et.yml @@ -99,6 +99,8 @@ email: send_failure: 'Meili ei õnnestunud saata. Kontakteeru meeskonnaga.' change_password_expired: '&3Enam ei saa vahetada oma parooli kasutades seda käsklust.' email_cooldown_error: '&cEmail juba saadeti. Sa pead ootama %time ennem, kui saad uuesti saata.' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_eu.yml b/src/main/resources/messages/messages_eu.yml index 0507d104..65bd5575 100644 --- a/src/main/resources/messages/messages_eu.yml +++ b/src/main/resources/messages/messages_eu.yml @@ -99,6 +99,8 @@ email: # TODO send_failure: 'The email could not be sent. Please contact an administrator.' # TODO change_password_expired: 'You cannot change your password using this command anymore.' # TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_fi.yml b/src/main/resources/messages/messages_fi.yml index b0b4f303..95d0a732 100644 --- a/src/main/resources/messages/messages_fi.yml +++ b/src/main/resources/messages/messages_fi.yml @@ -99,6 +99,8 @@ email: # TODO send_failure: 'The email could not be sent. Please contact an administrator.' # TODO change_password_expired: 'You cannot change your password using this command anymore.' # TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_fr.yml b/src/main/resources/messages/messages_fr.yml index 52fd4c88..710da4d7 100644 --- a/src/main/resources/messages/messages_fr.yml +++ b/src/main/resources/messages/messages_fr.yml @@ -102,6 +102,8 @@ email: send_failure: '&cLe mail n''a pas pu être envoyé. Veuillez contacter un admin.' change_password_expired: 'Vous ne pouvez pas changer votre mot de passe avec cette commande.' email_cooldown_error: '&cUn mail de récupération a déjà été envoyé récemment. Veuillez attendre %time pour le demander de nouveau.' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_gl.yml b/src/main/resources/messages/messages_gl.yml index d22caa7c..fb79b664 100644 --- a/src/main/resources/messages/messages_gl.yml +++ b/src/main/resources/messages/messages_gl.yml @@ -99,6 +99,8 @@ email: # TODO send_failure: 'The email could not be sent. Please contact an administrator.' # TODO change_password_expired: 'You cannot change your password using this command anymore.' # TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_hu.yml b/src/main/resources/messages/messages_hu.yml index 1d420b74..72f8a15b 100644 --- a/src/main/resources/messages/messages_hu.yml +++ b/src/main/resources/messages/messages_hu.yml @@ -99,6 +99,8 @@ email: send_failure: 'Nem sikerült elküldeni az emailt. Lépj kapcsolatba egy adminnal.' change_password_expired: 'Ezzel a paranccsal már nem módosíthatja jelszavát.' email_cooldown_error: '&cEgy emailt már kiküldtünk. Következő email küldése előtt várnod kell: %time.' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_id.yml b/src/main/resources/messages/messages_id.yml index 65e84cd9..5e5b3f3c 100644 --- a/src/main/resources/messages/messages_id.yml +++ b/src/main/resources/messages/messages_id.yml @@ -99,6 +99,8 @@ email: # TODO send_failure: 'The email could not be sent. Please contact an administrator.' # TODO change_password_expired: 'You cannot change your password using this command anymore.' # TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_it.yml b/src/main/resources/messages/messages_it.yml index 22354cda..70162b77 100644 --- a/src/main/resources/messages/messages_it.yml +++ b/src/main/resources/messages/messages_it.yml @@ -102,6 +102,8 @@ email: send_failure: 'Non è stato possibile inviare l''email di recupero. Per favore contatta un amministratore.' change_password_expired: 'Non puoi più cambiare la tua password con questo comando.' email_cooldown_error: '&cUna email di recupero ti è già stata inviata recentemente. Devi attendere %time prima di poterne richiedere una nuova.' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_ko.yml b/src/main/resources/messages/messages_ko.yml index 41049b88..5d875932 100644 --- a/src/main/resources/messages/messages_ko.yml +++ b/src/main/resources/messages/messages_ko.yml @@ -101,6 +101,8 @@ email: send_failure: '이메일을 보낼 수 없습니다. 관리자에게 알려주세요.' change_password_expired: '더 이상 이 명령어를 통해 비밀번호를 변경할 수 없습니다.' email_cooldown_error: '&c이메일을 이미 발송했습니다. %time 후에 다시 발송할 수 있습니다.' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_lt.yml b/src/main/resources/messages/messages_lt.yml index 15eea304..3df01369 100644 --- a/src/main/resources/messages/messages_lt.yml +++ b/src/main/resources/messages/messages_lt.yml @@ -99,6 +99,8 @@ email: # TODO send_failure: 'The email could not be sent. Please contact an administrator.' # TODO change_password_expired: 'You cannot change your password using this command anymore.' # TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_nl.yml b/src/main/resources/messages/messages_nl.yml index b8fc4095..86f7b8ff 100644 --- a/src/main/resources/messages/messages_nl.yml +++ b/src/main/resources/messages/messages_nl.yml @@ -99,6 +99,8 @@ email: send_failure: 'De e-mail kon niet verzonden worden. Neem contact op met een administrator.' change_password_expired: 'Je kunt je wachtwoord niet meer veranderen met dit commando.' email_cooldown_error: '&cEr is recent al een e-mail verzonden. Je moet %time wachten voordat je een nieuw bericht kunt versturen.' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_pl.yml b/src/main/resources/messages/messages_pl.yml index cc512acc..41272a08 100644 --- a/src/main/resources/messages/messages_pl.yml +++ b/src/main/resources/messages/messages_pl.yml @@ -99,6 +99,8 @@ email: send_failure: 'Nie można wysłać e-maila. Skontaktuj się z administracją.' change_password_expired: 'Nie zmienisz już hasła przy użyciu tej komendy.' email_cooldown_error: '&cE-mail został wysłany, musisz poczekać %time przed wysłaniem następnego.' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_pt.yml b/src/main/resources/messages/messages_pt.yml index 2fb0ad2c..b7cabdc8 100644 --- a/src/main/resources/messages/messages_pt.yml +++ b/src/main/resources/messages/messages_pt.yml @@ -99,6 +99,8 @@ email: send_failure: 'Não foi possivel enviar o email. Por favor contate um administrador.' change_password_expired: 'Você não pode mais alterar a sua password usando este comando.' email_cooldown_error: '&cUm email já foi enviado recentemente.Por favor, espere %time antes de enviar novamente' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_ro.yml b/src/main/resources/messages/messages_ro.yml index 6fcacb05..4d40556d 100644 --- a/src/main/resources/messages/messages_ro.yml +++ b/src/main/resources/messages/messages_ro.yml @@ -99,6 +99,8 @@ email: send_failure: 'Email-ul nu a putut fi trimis. Ta rugam contactatezi un administrator.' change_password_expired: 'Nu mai iti poti schimba parola folosind aceasta comanda.' email_cooldown_error: '&cAi primit deja un mail pentru schimbarea parolei. Trebuie sa astepti %time inainte de a trimite unul nou.' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_ru.yml b/src/main/resources/messages/messages_ru.yml index 25a8ca03..81d5c6f6 100644 --- a/src/main/resources/messages/messages_ru.yml +++ b/src/main/resources/messages/messages_ru.yml @@ -79,7 +79,7 @@ on_join_validation: country_banned: '&4Вход с IP-адресов вашей страны запрещён на этом сервере.' not_owner_error: 'Вы не являетесь владельцем данной уч. записи. Выберите себе другое имя!' invalid_name_case: 'Неверное имя! Зайдите под именем %valid, а не %invalid.' - # TODO quick_command: 'You used a command too fast! Please, join the server again and wait more before using any command.' + quick_command: 'Вы вводили команды слишком часто! Пожалуйста заходите снова и вводите команды помедленнее.' # Email email: @@ -99,6 +99,8 @@ email: send_failure: 'Письмо не может быть отправлено. Свяжитесь в администратором.' change_password_expired: 'Больше нельзя сменить свой пароль, используя эту команду.' email_cooldown_error: '&cПисьмо было отправлено недавно. Подождите %time, прежде чем отправить новое.' + add_not_allowed: '&cДобавление электронной почты не было разрешено.' + change_not_allowed: '&cИзменение электронной почты не было разрешено.' # Password recovery by email recovery: @@ -117,8 +119,8 @@ captcha: usage_captcha: '&3Необходимо ввести текст с каптчи. Используйте «/captcha %captcha_code»' wrong_captcha: '&cНеверно! Используйте «/captcha %captcha_code».' valid_captcha: '&2Вы успешно решили каптчу!' - # TODO captcha_for_registration: 'To register you have to solve a captcha first, please use the command: /captcha %captcha_code' - # TODO register_captcha_valid: '&2Valid captcha! You may now register with /register' + captcha_for_registration: 'Чтобы зарегистрироваться, решите каптчу используя команду: «/captcha %captcha_code»' + register_captcha_valid: '&2Вы успешно решили каптчу! Теперь вы можете зарегистрироваться командой «/register»' # Verification code verification: diff --git a/src/main/resources/messages/messages_sk.yml b/src/main/resources/messages/messages_sk.yml index 5af6e503..2553fc0d 100644 --- a/src/main/resources/messages/messages_sk.yml +++ b/src/main/resources/messages/messages_sk.yml @@ -105,6 +105,8 @@ email: send_failure: 'Email nemohol byť poslaný. Prosím kontaktuj Administrátora.' change_password_expired: 'Už nemôžeš zmeniť svoje heslo týmto príkazom.' email_cooldown_error: '&cEmail bol nedávno poslaný. Musíš počkať %time predtým ako ti pošleme nový.' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_tr.yml b/src/main/resources/messages/messages_tr.yml index b9aff0ae..fd7d5e29 100644 --- a/src/main/resources/messages/messages_tr.yml +++ b/src/main/resources/messages/messages_tr.yml @@ -99,6 +99,8 @@ email: send_failure: 'Eposta gonderilemedi. Yetkili ile iletisime gec.' # TODO change_password_expired: 'You cannot change your password using this command anymore.' email_cooldown_error: '&cKisa bir sure once eposta gonderildi. Yeni bir eposta almak icin %time beklemelisin.' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_uk.yml b/src/main/resources/messages/messages_uk.yml index 3ecf5290..d509d4eb 100644 --- a/src/main/resources/messages/messages_uk.yml +++ b/src/main/resources/messages/messages_uk.yml @@ -95,10 +95,12 @@ email: # TODO email_show: '&2Your current email address is: &f%email' # TODO no_email_for_account: '&2You currently don''t have email address associated with this account.' already_used: '&4До цієї електронної пошти прив’язано забагато акаунтів!' - incomplete_settings: '&4[AuthMe] Error: Не всі необхідні налаштування є встановленими, щоб надсилати електронну пошту. Будь ласка, повідомте адміністратора!' + incomplete_settings: '&4Не всі необхідні налаштування є встановленими, щоб надсилати електронну пошту. Будь ласка, повідомте адміністратора!' # TODO send_failure: 'The email could not be sent. Please contact an administrator.' # TODO change_password_expired: 'You cannot change your password using this command anymore.' # TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_vn.yml b/src/main/resources/messages/messages_vn.yml index 5e138f4f..8fc170c0 100644 --- a/src/main/resources/messages/messages_vn.yml +++ b/src/main/resources/messages/messages_vn.yml @@ -99,6 +99,8 @@ email: send_failure: 'Không thể gửi thư. Vui lòng liên hệ với ban quản trị.' change_password_expired: '&cBạn không thể thay đổi mật khẩu bằng lệnh này từ nay.' email_cooldown_error: '&cMột bức thư đã được gửi gần đây. Bạn phải chờ %time trước khi có thể gửi một bức thư mới.' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_zhcn.yml b/src/main/resources/messages/messages_zhcn.yml index 78c3c89b..70193285 100644 --- a/src/main/resources/messages/messages_zhcn.yml +++ b/src/main/resources/messages/messages_zhcn.yml @@ -99,6 +99,8 @@ email: send_failure: '邮件发送失败,请联系管理员' change_password_expired: '您不能使用此命令更改密码' email_cooldown_error: '&c邮件已在几分钟前发送,您需要等待 %time 后才能再次请求发送' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_zhhk.yml b/src/main/resources/messages/messages_zhhk.yml index 9d080748..694d532e 100644 --- a/src/main/resources/messages/messages_zhhk.yml +++ b/src/main/resources/messages/messages_zhhk.yml @@ -102,6 +102,8 @@ email: send_failure: '&8[&6用戶系統&8] &c電郵系統錯誤,請聯絡伺服器管理員。 &7(err: smtperr)' change_password_expired: '&8[&6用戶系統&8] 此指令已過期,請重新辦理。' email_cooldown_error: '&8[&6用戶系統&8] &c你已經辦理過重寄郵件,請等待 %time 後再嘗試吧。' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_zhmc.yml b/src/main/resources/messages/messages_zhmc.yml index a151a8cf..c0b41802 100644 --- a/src/main/resources/messages/messages_zhmc.yml +++ b/src/main/resources/messages/messages_zhmc.yml @@ -99,6 +99,8 @@ email: # TODO send_failure: 'The email could not be sent. Please contact an administrator.' # TODO change_password_expired: 'You cannot change your password using this command anymore.' # TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/main/resources/messages/messages_zhtw.yml b/src/main/resources/messages/messages_zhtw.yml index 43a7b533..fa9946bf 100644 --- a/src/main/resources/messages/messages_zhtw.yml +++ b/src/main/resources/messages/messages_zhtw.yml @@ -101,6 +101,8 @@ email: send_failure: '&b【AuthMe】&4無法傳送電子郵件,請聯絡管理員.' change_password_expired: '&b【AuthMe】&6您現在不能使用這個指令變更密碼了.' email_cooldown_error: '&b【AuthMe】&c電子郵件已經寄出了. 您只能在 %time 後才能傳送.' + # TODO add_not_allowed: '&cAdding email was not allowed' + # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: diff --git a/src/test/java/fr/xephi/authme/process/email/AsyncAddEmailTest.java b/src/test/java/fr/xephi/authme/process/email/AsyncAddEmailTest.java index b945e9f9..6d2fdd22 100644 --- a/src/test/java/fr/xephi/authme/process/email/AsyncAddEmailTest.java +++ b/src/test/java/fr/xephi/authme/process/email/AsyncAddEmailTest.java @@ -4,7 +4,9 @@ import fr.xephi.authme.TestHelper; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.events.EmailChangedEvent; import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.CommonService; import fr.xephi.authme.service.ValidationService; import fr.xephi.authme.service.bungeecord.BungeeSender; @@ -15,11 +17,13 @@ import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import java.util.function.Function; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; /** @@ -49,6 +53,9 @@ public class AsyncAddEmailTest { @Mock private BungeeSender bungeeSender; + @Mock + private BukkitService bukkitService; + @BeforeClass public static void setUp() { TestHelper.setupLogger(); @@ -66,6 +73,8 @@ public class AsyncAddEmailTest { given(dataSource.updateEmail(any(PlayerAuth.class))).willReturn(true); given(validationService.validateEmail(email)).willReturn(true); given(validationService.isEmailFreeForRegistration(email, player)).willReturn(true); + EmailChangedEvent event = spy(new EmailChangedEvent(player, null, email, false)); + given(bukkitService.createAndCallEvent(any(Function.class))).willReturn(event); // when asyncAddEmail.addEmail(player, email); @@ -89,6 +98,8 @@ public class AsyncAddEmailTest { given(dataSource.updateEmail(any(PlayerAuth.class))).willReturn(false); given(validationService.validateEmail(email)).willReturn(true); given(validationService.isEmailFreeForRegistration(email, player)).willReturn(true); + EmailChangedEvent event = spy(new EmailChangedEvent(player, null, email, false)); + given(bukkitService.createAndCallEvent(any(Function.class))).willReturn(event); // when asyncAddEmail.addEmail(player, email); @@ -184,4 +195,27 @@ public class AsyncAddEmailTest { verify(playerCache, never()).updatePlayer(any(PlayerAuth.class)); } + @Test + public void shouldNotAddOnCancelledEvent() { + // given + String email = "player@mail.tld"; + given(player.getName()).willReturn("TestName"); + given(playerCache.isAuthenticated("testname")).willReturn(true); + PlayerAuth auth = mock(PlayerAuth.class); + given(auth.getEmail()).willReturn(null); + given(playerCache.getAuth("testname")).willReturn(auth); + given(validationService.validateEmail(email)).willReturn(true); + given(validationService.isEmailFreeForRegistration(email, player)).willReturn(true); + EmailChangedEvent event = spy(new EmailChangedEvent(player, null, email, false)); + event.setCancelled(true); + given(bukkitService.createAndCallEvent(any(Function.class))).willReturn(event); + + // when + asyncAddEmail.addEmail(player, email); + + // then + verify(service).send(player, MessageKey.EMAIL_ADD_NOT_ALLOWED); + verify(playerCache, never()).updatePlayer(any(PlayerAuth.class)); + } + } diff --git a/src/test/java/fr/xephi/authme/process/email/AsyncChangeEmailTest.java b/src/test/java/fr/xephi/authme/process/email/AsyncChangeEmailTest.java index 4427c45d..23fb01e6 100644 --- a/src/test/java/fr/xephi/authme/process/email/AsyncChangeEmailTest.java +++ b/src/test/java/fr/xephi/authme/process/email/AsyncChangeEmailTest.java @@ -3,7 +3,9 @@ package fr.xephi.authme.process.email; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.events.EmailChangedEvent; import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.CommonService; import fr.xephi.authme.service.ValidationService; import fr.xephi.authme.service.bungeecord.BungeeSender; @@ -14,10 +16,13 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import java.util.function.Function; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -48,6 +53,9 @@ public class AsyncChangeEmailTest { @Mock private BungeeSender bungeeSender; + @Mock + private BukkitService bukkitService; + @Test public void shouldChangeEmail() { // given @@ -59,7 +67,9 @@ public class AsyncChangeEmailTest { given(dataSource.updateEmail(auth)).willReturn(true); given(validationService.validateEmail(newEmail)).willReturn(true); given(validationService.isEmailFreeForRegistration(newEmail, player)).willReturn(true); - + EmailChangedEvent event = spy(new EmailChangedEvent(player, "old@mail.tld", newEmail, false)); + given(bukkitService.createAndCallEvent(any(Function.class))).willReturn(event); + // when process.changeEmail(player, "old@mail.tld", newEmail); @@ -81,6 +91,8 @@ public class AsyncChangeEmailTest { given(dataSource.updateEmail(auth)).willReturn(true); given(validationService.validateEmail(newEmail)).willReturn(true); given(validationService.isEmailFreeForRegistration(newEmail, player)).willReturn(true); + EmailChangedEvent event = spy(new EmailChangedEvent(player, oldEmail, newEmail, false)); + given(bukkitService.createAndCallEvent(any(Function.class))).willReturn(event); // when process.changeEmail(player, "old-mail@example.org", newEmail); @@ -102,6 +114,8 @@ public class AsyncChangeEmailTest { given(dataSource.updateEmail(auth)).willReturn(false); given(validationService.validateEmail(newEmail)).willReturn(true); given(validationService.isEmailFreeForRegistration(newEmail, player)).willReturn(true); + EmailChangedEvent event = spy(new EmailChangedEvent(player, "old@mail.tld", newEmail, false)); + given(bukkitService.createAndCallEvent(any(Function.class))).willReturn(event); // when process.changeEmail(player, "old@mail.tld", newEmail); @@ -219,6 +233,30 @@ public class AsyncChangeEmailTest { verify(service).send(player, MessageKey.REGISTER_MESSAGE); } + @Test + public void shouldNotChangeOnCancelledEvent() { + // given + String newEmail = "new@example.com"; + String oldEmail = "old@example.com"; + given(player.getName()).willReturn("Username"); + given(playerCache.isAuthenticated("username")).willReturn(true); + PlayerAuth auth = authWithMail(oldEmail); + given(playerCache.getAuth("username")).willReturn(auth); + given(validationService.validateEmail(newEmail)).willReturn(true); + given(validationService.isEmailFreeForRegistration(newEmail, player)).willReturn(true); + EmailChangedEvent event = spy(new EmailChangedEvent(player, oldEmail, newEmail, false)); + event.setCancelled(true); + given(bukkitService.createAndCallEvent(any(Function.class))).willReturn(event); + + // when + process.changeEmail(player, oldEmail, newEmail); + + // then + verify(dataSource, never()).updateEmail(any(PlayerAuth.class)); + verify(playerCache, never()).updatePlayer(any(PlayerAuth.class)); + verify(service).send(player, MessageKey.EMAIL_CHANGE_NOT_ALLOWED); + } + private static PlayerAuth authWithMail(String email) { PlayerAuth auth = mock(PlayerAuth.class); when(auth.getEmail()).thenReturn(email); From 80538b4bb24f6cdf512a19cbff8fc86374a769bd Mon Sep 17 00:00:00 2001 From: games647 Date: Thu, 5 Apr 2018 15:16:46 +0200 Subject: [PATCH 18/63] Force english language during unit testing Fixes #1536 --- pom.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a7653699..a04861c2 100644 --- a/pom.xml +++ b/pom.xml @@ -172,7 +172,8 @@ 2.20.1 - -Dfile.encoding=${project.build.sourceEncoding} @{argLine} + + -Dfile.encoding=${project.build.sourceEncoding} -Duser.language=en @{argLine} ${project.skipExtendedHashTests} From b56133fe8f4808bc122c25a954deeedd197d9c9f Mon Sep 17 00:00:00 2001 From: RatchetCinemaESP Date: Thu, 12 Apr 2018 15:58:28 +0200 Subject: [PATCH 19/63] Update messages_es.yml (#1553) Translated lines: - 83 - 103 - 104 --- src/main/resources/messages/messages_es.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/resources/messages/messages_es.yml b/src/main/resources/messages/messages_es.yml index bde1f29e..899631cc 100644 --- a/src/main/resources/messages/messages_es.yml +++ b/src/main/resources/messages/messages_es.yml @@ -80,7 +80,7 @@ on_join_validation: country_banned: '¡Tu país ha sido baneado de este servidor!' not_owner_error: 'No eres el propietario de esta cuenta. ¡Por favor, elije otro nombre!' invalid_name_case: 'Solo puedes unirte mediante el nombre de usuario %valid, no %invalid.' - # TODO quick_command: 'You used a command too fast! Please, join the server again and wait more before using any command.' + quick_command: 'Has usado el comando demasiado rápido! Porfavor, entra al servidor de nuevo y espera un poco antes de usar cualquier comando.' # Email email: @@ -100,8 +100,8 @@ email: send_failure: 'No se ha podido enviar el correo electrónico. Por favor, contacta con un administrador.' change_password_expired: 'No puedes cambiar la contraseña utilizando este comando.' email_cooldown_error: '&cEl correo ha sido enviado recientemente. Debes esperar %time antes de volver a enviar uno nuevo.' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' + add_not_allowed: '&cNo se permite añadir un Email' + change_not_allowed: '&cNo se permite el cambio de Email' # Password recovery by email recovery: From 71826db23d376a8d394499f8df3e9fdaf2776fb3 Mon Sep 17 00:00:00 2001 From: Maxetto Date: Fri, 13 Apr 2018 19:34:29 +0200 Subject: [PATCH 20/63] Update messages_it.yml --- src/main/resources/messages/messages_it.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/resources/messages/messages_it.yml b/src/main/resources/messages/messages_it.yml index 70162b77..d99cae84 100644 --- a/src/main/resources/messages/messages_it.yml +++ b/src/main/resources/messages/messages_it.yml @@ -4,8 +4,6 @@ # %username% - Sostituisce il nome dell'utente che riceve il messaggio. # %displayname% - Sostituisce il nickname (e i colori) dell'utente che riceve il messaggio. -# Registrazione - # Registration registration: disabled: '&cLa registrazione tramite i comandi di gioco è disabilitata.' @@ -82,7 +80,7 @@ on_join_validation: country_banned: '&4Il tuo paese è bandito da questo server!' not_owner_error: 'Non sei il proprietario di questo account. Per favore scegli un altro nome!' invalid_name_case: 'Dovresti entrare con questo nome utente "%valid", al posto di "%invalid".' - # TODO quick_command: 'You used a command too fast! Please, join the server again and wait more before using any command.' + quick_command: 'Hai usato un comando troppo velocemente dal tuo accesso! Per favore, rientra nel server e aspetta un po'' di più prima di usare un qualsiasi comando.' # Email email: @@ -102,12 +100,12 @@ email: send_failure: 'Non è stato possibile inviare l''email di recupero. Per favore contatta un amministratore.' change_password_expired: 'Non puoi più cambiare la tua password con questo comando.' email_cooldown_error: '&cUna email di recupero ti è già stata inviata recentemente. Devi attendere %time prima di poterne richiedere una nuova.' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' + add_not_allowed: '&cNon hai il permesso di aggiungere un indirizzo email' + change_not_allowed: '&cNon hai il permesso di cambiare l''indirizzo email' # Password recovery by email recovery: - forgot_password_hint: '&3Hai dimenticato la tua password? Puoi recuperarla eseguendo il comando: /email recovery ' + forgot_password_hint: '&3Hai dimenticato la tua password? Puoi recuperarla usando il comando: /email recovery ' command_usage: '&cUtilizzo: /email recovery ' email_sent: '&2Una email di recupero è stata appena inviata al tuo indirizzo email!' code: From 156260c7a9dd35753f83d108d4577ec7e1511078 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Thu, 19 Apr 2018 10:51:00 +0200 Subject: [PATCH 21/63] Remove duplicated relocation pattern --- pom.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pom.xml b/pom.xml index 703b9fa6..eb8a0b2d 100644 --- a/pom.xml +++ b/pom.xml @@ -295,10 +295,6 @@ org.bstats fr.xephi.authme.libs.org.bstats - - com.warrenstrange - fr.xephi.authme.libs.com.warrenstrange - From 5cc58da85f49e6b994fe73d04917709112b1233b Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Thu, 19 Apr 2018 11:02:20 +0200 Subject: [PATCH 22/63] Update HikariCP --- README.md | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 21910e4e..1688990c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ | **Code quality:** | [![Code Climate](https://codeclimate.com/github/AuthMe/AuthMeReloaded/badges/gpa.svg)](https://codeclimate.com/github/AuthMe/AuthMeReloaded) [![Coverage status](https://coveralls.io/repos/AuthMe-Team/AuthMeReloaded/badge.svg?branch=master&service=github)](https://coveralls.io/github/AuthMe-Team/AuthMeReloaded?branch=master) | | **Jenkins CI:** | [![Jenkins Status](https://img.shields.io/website-up-down-green-red/http/shields.io.svg?label=ci.codemc.org)](https://ci.codemc.org/) [![Build Status](https://ci.codemc.org/buildStatus/icon?job=AuthMe/AuthMeReloaded)](https://ci.codemc.org/job/AuthMe/job/AuthMeReloaded) ![Build Tests](https://img.shields.io/jenkins/t/https/ci.codemc.org/job/AuthMe/job/AuthMeReloaded.svg) | | **Other CIs:** | [![CircleCI](https://circleci.com/gh/AuthMe/AuthMeReloaded.svg?style=svg)](https://circleci.com/gh/AuthMe/AuthMeReloaded) | -| **Dependencies:** | [![Dependency Status](https://gemnasium.com/badges/github.com/AuthMe/AuthMeReloaded.svg)](https://gemnasium.com/github.com/AuthMe/AuthMeReloaded) | +| **Dependencies:** | [![Dependency Status](https://beta.gemnasium.com/badges/github.com/AuthMe/AuthMeReloaded.svg)](https://beta.gemnasium.com/projects/github.com/AuthMe/AuthMeReloaded) | ## Description diff --git a/pom.xml b/pom.xml index a04861c2..7182cfbb 100644 --- a/pom.xml +++ b/pom.xml @@ -441,7 +441,7 @@ com.zaxxer HikariCP - 2.7.8 + 3.1.0 true From ba4ed7bdd97355ef24bdb72c8aaa961bce5f79b5 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Thu, 19 Apr 2018 11:10:02 +0200 Subject: [PATCH 23/63] Update Mockito --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 7182cfbb..c971d156 100644 --- a/pom.xml +++ b/pom.xml @@ -820,7 +820,7 @@ org.mockito mockito-core test - 2.16.0 + 2.18.0 hamcrest-core @@ -839,7 +839,7 @@ com.h2database h2 - 1.4.196 + 1.4.197 test From 6e16abc34e2d417982a80167e970c7e4d7012039 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Thu, 19 Apr 2018 11:45:21 +0200 Subject: [PATCH 24/63] Don't purge users if unable to load permission data --- .../xephi/authme/listener/PlayerListener.java | 12 +++- .../authme/permission/PermissionsManager.java | 62 +++++++++++++++---- .../permission/handlers/LuckPermsHandler.java | 9 ++- .../handlers/PermissionHandler.java | 5 +- .../handlers/PermissionLoadUserException.java | 13 ++++ .../authme/task/purge/PurgeExecutor.java | 10 ++- .../fr/xephi/authme/task/purge/PurgeTask.java | 8 +-- 7 files changed, 85 insertions(+), 34 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/permission/handlers/PermissionLoadUserException.java diff --git a/src/main/java/fr/xephi/authme/listener/PlayerListener.java b/src/main/java/fr/xephi/authme/listener/PlayerListener.java index 7857114e..5b632712 100644 --- a/src/main/java/fr/xephi/authme/listener/PlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/PlayerListener.java @@ -1,11 +1,13 @@ package fr.xephi.authme.listener; +import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.QuickCommandsProtectionManager; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.message.Messages; import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.permission.handlers.PermissionLoadUserException; import fr.xephi.authme.process.Management; import fr.xephi.authme.service.AntiBotService; import fr.xephi.authme.service.BukkitService; @@ -260,9 +262,13 @@ public class PlayerListener implements Listener { // Keep pre-UUID compatibility try { - permissionsManager.loadUserData(event.getUniqueId()); - } catch (NoSuchMethodError e) { - permissionsManager.loadUserData(name); + try { + permissionsManager.loadUserData(event.getUniqueId()); + } catch (NoSuchMethodError e) { + permissionsManager.loadUserData(name); + } + } catch (PermissionLoadUserException e) { + ConsoleLogger.logException("Unable to load the permission data of user " + name, e); } try { diff --git a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java index 55845f74..ec1fff9b 100644 --- a/src/main/java/fr/xephi/authme/permission/PermissionsManager.java +++ b/src/main/java/fr/xephi/authme/permission/PermissionsManager.java @@ -8,6 +8,7 @@ import fr.xephi.authme.permission.handlers.BPermissionsHandler; import fr.xephi.authme.permission.handlers.LuckPermsHandler; import fr.xephi.authme.permission.handlers.PermissionHandler; import fr.xephi.authme.permission.handlers.PermissionHandlerException; +import fr.xephi.authme.permission.handlers.PermissionLoadUserException; import fr.xephi.authme.permission.handlers.PermissionsExHandler; import fr.xephi.authme.permission.handlers.VaultHandler; import fr.xephi.authme.permission.handlers.ZPermissionsHandler; @@ -110,7 +111,9 @@ public class PermissionsManager implements Reloadable { * Creates a permission handler for the provided permission systems if possible. * * @param type the permission systems type for which to create a corresponding permission handler + * * @return the permission handler, or {@code null} if not possible + * * @throws PermissionHandlerException during initialization of the permission handler */ private PermissionHandler createPermissionHandler(PermissionsSystemType type) throws PermissionHandlerException { @@ -228,8 +231,9 @@ public class PermissionsManager implements Reloadable { /** * Check if the given player has permission for the given permission node. * - * @param joiningPlayer The player to check + * @param joiningPlayer The player to check * @param permissionNode The permission node to verify + * * @return true if the player has permission, false otherwise */ public boolean hasPermission(JoiningPlayer joiningPlayer, PermissionNode permissionNode) { @@ -262,7 +266,7 @@ public class PermissionsManager implements Reloadable { * Check whether the offline player with the given name has permission for the given permission node. * This method is used as a last resort when nothing besides the name is known. * - * @param name The name of the player + * @param name The name of the player * @param permissionNode The permission node to verify * * @return true if the player has permission, false otherwise @@ -317,7 +321,7 @@ public class PermissionsManager implements Reloadable { * @param groupName The group name. * * @return True if the player is in the specified group, false otherwise. - * False is also returned if groups aren't supported by the used permissions system. + * False is also returned if groups aren't supported by the used permissions system. */ public boolean isInGroup(OfflinePlayer player, String groupName) { return isEnabled() && handler.isInGroup(player, groupName); @@ -330,7 +334,7 @@ public class PermissionsManager implements Reloadable { * @param groupName The name of the group. * * @return True if succeed, false otherwise. - * False is also returned if this feature isn't supported for the current permissions system. + * False is also returned if this feature isn't supported for the current permissions system. */ public boolean addGroup(OfflinePlayer player, String groupName) { if (!isEnabled() || StringUtils.isEmpty(groupName)) { @@ -346,7 +350,7 @@ public class PermissionsManager implements Reloadable { * @param groupNames The name of the groups to add. * * @return True if at least one group was added, false otherwise. - * False is also returned if this feature isn't supported for the current permissions system. + * False is also returned if this feature isn't supported for the current permissions system. */ public boolean addGroups(OfflinePlayer player, Collection groupNames) { // If no permissions system is used, return false @@ -373,7 +377,7 @@ public class PermissionsManager implements Reloadable { * @param groupName The name of the group. * * @return True if succeed, false otherwise. - * False is also returned if this feature isn't supported for the current permissions system. + * False is also returned if this feature isn't supported for the current permissions system. */ public boolean removeGroup(OfflinePlayer player, String groupName) { return isEnabled() && handler.removeFromGroup(player, groupName); @@ -386,7 +390,7 @@ public class PermissionsManager implements Reloadable { * @param groupNames The name of the groups to remove. * * @return True if at least one group was removed, false otherwise. - * False is also returned if this feature isn't supported for the current permissions system. + * False is also returned if this feature isn't supported for the current permissions system. */ public boolean removeGroups(OfflinePlayer player, Collection groupNames) { // If no permissions system is used, return false @@ -414,7 +418,7 @@ public class PermissionsManager implements Reloadable { * @param groupName The name of the group. * * @return True if succeed, false otherwise. - * False is also returned if this feature isn't supported for the current permissions system. + * False is also returned if this feature isn't supported for the current permissions system. */ public boolean setGroup(OfflinePlayer player, String groupName) { return isEnabled() && handler.setGroup(player, groupName); @@ -428,7 +432,7 @@ public class PermissionsManager implements Reloadable { * @param player The player to remove all groups from. * * @return True if succeed, false otherwise. - * False will also be returned if this feature isn't supported for the used permissions system. + * False will also be returned if this feature isn't supported for the used permissions system. */ public boolean removeAllGroups(OfflinePlayer player) { // If no permissions system is used, return false @@ -443,15 +447,47 @@ public class PermissionsManager implements Reloadable { return removeGroups(player, groupNames); } - public void loadUserData(UUID uuid) { - if(!isEnabled()) { + /** + * Loads the permission data of the given player. + * + * @param offlinePlayer the offline player. + * @return true if the load was successful. + */ + public boolean loadUserData(OfflinePlayer offlinePlayer) { + try { + try { + loadUserData(offlinePlayer.getUniqueId()); + } catch (NoSuchMethodError e) { + loadUserData(offlinePlayer.getName()); + } + } catch (PermissionLoadUserException e) { + ConsoleLogger.logException("Unable to load the permission data of user " + offlinePlayer.getName(), e); + return false; + } + return true; + } + + /** + * Loads the permission data of the given player unique identifier. + * + * @param uuid the {@link UUID} of the player. + * @throws PermissionLoadUserException if the action failed. + */ + public void loadUserData(UUID uuid) throws PermissionLoadUserException { + if (!isEnabled()) { return; } handler.loadUserData(uuid); } - public void loadUserData(String name) { - if(!isEnabled()) { + /** + * Loads the permission data of the given player name. + * + * @param name the name of the player. + * @throws PermissionLoadUserException if the action failed. + */ + public void loadUserData(String name) throws PermissionLoadUserException { + if (!isEnabled()) { return; } handler.loadUserData(name); diff --git a/src/main/java/fr/xephi/authme/permission/handlers/LuckPermsHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/LuckPermsHandler.java index f01f14c3..0d465ef3 100644 --- a/src/main/java/fr/xephi/authme/permission/handlers/LuckPermsHandler.java +++ b/src/main/java/fr/xephi/authme/permission/handlers/LuckPermsHandler.java @@ -189,22 +189,21 @@ public class LuckPermsHandler implements PermissionHandler { } @Override - public void loadUserData(UUID uuid) { + public void loadUserData(UUID uuid) throws PermissionLoadUserException { try { luckPermsApi.getUserManager().loadUser(uuid).get(5, TimeUnit.SECONDS); } catch (InterruptedException | ExecutionException | TimeoutException e) { - e.printStackTrace(); + throw new PermissionLoadUserException("Unable to load the permission data of the user " + uuid, e); } } @Override - public void loadUserData(String name) { + public void loadUserData(String name) throws PermissionLoadUserException { try { UUID uuid = luckPermsApi.getStorage().getUUID(name).get(5, TimeUnit.SECONDS); loadUserData(uuid); } catch (InterruptedException | ExecutionException | TimeoutException e) { - e.printStackTrace(); + throw new PermissionLoadUserException("Unable to load the permission data of the user " + name, e); } } - } diff --git a/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java b/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java index fe3f5405..831bc583 100644 --- a/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java +++ b/src/main/java/fr/xephi/authme/permission/handlers/PermissionHandler.java @@ -107,10 +107,9 @@ public interface PermissionHandler { */ PermissionsSystemType getPermissionSystem(); - default void loadUserData(UUID uuid) { + default void loadUserData(UUID uuid) throws PermissionLoadUserException { } - default void loadUserData(String name) { + default void loadUserData(String name) throws PermissionLoadUserException { } - } diff --git a/src/main/java/fr/xephi/authme/permission/handlers/PermissionLoadUserException.java b/src/main/java/fr/xephi/authme/permission/handlers/PermissionLoadUserException.java new file mode 100644 index 00000000..697b4918 --- /dev/null +++ b/src/main/java/fr/xephi/authme/permission/handlers/PermissionLoadUserException.java @@ -0,0 +1,13 @@ +package fr.xephi.authme.permission.handlers; + +import java.util.UUID; + +/** + * Exception thrown when a {@link PermissionHandler#loadUserData(UUID uuid)} request fails. + */ +public class PermissionLoadUserException extends Exception { + + public PermissionLoadUserException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/fr/xephi/authme/task/purge/PurgeExecutor.java b/src/main/java/fr/xephi/authme/task/purge/PurgeExecutor.java index a3e42f75..36c951ff 100644 --- a/src/main/java/fr/xephi/authme/task/purge/PurgeExecutor.java +++ b/src/main/java/fr/xephi/authme/task/purge/PurgeExecutor.java @@ -49,7 +49,7 @@ public class PurgeExecutor { * players and names. * * @param players the players to purge - * @param names names to purge + * @param names names to purge */ public void executePurge(Collection players, Collection names) { // Purge other data @@ -212,15 +212,13 @@ public class PurgeExecutor { } for (OfflinePlayer offlinePlayer : cleared) { - try { - permissionsManager.loadUserData(offlinePlayer.getUniqueId()); - } catch (NoSuchMethodError e) { - permissionsManager.loadUserData(offlinePlayer.getName()); + if (!permissionsManager.loadUserData(offlinePlayer)) { + ConsoleLogger.warning("Unable to purge the permissions of user " + offlinePlayer + "!"); + continue; } permissionsManager.removeAllGroups(offlinePlayer); } ConsoleLogger.info("AutoPurge: Removed permissions from " + cleared.size() + " player(s)."); } - } diff --git a/src/main/java/fr/xephi/authme/task/purge/PurgeTask.java b/src/main/java/fr/xephi/authme/task/purge/PurgeTask.java index 27b42415..686bab86 100644 --- a/src/main/java/fr/xephi/authme/task/purge/PurgeTask.java +++ b/src/main/java/fr/xephi/authme/task/purge/PurgeTask.java @@ -3,6 +3,7 @@ package fr.xephi.authme.task.purge; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.permission.PlayerStatePermission; +import fr.xephi.authme.permission.handlers.PermissionLoadUserException; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.OfflinePlayer; @@ -73,10 +74,9 @@ class PurgeTask extends BukkitRunnable { OfflinePlayer offlinePlayer = offlinePlayers[nextPosition]; if (offlinePlayer.getName() != null && toPurge.remove(offlinePlayer.getName().toLowerCase())) { - try { - permissionsManager.loadUserData(offlinePlayer.getUniqueId()); - } catch (NoSuchMethodError e) { - permissionsManager.loadUserData(offlinePlayer.getName()); + if(!permissionsManager.loadUserData(offlinePlayer)) { + ConsoleLogger.warning("Unable to check if the user " + offlinePlayer.getName() + " can be purged!"); + continue; } if (!permissionsManager.hasPermissionOffline(offlinePlayer, PlayerStatePermission.BYPASS_PURGE)) { playerPortion.add(offlinePlayer); From d533f8e19c1af8d3788efb77ea0cb7a27263ec7f Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Thu, 19 Apr 2018 12:09:07 +0200 Subject: [PATCH 25/63] Fix unit testing whoops --- .../xephi/authme/task/purge/PurgeTaskTest.java | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/test/java/fr/xephi/authme/task/purge/PurgeTaskTest.java b/src/test/java/fr/xephi/authme/task/purge/PurgeTaskTest.java index 93538c85..2ecf6a13 100644 --- a/src/test/java/fr/xephi/authme/task/purge/PurgeTaskTest.java +++ b/src/test/java/fr/xephi/authme/task/purge/PurgeTaskTest.java @@ -216,18 +216,16 @@ public class PurgeTaskTest { private void setPermissionsBehavior() { given(permissionsManager.hasPermissionOffline(any(OfflinePlayer.class), eq(BYPASS_NODE))) - .willAnswer(new Answer() { - @Override - public Boolean answer(InvocationOnMock invocationOnMock) throws Throwable { - OfflinePlayer player = invocationOnMock.getArgument(0); - Boolean hasPermission = playerBypassAssignments.get(player); - if (hasPermission == null) { - throw new IllegalStateException("Unexpected check of '" + BYPASS_NODE - + "' with player = " + player); - } - return hasPermission; + .willAnswer((Answer) invocationOnMock -> { + OfflinePlayer player = invocationOnMock.getArgument(0); + Boolean hasPermission = playerBypassAssignments.get(player); + if (hasPermission == null) { + throw new IllegalStateException("Unexpected check of '" + BYPASS_NODE + + "' with player = " + player); } + return hasPermission; }); + given(permissionsManager.loadUserData(any(OfflinePlayer.class))).willReturn(true); } private void assertRanPurgeWithPlayers(OfflinePlayer... players) { From bebff1c0c87d2302d82335768eeed80f897e305f Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Thu, 19 Apr 2018 12:17:08 +0200 Subject: [PATCH 26/63] Actually provide a config to circleci Epic fail xD --- .circleci/{circle.yml => config.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .circleci/{circle.yml => config.yml} (100%) diff --git a/.circleci/circle.yml b/.circleci/config.yml similarity index 100% rename from .circleci/circle.yml rename to .circleci/config.yml From 65a1438c479b600161dce827a27ce49e603b5078 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Thu, 19 Apr 2018 12:19:59 +0200 Subject: [PATCH 27/63] Fix circleci config format --- .circleci/config.yml | 92 ++++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index cb1e2ba6..b544c67a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,49 +1,49 @@ version: 2 jobs: - build_and_test_jdk8: - working_directory: ~/authmereloaded-jdk8 - docker: - - image: circleci/openjdk:8-jdk - environment: - MAVEN_OPTS: -Xmx2048m - steps: - - checkout - - restore_cache: - keys: - - authmereloaded-{{ checksum "pom.xml" }} - - authmereloaded- - - run: mvn -T 2 dependency:go-offline - - save_cache: - paths: - - ~/.m2 - key: authmereloaded-{{ checksum "pom.xml" }} - - run: mvn -T 2 package - - store_test_results: - path: target/surefire-reports - - store_artifacts: - path: target/*.jar - build_and_test_jdk9: - working_directory: ~/authmereloaded-jdk9 - docker: - - image: circleci/openjdk:9-jdk - environment: - MAVEN_OPTS: -Xmx2048m - steps: - - checkout - - restore_cache: - key: authmereloaded-{{ checksum "pom.xml" }} - - run: mvn -T 2 dependency:go-offline - - save_cache: - paths: - - ~/.m2 - key: authmereloaded-{{ checksum "pom.xml" }} - - run: mvn -T 2 package - - store_test_results: - path: target/surefire-reports - - run: cp ./target/*.jar $CIRCLE_ARTIFACTS + build_and_test_jdk8: + working_directory: ~/authmereloaded-jdk8 + docker: + - image: circleci/openjdk:8-jdk + environment: + MAVEN_OPTS: -Xmx2048m + steps: + - checkout + - restore_cache: + keys: + - authmereloaded-{{ checksum "pom.xml" }} + - authmereloaded- + - run: mvn -T 2 dependency:go-offline + - save_cache: + paths: + - ~/.m2 + key: authmereloaded-{{ checksum "pom.xml" }} + - run: mvn -T 2 package + - store_test_results: + path: target/surefire-reports + - store_artifacts: + path: target/*.jar + build_and_test_jdk9: + working_directory: ~/authmereloaded-jdk9 + docker: + - image: circleci/openjdk:9-jdk + environment: + MAVEN_OPTS: -Xmx2048m + steps: + - checkout + - restore_cache: + key: authmereloaded-{{ checksum "pom.xml" }} + - run: mvn -T 2 dependency:go-offline + - save_cache: + paths: + - ~/.m2 + key: authmereloaded-{{ checksum "pom.xml" }} + - run: mvn -T 2 package + - store_test_results: + path: target/surefire-reports + - run: cp ./target/*.jar $CIRCLE_ARTIFACTS workflows: - version: 2 - build_and_test: - jobs: - - build_and_test_jdk8 - - build_and_test_jdk9 + version: 2 + build_and_test: + jobs: + - build_and_test_jdk8 + - build_and_test_jdk9 From 8722a3dbab42454e4423f82c15e522912478c5af Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Thu, 19 Apr 2018 12:29:30 +0200 Subject: [PATCH 28/63] Improve circle configuration file --- .circleci/config.yml | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b544c67a..3fbbaae4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,41 +9,44 @@ jobs: steps: - checkout - restore_cache: - keys: + keys: - authmereloaded-{{ checksum "pom.xml" }} - authmereloaded- - run: mvn -T 2 dependency:go-offline - save_cache: - paths: - - ~/.m2 - key: authmereloaded-{{ checksum "pom.xml" }} + paths: + - ~/.m2 + key: authmereloaded-{{ checksum "pom.xml" }} - run: mvn -T 2 package - store_test_results: - path: target/surefire-reports + path: target/surefire-reports - store_artifacts: - path: target/*.jar - build_and_test_jdk9: - working_directory: ~/authmereloaded-jdk9 + path: target/*.jar + build_and_test_jdk10: + working_directory: ~/authmereloaded-jdk10 docker: - - image: circleci/openjdk:9-jdk + - image: circleci/openjdk:10-jdk environment: - MAVEN_OPTS: -Xmx2048m + MAVEN_OPTS: -Xmx2048m steps: - checkout - restore_cache: - key: authmereloaded-{{ checksum "pom.xml" }} + keys: + - authmereloaded-{{ checksum "pom.xml" }} + - authmereloaded- - run: mvn -T 2 dependency:go-offline - save_cache: - paths: - - ~/.m2 - key: authmereloaded-{{ checksum "pom.xml" }} + paths: + - ~/.m2 + key: authmereloaded-{{ checksum "pom.xml" }} - run: mvn -T 2 package - store_test_results: - path: target/surefire-reports - - run: cp ./target/*.jar $CIRCLE_ARTIFACTS + path: target/surefire-reports + - store_artifacts: + path: target/*.jar workflows: version: 2 build_and_test: jobs: - build_and_test_jdk8 - - build_and_test_jdk9 + - build_and_test_jdk10 From 65ad91372e05c9ac3525b58cbf347919ce852b3f Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Thu, 19 Apr 2018 12:32:50 +0200 Subject: [PATCH 29/63] Fix JDK 10 surefire plugin, use batch mode in circleci --- .circleci/config.yml | 8 ++++---- pom.xml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 3fbbaae4..c988f79c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,12 +12,12 @@ jobs: keys: - authmereloaded-{{ checksum "pom.xml" }} - authmereloaded- - - run: mvn -T 2 dependency:go-offline + - run: mvn -T 2 -B dependency:go-offline - save_cache: paths: - ~/.m2 key: authmereloaded-{{ checksum "pom.xml" }} - - run: mvn -T 2 package + - run: mvn -T 2 -B package - store_test_results: path: target/surefire-reports - store_artifacts: @@ -34,12 +34,12 @@ jobs: keys: - authmereloaded-{{ checksum "pom.xml" }} - authmereloaded- - - run: mvn -T 2 dependency:go-offline + - run: mvn -T 2 -B dependency:go-offline - save_cache: paths: - ~/.m2 key: authmereloaded-{{ checksum "pom.xml" }} - - run: mvn -T 2 package + - run: mvn -T 2 -B package - store_test_results: path: target/surefire-reports - store_artifacts: diff --git a/pom.xml b/pom.xml index c971d156..c6756e67 100644 --- a/pom.xml +++ b/pom.xml @@ -169,7 +169,7 @@ org.apache.maven.plugins maven-surefire-plugin - 2.20.1 + 2.21.0 From 5194f76f39b4d8ad5d01a4c0db00dd5725bdb6d7 Mon Sep 17 00:00:00 2001 From: RikoDEV Date: Sat, 21 Apr 2018 02:24:27 +0200 Subject: [PATCH 30/63] Update of the Polish translation by RikoDEV (#1560) --- src/main/resources/messages/messages_pl.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/messages/messages_pl.yml b/src/main/resources/messages/messages_pl.yml index 41272a08..6184f8e2 100644 --- a/src/main/resources/messages/messages_pl.yml +++ b/src/main/resources/messages/messages_pl.yml @@ -99,8 +99,8 @@ email: send_failure: 'Nie można wysłać e-maila. Skontaktuj się z administracją.' change_password_expired: 'Nie zmienisz już hasła przy użyciu tej komendy.' email_cooldown_error: '&cE-mail został wysłany, musisz poczekać %time przed wysłaniem następnego.' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' + add_not_allowed: '&cMożliwość dodania adresu e-mail jest wyłączona.' + change_not_allowed: '&cMożliwość zmiany adresu e-mail jest wyłączona.' # Password recovery by email recovery: From baec0349097c1b88ee44d1b680ee5b57888f435d Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Sat, 21 Apr 2018 13:02:14 +0200 Subject: [PATCH 31/63] #1555 Add RegisterEvent and AuthMeAsyncPreRegisterEvent (#1559) * #1555 Add RegisterEvent and AuthMeAsyncPreRegisterEvent * Add missing javadoc --- .../events/AuthMeAsyncPreRegisterEvent.java | 70 +++++++++++++++++++ .../fr/xephi/authme/events/RegisterEvent.java | 47 +++++++++++++ .../process/register/AsyncRegister.java | 30 ++++++-- .../register/ProcessSyncEmailRegister.java | 6 ++ .../register/ProcessSyncPasswordRegister.java | 6 ++ .../process/login/AsynchronousLoginTest.java | 9 +-- .../process/register/AsyncRegisterTest.java | 34 +++++++++ 7 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 src/main/java/fr/xephi/authme/events/AuthMeAsyncPreRegisterEvent.java create mode 100644 src/main/java/fr/xephi/authme/events/RegisterEvent.java diff --git a/src/main/java/fr/xephi/authme/events/AuthMeAsyncPreRegisterEvent.java b/src/main/java/fr/xephi/authme/events/AuthMeAsyncPreRegisterEvent.java new file mode 100644 index 00000000..af26ad51 --- /dev/null +++ b/src/main/java/fr/xephi/authme/events/AuthMeAsyncPreRegisterEvent.java @@ -0,0 +1,70 @@ +package fr.xephi.authme.events; + +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * This event is called when a player uses the register command, + * it's fired even when a user does a /register with invalid arguments. + * {@link #setCanRegister(boolean) event.setCanRegister(false)} prevents the player from registering. + */ +public class AuthMeAsyncPreRegisterEvent extends CustomEvent { + + private static final HandlerList handlers = new HandlerList(); + private final Player player; + private boolean canRegister = true; + + /** + * Constructor. + * + * @param player The player + * @param isAsync True if the event is async, false otherwise + */ + public AuthMeAsyncPreRegisterEvent(Player player, boolean isAsync) { + super(isAsync); + this.player = player; + } + + /** + * Return the player concerned by this event. + * + * @return The player who executed a valid {@code /login} command + */ + public Player getPlayer() { + return player; + } + + /** + * Return whether the player is allowed to register. + * + * @return True if the player can log in, false otherwise + */ + public boolean canRegister() { + return canRegister; + } + + /** + * Define whether or not the player may register. + * + * @param canRegister True to allow the player to log in; false to prevent him + */ + public void setCanRegister(boolean canRegister) { + this.canRegister = canRegister; + } + + /** + * Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link Event}. + * + * @return The list of handlers + */ + public static HandlerList getHandlerList() { + return handlers; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + +} diff --git a/src/main/java/fr/xephi/authme/events/RegisterEvent.java b/src/main/java/fr/xephi/authme/events/RegisterEvent.java new file mode 100644 index 00000000..2a98d054 --- /dev/null +++ b/src/main/java/fr/xephi/authme/events/RegisterEvent.java @@ -0,0 +1,47 @@ +package fr.xephi.authme.events; + +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * Event fired when a player has successfully registered. + */ +public class RegisterEvent extends CustomEvent { + + private static final HandlerList handlers = new HandlerList(); + private final Player player; + + /** + * Constructor. + * + * @param player The player + */ + public RegisterEvent(Player player) { + this.player = player; + } + + /** + * Return the player that has successfully logged in or registered. + * + * @return The player + */ + public Player getPlayer() { + return player; + } + + /** + * Return the list of handlers, equivalent to {@link #getHandlers()} and required by {@link Event}. + * + * @return The list of handlers + */ + public static HandlerList getHandlerList() { + return handlers; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + +} diff --git a/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java b/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java index d50fe9a3..78eaa567 100644 --- a/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java @@ -4,14 +4,17 @@ import ch.jalu.injector.factory.SingletonStore; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.events.AuthMeAsyncPreRegisterEvent; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.process.AsynchronousProcess; import fr.xephi.authme.process.register.executors.RegistrationExecutor; import fr.xephi.authme.process.register.executors.RegistrationMethod; import fr.xephi.authme.process.register.executors.RegistrationParameters; +import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.CommonService; import fr.xephi.authme.service.bungeecord.BungeeSender; import fr.xephi.authme.service.bungeecord.MessageType; +import fr.xephi.authme.settings.properties.PluginSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.util.PlayerUtils; @@ -32,6 +35,8 @@ public class AsyncRegister implements AsynchronousProcess { @Inject private PlayerCache playerCache; @Inject + private BukkitService bukkitService; + @Inject private CommonService service; @Inject private SingletonStore registrationExecutorFactory; @@ -44,9 +49,9 @@ public class AsyncRegister implements AsynchronousProcess { /** * Performs the registration process for the given player. * - * @param variant the registration method + * @param variant the registration method * @param parameters the parameters - * @param

parameters type + * @param

parameters type */ public

void register(RegistrationMethod

variant, P parameters) { if (preRegisterCheck(parameters.getPlayer())) { @@ -57,6 +62,13 @@ public class AsyncRegister implements AsynchronousProcess { } } + /** + * Checks if the player is able to register, in that case the {@link AuthMeAsyncPreRegisterEvent} is invoked. + * + * @param player the player which is trying to register. + * + * @return true if the checks are successful and the event hasn't marked the action as denied, false otherwise. + */ private boolean preRegisterCheck(Player player) { final String name = player.getName().toLowerCase(); if (playerCache.isAuthenticated(name)) { @@ -70,6 +82,13 @@ public class AsyncRegister implements AsynchronousProcess { return false; } + boolean isAsync = service.getProperty(PluginSettings.USE_ASYNC_TASKS); + AuthMeAsyncPreRegisterEvent event = new AuthMeAsyncPreRegisterEvent(player, isAsync); + bukkitService.callEvent(event); + if (!event.canRegister()) { + return false; + } + return isPlayerIpAllowedToRegister(player); } @@ -77,11 +96,11 @@ public class AsyncRegister implements AsynchronousProcess { * Executes the registration. * * @param parameters the registration parameters - * @param executor the executor to perform the registration process with - * @param

registration params type + * @param executor the executor to perform the registration process with + * @param

registration params type */ private

- void executeRegistration(P parameters, RegistrationExecutor

executor) { + void executeRegistration(P parameters, RegistrationExecutor

executor) { PlayerAuth auth = executor.buildPlayerAuth(parameters); if (database.saveAuth(auth)) { executor.executePostPersistAction(parameters); @@ -95,6 +114,7 @@ public class AsyncRegister implements AsynchronousProcess { * Checks whether the registration threshold has been exceeded for the given player's IP address. * * @param player the player to check + * * @return true if registration may take place, false otherwise (IP check failed) */ private boolean isPlayerIpAllowedToRegister(Player player) { diff --git a/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java b/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java index 6ab4a874..e740a0de 100644 --- a/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/ProcessSyncEmailRegister.java @@ -2,8 +2,10 @@ package fr.xephi.authme.process.register; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.limbo.LimboService; +import fr.xephi.authme.events.RegisterEvent; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.process.SynchronousProcess; +import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.CommonService; import fr.xephi.authme.util.PlayerUtils; import org.bukkit.entity.Player; @@ -15,6 +17,9 @@ import javax.inject.Inject; */ public class ProcessSyncEmailRegister implements SynchronousProcess { + @Inject + private BukkitService bukkitService; + @Inject private CommonService service; @@ -34,6 +39,7 @@ public class ProcessSyncEmailRegister implements SynchronousProcess { limboService.replaceTasksAfterRegistration(player); player.saveData(); + bukkitService.callEvent(new RegisterEvent(player)); ConsoleLogger.fine(player.getName() + " registered " + PlayerUtils.getPlayerIp(player)); } diff --git a/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java b/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java index 30e7f59a..dc8aa136 100644 --- a/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/ProcessSyncPasswordRegister.java @@ -2,8 +2,10 @@ package fr.xephi.authme.process.register; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.limbo.LimboService; +import fr.xephi.authme.events.RegisterEvent; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.process.SynchronousProcess; +import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.CommonService; import fr.xephi.authme.service.bungeecord.BungeeSender; import fr.xephi.authme.settings.commandconfig.CommandManager; @@ -31,6 +33,9 @@ public class ProcessSyncPasswordRegister implements SynchronousProcess { @Inject private CommandManager commandManager; + @Inject + private BukkitService bukkitService; + ProcessSyncPasswordRegister() { } @@ -60,6 +65,7 @@ public class ProcessSyncPasswordRegister implements SynchronousProcess { } player.saveData(); + bukkitService.callEvent(new RegisterEvent(player)); ConsoleLogger.fine(player.getName() + " registered " + PlayerUtils.getPlayerIp(player)); // Kick Player after Registration is enabled, kick the player diff --git a/src/test/java/fr/xephi/authme/process/login/AsynchronousLoginTest.java b/src/test/java/fr/xephi/authme/process/login/AsynchronousLoginTest.java index 9f21b7ba..9b57fb31 100644 --- a/src/test/java/fr/xephi/authme/process/login/AsynchronousLoginTest.java +++ b/src/test/java/fr/xephi/authme/process/login/AsynchronousLoginTest.java @@ -156,12 +156,9 @@ public class AsynchronousLoginTest { given(commonService.getProperty(DatabaseSettings.MYSQL_COL_GROUP)).willReturn(""); given(commonService.getProperty(PluginSettings.USE_ASYNC_TASKS)).willReturn(true); doReturn(false).when(asynchronousLogin).hasReachedMaxLoggedInPlayersForIp(any(Player.class), anyString()); - doAnswer(new Answer() { - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - ((AuthMeAsyncPreLoginEvent) invocation.getArgument(0)).setCanLogin(false); - return null; - } + doAnswer((Answer) invocation -> { + ((AuthMeAsyncPreLoginEvent) invocation.getArgument(0)).setCanLogin(false); + return null; }).when(bukkitService).callEvent(any(AuthMeAsyncPreLoginEvent.class)); // when diff --git a/src/test/java/fr/xephi/authme/process/register/AsyncRegisterTest.java b/src/test/java/fr/xephi/authme/process/register/AsyncRegisterTest.java index 77f91ca8..59c384bb 100644 --- a/src/test/java/fr/xephi/authme/process/register/AsyncRegisterTest.java +++ b/src/test/java/fr/xephi/authme/process/register/AsyncRegisterTest.java @@ -4,12 +4,15 @@ import ch.jalu.injector.factory.SingletonStore; import fr.xephi.authme.TestHelper; import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.events.AuthMeAsyncPreRegisterEvent; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.process.register.executors.PasswordRegisterParams; import fr.xephi.authme.process.register.executors.RegistrationExecutor; import fr.xephi.authme.process.register.executors.RegistrationMethod; import fr.xephi.authme.process.register.executors.TwoFactorRegisterParams; +import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.settings.properties.PluginSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import org.bukkit.entity.Player; @@ -18,9 +21,11 @@ import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.only; import static org.mockito.Mockito.verify; @@ -40,6 +45,8 @@ public class AsyncRegisterTest { @Mock private CommonService commonService; @Mock + private BukkitService bukkitService; + @Mock private DataSource dataSource; @Mock private SingletonStore registrationExecutorStore; @@ -102,6 +109,32 @@ public class AsyncRegisterTest { @Test @SuppressWarnings("unchecked") public void shouldStopForFailedExecutorCheck() { + // given + String name = "edbert"; + Player player = mockPlayerWithName(name); + TestHelper.mockPlayerIp(player, "33.44.55.66"); + given(playerCache.isAuthenticated(name)).willReturn(false); + given(commonService.getProperty(RegistrationSettings.IS_ENABLED)).willReturn(true); + given(dataSource.isAuthAvailable(name)).willReturn(false); + given(commonService.getProperty(PluginSettings.USE_ASYNC_TASKS)).willReturn(true); + RegistrationExecutor executor = mock(RegistrationExecutor.class); + TwoFactorRegisterParams params = TwoFactorRegisterParams.of(player); + singletonStoreWillReturn(registrationExecutorStore, executor); + doAnswer((Answer) invocation -> { + ((AuthMeAsyncPreRegisterEvent) invocation.getArgument(0)).setCanRegister(false); + return null; + }).when(bukkitService).callEvent(any(AuthMeAsyncPreRegisterEvent.class)); + + // when + asyncRegister.register(RegistrationMethod.TWO_FACTOR_REGISTRATION, params); + + // then + verify(dataSource, only()).isAuthAvailable(name); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldStopForCancelledEvent() { // given String name = "edbert"; Player player = mockPlayerWithName(name); @@ -110,6 +143,7 @@ public class AsyncRegisterTest { given(commonService.getProperty(RegistrationSettings.IS_ENABLED)).willReturn(true); given(commonService.getProperty(RestrictionSettings.MAX_REGISTRATION_PER_IP)).willReturn(0); given(dataSource.isAuthAvailable(name)).willReturn(false); + given(commonService.getProperty(PluginSettings.USE_ASYNC_TASKS)).willReturn(true); RegistrationExecutor executor = mock(RegistrationExecutor.class); TwoFactorRegisterParams params = TwoFactorRegisterParams.of(player); given(executor.isRegistrationAdmitted(params)).willReturn(false); From 29ac3a702207ac080980c9f132716dcdc1e4c4b3 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 22 Apr 2018 11:13:27 +0200 Subject: [PATCH 32/63] #1141 Fixes to TOTP implementation - Revert back to SHA1 as HMAC hash function so that it works with Google authenticator - Add message to user to tell him to run /2fa confirm to add a TOTP code --- docs/commands.md | 13 +++- docs/config.md | 13 +++- docs/permission_nodes.md | 7 ++- docs/translations.md | 62 +++++++++---------- .../authme/command/CommandInitializer.java | 3 +- .../executable/totp/AddTotpCommand.java | 1 + .../fr/xephi/authme/message/MessageKey.java | 25 ++++---- .../security/totp/TotpAuthenticator.java | 8 +-- src/main/resources/messages/messages_bg.yml | 18 +++++- src/main/resources/messages/messages_br.yml | 18 +++++- src/main/resources/messages/messages_cz.yml | 18 +++++- src/main/resources/messages/messages_de.yml | 18 +++++- src/main/resources/messages/messages_en.yml | 1 + src/main/resources/messages/messages_eo.yml | 18 +++++- src/main/resources/messages/messages_es.yml | 18 +++++- src/main/resources/messages/messages_et.yml | 18 +++++- src/main/resources/messages/messages_eu.yml | 18 +++++- src/main/resources/messages/messages_fi.yml | 18 +++++- src/main/resources/messages/messages_fr.yml | 18 +++++- src/main/resources/messages/messages_gl.yml | 18 +++++- src/main/resources/messages/messages_hu.yml | 18 +++++- src/main/resources/messages/messages_id.yml | 18 +++++- src/main/resources/messages/messages_it.yml | 18 +++++- src/main/resources/messages/messages_ko.yml | 18 +++++- src/main/resources/messages/messages_lt.yml | 18 +++++- src/main/resources/messages/messages_nl.yml | 18 +++++- src/main/resources/messages/messages_pl.yml | 18 +++++- src/main/resources/messages/messages_pt.yml | 18 +++++- src/main/resources/messages/messages_ro.yml | 18 +++++- src/main/resources/messages/messages_ru.yml | 18 +++++- src/main/resources/messages/messages_sk.yml | 18 +++++- src/main/resources/messages/messages_tr.yml | 18 +++++- src/main/resources/messages/messages_uk.yml | 18 +++++- src/main/resources/messages/messages_vn.yml | 18 +++++- src/main/resources/messages/messages_zhcn.yml | 18 +++++- src/main/resources/messages/messages_zhhk.yml | 18 +++++- src/main/resources/messages/messages_zhmc.yml | 18 +++++- src/main/resources/messages/messages_zhtw.yml | 18 +++++- src/main/resources/plugin.yml | 4 +- 39 files changed, 510 insertions(+), 149 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index 5bb49c2b..163399c0 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1,5 +1,5 @@ - + ## AuthMe Commands You can use the following commands to use the features of AuthMe. Mandatory arguments are marked with `< >` @@ -85,6 +85,15 @@ brackets; optional arguments are enclosed in square brackets (`[ ]`). - **/changepassword** <oldPassword> <newPassword>: Command to change your password using AuthMeReloaded.
Requires `authme.player.changepassword` - **/changepassword help** [query]: View detailed help for /changepassword commands. +- **/totp**: Performs actions related to two-factor authentication. +- **/totp code** <code>: Processes the two-factor authentication code during login. +- **/totp add**: Enables two-factor authentication for your account. +
Requires `authme.player.totpadd` +- **/totp confirm** <code>: Saves the generated TOTP secret after confirmation. +
Requires `authme.player.totpadd` +- **/totp remove** <code>: Disables two-factor authentication for your account. +
Requires `authme.player.totpremove` +- **/totp help** [query]: View detailed help for /totp commands. - **/captcha** <captcha>: Captcha command for AuthMeReloaded.
Requires `authme.player.captcha` - **/captcha help** [query]: View detailed help for /captcha commands. @@ -95,4 +104,4 @@ brackets; optional arguments are enclosed in square brackets (`[ ]`). --- -This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Fri Feb 02 20:09:14 CET 2018 +This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Sun Apr 22 11:00:10 CEST 2018 diff --git a/docs/config.md b/docs/config.md index 956da56a..51ee86a9 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1,5 +1,5 @@ - + ## AuthMe Configuration The first time you run AuthMe it will create a config.yml file in the plugins/AuthMe folder, @@ -43,6 +43,8 @@ DataSource: mySQLColumnLogged: 'isLogged' # Column for storing if a player has a valid session or not mySQLColumnHasSession: 'hasSession' + # Column for storing a player's TOTP key (for two-factor authentication) + mySQLtotpKey: 'totp' # Column for storing the player's last IP mySQLColumnIp: 'ip' # Column for storing players lastlogins @@ -138,6 +140,8 @@ settings: - '/reg' - '/email' - '/captcha' + - '/2fa' + - '/totp' # Max number of allowed registrations per IP # The value 0 means an unlimited number of registrations! maxRegPerIp: 1 @@ -384,7 +388,7 @@ Protection: # Apply the protection also to registered usernames enableProtectionRegistered: true # Countries allowed to join the server and register. For country codes, see - # http://dev.maxmind.com/geoip/legacy/codes/iso3166/ + # https://dev.maxmind.com/geoip/legacy/codes/iso3166/ # PLEASE USE QUOTES! countries: - 'US' @@ -404,6 +408,9 @@ Protection: antiBotDuration: 10 # Delay in seconds before the antibot activation antiBotDelay: 60 + quickCommands: + # Kicks the player that issued a command before the defined time after the join process + denyCommandsBeforeMilliseconds: 1000 Purge: # If enabled, AuthMe automatically purges old, unused accounts useAutoPurge: false @@ -555,4 +562,4 @@ To change settings on a running server, save your changes to config.yml and use --- -This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Sun Jan 21 18:49:44 CET 2018 +This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Sun Apr 22 11:00:10 CEST 2018 diff --git a/docs/permission_nodes.md b/docs/permission_nodes.md index f3b5edac..828d23b5 100644 --- a/docs/permission_nodes.md +++ b/docs/permission_nodes.md @@ -1,5 +1,5 @@ - + ## AuthMe Permission Nodes The following are the permission nodes that are currently supported by the latest dev builds. @@ -57,13 +57,16 @@ The following are the permission nodes that are currently supported by the lates - **authme.player.email.see** – Command permission to see the own email address. - **authme.player.login** – Command permission to login. - **authme.player.logout** – Command permission to logout. +- **authme.player.protection.quickcommandsprotection** – Permission that enables on join quick commands checks for the player. - **authme.player.register** – Command permission to register. - **authme.player.security.verificationcode** – Permission to use the email verification codes feature. - **authme.player.seeownaccounts** – Permission to use to see own other accounts. +- **authme.player.totpadd** – Permission to enable two-factor authentication. +- **authme.player.totpremove** – Permission to disable two-factor authentication. - **authme.player.unregister** – Command permission to unregister. - **authme.vip** – When the server is full and someone with this permission joins the server, someone will be kicked. --- -This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Fri Dec 01 19:16:17 CET 2017 +This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Sun Apr 22 11:00:13 CEST 2018 diff --git a/docs/translations.md b/docs/translations.md index 2f778c29..66f4c793 100644 --- a/docs/translations.md +++ b/docs/translations.md @@ -1,5 +1,5 @@ - + # AuthMe Translations The following translations are available in AuthMe. Set `messagesLanguage` to the language code @@ -8,37 +8,37 @@ in your config.yml to use the language, or use another language code to start a Code | Language | Translated |   ---- | -------- | ---------: | ------ [en](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_en.yml) | English | 100% | bar -[bg](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_bg.yml) | Bulgarian | 86% | bar -[br](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_br.yml) | Brazilian | 90% | bar -[cz](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_cz.yml) | Czech | 90% | bar -[de](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_de.yml) | German | 90% | bar -[eo](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_eo.yml) | Esperanto | 90% | bar -[es](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_es.yml) | Spanish | 100% | bar -[et](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_et.yml) | Estonian | 90% | bar -[eu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_eu.yml) | Basque | 48% | bar -[fi](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_fi.yml) | Finnish | 51% | bar -[fr](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_fr.yml) | French | 100% | bar -[gl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_gl.yml) | Galician | 54% | bar -[hu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_hu.yml) | Hungarian | 98% | bar -[id](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_id.yml) | Indonesian | 53% | bar -[it](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_it.yml) | Italian | 100% | bar -[ko](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ko.yml) | Korean | 98% | bar -[lt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_lt.yml) | Lithuanian | 40% | bar -[nl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_nl.yml) | Dutch | 90% | bar -[pl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pl.yml) | Polish | 100% | bar -[pt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pt.yml) | Portuguese | 90% | bar -[ro](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ro.yml) | Romanian | 90% | bar -[ru](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ru.yml) | Russian | 98% | bar -[sk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_sk.yml) | Slovakian | 90% | bar -[tr](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_tr.yml) | Turkish | 86% | bar -[uk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_uk.yml) | Ukrainian | 71% | bar -[vn](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_vn.yml) | Vietnamese | 87% | bar -[zhcn](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhcn.yml) | Chinese (China) | 100% | bar -[zhhk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhhk.yml) | Chinese (Hong Kong) | 90% | bar -[zhmc](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhmc.yml) | Chinese (Macau) | 73% | bar -[zhtw](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhtw.yml) | Chinese (Taiwan) | 98% | bar +[bg](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_bg.yml) | Bulgarian | 76% | bar +[br](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_br.yml) | Brazilian | 80% | bar +[cz](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_cz.yml) | Czech | 80% | bar +[de](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_de.yml) | German | 80% | bar +[eo](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_eo.yml) | Esperanto | 80% | bar +[es](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_es.yml) | Spanish | 92% | bar +[et](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_et.yml) | Estonian | 80% | bar +[eu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_eu.yml) | Basque | 42% | bar +[fi](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_fi.yml) | Finnish | 45% | bar +[fr](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_fr.yml) | French | 89% | bar +[gl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_gl.yml) | Galician | 48% | bar +[hu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_hu.yml) | Hungarian | 87% | bar +[id](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_id.yml) | Indonesian | 47% | bar +[it](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_it.yml) | Italian | 92% | bar +[ko](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ko.yml) | Korean | 89% | bar +[lt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_lt.yml) | Lithuanian | 36% | bar +[nl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_nl.yml) | Dutch | 80% | bar +[pl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pl.yml) | Polish | 92% | bar +[pt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pt.yml) | Portuguese | 80% | bar +[ro](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ro.yml) | Romanian | 80% | bar +[ru](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ru.yml) | Russian | 92% | bar +[sk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_sk.yml) | Slovakian | 80% | bar +[tr](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_tr.yml) | Turkish | 76% | bar +[uk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_uk.yml) | Ukrainian | 63% | bar +[vn](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_vn.yml) | Vietnamese | 77% | bar +[zhcn](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhcn.yml) | Chinese (China) | 89% | bar +[zhhk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhhk.yml) | Chinese (Hong Kong) | 80% | bar +[zhmc](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhmc.yml) | Chinese (Macau) | 65% | bar +[zhtw](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_zhtw.yml) | Chinese (Taiwan) | 87% | bar --- -This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Fri Feb 02 20:09:17 CET 2018 +This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Sun Apr 22 11:09:12 CEST 2018 diff --git a/src/main/java/fr/xephi/authme/command/CommandInitializer.java b/src/main/java/fr/xephi/authme/command/CommandInitializer.java index e2450ba8..6dbbcea5 100644 --- a/src/main/java/fr/xephi/authme/command/CommandInitializer.java +++ b/src/main/java/fr/xephi/authme/command/CommandInitializer.java @@ -563,10 +563,11 @@ public class CommandInitializer { // Register the base totp code CommandDescription.builder() - .parent(null) + .parent(totpBase) .labels("code", "c") .description("Command for logging in") .detailedDescription("Processes the two-factor authentication code during login.") + .withArgument("code", "The TOTP code to use to log in", MANDATORY) .executableCommand(TotpCodeCommand.class) .register(); diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java index 9238c3b0..a52741b2 100644 --- a/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java @@ -35,6 +35,7 @@ public class AddTotpCommand extends PlayerCommand { TotpGenerationResult createdTotpInfo = generateTotpService.generateTotpKey(player); messages.send(player, MessageKey.TWO_FACTOR_CREATE, createdTotpInfo.getTotpKey(), createdTotpInfo.getAuthenticatorQrCodeUrl()); + messages.send(player, MessageKey.TWO_FACTOR_CREATE_CONFIRMATION_REQUIRED); } else { messages.send(player, MessageKey.TWO_FACTOR_ALREADY_ENABLED); } diff --git a/src/main/java/fr/xephi/authme/message/MessageKey.java b/src/main/java/fr/xephi/authme/message/MessageKey.java index c2fc5d47..6171f80a 100644 --- a/src/main/java/fr/xephi/authme/message/MessageKey.java +++ b/src/main/java/fr/xephi/authme/message/MessageKey.java @@ -125,7 +125,7 @@ public enum MessageKey { /** Forgot your password? Please use the command: /email recovery <yourEmail> */ FORGOT_PASSWORD_MESSAGE("recovery.forgot_password_hint"), - /** To login you have to solve a captcha code, please use the command: /captcha %captcha_code */ + /** To log in you have to solve a captcha code, please use the command: /captcha %captcha_code */ USAGE_CAPTCHA("captcha.usage_captcha", "%captcha_code"), /** Wrong captcha, please type "/captcha %captcha_code" into the chat! */ @@ -134,7 +134,7 @@ public enum MessageKey { /** Captcha code solved correctly! */ CAPTCHA_SUCCESS("captcha.valid_captcha"), - /** To register you have to solve a captcha code first, please use the command: /captcha %captcha_code */ + /** To register you have to solve a captcha first, please use the command: /captcha %captcha_code */ CAPTCHA_FOR_REGISTRATION_REQUIRED("captcha.captcha_for_registration", "%captcha_code"), /** Valid captcha! You may now register with /register */ @@ -203,7 +203,10 @@ public enum MessageKey { /** Your secret code is %code. You can scan it from here %url */ TWO_FACTOR_CREATE("two_factor.code_created", "%code", "%url"), - /** Please submit your two-factor authentication code with /2fa code <code>. */ + /** Please confirm your code with /2fa confirm <code> */ + TWO_FACTOR_CREATE_CONFIRMATION_REQUIRED("two_factor.confirmation_required"), + + /** Please submit your two-factor authentication code with /2fa code <code> */ TWO_FACTOR_CODE_REQUIRED("two_factor.code_required"), /** Two-factor authentication is already enabled for your account! */ @@ -276,27 +279,21 @@ public enum MessageKey { EMAIL_COOLDOWN_ERROR("email.email_cooldown_error", "%time"), /** - * The command you are trying to execute is sensitive and requires a verification! - * A verification code has been sent to your email, - * run the command "/verification [code]" to verify your identity. + * This command is sensitive and requires an email verification! + * Check your inbox and follow the email's instructions. */ VERIFICATION_CODE_REQUIRED("verification.code_required"), /** Usage: /verification <code> */ USAGE_VERIFICATION_CODE("verification.command_usage"), - /** Incorrect code, please type "/verification <code>" into the chat! */ + /** Incorrect code, please type "/verification <code>" into the chat, using the code you received by email */ INCORRECT_VERIFICATION_CODE("verification.incorrect_code"), - /** - * Your identity has been verified! - * You can now execute every sensitive command within the current session! - */ + /** Your identity has been verified! You can now execute all commands within the current session! */ VERIFICATION_CODE_VERIFIED("verification.success"), - /** - * You can already execute every sensitive command within the current session! - */ + /** You can already execute every sensitive command within the current session! */ VERIFICATION_CODE_ALREADY_VERIFIED("verification.already_verified"), /** Your code has expired! Execute another sensitive command to get a new code! */ diff --git a/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java b/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java index cffc09cd..9fc1c6a2 100644 --- a/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java +++ b/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java @@ -1,11 +1,8 @@ package fr.xephi.authme.security.totp; import com.warrenstrange.googleauth.GoogleAuthenticator; -import com.warrenstrange.googleauth.GoogleAuthenticatorConfig; -import com.warrenstrange.googleauth.GoogleAuthenticatorConfig.GoogleAuthenticatorConfigBuilder; import com.warrenstrange.googleauth.GoogleAuthenticatorKey; import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator; -import com.warrenstrange.googleauth.HmacHashFunction; import com.warrenstrange.googleauth.IGoogleAuthenticator; import fr.xephi.authme.service.BukkitService; import org.bukkit.entity.Player; @@ -30,10 +27,7 @@ public class TotpAuthenticator { * @return new Google Authenticator instance */ protected IGoogleAuthenticator createGoogleAuthenticator() { - GoogleAuthenticatorConfig config = new GoogleAuthenticatorConfigBuilder() - .setHmacHashFunction(HmacHashFunction.HmacSHA512) - .build(); - return new GoogleAuthenticator(config); + return new GoogleAuthenticator(); } /** diff --git a/src/main/resources/messages/messages_bg.yml b/src/main/resources/messages/messages_bg.yml index 3a6f3494..4687a8b9 100644 --- a/src/main/resources/messages/messages_bg.yml +++ b/src/main/resources/messages/messages_bg.yml @@ -60,7 +60,6 @@ misc: logout: '&2Излязохте успешно!' reload: '&2Конфигурацията и база данните бяха презаредени правилно!' usage_change_password: '&cКоманда: /changepassword Стара-Парола Нова-Парола' - two_factor_create: '&2Кода е %code. Можеш да го провериш оттука: %url' accounts_owned_self: 'Претежаваш %count акаунт/а:' accounts_owned_other: 'Потребителят %name има %count акаунт/а:' @@ -90,8 +89,10 @@ email: old_email_invalid: '&cСтарият имейл е грешен, опитайте отново!' invalid: '&cИмейла е невалиден, опитайте с друг!' added: '&2Имейл адреса е добавен!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cМоля потвърди своя имейл адрес!' changed: '&2Имейл адреса е сменен!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2Твоят имейл адрес е: &f%email' no_email_for_account: '&2Няма добавен имейл адрес към акаунта.' already_used: '&4Имейл адреса вече се използва, опитайте с друг.' @@ -99,8 +100,6 @@ email: send_failure: 'Съобщението не беше изпратено. Моля свържете се с администратора.' # TODO change_password_expired: 'You cannot change your password using this command anymore.' email_cooldown_error: '&cВече е бил изпратен имейл адрес. Трябва а изчакаш %time преди да пратиш нов.' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -142,3 +141,16 @@ time: hours: 'часа' day: 'ден' days: 'дена' + +# Two-factor authentication +two_factor: + code_created: '&2Кода е %code. Можеш да го провериш оттука: %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_br.yml b/src/main/resources/messages/messages_br.yml index 69062b23..40c58fc8 100644 --- a/src/main/resources/messages/messages_br.yml +++ b/src/main/resources/messages/messages_br.yml @@ -63,7 +63,6 @@ misc: logout: '&2Desconectado com sucesso!' reload: '&2Configuração e o banco de dados foram recarregados corretamente!' usage_change_password: '&cUse: /changepassword ' - two_factor_create: '&2O seu código secreto é %code. Você pode verificá-lo a partir daqui %url' accounts_owned_self: 'Você tem %count contas:' accounts_owned_other: 'O jogador %name tem %count contas:' @@ -93,8 +92,10 @@ email: old_email_invalid: '&cE-mail velho inválido, tente novamente!' invalid: '&E-mail inválido, tente novamente!' added: '&2Email adicionado com sucesso à sua conta!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cPor favor confirme seu endereço de email!' changed: '&2Troca de email com sucesso.!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2O seu endereço de e-mail atual é: &f%email' no_email_for_account: '&2Você atualmente não têm endereço de e-mail associado a esta conta.' already_used: '&4O endereço de e-mail já está sendo usado' @@ -102,8 +103,6 @@ email: send_failure: '&cO e-mail não pôde ser enviado, reporte isso a um administrador!' change_password_expired: 'Você não pode mais usar esse comando de recuperação de senha!' email_cooldown_error: '&cUm e-mail já foi enviado, espere mais %time antes de enviar novamente!' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -145,3 +144,16 @@ time: hours: 'horas' day: 'dia' days: 'dias' + +# Two-factor authentication +two_factor: + code_created: '&2O seu código secreto é %code. Você pode verificá-lo a partir daqui %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_cz.yml b/src/main/resources/messages/messages_cz.yml index e3bd5c35..025b39f7 100644 --- a/src/main/resources/messages/messages_cz.yml +++ b/src/main/resources/messages/messages_cz.yml @@ -60,7 +60,6 @@ misc: logout: '&cÚspěšně jsi se odhlásil.' reload: '&cZnovu načtení nastavení AuthMe proběhlo úspěšně.' usage_change_password: '&cPoužij: "/changepassword StaréHeslo NovéHeslo".' - two_factor_create: '&2Tvůj tajný kód je %code. Můžeš ho oskenovat zde %url' accounts_owned_self: 'Vlastníš tyto účty (%count):' accounts_owned_other: 'Hráč %name vlastní tyto účty (%count):' @@ -90,8 +89,10 @@ email: old_email_invalid: '[AuthMe] Starý email je chybně zadán!' invalid: '[AuthMe] Nesprávný email' added: '[AuthMe] Email přidán!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '[AuthMe] Potvrď prosím svůj email!' changed: '[AuthMe] Email změněn!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2Váš aktuální email je: &f%email' no_email_for_account: '&2K tomuto účtu nemáte přidanou žádnou emailovou adresu.' already_used: '&4Tato emailová adresa je již používána' @@ -99,8 +100,6 @@ email: send_failure: 'Email nemohl být odeslán. Kontaktujte prosím admina.' change_password_expired: 'Nemůžeš si změnit heslo pomocí toho příkazu.' email_cooldown_error: '&cEmail už byl nedávno odeslán. Musíš čekat %time před odesláním nového.' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -142,3 +141,16 @@ time: hours: 'hodin' day: 'dny' days: 'dnu' + +# Two-factor authentication +two_factor: + code_created: '&2Tvůj tajný kód je %code. Můžeš ho oskenovat zde %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_de.yml b/src/main/resources/messages/messages_de.yml index 97639a9a..00662623 100644 --- a/src/main/resources/messages/messages_de.yml +++ b/src/main/resources/messages/messages_de.yml @@ -60,7 +60,6 @@ misc: logout: '&2Erfolgreich ausgeloggt' reload: '&2Konfiguration und Datenbank wurden erfolgreich neu geladen.' usage_change_password: '&cBenutze: /changepassword ' - two_factor_create: '&2Dein geheimer Code ist %code. Du kannst ihn hier abfragen: %url' accounts_owned_self: 'Du besitzt %count Accounts:' accounts_owned_other: 'Der Spieler %name hat %count Accounts:' @@ -90,8 +89,10 @@ email: old_email_invalid: '&cDie alte E-Mail ist ungültig!' invalid: '&cUngültige E-Mail!' added: '&2E-Mail hinzugefügt!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cBitte bestätige deine E-Mail!' changed: '&2E-Mail aktualisiert!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2Deine aktuelle E-Mail-Adresse ist: &f%email' no_email_for_account: '&2Du hast zur Zeit keine E-Mail-Adresse für deinen Account hinterlegt.' already_used: '&4Diese E-Mail-Adresse wird bereits genutzt.' @@ -99,8 +100,6 @@ email: send_failure: 'Die E-Mail konnte nicht gesendet werden. Bitte kontaktiere einen Administrator.' change_password_expired: 'Mit diesem Befehl kannst du dein Passwort nicht mehr ändern.' email_cooldown_error: '&cEine E-Mail wurde erst kürzlich versendet. Du musst %time warten, bevor du eine neue anfordern kannst.' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -142,3 +141,16 @@ time: hours: 'Stunden' day: 'Tag' days: 'Tage' + +# Two-factor authentication +two_factor: + code_created: '&2Dein geheimer Code ist %code. Du kannst ihn hier abfragen: %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_en.yml b/src/main/resources/messages/messages_en.yml index e32d1e46..917d57cd 100644 --- a/src/main/resources/messages/messages_en.yml +++ b/src/main/resources/messages/messages_en.yml @@ -132,6 +132,7 @@ verification: two_factor: code_created: '&2Your secret code is %code. You can scan it from here %url' + confirmation_required: 'Please confirm your code with /2fa confirm ' code_required: 'Please submit your two-factor authentication code with /2fa code ' already_enabled: 'Two-factor authentication is already enabled for your account!' enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' diff --git a/src/main/resources/messages/messages_eo.yml b/src/main/resources/messages/messages_eo.yml index 390acdca..5636ab72 100644 --- a/src/main/resources/messages/messages_eo.yml +++ b/src/main/resources/messages/messages_eo.yml @@ -60,7 +60,6 @@ misc: logout: '&2Elsalutita sukcese!' reload: '&2Agordo kaj datumbazo estis larditaj korekte!' usage_change_password: '&cUzado: /changepassword ' - two_factor_create: '&2Via sekreta kodo estas %code. Vi povas skani ĝin de tie %url' accounts_owned_self: 'Vi posedas %count kontoj:' accounts_owned_other: 'La ludanto %name havas %count kontojn::' @@ -90,8 +89,10 @@ email: old_email_invalid: '&cNevalida malnovaj retpoŝto, provu denove!' invalid: '&cNevalida retadreso, provu denove!' added: '&2Retpoŝtadreso sukcese aldonitaj al via konto!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cBonvolu konfirmi vian retadreson!' changed: '&2Retpoŝtadreso ŝanĝis ĝuste!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2Via nuna retadreso estas: &f%email' no_email_for_account: '&2Vi aktuale ne havas retadreson asociita kun ĉi tiu konto.' already_used: '&4La retpoŝto jam estas uzata' @@ -99,8 +100,6 @@ email: send_failure: 'La retpoŝto ne estis sendita. Bonvolu kontakti administranto.' change_password_expired: 'Vi ne povas ŝanĝi vian pasvorton per tiu ĉi komando plu.' email_cooldown_error: '&cRetmesaĝon jam sendita lastatempe. Vi devas atendi %time antaŭ vi povas sendi novan.' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -142,3 +141,16 @@ time: hours: 'horoj' day: 'tago' days: 'tagoj' + +# Two-factor authentication +two_factor: + code_created: '&2Via sekreta kodo estas %code. Vi povas skani ĝin de tie %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_es.yml b/src/main/resources/messages/messages_es.yml index 899631cc..6070411f 100644 --- a/src/main/resources/messages/messages_es.yml +++ b/src/main/resources/messages/messages_es.yml @@ -61,7 +61,6 @@ misc: logout: '&cDesconectado correctamente.' reload: '&fLa configuración y la base de datos han sido recargados' usage_change_password: '&fUso: /changepw contraseñaActual contraseñaNueva' - two_factor_create: '&2Tu código secreto es %code. Lo puedes escanear desde aquí %url' accounts_owned_self: 'Eres propietario de %count cuentas:' accounts_owned_other: 'El jugador %name tiene %count cuentas:' @@ -91,8 +90,10 @@ email: old_email_invalid: '[AuthMe] Email anterior inválido!' invalid: '[AuthMe] Email inválido' added: '[AuthMe] Email agregado !' + add_not_allowed: '&cNo se permite añadir un Email' request_confirmation: '[AuthMe] Confirma tu Email !' changed: '[AuthMe] Email cambiado !' + change_not_allowed: '&cNo se permite el cambio de Email' email_show: '&2Tu dirección de E-Mail actual es: &f%email' no_email_for_account: '&2No tienes ningun E-Mail asociado en esta cuenta.' already_used: '&4La dirección Email ya está siendo usada' @@ -100,8 +101,6 @@ email: send_failure: 'No se ha podido enviar el correo electrónico. Por favor, contacta con un administrador.' change_password_expired: 'No puedes cambiar la contraseña utilizando este comando.' email_cooldown_error: '&cEl correo ha sido enviado recientemente. Debes esperar %time antes de volver a enviar uno nuevo.' - add_not_allowed: '&cNo se permite añadir un Email' - change_not_allowed: '&cNo se permite el cambio de Email' # Password recovery by email recovery: @@ -143,3 +142,16 @@ time: hours: 'horas' day: 'día' days: 'días' + +# Two-factor authentication +two_factor: + code_created: '&2Tu código secreto es %code. Lo puedes escanear desde aquí %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_et.yml b/src/main/resources/messages/messages_et.yml index ba5e0b15..3484fae6 100644 --- a/src/main/resources/messages/messages_et.yml +++ b/src/main/resources/messages/messages_et.yml @@ -60,7 +60,6 @@ misc: logout: '&2Edukalt välja logitud!!' reload: '&2Andmebaas uuendatud!' usage_change_password: '&cKasutus: /changepassword ' - two_factor_create: '&2Su salajane kood on %code. Skänni see siin: %url' accounts_owned_self: 'Sa omad %count kontot:' accounts_owned_other: 'Mängijal %name on %count kontot:' @@ -90,8 +89,10 @@ email: old_email_invalid: '&cVale vana meiliaadress, proovi uuesti.' invalid: '&cVale meiliaadress, proovi uuesti.' added: '&2Meiliaadress edukalt vahetatud!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cPalun kinnita oma meiliaadress.' changed: '&2Meiliaadress edukalt vahetatud.' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2Su meiliaadress on: &f%email' no_email_for_account: '&2Selle kasutajaga pole seotud ühtegi meiliaadressi.' already_used: '&4Meiliaadress juba kasutuses.' @@ -99,8 +100,6 @@ email: send_failure: 'Meili ei õnnestunud saata. Kontakteeru meeskonnaga.' change_password_expired: '&3Enam ei saa vahetada oma parooli kasutades seda käsklust.' email_cooldown_error: '&cEmail juba saadeti. Sa pead ootama %time ennem, kui saad uuesti saata.' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -142,3 +141,16 @@ time: hours: 'tundi' day: 'päev' days: 'päeva' + +# Two-factor authentication +two_factor: + code_created: '&2Su salajane kood on %code. Skänni see siin: %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_eu.yml b/src/main/resources/messages/messages_eu.yml index 65bd5575..cfa0f78b 100644 --- a/src/main/resources/messages/messages_eu.yml +++ b/src/main/resources/messages/messages_eu.yml @@ -60,7 +60,6 @@ misc: logout: '&cAtera zara' reload: '&fConfiguration and database has been reloaded' usage_change_password: '&fErabili: /changepassword pasahitzZaharra pasahitzBerria' - # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO accounts_owned_self: 'You own %count accounts:' # TODO accounts_owned_other: 'The player %name has %count accounts:' @@ -90,8 +89,10 @@ email: old_email_invalid: '[AuthMe] Email zaharra okerra!' invalid: '[AuthMe] Email okerrea' added: '[AuthMe] Emaila gehitu duzu !' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '[AuthMe] Konfirmatu zure emaila !' changed: '[AuthMe] Emaila aldatua!' + # TODO change_not_allowed: '&cChanging email was not allowed' # TODO email_show: '&2Your current email address is: &f%email' # TODO no_email_for_account: '&2You currently don''t have email address associated with this account.' # TODO already_used: '&4The email address is already being used' @@ -99,8 +100,6 @@ email: # TODO send_failure: 'The email could not be sent. Please contact an administrator.' # TODO change_password_expired: 'You cannot change your password using this command anymore.' # TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -142,3 +141,16 @@ time: # TODO hours: 'hours' # TODO day: 'day' # TODO days: 'days' + +# Two-factor authentication +two_factor: + # TODO code_created: '&2Your secret code is %code. You can scan it from here %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_fi.yml b/src/main/resources/messages/messages_fi.yml index 95d0a732..00a29ba4 100644 --- a/src/main/resources/messages/messages_fi.yml +++ b/src/main/resources/messages/messages_fi.yml @@ -60,7 +60,6 @@ misc: logout: '&cKirjauduit ulos palvelimelta.' reload: '&fAsetukset uudelleenladattu' usage_change_password: '&fKäyttötapa: /changepassword vanhaSalasana uusiSalasana' - # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO accounts_owned_self: 'You own %count accounts:' # TODO accounts_owned_other: 'The player %name has %count accounts:' @@ -90,8 +89,10 @@ email: old_email_invalid: '[AuthMe] Vanha sähköposti on väärä!' invalid: '[AuthMe] Väärä sähköposti' added: '[AuthMe] Sähköposti lisätty!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '[AuthMe] Vahvistuta sähköposti!' changed: '[AuthMe] Sähköposti vaihdettu!' + # TODO change_not_allowed: '&cChanging email was not allowed' # TODO email_show: '&2Your current email address is: &f%email' # TODO no_email_for_account: '&2You currently don''t have email address associated with this account.' # TODO already_used: '&4The email address is already being used' @@ -99,8 +100,6 @@ email: # TODO send_failure: 'The email could not be sent. Please contact an administrator.' # TODO change_password_expired: 'You cannot change your password using this command anymore.' # TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -142,3 +141,16 @@ time: # TODO hours: 'hours' # TODO day: 'day' # TODO days: 'days' + +# Two-factor authentication +two_factor: + # TODO code_created: '&2Your secret code is %code. You can scan it from here %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_fr.yml b/src/main/resources/messages/messages_fr.yml index 710da4d7..9a492cc7 100644 --- a/src/main/resources/messages/messages_fr.yml +++ b/src/main/resources/messages/messages_fr.yml @@ -63,7 +63,6 @@ misc: logout: '&cVous avez été déconnecté !' reload: '&aAuthMe a été relancé avec succès.' usage_change_password: '&cPour changer de mot de passe, utilisez "/changepassword "' - two_factor_create: '&aVotre code secret est &2%code&a. Vous pouvez le scanner depuis &2%url' accounts_owned_self: 'Vous avez %count comptes:' accounts_owned_other: 'Le joueur %name a %count comptes:' @@ -93,8 +92,10 @@ email: old_email_invalid: '&cAncien email invalide !' invalid: '&cL''email inscrit est invalide !' added: '&aEmail enregistré. En cas de perte de MDP, faites "/email recover "' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cLa confirmation de l''email est manquante ou éronnée.' changed: '&aVotre email a été mis à jour.' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&fL''email enregistré pour votre compte est: %email' no_email_for_account: '&c&oVous n''avez aucun email enregistré sur votre compte.' already_used: '&cCet email est déjà utilisé !' @@ -102,8 +103,6 @@ email: send_failure: '&cLe mail n''a pas pu être envoyé. Veuillez contacter un admin.' change_password_expired: 'Vous ne pouvez pas changer votre mot de passe avec cette commande.' email_cooldown_error: '&cUn mail de récupération a déjà été envoyé récemment. Veuillez attendre %time pour le demander de nouveau.' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -145,3 +144,16 @@ time: hours: 'heures' day: 'jour' days: 'jours' + +# Two-factor authentication +two_factor: + code_created: '&aVotre code secret est &2%code&a. Vous pouvez le scanner depuis &2%url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_gl.yml b/src/main/resources/messages/messages_gl.yml index fb79b664..6b6f8707 100644 --- a/src/main/resources/messages/messages_gl.yml +++ b/src/main/resources/messages/messages_gl.yml @@ -60,7 +60,6 @@ misc: logout: '&cSesión pechada con éxito' reload: '&fRecargáronse a configuración e a base de datos' usage_change_password: '&fUso: /changepassword ' - # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO accounts_owned_self: 'You own %count accounts:' # TODO accounts_owned_other: 'The player %name has %count accounts:' @@ -90,8 +89,10 @@ email: old_email_invalid: '[AuthMe] O correo vello non é válido!' invalid: '[AuthMe] Correo non válido' added: '[AuthMe] Correo engadido!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '[AuthMe] Confirma o teu correo!' changed: '[AuthMe] Cambiouse o correo!' + # TODO change_not_allowed: '&cChanging email was not allowed' # TODO email_show: '&2Your current email address is: &f%email' # TODO no_email_for_account: '&2You currently don''t have email address associated with this account.' # TODO already_used: '&4The email address is already being used' @@ -99,8 +100,6 @@ email: # TODO send_failure: 'The email could not be sent. Please contact an administrator.' # TODO change_password_expired: 'You cannot change your password using this command anymore.' # TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -142,3 +141,16 @@ time: # TODO hours: 'hours' # TODO day: 'day' # TODO days: 'days' + +# Two-factor authentication +two_factor: + # TODO code_created: '&2Your secret code is %code. You can scan it from here %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_hu.yml b/src/main/resources/messages/messages_hu.yml index 72f8a15b..4f2fe602 100644 --- a/src/main/resources/messages/messages_hu.yml +++ b/src/main/resources/messages/messages_hu.yml @@ -60,7 +60,6 @@ misc: logout: '&cSikeresen kijelentkeztél!' reload: 'Beállítások és az adatbázis újratöltve!' usage_change_password: 'Használat: "/changepassword <új jelszó>".' - two_factor_create: '&2A titkos kódod a következő: %code. Vagy skenneld be a következő oldalról: %url' accounts_owned_self: '%count db regisztrációd van:' accounts_owned_other: 'A %name nevű játékosnak, %count db regisztrációja van:' @@ -90,8 +89,10 @@ email: old_email_invalid: '&cHibás a régi email cím, próbáld újra!' invalid: '&cHibás az email cím, próbáld újra!' added: '&2Az email címed rögzítése sikeresen megtörtént!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cKérlek, ellenőrízd az email címedet!' changed: '&2Az email cím cseréje sikeresen megtörtént!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2A jelenlegi email-ed a következő: &f%email' no_email_for_account: '&2Ehhez a felhasználóhoz jelenleg még nincs email hozzárendelve.' already_used: '&4Ez az email cím már használatban van!' @@ -99,8 +100,6 @@ email: send_failure: 'Nem sikerült elküldeni az emailt. Lépj kapcsolatba egy adminnal.' change_password_expired: 'Ezzel a paranccsal már nem módosíthatja jelszavát.' email_cooldown_error: '&cEgy emailt már kiküldtünk. Következő email küldése előtt várnod kell: %time.' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -142,3 +141,16 @@ time: hours: 'óra' day: 'nap' days: 'nap' + +# Two-factor authentication +two_factor: + code_created: '&2A titkos kódod a következő: %code. Vagy skenneld be a következő oldalról: %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_id.yml b/src/main/resources/messages/messages_id.yml index 5e5b3f3c..9e9d6121 100644 --- a/src/main/resources/messages/messages_id.yml +++ b/src/main/resources/messages/messages_id.yml @@ -60,7 +60,6 @@ misc: logout: '&2Berhasil logout!' reload: '&2Konfigurasi dan database telah dimuat ulang!' usage_change_password: '&cUsage: /changepassword ' - # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO accounts_owned_self: 'You own %count accounts:' # TODO accounts_owned_other: 'The player %name has %count accounts:' @@ -90,8 +89,10 @@ email: old_email_invalid: '&cEmail lama tidak valid, coba lagi!' invalid: '&cAlamat email tidak valid, coba lagi!' added: '&2Berhasil menambahkan alamat email ke akunmu!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cSilahkan konfirmasi alamat email kamu!' changed: '&2Alamat email telah diubah dengan benar!' + # TODO change_not_allowed: '&cChanging email was not allowed' # TODO email_show: '&2Your current email address is: &f%email' # TODO no_email_for_account: '&2You currently don''t have email address associated with this account.' # TODO already_used: '&4The email address is already being used' @@ -99,8 +100,6 @@ email: # TODO send_failure: 'The email could not be sent. Please contact an administrator.' # TODO change_password_expired: 'You cannot change your password using this command anymore.' # TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -142,3 +141,16 @@ time: # TODO hours: 'hours' # TODO day: 'day' # TODO days: 'days' + +# Two-factor authentication +two_factor: + # TODO code_created: '&2Your secret code is %code. You can scan it from here %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_it.yml b/src/main/resources/messages/messages_it.yml index d99cae84..d46a244f 100644 --- a/src/main/resources/messages/messages_it.yml +++ b/src/main/resources/messages/messages_it.yml @@ -61,7 +61,6 @@ misc: logout: '&2Disconnessione avvenuta correttamente!' reload: '&2La configurazione e il database sono stati ricaricati correttamente!' usage_change_password: '&cUtilizzo: /changepassword ' - two_factor_create: '&2Il tuo codice segreto è: &f%code%%nl%&2Puoi anche scannerizzare il codice QR da qui: &f%url' accounts_owned_self: 'Possiedi %count account:' accounts_owned_other: 'Il giocatore %name possiede %count account:' @@ -91,8 +90,10 @@ email: old_email_invalid: '&cIl vecchio indirizzo email inserito non è valido, riprova!' invalid: '&cL''indirizzo email inserito non è valido, riprova!' added: '&2Indirizzo email aggiunto correttamente al tuo account!' + add_not_allowed: '&cNon hai il permesso di aggiungere un indirizzo email' request_confirmation: '&cPer favore, conferma il tuo indirizzo email!' changed: '&2Indirizzo email cambiato correttamente!' + change_not_allowed: '&cNon hai il permesso di cambiare l''indirizzo email' email_show: '&2Il tuo indirizzo email al momento è: &f%email' no_email_for_account: '&2Al momento non hai nessun indirizzo email associato al tuo account.' already_used: '&4L''indirizzo email inserito è già in uso' @@ -100,8 +101,6 @@ email: send_failure: 'Non è stato possibile inviare l''email di recupero. Per favore contatta un amministratore.' change_password_expired: 'Non puoi più cambiare la tua password con questo comando.' email_cooldown_error: '&cUna email di recupero ti è già stata inviata recentemente. Devi attendere %time prima di poterne richiedere una nuova.' - add_not_allowed: '&cNon hai il permesso di aggiungere un indirizzo email' - change_not_allowed: '&cNon hai il permesso di cambiare l''indirizzo email' # Password recovery by email recovery: @@ -143,3 +142,16 @@ time: hours: 'ore' day: 'giorno' days: 'giorni' + +# Two-factor authentication +two_factor: + code_created: '&2Il tuo codice segreto è: &f%code%%nl%&2Puoi anche scannerizzare il codice QR da qui: &f%url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_ko.yml b/src/main/resources/messages/messages_ko.yml index 5d875932..32b5ffd4 100644 --- a/src/main/resources/messages/messages_ko.yml +++ b/src/main/resources/messages/messages_ko.yml @@ -62,7 +62,6 @@ misc: logout: '&2로그아웃 되었습니다!' reload: '&2설정과 데이터 베이스가 새로고침 되었습니다!' usage_change_password: '&c사용법: /changepassword <예전 비밀번호> <새 비밀번호>' - two_factor_create: '&2당신의 비밀 코드는 %code 입니다. %url 에서 스캔할 수 있습니다' accounts_owned_self: '%count 개의 계정을 소유하고 있습니다.' accounts_owned_other: '플레이어 %name 는 %count 개의 계정을 소유하고 있습니다:' @@ -92,8 +91,10 @@ email: old_email_invalid: '&c예전 이메일 주소가 잘못되었습니다. 다시 시도해보세요!' invalid: '&c이메일 주소가 잘못되었습니다. 다시 시도해보세요!' added: '&2계정에 이메일 주소를 추가했습니다!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&c이메일 주소를 확인해주세요!' changed: '&2이메일 주소가 변경되었습니다!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2현재 이메일 주소: &f%email' no_email_for_account: '&2현재 이 계정과 연결된 이메일 주소가 없습니다.' already_used: '&4이메일 주소가 이미 사용 중입니다.' @@ -101,8 +102,6 @@ email: send_failure: '이메일을 보낼 수 없습니다. 관리자에게 알려주세요.' change_password_expired: '더 이상 이 명령어를 통해 비밀번호를 변경할 수 없습니다.' email_cooldown_error: '&c이메일을 이미 발송했습니다. %time 후에 다시 발송할 수 있습니다.' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -144,3 +143,16 @@ time: hours: '시간' day: '일' days: '일' + +# Two-factor authentication +two_factor: + code_created: '&2당신의 비밀 코드는 %code 입니다. %url 에서 스캔할 수 있습니다' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_lt.yml b/src/main/resources/messages/messages_lt.yml index 3df01369..e58a9fd6 100644 --- a/src/main/resources/messages/messages_lt.yml +++ b/src/main/resources/messages/messages_lt.yml @@ -60,7 +60,6 @@ misc: logout: '&aSekmingai atsijungete' reload: '&aNustatymai ir duomenu baze buvo perkrauta.' usage_change_password: '&ePanaudojimas: /changepassword senasSlaptazodis naujasSlaptazodis' - # TODO two_factor_create: '&2Your secret code is %code. You can scan it from here %url' # TODO accounts_owned_self: 'You own %count accounts:' # TODO accounts_owned_other: 'The player %name has %count accounts:' @@ -90,8 +89,10 @@ email: # TODO old_email_invalid: '&cInvalid old email, try again!' # TODO invalid: '&cInvalid email address, try again!' # TODO added: '&2Email address successfully added to your account!' + # TODO add_not_allowed: '&cAdding email was not allowed' # TODO request_confirmation: '&cPlease confirm your email address!' # TODO changed: '&2Email address changed correctly!' + # TODO change_not_allowed: '&cChanging email was not allowed' # TODO email_show: '&2Your current email address is: &f%email' # TODO no_email_for_account: '&2You currently don''t have email address associated with this account.' # TODO already_used: '&4The email address is already being used' @@ -99,8 +100,6 @@ email: # TODO send_failure: 'The email could not be sent. Please contact an administrator.' # TODO change_password_expired: 'You cannot change your password using this command anymore.' # TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -142,3 +141,16 @@ time: # TODO hours: 'hours' # TODO day: 'day' # TODO days: 'days' + +# Two-factor authentication +two_factor: + # TODO code_created: '&2Your secret code is %code. You can scan it from here %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_nl.yml b/src/main/resources/messages/messages_nl.yml index 86f7b8ff..00b99161 100644 --- a/src/main/resources/messages/messages_nl.yml +++ b/src/main/resources/messages/messages_nl.yml @@ -60,7 +60,6 @@ misc: logout: '&2Je bent succesvol uitgelogd!' reload: '&2De configuratie en database zijn succesvol herladen!' usage_change_password: '&cGebruik: /changepassword ' - two_factor_create: '&2Je geheime code is %code. Je kunt hem scannen op %url' accounts_owned_self: 'Je bezit %count accounts:' accounts_owned_other: 'De speler %name heeft %count accounts:' @@ -90,8 +89,10 @@ email: old_email_invalid: '&cOngeldig oud e-mailadres, probeer het opnieuw!' invalid: '&cOngeldig E-mailadres, probeer het opnieuw!' added: '&2Het e-mailadres is succesvol toegevoegd aan je account!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cVerifiëer je e-mailadres alsjeblieft!' changed: '&2Het e-mailadres is succesvol veranderd!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2Jouw huidige e-mailadres is: %email' no_email_for_account: '&2Je hebt nog geen e-mailadres toegevoegd aan dit account.' already_used: '&4Dit e-mailadres wordt al gebruikt' @@ -99,8 +100,6 @@ email: send_failure: 'De e-mail kon niet verzonden worden. Neem contact op met een administrator.' change_password_expired: 'Je kunt je wachtwoord niet meer veranderen met dit commando.' email_cooldown_error: '&cEr is recent al een e-mail verzonden. Je moet %time wachten voordat je een nieuw bericht kunt versturen.' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -142,3 +141,16 @@ time: hours: 'uren' day: 'dag' days: 'dagen' + +# Two-factor authentication +two_factor: + code_created: '&2Je geheime code is %code. Je kunt hem scannen op %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_pl.yml b/src/main/resources/messages/messages_pl.yml index 6184f8e2..b923533b 100644 --- a/src/main/resources/messages/messages_pl.yml +++ b/src/main/resources/messages/messages_pl.yml @@ -60,7 +60,6 @@ misc: logout: '&cPomyślnie wylogowany' reload: '&fKonfiguracja bazy danych została przeładowana.' usage_change_password: '&fUżycie: /changepassword ' - two_factor_create: '&2Twój sekretny kod to %code. Możesz zeskanować go tutaj: %url' accounts_owned_self: '&7Posiadasz %count kont:' accounts_owned_other: '&7Gracz %name posiada %count kont:' @@ -90,8 +89,10 @@ email: old_email_invalid: '[AuthMe] Stary e-mail niepoprawny!' invalid: '[AuthMe] Nieprawidłowy adres e-mail.' added: '[AuthMe] E-mail został dodany do Twojego konta!' + add_not_allowed: '&cMożliwość dodania adresu e-mail jest wyłączona.' request_confirmation: '[AuthMe] Potwierdź swój adres e-mail!' changed: '[AuthMe] E-mail został zmieniony!' + change_not_allowed: '&cMożliwość zmiany adresu e-mail jest wyłączona.' email_show: '&2Twój aktualny adres e-mail to: &f%email' no_email_for_account: '&2Nie posiadasz adresu e-mail przypisanego do tego konta.' already_used: '&4Ten adres e-mail jest aktualnie używany!' @@ -99,8 +100,6 @@ email: send_failure: 'Nie można wysłać e-maila. Skontaktuj się z administracją.' change_password_expired: 'Nie zmienisz już hasła przy użyciu tej komendy.' email_cooldown_error: '&cE-mail został wysłany, musisz poczekać %time przed wysłaniem następnego.' - add_not_allowed: '&cMożliwość dodania adresu e-mail jest wyłączona.' - change_not_allowed: '&cMożliwość zmiany adresu e-mail jest wyłączona.' # Password recovery by email recovery: @@ -142,3 +141,16 @@ time: hours: 'godzin' day: 'dzień' days: 'dni' + +# Two-factor authentication +two_factor: + code_created: '&2Twój sekretny kod to %code. Możesz zeskanować go tutaj: %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_pt.yml b/src/main/resources/messages/messages_pt.yml index b7cabdc8..56b19dd3 100644 --- a/src/main/resources/messages/messages_pt.yml +++ b/src/main/resources/messages/messages_pt.yml @@ -60,7 +60,6 @@ misc: logout: '&cSaida com sucesso' reload: '&fConfiguração e base de dados foram recarregadas' usage_change_password: '&fUse: /changepassword ' - two_factor_create: '&2O seu código secreto é o %code. Você pode verificá-lo a partir daqui %url' accounts_owned_self: 'Você possui %count contas:' accounts_owned_other: 'O jogador %name possui %count contas:' @@ -90,8 +89,10 @@ email: old_email_invalid: 'Email antigo inválido!' invalid: 'Email inválido!' added: 'Email adicionado com sucesso!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: 'Confirme o seu email!' changed: 'Email alterado com sucesso!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2O seu endereço de email atual é &f%email' no_email_for_account: '&2Você atualmente não tem um endereço de email associado a essa conta.' already_used: '&4O endereço de e-mail já está sendo usado' @@ -99,8 +100,6 @@ email: send_failure: 'Não foi possivel enviar o email. Por favor contate um administrador.' change_password_expired: 'Você não pode mais alterar a sua password usando este comando.' email_cooldown_error: '&cUm email já foi enviado recentemente.Por favor, espere %time antes de enviar novamente' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -142,3 +141,16 @@ time: hours: 'horas' day: 'dia' days: 'dias' + +# Two-factor authentication +two_factor: + code_created: '&2O seu código secreto é o %code. Você pode verificá-lo a partir daqui %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_ro.yml b/src/main/resources/messages/messages_ro.yml index 4d40556d..3d9c6cfe 100644 --- a/src/main/resources/messages/messages_ro.yml +++ b/src/main/resources/messages/messages_ro.yml @@ -60,7 +60,6 @@ misc: logout: '&2Te-ai dezautentificat cu succes!' reload: '&2Configuratiile si baza de date sau reincarcat corect!' usage_change_password: '&cFoloseste comanda: /changepassword ' - two_factor_create: '&2Codul tau secret este %code. Il poti scana de aici %url' accounts_owned_self: 'Detii %count conturi:' accounts_owned_other: 'Jucatorul %name are %count conturi:' @@ -90,8 +89,10 @@ email: old_email_invalid: '&cEmail-ul vechi este invalid, incearca din nou!' invalid: '&cEmail-ul este invalid, incearca din nou!' added: '&2Email-ul a fost adaugat cu succes la contul tau!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cTe rugam sa confirmi adresa ta de email!' changed: '&2Email-ul a fost schimbat cu succes!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2Adresa ta curenta de email este: &f%email' no_email_for_account: '&2Nu ai nici o adresa de email asociata cu acest cont.' already_used: '&4Email-ul acesta este deja folosit de altcineva' @@ -99,8 +100,6 @@ email: send_failure: 'Email-ul nu a putut fi trimis. Ta rugam contactatezi un administrator.' change_password_expired: 'Nu mai iti poti schimba parola folosind aceasta comanda.' email_cooldown_error: '&cAi primit deja un mail pentru schimbarea parolei. Trebuie sa astepti %time inainte de a trimite unul nou.' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -142,3 +141,16 @@ time: hours: 'ore' day: 'zi' days: 'zile' + +# Two-factor authentication +two_factor: + code_created: '&2Codul tau secret este %code. Il poti scana de aici %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_ru.yml b/src/main/resources/messages/messages_ru.yml index 81d5c6f6..fd28978a 100644 --- a/src/main/resources/messages/messages_ru.yml +++ b/src/main/resources/messages/messages_ru.yml @@ -60,7 +60,6 @@ misc: logout: '&2Вы успешно вышли.' reload: '&6Конфигурация и база данных перезагружены.' usage_change_password: '&cИспользование: /changepassword <пароль> <новый пароль>' - two_factor_create: '&2Ваш секретный код — %code. Просканируйте его здесь: %url' accounts_owned_self: 'У вас %count уч. записей:' accounts_owned_other: 'У игрока %name %count уч. записей:' @@ -90,8 +89,10 @@ email: old_email_invalid: '&cНедействительная старая электронная почта!' invalid: '&cНедействительный адрес электронной почты!' added: '&2Электронная почта успешно добавлена!' + add_not_allowed: '&cДобавление электронной почты не было разрешено.' request_confirmation: '&cПодтвердите свою электронную почту!' changed: '&2Адрес электронной почты изменён!' + change_not_allowed: '&cИзменение электронной почты не было разрешено.' email_show: '&2Текущий адрес электронной почты — &f%email' no_email_for_account: '&2К вашей уч. записи не привязана электронная почта.' already_used: '&4Эта электронная почта уже используется.' @@ -99,8 +100,6 @@ email: send_failure: 'Письмо не может быть отправлено. Свяжитесь в администратором.' change_password_expired: 'Больше нельзя сменить свой пароль, используя эту команду.' email_cooldown_error: '&cПисьмо было отправлено недавно. Подождите %time, прежде чем отправить новое.' - add_not_allowed: '&cДобавление электронной почты не было разрешено.' - change_not_allowed: '&cИзменение электронной почты не было разрешено.' # Password recovery by email recovery: @@ -142,3 +141,16 @@ time: hours: 'ч.' day: 'дн.' days: 'дн.' + +# Two-factor authentication +two_factor: + code_created: '&2Ваш секретный код — %code. Просканируйте его здесь: %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_sk.yml b/src/main/resources/messages/messages_sk.yml index 2553fc0d..73ba9f6e 100644 --- a/src/main/resources/messages/messages_sk.yml +++ b/src/main/resources/messages/messages_sk.yml @@ -66,7 +66,6 @@ misc: logout: '&cBol si úspešne odhlásený.' reload: '&fZnovu načítanie konfigurácie a databázy bolo úspešné.' usage_change_password: '&fPoužitie: /changepassword ' - two_factor_create: '&2Tvoj tajný kód je %code. Môžeš ho oskenovať tu: %url' accounts_owned_self: 'Vlastníš tieto účty(%count): ' accounts_owned_other: 'Hráč %name vlastní tieto účty(%count): ' @@ -96,8 +95,10 @@ email: old_email_invalid: '&cNeplatný starý email, skús to znovu!' invalid: '&cNeplatná emailová adresa, skús to znovu!' added: '&2Emailová adresa bola úspešne pridaná k tvojmu účtu!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cProsím potvrď svoju emailovú adresu!' changed: '&2Emailová adresa bola úspešne zmenená!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2Tvoja súčastná emailová adresa je: &f%email' no_email_for_account: '&2Momentálne nemáš emailovú adresu spojenú s týmto účtom.' already_used: '&4Túto emailovú adresu už niekto používa.' @@ -105,8 +106,6 @@ email: send_failure: 'Email nemohol byť poslaný. Prosím kontaktuj Administrátora.' change_password_expired: 'Už nemôžeš zmeniť svoje heslo týmto príkazom.' email_cooldown_error: '&cEmail bol nedávno poslaný. Musíš počkať %time predtým ako ti pošleme nový.' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -148,3 +147,16 @@ time: hours: 'hod.' day: 'd.' days: 'd.' + +# Two-factor authentication +two_factor: + code_created: '&2Tvoj tajný kód je %code. Môžeš ho oskenovať tu: %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_tr.yml b/src/main/resources/messages/messages_tr.yml index fd7d5e29..39a14b86 100644 --- a/src/main/resources/messages/messages_tr.yml +++ b/src/main/resources/messages/messages_tr.yml @@ -60,7 +60,6 @@ misc: logout: '&2Basariyla cikis yaptin!' reload: '&2Ayarlar ve veritabani yenilendi!' usage_change_password: '&cKullanim: /changepassword ' - two_factor_create: '&2Gizli kodunuz %code. Buradan test edebilirsin, %url' accounts_owned_self: 'Sen %count hesaba sahipsin:' accounts_owned_other: 'Oyuncu %name %count hesaba sahip:' @@ -90,8 +89,10 @@ email: old_email_invalid: '&cGecersiz eski eposta, tekrar deneyin!' invalid: '&cGecersiz eposta, tekrar deneyin!' added: '&2Eposta basariyla kullaniciniza eklendi!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cLutfen tekrar epostanizi giriniz!' changed: '&2Epostaniz basariyla degistirildi!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2Suanki eposta adresin: &f%email' no_email_for_account: '&2Bu hesapla iliskili bir eposta bulunmuyor.' already_used: '&4Eposta adresi zaten kullaniliyor.' @@ -99,8 +100,6 @@ email: send_failure: 'Eposta gonderilemedi. Yetkili ile iletisime gec.' # TODO change_password_expired: 'You cannot change your password using this command anymore.' email_cooldown_error: '&cKisa bir sure once eposta gonderildi. Yeni bir eposta almak icin %time beklemelisin.' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -142,3 +141,16 @@ time: hours: 'saat' day: 'gun' days: 'gun' + +# Two-factor authentication +two_factor: + code_created: '&2Gizli kodunuz %code. Buradan test edebilirsin, %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_uk.yml b/src/main/resources/messages/messages_uk.yml index d509d4eb..034238d3 100644 --- a/src/main/resources/messages/messages_uk.yml +++ b/src/main/resources/messages/messages_uk.yml @@ -60,7 +60,6 @@ misc: logout: '&2Ви вийшли зі свого акаунта!' reload: '&2Конфігурації та базу даних було успішно перезавантажено!' usage_change_password: '&cСинтаксис: /changepassword <старийПароль> <новийПароль>' - two_factor_create: '&2Ваш секретний код — %code %nl%&2Можете зкопіювати його за цим посиланням — %url' accounts_owned_self: 'Кількість ваших твінк‒акаунтів: %count:' accounts_owned_other: 'Кількість твінк‒акаунтів гравця %name: %count' @@ -90,8 +89,10 @@ email: old_email_invalid: '&cСтарий e-mail, що прив’язано до вашого акаунта, відрізняється від введеного вами.' invalid: '&cФормат вказаного e-mail’у є некоректним, або його домен внесено до блеклисту.' added: '&2Електронну пошту успішно прив’язано до вашого акаунта.' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cАдреси не співпадають.' changed: '&2E-mail успішно змінено.' + # TODO change_not_allowed: '&cChanging email was not allowed' # TODO email_show: '&2Your current email address is: &f%email' # TODO no_email_for_account: '&2You currently don''t have email address associated with this account.' already_used: '&4До цієї електронної пошти прив’язано забагато акаунтів!' @@ -99,8 +100,6 @@ email: # TODO send_failure: 'The email could not be sent. Please contact an administrator.' # TODO change_password_expired: 'You cannot change your password using this command anymore.' # TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -142,3 +141,16 @@ time: # TODO hours: 'hours' # TODO day: 'day' # TODO days: 'days' + +# Two-factor authentication +two_factor: + code_created: '&2Ваш секретний код — %code %nl%&2Можете зкопіювати його за цим посиланням — %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_vn.yml b/src/main/resources/messages/messages_vn.yml index 8fc170c0..10a813ef 100644 --- a/src/main/resources/messages/messages_vn.yml +++ b/src/main/resources/messages/messages_vn.yml @@ -60,7 +60,6 @@ misc: logout: '&2Bạn đã đăng xuất!' reload: '&2Cấu hình và cơ sở dử liệu đã được nạp lại!' usage_change_password: '&cSử dụng: /changepassword ' - two_factor_create: '&2Mã bí mật của bạn là %code. Bạn có thể quét nó tại đây %url' accounts_owned_self: 'Bạn sở hữu %count tài khoản:' accounts_owned_other: 'Người chơi %name có %count tài khoản:' @@ -90,8 +89,10 @@ email: old_email_invalid: '&cEmail cũ không hợp lệ, vui lòng thử lại!' invalid: '&cĐại chỉ email không hợp lệ, vui lòng thử lại!' added: '&2Địa chỉ email đã thêm vào tài khoản của bạn thành công!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&cVui lòng xác nhận địa chỉ email của bạn!' changed: '&2Địa chỉ email đã thay đổi!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2Địa chỉ email hiện tại của bạn là: &f%email' no_email_for_account: '&2Hiện tại bạn chưa liên kết bất kỳ email nào với tài khoản này.' already_used: '&4Địa chỉ email đã được sử dụng' @@ -99,8 +100,6 @@ email: send_failure: 'Không thể gửi thư. Vui lòng liên hệ với ban quản trị.' change_password_expired: '&cBạn không thể thay đổi mật khẩu bằng lệnh này từ nay.' email_cooldown_error: '&cMột bức thư đã được gửi gần đây. Bạn phải chờ %time trước khi có thể gửi một bức thư mới.' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -142,3 +141,16 @@ time: hours: 'giờ' day: 'ngày' days: 'ngày' + +# Two-factor authentication +two_factor: + code_created: '&2Mã bí mật của bạn là %code. Bạn có thể quét nó tại đây %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_zhcn.yml b/src/main/resources/messages/messages_zhcn.yml index 70193285..ec7a4b01 100644 --- a/src/main/resources/messages/messages_zhcn.yml +++ b/src/main/resources/messages/messages_zhcn.yml @@ -60,7 +60,6 @@ misc: logout: '&8[&6玩家系统&8] &c已成功登出!' reload: '&8[&6玩家系统&8] &f配置以及数据已经重新加载完毕' usage_change_password: '&8[&6玩家系统&8] &f正确用法:“/changepassword 旧密码 新密码”' - two_factor_create: '&8[&6玩家系统&8] &2你的代码是 %code,你可以使用 %url 来扫描' accounts_owned_self: '您拥有 %count 个账户:' accounts_owned_other: '玩家 %name 拥有 %count 个账户:' @@ -90,8 +89,10 @@ email: old_email_invalid: '&8[&6玩家系统&8] &f旧邮箱无效!' invalid: '&8[&6玩家系统&8] &f无效的邮箱' added: '&8[&6玩家系统&8] &f邮箱已添加 !' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&8[&6玩家系统&8] &f确认你的邮箱 !' changed: '&8[&6玩家系统&8] &f邮箱已改变 !' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&2您当前的电子邮件地址为: &f%email' no_email_for_account: '&2您当前并没有任何邮箱与该账号绑定' already_used: '&8[&6玩家系统&8] &4邮箱已被使用' @@ -99,8 +100,6 @@ email: send_failure: '邮件发送失败,请联系管理员' change_password_expired: '您不能使用此命令更改密码' email_cooldown_error: '&c邮件已在几分钟前发送,您需要等待 %time 后才能再次请求发送' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -142,3 +141,16 @@ time: hours: '小时' day: '天' days: '天' + +# Two-factor authentication +two_factor: + code_created: '&8[&6玩家系统&8] &2你的代码是 %code,你可以使用 %url 来扫描' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_zhhk.yml b/src/main/resources/messages/messages_zhhk.yml index 694d532e..996872c8 100644 --- a/src/main/resources/messages/messages_zhhk.yml +++ b/src/main/resources/messages/messages_zhhk.yml @@ -63,7 +63,6 @@ misc: logout: '&8[&6用戶系統&8] &b你成功登出了。' reload: '&8[&6用戶系統&8] &b登入系統設定及資料庫重新載入完畢。' usage_change_password: '&8[&6用戶系統&8] &f用法:《 /changepassword <舊密碼> <新密碼> 》' - two_factor_create: '&8[&6用戶系統 - 兩步驗證碼&8] &b你的登入金鑰為&9「%c%code&9」&b,掃描連結為:&c %url' accounts_owned_self: '你擁有 %count 個帳戶:' accounts_owned_other: '玩家《%name》擁有 %count 個帳戶:' @@ -93,8 +92,10 @@ email: old_email_invalid: '&8[&6用戶系統&8] &c你所填寫的舊電郵地址並不正確。' invalid: '&8[&6用戶系統&8] &c你所填寫的電郵地址並不正確。' added: '&8[&6用戶系統&8] &a已新增你的電郵地址。' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&8[&6用戶系統&8] &5請重覆輸入你的電郵地址。' changed: '&8[&6用戶系統&8] &a你的電郵地址已更改。' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&8[&6用戶系統&8] &2你所使用的電郵地址為:&f%email' no_email_for_account: '&8[&6用戶系統&8] &2你並未有綁定電郵地址到此帳戶。' already_used: '&8[&6用戶系統&8] &4這個電郵地址已被使用。' @@ -102,8 +103,6 @@ email: send_failure: '&8[&6用戶系統&8] &c電郵系統錯誤,請聯絡伺服器管理員。 &7(err: smtperr)' change_password_expired: '&8[&6用戶系統&8] 此指令已過期,請重新辦理。' email_cooldown_error: '&8[&6用戶系統&8] &c你已經辦理過重寄郵件,請等待 %time 後再嘗試吧。' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -145,3 +144,16 @@ time: hours: '小時' day: '日' days: '日' + +# Two-factor authentication +two_factor: + code_created: '&8[&6用戶系統 - 兩步驗證碼&8] &b你的登入金鑰為&9「%c%code&9」&b,掃描連結為:&c %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_zhmc.yml b/src/main/resources/messages/messages_zhmc.yml index c0b41802..cf9fe986 100644 --- a/src/main/resources/messages/messages_zhmc.yml +++ b/src/main/resources/messages/messages_zhmc.yml @@ -60,7 +60,6 @@ misc: logout: '&2已成功註銷!' reload: '&2伺服器已正確地被重新加載配置和數據庫!' usage_change_password: '&c使用方法: "/changepassword [舊密碼] [新密碼]"' - two_factor_create: '&2您的密碼是 %code。您可以從這裡掃描 %url' accounts_owned_self: '您擁有 %count 個帳戶:' accounts_owned_other: '玩家 %name 擁有 %count 個帳戶:' @@ -90,8 +89,10 @@ email: old_email_invalid: '&c舊電子郵件地址無效,請重試!' invalid: '&c電子郵件地址無效,請重試!' added: '&2電子郵件地址已成功添加到您的帳戶!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&c請確認你的電郵地址!' changed: '&2已正確地更改電子郵件地址!' + # TODO change_not_allowed: '&cChanging email was not allowed' # TODO email_show: '&2Your current email address is: &f%email' # TODO no_email_for_account: '&2You currently don''t have email address associated with this account.' already_used: '&4此電子郵件地址已被使用' @@ -99,8 +100,6 @@ email: # TODO send_failure: 'The email could not be sent. Please contact an administrator.' # TODO change_password_expired: 'You cannot change your password using this command anymore.' # TODO email_cooldown_error: '&cAn email was already sent recently. You must wait %time before you can send a new one.' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -142,3 +141,16 @@ time: # TODO hours: 'hours' # TODO day: 'day' # TODO days: 'days' + +# Two-factor authentication +two_factor: + code_created: '&2您的密碼是 %code。您可以從這裡掃描 %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/messages/messages_zhtw.yml b/src/main/resources/messages/messages_zhtw.yml index fa9946bf..f6192193 100644 --- a/src/main/resources/messages/messages_zhtw.yml +++ b/src/main/resources/messages/messages_zhtw.yml @@ -62,7 +62,6 @@ misc: logout: '&b【AuthMe】&6您已成功登出' reload: '&b【AuthMe】&6已重新讀取設定檔及資料庫' usage_change_password: '&b【AuthMe】&6用法: &c"/changepassword <舊密碼> <新密碼>"' - two_factor_create: '&b【AuthMe - 兩步驗證碼】&b您的登入金鑰為&9「%c%code&9」&b,掃描連結為:&c %url' accounts_owned_self: '&b【AuthMe】&6您擁有 %count 個帳號:' accounts_owned_other: '&b【AuthMe】&6玩家 %name 擁有 %count 個帳號:' @@ -92,8 +91,10 @@ email: old_email_invalid: '&b【AuthMe】&6舊的Email無效!' invalid: '&b【AuthMe】&6無效的Email!' added: '&b【AuthMe】&6已添加Email!' + # TODO add_not_allowed: '&cAdding email was not allowed' request_confirmation: '&b【AuthMe】&6請驗證您的Email!' changed: '&b【AuthMe】&6Email已變更!' + # TODO change_not_allowed: '&cChanging email was not allowed' email_show: '&b【AuthMe】&2目前的電子郵件: &f%email' no_email_for_account: '&b【AuthMe】&2您目前沒有設置電子郵件.' already_used: '&b【AuthMe】&4這個電郵地址已被使用。' @@ -101,8 +102,6 @@ email: send_failure: '&b【AuthMe】&4無法傳送電子郵件,請聯絡管理員.' change_password_expired: '&b【AuthMe】&6您現在不能使用這個指令變更密碼了.' email_cooldown_error: '&b【AuthMe】&c電子郵件已經寄出了. 您只能在 %time 後才能傳送.' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' # Password recovery by email recovery: @@ -144,3 +143,16 @@ time: hours: '時' day: '天' days: '天' + +# Two-factor authentication +two_factor: + code_created: '&b【AuthMe - 兩步驗證碼】&b您的登入金鑰為&9「%c%code&9」&b,掃描連結為:&c %url' + # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' + # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' + # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' + # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' + # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' + # TODO removed_success: 'Successfully removed two-factor auth from your account' + # TODO invalid_code: 'Invalid code!' diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 46f2dd13..1be18288 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -23,7 +23,7 @@ commands: usage: /email show|add|change|recover|code|setpassword login: description: Login command - usage: /login [2facode] + usage: /login aliases: - l - log @@ -48,7 +48,7 @@ commands: - cp totp: description: TOTP commands - usage: /totp add|confirm|remove + usage: /totp code|add|confirm|remove aliases: - 2fa captcha: From ecaffbabfca85ef8cf936b088f6b4d61cc88e965 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 22 Apr 2018 12:45:34 +0200 Subject: [PATCH 33/63] Small cleanups / changes amassed over time - Small javadoc fixes - Simplifications - Move logException method from StringUtils to ExceptionUtils --- src/main/java/fr/xephi/authme/AuthMe.java | 9 +++------ .../java/fr/xephi/authme/ConsoleLogger.java | 4 ++-- .../data/limbo/AllowFlightRestoreType.java | 2 +- .../authme/process/register/AsyncRegister.java | 6 ++---- .../xephi/authme/security/crypts/BCrypt.java | 4 ++-- .../fr/xephi/authme/security/crypts/Ipb4.java | 6 +++--- .../xephi/authme/security/crypts/XfBCrypt.java | 4 ++-- .../fr/xephi/authme/util/ExceptionUtils.java | 10 ++++++++++ .../java/fr/xephi/authme/util/StringUtils.java | 11 ----------- .../fr/xephi/authme/CodeClimateConfigTest.java | 18 +++--------------- .../message/YamlTextFileCheckerTest.java | 3 ++- .../xephi/authme/util/ExceptionUtilsTest.java | 14 ++++++++++++++ .../fr/xephi/authme/util/StringUtilsTest.java | 14 -------------- 13 files changed, 44 insertions(+), 61 deletions(-) diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index 4aeb536c..6a2b3583 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -2,9 +2,7 @@ package fr.xephi.authme; import ch.jalu.injector.Injector; import ch.jalu.injector.InjectorBuilder; - import com.google.common.annotations.VisibleForTesting; - import fr.xephi.authme.api.NewAPI; import fr.xephi.authme.command.CommandHandler; import fr.xephi.authme.datasource.DataSource; @@ -35,9 +33,6 @@ import fr.xephi.authme.settings.properties.SecuritySettings; import fr.xephi.authme.task.CleanupTask; import fr.xephi.authme.task.purge.PurgeService; import fr.xephi.authme.util.ExceptionUtils; - -import java.io.File; - import org.apache.commons.lang.SystemUtils; import org.bukkit.Server; import org.bukkit.command.Command; @@ -48,6 +43,8 @@ import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPluginLoader; import org.bukkit.scheduler.BukkitScheduler; +import java.io.File; + import static fr.xephi.authme.service.BukkitService.TICKS_PER_MINUTE; import static fr.xephi.authme.util.Utils.isClassLoaded; @@ -160,7 +157,7 @@ public class AuthMe extends JavaPlugin { // Sponsor messages ConsoleLogger.info("Development builds are available on our jenkins, thanks to FastVM.io"); - ConsoleLogger.info("Do you want a good vps for your game server? Look at our sponsor FastVM.io leader " + ConsoleLogger.info("Do you want a good vps for your game server? Look at our sponsor FastVM.io leader " + "as virtual server provider!"); // Successful message diff --git a/src/main/java/fr/xephi/authme/ConsoleLogger.java b/src/main/java/fr/xephi/authme/ConsoleLogger.java index 0d933298..3e8d59a4 100644 --- a/src/main/java/fr/xephi/authme/ConsoleLogger.java +++ b/src/main/java/fr/xephi/authme/ConsoleLogger.java @@ -5,7 +5,7 @@ import fr.xephi.authme.output.LogLevel; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.PluginSettings; import fr.xephi.authme.settings.properties.SecuritySettings; -import fr.xephi.authme.util.StringUtils; +import fr.xephi.authme.util.ExceptionUtils; import java.io.File; import java.io.FileWriter; @@ -101,7 +101,7 @@ public final class ConsoleLogger { * @param th The Throwable to log */ public static void logException(String message, Throwable th) { - warning(message + " " + StringUtils.formatException(th)); + warning(message + " " + ExceptionUtils.formatException(th)); writeLog(Throwables.getStackTraceAsString(th)); } diff --git a/src/main/java/fr/xephi/authme/data/limbo/AllowFlightRestoreType.java b/src/main/java/fr/xephi/authme/data/limbo/AllowFlightRestoreType.java index 52388521..753650b6 100644 --- a/src/main/java/fr/xephi/authme/data/limbo/AllowFlightRestoreType.java +++ b/src/main/java/fr/xephi/authme/data/limbo/AllowFlightRestoreType.java @@ -32,7 +32,7 @@ public enum AllowFlightRestoreType { } }, - /** Always set flight enabled to false. */ + /** The user's flight handling is not modified. */ NOTHING { @Override public void restoreAllowFlight(Player player, LimboPlayer limbo) { diff --git a/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java b/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java index 78eaa567..fa5d0361 100644 --- a/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java @@ -14,7 +14,6 @@ import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.CommonService; import fr.xephi.authme.service.bungeecord.BungeeSender; import fr.xephi.authme.service.bungeecord.MessageType; -import fr.xephi.authme.settings.properties.PluginSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import fr.xephi.authme.util.PlayerUtils; @@ -82,9 +81,8 @@ public class AsyncRegister implements AsynchronousProcess { return false; } - boolean isAsync = service.getProperty(PluginSettings.USE_ASYNC_TASKS); - AuthMeAsyncPreRegisterEvent event = new AuthMeAsyncPreRegisterEvent(player, isAsync); - bukkitService.callEvent(event); + AuthMeAsyncPreRegisterEvent event = bukkitService.createAndCallEvent( + isAsync -> new AuthMeAsyncPreRegisterEvent(player, isAsync)); if (!event.canRegister()) { return false; } diff --git a/src/main/java/fr/xephi/authme/security/crypts/BCrypt.java b/src/main/java/fr/xephi/authme/security/crypts/BCrypt.java index 02e12d45..8b454c79 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/BCrypt.java +++ b/src/main/java/fr/xephi/authme/security/crypts/BCrypt.java @@ -8,7 +8,7 @@ import fr.xephi.authme.security.crypts.description.SaltType; import fr.xephi.authme.security.crypts.description.Usage; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.HooksSettings; -import fr.xephi.authme.util.StringUtils; +import fr.xephi.authme.util.ExceptionUtils; import javax.inject.Inject; @@ -39,7 +39,7 @@ public class BCrypt implements EncryptionMethod { try { return HashUtils.isValidBcryptHash(hash.getHash()) && BCryptService.checkpw(password, hash.getHash()); } catch (IllegalArgumentException e) { - ConsoleLogger.warning("Bcrypt checkpw() returned " + StringUtils.formatException(e)); + ConsoleLogger.warning("Bcrypt checkpw() returned " + ExceptionUtils.formatException(e)); } return false; } diff --git a/src/main/java/fr/xephi/authme/security/crypts/Ipb4.java b/src/main/java/fr/xephi/authme/security/crypts/Ipb4.java index 76289795..c7bfcd65 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/Ipb4.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Ipb4.java @@ -2,12 +2,12 @@ package fr.xephi.authme.security.crypts; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.security.HashUtils; -import fr.xephi.authme.util.RandomStringUtils; import fr.xephi.authme.security.crypts.description.HasSalt; import fr.xephi.authme.security.crypts.description.Recommendation; import fr.xephi.authme.security.crypts.description.SaltType; import fr.xephi.authme.security.crypts.description.Usage; -import fr.xephi.authme.util.StringUtils; +import fr.xephi.authme.util.ExceptionUtils; +import fr.xephi.authme.util.RandomStringUtils; /** @@ -37,7 +37,7 @@ public class Ipb4 implements EncryptionMethod { try { return HashUtils.isValidBcryptHash(hash.getHash()) && BCryptService.checkpw(password, hash.getHash()); } catch (IllegalArgumentException e) { - ConsoleLogger.warning("Bcrypt checkpw() returned " + StringUtils.formatException(e)); + ConsoleLogger.warning("Bcrypt checkpw() returned " + ExceptionUtils.formatException(e)); } return false; } diff --git a/src/main/java/fr/xephi/authme/security/crypts/XfBCrypt.java b/src/main/java/fr/xephi/authme/security/crypts/XfBCrypt.java index 3ef4e430..846807e6 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/XfBCrypt.java +++ b/src/main/java/fr/xephi/authme/security/crypts/XfBCrypt.java @@ -2,7 +2,7 @@ package fr.xephi.authme.security.crypts; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.security.HashUtils; -import fr.xephi.authme.util.StringUtils; +import fr.xephi.authme.util.ExceptionUtils; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -32,7 +32,7 @@ public class XfBCrypt implements EncryptionMethod { try { return HashUtils.isValidBcryptHash(hash.getHash()) && BCryptService.checkpw(password, hash.getHash()); } catch (IllegalArgumentException e) { - ConsoleLogger.warning("XfBCrypt checkpw() returned " + StringUtils.formatException(e)); + ConsoleLogger.warning("XfBCrypt checkpw() returned " + ExceptionUtils.formatException(e)); } return false; } diff --git a/src/main/java/fr/xephi/authme/util/ExceptionUtils.java b/src/main/java/fr/xephi/authme/util/ExceptionUtils.java index 6a5adde6..fd5ae885 100644 --- a/src/main/java/fr/xephi/authme/util/ExceptionUtils.java +++ b/src/main/java/fr/xephi/authme/util/ExceptionUtils.java @@ -33,4 +33,14 @@ public final class ExceptionUtils { } return null; } + + /** + * Format the information from a Throwable as string, retaining the type and its message. + * + * @param th the throwable to process + * @return string with the type of the Throwable and its message, e.g. "[IOException]: Could not open stream" + */ + public static String formatException(Throwable th) { + return "[" + th.getClass().getSimpleName() + "]: " + th.getMessage(); + } } diff --git a/src/main/java/fr/xephi/authme/util/StringUtils.java b/src/main/java/fr/xephi/authme/util/StringUtils.java index 1f200c0f..5c861300 100644 --- a/src/main/java/fr/xephi/authme/util/StringUtils.java +++ b/src/main/java/fr/xephi/authme/util/StringUtils.java @@ -66,17 +66,6 @@ public final class StringUtils { return str == null || str.trim().isEmpty(); } - /** - * Format the information from a Throwable as string, retaining the type and its message. - * - * @param th The throwable to process - * - * @return String with the type of the Throwable and its message, e.g. "[IOException]: Could not open stream" - */ - public static String formatException(Throwable th) { - return "[" + th.getClass().getSimpleName() + "]: " + th.getMessage(); - } - /** * Check that the given needle is in the middle of the haystack, i.e. that the haystack * contains the needle and that it is not at the very start or end. diff --git a/src/test/java/fr/xephi/authme/CodeClimateConfigTest.java b/src/test/java/fr/xephi/authme/CodeClimateConfigTest.java index 7eff3a72..e645922e 100644 --- a/src/test/java/fr/xephi/authme/CodeClimateConfigTest.java +++ b/src/test/java/fr/xephi/authme/CodeClimateConfigTest.java @@ -30,21 +30,9 @@ public class CodeClimateConfigTest { assertThat(excludePaths, not(empty())); removeTestsExclusionOrThrow(excludePaths); for (String path : excludePaths) { - verifySourceFileExists(path); - } - } - - private static void verifySourceFileExists(String path) { - // Note ljacqu 20170323: In the future, we could have legitimate exclusions that don't fulfill these checks, - // in which case this test needs to be adapted accordingly. - if (!path.startsWith(TestHelper.SOURCES_FOLDER)) { - fail("Unexpected path '" + path + "': expected to start with sources folder"); - } else if (!path.endsWith(".java")) { - fail("Expected path '" + path + "' to end with '.java'"); - } - - if (!new File(path).exists()) { - fail("Path '" + path + "' does not exist!"); + if (!new File(path).exists()) { + fail("Path '" + path + "' does not exist!"); + } } } diff --git a/src/test/java/fr/xephi/authme/message/YamlTextFileCheckerTest.java b/src/test/java/fr/xephi/authme/message/YamlTextFileCheckerTest.java index 9fbd27bd..b04259b2 100644 --- a/src/test/java/fr/xephi/authme/message/YamlTextFileCheckerTest.java +++ b/src/test/java/fr/xephi/authme/message/YamlTextFileCheckerTest.java @@ -2,6 +2,7 @@ package fr.xephi.authme.message; import fr.xephi.authme.TestHelper; import fr.xephi.authme.command.help.HelpSection; +import fr.xephi.authme.util.ExceptionUtils; import fr.xephi.authme.util.StringUtils; import org.bukkit.configuration.file.YamlConfiguration; import org.junit.BeforeClass; @@ -85,7 +86,7 @@ public class YamlTextFileCheckerTest { errors.add("Message for '" + mandatoryKey + "' is empty"); } } catch (Exception e) { - errors.add("Could not load file: " + StringUtils.formatException(e)); + errors.add("Could not load file: " + ExceptionUtils.formatException(e)); } } } diff --git a/src/test/java/fr/xephi/authme/util/ExceptionUtilsTest.java b/src/test/java/fr/xephi/authme/util/ExceptionUtilsTest.java index 8685d7f3..9f60c53a 100644 --- a/src/test/java/fr/xephi/authme/util/ExceptionUtilsTest.java +++ b/src/test/java/fr/xephi/authme/util/ExceptionUtilsTest.java @@ -4,8 +4,10 @@ import fr.xephi.authme.ReflectionTestUtils; import fr.xephi.authme.TestHelper; import org.junit.Test; +import java.net.MalformedURLException; import java.util.ConcurrentModificationException; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.sameInstance; import static org.junit.Assert.assertThat; @@ -58,4 +60,16 @@ public class ExceptionUtilsTest { // given / when / then TestHelper.validateHasOnlyPrivateEmptyConstructor(ExceptionUtils.class); } + + @Test + public void shouldFormatException() { + // given + MalformedURLException ex = new MalformedURLException("Unrecognized URL format"); + + // when + String result = ExceptionUtils.formatException(ex); + + // then + assertThat(result, equalTo("[MalformedURLException]: Unrecognized URL format")); + } } diff --git a/src/test/java/fr/xephi/authme/util/StringUtilsTest.java b/src/test/java/fr/xephi/authme/util/StringUtilsTest.java index 7111f81b..76e7ae75 100644 --- a/src/test/java/fr/xephi/authme/util/StringUtilsTest.java +++ b/src/test/java/fr/xephi/authme/util/StringUtilsTest.java @@ -3,8 +3,6 @@ package fr.xephi.authme.util; import fr.xephi.authme.TestHelper; import org.junit.Test; -import java.net.MalformedURLException; - import static java.util.Arrays.asList; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; @@ -63,18 +61,6 @@ public class StringUtilsTest { assertFalse(StringUtils.isEmpty(" test")); } - @Test - public void shouldFormatException() { - // given - MalformedURLException ex = new MalformedURLException("Unrecognized URL format"); - - // when - String result = StringUtils.formatException(ex); - - // then - assertThat(result, equalTo("[MalformedURLException]: Unrecognized URL format")); - } - @Test public void shouldGetDifferenceWithNullString() { // given/when/then From cff456c285cd3c8042579b4f3a1063e894a89de7 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 22 Apr 2018 12:51:41 +0200 Subject: [PATCH 34/63] Help message updater: specify the name of the updated file --- .../authme/UpdateHelpMessagesCommand.java | 5 +- .../service/HelpTranslationGenerator.java | 4 +- .../authme/UpdateHelpMessagesCommandTest.java | 70 +++++++++++++++++++ 3 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 src/test/java/fr/xephi/authme/command/executable/authme/UpdateHelpMessagesCommandTest.java diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/UpdateHelpMessagesCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/UpdateHelpMessagesCommand.java index c737b98d..d790962a 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/UpdateHelpMessagesCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/UpdateHelpMessagesCommand.java @@ -7,6 +7,7 @@ import fr.xephi.authme.service.HelpTranslationGenerator; import org.bukkit.command.CommandSender; import javax.inject.Inject; +import java.io.File; import java.io.IOException; import java.util.List; @@ -24,8 +25,8 @@ public class UpdateHelpMessagesCommand implements ExecutableCommand { @Override public void executeCommand(CommandSender sender, List arguments) { try { - helpTranslationGenerator.updateHelpFile(); - sender.sendMessage("Successfully updated the help file"); + File updatedFile = helpTranslationGenerator.updateHelpFile(); + sender.sendMessage("Successfully updated the help file '" + updatedFile.getName() + "'"); helpMessagesService.reloadMessagesFile(); } catch (IOException e) { sender.sendMessage("Could not update help file: " + e.getMessage()); diff --git a/src/main/java/fr/xephi/authme/service/HelpTranslationGenerator.java b/src/main/java/fr/xephi/authme/service/HelpTranslationGenerator.java index 6ecd0549..21407b4f 100644 --- a/src/main/java/fr/xephi/authme/service/HelpTranslationGenerator.java +++ b/src/main/java/fr/xephi/authme/service/HelpTranslationGenerator.java @@ -44,15 +44,17 @@ public class HelpTranslationGenerator { /** * Updates the help file to contain entries for all commands. * + * @return the help file that has been updated * @throws IOException if the help file cannot be written to */ - public void updateHelpFile() throws IOException { + public File updateHelpFile() throws IOException { String languageCode = settings.getProperty(PluginSettings.MESSAGES_LANGUAGE); File helpFile = new File(dataFolder, "messages/help_" + languageCode + ".yml"); Map helpEntries = generateHelpMessageEntries(); String helpEntriesYaml = exportToYaml(helpEntries); Files.write(helpFile.toPath(), helpEntriesYaml.getBytes(), StandardOpenOption.TRUNCATE_EXISTING); + return helpFile; } private static String exportToYaml(Map helpEntries) { diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/UpdateHelpMessagesCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/UpdateHelpMessagesCommandTest.java new file mode 100644 index 00000000..94e53ff2 --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/authme/UpdateHelpMessagesCommandTest.java @@ -0,0 +1,70 @@ +package fr.xephi.authme.command.executable.authme; + +import fr.xephi.authme.TestHelper; +import fr.xephi.authme.command.help.HelpMessagesService; +import fr.xephi.authme.service.HelpTranslationGenerator; +import org.bukkit.command.CommandSender; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link UpdateHelpMessagesCommand}. + */ +@RunWith(MockitoJUnitRunner.class) +public class UpdateHelpMessagesCommandTest { + + @InjectMocks + private UpdateHelpMessagesCommand command; + + @Mock + private HelpTranslationGenerator helpTranslationGenerator; + @Mock + private HelpMessagesService helpMessagesService; + + @BeforeClass + public static void setUpLogger() { + TestHelper.setupLogger(); + } + + @Test + public void shouldUpdateHelpMessage() throws IOException { + // given + File updatedFile = new File("some/path/help_xx.yml"); + given(helpTranslationGenerator.updateHelpFile()).willReturn(updatedFile); + CommandSender sender = mock(CommandSender.class); + + // when + command.executeCommand(sender, Collections.emptyList()); + + // then + verify(helpMessagesService).reloadMessagesFile(); + verify(sender).sendMessage("Successfully updated the help file 'help_xx.yml'"); + } + + @Test + public void shouldCatchAndReportException() throws IOException { + // given + given(helpTranslationGenerator.updateHelpFile()).willThrow(new IOException("Couldn't do the thing")); + CommandSender sender = mock(CommandSender.class); + + // when + command.executeCommand(sender, Collections.emptyList()); + + // then + verify(sender).sendMessage("Could not update help file: Couldn't do the thing"); + verifyZeroInteractions(helpMessagesService); + } +} From ecdcaf24796e4668a7e622012ae08232165b87c1 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 22 Apr 2018 13:26:51 +0200 Subject: [PATCH 35/63] Fix failing tests --- .../process/register/AsyncRegisterTest.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/test/java/fr/xephi/authme/process/register/AsyncRegisterTest.java b/src/test/java/fr/xephi/authme/process/register/AsyncRegisterTest.java index 59c384bb..029ff90c 100644 --- a/src/test/java/fr/xephi/authme/process/register/AsyncRegisterTest.java +++ b/src/test/java/fr/xephi/authme/process/register/AsyncRegisterTest.java @@ -12,7 +12,6 @@ import fr.xephi.authme.process.register.executors.RegistrationMethod; import fr.xephi.authme.process.register.executors.TwoFactorRegisterParams; import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.CommonService; -import fr.xephi.authme.settings.properties.PluginSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import org.bukkit.entity.Player; @@ -21,11 +20,11 @@ import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import org.mockito.stubbing.Answer; + +import java.util.function.Function; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.only; import static org.mockito.Mockito.verify; @@ -108,7 +107,7 @@ public class AsyncRegisterTest { @Test @SuppressWarnings("unchecked") - public void shouldStopForFailedExecutorCheck() { + public void shouldStopForCanceledEvent() { // given String name = "edbert"; Player player = mockPlayerWithName(name); @@ -116,14 +115,13 @@ public class AsyncRegisterTest { given(playerCache.isAuthenticated(name)).willReturn(false); given(commonService.getProperty(RegistrationSettings.IS_ENABLED)).willReturn(true); given(dataSource.isAuthAvailable(name)).willReturn(false); - given(commonService.getProperty(PluginSettings.USE_ASYNC_TASKS)).willReturn(true); RegistrationExecutor executor = mock(RegistrationExecutor.class); TwoFactorRegisterParams params = TwoFactorRegisterParams.of(player); singletonStoreWillReturn(registrationExecutorStore, executor); - doAnswer((Answer) invocation -> { - ((AuthMeAsyncPreRegisterEvent) invocation.getArgument(0)).setCanRegister(false); - return null; - }).when(bukkitService).callEvent(any(AuthMeAsyncPreRegisterEvent.class)); + + AuthMeAsyncPreRegisterEvent canceledEvent = new AuthMeAsyncPreRegisterEvent(player, true); + canceledEvent.setCanRegister(false); + given(bukkitService.createAndCallEvent(any(Function.class))).willReturn(canceledEvent); // when asyncRegister.register(RegistrationMethod.TWO_FACTOR_REGISTRATION, params); @@ -134,7 +132,7 @@ public class AsyncRegisterTest { @Test @SuppressWarnings("unchecked") - public void shouldStopForCancelledEvent() { + public void shouldStopForFailedExecutorCheck() { // given String name = "edbert"; Player player = mockPlayerWithName(name); @@ -143,12 +141,14 @@ public class AsyncRegisterTest { given(commonService.getProperty(RegistrationSettings.IS_ENABLED)).willReturn(true); given(commonService.getProperty(RestrictionSettings.MAX_REGISTRATION_PER_IP)).willReturn(0); given(dataSource.isAuthAvailable(name)).willReturn(false); - given(commonService.getProperty(PluginSettings.USE_ASYNC_TASKS)).willReturn(true); RegistrationExecutor executor = mock(RegistrationExecutor.class); TwoFactorRegisterParams params = TwoFactorRegisterParams.of(player); given(executor.isRegistrationAdmitted(params)).willReturn(false); singletonStoreWillReturn(registrationExecutorStore, executor); + given(bukkitService.createAndCallEvent(any(Function.class))) + .willReturn(new AuthMeAsyncPreRegisterEvent(player, false)); + // when asyncRegister.register(RegistrationMethod.TWO_FACTOR_REGISTRATION, params); From d55b4bb3b5ad7d35154e8e614eb31cf7ab7bafc1 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 22 Apr 2018 21:27:38 +0200 Subject: [PATCH 36/63] #1561 Fix timing attacks by comparing hashes in constant time (#1563) * #1561 Fix timing attacks by comparing hashes in constant time * #1561 Fix timing attacks in phpBB fallback hashes - As noted by @games647 --- .../java/fr/xephi/authme/security/HashUtils.java | 15 +++++++++++++++ .../fr/xephi/authme/security/crypts/BCrypt2y.java | 4 +++- .../fr/xephi/authme/security/crypts/Joomla.java | 4 +++- .../fr/xephi/authme/security/crypts/Md5vB.java | 3 ++- .../fr/xephi/authme/security/crypts/PhpBB.java | 6 ++++-- .../security/crypts/SeparateSaltMethod.java | 4 +++- .../fr/xephi/authme/security/crypts/Sha256.java | 5 +++-- .../java/fr/xephi/authme/security/crypts/Smf.java | 4 +++- .../authme/security/crypts/UnsaltedMethod.java | 4 +++- .../security/crypts/UsernameSaltMethod.java | 4 +++- .../fr/xephi/authme/security/crypts/Wbb4.java | 5 +++-- .../xephi/authme/security/crypts/Wordpress.java | 4 +++- .../fr/xephi/authme/security/crypts/XAuth.java | 6 ++++-- .../security/HashAlgorithmIntegrationTest.java | 2 ++ .../fr/xephi/authme/security/HashUtilsTest.java | 9 +++++++++ 15 files changed, 63 insertions(+), 16 deletions(-) diff --git a/src/main/java/fr/xephi/authme/security/HashUtils.java b/src/main/java/fr/xephi/authme/security/HashUtils.java index 3578c80f..642081c6 100644 --- a/src/main/java/fr/xephi/authme/security/HashUtils.java +++ b/src/main/java/fr/xephi/authme/security/HashUtils.java @@ -1,6 +1,7 @@ package fr.xephi.authme.security; import java.math.BigInteger; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -78,6 +79,20 @@ public final class HashUtils { return hash.length() > 3 && hash.substring(0, 2).equals("$2"); } + /** + * Checks whether the two strings are equal to each other in a time-constant manner. + * This helps to avoid timing side channel attacks, + * cf. issue #1561. + * + * @param string1 first string + * @param string2 second string + * @return true if the strings are equal to each other, false otherwise + */ + public static boolean isEqual(String string1, String string2) { + return MessageDigest.isEqual( + string1.getBytes(StandardCharsets.UTF_8), string2.getBytes(StandardCharsets.UTF_8)); + } + /** * Hash the message with the given algorithm and return the hash in its hexadecimal notation. * diff --git a/src/main/java/fr/xephi/authme/security/crypts/BCrypt2y.java b/src/main/java/fr/xephi/authme/security/crypts/BCrypt2y.java index cf4807ab..a22a6890 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/BCrypt2y.java +++ b/src/main/java/fr/xephi/authme/security/crypts/BCrypt2y.java @@ -3,6 +3,8 @@ package fr.xephi.authme.security.crypts; import fr.xephi.authme.security.crypts.description.Recommendation; import fr.xephi.authme.security.crypts.description.Usage; +import static fr.xephi.authme.security.HashUtils.isEqual; + @Recommendation(Usage.RECOMMENDED) public class BCrypt2y extends HexSaltedMethod { @@ -23,7 +25,7 @@ public class BCrypt2y extends HexSaltedMethod { // The salt is the first 29 characters of the hash String salt = hash.substring(0, 29); - return hash.equals(computeHash(password, salt, null)); + return isEqual(hash, computeHash(password, salt, null)); } @Override diff --git a/src/main/java/fr/xephi/authme/security/crypts/Joomla.java b/src/main/java/fr/xephi/authme/security/crypts/Joomla.java index 462f5cb2..2ecc1d8d 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/Joomla.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Joomla.java @@ -4,6 +4,8 @@ import fr.xephi.authme.security.HashUtils; import fr.xephi.authme.security.crypts.description.Recommendation; import fr.xephi.authme.security.crypts.description.Usage; +import static fr.xephi.authme.security.HashUtils.isEqual; + @Recommendation(Usage.ACCEPTABLE) public class Joomla extends HexSaltedMethod { @@ -16,7 +18,7 @@ public class Joomla extends HexSaltedMethod { public boolean comparePassword(String password, HashedPassword hashedPassword, String unusedName) { String hash = hashedPassword.getHash(); String[] hashParts = hash.split(":"); - return hashParts.length == 2 && hash.equals(computeHash(password, hashParts[1], null)); + return hashParts.length == 2 && isEqual(hash, computeHash(password, hashParts[1], null)); } @Override diff --git a/src/main/java/fr/xephi/authme/security/crypts/Md5vB.java b/src/main/java/fr/xephi/authme/security/crypts/Md5vB.java index c244ec49..00656964 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/Md5vB.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Md5vB.java @@ -1,5 +1,6 @@ package fr.xephi.authme.security.crypts; +import static fr.xephi.authme.security.HashUtils.isEqual; import static fr.xephi.authme.security.HashUtils.md5; public class Md5vB extends HexSaltedMethod { @@ -13,7 +14,7 @@ public class Md5vB extends HexSaltedMethod { public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { String hash = hashedPassword.getHash(); String[] line = hash.split("\\$"); - return line.length == 4 && hash.equals(computeHash(password, line[2], name)); + return line.length == 4 && isEqual(hash, computeHash(password, line[2], name)); } @Override diff --git a/src/main/java/fr/xephi/authme/security/crypts/PhpBB.java b/src/main/java/fr/xephi/authme/security/crypts/PhpBB.java index 70ac322d..2d641706 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/PhpBB.java +++ b/src/main/java/fr/xephi/authme/security/crypts/PhpBB.java @@ -10,6 +10,8 @@ import fr.xephi.authme.security.crypts.description.Usage; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; +import static fr.xephi.authme.security.HashUtils.isEqual; + /** * Encryption method compatible with phpBB3. *

@@ -43,7 +45,7 @@ public class PhpBB implements EncryptionMethod { } else if (hash.length() == 34) { return PhpassSaltedMd5.phpbb_check_hash(password, hash); } else { - return PhpassSaltedMd5.md5(password).equals(hash); + return isEqual(hash, PhpassSaltedMd5.md5(password)); } } @@ -153,7 +155,7 @@ public class PhpBB implements EncryptionMethod { } private static boolean phpbb_check_hash(String password, String hash) { - return _hash_crypt_private(password, hash).equals(hash); + return isEqual(hash, _hash_crypt_private(password, hash)); // #1561: fix timing issue } } } diff --git a/src/main/java/fr/xephi/authme/security/crypts/SeparateSaltMethod.java b/src/main/java/fr/xephi/authme/security/crypts/SeparateSaltMethod.java index d0dacda4..c0ec13dd 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/SeparateSaltMethod.java +++ b/src/main/java/fr/xephi/authme/security/crypts/SeparateSaltMethod.java @@ -1,5 +1,7 @@ package fr.xephi.authme.security.crypts; +import static fr.xephi.authme.security.HashUtils.isEqual; + /** * Common supertype for encryption methods which store their salt separately from the hash. */ @@ -19,7 +21,7 @@ public abstract class SeparateSaltMethod implements EncryptionMethod { @Override public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { - return hashedPassword.getHash().equals(computeHash(password, hashedPassword.getSalt(), null)); + return isEqual(hashedPassword.getHash(), computeHash(password, hashedPassword.getSalt(), null)); } @Override diff --git a/src/main/java/fr/xephi/authme/security/crypts/Sha256.java b/src/main/java/fr/xephi/authme/security/crypts/Sha256.java index 1b77a2e4..ce6b2549 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/Sha256.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Sha256.java @@ -3,6 +3,7 @@ package fr.xephi.authme.security.crypts; import fr.xephi.authme.security.crypts.description.Recommendation; import fr.xephi.authme.security.crypts.description.Usage; +import static fr.xephi.authme.security.HashUtils.isEqual; import static fr.xephi.authme.security.HashUtils.sha256; @Recommendation(Usage.RECOMMENDED) @@ -14,10 +15,10 @@ public class Sha256 extends HexSaltedMethod { } @Override - public boolean comparePassword(String password, HashedPassword hashedPassword, String playerName) { + public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { String hash = hashedPassword.getHash(); String[] line = hash.split("\\$"); - return line.length == 4 && hash.equals(computeHash(password, line[2], "")); + return line.length == 4 && isEqual(hash, computeHash(password, line[2], name)); } @Override diff --git a/src/main/java/fr/xephi/authme/security/crypts/Smf.java b/src/main/java/fr/xephi/authme/security/crypts/Smf.java index 24d28fe6..e24c1b83 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/Smf.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Smf.java @@ -7,6 +7,8 @@ import fr.xephi.authme.security.crypts.description.SaltType; import fr.xephi.authme.security.crypts.description.Usage; import fr.xephi.authme.util.RandomStringUtils; +import static fr.xephi.authme.security.HashUtils.isEqual; + /** * Hashing algorithm for SMF forums. *

@@ -32,7 +34,7 @@ public class Smf implements EncryptionMethod { @Override public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { - return computeHash(password, null, name).equals(hashedPassword.getHash()); + return isEqual(hashedPassword.getHash(), computeHash(password, null, name)); } @Override diff --git a/src/main/java/fr/xephi/authme/security/crypts/UnsaltedMethod.java b/src/main/java/fr/xephi/authme/security/crypts/UnsaltedMethod.java index a8f2040e..33815ec7 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/UnsaltedMethod.java +++ b/src/main/java/fr/xephi/authme/security/crypts/UnsaltedMethod.java @@ -5,6 +5,8 @@ import fr.xephi.authme.security.crypts.description.Recommendation; import fr.xephi.authme.security.crypts.description.SaltType; import fr.xephi.authme.security.crypts.description.Usage; +import static fr.xephi.authme.security.HashUtils.isEqual; + /** * Common type for encryption methods which do not use any salt whatsoever. */ @@ -26,7 +28,7 @@ public abstract class UnsaltedMethod implements EncryptionMethod { @Override public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { - return hashedPassword.getHash().equals(computeHash(password)); + return isEqual(hashedPassword.getHash(), computeHash(password)); } @Override diff --git a/src/main/java/fr/xephi/authme/security/crypts/UsernameSaltMethod.java b/src/main/java/fr/xephi/authme/security/crypts/UsernameSaltMethod.java index 23101e22..f5930fcf 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/UsernameSaltMethod.java +++ b/src/main/java/fr/xephi/authme/security/crypts/UsernameSaltMethod.java @@ -5,6 +5,8 @@ import fr.xephi.authme.security.crypts.description.Recommendation; import fr.xephi.authme.security.crypts.description.SaltType; import fr.xephi.authme.security.crypts.description.Usage; +import static fr.xephi.authme.security.HashUtils.isEqual; + /** * Common supertype of encryption methods that use a player's username * (or something based on it) as embedded salt. @@ -23,7 +25,7 @@ public abstract class UsernameSaltMethod implements EncryptionMethod { @Override public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { - return hashedPassword.getHash().equals(computeHash(password, name).getHash()); + return isEqual(hashedPassword.getHash(), computeHash(password, name).getHash()); } @Override diff --git a/src/main/java/fr/xephi/authme/security/crypts/Wbb4.java b/src/main/java/fr/xephi/authme/security/crypts/Wbb4.java index d1d4953d..f396c5d8 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/Wbb4.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Wbb4.java @@ -3,6 +3,7 @@ package fr.xephi.authme.security.crypts; import fr.xephi.authme.security.crypts.description.Recommendation; import fr.xephi.authme.security.crypts.description.Usage; +import static fr.xephi.authme.security.HashUtils.isEqual; import static fr.xephi.authme.security.crypts.BCryptService.hashpw; @Recommendation(Usage.RECOMMENDED) @@ -14,12 +15,12 @@ public class Wbb4 extends HexSaltedMethod { } @Override - public boolean comparePassword(String password, HashedPassword hashedPassword, String playerName) { + public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { if (hashedPassword.getHash().length() != 60) { return false; } String salt = hashedPassword.getHash().substring(0, 29); - return computeHash(password, salt, null).equals(hashedPassword.getHash()); + return isEqual(hashedPassword.getHash(), computeHash(password, salt, name)); } @Override diff --git a/src/main/java/fr/xephi/authme/security/crypts/Wordpress.java b/src/main/java/fr/xephi/authme/security/crypts/Wordpress.java index 768b92c5..f70c0949 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/Wordpress.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Wordpress.java @@ -12,6 +12,8 @@ import java.security.MessageDigest; import java.security.SecureRandom; import java.util.Arrays; +import static fr.xephi.authme.security.HashUtils.isEqual; + @Recommendation(Usage.ACCEPTABLE) @HasSalt(value = SaltType.TEXT, length = 9) // Note ljacqu 20151228: Wordpress is actually a salted algorithm but salt generation is handled internally @@ -115,7 +117,7 @@ public class Wordpress extends UnsaltedMethod { public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { String hash = hashedPassword.getHash(); String comparedHash = crypt(password, hash); - return comparedHash.equals(hash); + return isEqual(hash, comparedHash); } } diff --git a/src/main/java/fr/xephi/authme/security/crypts/XAuth.java b/src/main/java/fr/xephi/authme/security/crypts/XAuth.java index 9f921b6a..62f2e0d7 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/XAuth.java +++ b/src/main/java/fr/xephi/authme/security/crypts/XAuth.java @@ -3,6 +3,8 @@ package fr.xephi.authme.security.crypts; import fr.xephi.authme.security.crypts.description.Recommendation; import fr.xephi.authme.security.crypts.description.Usage; +import static fr.xephi.authme.security.HashUtils.isEqual; + @Recommendation(Usage.RECOMMENDED) public class XAuth extends HexSaltedMethod { @@ -23,14 +25,14 @@ public class XAuth extends HexSaltedMethod { } @Override - public boolean comparePassword(String password, HashedPassword hashedPassword, String playerName) { + public boolean comparePassword(String password, HashedPassword hashedPassword, String name) { String hash = hashedPassword.getHash(); int saltPos = password.length() >= hash.length() ? hash.length() - 1 : password.length(); if (saltPos + 12 > hash.length()) { return false; } String salt = hash.substring(saltPos, saltPos + 12); - return hash.equals(computeHash(password, salt, null)); + return isEqual(hash, computeHash(password, salt, name)); } @Override diff --git a/src/test/java/fr/xephi/authme/security/HashAlgorithmIntegrationTest.java b/src/test/java/fr/xephi/authme/security/HashAlgorithmIntegrationTest.java index fc9fd0d6..e59dae64 100644 --- a/src/test/java/fr/xephi/authme/security/HashAlgorithmIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/security/HashAlgorithmIntegrationTest.java @@ -2,6 +2,7 @@ package fr.xephi.authme.security; import ch.jalu.injector.Injector; import ch.jalu.injector.InjectorBuilder; +import fr.xephi.authme.TestHelper; import fr.xephi.authme.security.crypts.Argon2; import fr.xephi.authme.security.crypts.EncryptionMethod; import fr.xephi.authme.security.crypts.HashedPassword; @@ -40,6 +41,7 @@ public class HashAlgorithmIntegrationTest { given(settings.getProperty(SecuritySettings.PBKDF2_NUMBER_OF_ROUNDS)).willReturn(10_000); injector = new InjectorBuilder().addDefaultHandlers("fr.xephi.authme").create(); injector.register(Settings.class, settings); + TestHelper.setupLogger(); } @Test diff --git a/src/test/java/fr/xephi/authme/security/HashUtilsTest.java b/src/test/java/fr/xephi/authme/security/HashUtilsTest.java index 5c1fda22..440e748a 100644 --- a/src/test/java/fr/xephi/authme/security/HashUtilsTest.java +++ b/src/test/java/fr/xephi/authme/security/HashUtilsTest.java @@ -123,4 +123,13 @@ public class HashUtilsTest { assertThat(HashUtils.isValidBcryptHash("#2ae5fc78"), equalTo(false)); } + @Test + public void shouldCompareStrings() { + // given / when / then + assertThat(HashUtils.isEqual("test", "test"), equalTo(true)); + assertThat(HashUtils.isEqual("test", "Test"), equalTo(false)); + assertThat(HashUtils.isEqual("1234", "1234."), equalTo(false)); + assertThat(HashUtils.isEqual("ພາສາຫວຽດນາມ", "ພາສາຫວຽດນາມ"), equalTo(true)); + assertThat(HashUtils.isEqual("test", "tëst"), equalTo(false)); + } } From b69767c705a55ad682975b6cb8b5ddf14a429ec8 Mon Sep 17 00:00:00 2001 From: games647 Date: Tue, 1 May 2018 13:39:19 +0200 Subject: [PATCH 37/63] Upgrade jacoco dependency to fix Java 10 compatibilty and CircleCI tests --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c6756e67..882d09a7 100644 --- a/pom.xml +++ b/pom.xml @@ -150,7 +150,7 @@ org.jacoco jacoco-maven-plugin - 0.8.0 + 0.8.1 pre-unit-test From 1e3ed795c10125356a56ed1d34c5aa0bfbcc850c Mon Sep 17 00:00:00 2001 From: ljacqu Date: Tue, 1 May 2018 22:49:07 +0200 Subject: [PATCH 38/63] #1141 2FA implementation fixes - Merge TotpService into TotpAuthenticator - Add missing tests - Migrate old 2fa enabled key to new one --- .../executable/totp/ConfirmTotpCommand.java | 6 +- .../executable/totp/RemoveTotpCommand.java | 6 +- .../executable/totp/TotpCodeCommand.java | 6 +- .../message/updater/MessageUpdater.java | 26 +++- .../security/totp/TotpAuthenticator.java | 9 +- .../authme/security/totp/TotpService.java | 18 --- .../executable/totp/AddTotpCommandTest.java | 93 ++++++++++++ .../totp/ConfirmTotpCommandTest.java | 139 ++++++++++++++++++ .../executable/totp/TotpBaseCommandTest.java | 47 ++++++ .../message/updater/MessageUpdaterTest.java | 17 +++ .../security/totp/TotpAuthenticatorTest.java | 20 +++ .../authme/security/totp/TotpServiceTest.java | 46 ------ .../xephi/authme/message/messages_test2.yml | 2 + 13 files changed, 356 insertions(+), 79 deletions(-) delete mode 100644 src/main/java/fr/xephi/authme/security/totp/TotpService.java create mode 100644 src/test/java/fr/xephi/authme/command/executable/totp/AddTotpCommandTest.java create mode 100644 src/test/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommandTest.java create mode 100644 src/test/java/fr/xephi/authme/command/executable/totp/TotpBaseCommandTest.java delete mode 100644 src/test/java/fr/xephi/authme/security/totp/TotpServiceTest.java diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java index 52d1a980..9dcd99a6 100644 --- a/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java @@ -33,13 +33,17 @@ public class ConfirmTotpCommand extends PlayerCommand { messages.send(player, MessageKey.REGISTER_MESSAGE); } else if (auth.getTotpKey() != null) { messages.send(player, MessageKey.TWO_FACTOR_ALREADY_ENABLED); + } else { + verifyTotpCodeConfirmation(player, arguments.get(0)); } + } + private void verifyTotpCodeConfirmation(Player player, String inputTotpCode) { final TotpGenerationResult totpDetails = generateTotpService.getGeneratedTotpKey(player); if (totpDetails == null) { messages.send(player, MessageKey.TWO_FACTOR_ENABLE_ERROR_NO_CODE); } else { - boolean isCodeValid = generateTotpService.isTotpCodeCorrectForGeneratedTotpKey(player, arguments.get(0)); + boolean isCodeValid = generateTotpService.isTotpCodeCorrectForGeneratedTotpKey(player, inputTotpCode); if (isCodeValid) { generateTotpService.removeGenerateTotpKey(player); dataSource.setTotpKey(player.getName(), totpDetails.getTotpKey()); diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java index 20ef4a1b..1ad42cb8 100644 --- a/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java @@ -5,7 +5,7 @@ import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.message.Messages; -import fr.xephi.authme.security.totp.TotpService; +import fr.xephi.authme.security.totp.TotpAuthenticator; import org.bukkit.entity.Player; import javax.inject.Inject; @@ -20,7 +20,7 @@ public class RemoveTotpCommand extends PlayerCommand { private DataSource dataSource; @Inject - private TotpService totpService; + private TotpAuthenticator totpAuthenticator; @Inject private Messages messages; @@ -31,7 +31,7 @@ public class RemoveTotpCommand extends PlayerCommand { if (auth.getTotpKey() == null) { messages.send(player, MessageKey.TWO_FACTOR_NOT_ENABLED_ERROR); } else { - if (totpService.verifyCode(auth, arguments.get(0))) { + if (totpAuthenticator.checkCode(auth, arguments.get(0))) { dataSource.removeTotpKey(auth.getNickname()); messages.send(player, MessageKey.TWO_FACTOR_REMOVED_SUCCESS); } else { diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/TotpCodeCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/TotpCodeCommand.java index d8b7a28f..3ddbb964 100644 --- a/src/main/java/fr/xephi/authme/command/executable/totp/TotpCodeCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/totp/TotpCodeCommand.java @@ -10,7 +10,7 @@ import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.message.Messages; import fr.xephi.authme.process.login.AsynchronousLogin; -import fr.xephi.authme.security.totp.TotpService; +import fr.xephi.authme.security.totp.TotpAuthenticator; import org.bukkit.entity.Player; import javax.inject.Inject; @@ -31,7 +31,7 @@ public class TotpCodeCommand extends PlayerCommand { private Messages messages; @Inject - private TotpService totpService; + private TotpAuthenticator totpAuthenticator; @Inject private DataSource dataSource; @@ -61,7 +61,7 @@ public class TotpCodeCommand extends PlayerCommand { } private void processCode(Player player, PlayerAuth auth, String inputCode) { - boolean isCodeValid = totpService.verifyCode(auth, inputCode); + boolean isCodeValid = totpAuthenticator.checkCode(auth, inputCode); if (isCodeValid) { asynchronousLogin.performLogin(player, auth); } else { diff --git a/src/main/java/fr/xephi/authme/message/updater/MessageUpdater.java b/src/main/java/fr/xephi/authme/message/updater/MessageUpdater.java index 579968ee..433f0b17 100644 --- a/src/main/java/fr/xephi/authme/message/updater/MessageUpdater.java +++ b/src/main/java/fr/xephi/authme/message/updater/MessageUpdater.java @@ -5,7 +5,7 @@ import ch.jalu.configme.configurationdata.ConfigurationData; import ch.jalu.configme.configurationdata.PropertyListBuilder; import ch.jalu.configme.properties.Property; import ch.jalu.configme.properties.StringProperty; -import ch.jalu.configme.resource.YamlFileResource; +import ch.jalu.configme.resource.PropertyResource; import com.google.common.collect.ImmutableMap; import com.google.common.io.Files; import fr.xephi.authme.ConsoleLogger; @@ -57,14 +57,16 @@ public class MessageUpdater { */ private boolean migrateAndSave(File userFile, JarMessageSource jarMessageSource) { // YamlConfiguration escapes all special characters when saving, making the file hard to use, so use ConfigMe - YamlFileResource userResource = new MigraterYamlFileResource(userFile); + PropertyResource userResource = new MigraterYamlFileResource(userFile); // Step 1: Migrate any old keys in the file to the new paths boolean movedOldKeys = migrateOldKeys(userResource); - // Step 2: Take any missing messages from the message files shipped in the AuthMe JAR + // Step 2: Perform newer migrations + boolean movedNewerKeys = migrateKeys(userResource); + // Step 3: Take any missing messages from the message files shipped in the AuthMe JAR boolean addedMissingKeys = addMissingKeys(jarMessageSource, userResource); - if (movedOldKeys || addedMissingKeys) { + if (movedOldKeys || movedNewerKeys || addedMissingKeys) { backupMessagesFile(userFile); SettingsManager settingsManager = new SettingsManager(userResource, null, CONFIGURATION_DATA); @@ -75,7 +77,19 @@ public class MessageUpdater { return false; } - private boolean migrateOldKeys(YamlFileResource userResource) { + private boolean migrateKeys(PropertyResource userResource) { + return moveIfApplicable(userResource, "misc.two_factor_create", MessageKey.TWO_FACTOR_CREATE.getKey()); + } + + private static boolean moveIfApplicable(PropertyResource resource, String oldPath, String newPath) { + if (resource.getString(newPath) == null && resource.getString(oldPath) != null) { + resource.setValue(newPath, resource.getString(oldPath)); + return true; + } + return false; + } + + private boolean migrateOldKeys(PropertyResource userResource) { boolean hasChange = OldMessageKeysMigrater.migrateOldPaths(userResource); if (hasChange) { ConsoleLogger.info("Old keys have been moved to the new ones in your messages_xx.yml file"); @@ -83,7 +97,7 @@ public class MessageUpdater { return hasChange; } - private boolean addMissingKeys(JarMessageSource jarMessageSource, YamlFileResource userResource) { + private boolean addMissingKeys(JarMessageSource jarMessageSource, PropertyResource userResource) { List addedKeys = new ArrayList<>(); for (Property property : CONFIGURATION_DATA.getProperties()) { final String key = property.getPath(); diff --git a/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java b/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java index 9fc1c6a2..eb922cf4 100644 --- a/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java +++ b/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java @@ -4,13 +4,14 @@ import com.warrenstrange.googleauth.GoogleAuthenticator; import com.warrenstrange.googleauth.GoogleAuthenticatorKey; import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator; import com.warrenstrange.googleauth.IGoogleAuthenticator; +import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.service.BukkitService; import org.bukkit.entity.Player; import javax.inject.Inject; /** - * Provides rudimentary TOTP functions (wraps third-party TOTP implementation). + * Provides TOTP functions (wrapping a third-party TOTP implementation). */ public class TotpAuthenticator { @@ -30,6 +31,10 @@ public class TotpAuthenticator { return new GoogleAuthenticator(); } + public boolean checkCode(PlayerAuth auth, String totpCode) { + return checkCode(auth.getTotpKey(), totpCode); + } + /** * Returns whether the given input code matches for the provided TOTP key. * @@ -58,7 +63,7 @@ public class TotpAuthenticator { private final String totpKey; private final String authenticatorQrCodeUrl; - TotpGenerationResult(String totpKey, String authenticatorQrCodeUrl) { + public TotpGenerationResult(String totpKey, String authenticatorQrCodeUrl) { this.totpKey = totpKey; this.authenticatorQrCodeUrl = authenticatorQrCodeUrl; } diff --git a/src/main/java/fr/xephi/authme/security/totp/TotpService.java b/src/main/java/fr/xephi/authme/security/totp/TotpService.java deleted file mode 100644 index 15e3381f..00000000 --- a/src/main/java/fr/xephi/authme/security/totp/TotpService.java +++ /dev/null @@ -1,18 +0,0 @@ -package fr.xephi.authme.security.totp; - -import fr.xephi.authme.data.auth.PlayerAuth; - -import javax.inject.Inject; - -/** - * Service for TOTP actions. - */ -public class TotpService { - - @Inject - private TotpAuthenticator totpAuthenticator; - - public boolean verifyCode(PlayerAuth auth, String totpCode) { - return totpAuthenticator.checkCode(auth.getTotpKey(), totpCode); - } -} diff --git a/src/test/java/fr/xephi/authme/command/executable/totp/AddTotpCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/totp/AddTotpCommandTest.java new file mode 100644 index 00000000..8791226a --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/totp/AddTotpCommandTest.java @@ -0,0 +1,93 @@ +package fr.xephi.authme.command.executable.totp; + +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.security.totp.GenerateTotpService; +import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult; +import org.bukkit.entity.Player; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Collections; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link AddTotpCommand}. + */ +@RunWith(MockitoJUnitRunner.class) +public class AddTotpCommandTest { + + @InjectMocks + private AddTotpCommand addTotpCommand; + + @Mock + private GenerateTotpService generateTotpService; + @Mock + private DataSource dataSource; + @Mock + private Messages messages; + + @Test + public void shouldHandleNonExistentUser() { + // given + Player player = mockPlayerWithName("bob"); + given(dataSource.getAuth("bob")).willReturn(null); + + // when + addTotpCommand.runCommand(player, Collections.emptyList()); + + // then + verify(messages).send(player, MessageKey.REGISTER_MESSAGE); + verifyZeroInteractions(generateTotpService); + } + + @Test + public void shouldNotAddCodeForAlreadyExistingTotp() { + // given + Player player = mockPlayerWithName("arend"); + PlayerAuth auth = PlayerAuth.builder().name("arend") + .totpKey("TOTP2345").build(); + given(dataSource.getAuth("arend")).willReturn(auth); + + // when + addTotpCommand.runCommand(player, Collections.emptyList()); + + // then + verify(messages).send(player, MessageKey.TWO_FACTOR_ALREADY_ENABLED); + verifyZeroInteractions(generateTotpService); + } + + @Test + public void shouldGenerateTotpCode() { + // given + Player player = mockPlayerWithName("charles"); + PlayerAuth auth = PlayerAuth.builder().name("charles").build(); + given(dataSource.getAuth("charles")).willReturn(auth); + + TotpGenerationResult generationResult = new TotpGenerationResult( + "777Key214", "http://example.org/qr-code/link"); + given(generateTotpService.generateTotpKey(player)).willReturn(generationResult); + + // when + addTotpCommand.runCommand(player, Collections.emptyList()); + + // then + verify(messages).send(player, MessageKey.TWO_FACTOR_CREATE, generationResult.getTotpKey(), generationResult.getAuthenticatorQrCodeUrl()); + verify(messages).send(player, MessageKey.TWO_FACTOR_CREATE_CONFIRMATION_REQUIRED); + } + + private static Player mockPlayerWithName(String name) { + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + return player; + } +} diff --git a/src/test/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommandTest.java new file mode 100644 index 00000000..0d921d37 --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommandTest.java @@ -0,0 +1,139 @@ +package fr.xephi.authme.command.executable.totp; + +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.security.totp.GenerateTotpService; +import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult; +import org.bukkit.entity.Player; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Collections; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.only; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link ConfirmTotpCommand}. + */ +@RunWith(MockitoJUnitRunner.class) +public class ConfirmTotpCommandTest { + + @InjectMocks + private ConfirmTotpCommand command; + + @Mock + private GenerateTotpService generateTotpService; + @Mock + private DataSource dataSource; + @Mock + private Messages messages; + + @Test + public void shouldAddTotpCodeToUserAfterSuccessfulConfirmation() { + // given + Player player = mock(Player.class); + String playerName = "George"; + given(player.getName()).willReturn(playerName); + PlayerAuth auth = PlayerAuth.builder().name(playerName).build(); + given(dataSource.getAuth(playerName)).willReturn(auth); + given(generateTotpService.getGeneratedTotpKey(player)).willReturn(new TotpGenerationResult("totp-key", "url-not-relevant")); + String totpCode = "954321"; + given(generateTotpService.isTotpCodeCorrectForGeneratedTotpKey(player, totpCode)).willReturn(true); + + // when + command.runCommand(player, Collections.singletonList(totpCode)); + + // then + verify(generateTotpService).isTotpCodeCorrectForGeneratedTotpKey(player, totpCode); + verify(generateTotpService).removeGenerateTotpKey(player); + verify(dataSource).setTotpKey(playerName, "totp-key"); + verify(messages).send(player, MessageKey.TWO_FACTOR_ENABLE_SUCCESS); + } + + @Test + public void shouldHandleWrongTotpCode() { + // given + Player player = mock(Player.class); + String playerName = "George"; + given(player.getName()).willReturn(playerName); + PlayerAuth auth = PlayerAuth.builder().name(playerName).build(); + given(dataSource.getAuth(playerName)).willReturn(auth); + given(generateTotpService.getGeneratedTotpKey(player)).willReturn(new TotpGenerationResult("totp-key", "url-not-relevant")); + String totpCode = "754321"; + given(generateTotpService.isTotpCodeCorrectForGeneratedTotpKey(player, totpCode)).willReturn(false); + + // when + command.runCommand(player, Collections.singletonList(totpCode)); + + // then + verify(generateTotpService).isTotpCodeCorrectForGeneratedTotpKey(player, totpCode); + verify(generateTotpService, never()).removeGenerateTotpKey(any(Player.class)); + verify(dataSource, only()).getAuth(playerName); + verify(messages).send(player, MessageKey.TWO_FACTOR_ENABLE_ERROR_WRONG_CODE); + } + + @Test + public void shouldHandleMissingTotpKey() { + // given + Player player = mock(Player.class); + String playerName = "George"; + given(player.getName()).willReturn(playerName); + PlayerAuth auth = PlayerAuth.builder().name(playerName).build(); + given(dataSource.getAuth(playerName)).willReturn(auth); + given(generateTotpService.getGeneratedTotpKey(player)).willReturn(null); + + // when + command.runCommand(player, Collections.singletonList("871634")); + + // then + verify(generateTotpService, only()).getGeneratedTotpKey(player); + verify(dataSource, only()).getAuth(playerName); + verify(messages).send(player, MessageKey.TWO_FACTOR_ENABLE_ERROR_NO_CODE); + } + + @Test + public void shouldStopForAlreadyExistingTotpKeyOnAccount() { + // given + Player player = mock(Player.class); + String playerName = "George"; + given(player.getName()).willReturn(playerName); + PlayerAuth auth = PlayerAuth.builder().name(playerName).totpKey("A987234").build(); + given(dataSource.getAuth(playerName)).willReturn(auth); + + // when + command.runCommand(player, Collections.singletonList("871634")); + + // then + verify(dataSource, only()).getAuth(playerName); + verifyZeroInteractions(generateTotpService); + verify(messages).send(player, MessageKey.TWO_FACTOR_ALREADY_ENABLED); + } + + @Test + public void shouldHandleMissingAuthAccount() { + // given + Player player = mock(Player.class); + String playerName = "George"; + given(player.getName()).willReturn(playerName); + given(dataSource.getAuth(playerName)).willReturn(null); + + // when + command.runCommand(player, Collections.singletonList("984685")); + + // then + verify(dataSource, only()).getAuth(playerName); + verifyZeroInteractions(generateTotpService); + verify(messages).send(player, MessageKey.REGISTER_MESSAGE); + } +} diff --git a/src/test/java/fr/xephi/authme/command/executable/totp/TotpBaseCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/totp/TotpBaseCommandTest.java new file mode 100644 index 00000000..0e279f68 --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/totp/TotpBaseCommandTest.java @@ -0,0 +1,47 @@ +package fr.xephi.authme.command.executable.totp; + +import fr.xephi.authme.command.CommandMapper; +import fr.xephi.authme.command.FoundCommandResult; +import fr.xephi.authme.command.help.HelpProvider; +import org.bukkit.command.CommandSender; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Collections; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Test for {@link TotpBaseCommand}. + */ +@RunWith(MockitoJUnitRunner.class) +public class TotpBaseCommandTest { + + @InjectMocks + private TotpBaseCommand command; + + @Mock + private CommandMapper mapper; + @Mock + private HelpProvider helpProvider; + + @Test + public void shouldOutputHelp() { + // given + CommandSender sender = mock(CommandSender.class); + FoundCommandResult mappingResult = mock(FoundCommandResult.class); + given(mapper.mapPartsToCommand(sender, Collections.singletonList("totp"))).willReturn(mappingResult); + + // when + command.executeCommand(sender, Collections.emptyList()); + + // then + verify(mapper).mapPartsToCommand(sender, Collections.singletonList("totp")); + verify(helpProvider).outputHelp(sender, mappingResult, HelpProvider.SHOW_CHILDREN); + } +} diff --git a/src/test/java/fr/xephi/authme/message/updater/MessageUpdaterTest.java b/src/test/java/fr/xephi/authme/message/updater/MessageUpdaterTest.java index 83f5a5c6..254e0b1a 100644 --- a/src/test/java/fr/xephi/authme/message/updater/MessageUpdaterTest.java +++ b/src/test/java/fr/xephi/authme/message/updater/MessageUpdaterTest.java @@ -100,6 +100,23 @@ public class MessageUpdaterTest { equalTo("seconds in plural")); } + @Test + public void shouldPerformNewerMigrations() throws IOException { + // given + File messagesFile = temporaryFolder.newFile(); + Files.copy(TestHelper.getJarFile(TestHelper.PROJECT_ROOT + "message/messages_test2.yml"), messagesFile); + + // when + boolean wasChanged = messageUpdater.migrateAndSave(messagesFile, "messages/messages_en.yml", "messages/messages_en.yml"); + + // then + assertThat(wasChanged, equalTo(true)); + FileConfiguration configuration = YamlConfiguration.loadConfiguration(messagesFile); + assertThat(configuration.getString(MessageKey.TWO_FACTOR_CREATE.getKey()), equalTo("Old 2fa create text")); + assertThat(configuration.getString(MessageKey.WRONG_PASSWORD.getKey()), equalTo("test2 - wrong password")); // from pre-5.5 key + assertThat(configuration.getString(MessageKey.SECOND.getKey()), equalTo("second")); // from messages_en.yml + } + @Test public void shouldHaveAllKeysInConfigurationData() { // given diff --git a/src/test/java/fr/xephi/authme/security/totp/TotpAuthenticatorTest.java b/src/test/java/fr/xephi/authme/security/totp/TotpAuthenticatorTest.java index 27434cce..3afc8181 100644 --- a/src/test/java/fr/xephi/authme/security/totp/TotpAuthenticatorTest.java +++ b/src/test/java/fr/xephi/authme/security/totp/TotpAuthenticatorTest.java @@ -1,6 +1,7 @@ package fr.xephi.authme.security.totp; import com.warrenstrange.googleauth.IGoogleAuthenticator; +import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult; import fr.xephi.authme.service.BukkitService; import org.bukkit.entity.Player; @@ -86,6 +87,25 @@ public class TotpAuthenticatorTest { verifyZeroInteractions(googleAuthenticator); } + @Test + public void shouldVerifyCode() { + // given + String totpKey = "ASLO43KDF2J"; + PlayerAuth auth = PlayerAuth.builder() + .name("Maya") + .totpKey(totpKey) + .build(); + String inputCode = "408435"; + given(totpAuthenticator.checkCode(totpKey, inputCode)).willReturn(true); + + // when + boolean result = totpAuthenticator.checkCode(auth, inputCode); + + // then + assertThat(result, equalTo(true)); + verify(googleAuthenticator).authorize(totpKey, 408435); + } + private final class TotpAuthenticatorTestImpl extends TotpAuthenticator { TotpAuthenticatorTestImpl(BukkitService bukkitService) { diff --git a/src/test/java/fr/xephi/authme/security/totp/TotpServiceTest.java b/src/test/java/fr/xephi/authme/security/totp/TotpServiceTest.java deleted file mode 100644 index 7d321cf1..00000000 --- a/src/test/java/fr/xephi/authme/security/totp/TotpServiceTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package fr.xephi.authme.security.totp; - -import fr.xephi.authme.data.auth.PlayerAuth; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import static org.hamcrest.Matchers.equalTo; -import static org.junit.Assert.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.verify; - -/** - * Test for {@link TotpService}. - */ -@RunWith(MockitoJUnitRunner.class) -public class TotpServiceTest { - - @InjectMocks - private TotpService totpService; - - @Mock - private TotpAuthenticator totpAuthenticator; - - @Test - public void shouldVerifyCode() { - // given - String totpKey = "ASLO43KDF2J"; - PlayerAuth auth = PlayerAuth.builder() - .name("Maya") - .totpKey(totpKey) - .build(); - String inputCode = "408435"; - given(totpAuthenticator.checkCode(totpKey, inputCode)).willReturn(true); - - // when - boolean result = totpService.verifyCode(auth, inputCode); - - // then - assertThat(result, equalTo(true)); - verify(totpAuthenticator).checkCode(totpKey, inputCode); - } - -} diff --git a/src/test/resources/fr/xephi/authme/message/messages_test2.yml b/src/test/resources/fr/xephi/authme/message/messages_test2.yml index e4a60723..f2871ddb 100644 --- a/src/test/resources/fr/xephi/authme/message/messages_test2.yml +++ b/src/test/resources/fr/xephi/authme/message/messages_test2.yml @@ -4,3 +4,5 @@ unknown_user: 'Message from test2' login: 'test2 - login' not_logged_in: 'test2 - not logged in' wrong_pwd: 'test2 - wrong password' +misc: + two_factor_create: 'Old 2fa create text' From 4f9a869a46012682ad57058af86a4db0fe5cbd3d Mon Sep 17 00:00:00 2001 From: rafael59r2 Date: Sun, 13 May 2018 14:15:01 +0100 Subject: [PATCH 39/63] Translation messages_pt.yml (#1569) --- src/main/resources/messages/messages_pt.yml | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/resources/messages/messages_pt.yml b/src/main/resources/messages/messages_pt.yml index b7cabdc8..2a735541 100644 --- a/src/main/resources/messages/messages_pt.yml +++ b/src/main/resources/messages/messages_pt.yml @@ -79,7 +79,7 @@ on_join_validation: country_banned: 'O seu país está banido deste servidor' not_owner_error: 'Não é o proprietário da conta. Por favor, escolha outro nome!' invalid_name_case: 'Deve se juntar usando nome de usuário %valid, não %invalid.' - # TODO quick_command: 'You used a command too fast! Please, join the server again and wait more before using any command.' + quick_command: 'Você usou o comando demasiado rapido por favor re-entre no servidor e aguarde antes de digitar qualquer comando.' # Email email: @@ -99,8 +99,8 @@ email: send_failure: 'Não foi possivel enviar o email. Por favor contate um administrador.' change_password_expired: 'Você não pode mais alterar a sua password usando este comando.' email_cooldown_error: '&cUm email já foi enviado recentemente.Por favor, espere %time antes de enviar novamente' - # TODO add_not_allowed: '&cAdding email was not allowed' - # TODO change_not_allowed: '&cChanging email was not allowed' + add_not_allowed: '&cAdicionar e-mail não é permitido' + change_not_allowed: '&cAlterar e-mail não é permitido' # Password recovery by email recovery: @@ -119,18 +119,18 @@ captcha: usage_captcha: '&cPrecisa digitar um captcha, escreva: /captcha %captcha_code' wrong_captcha: '&cCaptcha errado, por favor escreva: /captcha %captcha_code' valid_captcha: '&cO seu captcha é válido!' - # TODO captcha_for_registration: 'To register you have to solve a captcha first, please use the command: /captcha %captcha_code' - # TODO register_captcha_valid: '&2Valid captcha! You may now register with /register' + captcha_for_registration: 'Para se registar tem de resolver o captcha primeiro, por favor use: /captcha %captcha_code' + register_captcha_valid: '&2Captcha Valido! Agora você pode te registar com /register' # Verification code verification: - # TODO code_required: '&3This command is sensitive and requires an email verification! Check your inbox and follow the email''s instructions.' - # TODO command_usage: '&cUsage: /verification ' - # TODO incorrect_code: '&cIncorrect code, please type "/verification " into the chat, using the code you received by email' - # TODO success: '&2Your identity has been verified! You can now execute all commands within the current session!' - # TODO already_verified: '&2You can already execute every sensitive command within the current session!' - # TODO code_expired: '&3Your code has expired! Execute another sensitive command to get a new code!' - # TODO email_needed: '&3To verify your identity you need to link an email address with your account!!' + code_required: '&3Este codigo é sensivel e requer uma verificação por e-mail! Verifique na sua caixa de entrada e siga as instruções do e-mail.' + command_usage: '&cUso: /verification ' + incorrect_code: '&cCodigo incorreto, por favor digite "/verification " no chat, utilizando o codigo que recebeu no e-mail.' + success: '&2Sua identidade foi verificada! Agora você pode executar todos os comandos nesta sessão!' + already_verified: '&2Você já pode digitar todos os comandos sensiveis nesta sessão!' + code_expired: '&3Seu codigo expirou! Execute outro comando sensivel para obter um novo codigo!' + email_needed: '&3Para confirmar a sua identidade necessita de associar um endereço de e-mail!!' # Time units time: From 729c567dd56e77050a804c51756cd549689f42ba Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 13 May 2018 18:49:40 +0200 Subject: [PATCH 40/63] #1141 Check that user is logged in before changing TOTP key - Use PlayerCache to check that user is logged in where appropriate - Add log statements --- .../executable/totp/AddTotpCommand.java | 8 +- .../executable/totp/ConfirmTotpCommand.java | 27 +++- .../executable/totp/RemoveTotpCommand.java | 25 ++- .../executable/totp/TotpCodeCommand.java | 5 + .../fr/xephi/authme/data/auth/PlayerAuth.java | 4 + .../executable/totp/AddTotpCommandTest.java | 14 +- .../totp/ConfirmTotpCommandTest.java | 47 ++++-- .../totp/RemoveTotpCommandTest.java | 149 ++++++++++++++++++ 8 files changed, 244 insertions(+), 35 deletions(-) create mode 100644 src/test/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommandTest.java diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java index a52741b2..e8a78bd1 100644 --- a/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/totp/AddTotpCommand.java @@ -2,7 +2,7 @@ package fr.xephi.authme.command.executable.totp; import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.data.auth.PlayerAuth; -import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.message.Messages; import fr.xephi.authme.security.totp.GenerateTotpService; @@ -21,16 +21,16 @@ public class AddTotpCommand extends PlayerCommand { private GenerateTotpService generateTotpService; @Inject - private DataSource dataSource; + private PlayerCache playerCache; @Inject private Messages messages; @Override protected void runCommand(Player player, List arguments) { - PlayerAuth auth = dataSource.getAuth(player.getName()); + PlayerAuth auth = playerCache.getAuth(player.getName()); if (auth == null) { - messages.send(player, MessageKey.REGISTER_MESSAGE); + messages.send(player, MessageKey.NOT_LOGGED_IN); } else if (auth.getTotpKey() == null) { TotpGenerationResult createdTotpInfo = generateTotpService.generateTotpKey(player); messages.send(player, MessageKey.TWO_FACTOR_CREATE, diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java index 9dcd99a6..1ab8192b 100644 --- a/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommand.java @@ -1,7 +1,9 @@ package fr.xephi.authme.command.executable.totp; +import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.message.Messages; @@ -20,6 +22,9 @@ public class ConfirmTotpCommand extends PlayerCommand { @Inject private GenerateTotpService generateTotpService; + @Inject + private PlayerCache playerCache; + @Inject private DataSource dataSource; @@ -28,17 +33,17 @@ public class ConfirmTotpCommand extends PlayerCommand { @Override protected void runCommand(Player player, List arguments) { - PlayerAuth auth = dataSource.getAuth(player.getName()); + PlayerAuth auth = playerCache.getAuth(player.getName()); if (auth == null) { - messages.send(player, MessageKey.REGISTER_MESSAGE); + messages.send(player, MessageKey.NOT_LOGGED_IN); } else if (auth.getTotpKey() != null) { messages.send(player, MessageKey.TWO_FACTOR_ALREADY_ENABLED); } else { - verifyTotpCodeConfirmation(player, arguments.get(0)); + verifyTotpCodeConfirmation(player, auth, arguments.get(0)); } } - private void verifyTotpCodeConfirmation(Player player, String inputTotpCode) { + private void verifyTotpCodeConfirmation(Player player, PlayerAuth auth, String inputTotpCode) { final TotpGenerationResult totpDetails = generateTotpService.getGeneratedTotpKey(player); if (totpDetails == null) { messages.send(player, MessageKey.TWO_FACTOR_ENABLE_ERROR_NO_CODE); @@ -46,11 +51,21 @@ public class ConfirmTotpCommand extends PlayerCommand { boolean isCodeValid = generateTotpService.isTotpCodeCorrectForGeneratedTotpKey(player, inputTotpCode); if (isCodeValid) { generateTotpService.removeGenerateTotpKey(player); - dataSource.setTotpKey(player.getName(), totpDetails.getTotpKey()); - messages.send(player, MessageKey.TWO_FACTOR_ENABLE_SUCCESS); + insertTotpKeyIntoDatabase(player, auth, totpDetails); } else { messages.send(player, MessageKey.TWO_FACTOR_ENABLE_ERROR_WRONG_CODE); } } } + + private void insertTotpKeyIntoDatabase(Player player, PlayerAuth auth, TotpGenerationResult totpDetails) { + if (dataSource.setTotpKey(player.getName(), totpDetails.getTotpKey())) { + messages.send(player, MessageKey.TWO_FACTOR_ENABLE_SUCCESS); + auth.setTotpKey(totpDetails.getTotpKey()); + playerCache.updatePlayer(auth); + ConsoleLogger.info("Player '" + player.getName() + "' has successfully added a TOTP key to their account"); + } else { + messages.send(player, MessageKey.ERROR); + } + } } diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java index 1ad42cb8..ebcf554c 100644 --- a/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommand.java @@ -1,7 +1,9 @@ package fr.xephi.authme.command.executable.totp; +import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.message.Messages; @@ -19,6 +21,9 @@ public class RemoveTotpCommand extends PlayerCommand { @Inject private DataSource dataSource; + @Inject + private PlayerCache playerCache; + @Inject private TotpAuthenticator totpAuthenticator; @@ -27,16 +32,28 @@ public class RemoveTotpCommand extends PlayerCommand { @Override protected void runCommand(Player player, List arguments) { - PlayerAuth auth = dataSource.getAuth(player.getName()); - if (auth.getTotpKey() == null) { + PlayerAuth auth = playerCache.getAuth(player.getName()); + if (auth == null) { + messages.send(player, MessageKey.NOT_LOGGED_IN); + } else if (auth.getTotpKey() == null) { messages.send(player, MessageKey.TWO_FACTOR_NOT_ENABLED_ERROR); } else { if (totpAuthenticator.checkCode(auth, arguments.get(0))) { - dataSource.removeTotpKey(auth.getNickname()); - messages.send(player, MessageKey.TWO_FACTOR_REMOVED_SUCCESS); + removeTotpKeyFromDatabase(player, auth); } else { messages.send(player, MessageKey.TWO_FACTOR_INVALID_CODE); } } } + + private void removeTotpKeyFromDatabase(Player player, PlayerAuth auth) { + if (dataSource.removeTotpKey(auth.getNickname())) { + auth.setTotpKey(null); + playerCache.updatePlayer(auth); + messages.send(player, MessageKey.TWO_FACTOR_REMOVED_SUCCESS); + ConsoleLogger.info("Player '" + player.getName() + "' removed their TOTP key"); + } else { + messages.send(player, MessageKey.ERROR); + } + } } diff --git a/src/main/java/fr/xephi/authme/command/executable/totp/TotpCodeCommand.java b/src/main/java/fr/xephi/authme/command/executable/totp/TotpCodeCommand.java index 3ddbb964..39875902 100644 --- a/src/main/java/fr/xephi/authme/command/executable/totp/TotpCodeCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/totp/TotpCodeCommand.java @@ -1,5 +1,6 @@ package fr.xephi.authme.command.executable.totp; +import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.data.auth.PlayerCache; @@ -56,6 +57,8 @@ public class TotpCodeCommand extends PlayerCommand { if (limbo != null && limbo.getState() == LimboPlayerState.TOTP_REQUIRED) { processCode(player, auth, arguments.get(0)); } else { + ConsoleLogger.debug(() -> "Aborting TOTP check for player '" + player.getName() + + "'. Invalid limbo state: " + (limbo == null ? "no limbo" : limbo.getState())); messages.send(player, MessageKey.LOGIN_MESSAGE); } } @@ -63,8 +66,10 @@ public class TotpCodeCommand extends PlayerCommand { private void processCode(Player player, PlayerAuth auth, String inputCode) { boolean isCodeValid = totpAuthenticator.checkCode(auth, inputCode); if (isCodeValid) { + ConsoleLogger.debug("Successfully checked TOTP code for `{0}`", player.getName()); asynchronousLogin.performLogin(player, auth); } else { + ConsoleLogger.debug("Input TOTP code was invalid for player `{0}`", player.getName()); messages.send(player, MessageKey.TWO_FACTOR_INVALID_CODE); } } diff --git a/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java b/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java index 534c0c01..53d74dfb 100644 --- a/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java +++ b/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java @@ -165,6 +165,10 @@ public class PlayerAuth { return totpKey; } + public void setTotpKey(String totpKey) { + this.totpKey = totpKey; + } + @Override public boolean equals(Object obj) { if (!(obj instanceof PlayerAuth)) { diff --git a/src/test/java/fr/xephi/authme/command/executable/totp/AddTotpCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/totp/AddTotpCommandTest.java index 8791226a..ff8608b3 100644 --- a/src/test/java/fr/xephi/authme/command/executable/totp/AddTotpCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/totp/AddTotpCommandTest.java @@ -1,7 +1,7 @@ package fr.xephi.authme.command.executable.totp; import fr.xephi.authme.data.auth.PlayerAuth; -import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.message.Messages; import fr.xephi.authme.security.totp.GenerateTotpService; @@ -32,21 +32,21 @@ public class AddTotpCommandTest { @Mock private GenerateTotpService generateTotpService; @Mock - private DataSource dataSource; + private PlayerCache playerCache; @Mock private Messages messages; @Test - public void shouldHandleNonExistentUser() { + public void shouldHandleNonLoggedInUser() { // given Player player = mockPlayerWithName("bob"); - given(dataSource.getAuth("bob")).willReturn(null); + given(playerCache.getAuth("bob")).willReturn(null); // when addTotpCommand.runCommand(player, Collections.emptyList()); // then - verify(messages).send(player, MessageKey.REGISTER_MESSAGE); + verify(messages).send(player, MessageKey.NOT_LOGGED_IN); verifyZeroInteractions(generateTotpService); } @@ -56,7 +56,7 @@ public class AddTotpCommandTest { Player player = mockPlayerWithName("arend"); PlayerAuth auth = PlayerAuth.builder().name("arend") .totpKey("TOTP2345").build(); - given(dataSource.getAuth("arend")).willReturn(auth); + given(playerCache.getAuth("arend")).willReturn(auth); // when addTotpCommand.runCommand(player, Collections.emptyList()); @@ -71,7 +71,7 @@ public class AddTotpCommandTest { // given Player player = mockPlayerWithName("charles"); PlayerAuth auth = PlayerAuth.builder().name("charles").build(); - given(dataSource.getAuth("charles")).willReturn(auth); + given(playerCache.getAuth("charles")).willReturn(auth); TotpGenerationResult generationResult = new TotpGenerationResult( "777Key214", "http://example.org/qr-code/link"); diff --git a/src/test/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommandTest.java index 0d921d37..17a011ee 100644 --- a/src/test/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/totp/ConfirmTotpCommandTest.java @@ -1,12 +1,15 @@ package fr.xephi.authme.command.executable.totp; +import fr.xephi.authme.TestHelper; import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.message.Messages; import fr.xephi.authme.security.totp.GenerateTotpService; import fr.xephi.authme.security.totp.TotpAuthenticator.TotpGenerationResult; import org.bukkit.entity.Player; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; @@ -15,7 +18,10 @@ import org.mockito.junit.MockitoJUnitRunner; import java.util.Collections; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -37,8 +43,15 @@ public class ConfirmTotpCommandTest { @Mock private DataSource dataSource; @Mock + private PlayerCache playerCache; + @Mock private Messages messages; + @BeforeClass + public static void setUpLogger() { + TestHelper.setupLogger(); + } + @Test public void shouldAddTotpCodeToUserAfterSuccessfulConfirmation() { // given @@ -46,10 +59,12 @@ public class ConfirmTotpCommandTest { String playerName = "George"; given(player.getName()).willReturn(playerName); PlayerAuth auth = PlayerAuth.builder().name(playerName).build(); - given(dataSource.getAuth(playerName)).willReturn(auth); - given(generateTotpService.getGeneratedTotpKey(player)).willReturn(new TotpGenerationResult("totp-key", "url-not-relevant")); + given(playerCache.getAuth(playerName)).willReturn(auth); + String generatedTotpKey = "totp-key"; + given(generateTotpService.getGeneratedTotpKey(player)).willReturn(new TotpGenerationResult(generatedTotpKey, "url-not-relevant")); String totpCode = "954321"; given(generateTotpService.isTotpCodeCorrectForGeneratedTotpKey(player, totpCode)).willReturn(true); + given(dataSource.setTotpKey(anyString(), anyString())).willReturn(true); // when command.runCommand(player, Collections.singletonList(totpCode)); @@ -57,8 +72,10 @@ public class ConfirmTotpCommandTest { // then verify(generateTotpService).isTotpCodeCorrectForGeneratedTotpKey(player, totpCode); verify(generateTotpService).removeGenerateTotpKey(player); - verify(dataSource).setTotpKey(playerName, "totp-key"); + verify(dataSource).setTotpKey(playerName, generatedTotpKey); + verify(playerCache).updatePlayer(auth); verify(messages).send(player, MessageKey.TWO_FACTOR_ENABLE_SUCCESS); + assertThat(auth.getTotpKey(), equalTo(generatedTotpKey)); } @Test @@ -68,7 +85,7 @@ public class ConfirmTotpCommandTest { String playerName = "George"; given(player.getName()).willReturn(playerName); PlayerAuth auth = PlayerAuth.builder().name(playerName).build(); - given(dataSource.getAuth(playerName)).willReturn(auth); + given(playerCache.getAuth(playerName)).willReturn(auth); given(generateTotpService.getGeneratedTotpKey(player)).willReturn(new TotpGenerationResult("totp-key", "url-not-relevant")); String totpCode = "754321"; given(generateTotpService.isTotpCodeCorrectForGeneratedTotpKey(player, totpCode)).willReturn(false); @@ -79,8 +96,9 @@ public class ConfirmTotpCommandTest { // then verify(generateTotpService).isTotpCodeCorrectForGeneratedTotpKey(player, totpCode); verify(generateTotpService, never()).removeGenerateTotpKey(any(Player.class)); - verify(dataSource, only()).getAuth(playerName); + verify(playerCache, only()).getAuth(playerName); verify(messages).send(player, MessageKey.TWO_FACTOR_ENABLE_ERROR_WRONG_CODE); + verifyZeroInteractions(dataSource); } @Test @@ -90,7 +108,7 @@ public class ConfirmTotpCommandTest { String playerName = "George"; given(player.getName()).willReturn(playerName); PlayerAuth auth = PlayerAuth.builder().name(playerName).build(); - given(dataSource.getAuth(playerName)).willReturn(auth); + given(playerCache.getAuth(playerName)).willReturn(auth); given(generateTotpService.getGeneratedTotpKey(player)).willReturn(null); // when @@ -98,8 +116,9 @@ public class ConfirmTotpCommandTest { // then verify(generateTotpService, only()).getGeneratedTotpKey(player); - verify(dataSource, only()).getAuth(playerName); + verify(playerCache, only()).getAuth(playerName); verify(messages).send(player, MessageKey.TWO_FACTOR_ENABLE_ERROR_NO_CODE); + verifyZeroInteractions(dataSource); } @Test @@ -109,14 +128,14 @@ public class ConfirmTotpCommandTest { String playerName = "George"; given(player.getName()).willReturn(playerName); PlayerAuth auth = PlayerAuth.builder().name(playerName).totpKey("A987234").build(); - given(dataSource.getAuth(playerName)).willReturn(auth); + given(playerCache.getAuth(playerName)).willReturn(auth); // when command.runCommand(player, Collections.singletonList("871634")); // then - verify(dataSource, only()).getAuth(playerName); - verifyZeroInteractions(generateTotpService); + verify(playerCache, only()).getAuth(playerName); + verifyZeroInteractions(generateTotpService, dataSource); verify(messages).send(player, MessageKey.TWO_FACTOR_ALREADY_ENABLED); } @@ -126,14 +145,14 @@ public class ConfirmTotpCommandTest { Player player = mock(Player.class); String playerName = "George"; given(player.getName()).willReturn(playerName); - given(dataSource.getAuth(playerName)).willReturn(null); + given(playerCache.getAuth(playerName)).willReturn(null); // when command.runCommand(player, Collections.singletonList("984685")); // then - verify(dataSource, only()).getAuth(playerName); - verifyZeroInteractions(generateTotpService); - verify(messages).send(player, MessageKey.REGISTER_MESSAGE); + verify(playerCache, only()).getAuth(playerName); + verifyZeroInteractions(generateTotpService, dataSource); + verify(messages).send(player, MessageKey.NOT_LOGGED_IN); } } diff --git a/src/test/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommandTest.java new file mode 100644 index 00000000..18903972 --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/totp/RemoveTotpCommandTest.java @@ -0,0 +1,149 @@ +package fr.xephi.authme.command.executable.totp; + +import fr.xephi.authme.TestHelper; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.data.auth.PlayerCache; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.message.MessageKey; +import fr.xephi.authme.message.Messages; +import fr.xephi.authme.security.totp.TotpAuthenticator; +import org.bukkit.entity.Player; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static java.util.Collections.singletonList; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.only; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Test for {@link RemoveTotpCommand}. + */ +@RunWith(MockitoJUnitRunner.class) +public class RemoveTotpCommandTest { + + @InjectMocks + private RemoveTotpCommand command; + + @Mock + private DataSource dataSource; + @Mock + private PlayerCache playerCache; + @Mock + private TotpAuthenticator totpAuthenticator; + @Mock + private Messages messages; + + @BeforeClass + public static void setUpLogger() { + TestHelper.setupLogger(); + } + + @Test + public void shouldRemoveTotpKey() { + // given + String name = "aws"; + PlayerAuth auth = PlayerAuth.builder().name(name).totpKey("some-totp-key").build(); + given(playerCache.getAuth(name)).willReturn(auth); + String inputCode = "93847"; + given(totpAuthenticator.checkCode(auth, inputCode)).willReturn(true); + given(dataSource.removeTotpKey(name)).willReturn(true); + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + + // when + command.runCommand(player, singletonList(inputCode)); + + // then + verify(dataSource).removeTotpKey(name); + verify(messages, only()).send(player, MessageKey.TWO_FACTOR_REMOVED_SUCCESS); + verify(playerCache).updatePlayer(auth); + assertThat(auth.getTotpKey(), nullValue()); + } + + @Test + public void shouldHandleDatabaseError() { + // given + String name = "aws"; + PlayerAuth auth = PlayerAuth.builder().name(name).totpKey("some-totp-key").build(); + given(playerCache.getAuth(name)).willReturn(auth); + String inputCode = "93847"; + given(totpAuthenticator.checkCode(auth, inputCode)).willReturn(true); + given(dataSource.removeTotpKey(name)).willReturn(false); + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + + // when + command.runCommand(player, singletonList(inputCode)); + + // then + verify(dataSource).removeTotpKey(name); + verify(messages, only()).send(player, MessageKey.ERROR); + verify(playerCache, only()).getAuth(name); + } + + @Test + public void shouldHandleInvalidCode() { + // given + String name = "cesar"; + PlayerAuth auth = PlayerAuth.builder().name(name).totpKey("some-totp-key").build(); + given(playerCache.getAuth(name)).willReturn(auth); + String inputCode = "93847"; + given(totpAuthenticator.checkCode(auth, inputCode)).willReturn(false); + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + + // when + command.runCommand(player, singletonList(inputCode)); + + // then + verifyZeroInteractions(dataSource); + verify(messages, only()).send(player, MessageKey.TWO_FACTOR_INVALID_CODE); + verify(playerCache, only()).getAuth(name); + } + + @Test + public void shouldHandleUserWithoutTotpKey() { + // given + String name = "cesar"; + PlayerAuth auth = PlayerAuth.builder().name(name).build(); + given(playerCache.getAuth(name)).willReturn(auth); + String inputCode = "654684"; + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + + // when + command.runCommand(player, singletonList(inputCode)); + + // then + verifyZeroInteractions(dataSource, totpAuthenticator); + verify(messages, only()).send(player, MessageKey.TWO_FACTOR_NOT_ENABLED_ERROR); + verify(playerCache, only()).getAuth(name); + } + + @Test + public void shouldHandleNonLoggedInUser() { + // given + String name = "cesar"; + given(playerCache.getAuth(name)).willReturn(null); + String inputCode = "654684"; + Player player = mock(Player.class); + given(player.getName()).willReturn(name); + + // when + command.runCommand(player, singletonList(inputCode)); + + // then + verifyZeroInteractions(dataSource, totpAuthenticator); + verify(messages, only()).send(player, MessageKey.NOT_LOGGED_IN); + verify(playerCache, only()).getAuth(name); + } +} From c96e28f726ef36453849c42524e0323acec6716b Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 13 May 2018 22:52:41 +0200 Subject: [PATCH 41/63] Add debug logging for teleports (relates to #1521) --- .../fr/xephi/authme/service/TeleportationService.java | 11 +++++++++-- .../java/fr/xephi/authme/settings/SpawnLoader.java | 2 ++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/java/fr/xephi/authme/service/TeleportationService.java b/src/main/java/fr/xephi/authme/service/TeleportationService.java index 10f9e117..1588c440 100644 --- a/src/main/java/fr/xephi/authme/service/TeleportationService.java +++ b/src/main/java/fr/xephi/authme/service/TeleportationService.java @@ -1,5 +1,6 @@ package fr.xephi.authme.service; +import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.data.limbo.LimboPlayer; @@ -63,12 +64,13 @@ public class TeleportationService implements Reloadable { public void teleportOnJoin(final Player player) { if (!settings.getProperty(RestrictionSettings.NO_TELEPORT) && settings.getProperty(TELEPORT_UNAUTHED_TO_SPAWN)) { + ConsoleLogger.debug("Teleport on join for player `{0}`", player.getName()); teleportToSpawn(player, playerCache.isAuthenticated(player.getName())); } } /** - * Returns the player's custom on join location + * Returns the player's custom on join location. * * @param player the player to process * @@ -82,10 +84,11 @@ public class TeleportationService implements Reloadable { SpawnTeleportEvent event = new SpawnTeleportEvent(player, location, playerCache.isAuthenticated(player.getName())); bukkitService.callEvent(event); - if(!isEventValid(event)) { + if (!isEventValid(event)) { return null; } + ConsoleLogger.debug("Returning custom location for >1.9 join event for player `{0}`", player.getName()); return location; } return null; @@ -107,6 +110,7 @@ public class TeleportationService implements Reloadable { } if (!player.hasPlayedBefore() || !dataSource.isAuthAvailable(player.getName())) { + ConsoleLogger.debug("Attempting to teleport player `{0}` to first spawn", player.getName()); performTeleportation(player, new FirstSpawnTeleportEvent(player, firstSpawn)); } } @@ -130,12 +134,15 @@ public class TeleportationService implements Reloadable { // The world in LimboPlayer is from where the player comes, before any teleportation by AuthMe if (mustForceSpawnAfterLogin(worldName)) { + ConsoleLogger.debug("Teleporting `{0}` to spawn because of 'force-spawn after login'", player.getName()); teleportToSpawn(player, true); } else if (settings.getProperty(TELEPORT_UNAUTHED_TO_SPAWN)) { if (settings.getProperty(RestrictionSettings.SAVE_QUIT_LOCATION) && auth.getQuitLocY() != 0) { Location location = buildLocationFromAuth(player, auth); + ConsoleLogger.debug("Teleporting `{0}` after login, based on the player auth", player.getName()); teleportBackFromSpawn(player, location); } else if (limbo != null && limbo.getLocation() != null) { + ConsoleLogger.debug("Teleporting `{0}` after login, based on the limbo player", player.getName()); teleportBackFromSpawn(player, limbo.getLocation()); } } diff --git a/src/main/java/fr/xephi/authme/settings/SpawnLoader.java b/src/main/java/fr/xephi/authme/settings/SpawnLoader.java index ea235b3c..d2f2edbf 100644 --- a/src/main/java/fr/xephi/authme/settings/SpawnLoader.java +++ b/src/main/java/fr/xephi/authme/settings/SpawnLoader.java @@ -198,9 +198,11 @@ public class SpawnLoader implements Reloadable { // ignore } if (spawnLoc != null) { + ConsoleLogger.debug("Spawn location determined as `{0}` for world `{1}`", spawnLoc, world.getName()); return spawnLoc; } } + ConsoleLogger.debug("Fall back to default world spawn location. World: `{0}`", world.getName()); return world.getSpawnLocation(); // return default location } From 54feb1097e170de2164d95413832680c7dcaa4bc Mon Sep 17 00:00:00 2001 From: rafael59r2 Date: Sun, 13 May 2018 21:56:25 +0100 Subject: [PATCH 42/63] Update messages_pt.yml (#1570) --- src/main/resources/messages/messages_pt.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/resources/messages/messages_pt.yml b/src/main/resources/messages/messages_pt.yml index d6e26505..5d5b0ce9 100644 --- a/src/main/resources/messages/messages_pt.yml +++ b/src/main/resources/messages/messages_pt.yml @@ -145,12 +145,12 @@ time: # Two-factor authentication two_factor: code_created: '&2O seu código secreto é o %code. Você pode verificá-lo a partir daqui %url' - # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' - # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' - # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' - # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' - # TODO enable_success: 'Successfully enabled two-factor authentication for your account' - # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' - # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' - # TODO removed_success: 'Successfully removed two-factor auth from your account' - # TODO invalid_code: 'Invalid code!' + confirmation_required: 'Por favor confirme seu codigo de 2 etapas com /2fa confirm ' + code_required: 'Por favor submita seu codigo de duas etapas com /2fa code ' + already_enabled: 'Autenticação de duas etapas já se encontra habilitada na sua conta!' + enable_error_no_code: 'Nenhuma chave 2fa foi gerada por você ou expirou. Por favor digite /2fa add' + enable_success: 'Autenticação de duas etapas habilitada com sucesso na sua conta' + enable_error_wrong_code: 'Codigo errado ou o codigo expirou. Por favor digite /2fa add' + not_enabled_error: 'Autenticação de duas etapas não está habilitada na sua conta. Digite /2fa add' + removed_success: 'Autenticação de duas etapas removida com sucesso da sua conta' + invalid_code: 'Codigo invalido!' From 3c0caf2ac3f57bd8f0e7643d5884da6d7ecbb82e Mon Sep 17 00:00:00 2001 From: Maxetto Date: Mon, 14 May 2018 00:53:46 +0200 Subject: [PATCH 43/63] [Messages_IT] Add 2FA messages --- src/main/resources/messages/messages_it.yml | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/resources/messages/messages_it.yml b/src/main/resources/messages/messages_it.yml index d46a244f..9e9721b9 100644 --- a/src/main/resources/messages/messages_it.yml +++ b/src/main/resources/messages/messages_it.yml @@ -7,7 +7,7 @@ # Registration registration: disabled: '&cLa registrazione tramite i comandi di gioco è disabilitata.' - name_taken: '&cHai già eseguito la registrazione, non puoi eseguirla nuovamente.' + name_taken: '&cHai già eseguito la registrazione!' register_request: '&3Per favore, esegui la registrazione con il comando: /register ' command_usage: '&cUtilizzo: /register ' reg_only: '&4Puoi giocare in questo server solo dopo aver eseguito la registrazione attraverso il sito web! Per favore, vai su http://esempio.it per procedere!' @@ -39,7 +39,7 @@ error: no_permission: '&4Non hai il permesso di eseguire questa operazione.' unexpected_error: '&4Qualcosa è andato storto, riporta questo errore ad un amministratore!' max_registration: '&cHai raggiunto il numero massimo di registrazioni (%reg_count/%max_acc %reg_names) per questo indirizzo IP!' - logged_in: '&cHai già eseguito l''autenticazione, non è necessario eseguirla nuovamente!' + logged_in: '&cHai già eseguito l''autenticazione!' kick_for_vip: '&3Un utente VIP è entrato mentre il server era pieno e ha preso il tuo posto!' tempban_max_logins: '&cSei stato temporaneamente bandito per aver fallito l''autenticazione troppe volte.' @@ -146,12 +146,12 @@ time: # Two-factor authentication two_factor: code_created: '&2Il tuo codice segreto è: &f%code%%nl%&2Puoi anche scannerizzare il codice QR da qui: &f%url' - # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' - # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' - # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' - # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' - # TODO enable_success: 'Successfully enabled two-factor authentication for your account' - # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' - # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' - # TODO removed_success: 'Successfully removed two-factor auth from your account' - # TODO invalid_code: 'Invalid code!' + confirmation_required: 'Per favore conferma il tuo codice con: /2fa confirm ' + code_required: 'Per favore inserisci il tuo codice per l''autenticazione a 2 fattori con: /2fa code ' + already_enabled: 'Hai già abilitato l''autenticazione a 2 fattori!' + enable_error_no_code: 'Non hai ancora generato un codice per l''autenticazione a 2 fattori oppure il tuo codice è scaduto. Per favore scrivi: /2fa add' + enable_success: 'Autenticazione a 2 fattori abilitata correttamente' + enable_error_wrong_code: 'Hai inserito un codice sbagliato o scaduto. Per favore scrivi: /2fa add' + not_enabled_error: 'L''autenticazione a 2 fattori non è ancora abilitata per il tuo account. Scrivi: /2fa add' + removed_success: 'Autenticazione a 2 fattori rimossa correttamente' + invalid_code: 'Il codice inserito non è valido, riprova!' From 80dce1a92f620ac4586200bd17f83505a6bed2c8 Mon Sep 17 00:00:00 2001 From: RikoDEV Date: Mon, 14 May 2018 21:08:20 +0200 Subject: [PATCH 44/63] Update messages_pl.yml (#1571) * Update messages_pl.yml --- src/main/resources/messages/messages_pl.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/resources/messages/messages_pl.yml b/src/main/resources/messages/messages_pl.yml index b923533b..78f63a83 100644 --- a/src/main/resources/messages/messages_pl.yml +++ b/src/main/resources/messages/messages_pl.yml @@ -145,12 +145,12 @@ time: # Two-factor authentication two_factor: code_created: '&2Twój sekretny kod to %code. Możesz zeskanować go tutaj: %url' - # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' - # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' - # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' - # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' - # TODO enable_success: 'Successfully enabled two-factor authentication for your account' - # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' - # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' - # TODO removed_success: 'Successfully removed two-factor auth from your account' - # TODO invalid_code: 'Invalid code!' + confirmation_required: 'Musisz potwierdzić swój kod komendą /2fa confirm ' + code_required: 'Wpisz swój kod weryfikacji dwuetapowej przy pomocy komendy /2fa code ' + already_enabled: '&aWeryfikacja dwuetapowa jest już włączona dla Twojego konta.' + enable_error_no_code: '&cKod weryfikacji dwuetapowej nie został dla Ciebie wygenerowany lub wygasł. Wpisz komende /2fa add' + enable_success: '&aWeryfikacja dwuetapowa została włączona dla Twojego konta.' + enable_error_wrong_code: '&cWpisany kod jest nieprawidłowy lub wygasły. Wpisz ponownie /2fa add' + not_enabled_error: 'Weryfikacja dwuetapowa nie jest włączona dla twojego konta. Wpisz komende /2fa add' + removed_success: '&aPomyślnie usunięto weryfikacje dwuetapową z Twojego konta.' + invalid_code: '&cWpisany kod jest nieprawidłowy, spróbuj jeszcze raz.' From 8e4288f911629e9ae7abeefd65b06fdf1700ac52 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 20 May 2018 13:10:26 +0200 Subject: [PATCH 45/63] Minor code householding --- .../executable/authme/PurgeCommand.java | 7 +++---- .../authme/data/limbo/LimboServiceHelper.java | 4 ---- .../xephi/authme/security/crypts/Pbkdf2.java | 10 +++++----- .../authme/security/crypts/Pbkdf2Django.java | 10 +++++----- .../security/totp/TotpAuthenticator.java | 10 +++------- .../authme/listener/OnJoinVerifierTest.java | 13 +++---------- .../process/login/AsynchronousLoginTest.java | 9 ++++----- .../authme/service/AntiBotServiceTest.java | 4 ++-- .../service/BukkitServiceTestHelper.java | 18 ++++++++++++++++++ .../WelcomeMessageConfigurationTest.java | 9 +++++---- 10 files changed, 48 insertions(+), 46 deletions(-) diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java index 58b235ed..1538061e 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/PurgeCommand.java @@ -1,5 +1,6 @@ package fr.xephi.authme.command.executable.authme; +import com.google.common.primitives.Ints; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.task.purge.PurgeService; import org.bukkit.ChatColor; @@ -26,10 +27,8 @@ public class PurgeCommand implements ExecutableCommand { String daysStr = arguments.get(0); // Convert the days string to an integer value, and make sure it's valid - int days; - try { - days = Integer.parseInt(daysStr); - } catch (NumberFormatException ex) { + Integer days = Ints.tryParse(daysStr); + if (days == null) { sender.sendMessage(ChatColor.RED + "The value you've entered is invalid!"); return; } diff --git a/src/main/java/fr/xephi/authme/data/limbo/LimboServiceHelper.java b/src/main/java/fr/xephi/authme/data/limbo/LimboServiceHelper.java index b13a260d..4d63a2d9 100644 --- a/src/main/java/fr/xephi/authme/data/limbo/LimboServiceHelper.java +++ b/src/main/java/fr/xephi/authme/data/limbo/LimboServiceHelper.java @@ -3,7 +3,6 @@ package fr.xephi.authme.data.limbo; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.settings.Settings; -import fr.xephi.authme.settings.SpawnLoader; import fr.xephi.authme.settings.properties.LimboSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import org.bukkit.Location; @@ -20,9 +19,6 @@ import static fr.xephi.authme.util.Utils.isCollectionEmpty; */ class LimboServiceHelper { - @Inject - private SpawnLoader spawnLoader; - @Inject private PermissionsManager permissionsManager; diff --git a/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2.java b/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2.java index 5367a2a1..d9695abc 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2.java @@ -1,5 +1,6 @@ package fr.xephi.authme.security.crypts; +import com.google.common.primitives.Ints; import de.rtner.misc.BinTools; import de.rtner.security.auth.spi.PBKDF2Engine; import de.rtner.security.auth.spi.PBKDF2Parameters; @@ -38,13 +39,12 @@ public class Pbkdf2 extends HexSaltedMethod { if (line.length != 4) { return false; } - int iterations; - try { - iterations = Integer.parseInt(line[1]); - } catch (NumberFormatException e) { - ConsoleLogger.logException("Cannot read number of rounds for Pbkdf2", e); + Integer iterations = Ints.tryParse(line[1]); + if (iterations == null) { + ConsoleLogger.warning("Cannot read number of rounds for Pbkdf2: '" + line[1] + "'"); return false; } + String salt = line[2]; byte[] derivedKey = BinTools.hex2bin(line[3]); PBKDF2Parameters params = new PBKDF2Parameters("HmacSHA256", "UTF-8", salt.getBytes(), iterations, derivedKey); diff --git a/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2Django.java b/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2Django.java index f5a0abb6..e32930db 100644 --- a/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2Django.java +++ b/src/main/java/fr/xephi/authme/security/crypts/Pbkdf2Django.java @@ -1,5 +1,6 @@ package fr.xephi.authme.security.crypts; +import com.google.common.primitives.Ints; import de.rtner.security.auth.spi.PBKDF2Engine; import de.rtner.security.auth.spi.PBKDF2Parameters; import fr.xephi.authme.ConsoleLogger; @@ -27,13 +28,12 @@ public class Pbkdf2Django extends HexSaltedMethod { if (line.length != 4) { return false; } - int iterations; - try { - iterations = Integer.parseInt(line[1]); - } catch (NumberFormatException e) { - ConsoleLogger.logException("Could not read number of rounds for Pbkdf2Django:", e); + Integer iterations = Ints.tryParse(line[1]); + if (iterations == null) { + ConsoleLogger.warning("Cannot read number of rounds for Pbkdf2Django: '" + line[1] + "'"); return false; } + String salt = line[2]; byte[] derivedKey = Base64.getDecoder().decode(line[3]); PBKDF2Parameters params = new PBKDF2Parameters("HmacSHA256", "ASCII", salt.getBytes(), iterations, derivedKey); diff --git a/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java b/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java index eb922cf4..4905a521 100644 --- a/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java +++ b/src/main/java/fr/xephi/authme/security/totp/TotpAuthenticator.java @@ -1,5 +1,6 @@ package fr.xephi.authme.security.totp; +import com.google.common.primitives.Ints; import com.warrenstrange.googleauth.GoogleAuthenticator; import com.warrenstrange.googleauth.GoogleAuthenticatorKey; import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator; @@ -43,13 +44,8 @@ public class TotpAuthenticator { * @return true if code is valid, false otherwise */ public boolean checkCode(String totpKey, String inputCode) { - try { - Integer totpCode = Integer.valueOf(inputCode); - return authenticator.authorize(totpKey, totpCode); - } catch (NumberFormatException e) { - // ignore - } - return false; + Integer totpCode = Ints.tryParse(inputCode); + return totpCode != null && authenticator.authorize(totpKey, totpCode); } public TotpGenerationResult generateTotpKey(Player player) { diff --git a/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java b/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java index 2f6121a9..114d4210 100644 --- a/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java +++ b/src/test/java/fr/xephi/authme/listener/OnJoinVerifierTest.java @@ -30,10 +30,10 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.List; +import static fr.xephi.authme.service.BukkitServiceTestHelper.returnGivenOnlinePlayers; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; @@ -123,7 +123,7 @@ public class OnJoinVerifierTest { List onlinePlayers = Arrays.asList(mock(Player.class), mock(Player.class)); given(permissionsManager.hasPermission(onlinePlayers.get(0), PlayerStatePermission.IS_VIP)).willReturn(true); given(permissionsManager.hasPermission(onlinePlayers.get(1), PlayerStatePermission.IS_VIP)).willReturn(false); - returnOnlineListFromBukkitServer(onlinePlayers); + returnGivenOnlinePlayers(bukkitService, onlinePlayers); given(server.getMaxPlayers()).willReturn(onlinePlayers.size()); given(messages.retrieveSingle(player, MessageKey.KICK_FOR_VIP)).willReturn("kick for vip"); @@ -147,7 +147,7 @@ public class OnJoinVerifierTest { given(permissionsManager.hasPermission(player, PlayerStatePermission.IS_VIP)).willReturn(true); List onlinePlayers = Collections.singletonList(mock(Player.class)); given(permissionsManager.hasPermission(onlinePlayers.get(0), PlayerStatePermission.IS_VIP)).willReturn(true); - returnOnlineListFromBukkitServer(onlinePlayers); + returnGivenOnlinePlayers(bukkitService, onlinePlayers); given(server.getMaxPlayers()).willReturn(onlinePlayers.size()); given(messages.retrieveSingle(player, MessageKey.KICK_FULL_SERVER)).willReturn("kick full server"); @@ -501,13 +501,6 @@ public class OnJoinVerifierTest { onJoinVerifier.checkPlayerCountry(joiningPlayer, ip, false); } - @SuppressWarnings({ "unchecked", "rawtypes" }) - private void returnOnlineListFromBukkitServer(Collection onlineList) { - // Note ljacqu 20160529: The compiler gets lost in generics because Collection is returned - // from getOnlinePlayers(). We need to uncheck onlineList to a simple Collection or it will refuse to compile. - given(bukkitService.getOnlinePlayers()).willReturn((Collection) onlineList); - } - private void expectValidationExceptionWith(MessageKey messageKey, String... args) { expectedException.expect(exceptionWithData(messageKey, args)); } diff --git a/src/test/java/fr/xephi/authme/process/login/AsynchronousLoginTest.java b/src/test/java/fr/xephi/authme/process/login/AsynchronousLoginTest.java index 9b57fb31..f1981017 100644 --- a/src/test/java/fr/xephi/authme/process/login/AsynchronousLoginTest.java +++ b/src/test/java/fr/xephi/authme/process/login/AsynchronousLoginTest.java @@ -21,13 +21,13 @@ import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; -import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.MockitoJUnitRunner; import org.mockito.stubbing.Answer; import java.util.Arrays; -import java.util.Collection; +import java.util.List; +import static fr.xephi.authme.service.BukkitServiceTestHelper.returnGivenOnlinePlayers; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -241,7 +241,6 @@ public class AsynchronousLoginTest { return player; } - @SuppressWarnings({ "unchecked", "rawtypes" }) private void mockOnlinePlayersInBukkitService() { // 127.0.0.4: albania (online), brazil (offline) Player playerA = mockPlayer("albania"); @@ -266,8 +265,8 @@ public class AsynchronousLoginTest { Player playerF = mockPlayer("france"); TestHelper.mockPlayerIp(playerF, "192.168.0.0"); - Collection onlinePlayers = Arrays.asList(playerA, playerB, playerC, playerD, playerE, playerF); - given(bukkitService.getOnlinePlayers()).willReturn(onlinePlayers); + List onlinePlayers = Arrays.asList(playerA, playerB, playerC, playerD, playerE, playerF); + returnGivenOnlinePlayers(bukkitService, onlinePlayers); } } diff --git a/src/test/java/fr/xephi/authme/service/AntiBotServiceTest.java b/src/test/java/fr/xephi/authme/service/AntiBotServiceTest.java index 62dda8e5..f992e255 100644 --- a/src/test/java/fr/xephi/authme/service/AntiBotServiceTest.java +++ b/src/test/java/fr/xephi/authme/service/AntiBotServiceTest.java @@ -19,6 +19,7 @@ import org.mockito.Mock; import java.util.Arrays; import java.util.List; +import static fr.xephi.authme.service.BukkitServiceTestHelper.returnGivenOnlinePlayers; import static fr.xephi.authme.service.BukkitServiceTestHelper.setBukkitServiceToScheduleSyncDelayedTaskWithDelay; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; @@ -154,11 +155,10 @@ public class AntiBotServiceTest { } @Test - @SuppressWarnings({"unchecked", "rawtypes"}) public void shouldInformPlayersOnActivation() { // given - listening antibot List players = Arrays.asList(mock(Player.class), mock(Player.class)); - given(bukkitService.getOnlinePlayers()).willReturn((List) players); + returnGivenOnlinePlayers(bukkitService, players); given(permissionsManager.hasPermission(players.get(0), AdminPermission.ANTIBOT_MESSAGES)).willReturn(false); given(permissionsManager.hasPermission(players.get(1), AdminPermission.ANTIBOT_MESSAGES)).willReturn(true); diff --git a/src/test/java/fr/xephi/authme/service/BukkitServiceTestHelper.java b/src/test/java/fr/xephi/authme/service/BukkitServiceTestHelper.java index 9807e4f5..7d57869d 100644 --- a/src/test/java/fr/xephi/authme/service/BukkitServiceTestHelper.java +++ b/src/test/java/fr/xephi/authme/service/BukkitServiceTestHelper.java @@ -1,7 +1,12 @@ package fr.xephi.authme.service; +import org.bukkit.entity.Player; + +import java.util.Collection; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.doAnswer; /** @@ -81,4 +86,17 @@ public final class BukkitServiceTestHelper { return null; }).when(bukkitService).scheduleSyncDelayedTask(any(Runnable.class), anyLong()); } + + /** + * Sets a BukkitService mock to return the given players when its method + * {@link BukkitService#getOnlinePlayers()} is invoked. + * + * @param bukkitService the mock to set behavior on + * @param players the players to return + */ + @SuppressWarnings("unchecked") + public static void returnGivenOnlinePlayers(BukkitService bukkitService, Collection players) { + // The compiler gets lost in generics because Collection is returned from getOnlinePlayers() + given(bukkitService.getOnlinePlayers()).willReturn((Collection) players); + } } diff --git a/src/test/java/fr/xephi/authme/settings/WelcomeMessageConfigurationTest.java b/src/test/java/fr/xephi/authme/settings/WelcomeMessageConfigurationTest.java index c650dd6f..2109edbd 100644 --- a/src/test/java/fr/xephi/authme/settings/WelcomeMessageConfigurationTest.java +++ b/src/test/java/fr/xephi/authme/settings/WelcomeMessageConfigurationTest.java @@ -24,6 +24,7 @@ import java.nio.file.Files; import java.util.Arrays; import java.util.List; +import static fr.xephi.authme.service.BukkitServiceTestHelper.returnGivenOnlinePlayers; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; @@ -68,7 +69,7 @@ public class WelcomeMessageConfigurationTest { } @Test - public void shouldLoadWelcomeMessage() throws IOException { + public void shouldLoadWelcomeMessage() { // given String welcomeMessage = "This is my welcome message for testing\nBye!"; setWelcomeMessageAndReload(welcomeMessage); @@ -84,7 +85,7 @@ public class WelcomeMessageConfigurationTest { } @Test - public void shouldReplaceNameAndIpAndCountry() throws IOException { + public void shouldReplaceNameAndIpAndCountry() { // given String welcomeMessage = "Hello {PLAYER}, your IP is {IP}\nYour country is {COUNTRY}.\nWelcome to {SERVER}!"; setWelcomeMessageAndReload(welcomeMessage); @@ -108,11 +109,11 @@ public class WelcomeMessageConfigurationTest { } @Test - public void shouldApplyOtherReplacements() throws IOException { + public void shouldApplyOtherReplacements() { // given String welcomeMessage = "{ONLINE}/{MAXPLAYERS} online\n{LOGINS} logged in\nYour world is {WORLD}\nServer: {VERSION}"; setWelcomeMessageAndReload(welcomeMessage); - given(bukkitService.getOnlinePlayers()).willReturn((List) Arrays.asList(mock(Player.class), mock(Player.class))); + returnGivenOnlinePlayers(bukkitService, Arrays.asList(mock(Player.class), mock(Player.class))); given(server.getMaxPlayers()).willReturn(20); given(playerCache.getLogged()).willReturn(1); given(server.getBukkitVersion()).willReturn("Bukkit-456.77.8"); From 61420429962cecbaec748888c5a5d236983901ee Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 21 May 2018 08:45:18 +0200 Subject: [PATCH 46/63] #1417 Add permission node to allow chat before login --- docs/permission_nodes.md | 5 ++-- .../xephi/authme/listener/PlayerListener.java | 25 ++++++++++++------ .../permission/PlayerStatePermission.java | 9 +++++-- src/main/resources/plugin.yml | 3 +++ .../authme/listener/PlayerListenerTest.java | 26 +++++++++++++++++++ 5 files changed, 56 insertions(+), 12 deletions(-) diff --git a/docs/permission_nodes.md b/docs/permission_nodes.md index 828d23b5..643a327b 100644 --- a/docs/permission_nodes.md +++ b/docs/permission_nodes.md @@ -1,5 +1,5 @@ - + ## AuthMe Permission Nodes The following are the permission nodes that are currently supported by the latest dev builds. @@ -30,6 +30,7 @@ The following are the permission nodes that are currently supported by the lates - **authme.admin.switchantibot** – Administrator command to toggle the AntiBot protection status. - **authme.admin.unregister** – Administrator command to unregister an existing user. - **authme.admin.updatemessages** – Permission to use the update messages command. +- **authme.allowchatbeforelogin** – Permission to send chat messages before being logged in. - **authme.allowmultipleaccounts** – Permission to be able to register multiple accounts. - **authme.bypassantibot** – Permission node to bypass AntiBot protection. - **authme.bypasscountrycheck** – Permission to bypass the GeoIp country code check. @@ -69,4 +70,4 @@ The following are the permission nodes that are currently supported by the lates --- -This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Sun Apr 22 11:00:13 CEST 2018 +This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Mon May 21 08:43:08 CEST 2018 diff --git a/src/main/java/fr/xephi/authme/listener/PlayerListener.java b/src/main/java/fr/xephi/authme/listener/PlayerListener.java index 5b632712..660119ed 100644 --- a/src/main/java/fr/xephi/authme/listener/PlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/PlayerListener.java @@ -7,6 +7,7 @@ import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.message.Messages; import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.permission.PlayerStatePermission; import fr.xephi.authme.permission.handlers.PermissionLoadUserException; import fr.xephi.authme.process.Management; import fr.xephi.authme.service.AntiBotService; @@ -65,7 +66,7 @@ public class PlayerListener implements Listener { @Inject private Settings settings; @Inject - private Messages m; + private Messages messages; @Inject private DataSource dataSource; @Inject @@ -107,12 +108,12 @@ public class PlayerListener implements Listener { final Player player = event.getPlayer(); if (!quickCommandsProtectionManager.isAllowed(player.getName())) { event.setCancelled(true); - player.kickPlayer(m.retrieveSingle(player, MessageKey.QUICK_COMMAND_PROTECTION_KICK)); + player.kickPlayer(messages.retrieveSingle(player, MessageKey.QUICK_COMMAND_PROTECTION_KICK)); return; } if (listenerService.shouldCancelEvent(player)) { event.setCancelled(true); - m.send(player, MessageKey.DENIED_COMMAND); + messages.send(player, MessageKey.DENIED_COMMAND); } } @@ -123,10 +124,18 @@ public class PlayerListener implements Listener { } final Player player = event.getPlayer(); - if (listenerService.shouldCancelEvent(player)) { + final boolean mayPlayerSendChat = !listenerService.shouldCancelEvent(player) + || permissionsManager.hasPermission(player, PlayerStatePermission.ALLOW_CHAT_BEFORE_LOGIN); + if (mayPlayerSendChat) { + removeUnauthorizedRecipients(event); + } else { event.setCancelled(true); - m.send(player, MessageKey.DENIED_CHAT); - } else if (settings.getProperty(RestrictionSettings.HIDE_CHAT)) { + messages.send(player, MessageKey.DENIED_CHAT); + } + } + + private void removeUnauthorizedRecipients(AsyncPlayerChatEvent event) { + if (settings.getProperty(RestrictionSettings.HIDE_CHAT)) { event.getRecipients().removeIf(listenerService::shouldCancelEvent); if (event.getRecipients().isEmpty()) { event.setCancelled(true); @@ -274,7 +283,7 @@ public class PlayerListener implements Listener { try { runOnJoinChecks(JoiningPlayer.fromName(name), event.getAddress().getHostAddress()); } catch (FailedVerificationException e) { - event.setKickMessage(m.retrieveSingle(name, e.getReason(), e.getArgs())); + event.setKickMessage(messages.retrieveSingle(name, e.getReason(), e.getArgs())); event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER); } } @@ -302,7 +311,7 @@ public class PlayerListener implements Listener { try { runOnJoinChecks(JoiningPlayer.fromPlayerObject(player), event.getAddress().getHostAddress()); } catch (FailedVerificationException e) { - event.setKickMessage(m.retrieveSingle(player, e.getReason(), e.getArgs())); + event.setKickMessage(messages.retrieveSingle(player, e.getReason(), e.getArgs())); event.setResult(PlayerLoginEvent.Result.KICK_OTHER); } } diff --git a/src/main/java/fr/xephi/authme/permission/PlayerStatePermission.java b/src/main/java/fr/xephi/authme/permission/PlayerStatePermission.java index 2a051722..667b55d5 100644 --- a/src/main/java/fr/xephi/authme/permission/PlayerStatePermission.java +++ b/src/main/java/fr/xephi/authme/permission/PlayerStatePermission.java @@ -34,7 +34,12 @@ public enum PlayerStatePermission implements PermissionNode { /** * Permission to bypass the GeoIp country code check. */ - BYPASS_COUNTRY_CHECK("authme.bypasscountrycheck", DefaultPermission.NOT_ALLOWED); + BYPASS_COUNTRY_CHECK("authme.bypasscountrycheck", DefaultPermission.NOT_ALLOWED), + + /** + * Permission to send chat messages before being logged in. + */ + ALLOW_CHAT_BEFORE_LOGIN("authme.allowchatbeforelogin", DefaultPermission.NOT_ALLOWED); /** * The permission node. @@ -42,7 +47,7 @@ public enum PlayerStatePermission implements PermissionNode { private String node; /** - * The default permission level + * The default permission level. */ private DefaultPermission defaultPermission; diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 1be18288..90e31c92 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -161,6 +161,9 @@ permissions: authme.admin.updatemessages: description: Permission to use the update messages command. default: op + authme.allowchatbeforelogin: + description: Permission to send chat messages before being logged in. + default: false authme.allowmultipleaccounts: description: Permission to be able to register multiple accounts. default: op diff --git a/src/test/java/fr/xephi/authme/listener/PlayerListenerTest.java b/src/test/java/fr/xephi/authme/listener/PlayerListenerTest.java index 42d14e46..59d837a9 100644 --- a/src/test/java/fr/xephi/authme/listener/PlayerListenerTest.java +++ b/src/test/java/fr/xephi/authme/listener/PlayerListenerTest.java @@ -6,6 +6,8 @@ import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.message.MessageKey; import fr.xephi.authme.message.Messages; +import fr.xephi.authme.permission.PermissionsManager; +import fr.xephi.authme.permission.PlayerStatePermission; import fr.xephi.authme.process.Management; import fr.xephi.authme.service.AntiBotService; import fr.xephi.authme.service.BukkitService; @@ -60,6 +62,7 @@ import static fr.xephi.authme.service.BukkitServiceTestHelper.setBukkitServiceTo import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -113,6 +116,8 @@ public class PlayerListenerTest { private JoinMessageService joinMessageService; @Mock private QuickCommandsProtectionManager quickCommandsProtectionManager; + @Mock + private PermissionsManager permissionsManager; /** * #831: If a player is kicked because of "logged in from another location", the kick @@ -289,12 +294,14 @@ public class PlayerListenerTest { given(settings.getProperty(RestrictionSettings.ALLOW_CHAT)).willReturn(false); AsyncPlayerChatEvent event = newAsyncChatEvent(); given(listenerService.shouldCancelEvent(event.getPlayer())).willReturn(true); + given(permissionsManager.hasPermission(event.getPlayer(), PlayerStatePermission.ALLOW_CHAT_BEFORE_LOGIN)).willReturn(false); // when listener.onPlayerChat(event); // then verify(listenerService).shouldCancelEvent(event.getPlayer()); + verify(permissionsManager).hasPermission(event.getPlayer(), PlayerStatePermission.ALLOW_CHAT_BEFORE_LOGIN); verify(event).setCancelled(true); verify(messages).send(event.getPlayer(), MessageKey.DENIED_CHAT); } @@ -356,6 +363,25 @@ public class PlayerListenerTest { assertThat(event.getRecipients(), empty()); } + @Test + public void shouldAllowChatForBypassPermission() { + // given + given(settings.getProperty(RestrictionSettings.ALLOW_CHAT)).willReturn(false); + AsyncPlayerChatEvent event = newAsyncChatEvent(); + given(listenerService.shouldCancelEvent(event.getPlayer())).willReturn(true); + given(permissionsManager.hasPermission(event.getPlayer(), PlayerStatePermission.ALLOW_CHAT_BEFORE_LOGIN)).willReturn(true); + given(settings.getProperty(RestrictionSettings.HIDE_CHAT)).willReturn(false); + + // when + listener.onPlayerChat(event); + + // then + assertThat(event.isCancelled(), equalTo(false)); + verify(listenerService).shouldCancelEvent(event.getPlayer()); + verify(permissionsManager).hasPermission(event.getPlayer(), PlayerStatePermission.ALLOW_CHAT_BEFORE_LOGIN); + assertThat(event.getRecipients(), hasSize(3)); + } + @Test public void shouldAllowUnlimitedMovement() { // given From c4b02d74b768e77fa9470a8cda501f7469b52f45 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 21 May 2018 09:01:00 +0200 Subject: [PATCH 47/63] Fix generic type in PlayerAuth matcher --- src/test/java/fr/xephi/authme/AuthMeMatchers.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/fr/xephi/authme/AuthMeMatchers.java b/src/test/java/fr/xephi/authme/AuthMeMatchers.java index 33768883..653fb04c 100644 --- a/src/test/java/fr/xephi/authme/AuthMeMatchers.java +++ b/src/test/java/fr/xephi/authme/AuthMeMatchers.java @@ -44,8 +44,8 @@ public final class AuthMeMatchers { }; } - public static Matcher hasAuthBasicData(String name, String realName, - String email, String lastIp) { + public static Matcher hasAuthBasicData(String name, String realName, + String email, String lastIp) { return new TypeSafeMatcher() { @Override public boolean matchesSafely(PlayerAuth item) { From 68b896cfc3e8c5df7284776baee52855750a0e59 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 21 May 2018 09:10:27 +0200 Subject: [PATCH 48/63] Move salt column configuration to the other column configs --- docs/config.md | 8 ++++---- .../authme/settings/SettingsMigrationService.java | 14 ++++++++++++++ .../settings/properties/DatabaseSettings.java | 2 +- .../settings/SettingsMigrationServiceTest.java | 2 ++ .../fr/xephi/authme/settings/config-old.yml | 2 +- 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/docs/config.md b/docs/config.md index 51ee86a9..9444bb83 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1,5 +1,5 @@ - + ## AuthMe Configuration The first time you run AuthMe it will create a config.yml file in the plugins/AuthMe folder, @@ -37,6 +37,8 @@ DataSource: mySQLRealName: 'realname' # Column for storing players passwords mySQLColumnPassword: 'password' + # Column for storing players passwords salts + mySQLColumnSalt: '' # Column for storing players emails mySQLColumnEmail: 'email' # Column for storing if a player is logged in or not @@ -71,8 +73,6 @@ DataSource: # You should set this at least 30 seconds less than mysql server wait_timeout maxLifetime: 1800 ExternalBoardOptions: - # Column for storing players passwords salts - mySQLColumnSalt: '' # Column for storing players groups mySQLColumnGroup: '' # -1 means disabled. If you want that only activated players @@ -562,4 +562,4 @@ To change settings on a running server, save your changes to config.yml and use --- -This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Sun Apr 22 11:00:10 CEST 2018 +This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Mon May 21 09:08:25 CEST 2018 diff --git a/src/main/java/fr/xephi/authme/settings/SettingsMigrationService.java b/src/main/java/fr/xephi/authme/settings/SettingsMigrationService.java index c5ab3fd7..d95f73ce 100644 --- a/src/main/java/fr/xephi/authme/settings/SettingsMigrationService.java +++ b/src/main/java/fr/xephi/authme/settings/SettingsMigrationService.java @@ -10,6 +10,7 @@ import fr.xephi.authme.output.LogLevel; import fr.xephi.authme.process.register.RegisterSecondaryArgument; import fr.xephi.authme.process.register.RegistrationType; import fr.xephi.authme.security.HashAlgorithm; +import fr.xephi.authme.settings.properties.DatabaseSettings; import fr.xephi.authme.settings.properties.PluginSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.SecuritySettings; @@ -74,6 +75,7 @@ public class SettingsMigrationService extends PlainMigrationService { | convertToRegistrationType(resource) | mergeAndMovePermissionGroupSettings(resource) | moveDeprecatedHashAlgorithmIntoLegacySection(resource) + | moveSaltColumnConfigWithOtherColumnConfigs(resource) || hasDeprecatedProperties(resource); } @@ -313,6 +315,18 @@ public class SettingsMigrationService extends PlainMigrationService { return false; } + /** + * Moves the property for the password salt column name to the same path as all other column name properties. + * + * @param resource The property resource + * @return True if the configuration has changed, false otherwise + */ + private static boolean moveSaltColumnConfigWithOtherColumnConfigs(PropertyResource resource) { + Property oldProperty = newProperty("ExternalBoardOptions.mySQLColumnSalt", + DatabaseSettings.MYSQL_COL_SALT.getDefaultValue()); + return moveProperty(oldProperty, DatabaseSettings.MYSQL_COL_SALT, resource); + } + /** * Retrieves the old config to run a command when alt accounts are detected and sets them to this instance * for further processing. diff --git a/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java b/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java index 8ebccf94..0818c269 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java @@ -65,7 +65,7 @@ public final class DatabaseSettings implements SettingsHolder { @Comment("Column for storing players passwords salts") public static final Property MYSQL_COL_SALT = - newProperty("ExternalBoardOptions.mySQLColumnSalt", ""); + newProperty("DataSource.mySQLColumnSalt", ""); @Comment("Column for storing players emails") public static final Property MYSQL_COL_EMAIL = diff --git a/src/test/java/fr/xephi/authme/settings/SettingsMigrationServiceTest.java b/src/test/java/fr/xephi/authme/settings/SettingsMigrationServiceTest.java index de53ec16..51cef193 100644 --- a/src/test/java/fr/xephi/authme/settings/SettingsMigrationServiceTest.java +++ b/src/test/java/fr/xephi/authme/settings/SettingsMigrationServiceTest.java @@ -24,6 +24,7 @@ import java.util.ArrayList; import java.util.List; import static fr.xephi.authme.TestHelper.getJarFile; +import static fr.xephi.authme.settings.properties.DatabaseSettings.MYSQL_COL_SALT; import static fr.xephi.authme.settings.properties.PluginSettings.ENABLE_PERMISSION_CHECK; import static fr.xephi.authme.settings.properties.PluginSettings.LOG_LEVEL; import static fr.xephi.authme.settings.properties.PluginSettings.REGISTERED_GROUP; @@ -128,6 +129,7 @@ public class SettingsMigrationServiceTest { assertThat(settings.getProperty(UNREGISTERED_GROUP), equalTo("")); assertThat(settings.getProperty(PASSWORD_HASH), equalTo(HashAlgorithm.SHA256)); assertThat(settings.getProperty(LEGACY_HASHES), contains(HashAlgorithm.PBKDF2, HashAlgorithm.WORDPRESS, HashAlgorithm.SHA512)); + assertThat(settings.getProperty(MYSQL_COL_SALT), equalTo("salt_col_name")); // Check migration of old setting to email.html assertThat(Files.readLines(new File(dataFolder, "email.html"), StandardCharsets.UTF_8), diff --git a/src/test/resources/fr/xephi/authme/settings/config-old.yml b/src/test/resources/fr/xephi/authme/settings/config-old.yml index 8c1dc7f3..2e34180c 100644 --- a/src/test/resources/fr/xephi/authme/settings/config-old.yml +++ b/src/test/resources/fr/xephi/authme/settings/config-old.yml @@ -275,7 +275,7 @@ settings: applyBlindEffect: false ExternalBoardOptions: # MySQL column for the salt , needed for some forum/cms support - mySQLColumnSalt: '' + mySQLColumnSalt: 'salt_col_name' # MySQL column for the group, needed for some forum/cms support mySQLColumnGroup: '' # -1 mean disabled. If u want that only From 768ef9179aa278ebe60cc1aa23717a54c7948ad2 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 21 May 2018 13:07:13 +0200 Subject: [PATCH 49/63] Update datasource columns version - Fixes #1551 Bad closing of resources in case of an exception - Facilitates initialization of SQL handler implementation --- pom.xml | 2 +- .../columnshandler/AuthMeColumnsHandler.java | 13 +++--- .../columnshandler/ConnectionSupplier.java | 20 --------- .../MySqlPreparedStatementGenerator.java | 44 ------------------- 4 files changed, 8 insertions(+), 71 deletions(-) delete mode 100644 src/main/java/fr/xephi/authme/datasource/columnshandler/ConnectionSupplier.java delete mode 100644 src/main/java/fr/xephi/authme/datasource/columnshandler/MySqlPreparedStatementGenerator.java diff --git a/pom.xml b/pom.xml index c4cfe5f8..535b41b2 100644 --- a/pom.xml +++ b/pom.xml @@ -800,7 +800,7 @@ ch.jalu datasourcecolumns - 0.1-SNAPSHOT + 0.1.1-SNAPSHOT true diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java index bb0d80b7..50575b4e 100644 --- a/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java +++ b/src/main/java/fr/xephi/authme/datasource/columnshandler/AuthMeColumnsHandler.java @@ -5,9 +5,8 @@ import ch.jalu.datasourcecolumns.data.DataSourceValues; import ch.jalu.datasourcecolumns.data.UpdateValues; import ch.jalu.datasourcecolumns.predicate.Predicate; import ch.jalu.datasourcecolumns.sqlimplementation.PredicateSqlGenerator; -import ch.jalu.datasourcecolumns.sqlimplementation.PreparedStatementGenerator; -import ch.jalu.datasourcecolumns.sqlimplementation.ResultSetValueRetriever; import ch.jalu.datasourcecolumns.sqlimplementation.SqlColumnsHandler; +import ch.jalu.datasourcecolumns.sqlimplementation.statementgenerator.ConnectionSupplier; import fr.xephi.authme.data.auth.PlayerAuth; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.properties.DatabaseSettings; @@ -16,6 +15,8 @@ import java.sql.Connection; import java.sql.SQLException; import java.util.List; +import static ch.jalu.datasourcecolumns.sqlimplementation.SqlColumnsHandlerConfig.forConnectionPool; +import static ch.jalu.datasourcecolumns.sqlimplementation.SqlColumnsHandlerConfig.forSingleConnection; import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException; /** @@ -43,8 +44,9 @@ public final class AuthMeColumnsHandler { String nameColumn = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME); SqlColumnsHandler sqlColHandler = new SqlColumnsHandler<>( - PreparedStatementGenerator.fromConnection(connection), columnContext, tableName, nameColumn, - new ResultSetValueRetriever<>(columnContext), new PredicateSqlGenerator<>(columnContext, true)); + forSingleConnection(connection, tableName, nameColumn, columnContext) + .setPredicateSqlGenerator(new PredicateSqlGenerator<>(columnContext, true)) + ); return new AuthMeColumnsHandler(sqlColHandler); } @@ -61,8 +63,7 @@ public final class AuthMeColumnsHandler { String nameColumn = settings.getProperty(DatabaseSettings.MYSQL_COL_NAME); SqlColumnsHandler sqlColHandler = new SqlColumnsHandler<>( - new MySqlPreparedStatementGenerator(connectionSupplier), columnContext, tableName, nameColumn, - new ResultSetValueRetriever<>(columnContext), new PredicateSqlGenerator<>(columnContext)); + forConnectionPool(connectionSupplier, tableName, nameColumn, columnContext)); return new AuthMeColumnsHandler(sqlColHandler); } diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/ConnectionSupplier.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/ConnectionSupplier.java deleted file mode 100644 index 77fbe8f3..00000000 --- a/src/main/java/fr/xephi/authme/datasource/columnshandler/ConnectionSupplier.java +++ /dev/null @@ -1,20 +0,0 @@ -package fr.xephi.authme.datasource.columnshandler; - -import java.sql.Connection; -import java.sql.SQLException; - -/** - * Supplier of connections to a database. - */ -@FunctionalInterface -public interface ConnectionSupplier { - - /** - * Returns a connection to the database. - * - * @return the connection - * @throws SQLException . - */ - Connection get() throws SQLException; - -} diff --git a/src/main/java/fr/xephi/authme/datasource/columnshandler/MySqlPreparedStatementGenerator.java b/src/main/java/fr/xephi/authme/datasource/columnshandler/MySqlPreparedStatementGenerator.java deleted file mode 100644 index c20357ae..00000000 --- a/src/main/java/fr/xephi/authme/datasource/columnshandler/MySqlPreparedStatementGenerator.java +++ /dev/null @@ -1,44 +0,0 @@ -package fr.xephi.authme.datasource.columnshandler; - -import ch.jalu.datasourcecolumns.sqlimplementation.PreparedStatementGenerator; -import ch.jalu.datasourcecolumns.sqlimplementation.PreparedStatementResult; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; - -/** - * Implementation of {@link PreparedStatementGenerator} for MySQL which ensures that the connection - * taken from the connection pool is also closed after the prepared statement has been executed. - */ -class MySqlPreparedStatementGenerator implements PreparedStatementGenerator { - - private final ConnectionSupplier connectionSupplier; - - MySqlPreparedStatementGenerator(ConnectionSupplier connectionSupplier) { - this.connectionSupplier = connectionSupplier; - } - - @Override - public PreparedStatementResult create(String sql) throws SQLException { - Connection connection = connectionSupplier.get(); - return new MySqlPreparedStatementResult(connection, connection.prepareStatement(sql)); - } - - /** Prepared statement result which also closes the associated connection. */ - private static final class MySqlPreparedStatementResult extends PreparedStatementResult { - - private final Connection connection; - - MySqlPreparedStatementResult(Connection connection, PreparedStatement preparedStatement) { - super(preparedStatement); - this.connection = connection; - } - - @Override - public void close() throws SQLException { - super.close(); - connection.close(); - } - } -} From b9943675ba02d21de5a443e92b0377a30a1f1e4b Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 21 May 2018 13:29:34 +0200 Subject: [PATCH 50/63] #1557 Disallow player from using /email setpassword more than once --- .../authme/command/CommandInitializer.java | 4 +- ...mand.java => EmailSetPasswordCommand.java} | 5 +- .../service/PasswordRecoveryService.java | 17 ++++--- ....java => EmailSetPasswordCommandTest.java} | 11 +++-- .../service/PasswordRecoveryServiceTest.java | 49 +++++++++++++++++++ 5 files changed, 72 insertions(+), 14 deletions(-) rename src/main/java/fr/xephi/authme/command/executable/email/{SetPasswordCommand.java => EmailSetPasswordCommand.java} (89%) rename src/test/java/fr/xephi/authme/command/executable/email/{SetPasswordCommandTest.java => EmailSetPasswordCommandTest.java} (90%) diff --git a/src/main/java/fr/xephi/authme/command/CommandInitializer.java b/src/main/java/fr/xephi/authme/command/CommandInitializer.java index 6dbbcea5..ba48e011 100644 --- a/src/main/java/fr/xephi/authme/command/CommandInitializer.java +++ b/src/main/java/fr/xephi/authme/command/CommandInitializer.java @@ -33,9 +33,9 @@ import fr.xephi.authme.command.executable.changepassword.ChangePasswordCommand; import fr.xephi.authme.command.executable.email.AddEmailCommand; import fr.xephi.authme.command.executable.email.ChangeEmailCommand; import fr.xephi.authme.command.executable.email.EmailBaseCommand; +import fr.xephi.authme.command.executable.email.EmailSetPasswordCommand; import fr.xephi.authme.command.executable.email.ProcessCodeCommand; import fr.xephi.authme.command.executable.email.RecoverEmailCommand; -import fr.xephi.authme.command.executable.email.SetPasswordCommand; import fr.xephi.authme.command.executable.email.ShowEmailCommand; import fr.xephi.authme.command.executable.login.LoginCommand; import fr.xephi.authme.command.executable.logout.LogoutCommand; @@ -540,7 +540,7 @@ public class CommandInitializer { .detailedDescription("Set a new password after successfully recovering your account.") .withArgument("password", "New password", MANDATORY) .permission(PlayerPermission.RECOVER_EMAIL) - .executableCommand(SetPasswordCommand.class) + .executableCommand(EmailSetPasswordCommand.class) .register(); return emailBase; diff --git a/src/main/java/fr/xephi/authme/command/executable/email/SetPasswordCommand.java b/src/main/java/fr/xephi/authme/command/executable/email/EmailSetPasswordCommand.java similarity index 89% rename from src/main/java/fr/xephi/authme/command/executable/email/SetPasswordCommand.java rename to src/main/java/fr/xephi/authme/command/executable/email/EmailSetPasswordCommand.java index d5d084aa..376e8db2 100644 --- a/src/main/java/fr/xephi/authme/command/executable/email/SetPasswordCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/email/EmailSetPasswordCommand.java @@ -18,7 +18,7 @@ import java.util.List; /** * Command for changing password following successful recovery. */ -public class SetPasswordCommand extends PlayerCommand { +public class EmailSetPasswordCommand extends PlayerCommand { @Inject private DataSource dataSource; @@ -45,11 +45,14 @@ public class SetPasswordCommand extends PlayerCommand { if (!result.hasError()) { HashedPassword hashedPassword = passwordSecurity.computeHash(password, name); dataSource.updatePassword(name, hashedPassword); + recoveryService.removeFromSuccessfulRecovery(player); ConsoleLogger.info("Player '" + name + "' has changed their password from recovery"); commonService.send(player, MessageKey.PASSWORD_CHANGED_SUCCESS); } else { commonService.send(player, result.getMessageKey(), result.getArgs()); } + } else { + commonService.send(player, MessageKey.CHANGE_PASSWORD_EXPIRED); } } } diff --git a/src/main/java/fr/xephi/authme/service/PasswordRecoveryService.java b/src/main/java/fr/xephi/authme/service/PasswordRecoveryService.java index f24b45f3..200aa11c 100644 --- a/src/main/java/fr/xephi/authme/service/PasswordRecoveryService.java +++ b/src/main/java/fr/xephi/authme/service/PasswordRecoveryService.java @@ -121,6 +121,16 @@ public class PasswordRecoveryService implements Reloadable, HasCleanup { commonService.send(player, MessageKey.RECOVERY_CHANGE_PASSWORD); } + /** + * Removes a player from the list of successful recovers so that he can + * no longer use the /email setpassword command. + * + * @param player The player to remove. + */ + public void removeFromSuccessfulRecovery(Player player) { + successfulRecovers.remove(player.getName()); + } + /** * Check if a player is able to have emails sent. * @@ -149,12 +159,7 @@ public class PasswordRecoveryService implements Reloadable, HasCleanup { String playerAddress = PlayerUtils.getPlayerIp(player); String storedAddress = successfulRecovers.get(name); - if (storedAddress == null || !playerAddress.equals(storedAddress)) { - messages.send(player, MessageKey.CHANGE_PASSWORD_EXPIRED); - return false; - } - - return true; + return storedAddress != null && playerAddress.equals(storedAddress); } @Override diff --git a/src/test/java/fr/xephi/authme/command/executable/email/SetPasswordCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/email/EmailSetPasswordCommandTest.java similarity index 90% rename from src/test/java/fr/xephi/authme/command/executable/email/SetPasswordCommandTest.java rename to src/test/java/fr/xephi/authme/command/executable/email/EmailSetPasswordCommandTest.java index 33acaf3f..bf7fba78 100644 --- a/src/test/java/fr/xephi/authme/command/executable/email/SetPasswordCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/email/EmailSetPasswordCommandTest.java @@ -24,13 +24,13 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; /** - * Tests for {@link SetPasswordCommand}. + * Tests for {@link EmailSetPasswordCommand}. */ @RunWith(MockitoJUnitRunner.class) -public class SetPasswordCommandTest { +public class EmailSetPasswordCommandTest { @InjectMocks - private SetPasswordCommand command; + private EmailSetPasswordCommand command; @Mock private DataSource dataSource; @@ -70,6 +70,7 @@ public class SetPasswordCommandTest { // then verify(validationService).validatePassword("abc123", name); verify(dataSource).updatePassword(name, hashedPassword); + verify(recoveryService).removeFromSuccessfulRecovery(player); verify(commonService).send(player, MessageKey.PASSWORD_CHANGED_SUCCESS); } @@ -101,7 +102,7 @@ public class SetPasswordCommandTest { command.runCommand(player, Collections.singletonList("abc123")); // then - verifyZeroInteractions(validationService); - verifyZeroInteractions(dataSource); + verifyZeroInteractions(validationService, dataSource); + verify(commonService).send(player, MessageKey.CHANGE_PASSWORD_EXPIRED); } } diff --git a/src/test/java/fr/xephi/authme/service/PasswordRecoveryServiceTest.java b/src/test/java/fr/xephi/authme/service/PasswordRecoveryServiceTest.java index 48973bbf..f0fc9b87 100644 --- a/src/test/java/fr/xephi/authme/service/PasswordRecoveryServiceTest.java +++ b/src/test/java/fr/xephi/authme/service/PasswordRecoveryServiceTest.java @@ -3,6 +3,7 @@ package fr.xephi.authme.service; import ch.jalu.injector.testing.BeforeInjecting; import ch.jalu.injector.testing.DelayedInjectionRunner; import ch.jalu.injector.testing.InjectDelayed; +import fr.xephi.authme.TestHelper; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.mail.EmailService; import fr.xephi.authme.message.MessageKey; @@ -14,6 +15,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -70,4 +73,50 @@ public class PasswordRecoveryServiceTest { verify(emailService).sendRecoveryCode(name, email, code); verify(commonService).send(player, MessageKey.RECOVERY_CODE_SENT); } + + @Test + public void shouldKeepTrackOfSuccessfulRecoversByIp() { + // given + Player bobby = mock(Player.class); + TestHelper.mockPlayerIp(bobby, "192.168.8.8"); + given(bobby.getName()).willReturn("bobby"); + + Player bobby2 = mock(Player.class); + TestHelper.mockPlayerIp(bobby2, "127.0.0.1"); + given(bobby2.getName()).willReturn("bobby"); + + Player other = mock(Player.class); + TestHelper.mockPlayerIp(other, "192.168.8.8"); + given(other.getName()).willReturn("other"); + + // when + recoveryService.addSuccessfulRecovery(bobby); + + // then + assertThat(recoveryService.canChangePassword(bobby), equalTo(true)); + assertThat(recoveryService.canChangePassword(bobby2), equalTo(false)); + assertThat(recoveryService.canChangePassword(other), equalTo(false)); + } + + @Test + public void shouldRemovePlayerFromSuccessfulRecovers() { + // given + Player bobby = mock(Player.class); + TestHelper.mockPlayerIp(bobby, "192.168.8.8"); + given(bobby.getName()).willReturn("bobby"); + recoveryService.addSuccessfulRecovery(bobby); + + Player other = mock(Player.class); + TestHelper.mockPlayerIp(other, "8.8.8.8"); + given(other.getName()).willReturn("other"); + recoveryService.addSuccessfulRecovery(other); + + // when + recoveryService.removeFromSuccessfulRecovery(other); + + + // then + assertThat(recoveryService.canChangePassword(bobby), equalTo(true)); + assertThat(recoveryService.canChangePassword(other), equalTo(false)); + } } From 14d3d1ad91fd1f8afb0afbf2eba6eb7eecf0d150 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Tue, 29 May 2018 00:51:25 +0200 Subject: [PATCH 51/63] Update dependencies --- pom.xml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 535b41b2..3f0fbfa1 100644 --- a/pom.xml +++ b/pom.xml @@ -76,6 +76,10 @@ 1.12.2-R0.1-SNAPSHOT + + 3.3.9 + + @@ -388,7 +392,7 @@ com.google.code.gson gson - 2.8.2 + 2.8.5 true @@ -396,7 +400,7 @@ com.google.guava guava - 24.1-jre + 25.1-jre true @@ -474,7 +478,7 @@ com.warrenstrange googleauth - 1.1.2 + 1.1.5 true @@ -554,7 +558,7 @@ me.lucko.luckperms luckperms-api - 4.1 + 4.2 provided @@ -824,7 +828,7 @@ org.mockito mockito-core test - 2.18.0 + 2.18.3 hamcrest-core @@ -837,7 +841,7 @@ org.xerial sqlite-jdbc - 3.21.0.1 + 3.23.1 test From 5058747b1034b156a8a4a9582fbf4038b8953f6e Mon Sep 17 00:00:00 2001 From: games647 Date: Sat, 2 Jun 2018 21:50:02 +0200 Subject: [PATCH 52/63] Download database only if there is a newer one uploaded (Fixes #1581) --- .../fr/xephi/authme/service/GeoIpService.java | 178 +++++++++++------- 1 file changed, 109 insertions(+), 69 deletions(-) diff --git a/src/main/java/fr/xephi/authme/service/GeoIpService.java b/src/main/java/fr/xephi/authme/service/GeoIpService.java index b973b633..5f1fe86d 100644 --- a/src/main/java/fr/xephi/authme/service/GeoIpService.java +++ b/src/main/java/fr/xephi/authme/service/GeoIpService.java @@ -21,8 +21,9 @@ import fr.xephi.authme.util.InternetProtocolUtils; import java.io.BufferedInputStream; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; -import java.io.OutputStream; +import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.URL; import java.net.UnknownHostException; @@ -33,6 +34,9 @@ import java.nio.file.StandardCopyOption; import java.nio.file.attribute.FileTime; import java.time.Duration; import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.Objects; import java.util.Optional; import java.util.zip.GZIPInputStream; @@ -55,6 +59,10 @@ public class GeoIpService { private static final int UPDATE_INTERVAL_DAYS = 30; + // The server for MaxMind doesn't seem to understand RFC1123, + // but every HTTP implementation have to support RFC 1023 + private static final String TIME_RFC_1023 = "EEE, dd-MMM-yy HH:mm:ss zzz"; + private final Path dataFile; private final BukkitService bukkitService; @@ -104,7 +112,7 @@ public class GeoIpService { // don't fire the update task - we are up to date return true; } else { - ConsoleLogger.debug("GEO Ip database is older than " + UPDATE_INTERVAL_DAYS + " Days"); + ConsoleLogger.debug("GEO IP database is older than " + UPDATE_INTERVAL_DAYS + " Days"); } } catch (IOException ioEx) { ConsoleLogger.logException("Failed to load GeoLiteAPI database", ioEx); @@ -113,53 +121,90 @@ public class GeoIpService { } // File is outdated or doesn't exist - let's try to download the data file! - startDownloadTask(); + // use bukkit's cached threads + bukkitService.runTaskAsynchronously(this::updateDatabase); return false; } /** - * Create a thread which will attempt to download new data from the GeoLite website. + * Tries to update the database by downloading a new version from the website. */ - private void startDownloadTask() { + private void updateDatabase() { downloading = true; - // use bukkit's cached threads - bukkitService.runTaskAsynchronously(() -> { - ConsoleLogger.info("Downloading GEO IP database, because the old database is outdated or doesn't exist"); + ConsoleLogger.info("Downloading GEO IP database, because the old database is older than " + + UPDATE_INTERVAL_DAYS + " days or doesn't exist"); - Path tempFile = null; - try { - // download database to temporarily location - tempFile = Files.createTempFile(ARCHIVE_FILE, null); - try (OutputStream out = Files.newOutputStream(tempFile)) { - Resources.copy(new URL(ARCHIVE_URL), out); - } - - // MD5 checksum verification - String targetChecksum = Resources.toString(new URL(CHECKSUM_URL), StandardCharsets.UTF_8); - if (!verifyChecksum(Hashing.md5(), tempFile, targetChecksum)) { - return; - } - - // tar extract database and copy to target destination - if (!extractDatabase(tempFile, dataFile)) { - ConsoleLogger.warning("Cannot find database inside downloaded GEO IP file at " + tempFile); - return; - } - - ConsoleLogger.info("Successfully downloaded new GEO IP database to " + dataFile); - - //only set this value to false on success otherwise errors could lead to endless download triggers - downloading = false; - } catch (IOException ioEx) { - ConsoleLogger.logException("Could not download GeoLiteAPI database", ioEx); - } finally { - // clean up - if (tempFile != null) { - FileUtils.delete(tempFile.toFile()); - } + Path tempFile = null; + try { + // download database to temporarily location + tempFile = Files.createTempFile(ARCHIVE_FILE, null); + if (!downloadDatabaseArchive(tempFile)) { + ConsoleLogger.info("There is no newer GEO IP database uploaded. Using the old one for now."); + return; } - }); + + // MD5 checksum verification + String expectedChecksum = Resources.toString(new URL(CHECKSUM_URL), StandardCharsets.UTF_8); + verifyChecksum(Hashing.md5(), tempFile, expectedChecksum); + + // tar extract database and copy to target destination + extractDatabase(tempFile, dataFile); + + //only set this value to false on success otherwise errors could lead to endless download triggers + ConsoleLogger.info("Successfully downloaded new GEO IP database to " + dataFile); + downloading = false; + } catch (IOException ioEx) { + ConsoleLogger.logException("Could not download GeoLiteAPI database", ioEx); + } finally { + // clean up + if (tempFile != null) { + FileUtils.delete(tempFile.toFile()); + } + } + } + + /** + * Downloads the archive to the destination file if it's newer than the locally version. + * + * @param lastModified modification timestamp of the already present file + * @param destination save file + * @return false if we already have the newest version, true if successful + * @throws IOException if failed during downloading and writing to destination file + */ + private boolean downloadDatabaseArchive(Instant lastModified, Path destination) throws IOException { + HttpURLConnection connection = (HttpURLConnection) new URL(ARCHIVE_URL).openConnection(); + if (lastModified != null) { + // Only download if we actually need a newer version - this field is specified in GMT zone + ZonedDateTime zonedTime = lastModified.atZone(ZoneId.of("GMT")); + String timeFormat = DateTimeFormatter.ofPattern(TIME_RFC_1023).format(zonedTime); + connection.addRequestProperty("If-Modified-Since", timeFormat); + } + + if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) { + //we already have the newest version + connection.getInputStream().close(); + return false; + } + + Files.copy(connection.getInputStream(), destination, StandardCopyOption.REPLACE_EXISTING); + return true; + } + + /** + * Downloads the archive to the destination file if it's newer than the locally version. + * + * @param destination save file + * @return false if we already have the newest version, true if successful + * @throws IOException if failed during downloading and writing to destination file + */ + private boolean downloadDatabaseArchive(Path destination) throws IOException { + Instant lastModified = null; + if (Files.exists(dataFile)) { + lastModified = Files.getLastModifiedTime(dataFile).toInstant(); + } + + return downloadDatabaseArchive(lastModified, destination); } /** @@ -168,19 +213,15 @@ public class GeoIpService { * @param function the checksum function like MD5, SHA256 used to generate the checksum from the file * @param file the file we want to calculate the checksum from * @param expectedChecksum the expected checksum - * @return true if equal, false otherwise - * @throws IOException on I/O error reading the file + * @throws IOException on I/O error reading the file or the checksum verification failed */ - private boolean verifyChecksum(HashFunction function, Path file, String expectedChecksum) throws IOException { + private void verifyChecksum(HashFunction function, Path file, String expectedChecksum) throws IOException { HashCode actualHash = function.hashBytes(Files.readAllBytes(file)); HashCode expectedHash = HashCode.fromString(expectedChecksum); - if (Objects.equals(actualHash, expectedHash)) { - return true; + if (!Objects.equals(actualHash, expectedHash)) { + throw new IOException("GEO IP Checksum verification failed. " + + "Expected: " + expectedChecksum + "Actual:" + actualHash); } - - ConsoleLogger.warning("GEO IP checksum verification failed"); - ConsoleLogger.warning("Expected: " + expectedHash + " Actual: " + actualHash); - return false; } /** @@ -188,38 +229,37 @@ public class GeoIpService { * * @param tarInputFile gzipped tar input file where the database is * @param outputFile destination file for the database - * @return true if the database was found, false otherwise - * @throws IOException on I/O error reading the tar archive or writing the output + * @throws IOException on I/O error reading the tar archive, or writing the output + * @throws FileNotFoundException if the database cannot be found inside the archive */ - private boolean extractDatabase(Path tarInputFile, Path outputFile) throws IOException { + private void extractDatabase(Path tarInputFile, Path outputFile) throws FileNotFoundException, IOException { // .gz -> gzipped file try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(tarInputFile)); TarInputStream tarIn = new TarInputStream(new GZIPInputStream(in))) { - TarEntry entry; - while ((entry = tarIn.getNextEntry()) != null) { - if (!entry.isDirectory()) { - // filename including folders (absolute path inside the archive) - String filename = entry.getName(); - if (filename.endsWith(DATABASE_EXT)) { - // found the database file - Files.copy(tarIn, outputFile, StandardCopyOption.REPLACE_EXISTING); - - // update the last modification date to be same as in the archive - Files.setLastModifiedTime(outputFile, FileTime.from(entry.getModTime().toInstant())); - return true; - } + for (TarEntry entry = tarIn.getNextEntry(); entry != null; entry = tarIn.getNextEntry()) { + // filename including folders (absolute path inside the archive) + String filename = entry.getName(); + if (entry.isDirectory() || !filename.endsWith(DATABASE_EXT)) { + continue; } + + // found the database file and copy file + Files.copy(tarIn, outputFile, StandardCopyOption.REPLACE_EXISTING); + + // update the last modification date to be same as in the archive + Files.setLastModifiedTime(outputFile, FileTime.from(entry.getModTime().toInstant())); + return; } } - return false; + throw new FileNotFoundException("Cannot find database inside downloaded GEO IP file at " + tarInputFile); } /** * Get the country code of the given IP address. * * @param ip textual IP address to lookup. - * @return two-character ISO 3166-1 alpha code for the country. + * @return two-character ISO 3166-1 alpha code for the country or "--" if it cannot be fetched. */ public String getCountryCode(String ip) { return getCountry(ip).map(Country::getIsoCode).orElse("--"); @@ -229,7 +269,7 @@ public class GeoIpService { * Get the country name of the given IP address. * * @param ip textual IP address to lookup. - * @return The name of the country. + * @return The name of the country or "N/A" if it cannot be fetched. */ public String getCountryName(String ip) { return getCountry(ip).map(Country::getName).orElse("N/A"); @@ -255,7 +295,7 @@ public class GeoIpService { try { InetAddress address = InetAddress.getByName(ip); - //Reader.getCountry() can be null for unknown addresses + // Reader.getCountry() can be null for unknown addresses return Optional.ofNullable(databaseReader.getCountry(address)).map(CountryResponse::getCountry); } catch (UnknownHostException e) { // Ignore invalid ip addresses From f39141ed537fae44af24ee10d133d67d496f8d9a Mon Sep 17 00:00:00 2001 From: games647 Date: Sat, 2 Jun 2018 21:50:43 +0200 Subject: [PATCH 53/63] Fix race condition starting multiple database downloads (Related #1581) --- src/main/java/fr/xephi/authme/service/GeoIpService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/fr/xephi/authme/service/GeoIpService.java b/src/main/java/fr/xephi/authme/service/GeoIpService.java index 5f1fe86d..64e10d60 100644 --- a/src/main/java/fr/xephi/authme/service/GeoIpService.java +++ b/src/main/java/fr/xephi/authme/service/GeoIpService.java @@ -120,6 +120,9 @@ public class GeoIpService { } } + //set the downloading flag in order to fix race conditions outside + downloading = true; + // File is outdated or doesn't exist - let's try to download the data file! // use bukkit's cached threads bukkitService.runTaskAsynchronously(this::updateDatabase); @@ -130,8 +133,6 @@ public class GeoIpService { * Tries to update the database by downloading a new version from the website. */ private void updateDatabase() { - downloading = true; - ConsoleLogger.info("Downloading GEO IP database, because the old database is older than " + UPDATE_INTERVAL_DAYS + " days or doesn't exist"); From 135e323358761d2d78aaf68aac66117c1733f63d Mon Sep 17 00:00:00 2001 From: games647 Date: Sun, 3 Jun 2018 09:47:52 +0200 Subject: [PATCH 54/63] Set the downloading flag in order to mark it as successful (Related #1581) --- src/main/java/fr/xephi/authme/service/GeoIpService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/fr/xephi/authme/service/GeoIpService.java b/src/main/java/fr/xephi/authme/service/GeoIpService.java index 64e10d60..363264bc 100644 --- a/src/main/java/fr/xephi/authme/service/GeoIpService.java +++ b/src/main/java/fr/xephi/authme/service/GeoIpService.java @@ -141,7 +141,8 @@ public class GeoIpService { // download database to temporarily location tempFile = Files.createTempFile(ARCHIVE_FILE, null); if (!downloadDatabaseArchive(tempFile)) { - ConsoleLogger.info("There is no newer GEO IP database uploaded. Using the old one for now."); + ConsoleLogger.info("There is no newer GEO IP database uploaded to MaxMind. Using the old one for now."); + downloading = false; return; } From 0a3b66bc7d1110d74ddb07a1873d1ea647198e5e Mon Sep 17 00:00:00 2001 From: games647 Date: Sun, 3 Jun 2018 13:34:51 +0200 Subject: [PATCH 55/63] Start a reading instance after downloading (Related #1581) --- .../fr/xephi/authme/service/GeoIpService.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/fr/xephi/authme/service/GeoIpService.java b/src/main/java/fr/xephi/authme/service/GeoIpService.java index 363264bc..be274687 100644 --- a/src/main/java/fr/xephi/authme/service/GeoIpService.java +++ b/src/main/java/fr/xephi/authme/service/GeoIpService.java @@ -106,8 +106,7 @@ public class GeoIpService { try { FileTime lastModifiedTime = Files.getLastModifiedTime(dataFile); if (Duration.between(lastModifiedTime.toInstant(), Instant.now()).toDays() <= UPDATE_INTERVAL_DAYS) { - databaseReader = new Reader(dataFile.toFile(), FileMode.MEMORY, new CHMCache()); - ConsoleLogger.info(LICENSE); + startReading(); // don't fire the update task - we are up to date return true; @@ -142,7 +141,7 @@ public class GeoIpService { tempFile = Files.createTempFile(ARCHIVE_FILE, null); if (!downloadDatabaseArchive(tempFile)) { ConsoleLogger.info("There is no newer GEO IP database uploaded to MaxMind. Using the old one for now."); - downloading = false; + startReading(); return; } @@ -155,7 +154,7 @@ public class GeoIpService { //only set this value to false on success otherwise errors could lead to endless download triggers ConsoleLogger.info("Successfully downloaded new GEO IP database to " + dataFile); - downloading = false; + startReading(); } catch (IOException ioEx) { ConsoleLogger.logException("Could not download GeoLiteAPI database", ioEx); } finally { @@ -166,6 +165,14 @@ public class GeoIpService { } } + private void startReading() throws IOException { + databaseReader = new Reader(dataFile.toFile(), FileMode.MEMORY, new CHMCache()); + ConsoleLogger.info(LICENSE); + + // clear downloading flag, because we now have working reader instance + downloading = false; + } + /** * Downloads the archive to the destination file if it's newer than the locally version. * From 38fd133e82e58abc9acf790a99e32242ac04f231 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Tue, 19 Jun 2018 18:44:32 +0200 Subject: [PATCH 56/63] Update libraries --- README.md | 1 - pom.xml | 14 +++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1688990c..5b91291c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ | **Code quality:** | [![Code Climate](https://codeclimate.com/github/AuthMe/AuthMeReloaded/badges/gpa.svg)](https://codeclimate.com/github/AuthMe/AuthMeReloaded) [![Coverage status](https://coveralls.io/repos/AuthMe-Team/AuthMeReloaded/badge.svg?branch=master&service=github)](https://coveralls.io/github/AuthMe-Team/AuthMeReloaded?branch=master) | | **Jenkins CI:** | [![Jenkins Status](https://img.shields.io/website-up-down-green-red/http/shields.io.svg?label=ci.codemc.org)](https://ci.codemc.org/) [![Build Status](https://ci.codemc.org/buildStatus/icon?job=AuthMe/AuthMeReloaded)](https://ci.codemc.org/job/AuthMe/job/AuthMeReloaded) ![Build Tests](https://img.shields.io/jenkins/t/https/ci.codemc.org/job/AuthMe/job/AuthMeReloaded.svg) | | **Other CIs:** | [![CircleCI](https://circleci.com/gh/AuthMe/AuthMeReloaded.svg?style=svg)](https://circleci.com/gh/AuthMe/AuthMeReloaded) | -| **Dependencies:** | [![Dependency Status](https://beta.gemnasium.com/badges/github.com/AuthMe/AuthMeReloaded.svg)](https://beta.gemnasium.com/projects/github.com/AuthMe/AuthMeReloaded) | ## Description diff --git a/pom.xml b/pom.xml index 3f0fbfa1..c772c9f1 100644 --- a/pom.xml +++ b/pom.xml @@ -135,12 +135,12 @@ org.apache.maven.plugins maven-clean-plugin - 3.0.0 + 3.1.0 org.apache.maven.plugins maven-resources-plugin - 3.0.2 + 3.1.0 org.apache.maven.plugins @@ -186,12 +186,12 @@ org.apache.maven.plugins maven-jar-plugin - 3.0.2 + 3.1.0 org.apache.maven.plugins maven-javadoc-plugin - 3.0.0 + 3.0.1 attach-javadoc @@ -231,7 +231,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.1.0 + 3.1.1 package @@ -441,7 +441,7 @@ com.zaxxer HikariCP - 3.1.0 + 3.2.0 true @@ -828,7 +828,7 @@ org.mockito mockito-core test - 2.18.3 + 2.19.0 hamcrest-core From fc07ad3df1887cf6b562af0b2ebde772b112c8bf Mon Sep 17 00:00:00 2001 From: ljacqu Date: Mon, 25 Jun 2018 21:54:42 +0200 Subject: [PATCH 57/63] Update translations page --- docs/translations.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/translations.md b/docs/translations.md index 66f4c793..c8ace37b 100644 --- a/docs/translations.md +++ b/docs/translations.md @@ -1,5 +1,5 @@ - + # AuthMe Translations The following translations are available in AuthMe. Set `messagesLanguage` to the language code @@ -21,12 +21,12 @@ Code | Language | Translated |   [gl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_gl.yml) | Galician | 48% | bar [hu](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_hu.yml) | Hungarian | 87% | bar [id](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_id.yml) | Indonesian | 47% | bar -[it](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_it.yml) | Italian | 92% | bar +[it](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_it.yml) | Italian | 100% | bar [ko](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ko.yml) | Korean | 89% | bar [lt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_lt.yml) | Lithuanian | 36% | bar [nl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_nl.yml) | Dutch | 80% | bar -[pl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pl.yml) | Polish | 92% | bar -[pt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pt.yml) | Portuguese | 80% | bar +[pl](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pl.yml) | Polish | 100% | bar +[pt](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_pt.yml) | Portuguese | 100% | bar [ro](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ro.yml) | Romanian | 80% | bar [ru](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_ru.yml) | Russian | 92% | bar [sk](https://github.com/AuthMe/AuthMeReloaded/blob/master/src/main/resources/messages/messages_sk.yml) | Slovakian | 80% | bar @@ -41,4 +41,4 @@ Code | Language | Translated |   --- -This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Sun Apr 22 11:09:12 CEST 2018 +This page was automatically generated on the [AuthMe/AuthMeReloaded repository](https://github.com/AuthMe/AuthMeReloaded/tree/master/docs/) on Mon Jun 25 21:53:35 CEST 2018 From 0227cb3f7410223514dacbf99f94ec19a689eb3a Mon Sep 17 00:00:00 2001 From: games647 Date: Wed, 4 Jul 2018 02:05:17 +0200 Subject: [PATCH 58/63] Add IPv6 support for isLocal checks (#1592) * Add IPv6 support for isLocal checks * Replace magic values like 127.0.0.1 and use our utility * Support for IPv6 local adresses in IPv6 only or dual stack environments * Loopback [::1] * Site-Local fc00::/7 * Link-local fe80::/10 * Introduce extra method for loopback addresses * Use public IP for passMaxLogin check * Use non-local IP addresses in test after change in verification --- .../authme/process/join/AsynchronousJoin.java | 4 +- .../process/login/AsynchronousLogin.java | 4 +- .../process/register/AsyncRegister.java | 4 +- .../authme/util/InternetProtocolUtils.java | 60 ++++++++++++++++--- .../process/login/AsynchronousLoginTest.java | 30 +++++----- .../util/InternetProtocolUtilsTest.java | 32 +++++++++- 6 files changed, 104 insertions(+), 30 deletions(-) diff --git a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index 7fb13bac..9924eef8 100644 --- a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -18,6 +18,7 @@ import fr.xephi.authme.settings.commandconfig.CommandManager; import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; +import fr.xephi.authme.util.InternetProtocolUtils; import fr.xephi.authme.util.PlayerUtils; import org.bukkit.GameMode; import org.bukkit.Server; @@ -183,8 +184,7 @@ public class AsynchronousJoin implements AsynchronousProcess { private boolean validatePlayerCountForIp(final Player player, String ip) { if (service.getProperty(RestrictionSettings.MAX_JOIN_PER_IP) > 0 && !service.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS) - && !"127.0.0.1".equalsIgnoreCase(ip) - && !"localhost".equalsIgnoreCase(ip) + && !InternetProtocolUtils.isLoopbackAddress(ip) && countOnlinePlayersByIp(ip) > service.getProperty(RestrictionSettings.MAX_JOIN_PER_IP)) { bukkitService.scheduleSyncTaskFromOptionallyAsyncTask( diff --git a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java index 22c46dd9..2441ea28 100644 --- a/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/AsynchronousLogin.java @@ -30,6 +30,7 @@ import fr.xephi.authme.settings.properties.EmailSettings; import fr.xephi.authme.settings.properties.HooksSettings; import fr.xephi.authme.settings.properties.PluginSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; +import fr.xephi.authme.util.InternetProtocolUtils; import fr.xephi.authme.util.PlayerUtils; import fr.xephi.authme.util.Utils; import org.bukkit.ChatColor; @@ -325,8 +326,7 @@ public class AsynchronousLogin implements AsynchronousProcess { // Do not perform the check if player has multiple accounts permission or if IP is localhost if (service.getProperty(RestrictionSettings.MAX_LOGIN_PER_IP) <= 0 || service.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS) - || "127.0.0.1".equalsIgnoreCase(ip) - || "localhost".equalsIgnoreCase(ip)) { + || InternetProtocolUtils.isLoopbackAddress(ip)) { return false; } diff --git a/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java b/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java index fa5d0361..2655c681 100644 --- a/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java +++ b/src/main/java/fr/xephi/authme/process/register/AsyncRegister.java @@ -16,6 +16,7 @@ import fr.xephi.authme.service.bungeecord.BungeeSender; import fr.xephi.authme.service.bungeecord.MessageType; import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; +import fr.xephi.authme.util.InternetProtocolUtils; import fr.xephi.authme.util.PlayerUtils; import org.bukkit.entity.Player; @@ -119,8 +120,7 @@ public class AsyncRegister implements AsynchronousProcess { final int maxRegPerIp = service.getProperty(RestrictionSettings.MAX_REGISTRATION_PER_IP); final String ip = PlayerUtils.getPlayerIp(player); if (maxRegPerIp > 0 - && !"127.0.0.1".equalsIgnoreCase(ip) - && !"localhost".equalsIgnoreCase(ip) + && !InternetProtocolUtils.isLoopbackAddress(ip) && !service.hasPermission(player, ALLOW_MULTIPLE_ACCOUNTS)) { List otherAccounts = database.getAllAuthsByIp(ip); if (otherAccounts.size() >= maxRegPerIp) { diff --git a/src/main/java/fr/xephi/authme/util/InternetProtocolUtils.java b/src/main/java/fr/xephi/authme/util/InternetProtocolUtils.java index 548e1e91..03942154 100644 --- a/src/main/java/fr/xephi/authme/util/InternetProtocolUtils.java +++ b/src/main/java/fr/xephi/authme/util/InternetProtocolUtils.java @@ -1,16 +1,13 @@ package fr.xephi.authme.util; -import java.util.regex.Pattern; +import java.net.InetAddress; +import java.net.UnknownHostException; /** * Utility class about the InternetProtocol */ public final class InternetProtocolUtils { - private static final Pattern LOCAL_ADDRESS_PATTERN = - Pattern.compile("(^127\\.)|(^(0)?10\\.)|(^172\\.(0)?1[6-9]\\.)|(^172\\.(0)?2[0-9]\\.)" - + "|(^172\\.(0)?3[0-1]\\.)|(^169\\.254\\.)|(^192\\.168\\.)"); - // Utility class private InternetProtocolUtils() { } @@ -19,10 +16,57 @@ public final class InternetProtocolUtils { * Checks if the specified address is a private or loopback address * * @param address address to check - * - * @return true if the address is a local or loopback address, false otherwise + * @return true if the address is a local (site and link) or loopback address, false otherwise */ public static boolean isLocalAddress(String address) { - return LOCAL_ADDRESS_PATTERN.matcher(address).find(); + try { + InetAddress inetAddress = InetAddress.getByName(address); + + // Examples: 127.0.0.1, localhost or [::1] + return isLoopbackAddress(address) + // Example: 10.0.0.0, 172.16.0.0, 192.168.0.0, fec0::/10 (deprecated) + // Ref: https://en.wikipedia.org/wiki/IP_address#Private_addresses + || inetAddress.isSiteLocalAddress() + // Example: 169.254.0.0/16, fe80::/10 + // Ref: https://en.wikipedia.org/wiki/IP_address#Address_autoconfiguration + || inetAddress.isLinkLocalAddress() + // non deprecated unique site-local that java doesn't check yet -> fc00::/7 + || isIPv6UniqueSiteLocal(inetAddress); + } catch (UnknownHostException e) { + return false; + } + } + + /** + * Checks if the specified address is a loopback address. This can be one of the following: + *

    + *
  • 127.0.0.1
  • + *
  • localhost
  • + *
  • [::1]
  • + *
+ * + * @param address address to check + * @return true if the address is a loopback one + */ + public static boolean isLoopbackAddress(String address) { + try { + InetAddress inetAddress = InetAddress.getByName(address); + return inetAddress.isLoopbackAddress(); + } catch (UnknownHostException e) { + return false; + } + } + + private static boolean isLoopbackAddress(InetAddress address) { + return address.isLoopbackAddress(); + } + + private static boolean isIPv6UniqueSiteLocal(InetAddress address) { + // ref: https://en.wikipedia.org/wiki/Unique_local_address + + // currently undefined but could be used in the near future fc00::/8 + return (address.getAddress()[0] & 0xFF) == 0xFC + // in use for unique site-local fd00::/8 + || (address.getAddress()[0] & 0xFF) == 0xFD; } } diff --git a/src/test/java/fr/xephi/authme/process/login/AsynchronousLoginTest.java b/src/test/java/fr/xephi/authme/process/login/AsynchronousLoginTest.java index f1981017..2c02e581 100644 --- a/src/test/java/fr/xephi/authme/process/login/AsynchronousLoginTest.java +++ b/src/test/java/fr/xephi/authme/process/login/AsynchronousLoginTest.java @@ -124,7 +124,7 @@ public class AsynchronousLoginTest { public void shouldNotForceLoginUserWithAlreadyOnlineIp() { // given String name = "oscar"; - String ip = "127.0.12.245"; + String ip = "1.1.1.245"; Player player = mockPlayer(name); TestHelper.mockPlayerIp(player, ip); given(playerCache.isAuthenticated(name)).willReturn(false); @@ -147,7 +147,7 @@ public class AsynchronousLoginTest { public void shouldNotForceLoginForCanceledEvent() { // given String name = "oscar"; - String ip = "127.0.12.245"; + String ip = "1.1.1.245"; Player player = mockPlayer(name); TestHelper.mockPlayerIp(player, ip); given(playerCache.isAuthenticated(name)).willReturn(false); @@ -180,7 +180,7 @@ public class AsynchronousLoginTest { mockOnlinePlayersInBukkitService(); // when - boolean result = asynchronousLogin.hasReachedMaxLoggedInPlayersForIp(player, "127.0.0.4"); + boolean result = asynchronousLogin.hasReachedMaxLoggedInPlayersForIp(player, "1.1.1.1"); // then assertThat(result, equalTo(false)); @@ -195,7 +195,7 @@ public class AsynchronousLoginTest { given(commonService.getProperty(RestrictionSettings.MAX_LOGIN_PER_IP)).willReturn(0); // when - boolean result = asynchronousLogin.hasReachedMaxLoggedInPlayersForIp(player, "192.168.0.1"); + boolean result = asynchronousLogin.hasReachedMaxLoggedInPlayersForIp(player, "2.2.2.2"); // then assertThat(result, equalTo(false)); @@ -210,7 +210,7 @@ public class AsynchronousLoginTest { given(commonService.hasPermission(player, PlayerStatePermission.ALLOW_MULTIPLE_ACCOUNTS)).willReturn(true); // when - boolean result = asynchronousLogin.hasReachedMaxLoggedInPlayersForIp(player, "127.0.0.4"); + boolean result = asynchronousLogin.hasReachedMaxLoggedInPlayersForIp(player, "1.1.1.1"); // then assertThat(result, equalTo(false)); @@ -227,7 +227,7 @@ public class AsynchronousLoginTest { mockOnlinePlayersInBukkitService(); // when - boolean result = asynchronousLogin.hasReachedMaxLoggedInPlayersForIp(player, "192.168.0.1"); + boolean result = asynchronousLogin.hasReachedMaxLoggedInPlayersForIp(player, "2.2.2.2"); // then assertThat(result, equalTo(true)); @@ -242,28 +242,28 @@ public class AsynchronousLoginTest { } private void mockOnlinePlayersInBukkitService() { - // 127.0.0.4: albania (online), brazil (offline) + // 1.1.1.1: albania (online), brazil (offline) Player playerA = mockPlayer("albania"); - TestHelper.mockPlayerIp(playerA, "127.0.0.4"); + TestHelper.mockPlayerIp(playerA, "1.1.1.1"); given(dataSource.isLogged(playerA.getName())).willReturn(true); Player playerB = mockPlayer("brazil"); - TestHelper.mockPlayerIp(playerB, "127.0.0.4"); + TestHelper.mockPlayerIp(playerB, "1.1.1.1"); given(dataSource.isLogged(playerB.getName())).willReturn(false); - // 192.168.0.1: congo (online), denmark (offline), ecuador (online) + // 2.2.2.2: congo (online), denmark (offline), ecuador (online) Player playerC = mockPlayer("congo"); - TestHelper.mockPlayerIp(playerC, "192.168.0.1"); + TestHelper.mockPlayerIp(playerC, "2.2.2.2"); given(dataSource.isLogged(playerC.getName())).willReturn(true); Player playerD = mockPlayer("denmark"); - TestHelper.mockPlayerIp(playerD, "192.168.0.1"); + TestHelper.mockPlayerIp(playerD, "2.2.2.2"); given(dataSource.isLogged(playerD.getName())).willReturn(false); Player playerE = mockPlayer("ecuador"); - TestHelper.mockPlayerIp(playerE, "192.168.0.1"); + TestHelper.mockPlayerIp(playerE, "2.2.2.2"); given(dataSource.isLogged(playerE.getName())).willReturn(true); - // 192.168.0.0: france (offline) + // 3.3.3.3: france (offline) Player playerF = mockPlayer("france"); - TestHelper.mockPlayerIp(playerF, "192.168.0.0"); + TestHelper.mockPlayerIp(playerF, "3.3.3.3"); List onlinePlayers = Arrays.asList(playerA, playerB, playerC, playerD, playerE, playerF); returnGivenOnlinePlayers(bukkitService, onlinePlayers); diff --git a/src/test/java/fr/xephi/authme/util/InternetProtocolUtilsTest.java b/src/test/java/fr/xephi/authme/util/InternetProtocolUtilsTest.java index d45c0a57..02d8872a 100644 --- a/src/test/java/fr/xephi/authme/util/InternetProtocolUtilsTest.java +++ b/src/test/java/fr/xephi/authme/util/InternetProtocolUtilsTest.java @@ -3,8 +3,8 @@ package fr.xephi.authme.util; import fr.xephi.authme.TestHelper; import org.junit.Test; -import static org.junit.Assert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; /** * Test for {@link InternetProtocolUtils} @@ -13,14 +13,44 @@ public class InternetProtocolUtilsTest { @Test public void shouldCheckLocalAddress() { + // loopback + assertThat(InternetProtocolUtils.isLocalAddress("localhost"), equalTo(true)); assertThat(InternetProtocolUtils.isLocalAddress("127.0.0.1"), equalTo(true)); + assertThat(InternetProtocolUtils.isLocalAddress("::1"), equalTo(true)); + + // site local assertThat(InternetProtocolUtils.isLocalAddress("10.0.0.1"), equalTo(true)); assertThat(InternetProtocolUtils.isLocalAddress("172.0.0.1"), equalTo(false)); assertThat(InternetProtocolUtils.isLocalAddress("172.16.0.1"), equalTo(true)); assertThat(InternetProtocolUtils.isLocalAddress("192.168.0.1"), equalTo(true)); + + // deprecated site-local + // ref: https://en.wikipedia.org/wiki/IPv6_address#Default_address_selection + assertThat(InternetProtocolUtils.isLocalAddress("fec0::"), equalTo(true)); + + // unique site-local (not deprecated!) + // ref: https://en.wikipedia.org/wiki/Unique_local_address + assertThat(InternetProtocolUtils.isLocalAddress("fde4:8dba:82e1::"), equalTo(true)); + assertThat(InternetProtocolUtils.isLocalAddress("fc00::"), equalTo(true)); + assertThat(InternetProtocolUtils.isLocalAddress("fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"), equalTo(true)); + assertThat(InternetProtocolUtils.isLocalAddress("fe00::"), equalTo(false)); + + // link local + assertThat(InternetProtocolUtils.isLocalAddress("169.254.0.64"), equalTo(true)); + assertThat(InternetProtocolUtils.isLocalAddress("FE80:0000:0000:0000:C800:0EFF:FE74:0008"), equalTo(true)); + + // public assertThat(InternetProtocolUtils.isLocalAddress("94.32.34.5"), equalTo(false)); } + @Test + public void testIsLoopback() { + // loopback + assertThat(InternetProtocolUtils.isLoopbackAddress("localhost"), equalTo(true)); + assertThat(InternetProtocolUtils.isLoopbackAddress("127.0.0.1"), equalTo(true)); + assertThat(InternetProtocolUtils.isLoopbackAddress("::1"), equalTo(true)); + } + @Test public void shouldHavePrivateConstructor() { // given / when / then From 4c640fddd9b7c7194a94ac08e463a632e481df68 Mon Sep 17 00:00:00 2001 From: Pavel Leshchev Date: Wed, 4 Jul 2018 05:05:51 +0500 Subject: [PATCH 59/63] Add come translations (#1596) --- src/main/resources/messages/messages_ru.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/messages/messages_ru.yml b/src/main/resources/messages/messages_ru.yml index fd28978a..c61d8569 100644 --- a/src/main/resources/messages/messages_ru.yml +++ b/src/main/resources/messages/messages_ru.yml @@ -145,11 +145,11 @@ time: # Two-factor authentication two_factor: code_created: '&2Ваш секретный код — %code. Просканируйте его здесь: %url' - # TODO confirmation_required: 'Please confirm your code with /2fa confirm ' + confirmation_required: 'Пожалуйста, подтвердите ваш код с помощью /2fa confirm <код>' # TODO code_required: 'Please submit your two-factor authentication code with /2fa code ' # TODO already_enabled: 'Two-factor authentication is already enabled for your account!' # TODO enable_error_no_code: 'No 2fa key has been generated for you or it has expired. Please run /2fa add' - # TODO enable_success: 'Successfully enabled two-factor authentication for your account' + enable_success: 'Двухфакторная аутентификация для вашего аккаунта успешно подключена' # TODO enable_error_wrong_code: 'Wrong code or code has expired. Please run /2fa add' # TODO not_enabled_error: 'Two-factor authentication is not enabled for your account. Run /2fa add' # TODO removed_success: 'Successfully removed two-factor auth from your account' From 1d118afd17659287520417afe7887dec4fb85531 Mon Sep 17 00:00:00 2001 From: DNx Date: Sat, 7 Jul 2018 20:54:38 +0700 Subject: [PATCH 60/63] Fix #1587 AsyncPlayerPreLoginEvent#getAddress() sometimes return null if it unresolved. In that case we should pass it to PlayerLoginEvent to do the join verification process. --- .../fr/xephi/authme/listener/PlayerListener.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/fr/xephi/authme/listener/PlayerListener.java b/src/main/java/fr/xephi/authme/listener/PlayerListener.java index 660119ed..b8c388e8 100644 --- a/src/main/java/fr/xephi/authme/listener/PlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/PlayerListener.java @@ -54,6 +54,8 @@ import org.bukkit.event.player.PlayerRespawnEvent; import org.bukkit.event.player.PlayerShearEntityEvent; import javax.inject.Inject; +import java.util.ArrayList; +import java.util.List; import static fr.xephi.authme.settings.properties.RestrictionSettings.ALLOWED_MOVEMENT_RADIUS; import static fr.xephi.authme.settings.properties.RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT; @@ -95,6 +97,7 @@ public class PlayerListener implements Listener { private BungeeSender bungeeSender; private boolean isAsyncPlayerPreLoginEventCalled = false; + private List unresolvedPlayerHostname = new ArrayList<>(); @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) { @@ -263,6 +266,11 @@ public class PlayerListener implements Listener { return; } + if (event.getAddress() == null) { + unresolvedPlayerHostname.add(event.getName()); + return; + } + final String name = event.getName(); if (validationService.isUnrestricted(name)) { @@ -307,7 +315,9 @@ public class PlayerListener implements Listener { return; } - if (!isAsyncPlayerPreLoginEventCalled || !settings.getProperty(PluginSettings.USE_ASYNC_PRE_LOGIN_EVENT)) { + + if (!isAsyncPlayerPreLoginEventCalled || !settings.getProperty(PluginSettings.USE_ASYNC_PRE_LOGIN_EVENT) + || unresolvedPlayerHostname.remove(name)) { try { runOnJoinChecks(JoiningPlayer.fromPlayerObject(player), event.getAddress().getHostAddress()); } catch (FailedVerificationException e) { From 1fd4f0e1a78fabd26aa5dbd13d80251705c2056b Mon Sep 17 00:00:00 2001 From: DNx Date: Sun, 8 Jul 2018 02:24:58 +0700 Subject: [PATCH 61/63] Improve previous commit --- .../java/fr/xephi/authme/listener/PlayerListener.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/fr/xephi/authme/listener/PlayerListener.java b/src/main/java/fr/xephi/authme/listener/PlayerListener.java index b8c388e8..9a4ea0d0 100644 --- a/src/main/java/fr/xephi/authme/listener/PlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/PlayerListener.java @@ -54,8 +54,8 @@ import org.bukkit.event.player.PlayerRespawnEvent; import org.bukkit.event.player.PlayerShearEntityEvent; import javax.inject.Inject; -import java.util.ArrayList; -import java.util.List; +import java.util.HashSet; +import java.util.Set; import static fr.xephi.authme.settings.properties.RestrictionSettings.ALLOWED_MOVEMENT_RADIUS; import static fr.xephi.authme.settings.properties.RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT; @@ -97,7 +97,7 @@ public class PlayerListener implements Listener { private BungeeSender bungeeSender; private boolean isAsyncPlayerPreLoginEventCalled = false; - private List unresolvedPlayerHostname = new ArrayList<>(); + private Set unresolvedPlayerHostname = new HashSet<>(); @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) { @@ -266,6 +266,8 @@ public class PlayerListener implements Listener { return; } + // getAddress() sometimes returning null if not yet resolved + // skip it and let PlayerLoginEvent to handle it if (event.getAddress() == null) { unresolvedPlayerHostname.add(event.getName()); return; From 6a219c3fc492307a293175d38cf1554d60c52b7c Mon Sep 17 00:00:00 2001 From: DNx Date: Sun, 8 Jul 2018 02:26:50 +0700 Subject: [PATCH 62/63] this one not get committed --- src/main/java/fr/xephi/authme/listener/PlayerListener.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/fr/xephi/authme/listener/PlayerListener.java b/src/main/java/fr/xephi/authme/listener/PlayerListener.java index 9a4ea0d0..0fbc1f90 100644 --- a/src/main/java/fr/xephi/authme/listener/PlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/PlayerListener.java @@ -271,6 +271,8 @@ public class PlayerListener implements Listener { if (event.getAddress() == null) { unresolvedPlayerHostname.add(event.getName()); return; + } else { + unresolvedPlayerHostname.remove(event.getName()); } final String name = event.getName(); From d4bcec1db405c764412f57c7b2b03dae2a204d59 Mon Sep 17 00:00:00 2001 From: Gabriele C Date: Sun, 15 Jul 2018 04:58:25 +0200 Subject: [PATCH 63/63] Test 1.13 build --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c772c9f1..d2213d37 100644 --- a/pom.xml +++ b/pom.xml @@ -73,7 +73,7 @@ Xephi, sgdc3, DNx5, timvisee, games647, ljacqu, Gnat008 - 1.12.2-R0.1-SNAPSHOT + 1.13-pre7-R0.1-SNAPSHOT