Move Envoy Server Standalone to server/ subdirectory

This commit is contained in:
2020-07-13 11:40:00 +02:00
parent 2bbdb82168
commit e6e0e939a9
59 changed files with 0 additions and 0 deletions

View File

@ -0,0 +1,89 @@
package envoy.server;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Set;
import java.util.logging.Level;
import com.jenkov.nioserver.Server;
import envoy.data.Config;
import envoy.data.ConfigItem;
import envoy.server.data.PersistenceManager;
import envoy.server.net.ConnectionManager;
import envoy.server.net.ObjectMessageProcessor;
import envoy.server.net.ObjectMessageReader;
import envoy.server.processors.*;
import envoy.util.EnvoyLog;
/**
* Starts the server.<br>
* <br>
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>Startup.java</strong><br>
* Created: <strong>24.12.2019</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Server Standalone v0.1-alpha
*/
public class Startup {
/**
* Initializes the logger with a new config instance.
*
* @since Envoy Server Standalone v0.1-beta
*/
private static void initLogging() {
final var items = new HashMap<String, ConfigItem<?>>();
items.put("homeDirectory",
new ConfigItem<>("homeDirectory", "h", File::new, new File(System.getProperty("user.home"), ".envoy-server"), true));
items.put("fileLevelBarrier", new ConfigItem<>("fileLevelBarrier", "fb", Level::parse, Level.WARNING, true));
items.put("consoleLevelBarrier", new ConfigItem<>("consoleLevelBarrier", "cb", Level::parse, Level.FINEST, true));
final var config = new Config();
config.load(items);
EnvoyLog.initialize(config);
EnvoyLog.attach("envoy");
}
/**
* Starts the server.
*
* @param args the run configuration. If it is "no-enter-to-stop" at position 0,
* no command to read in an enter press will be generated
* @throws IOException if the server crashes
* @since Envoy Server Standalone v0.1-alpha
*/
public static void main(String[] args) throws IOException {
initLogging();
final Server server = new Server(8080, ObjectMessageReader::new,
new ObjectMessageProcessor(Set.of(new LoginCredentialProcessor(),
new MessageProcessor(),
new GroupMessageProcessor(),
new GroupCreationProcessor(),
new MessageStatusChangeProcessor(),
new GroupMessageStatusChangeProcessor(),
new UserStatusChangeProcessor(),
new IDGeneratorRequestProcessor(),
new ContactSearchProcessor(),
new ContactOperationProcessor())));
// Initialize the current message ID
final PersistenceManager persistenceManager = PersistenceManager.getInstance();
if (persistenceManager.getConfigItemByID("currentMessageId") == null)
persistenceManager.addConfigItem(new envoy.server.data.ConfigItem("currentMessageId", "0"));
server.start();
server.getSocketProcessor().registerSocketIdListener(ConnectionManager.getInstance());
if (args.length == 0 || !args[0].equalsIgnoreCase("no-enter-to-stop")) {
System.out.println("Press the return key to stop the server...");
System.in.read();
System.out.println("Stopped");
System.exit(0);
}
}
}

View File

@ -0,0 +1,65 @@
package envoy.server.data;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
/**
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>ConfigItem.java</strong><br>
* Created: <strong>28 Jan 2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Server Standalone v0.1-alpha
*/
@Entity
@Table(name = "configuration")
public class ConfigItem {
@Id
private String key;
private String value;
/**
* Creates an instance of @link{ConfigItem}.
*
* @since Envoy Server Standalone v0.1-alpha
*/
public ConfigItem() {}
/**
* Creates an instance of @link{ConfigItem}.
*
* @param key the name of this {@link ConfigItem}
* @param value the value of this {@link ConfigItem}
* @since Envoy Server Standalone v0.1-alpha
*/
public ConfigItem(String key, String value) {
this.key = key;
this.value = value;
}
/**
* @return the key
* @since Envoy Server Standalone v0.1-alpha
*/
public String getKey() { return key; }
/**
* @param key the key to set
* @since Envoy Server Standalone v0.1-alpha
*/
public void setKey(String key) { this.key = key; }
/**
* @return the value
* @since Envoy Server Standalone v0.1-alpha
*/
public String getValue() { return value; }
/**
* @param value the value to set
* @since Envoy Server Standalone v0.1-alpha
*/
public void setValue(String value) { this.value = value; }
}

View File

@ -0,0 +1,107 @@
package envoy.server.data;
import java.time.LocalDateTime;
import java.util.Set;
import javax.persistence.*;
/**
* This class acts as a superclass for all contacts, being {@link User}s and
* {@link Group}s. <br>
* <br>
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>Contact.java</strong><br>
* Created: <strong>24.03.2020</strong><br>
*
* @author Maximilian K&auml;fer
* @since Envoy Server Standalone v0.1-alpha
*/
@Entity
@Table(name = "contacts")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public abstract class Contact {
@Id
@GeneratedValue
protected long id;
protected String name;
@Column(name = "creation_date")
private LocalDateTime creationDate;
@ManyToMany(fetch = FetchType.EAGER)
protected Set<Contact> contacts;
/**
* @return a {@link envoy.data.Contact} object of this envoy.server.data.Contact
* object.
* @since Envoy Server Standalone v0.1-beta
*/
public abstract envoy.data.Contact toCommon();
/**
* Transforms this contact into a {@link envoy.data.Contact} where the contacts
* set of contacts is empty.
*
* @return a {@link envoy.data.Contact} object of this contact
* object.
* @since Envoy Server Standalone v0.1-beta
*/
protected abstract envoy.data.Contact toFlatCommon();
/**
* @return the ID of this contact.
* @since Envoy Server Standalone v0.1-beta
*/
public long getID() { return id; }
/**
* Sets the ID of this contact.
*
* @param id to set for this contact
* @since Envoy Server Standalone v0.1-beta
*/
public void setID(long id) { this.id = id; }
/**
* @return the name of this contact.
* @since Envoy Server Standalone v0.1-beta
*/
public String getName() { return name; }
/**
* Sets the name of this contact.
*
* @param name to set for this contact
* @since Envoy Server Standalone v0.1-beta
*/
public void setName(String name) { this.name = name; }
/**
* @return the contacts
* @since Envoy Server Standalone v0.1-beta
*/
public Set<Contact> getContacts() { return contacts; }
/**
* @param contacts the contacts to set
* @since Envoy Server Standalone v0.1-beta
*/
public void setContacts(Set<Contact> contacts) { this.contacts = contacts; }
/**
* @return the creationDate
* @since Envoy Server Standalone v0.1-beta
*/
public LocalDateTime getCreationDate() { return creationDate; }
/**
* @param creationDate the creationDate to set
* @since Envoy Server Standalone v0.1-beta
*/
public void setCreationDate(LocalDateTime creationDate) { this.creationDate = creationDate; }
@Override
public String toString() { return String.format("%s[id=%d,name=%s, %d contact(s)]", getClass().getSimpleName(), id, name, contacts.size()); }
}

View File

@ -0,0 +1,54 @@
package envoy.server.data;
import java.util.stream.Collectors;
import javax.persistence.Entity;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
/**
* Represents a group inside the database. Referred to as "server group" as
* opposed to "group" from Envoy Common.<br>
* <br>
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>Group.java</strong><br>
* Created: <strong>24.03.2020</strong><br>
*
* @author Maximilian K&auml;fer
* @since Envoy Server Standalone v0.1-alpha
*/
@Entity
@NamedQueries({
@NamedQuery(
name = Group.findByName,
query = "SELECT g FROM Group g WHERE g.name = :name"
),
@NamedQuery(
name = Group.findPendingGroups,
query = "SELECT g FROM Group g WHERE g.creationDate > :lastSeen AND :user MEMBER OF g.contacts"
)
})
public class Group extends Contact {
/**
* Named query retrieving a group by name (parameter {@code :name}).
*
* @since Envoy Server Standalone v0.1-beta
*/
public static final String findByName = "Group.findByName";
/**
* Named query retrieving all pending groups for a specific user (parameter {@code :user}, {@code :lastSeen}).
*
* @since Envoy Server Standalone v0.1-beta
*/
public static final String findPendingGroups = "Group.findPendingGroups";
@Override
public envoy.data.Group toCommon() {
return new envoy.data.Group(id, name, contacts.parallelStream().map(User.class::cast).map(User::toFlatCommon).collect(Collectors.toSet()));
}
@Override
protected envoy.data.Group toFlatCommon() { return toCommon(); }
}

View File

