/* * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are * permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of * conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, this list * of conditions and the following disclaimer in the documentation and/or other materials * provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * The views and conclusions contained in the software and documentation are those of the * authors and should not be interpreted as representing official policies, either expressed * or implied, of BetaSteward_at_googlemail.com. */ package mage.server; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.regex.Matcher; import java.util.regex.Pattern; import mage.MageException; import mage.constants.Constants; import mage.interfaces.callback.ClientCallback; import mage.interfaces.callback.ClientCallbackMethod; import mage.players.net.UserData; import mage.players.net.UserGroup; import mage.server.game.GamesRoom; import mage.server.game.GamesRoomManager; import mage.server.util.ConfigSettings; import mage.server.util.SystemUtil; import mage.util.RandomUtil; import org.apache.log4j.Logger; import org.jboss.remoting.callback.AsynchInvokerCallbackHandler; import org.jboss.remoting.callback.Callback; import org.jboss.remoting.callback.HandleCallbackException; import org.jboss.remoting.callback.InvokerCallbackHandler; /** * @author BetaSteward_at_googlemail.com */ public class Session { private static final Logger logger = Logger.getLogger(Session.class); private final static Pattern alphabetsPattern = Pattern.compile("[a-zA-Z]"); private final static Pattern digitsPattern = Pattern.compile("[0-9]"); private final String sessionId; private UUID userId; private String host; private int messageId = 0; private final Date timeConnected; private boolean isAdmin = false; private final AsynchInvokerCallbackHandler callbackHandler; private final ReentrantLock lock; public Session(String sessionId, InvokerCallbackHandler callbackHandler) { this.sessionId = sessionId; this.callbackHandler = (AsynchInvokerCallbackHandler) callbackHandler; this.isAdmin = false; this.timeConnected = new Date(); this.lock = new ReentrantLock(); } public String registerUser(String userName, String password, String email) throws MageException { if (!ConfigSettings.instance.isAuthenticationActivated()) { String returnMessage = "Registration is disabled by the server config"; sendErrorMessageToClient(returnMessage); return returnMessage; } synchronized (AuthorizedUserRepository.instance) { String returnMessage = validateUserName(userName); if (returnMessage != null) { sendErrorMessageToClient(returnMessage); return returnMessage; } RandomString randomString = new RandomString(10); password = randomString.nextString(); returnMessage = validatePassword(password, userName); if (returnMessage != null) { sendErrorMessageToClient(returnMessage); return returnMessage; } returnMessage = validateEmail(email); if (returnMessage != null) { sendErrorMessageToClient(returnMessage); return returnMessage; } AuthorizedUserRepository.instance.add(userName, password, email); String text = "You are successfully registered as " + userName + '.'; text += " Your initial, generated password is: " + password; boolean success; String subject = "XMage Registration Completed"; if (!ConfigSettings.instance.getMailUser().isEmpty()) { success = MailClient.sendMessage(email, subject, text); } else { success = MailgunClient.sendMessage(email, subject, text); } if (success) { String ok = "Sent a registration confirmation / initial password email to " + email + " for " + 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; logger.info(ok); sendInfoMessageToClient(ok); } else { String err = "Failed sending a registration confirmation / initial password email to " + email + " for " + userName; logger.error(err); sendErrorMessageToClient(err); return err; } return null; } } private static String validateUserName(String userName) { if (userName.equals("Admin")) { return "User name Admin already in use"; } ConfigSettings config = ConfigSettings.instance; if (userName.length() < config.getMinUserNameLength()) { return "User name may not be shorter than " + config.getMinUserNameLength() + " characters"; } if (userName.length() > config.getMaxUserNameLength()) { return "User name may not be longer than " + config.getMaxUserNameLength() + " characters"; } Pattern invalidUserNamePattern = Pattern.compile(ConfigSettings.instance.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); if (authorizedUser != null) { return "User name '" + userName + "' already in use"; } return null; } private static String validatePassword(String password, String userName) { ConfigSettings config = ConfigSettings.instance; if (password.length() < config.getMinPasswordLength()) { return "Password may not be shorter than " + config.getMinPasswordLength() + " characters"; } if (password.length() > config.getMaxPasswordLength()) { return "Password may not be longer than " + config.getMaxPasswordLength() + " characters"; } if (password.equals(userName)) { return "Password may not be the same as your username"; } Matcher alphabetsMatcher = alphabetsPattern.matcher(password); Matcher digitsMatcher = digitsPattern.matcher(password); if (!alphabetsMatcher.find() || !digitsMatcher.find()) { return "Password has to include at least one alphabet (a-zA-Z) and also at least one digit (0-9)"; } return null; } private static String validateEmail(String email) { if (email == null || email.isEmpty()) { return "Email address cannot be blank"; } AuthorizedUser authorizedUser = AuthorizedUserRepository.instance.getByEmail(email); if (authorizedUser != null) { return "Email address '" + email + "' is associated with another user"; } return null; } public String connectUser(String userName, String password) throws MageException { String returnMessage = connectUserHandling(userName, password); if (returnMessage != null) { sendErrorMessageToClient(returnMessage); } return returnMessage; } public boolean isLocked() { return lock.isLocked(); } public String connectUserHandling(String userName, String password) throws MageException { this.isAdmin = false; AuthorizedUser authorizedUser = null; if (ConfigSettings.instance.isAuthenticationActivated()) { authorizedUser = AuthorizedUserRepository.instance.getByName(userName); String errorMsg = "Wrong username or password. In case you haven't, please register your account first."; if (authorizedUser == null) { return errorMsg; } if (!Main.isTestMode() && !authorizedUser.doCredentialsMatch(userName, password)) { return errorMsg; } if (!authorizedUser.active) { return "Your profile is deactivated, you can't sign on."; } if (authorizedUser.lockedUntil != null) { if (authorizedUser.lockedUntil.compareTo(Calendar.getInstance().getTime()) > 0) { return "Your profile is deactivated until " + SystemUtil.dateFormat.format(authorizedUser.lockedUntil); } else { UserManager.instance.createUser(userName, host, authorizedUser).ifPresent(user -> user.setLockedUntil(null) ); } } } Optional<User> selectUser = UserManager.instance.createUser(userName, host, authorizedUser); boolean reconnect = false; if (!selectUser.isPresent()) { // user already exists selectUser = UserManager.instance.getUserByName(userName); if (selectUser.isPresent()) { User user = selectUser.get(); // If authentication is not activated, check the identity using IP address. if (ConfigSettings.instance.isAuthenticationActivated() || user.getHost().equals(host)) { user.updateLastActivity(null); // minimizes possible expiration this.userId = user.getId(); if (user.getSessionId().isEmpty()) { logger.info("Reconnecting session for " + userName); reconnect = true; } else { //disconnect previous session logger.info("Disconnecting another user instance: " + userName); SessionManager.instance.disconnect(user.getSessionId(), DisconnectReason.ConnectingOtherInstance); } } else { return "User name " + userName + " already in use (or your IP address changed)"; } } } User user = selectUser.get(); if (!UserManager.instance.connectToSession(sessionId, user.getId())) { return "Error connecting " + userName; } this.userId = user.getId(); if (reconnect) { // must be connected to receive the message Optional<GamesRoom> room = GamesRoomManager.instance.getRoom(GamesRoomManager.instance.getMainRoomId()); if (!room.isPresent()) { logger.error("main room not found"); return null; } ChatManager.instance.joinChat(room.get().getChatId(), userId); ChatManager.instance.sendReconnectMessage(userId); } return null; } public void connectAdmin() { this.isAdmin = true; User user = UserManager.instance.createUser("Admin", host, null).orElse( UserManager.instance.getUserByName("Admin").get()); UserData adminUserData = UserData.getDefaultUserDataView(); adminUserData.setGroupId(UserGroup.ADMIN.getGroupId()); user.setUserData(adminUserData); if (!UserManager.instance.connectToSession(sessionId, user.getId())) { logger.info("Error connecting Admin!"); } this.userId = user.getId(); } public boolean setUserData(String userName, UserData userData, String clientVersion, String userIdStr) { Optional<User> _user = UserManager.instance.getUserByName(userName); _user.ifPresent(user -> { if (clientVersion != null) { user.setClientVersion(clientVersion); } user.setUserIdStr(userIdStr); if (user.getUserData() == null || user.getUserData().getGroupId() == UserGroup.DEFAULT.getGroupId()) { user.setUserData(userData); } else { user.getUserData().update(userData); } if (user.getUserData().getAvatarId() < Constants.MIN_AVATAR_ID || user.getUserData().getAvatarId() > Constants.MAX_AVATAR_ID) { user.getUserData().setAvatarId(Constants.DEFAULT_AVATAR_ID); } if (user.getUserData().getAvatarId() == 11) { user.getUserData().setAvatarId(updateAvatar(user.getName())); } }); return _user.isPresent(); } private int updateAvatar(String userName) { //TODO: move to separate class //TODO: add for checking for private key switch (userName) { case "North": return 1006; case "BetaSteward": return 1008; case "Bandit": return 1020; case "fireshoes": return 1021; case "noxx": return 1000; case "magenoxx": return 1018; } return 11; } public String getId() { return sessionId; } // because different threads can activate this public void userLostConnection() { boolean lockSet = false; try { if (lock.tryLock(5000, TimeUnit.MILLISECONDS)) { lockSet = true; logger.debug("SESSION LOCK SET sessionId: " + sessionId); } else { logger.warn("CAN'T GET LOCK - userId: " + userId + " hold count: " + lock.getHoldCount()); } Optional<User> _user = UserManager.instance.getUser(userId); if (!_user.isPresent()) { return; //user was already disconnected by other thread } User user = _user.get(); if (!user.isConnected()) { return; } if (!user.getSessionId().equals(sessionId)) { // user already reconnected with another instance logger.info("OLD SESSION IGNORED - " + user.getName()); return; } // logger.info("LOST CONNECTION - " + user.getName() + " id: " + userId); UserManager.instance.disconnect(userId, DisconnectReason.LostConnection); } catch (InterruptedException ex) { logger.error("SESSION LOCK lost connection - userId: " + userId, ex); } finally { if (lockSet) { lock.unlock(); logger.trace("SESSION LOCK UNLOCK sessionId: " + sessionId); } } } public void kill(DisconnectReason reason) { boolean lockSet = false; try { if (lock.tryLock(5000, TimeUnit.MILLISECONDS)) { lockSet = true; logger.debug("SESSION LOCK SET sessionId: " + sessionId); } else { logger.error("SESSION LOCK - kill: userId " + userId); } UserManager.instance.removeUser(userId, reason); } catch (InterruptedException ex) { logger.error("SESSION LOCK - kill: userId " + userId, ex); } finally { if (lockSet) { lock.unlock(); logger.debug("SESSION LOCK UNLOCK sessionId: " + sessionId); } } } public void fireCallback(final ClientCallback call) { try { call.setMessageId(messageId++); callbackHandler.handleCallbackOneway(new Callback(call)); } catch (HandleCallbackException ex) { // ex.printStackTrace(); UserManager.instance.getUser(userId).ifPresent(user -> { logger.warn("SESSION CALLBACK EXCEPTION - " + user.getName() + " userId " + userId); logger.warn(" - method: " + call.getMethod()); logger.warn(" - cause: " + getBasicCause(ex).toString()); logger.trace("Stack trace:", ex); userLostConnection(); }); } } public UUID getUserId() { return userId; } public boolean isAdmin() { return isAdmin; } public String getHost() { return host; } public Date getConnectionTime() { return timeConnected; } void setHost(String hostAddress) { this.host = hostAddress; } public void sendErrorMessageToClient(String message) { List<String> messageData = new LinkedList<>(); messageData.add("Error while connecting to server"); messageData.add(message); fireCallback(new ClientCallback(ClientCallbackMethod.SHOW_USERMESSAGE, null, messageData)); } public void sendInfoMessageToClient(String message) { List<String> messageData = new LinkedList<>(); messageData.add("Information about connecting to the server"); messageData.add(message); fireCallback(new ClientCallback(ClientCallbackMethod.SHOW_USERMESSAGE, null, messageData)); } public static Throwable getBasicCause(Throwable cause) { Throwable t = cause; while (t.getCause() != null) { t = t.getCause(); if (Objects.equals(t, cause)) { throw new IllegalArgumentException("Infinite cycle detected in causal chain"); } } return t; } } class RandomString { private static final char[] symbols; static { StringBuilder tmp = new StringBuilder(); for (char ch = '0'; ch <= '9'; ++ch) { tmp.append(ch); } for (char ch = 'a'; ch <= 'z'; ++ch) { tmp.append(ch); } symbols = tmp.toString().toCharArray(); } private final char[] buf; public RandomString(int length) { if (length < 8) { length = 8; } buf = new char[length]; } public String nextString() { for (int idx = 0; idx < buf.length; ++idx) { buf[idx] = symbols[RandomUtil.nextInt(symbols.length)]; } return new String(buf); } }