package envoy.server.processors; import static envoy.data.Message.MessageStatus.*; import static envoy.data.User.UserStatus.ONLINE; import static envoy.event.HandshakeRejection.*; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.*; import java.util.logging.Logger; import javax.persistence.NoResultException; import envoy.data.LoginCredentials; import envoy.event.*; import envoy.event.contact.ContactsChangedSinceLastLogin; import envoy.util.*; import envoy.server.data.*; import envoy.server.net.*; import envoy.server.util.*; /** * This {@link ObjectProcessor} handles {@link LoginCredentials}. * * @author Kai S. K. Engelbart * @author Maximilian Käfer * @since Envoy Server Standalone v0.1-alpha */ public final class LoginCredentialProcessor implements ObjectProcessor { private final PersistenceManager persistenceManager = PersistenceManager.getInstance(); private final ConnectionManager connectionManager = ConnectionManager.getInstance(); private static final Logger logger = EnvoyLog.getLogger(LoginCredentialProcessor.class); @Override public void process(LoginCredentials credentials, long socketID, ObjectWriteProxy writeProxy) { // Cache this write proxy for user-independent notifications UserStatusChangeProcessor.setWriteProxy(writeProxy); // Check for compatible versions if (!VersionUtil.verifyCompatibility(credentials.getClientVersion())) { logger.info("The client has the wrong version."); writeProxy.write(socketID, new HandshakeRejection(WRONG_VERSION)); return; } // Acquire a user object (or reject the handshake if that's impossible) User user = null; if (!credentials.isRegistration()) try { user = persistenceManager.getUserByName(credentials.getIdentifier()); // Check if the user is already online if (connectionManager.isOnline(user.getID())) { logger.warning(user + " is already online!"); writeProxy.write(socketID, new HandshakeRejection(INTERNAL_ERROR)); return; } // Authenticate with password or token if (credentials.usesToken()) { // Check the token if (user.getAuthToken() == null || user.getAuthTokenExpiration().isBefore(Instant.now()) || !user.getAuthToken().equals(credentials.getPassword())) { logger.info(user + " tried to use an invalid token."); writeProxy.write(socketID, new HandshakeRejection(INVALID_TOKEN)); return; } } else if (!PasswordUtil.validate(credentials.getPassword(), user.getPasswordHash())) { // Check the password hash logger.info(user + " has entered the wrong password."); writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER)); return; } } catch (final NoResultException e) { logger.info("The requested user does not exist."); writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER)); return; } else { // Validate user name if (!Bounds.isValidContactName(credentials.getIdentifier())) { logger.info("The requested user name is not valid."); writeProxy.write(socketID, new HandshakeRejection(INTERNAL_ERROR)); return; } try { // Check if the name is taken PersistenceManager.getInstance().getUserByName(credentials.getIdentifier()); // This code only gets executed if this user already exists logger.info("The requested user already exists."); writeProxy.write(socketID, new HandshakeRejection(USERNAME_TAKEN)); return; } catch (final NoResultException e) { // Create a new user user = new User(); user.setName(credentials.getIdentifier()); user.setLastSeen(Instant.now()); user.setStatus(ONLINE); user.setPasswordHash(PasswordUtil.hash(credentials.getPassword())); user.setContacts(new HashSet<>()); user.setLatestContactDeletion(Instant.EPOCH); persistenceManager.addContact(user); logger.info("Registered new " + user); } } logger.info(user + " successfully authenticated."); connectionManager.registerUser(user.getID(), socketID); // Change status and notify contacts about it UserStatusChangeProcessor.updateUserStatus(user, ONLINE); // Process token request if (credentials.requestToken()) { String token; if (user.getAuthToken() != null && user.getAuthTokenExpiration().isAfter(Instant.now())) // Reuse existing token and delay expiration date token = user.getAuthToken(); else { // Generate new token token = AuthTokenGenerator.nextToken(); user.setAuthToken(token); } user.setAuthTokenExpiration(Instant.now().plus( ServerConfig.getInstance().getAuthTokenExpiration().longValue(), ChronoUnit.DAYS)); persistenceManager.updateContact(user); writeProxy.write(socketID, new NewAuthToken(token)); } // Notify the user if a contact deletion has happened since he last logged in if (user.getLatestContactDeletion().isAfter(user.getLastSeen())) writeProxy.write(socketID, new ContactsChangedSinceLastLogin()); // Complete the handshake writeProxy.write(socketID, user.toCommon()); // Send pending (group) messages and status changes final var pendingMessages = PersistenceManager.getInstance().getPendingMessages(user, credentials.getLastSync()); pendingMessages.removeIf(GroupMessage.class::isInstance); logger.fine("Sending " + pendingMessages.size() + " pending messages to " + user + "..."); for (final var msg : pendingMessages) { final var msgCommon = msg.toCommon(); if (msg.getCreationDate().isAfter(credentials.getLastSync())) // Sync without side effects writeProxy.write(socketID, msgCommon); else if (msg.getStatus() == SENT) { // Send the message writeProxy.write(socketID, msgCommon); msg.received(); PersistenceManager.getInstance().updateMessage(msg); // Notify the sender about the delivery if (connectionManager.isOnline(msg.getSender().getID())) { msgCommon.nextStatus(); writeProxy.write(connectionManager.getSocketID(msg.getSender().getID()), new MessageStatusChange(msgCommon)); } } else { writeProxy.write(socketID, new MessageStatusChange(msgCommon)); } } final List pendingGroupMessages = PersistenceManager.getInstance() .getPendingGroupMessages(user, credentials.getLastSync()); logger.fine("Sending " + pendingGroupMessages.size() + " pending group messages to " + user + "..."); for (final var gmsg : pendingGroupMessages) { final var gmsgCommon = gmsg.toCommon(); // Deliver the message to the user if he hasn't received it yet if (gmsg.getCreationDate().isAfter(credentials.getLastSync()) || gmsg.getMemberMessageStatus().get(user.getID()) == SENT) { if (gmsg.getMemberMessageStatus().replace(user.getID(), RECEIVED) != RECEIVED) { gmsg.setLastStatusChangeDate(Instant.now()); writeProxy.write(socketID, gmsgCommon); // Notify all online group members about the status change writeProxy.writeToOnlineContacts(gmsg.getRecipient().getContacts(), new GroupMessageStatusChange(gmsg.getID(), RECEIVED, Instant.now(), connectionManager.getUserIDBySocketID(socketID))); if (Collections.min(gmsg.getMemberMessageStatus().values()) == RECEIVED) { gmsg.received(); // Notify online members about the status change writeProxy.writeToOnlineContacts(gmsg.getRecipient().getContacts(), new MessageStatusChange(gmsg.getID(), gmsg.getStatus(), Instant.now())); } PersistenceManager.getInstance().updateMessage(gmsg); } else { // Just send the message without updating if it was received in the past writeProxy.write(socketID, gmsgCommon); } } else { // Sending group message status changes if (gmsg.getStatus() == SENT && gmsg.getLastStatusChangeDate().isAfter(gmsg.getCreationDate()) || gmsg.getStatus() == RECEIVED && gmsg.getLastStatusChangeDate().isAfter(gmsg.getReceivedDate())) gmsg.getMemberMessageStatus() .forEach((memberID, memberStatus) -> writeProxy.write(socketID, new GroupMessageStatusChange(gmsg.getID(), memberStatus, gmsg.getLastStatusChangeDate(), memberID))); // Deliver just a status change instead of the whole message if (gmsg.getStatus() == RECEIVED && user.getLastSeen().isBefore(gmsg.getReceivedDate()) || gmsg.getStatus() == READ && user.getLastSeen().isBefore(gmsg.getReadDate())) writeProxy.write(socketID, new MessageStatusChange(gmsgCommon)); } } } }