@ -0,0 +1,104 @@
package envoy.server.data;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import javax.persistence.*;
import envoy.data.Group;
/**
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>GroupMessage.java</strong><br>
* Created: <strong>18.04.2020</strong><br>
*
* @author Maximilian K&auml;fer
* @since Envoy Server Standalone v0.1-beta
*/
@Entity
@NamedQuery(
name = GroupMessage.getPendingGroupMsg,
query = "SELECT m FROM GroupMessage m JOIN m.memberMessageStatus s WHERE (KEY(s) = :userId) AND ((m.creationDate > :lastSeen)"
+ "OR ((m.status = envoy.data.Message$MessageStatus.RECEIVED) AND (m.receivedDate > :lastSeen))"
+ "OR ((m.status = envoy.data.Message$MessageStatus.READ) AND (m.readDate > :lastSeen))"
+ "OR ((m.lastStatusChangeDate > :lastSeen)))"
)
public class GroupMessage extends Message {
/**
* Named query retrieving pending group messages sent to a group containing a
* specific user (parameter {@code userId}) that were sent after a certain time
* stamp (parameter {@code :lastSeen}).
*
* @since Envoy Server Standalone v0.1-beta
*/
public static final String getPendingGroupMsg = "GroupMessage.getPendingGroupMsg";
@ElementCollection
private Map<Long, envoy.data.Message.MessageStatus> memberMessageStatus;
@Column(name = "last_status_change_date")
protected LocalDateTime lastStatusChangeDate;
/**
* The constructor for a database object.
*
* @since Envoy Server Standalone v0.1-beta
*/
public GroupMessage() {}
/**
* Constructs a database groupMessage from a common groupMessage.
*
* @param groupMessage the {@link envoy.data.GroupMessage} to convert
* into a
* database {@link GroupMessage}
* @param lastStatusChangeDate the time stamp to set
* @since Envoy Server Standalone v0.1-beta
*/
public GroupMessage(envoy.data.GroupMessage groupMessage, LocalDateTime lastStatusChangeDate) {
super(groupMessage);
memberMessageStatus = groupMessage.getMemberStatuses();
this.lastStatusChangeDate = lastStatusChangeDate;
}
/**
* Converts this groupMessage into an instance of
* {@link envoy.data.GroupMessage}.
*
* @return a {@link envoy.data.GroupMessage} containing the same values as this
* groupMessage
* @since Envoy Server Standalone v0.1-beta
*/
@Override
public envoy.data.GroupMessage toCommon() {
return prepareBuilder().buildGroupMessage((Group) recipient.toCommon(), new HashMap<>(memberMessageStatus));
}
/**
* @return the memberMessageStatus
* @since Envoy Server Standalone v0.1-beta
*/
public Map<Long, envoy.data.Message.MessageStatus> getMemberMessageStatus() { return memberMessageStatus; }
/**
* @param memberMessageStatus the memberMessageStatus to set
* @since Envoy Server Standalone v0.1-beta
*/
public void setMemberMessageStatus(Map<Long, envoy.data.Message.MessageStatus> memberMessageStatus) {
this.memberMessageStatus = memberMessageStatus;
}
/**
* @return the date at which one of the member statuses changed last
* @since Envoy Server Standalone v0.1-beta
*/
public LocalDateTime getLastStatusChangeDate() { return lastStatusChangeDate; }
/**
* @param date the date to set
* @since Envoy Server Standalone v0.1-beta
*/
public void setLastStatusChangeDate(LocalDateTime date) { lastStatusChangeDate = date; }
}

View File

@ -0,0 +1,290 @@
package envoy.server.data;
import static envoy.data.Message.MessageStatus.*;
import java.time.LocalDateTime;
import javax.persistence.*;
import envoy.data.Attachment;
import envoy.data.Attachment.AttachmentType;
import envoy.data.Message.MessageStatus;
import envoy.data.MessageBuilder;
/**
* This class serves as a way to let Hibernate communicate with the server
* without bringing the dependency of JPA/Hibernate into the client.<br>
* It will be referenced as "database message" to clarify between the different
* message objects.<br>
* <br>
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>Message.java</strong><br>
* Created: <strong>02.01.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Server Standalone v0.1-alpha
*/
@Entity
@Table(name = "messages")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@NamedQuery(
name = Message.getPending,
query = "SELECT m FROM Message m WHERE (m.recipient = :user AND m.status = envoy.data.Message$MessageStatus.SENT) "
+ "OR (m.sender = :user) AND ((m.status = envoy.data.Message$MessageStatus.RECEIVED) AND (m.receivedDate > :lastSeen)"
+ "OR (m.status = envoy.data.Message$MessageStatus.READ) AND (m.readDate > :lastSeen))"
)
public class Message {
/**
* Named query retrieving pending messages for a user (parameter {@code :user})
* which was last seen after a specific date (parameter {@code :lastSeen}).
*
* @since Envoy Server Standalone v0.1-beta
*/
public static final String getPending = "Message.getPending";
@Id
protected long id;
@ManyToOne
@JoinColumn
protected User sender;
@ManyToOne
@JoinColumn
protected Contact recipient;
@Column(name = "creation_date")
protected LocalDateTime creationDate;
@Column(name = "received_date")
protected LocalDateTime receivedDate;
@Column(name = "read_date")
protected LocalDateTime readDate;
protected String text;
protected envoy.data.Message.MessageStatus status;
protected AttachmentType attachmentType;
protected byte[] attachment;
protected boolean forwarded;
/**
* The constructor for a database object.
*
* @since Envoy Server Standalone v0.1-alpha
*/
public Message() {}
/**
* Constructs a database message from a common message.
*
* @param message the {@link envoy.data.Message} to convert into a database
* {@link Message}
* @since Envoy Server Standalone v0.1-alpha
*/
public Message(envoy.data.Message message) {
PersistenceManager persistenceManager = PersistenceManager.getInstance();
id = message.getID();
status = message.getStatus();
text = message.getText();
creationDate = message.getCreationDate();
receivedDate = message.getReceivedDate();
readDate = message.getReadDate();
sender = persistenceManager.getUserByID(message.getSenderID());
recipient = persistenceManager.getContactByID(message.getRecipientID());
forwarded = message.isForwarded();
if (message.hasAttachment()) {
attachment = message.getAttachment().getData();
attachmentType = message.getAttachment().getType();
}
}
/**
* Converts this message into an instance of {@link envoy.data.Message}.
*
* @return a {@link envoy.data.Message} containing the same values as this
* message
* @since Envoy Server Standalone v0.1-alpha
*/
public envoy.data.Message toCommon() {
return prepareBuilder().build();
}
/**
* @return a message builder containing the state of this message
* @since Envoy Server Standalone v0.1-beta
*/
MessageBuilder prepareBuilder() {
var builder = new MessageBuilder(sender.getID(), recipient.getID(), id).setText(
text)
.setCreationDate(creationDate)
.setReceivedDate(receivedDate)
.setReadDate(readDate)
.setStatus(status)
.setForwarded(forwarded);
if (attachment != null) builder.setAttachment(new Attachment(attachment, attachmentType));
return builder;
}
/**
* Sets the message status to {@link MessageStatus#RECEIVED} and sets the
* current time stamp as the received date.
*
* @since Envoy Server Standalone v0.1-beta
*/
public void received() {
receivedDate = LocalDateTime.now();
status = RECEIVED;
}
/**
* Sets the message status to {@link MessageStatus#READ} and sets the
* current time stamp as the read date.
*
* @since Envoy Server Standalone v0.1-beta
*/
public void read() {
readDate = LocalDateTime.now();
status = READ;
}
/**
* @return the id of a {link envoy.data.Message}
* @since Envoy Server Standalone v0.1-alpha
*/
public long getID() { return id; }
/**
* @param id the id to set
* @since Envoy Server Standalone v0.1-alpha
* @see Message#getID()
*/
public void setId(long id) { this.id = id; }
/**
* @return the sender of a {link envoy.data.Message}
* @since Envoy Server Standalone v0.1-alpha
*/
public User getSender() { return sender; }
/**
* @param sender the sender to set
* @since Envoy Server Standalone v0.1-alpha
* @see Message#getSender()
*/
public void setSender(User sender) { this.sender = sender; }
/**
* @return the recipient of a {link envoy.data.Message}
* @since Envoy Server Standalone v0.1-alpha
*/
public Contact getRecipient() { return recipient; }
/**
* @param recipient the recipient to set
* @since Envoy Server Standalone v0.1-alpha
* @see Message#getRecipient()
*/
public void setRecipient(User recipient) { this.recipient = recipient; }
/**
* @return the date at which a {link envoy.data.Message} has been created
* @since Envoy Server Standalone v0.1-alpha
*/
public LocalDateTime getCreationDate() { return creationDate; }
/**
* @param creationDate the creation date to set
* @since Envoy Server Standalone v0.1-alpha
* @see Message#getCreationDate()
*/
public void setCreationDate(LocalDateTime creationDate) { this.creationDate = creationDate; }
/**
* @return the date at which a {link envoy.data.Message} has been received by
* the server
* @since Envoy Server Standalone v0.1-alpha
*/
public LocalDateTime getReceivedDate() { return receivedDate; }
/**
* @param receivedDate the received date to set
* @since Envoy Server Standalone v0.1-alpha
* @see Message#getReceivedDate()
*/
public void setReceivedDate(LocalDateTime receivedDate) { this.receivedDate = receivedDate; }
/**
* @return the date at which a {link envoy.data.Message} has been read
* @since Envoy Server Standalone v0.1-alpha
*/
public LocalDateTime getReadDate() { return readDate; }
/**
* @param readDate the read date to set
* @since Envoy Server Standalone v0.1-alpha
* @see Message#getReadDate()
*/
public void setReadDate(LocalDateTime readDate) { this.readDate = readDate; }
/**
* @return the status of a {link envoy.data.Message}
* @since Envoy Server Standalone v0.1-alpha
*/
public envoy.data.Message.MessageStatus getStatus() { return status; }
/**
* @param status the new status of a {link envoy.data.Message}
* @since Envoy Server Standalone v0.1-alpha
*/
public void setStatus(envoy.data.Message.MessageStatus status) { this.status = status; }
/**
* @return the text content of a {link envoy.data.Message}
* @since Envoy Server Standalone v0.1-alpha
*/
public String getText() { return text; }
/**
* @param text the new text content of a {@link envoy.data.Message}
* @since Envoy Server Standalone v0.1-alpha
*/
public void setText(String text) { this.text = text; }
/**
* @return the attachment of a {@link envoy.data.Message}
* @since Envoy Server Standalone v0.1-alpha
*/
public byte[] getAttachment() { return attachment; }
/**
* @param attachment the new attachment
* @since Envoy Server Standalone v0.1-alpha
*/
public void setAttachment(byte[] attachment) { this.attachment = attachment; }
/**
* @return the attachmentType
* @since Envoy Server Standalone v0.1-beta
*/
public AttachmentType getAttachmentType() { return attachmentType; }
/**
* @param attachmentType the attachmentType to set
* @since Envoy Server Standalone v0.1-beta
*/
public void setAttachmentType(AttachmentType attachmentType) { this.attachmentType = attachmentType; }
/**
* @return whether this message is a forwarded message
* @since Envoy Server Standalone v0.1-alpha
*/
public boolean isForwarded() { return forwarded; }
/**
* @param forwarded this message should be a forwarded message.
* @since Envoy Server Standalone v0.1-alpha
*/
public void setForwarded(boolean forwarded) { this.forwarded = forwarded; }
}

