From 301539d75b49e56e9b6bb16f35ead4feac8537c0 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Fri, 1 Oct 2021 21:52:09 +0400 Subject: [PATCH] Server improves: * Server: improved messages on register/reset dialogs; * Tests: added database compatible tests on new code or libs (auth db); --- .../main/java/mage/remote/SessionImpl.java | 2 +- .../mage/server/AuthorizedUserRepository.java | 17 +++-- .../main/java/mage/server/MageServerImpl.java | 26 ++++--- .../src/main/java/mage/server/Main.java | 10 ++- .../src/main/java/mage/server/Session.java | 45 ++++++++---- .../src/main/java/mage/server/User.java | 2 +- .../src/test/data/users-db-sample.h2.mv.db | Bin 0 -> 36864 bytes .../serverside/DatabaseCompatibleTest.java | 66 ++++++++++++++++++ 8 files changed, 138 insertions(+), 30 deletions(-) create mode 100644 Mage.Tests/src/test/data/users-db-sample.h2.mv.db create mode 100644 Mage.Tests/src/test/java/org/mage/test/serverside/DatabaseCompatibleTest.java diff --git a/Mage.Common/src/main/java/mage/remote/SessionImpl.java b/Mage.Common/src/main/java/mage/remote/SessionImpl.java index 3162aa68e6..c8b3f7f1a3 100644 --- a/Mage.Common/src/main/java/mage/remote/SessionImpl.java +++ b/Mage.Common/src/main/java/mage/remote/SessionImpl.java @@ -232,7 +232,7 @@ public class SessionImpl implements Session { public boolean work() throws Throwable { logger.info("Password reset: reseting password for username " + getUserName()); boolean result = server.resetPassword(sessionId, connection.getEmail(), connection.getAuthToken(), connection.getPassword()); - logger.info("Password reset: " + (result ? "DONE, check your email for new password" : "FAIL")); + logger.info("Password reset: " + (result ? "DONE, now you can login with new password" : "FAIL")); return result; } }); diff --git a/Mage.Server/src/main/java/mage/server/AuthorizedUserRepository.java b/Mage.Server/src/main/java/mage/server/AuthorizedUserRepository.java index f1915e72f1..2f817d7db0 100644 --- a/Mage.Server/src/main/java/mage/server/AuthorizedUserRepository.java +++ b/Mage.Server/src/main/java/mage/server/AuthorizedUserRepository.java @@ -22,9 +22,7 @@ import java.io.File; import java.sql.SQLException; import java.util.List; -public enum AuthorizedUserRepository { - - instance; +public class AuthorizedUserRepository { private static final String JDBC_URL = "jdbc:h2:file:./db/authorized_user.h2;AUTO_SERVER=TRUE"; private static final String VERSION_ENTITY_NAME = "authorized_user"; @@ -32,15 +30,20 @@ public enum AuthorizedUserRepository { private static final long DB_VERSION = 2; private static final RandomNumberGenerator rng = new SecureRandomNumberGenerator(); + private static final AuthorizedUserRepository instance; + static { + instance = new AuthorizedUserRepository(JDBC_URL); + } + private Dao dao; - AuthorizedUserRepository() { + public AuthorizedUserRepository(String connectionString) { File file = new File("db"); if (!file.exists()) { file.mkdirs(); } try { - ConnectionSource connectionSource = new JdbcConnectionSource(JDBC_URL); + ConnectionSource connectionSource = new JdbcConnectionSource(connectionString); TableUtils.createTableIfNotExists(connectionSource, AuthorizedUser.class); dao = DaoManager.createDao(connectionSource, AuthorizedUser.class); } catch (SQLException ex) { @@ -48,6 +51,10 @@ public enum AuthorizedUserRepository { } } + public static AuthorizedUserRepository getInstance() { + return instance; + } + public void add(final String userName, final String password, final String email) { try { Hash hash = new SimpleHash(Sha256Hash.ALGORITHM_NAME, password, rng.nextBytes(), 1024); diff --git a/Mage.Server/src/main/java/mage/server/MageServerImpl.java b/Mage.Server/src/main/java/mage/server/MageServerImpl.java index 0992ab6cc0..6fa1a21a88 100644 --- a/Mage.Server/src/main/java/mage/server/MageServerImpl.java +++ b/Mage.Server/src/main/java/mage/server/MageServerImpl.java @@ -83,15 +83,17 @@ public class MageServerImpl implements MageServer { @Override public boolean emailAuthToken(String sessionId, String email) throws MageException { if (!managerFactory.configSettings().isAuthenticationActivated()) { - sendErrorMessageToClient(sessionId, "Registration is disabled by the server config"); + sendErrorMessageToClient(sessionId, Session.REGISTRATION_DISABLED_MESSAGE); return false; } - AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.getByEmail(email); + + AuthorizedUser authorizedUser = AuthorizedUserRepository.getInstance().getByEmail(email); if (authorizedUser == null) { sendErrorMessageToClient(sessionId, "No user was found with the email address " + email); logger.info("Auth token is requested for " + email + " but there's no such user in DB"); return false; } + String authToken = generateAuthToken(); activeAuthTokens.put(email, authToken); String subject = "XMage Password Reset Auth Token"; @@ -113,23 +115,31 @@ public class MageServerImpl implements MageServer { @Override public boolean resetPassword(String sessionId, String email, String authToken, String password) throws MageException { if (!managerFactory.configSettings().isAuthenticationActivated()) { - sendErrorMessageToClient(sessionId, "Registration is disabled by the server config"); + sendErrorMessageToClient(sessionId, Session.REGISTRATION_DISABLED_MESSAGE); return false; } + + // multi-step reset: + // - send auth token + // - check auth token to confirm reset + String storedAuthToken = activeAuthTokens.get(email); if (storedAuthToken == null || !storedAuthToken.equals(authToken)) { sendErrorMessageToClient(sessionId, "Invalid auth token"); logger.info("Invalid auth token " + authToken + " is sent for " + email); return false; } - AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.getByEmail(email); + + AuthorizedUser authorizedUser = AuthorizedUserRepository.getInstance().getByEmail(email); if (authorizedUser == null) { - sendErrorMessageToClient(sessionId, "The user is no longer in the DB"); + sendErrorMessageToClient(sessionId, "User with that email doesn't exists"); logger.info("Auth token is valid, but the user with email address " + email + " is no longer in the DB"); return false; } - AuthorizedUserRepository.instance.remove(authorizedUser.getName()); - AuthorizedUserRepository.instance.add(authorizedUser.getName(), password, email); + + // recreate user with new password + AuthorizedUserRepository.getInstance().remove(authorizedUser.getName()); + AuthorizedUserRepository.getInstance().add(authorizedUser.getName(), password, email); activeAuthTokens.remove(email); return true; } @@ -1042,7 +1052,7 @@ public class MageServerImpl implements MageServer { @Override public void setActivation(final String sessionId, final String userName, boolean active) throws MageException { execute("setActivation", sessionId, () -> { - AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.getByName(userName); + AuthorizedUser authorizedUser = AuthorizedUserRepository.getInstance().getByName(userName); Optional u = managerFactory.userManager().getUserByName(userName); if (u.isPresent()) { User user = u.get(); diff --git a/Mage.Server/src/main/java/mage/server/Main.java b/Mage.Server/src/main/java/mage/server/Main.java index e78807b162..2c967fb7d0 100644 --- a/Mage.Server/src/main/java/mage/server/Main.java +++ b/Mage.Server/src/main/java/mage/server/Main.java @@ -66,7 +66,15 @@ public final class Main { public static final PluginClassLoader classLoader = new PluginClassLoader(); private static TransporterServer server; + + // special test mode: + // - fast game buttons; + // - cheat commands; + // - no deck validation; + // - simplified registration and login (no password check); + // - debug main menu for GUI and rendering testing; private static boolean testMode; + private static boolean fastDbMode; /** @@ -98,7 +106,7 @@ public final class Main { if (config.isAuthenticationActivated()) { logger.info("Check authorized user DB version ..."); - if (!AuthorizedUserRepository.instance.checkAlterAndMigrateAuthorizedUser()) { + if (!AuthorizedUserRepository.getInstance().checkAlterAndMigrateAuthorizedUser()) { logger.fatal("Failed to start server."); return; } diff --git a/Mage.Server/src/main/java/mage/server/Session.java b/Mage.Server/src/main/java/mage/server/Session.java index 237a27ec62..4d1dde28ad 100644 --- a/Mage.Server/src/main/java/mage/server/Session.java +++ b/Mage.Server/src/main/java/mage/server/Session.java @@ -35,6 +35,8 @@ public class Session { private static final Pattern alphabetsPattern = Pattern.compile("[a-zA-Z]"); private static final Pattern digitsPattern = Pattern.compile("[0-9]"); + public static final String REGISTRATION_DISABLED_MESSAGE = "Registration has been disabled on the server. You can use any name and empty password to login."; + private final ManagerFactory managerFactory; private final String sessionId; private UUID userId; @@ -60,30 +62,37 @@ public class Session { public String registerUser(String userName, String password, String email) throws MageException { if (!managerFactory.configSettings().isAuthenticationActivated()) { - String returnMessage = "Registration is disabled by the server config"; + String returnMessage = REGISTRATION_DISABLED_MESSAGE; sendErrorMessageToClient(returnMessage); return returnMessage; } - synchronized (AuthorizedUserRepository.instance) { + synchronized (AuthorizedUserRepository.getInstance()) { + // name String returnMessage = validateUserName(userName); if (returnMessage != null) { sendErrorMessageToClient(returnMessage); return returnMessage; } + // auto-generated password RandomString randomString = new RandomString(10); password = randomString.nextString(); returnMessage = validatePassword(password, userName); if (returnMessage != null) { - sendErrorMessageToClient(returnMessage); + logger.warn("pas: " + password); + sendErrorMessageToClient("Auto-generated password fail, try again: " + returnMessage); return returnMessage; } + + // email returnMessage = validateEmail(email); if (returnMessage != null) { sendErrorMessageToClient(returnMessage); return returnMessage; } - AuthorizedUserRepository.instance.add(userName, password, email); + + // create + AuthorizedUserRepository.getInstance().add(userName, password, email); String text = "You are successfully registered as " + userName + '.'; text += " Your initial, generated password is: " + password; @@ -95,15 +104,15 @@ public class Session { success = managerFactory.mailgunClient().sendMessage(email, subject, text); } if (success) { - String ok = "Sent a registration confirmation / initial password email to " + email + " for " + userName; + String ok = "Email with initial password sent to " + email + " for a user " + userName; logger.info(ok); sendInfoMessageToClient(ok); } else if (Main.isTestMode()) { - String ok = "Server is in test mode. Your account is registered with a password of " + password + " for " + userName; + String ok = "Email sending failed. Server is in test mode. Your account registered with a password " + password + " for a user " + userName; logger.info(ok); sendInfoMessageToClient(ok); } else { - String err = "Failed sending a registration confirmation / initial password email to " + email + " for " + userName; + String err = "Email sending failed. Try use another email address or service. Or reset password by email " + email + " for a user " + userName; logger.error(err); sendErrorMessageToClient(err); return err; @@ -113,9 +122,13 @@ public class Session { } private String validateUserName(String userName) { + // return error message or null on good name + if (userName.equals("Admin")) { + // virtual user for admin console return "User name Admin already in use"; } + ConfigSettings config = managerFactory.configSettings(); if (userName.length() < config.getMinUserNameLength()) { return "User name may not be shorter than " + config.getMinUserNameLength() + " characters"; @@ -123,15 +136,19 @@ public class Session { if (userName.length() > config.getMaxUserNameLength()) { return "User name may not be longer than " + config.getMaxUserNameLength() + " characters"; } + Pattern invalidUserNamePattern = Pattern.compile(managerFactory.configSettings().getInvalidUserNamePattern(), Pattern.CASE_INSENSITIVE); Matcher m = invalidUserNamePattern.matcher(userName); if (m.find()) { return "User name '" + userName + "' includes not allowed characters: use a-z, A-Z and 0-9"; } - AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.getByName(userName); + + AuthorizedUser authorizedUser = AuthorizedUserRepository.getInstance().getByName(userName); if (authorizedUser != null) { return "User name '" + userName + "' already in use"; } + + // all fine return null; } @@ -159,7 +176,7 @@ public class Session { if (email == null || email.isEmpty()) { return "Email address cannot be blank"; } - AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.getByEmail(email); + AuthorizedUser authorizedUser = AuthorizedUserRepository.getInstance().getByEmail(email); if (authorizedUser != null) { return "Email address '" + email + "' is associated with another user"; } @@ -182,8 +199,8 @@ public class Session { this.isAdmin = false; AuthorizedUser authorizedUser = null; if (managerFactory.configSettings().isAuthenticationActivated()) { - authorizedUser = AuthorizedUserRepository.instance.getByName(userName); - String errorMsg = "Wrong username or password. In case you haven't, please register your account first."; + authorizedUser = AuthorizedUserRepository.getInstance().getByName(userName); + String errorMsg = "Wrong username or password. You must register your account first."; if (authorizedUser == null) { return errorMsg; } @@ -193,16 +210,16 @@ public class Session { } if (!authorizedUser.active) { - return "Your profile is deactivated, you can't sign on."; + return "Your profile has been deactivated by admin."; } if (authorizedUser.lockedUntil != null) { if (authorizedUser.lockedUntil.compareTo(Calendar.getInstance().getTime()) > 0) { - return "Your profile is deactivated until " + SystemUtil.dateFormat.format(authorizedUser.lockedUntil); + return "Your profile has need deactivated by admin until " + SystemUtil.dateFormat.format(authorizedUser.lockedUntil); } else { + // unlock on timeout end managerFactory.userManager().createUser(userName, host, authorizedUser).ifPresent(user -> user.setLockedUntil(null) ); - } } } diff --git a/Mage.Server/src/main/java/mage/server/User.java b/Mage.Server/src/main/java/mage/server/User.java index bf7f025da4..41e0378c96 100644 --- a/Mage.Server/src/main/java/mage/server/User.java +++ b/Mage.Server/src/main/java/mage/server/User.java @@ -817,7 +817,7 @@ public class User { authorizedUser.chatLockedUntil = this.chatLockedUntil; authorizedUser.lockedUntil = this.lockedUntil; authorizedUser.active = this.active; - AuthorizedUserRepository.instance.update(authorizedUser); + AuthorizedUserRepository.getInstance().update(authorizedUser); } } } diff --git a/Mage.Tests/src/test/data/users-db-sample.h2.mv.db b/Mage.Tests/src/test/data/users-db-sample.h2.mv.db new file mode 100644 index 0000000000000000000000000000000000000000..91b77c4d569154529666f239d077686377467959 GIT binary patch literal 36864 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