View File

@ -0,0 +1,281 @@
package envoy.server.data;
import java.time.LocalDateTime;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import envoy.data.User.UserStatus;
import envoy.server.net.ConnectionManager;
/**
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>PersistenceManager.java</strong><br>
* Created: <strong>1 Jan 2020</strong><br>
*
* @author Leon Hofmeister
* @author Maximilian K&auml;fer
* @since Envoy Server Standalone v0.1-alpha
*/
public class PersistenceManager {
private final EntityManager entityManager = Persistence.createEntityManagerFactory("envoy").createEntityManager();
private final EntityTransaction transaction = entityManager.getTransaction();
private static final PersistenceManager persistenceManager = new PersistenceManager();
/**
* Creates the singleton instance of the @link{PersistenceManager}.
*
* @since Envoy Server Standalone v0.1-alpha
*/
private PersistenceManager() {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
transaction.begin();
ConnectionManager.getInstance()
.getOnlineUsers()
.stream()
.map(this::getUserByID)
.forEach(user -> { user.setStatus(UserStatus.OFFLINE); user.setLastSeen(LocalDateTime.now()); entityManager.merge(user); });
transaction.commit();
}));
}
/**
* @return the {@link PersistenceManager} singleton
* @since Envoy Server Standalone v0.1-alpha
*/
public static PersistenceManager getInstance() { return persistenceManager; }
/**
* Adds a {@link Contact} to the database.
*
* @param contact the {@link Contact} to add to the database
* @since Envoy Server Standalone v0.1-alpha
*/
public void addContact(Contact contact) { persist(contact); }
/**
* Adds a {@link Message} to the database.
*
* @param message the {@link Message} to add to the database
* @since Envoy Server Standalone v0.1-alpha
*/
public void addMessage(Message message) { persist(message); }
/**
* Adds a {@link ConfigItem} to the database.
*
* @param configItem the {@link ConfigItem} to add to the database
* @since Envoy Server Standalone v0.1-alpha
*/
public void addConfigItem(ConfigItem configItem) { persist(configItem); }
/**
* Updates a {@link Contact} in the database
*
* @param contact the {@link Contact} to add to the database
* @since Envoy Server Standalone v0.1-alpha
*/
public void updateContact(Contact contact) { merge(contact); }
/**
* Updates a {@link Message} in the database.
*
* @param message the message to update
* @since Envoy Server Standalone v0.1-alpha
*/
public void updateMessage(Message message) { merge(message); }
/**
* Updates a {@link ConfigItem} in the database.
*
* @param configItem the configItem to update
* @since Envoy Server Standalone v0.1-alpha
*/
public void updateConfigItem(ConfigItem configItem) { merge(configItem); }
/**
* Deletes a {@link Contact} in the database.
*
* @param contact the {@link Contact} to delete
* @since Envoy Server Standalone v0.1-alpha
*/
public void deleteContact(Contact contact) { remove(contact); }
/**
* Deletes a {@link Message} in the database.
*
* @param message the {@link Message} to delete
* @since Envoy Server Standalone v0.1-alpha
*/
public void deleteMessage(Message message) { remove(message); }
/**
* Searches for a {@link User} with a specific ID.
*
* @param id the id to search for
* @return the user with the specified ID or {@code null} if none was found
* @since Envoy Server Standalone v0.1-alpha
*/
public User getUserByID(long id) { return entityManager.find(User.class, id); }
/**
* Searches for a {@link Group} with a specific ID.
*
* @param id the id to search for
* @return the group with the specified ID or {@code null} if none was found
* @since Envoy Server Standalone v0.1-beta
*/
public Group getGroupByID(long id) { return entityManager.find(Group.class, id); }
/**
* Searches for a {@link Contact} with a specific ID.
*
* @param id the id to search for
* @return the contact with the specified ID or {@code null} if none was found
* @since Envoy Server Standalone v0.1-beta
*/
public Contact getContactByID(long id) { return entityManager.find(Contact.class, id); }
/**
* Searched for a {@link User} with a specific name.
*
* @param name the name of the user
* @return the user with the specified name
* @since Envoy Server Standalone v0.1-alpha
*/
public User getUserByName(String name) {
return (User) entityManager.createNamedQuery(User.findByName).setParameter("name", name).getSingleResult();
}
/**
* Searched for a {@link Group} with a specific name.
*
* @param name the name of the group
* @return the group with the specified name
* @since Envoy Server Standalone v0.1-alpha
*/
public Group getGroupByName(String name) {
return (Group) entityManager.createNamedQuery(Group.findByName).setParameter("name", name).getSingleResult();
}
/**
* Searches for a {@link Message} with a specific id.
*
* @param id the id to search for
* @return the message with the specified ID or {@code null} if none is found
* @since Envoy Server Standalone v0.1-alpha
*/
public Message getMessageByID(long id) { return entityManager.find(Message.class, id); }
/**
* @param key the name of this {@link ConfigItem}
* @return the {@link ConfigItem} with the given name
* @since Envoy Server Standalone v0.1-alpha
*/
public ConfigItem getConfigItemByID(String key) { return entityManager.find(ConfigItem.class, key); }
/**
* Returns all messages received while being offline or the ones that have
* changed.
*
* @param user the user who wants to receive his unread messages
* @return all messages that the client does not yet have (unread messages)
* @since Envoy Server Standalone v0.1-alpha
*/
public List<Message> getPendingMessages(User user) {
return entityManager
.createNamedQuery(Message.getPending)
.setParameter("user", user)
.setParameter("lastSeen", user.getLastSeen())
.getResultList();
}
/**
* Returns all groupMessages received while being offline or the ones that have
* changed.
*
* @param user the user who wants to receive his unread groupMessages
* @return all groupMessages that the client does not yet have (unread
* groupMessages)
* @since Envoy Server Standalone v0.1-alpha
*/
public List<GroupMessage> getPendingGroupMessages(User user) {
return entityManager.createNamedQuery(GroupMessage.getPendingGroupMsg)
.setParameter("userId", user.getID())
.setParameter("lastSeen", user.getLastSeen())
.getResultList();
}
/**
* Searches for users matching a search phrase. Contacts of the attached user
* and the attached user is ignored.
*
* @param searchPhrase the search phrase
* @param userId the ID of the user in whose context the search is
* performed
* @return a list of all users who matched the criteria
* @since Envoy Server Standalone v0.1-alpha
*/
public List<User> searchUsers(String searchPhrase, long userId) {
return entityManager.createNamedQuery(
User.searchByName)
.setParameter("searchPhrase", searchPhrase + "%")
.setParameter("context", getUserByID(userId))
.getResultList();
}
/**
* Adds a contact to the contact list of another contact and vice versa.
*
* @param contactID1 the ID of the first contact
* @param contactID2 the ID of the second contact
* @since Envoy Server Standalone v0.1-alpha
*/
public void addContactBidirectional(long contactID1, long contactID2) {
// Get users by ID
Contact c1 = getContactByID(contactID1);
Contact c2 = getContactByID(contactID2);
// Add users to each others contact lists
c1.getContacts().add(c2);
c2.getContacts().add(c1);
// Synchronize changes with the database
transaction.begin();
entityManager.merge(c1);
entityManager.merge(c2);
transaction.commit();
}
/**
* @param user the User whose contacts should be retrieved
* @return the contacts of this User
* @since Envoy Server Standalone v0.1-alpha
*/
public List<User> getContacts(User user) {
return entityManager.createNamedQuery(User.findContacts).setParameter("user", user).getResultList();
}
private void persist(Object obj) {
transaction.begin();
entityManager.persist(obj);
transaction.commit();
}
private void merge(Object obj) {
transaction.begin();
entityManager.merge(obj);
transaction.commit();
}
private void remove(Object obj) {
transaction.begin();
entityManager.remove(obj);
transaction.commit();
}
}

View File

@ -0,0 +1,116 @@
package envoy.server.data;
import java.time.LocalDateTime;
import java.util.Set;
import java.util.stream.Collectors;
import javax.persistence.*;
import envoy.data.User.UserStatus;
/**
* This class enables the storage of user specific data inside a database using
* Hibernate. Its objects will be referred to as database users as opposed to
* the common user objects present on both the client and the server.<br>
* <br>
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>User.java</strong><br>
* Created: <strong>02.01.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @author Maximilian K&auml;fer
* @since Envoy Server Standalone v0.1-alpha
*/
@Entity
@NamedQueries({
@NamedQuery(
name = User.findByName,
query = "SELECT u FROM User u WHERE u.name = :name"
),
@NamedQuery(
name = User.findContacts,
query = "SELECT u.contacts FROM User u WHERE u = :user"
),
@NamedQuery(
name = User.searchByName,
query = "SELECT u FROM User u WHERE (lower(u.name) LIKE lower(:searchPhrase) AND u <> :context AND :context NOT MEMBER OF u.contacts)"
)
})
public class User extends Contact {
/**
* Named query retrieving a user by name (parameter {@code :name}).
*
* @since Envoy Server Standalone v0.1-beta
*/
public static final String findByName = "User.findByName";
/**
* Named query retrieving the contacts of a given user (parameter
* {@code :user}).
*
* @since Envoy Server Standalone v0.1-beta
*/
public static final String findContacts = "User.findContacts";
/**
* Named query searching for users with a name like a search phrase (parameter
* {@code :searchPhrase}) that are not in the contact list of a given user
* (parameter {@code :context}).
*
* @since Envoy Server Standalone v0.1-beta
*/
public static final String searchByName = "User.searchByName";
@Column(name = "password_hash")
private String passwordHash;
@Column(name = "last_seen")
private LocalDateTime lastSeen;
private UserStatus status;
@Override
public envoy.data.User toCommon() {
return new envoy.data.User(id, name, status, contacts.parallelStream().map(Contact::toFlatCommon).collect(Collectors.toSet()));
}
@Override
protected envoy.data.User toFlatCommon() { return new envoy.data.User(id, name, status, Set.of()); }
/**
* @return the password hash
* @since Envoy Server Standalone v0.1-beta
*/
public String getPasswordHash() { return passwordHash; }
/**
* @param passwordHash the password hash to set
* @since Envoy Server Standalone v0.1-beta
*/
public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
/**
* @return the last date the user has been online
* @since Envoy Server Standalone v0.1-alpha
*/
public LocalDateTime getLastSeen() { return lastSeen; }
/**
* @param lastSeen the latest date at which the user has been online to set
* @since Envoy Server Standalone v0.1-alpha
*/
public void setLastSeen(LocalDateTime lastSeen) { this.lastSeen = lastSeen; }
/**
* @return the status
* @since Envoy Server Standalone v0.1-alpha
*/
public UserStatus getStatus() { return status; }
/**
* @param status the status to set
* @since Envoy Server Standalone v0.1-alpha
*/
public void setStatus(UserStatus status) { this.status = status; }
}

View File

@ -0,0 +1,9 @@
/**
* This package contains classes related to persistence.
*
* @author Kai S. K. Engelbart
* @author Leon Hofmeister
* @author Maximilian K&auml;fer
* @since Envoy v0.1-alpha
*/
package envoy.server.data;

View File

@ -0,0 +1,115 @@
package envoy.server.net;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
import com.jenkov.nioserver.ISocketIdListener;
import envoy.data.User.UserStatus;
import envoy.server.data.Group;
import envoy.server.data.PersistenceManager;
import envoy.server.processors.UserStatusChangeProcessor;
/**
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>ConnectionManager.java</strong><br>
* Created: <strong>03.01.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Server Standalone v0.1-alpha
*/
public class ConnectionManager implements ISocketIdListener {
/**
* Contains all socket IDs that have not yet performed a handshake / acquired
* their corresponding user ID.
*
* @since Envoy Server Standalone v0.1-alpha
*/
private Set<Long> pendingSockets = new HashSet<>();
/**
* Contains all socket IDs that have acquired a user ID as keys to these IDs.
*
* @since Envoy Server Standalone v0.1-alpha
*/
private Map<Long, Long> sockets = new HashMap<>();
private static ConnectionManager connectionManager = new ConnectionManager();
private ConnectionManager() {}
/**
* @return a singleton instance of this object
* @since Envoy Server Standalone v0.1-alpha
*/
public static ConnectionManager getInstance() { return connectionManager; }
@Override
public void socketCancelled(long socketID) {
if (!pendingSockets.remove(socketID)) {
// Notify contacts of this users offline-going
envoy.server.data.User user = PersistenceManager.getInstance().getUserByID(getUserIDBySocketID(socketID));
user.setStatus(UserStatus.OFFLINE);
user.setLastSeen(LocalDateTime.now());
UserStatusChangeProcessor.updateUserStatus(user);
// Remove the socket
sockets.entrySet().removeIf(e -> e.getValue() == socketID);
}
}
@Override
public void socketRegistered(long socketID) { pendingSockets.add(socketID); }
/**
* Associates a socket ID with a user ID.
*
* @param userID the user ID
* @param socketID the socket ID
* @since Envoy Server Standalone v0.1-alpha
*/
public void registerUser(long userID, long socketID) {
sockets.put(userID, socketID);
pendingSockets.remove(socketID);
}
/**
* @param userID the ID of the user registered at a socket
* @return the ID of the socket
* @since Envoy Server Standalone v0.1-alpha
*/
public long getSocketID(long userID) { return sockets.get(userID); }
/**
* @param socketID the id of the socket whose User is needed
* @return the userId associated with this socketId
* @since Envoy Server Standalone v0.1-alpha
*/
public long getUserIDBySocketID(long socketID) {
return sockets.entrySet().stream().filter(entry -> entry.getValue().equals(socketID)).findFirst().get().getKey();
}
/**
* @param userID the ID of the user to check for
* @return {@code true} if the user is online
* @since Envoy Server Standalone v0.1-alpha
*/
public boolean isOnline(long userID) { return sockets.containsKey(userID); }
/**
* @return the userIDs of all users who are currently online
* @since Envoy Server Standalone v0.1-alpha
*/
public Set<Long> getOnlineUsers() { return sockets.keySet(); }
/**
* @param group the group to search for
* @return a set of all IDs of currently active members in this group
* @since Envoy Server Standalone v0.1-beta
*/
public Set<Long> getOnlineUsersOfGroup(Group group) {
return group.getContacts().stream().map(envoy.server.data.Contact::getID).filter(this::isOnline).collect(Collectors.toSet());
}
}

View File

@ -0,0 +1,65 @@
package envoy.server.net;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.jenkov.nioserver.IMessageProcessor;
import com.jenkov.nioserver.Message;
import com.jenkov.nioserver.WriteProxy;
import envoy.server.processors.ObjectProcessor;
import envoy.util.EnvoyLog;
/**
* Handles incoming objects.<br>
* <br>
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>ObjectMessageProcessor.java</strong><br>
* Created: <strong>28.12.2019</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Server Standalone v0.1-alpha
*/
public class ObjectMessageProcessor implements IMessageProcessor {
private final Set<ObjectProcessor<?>> processors;
private static final Logger logger = EnvoyLog.getLogger(ObjectMessageProcessor.class);
/**
* The constructor to set the {@link ObjectProcessor}s.
*
* @param processors the {@link ObjectProcessor} to set
* @since Envoy Server Standalone v0.1-alpha
*/
public ObjectMessageProcessor(Set<ObjectProcessor<?>> processors) { this.processors = processors; }
@SuppressWarnings("unchecked")
@Override
public void process(Message message, WriteProxy writeProxy) {
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(message.sharedArray, message.offset + 4, message.length - 4))) {
Object obj = in.readObject();
if (obj == null) {
logger.warning("Received a null object");
return;
}
logger.fine("Received " + obj);
// Process object
processors.stream().filter(p -> p.getInputClass().equals(obj.getClass())).forEach((@SuppressWarnings("rawtypes") ObjectProcessor p) -> {
try {
p.process(p.getInputClass().cast(obj), message.socketId, new ObjectWriteProxy(writeProxy));
} catch (IOException e) {
logger.log(Level.SEVERE, "Exception during processor execution: ", e);
}
});
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,71 @@
package envoy.server.net;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import com.jenkov.nioserver.*;
import envoy.util.SerializationUtils;
/**
* This {@link IMessageReader} decodes serialized Java objects.<br>
* <br>
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>ObjectMessageReader.java</strong><br>
* Created: <strong>28.12.2019</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Server Standalone v0.1-alpha
*/
public class ObjectMessageReader implements IMessageReader {
private List<Message> completeMessages = new ArrayList<>();
private Message nextMessage;
private MessageBuffer messageBuffer;
@Override
public List<Message> getMessages() { return completeMessages; }
@Override
public void init(MessageBuffer messageBuffer) {
this.messageBuffer = messageBuffer;
nextMessage = messageBuffer.getMessage();
}
@Override
public void read(Socket socket, ByteBuffer buffer) throws IOException {
socket.read(buffer);
buffer.flip();
if (!buffer.hasRemaining()) {
buffer.clear();
return;
}
nextMessage.writeToMessage(buffer);
buffer.clear();
// Get message length
if (nextMessage.length < 4) return;
int length = SerializationUtils.bytesToInt(nextMessage.sharedArray, nextMessage.offset) + 4;
do {
// Separate first complete message
if (nextMessage.length >= length) {
Message message = messageBuffer.getMessage();
message.writePartialMessageToMessage(nextMessage, length);
message.length = nextMessage.length - length;
nextMessage.length = length;
completeMessages.add(nextMessage);
nextMessage = message;
}
// Get message length
if (nextMessage.length < 4) return;
length = SerializationUtils.bytesToInt(nextMessage.sharedArray, nextMessage.offset) + 4;
} while (nextMessage.length >= length);
}
}

View File

@ -0,0 +1,88 @@
package envoy.server.net;
import java.io.IOException;
import java.util.Set;
import java.util.logging.Logger;
import java.util.stream.Stream;
import com.jenkov.nioserver.Message;
import com.jenkov.nioserver.WriteProxy;
import envoy.server.data.Contact;
import envoy.util.EnvoyLog;
import envoy.util.SerializationUtils;
/**
* This class defines methods to send an object to a client.<br>
* <br>
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>ObjectWriteProxy.java</strong><br>
* Created: <strong>04.01.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Server Standalone v0.1-alpha
*/
public class ObjectWriteProxy {
private final WriteProxy writeProxy;
private static final ConnectionManager connectionManager = ConnectionManager.getInstance();
private static final Logger logger = EnvoyLog.getLogger(ObjectWriteProxy.class);
/**
* Creates an instance of {@link ObjectWriteProxy}.
*
* @param writeProxy the {@link WriteProxy} to write objects to another client
* @since Envoy Server Standalone v0.1-alpha
*/
public ObjectWriteProxy(WriteProxy writeProxy) { this.writeProxy = writeProxy; }
/**
* @param recipientSocketID the socket id of the recipient
* @param obj the object to return to the client
* @throws RuntimeException if the serialization of the object failed (this is
* highly unlikely)
* @since Envoy Server Standalone v0.1-alpha
*/
public void write(long recipientSocketID, Object obj) {
// Create message targeted at the client
final Message response = writeProxy.getMessage();
response.socketId = recipientSocketID;
logger.fine("Sending " + obj);
try {
// Serialize object to byte array
final byte[] objBytes = SerializationUtils.writeToByteArray(obj);
// Acquire object length in bytes
final byte[] objLen = SerializationUtils.intToBytes(objBytes.length);
response.writeToMessage(objLen);
response.writeToMessage(objBytes);
writeProxy.enqueue(response);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Sends an object to all contact in a set that are online.
*
* @param contacts the contacts to send the object to
* @param message the object to send
* @since Envoy Server Standalone v0.1-beta
*/
public void writeToOnlineContacts(Set<? extends Contact> contacts, Object message) { writeToOnlineContacts(contacts.stream(), message); }
/**
* Sends an object to all contact in a set that are online.
*
* @param contacts the contacts to send the object to
* @param message the object to send
* @since Envoy Server Standalone v0.1-beta
*/
public void writeToOnlineContacts(Stream<? extends Contact> contacts, Object message) {
contacts.map(Contact::getID).filter(connectionManager::isOnline).map(connectionManager::getSocketID).forEach(id -> write(id, message));
}
}

View File

@ -0,0 +1,9 @@
/**
* This package contains all classes related to client connection management.
*
* @author Kai S. K. Engelbart
* @author Leon Hofmeister
* @author Maximilian K&auml;fer
* @since Envoy v0.1-alpha
*/
package envoy.server.net;

View File

@ -0,0 +1,9 @@
/**
* This package contains the class that manages application startup.
*
* @author Kai S. K. Engelbart
* @author Leon Hofmeister
* @author Maximilian K&auml;fer
* @since Envoy Server Standalone v0.1-alpha
*/
package envoy.server;

View File

@ -0,0 +1,47 @@
package envoy.server.processors;
import java.util.logging.Logger;
import envoy.event.ElementOperation;
import envoy.event.contact.ContactOperation;
import envoy.server.data.PersistenceManager;
import envoy.server.net.ConnectionManager;
import envoy.server.net.ObjectWriteProxy;
import envoy.util.EnvoyLog;
/**
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>ContactOperationProcessor.java</strong><br>
* Created: <strong>08.02.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Server Standalone v0.1-alpha
*/
public class ContactOperationProcessor implements ObjectProcessor<ContactOperation> {
private static final ConnectionManager connectionManager = ConnectionManager.getInstance();
private static final Logger logger = EnvoyLog.getLogger(ContactOperationProcessor.class);
@Override
public void process(ContactOperation evt, long socketId, ObjectWriteProxy writeProxy) {
switch (evt.getOperationType()) {
case ADD:
final long userID = ConnectionManager.getInstance().getUserIDBySocketID(socketId);
final long contactId = evt.get().getID();
logger.fine(String.format("Adding user %s to the contact list of user %d.%n", evt.get(), userID));
PersistenceManager.getInstance().addContactBidirectional(userID, contactId);
// Notify the contact if online
if (ConnectionManager.getInstance().isOnline(contactId))
writeProxy.write(connectionManager.getSocketID(contactId),
new ContactOperation(PersistenceManager.getInstance().getUserByID(userID).toCommon(), ElementOperation.ADD));
break;
default:
logger.warning(String.format("Received %s with an unsupported operation.", evt));
}
}
@Override
public Class<ContactOperation> getInputClass() { return ContactOperation.class; }
}

View File

@ -0,0 +1,44 @@
package envoy.server.processors;
import java.io.IOException;
import java.util.stream.Collectors;
import envoy.data.Contact;
import envoy.event.contact.ContactSearchRequest;
import envoy.event.contact.ContactSearchResult;
import envoy.server.data.PersistenceManager;
import envoy.server.data.User;
import envoy.server.net.ConnectionManager;
import envoy.server.net.ObjectWriteProxy;
/**
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>ContactSearchProcessor.java</strong><br>
* Created: <strong>08.02.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @author Maximilian K&auml;fer
* @since Envoy Server Standalone v0.1-alpha
*/
public class ContactSearchProcessor implements ObjectProcessor<ContactSearchRequest> {
/**
* Writes a list of contacts to the client containing all {@link Contact}s
* matching the search phrase contained inside the request. The client and their
* contacts are excluded from the result.
*
* @since Envoy Server Standalone v0.1-alpha
*/
@Override
public void process(ContactSearchRequest request, long socketID, ObjectWriteProxy writeProxy) throws IOException {
writeProxy.write(socketID,
new ContactSearchResult(PersistenceManager.getInstance()
.searchUsers(request.get(), ConnectionManager.getInstance().getUserIDBySocketID(socketID))
.stream()
.map(User::toCommon)
.collect(Collectors.toList())));
}
@Override
public Class<ContactSearchRequest> getInputClass() { return ContactSearchRequest.class; }
}

View File

@ -0,0 +1,46 @@
package envoy.server.processors;
import java.util.HashSet;
import envoy.event.ElementOperation;
import envoy.event.GroupCreation;
import envoy.event.contact.ContactOperation;
import envoy.server.data.Contact;
import envoy.server.data.PersistenceManager;
import envoy.server.net.ConnectionManager;
import envoy.server.net.ObjectWriteProxy;
/**
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>GroupCreationProcessor.java</strong><br>
* Created: <strong>26.03.2020</strong><br>
*
* @author Maximilian K&auml;fer
* @since Envoy Server Standalone v0.1-beta
*/
public class GroupCreationProcessor implements ObjectProcessor<GroupCreation> {
private final PersistenceManager persistenceManager = PersistenceManager.getInstance();
private final ConnectionManager connectionManager = ConnectionManager.getInstance();
@Override
public void process(GroupCreation groupCreation, long socketID, ObjectWriteProxy writeProxy) {
envoy.server.data.Group group = new envoy.server.data.Group();
group.setName(groupCreation.get());
group.setContacts(new HashSet<>());
groupCreation.getInitialMemberIDs().stream().map(persistenceManager::getUserByID).forEach(group.getContacts()::add);
group.getContacts().add(persistenceManager.getContactByID(connectionManager.getUserIDBySocketID(socketID)));
group.getContacts().forEach(c -> c.getContacts().add(group));
group.getContacts().add(persistenceManager.getUserByID(connectionManager.getUserIDBySocketID(socketID)));
persistenceManager.addContact(group);
group.getContacts()
.stream()
.map(Contact::getID)
.filter(connectionManager::isOnline)
.map(connectionManager::getSocketID)
.forEach(memberSocketID -> writeProxy.write(memberSocketID, new ContactOperation(group.toCommon(), ElementOperation.ADD)));
}
@Override
public Class<GroupCreation> getInputClass() { return GroupCreation.class; }
}

View File

@ -0,0 +1,67 @@
package envoy.server.processors;
import static envoy.data.Message.MessageStatus.*;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.logging.Logger;
import javax.persistence.EntityExistsException;
import envoy.data.GroupMessage;
import envoy.event.MessageStatusChange;
import envoy.server.data.PersistenceManager;
import envoy.server.net.ConnectionManager;
import envoy.server.net.ObjectWriteProxy;
import envoy.util.EnvoyLog;
/**
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>GroupMessageProcessor.java</strong><br>
* Created: <strong>18.04.2020</strong><br>
*
* @author Maximilian K&auml;fer
* @since Envoy Server Standalone v0.1-beta
*/
public class GroupMessageProcessor implements ObjectProcessor<GroupMessage> {
private static final ConnectionManager connectionManager = ConnectionManager.getInstance();
private static final PersistenceManager persistenceManager = PersistenceManager.getInstance();
private static final Logger logger = EnvoyLog.getLogger(GroupCreationProcessor.class);
@Override
public void process(GroupMessage groupMessage, long socketID, ObjectWriteProxy writeProxy) {
groupMessage.nextStatus();
// Update statuses to SENT / RECEIVED depending on online status
groupMessage.getMemberStatuses().replaceAll((memberID, status) -> connectionManager.isOnline(memberID) ? RECEIVED : SENT);
// Set status for sender to READ
groupMessage.getMemberStatuses().replace(groupMessage.getSenderID(), READ);
// Increment the overall status to RECEIVED if necessary
if (Collections.min(groupMessage.getMemberStatuses().values()) == RECEIVED) {
groupMessage.nextStatus();
// Notify the sender of the status change
writeProxy.write(socketID, new MessageStatusChange(groupMessage));
}
// Deliver the message to the recipients that are online
writeProxy.writeToOnlineContacts(
persistenceManager.getGroupByID(groupMessage.getRecipientID())
.getContacts()
.stream()
.filter(c -> c.getID() != groupMessage.getSenderID()),
groupMessage);
try {
PersistenceManager.getInstance().addMessage(new envoy.server.data.GroupMessage(groupMessage, LocalDateTime.now()));
} catch (EntityExistsException e) {
logger.warning("Received a groupMessage with an ID that already exists");
}
}
@Override
public Class<GroupMessage> getInputClass() { return GroupMessage.class; }
}

View File

@ -0,0 +1,69 @@
package envoy.server.processors;
import static envoy.data.Message.MessageStatus.READ;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.logging.Level;
import java.util.logging.Logger;
import envoy.data.Message.MessageStatus;
import envoy.event.GroupMessageStatusChange;
import envoy.event.MessageStatusChange;
import envoy.server.data.GroupMessage;
import envoy.server.data.PersistenceManager;
import envoy.server.net.ConnectionManager;
import envoy.server.net.ObjectWriteProxy;
import envoy.util.EnvoyLog;
/**
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>GroupMessageStatusChangeProcessor.java</strong><br>
* Created: <strong>03.07.2020</strong><br>
*
* @author Maximilian K&auml;fer
* @since Envoy Server Standalone v0.1-beta
*/
public class GroupMessageStatusChangeProcessor implements ObjectProcessor<GroupMessageStatusChange> {
private static final ConnectionManager connectionManager = ConnectionManager.getInstance();
private static final PersistenceManager persistenceManager = PersistenceManager.getInstance();
private static final Logger logger = EnvoyLog.getLogger(MessageStatusChangeProcessor.class);
@Override
public void process(GroupMessageStatusChange statusChange, long socketID, ObjectWriteProxy writeProxy) {
GroupMessage gmsg = (GroupMessage) persistenceManager.getMessageByID(statusChange.getID());
// Any other status than READ is not supposed to be sent to the server
if (statusChange.get() != MessageStatus.READ) {
logger.log(Level.WARNING, "Invalid " + statusChange);
return;
}
// Apply the status change
gmsg.getMemberMessageStatus().replace(statusChange.getMemberID(), statusChange.get());
gmsg.setLastStatusChangeDate(LocalDateTime.now());
// Notifying the other members about the status change
final var userID = connectionManager.getUserIDBySocketID(socketID);
gmsg.getMemberMessageStatus()
.keySet()
.stream()
.filter(k -> userID != k)
.filter(connectionManager::isOnline)
.forEach(k -> writeProxy.write(connectionManager.getSocketID(k), statusChange));
// Increment overall status to READ if necessary
if (Collections.min(gmsg.getMemberMessageStatus().values()) == READ) {
gmsg.read();
// Notify online members about the status change
writeProxy.writeToOnlineContacts(gmsg.getRecipient().getContacts(),
new MessageStatusChange(gmsg.getID(), gmsg.getStatus(), LocalDateTime.now()));
}
persistenceManager.updateMessage(gmsg);
}
@Override
public Class<GroupMessageStatusChange> getInputClass() { return GroupMessageStatusChange.class; }
}

View File

@ -0,0 +1,53 @@
package envoy.server.processors;
import envoy.event.GroupResize;
import envoy.server.data.Contact;
import envoy.server.data.PersistenceManager;
import envoy.server.net.ConnectionManager;
import envoy.server.net.ObjectWriteProxy;
/**
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>GroupResizeProcessor.java</strong><br>
* Created: <strong>03.04.2020</strong><br>
*
* @author Maximilian K&auml;fer
* @since Envoy Server Standalone v0.1-beta
*/
public class GroupResizeProcessor implements ObjectProcessor<GroupResize> {
private static final PersistenceManager persistenceManager = PersistenceManager.getInstance();
private static final ConnectionManager connectionManager = ConnectionManager.getInstance();
@Override
public void process(GroupResize groupResize, long socketID, ObjectWriteProxy writeProxy) {
// Acquire the group to resize from the database
var group = persistenceManager.getGroupByID(groupResize.getGroupID());
// Perform the desired operation
switch (groupResize.getOperation()) {
case ADD:
group.getContacts().add(persistenceManager.getUserByID(groupResize.get().getID()));
break;
case REMOVE:
group.getContacts().remove(persistenceManager.getUserByID(groupResize.get().getID()));
break;
}
// Update the group in the database
persistenceManager.updateContact(group);
// Send the updated group to all of its members
var commonGroup = group.toCommon();
group.getContacts()
.stream()
.map(Contact::getID)
.filter(connectionManager::isOnline)
.map(connectionManager::getSocketID)
.forEach(memberSocketID -> writeProxy.write(memberSocketID, commonGroup));
}
@Override
public Class<GroupResize> getInputClass() { return GroupResize.class; }
}

View File

@ -0,0 +1,50 @@
package envoy.server.processors;
import java.io.IOException;
import envoy.data.IDGenerator;
import envoy.event.IDGeneratorRequest;
import envoy.server.data.ConfigItem;
import envoy.server.data.PersistenceManager;
import envoy.server.net.ObjectWriteProxy;
/**
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>IDGeneratorRequestProcessor.java</strong><br>
* Created: <strong>28 Jan 2020</strong><br>
*
* @author Kai S. K. Engelbart
* @author Maximilian K&auml;fer
* @since Envoy Server Standalone v0.1-alpha
*/
public class IDGeneratorRequestProcessor implements ObjectProcessor<IDGeneratorRequest> {
private static final long ID_RANGE = 200;
@Override
public Class<IDGeneratorRequest> getInputClass() { return IDGeneratorRequest.class; }
@Override
public void process(IDGeneratorRequest input, long socketID, ObjectWriteProxy writeProxy) throws IOException {
writeProxy.write(socketID, createIDGenerator());
}
/**
* @return a new IDGenerator
* @since Envoy Server Standalone v0.1-beta
*/
public static IDGenerator createIDGenerator() { return createIDGenerator(ID_RANGE); }
/**
* @param range of IDs used by the new IDGenerator
* @return a new IDGenerator with a specific range of IDs
* @since Envoy Server Standalone v0.1-beta
*/
public static IDGenerator createIDGenerator(long range) {
ConfigItem currentID = PersistenceManager.getInstance().getConfigItemByID("currentMessageId");
IDGenerator generator = new IDGenerator(Integer.parseInt(currentID.getValue()), range);
currentID.setValue(String.valueOf(Integer.parseInt(currentID.getValue()) + range));
PersistenceManager.getInstance().updateConfigItem(currentID);
return generator;
}
}

View File

@ -0,0 +1,189 @@
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.LocalDateTime;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.logging.Logger;
import javax.persistence.NoResultException;
import envoy.data.LoginCredentials;
import envoy.event.GroupMessageStatusChange;
import envoy.event.HandshakeRejection;
import envoy.event.MessageStatusChange;
import envoy.server.data.GroupMessage;
import envoy.server.data.PersistenceManager;
import envoy.server.data.User;
import envoy.server.net.ConnectionManager;
import envoy.server.net.ObjectWriteProxy;
import envoy.server.util.PasswordUtil;
import envoy.server.util.VersionUtil;
import envoy.util.Bounds;
import envoy.util.EnvoyLog;
/**
* This {@link ObjectProcessor} handles {@link LoginCredentials}.<br>
* <br>
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>LoginCredentialProcessor.java</strong><br>
* Created: <strong>30.12.2019</strong><br>
*
* @author Kai S. K. Engelbart
* @author Maximilian K&auml;fer
* @since Envoy Server Standalone v0.1-alpha
*/
public final class LoginCredentialProcessor implements ObjectProcessor<LoginCredentials> {
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-independant notifications
UserStatusChangeProcessor.setWriteProxy(writeProxy);
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());
// Checking if user is already online
if (connectionManager.isOnline(user.getID())) {
logger.warning(user + " is already online!");
writeProxy.write(socketID, new HandshakeRejection(INTERNAL_ERROR));
return;
}
// Evaluating the correctness of the password hash
if (!PasswordUtil.validate(credentials.getPassword(), user.getPasswordHash())) {
logger.info(user + " has entered the wrong password.");
writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER));
return;
}
} catch (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 {
// Checking that no user already has this identifier
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 (NoResultException e) {
// Creation of a new user
user = new User();
user.setName(credentials.getIdentifier());
user.setLastSeen(LocalDateTime.now());
user.setStatus(ONLINE);
user.setPasswordHash(PasswordUtil.hash(credentials.getPassword()));
user.setContacts(new HashSet<>());
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
user.setStatus(ONLINE);
UserStatusChangeProcessor.updateUserStatus(user);
// Complete the handshake
writeProxy.write(socketID, user.toCommon());
final var pendingMessages = PersistenceManager.getInstance().getPendingMessages(user);
pendingMessages.removeIf(GroupMessage.class::isInstance);
logger.fine("Sending " + pendingMessages.size() + " pending messages to " + user + "...");
for (var msg : pendingMessages) {
final var msgCommon = msg.toCommon();
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));
}
List<GroupMessage> pendingGroupMessages = PersistenceManager.getInstance().getPendingGroupMessages(user);
logger.fine("Sending " + pendingGroupMessages.size() + " pending group messages to " + user + "...");
for (var gmsg : pendingGroupMessages) {
final var gmsgCommon = gmsg.toCommon();
// Deliver the message to the user if he hasn't received it yet
if (gmsg.getMemberMessageStatus().get(user.getID()) == SENT) {
gmsg.getMemberMessageStatus().replace(user.getID(), RECEIVED);
gmsg.setLastStatusChangeDate(LocalDateTime.now());
writeProxy.write(socketID, gmsgCommon);
// Notify all online group members about the status change
writeProxy.writeToOnlineContacts(gmsg.getRecipient().getContacts(),
new GroupMessageStatusChange(gmsg.getID(), RECEIVED, LocalDateTime
.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(), LocalDateTime.now()));
}
PersistenceManager.getInstance().updateMessage(gmsg);
} 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() == SENT && user.getLastSeen().isBefore(gmsg.getCreationDate())
|| gmsg.getStatus() == RECEIVED && user.getLastSeen().isBefore(gmsg.getReceivedDate()))
writeProxy.write(socketID, new MessageStatusChange(gmsgCommon));
}
}
}
@Override
public Class<LoginCredentials> getInputClass() { return LoginCredentials.class; }
}

View File

@ -0,0 +1,64 @@
package envoy.server.processors;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.persistence.EntityExistsException;
import envoy.data.Message;
import envoy.event.MessageStatusChange;
import envoy.server.data.PersistenceManager;
import envoy.server.net.ConnectionManager;
import envoy.server.net.ObjectWriteProxy;
import envoy.util.EnvoyLog;
/**
* This {@link ObjectProcessor} handles incoming {@link Message}s.
* <p>
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>MessageProcessor.java</strong><br>
* Created: <strong>30.12.2019</strong><br>
*
* @author Kai S. K. Engelbart
* @author Maximilian K&auml;fer
* @since Envoy Server Standalone v0.1-alpha
*/
public class MessageProcessor implements ObjectProcessor<Message> {
private static final PersistenceManager persistenceManager = PersistenceManager.getInstance();
private static final ConnectionManager connectionManager = ConnectionManager.getInstance();
private static final Logger logger = EnvoyLog.getLogger(MessageProcessor.class);
@Override
public void process(Message message, long socketID, ObjectWriteProxy writeProxy) {
message.nextStatus();
// Convert to server message
final var serverMessage = new envoy.server.data.Message(message);
try {
// Persist the message
persistenceManager.addMessage(serverMessage);
// Send the message to the recipient if online
if (connectionManager.isOnline(message.getRecipientID())) {
writeProxy.write(connectionManager.getSocketID(message.getRecipientID()), message);
// Increment status
message.nextStatus();
serverMessage.received();
persistenceManager.updateMessage(serverMessage);
// Notify the sender about the delivery
// Note that the exact time stamp might differ slightly
writeProxy.write(socketID, new MessageStatusChange(message));
}
} catch (EntityExistsException e) {
logger.log(Level.WARNING, "Received " + message + " with an ID that already exists!");
}
}
@Override
public Class<Message> getInputClass() { return Message.class; }
}

View File

@ -0,0 +1,48 @@
package envoy.server.processors;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import envoy.data.Message.MessageStatus;
import envoy.event.MessageStatusChange;
import envoy.server.data.PersistenceManager;
import envoy.server.net.ConnectionManager;
import envoy.server.net.ObjectWriteProxy;
import envoy.util.EnvoyLog;
/**
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>MessageStatusChangeProcessor.java</strong><br>
* Created: <strong>10 Jan 2020</strong><br>
*
* @author Leon Hofmeister
* @since Envoy Server Standalone v0.1-alpha
*/
public class MessageStatusChangeProcessor implements ObjectProcessor<MessageStatusChange> {
private final ConnectionManager connectionManager = ConnectionManager.getInstance();
private final PersistenceManager persistenceManager = PersistenceManager.getInstance();
private final Logger logger = EnvoyLog.getLogger(MessageStatusChangeProcessor.class);
@Override
public void process(MessageStatusChange statusChange, long socketID, ObjectWriteProxy writeProxy) throws IOException {
// Any other status than READ is not supposed to be sent to the server
if (statusChange.get() != MessageStatus.READ) {
logger.log(Level.WARNING, "Invalid " + statusChange);
return;
}
final var msg = persistenceManager.getMessageByID(statusChange.getID());
msg.read();
persistenceManager.updateMessage(msg);
// Notifies the sender of the message about the status-update to READ
final long senderID = msg.getSender().getID();
if (connectionManager.isOnline(senderID)) writeProxy.write(connectionManager.getSocketID(senderID), statusChange);
}
@Override
public Class<MessageStatusChange> getInputClass() { return MessageStatusChange.class; }
}

View File

@ -0,0 +1,34 @@
package envoy.server.processors;
import java.io.IOException;
import envoy.event.NameChange;
import envoy.server.data.Contact;
import envoy.server.data.PersistenceManager;
import envoy.server.net.ObjectWriteProxy;
/**
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>NameChangeProcessor.java</strong><br>
* Created: <strong>26 Mar 2020</strong><br>
*
* @author Leon Hofmeister
* @since Envoy Server Standalone v0.1-beta
*/
public class NameChangeProcessor implements ObjectProcessor<NameChange> {
private static final PersistenceManager persistenceManager = PersistenceManager.getInstance();
@Override
public void process(NameChange nameChange, long socketID, ObjectWriteProxy writeProxy) throws IOException {
Contact toUpdate = persistenceManager.getContactByID(nameChange.getID());
toUpdate.setName(nameChange.get());
persistenceManager.updateContact(toUpdate);
// Notify online contacts of the name change
writeProxy.writeToOnlineContacts(toUpdate.getContacts(), nameChange);
}
@Override
public Class<NameChange> getInputClass() { return NameChange.class; }
}

View File

@ -0,0 +1,35 @@
package envoy.server.processors;
import java.io.IOException;
import envoy.server.net.ObjectWriteProxy;
/**
* This interface defines methods for processing objects of a specific
* type incoming from a client.<br>
* <br>
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>ObjectProcessor.java</strong><br>
* Created: <strong>30.12.2019</strong><br>
*
* @author Kai S. K. Engelbart
* @param <T> type of the request object
* @since Envoy Server Standalone v0.1-alpha
*/
public interface ObjectProcessor<T> {
/**
* @return the class of the request object
* @since Envoy Server Standalone v0.1-alpha
*/
Class<T> getInputClass();
/**
* @param input the request object
* @param socketID the ID of the socket from which the object was received
* @param writeProxy the object that allows writing to a client
* @throws IOException if something went wrong during processing
* @since Envoy Server Standalone v0.1-alpha
*/
void process(T input, long socketID, ObjectWriteProxy writeProxy) throws IOException;
}

View File

@ -0,0 +1,77 @@
package envoy.server.processors;
import java.util.logging.Logger;
import envoy.data.User.UserStatus;
import envoy.event.UserStatusChange;
import envoy.server.data.PersistenceManager;
import envoy.server.data.User;
import envoy.server.net.ObjectWriteProxy;
import envoy.util.EnvoyLog;
/**
* This processor handles incoming {@link UserStatusChange}.
* <p>
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>UserStatusChangeProcessor.java</strong><br>
* Created: <strong>1 Feb 2020</strong><br>
*
* @author Leon Hofmeister
* @since Envoy Server Standalone v0.1-alpha
*/
public class UserStatusChangeProcessor implements ObjectProcessor<UserStatusChange> {
private static ObjectWriteProxy writeProxy;
private static final PersistenceManager persistenceManager = PersistenceManager.getInstance();
private static final Logger logger = EnvoyLog.getLogger(UserStatusChangeProcessor.class);
@Override
public Class<UserStatusChange> getInputClass() { return UserStatusChange.class; }
@Override
public void process(UserStatusChange input, long socketID, ObjectWriteProxy writeProxy) {
// new status should not equal old status
if (input.get().equals(persistenceManager.getUserByID(input.getID()).getStatus())) {
logger.warning("Received an unnecessary UserStatusChange");
return;
}
updateUserStatus(input);
}
/**
* Sets the {@link UserStatus} for a given user. Both offline contacts and
* currently online contacts are notified.
*
* @param user the {@link UserStatusChange} that signals the change
* @since Envoy Server Standalone v0.1-alpha
*/
public static void updateUserStatus(User user) {
// Handling for newly logged in clients
persistenceManager.updateContact(user);
// Handling for contacts that are already online
writeProxy.writeToOnlineContacts(user.getContacts(), new UserStatusChange(user.getID(), user.getStatus()));
}
/**
* @param evt the {@link UserStatusChange}
* @since Envoy Server Standalone v0.1-alpha
*/
public static void updateUserStatus(UserStatusChange evt) { updateUserStatus(persistenceManager.getUserByID(evt.getID())); }
/**
* This method is only called by the LoginCredentialProcessor because every
* user needs to login (open a socket) before changing his status.
* Needed to ensure propagation of events because an uninitialized writeProxy
* would cause problems.
*
* @param writeProxy the writeProxy that is used to send objects back to clients
* @since Envoy Server Standalone v0.1-alpha
*/
public static void setWriteProxy(ObjectWriteProxy writeProxy) { UserStatusChangeProcessor.writeProxy = writeProxy; }
// TODO may cause an problem if two clients log in at the same time.
// Change Needed.
}

View File

@ -0,0 +1,10 @@
/**
* This package contains all classes that process data received from client
* connections.
*
* @author Kai S. K. Engelbart
* @author Leon Hofmeister
* @author Maximilian K&auml;fer
* @since Envoy v0.1-alpha
*/
package envoy.server.processors;

View File

@ -0,0 +1,98 @@
package envoy.server.util;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
/**
* Provides a password hashing and comparison mechanism using the
* {@code PBKDF2WithHmacSHA1} algorithm.
* <p>
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>PasswordUtil.java</strong><br>
* Created: <strong>10.07.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Server Standalone v0.1-beta
*/
public class PasswordUtil {
private static final int ITERATIONS = 1000;
private static final int KEY_LENGTH = 64 * 8;
private static final String SALT_ALGORITHM = "SHA1PRNG";
private static final String SECRET_KEY_ALGORITHM = "PBKDF2WithHmacSHA1";
private PasswordUtil() {}
/**
* Validates a password against a stored password hash
*
* @param current the password to validate
* @param stored the hash to validate against
* @return {@code true} if the password is correct
* @since Envoy Server Standalone v0.1-beta
*/
public static boolean validate(String current, String stored) {
try {
String[] parts = stored.split(":");
int iterations = Integer.parseInt(parts[0]);
byte[] salt = fromHex(parts[1]);
byte[] hash = fromHex(parts[2]);
var spec = new PBEKeySpec(current.toCharArray(), salt, iterations, KEY_LENGTH);
var skf = SecretKeyFactory.getInstance(SECRET_KEY_ALGORITHM);
byte[] testHash = skf.generateSecret(spec).getEncoded();
int diff = hash.length ^ testHash.length;
for (int i = 0; i < hash.length && i < testHash.length; ++i)
diff |= hash[i] ^ testHash[i];
return diff == 0;
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
/**
* Creates a parameterized salted password hash.
*
* @param password the password to hash
* @return a result string in the form of {@code iterations:salt:hash}
* @since Envoy Server Standalone v0.1-beta
*/
public static String hash(String password) {
try {
byte[] salt = salt();
var spec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, KEY_LENGTH);
var skf = SecretKeyFactory.getInstance(SECRET_KEY_ALGORITHM);
byte[] hash = skf.generateSecret(spec).getEncoded();
return ITERATIONS + ":" + toHex(salt) + ":" + toHex(hash);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
private static byte[] salt() throws NoSuchAlgorithmException {
SecureRandom sr = SecureRandom.getInstance(SALT_ALGORITHM);
byte[] salt = new byte[16];
sr.nextBytes(salt);
return salt;
}
private static String toHex(byte[] bytes) {
String hex = new BigInteger(1, bytes).toString(16);
int padding = bytes.length * 2 - hex.length();
return padding > 0 ? String.format("%0" + padding + "d", 0) + hex : hex;
}
private static byte[] fromHex(String hex) {
byte[] bytes = new byte[hex.length() / 2];
for (int i = 0; i < bytes.length; ++i)
bytes[i] = (byte) Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16);
return bytes;
}
}

View File

@ -0,0 +1,94 @@
package envoy.server.util;
import java.util.regex.Pattern;
/**
* Implements a comparison algorithm between Envoy versions and defines minimal
* and maximal client versions compatible with this server.
* <p>
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>VersionUtil.java</strong><br>
* Created: <strong>23.06.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Server Standalone v0.1-beta
*/
public class VersionUtil {
/**
* The minimal client version compatible with this server.
*
* @since Envoy Server Standalone v0.1-beta
*/
public static final String MIN_CLIENT_VERSION = "0.1-beta";
/**
* The maximal client version compatible with this server.
*
* @since Envoy Server Standalone v0.1-beta
*/
public static final String MAX_CLIENT_VERSION = "0.1-beta";
private static final Pattern versionPattern = Pattern.compile("(?<major>\\d).(?<minor>\\d)(?:-(?<suffix>\\w+))?");
private VersionUtil() {}
/**
* Parses an Envoy Client version string and checks whether that version is
* compatible with this server.
*
* @param version the version string to parse
* @return {@code true} if the given version is compatible with this server
* @since Envoy Server Standalone v0.1-beta
*/
public static boolean verifyCompatibility(String version) {
final var currentMatcher = versionPattern.matcher(version);
if (!currentMatcher.matches()) return false;
final var minMatcher = versionPattern.matcher(MIN_CLIENT_VERSION);
final var maxMatcher = versionPattern.matcher(MAX_CLIENT_VERSION);
if (!minMatcher.matches() || !maxMatcher.matches()) throw new RuntimeException("Invalid min or max client version configured!");
// Compare suffixes
{
final var currentSuffix = convertSuffix(currentMatcher.group("suffix"));
final var minSuffix = convertSuffix(minMatcher.group("suffix"));
final var maxSuffix = convertSuffix(maxMatcher.group("suffix"));
if (currentSuffix < minSuffix || currentSuffix > maxSuffix) return false;
}
// Compare major
{
final var currentMajor = Integer.parseInt(currentMatcher.group("major"));
final var minMajor = Integer.parseInt(minMatcher.group("major"));
final var maxMajor = Integer.parseInt(maxMatcher.group("major"));
if (currentMajor < minMajor || currentMajor > maxMajor) return false;
}
// Compare minor
{
final var currentMinor = Integer.parseInt(currentMatcher.group("minor"));
final var minMinor = Integer.parseInt(minMatcher.group("minor"));
final var maxMinor = Integer.parseInt(maxMatcher.group("minor"));
if (currentMinor < minMinor || currentMinor > maxMinor) return false;
}
return true;
}
private static int convertSuffix(String suffix) {
switch (suffix == null ? "" : suffix) {
case "alpha":
return 0;
case "beta":
return 1;
default:
return 2;
}
}
}

View File

@ -0,0 +1,11 @@
/**
* This package contains utility classes used in Envoy Server.
* <p>
* Project: <strong>envoy-server-standalone</strong><br>
* File: <strong>package-info.java</strong><br>
* Created: <strong>23.06.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Server Standalone v0.1-beta
*/
package envoy.server.util;

View File

@ -0,0 +1,20 @@
/**
* This module contains all classes defining the server application of the Envoy
* project.
*
* @author Kai S. K. Engelbart
* @author Leon Hofmeister
* @author Maximilian K&auml;fer
* @since Envoy Server Standalone v0.1-beta
*/
module envoy.server {
opens envoy.server.data;
requires transitive envoy.common;
requires transitive java.nio.server;
requires transitive java.persistence;
requires transitive java.sql;
requires transitive org.hibernate.orm.core;
}

View File

@ -0,0 +1,27 @@
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
version="2.1">
<persistence-unit name="envoy"
transaction-type="RESOURCE_LOCAL">
<properties>
<!-- JDBC -->
<property name="javax.persistence.jdbc.driver"
value="org.postgresql.Driver" />
<property name="javax.persistence.jdbc.url"
value="jdbc:postgresql://localhost/envoy" />
<property name="javax.persistence.jdbc.user" value="envoy" />
<property name="javax.persistence.jdbc.password"
value="envoy" />
<!-- Hibernate -->
<property name="hibernate.dialect"
value="org.hibernate.dialect.PostgreSQL95Dialect" />
<property name="hibernate.hbm2ddl.auto" value="update" />
</properties>
</persistence-unit>
</persistence>