From d34a4072847c3ece267fa2ac9d864a1659561b0b Mon Sep 17 00:00:00 2001
From: Andrew Lalis
Date: Sat, 25 Sep 2021 13:08:14 +0200
Subject: [PATCH 1/5] Updated user login system to use unique usernames, login
and logout stuff. Client not yet updated to new authentication flow.
---
.../nl/andrewl/concord_core/msg/Message.java | 16 +-
...geType.java => MessageTypeSerializer.java} | 14 +-
.../concord_core/msg/MessageUtils.java | 8 +-
.../andrewl/concord_core/msg/Serializer.java | 46 +++--
.../msg/types/client_setup/ClientLogin.java | 9 +
...istration.java => ClientRegistration.java} | 7 +-
.../client_setup/ClientSessionResume.java | 9 +
.../types/client_setup/Identification.java | 11 --
.../client_setup/RegistrationStatus.java | 15 ++
.../util/ChainedDataOutputStream.java | 7 +-
.../util/ExtendedDataInputStream.java | 8 +-
server/pom.xml | 6 +
server/src/main/java/module-info.java | 1 +
.../andrewl/concord_server/ConcordServer.java | 2 +
.../cli/command/ListClientsCommand.java | 2 +-
.../client/AuthenticationService.java | 114 +++++++++++
.../concord_server/client/ClientManager.java | 181 ++++++++++--------
.../concord_server/client/ClientThread.java | 20 +-
.../concord_server/config/ServerConfig.java | 2 +
19 files changed, 344 insertions(+), 134 deletions(-)
rename core/src/main/java/nl/andrewl/concord_core/msg/{MessageType.java => MessageTypeSerializer.java} (87%)
create mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/ClientLogin.java
rename core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/{Registration.java => ClientRegistration.java} (60%)
create mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/ClientSessionResume.java
delete mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/Identification.java
create mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/RegistrationStatus.java
create mode 100644 server/src/main/java/nl/andrewl/concord_server/client/AuthenticationService.java
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/Message.java b/core/src/main/java/nl/andrewl/concord_core/msg/Message.java
index 3be10cb..2c83a71 100644
--- a/core/src/main/java/nl/andrewl/concord_core/msg/Message.java
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/Message.java
@@ -8,12 +8,22 @@ package nl.andrewl.concord_core.msg;
*
*/
public interface Message {
+ /**
+ * Convenience method to get the serializer for this message's type, using
+ * the static auto-generated set of serializers.
+ * @param The message type.
+ * @return The serializer to use to read and write messages of this type.
+ */
@SuppressWarnings("unchecked")
- default MessageType getType() {
- return MessageType.get((Class) this.getClass());
+ default MessageTypeSerializer getTypeSerializer() {
+ return MessageTypeSerializer.get((Class) this.getClass());
}
+ /**
+ * Convenience method to determine the size of this message in bytes.
+ * @return The size of this message, in bytes.
+ */
default int byteSize() {
- return getType().byteSizeFunction().apply(this);
+ return getTypeSerializer().byteSizeFunction().apply(this);
}
}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/MessageType.java b/core/src/main/java/nl/andrewl/concord_core/msg/MessageTypeSerializer.java
similarity index 87%
rename from core/src/main/java/nl/andrewl/concord_core/msg/MessageType.java
rename to core/src/main/java/nl/andrewl/concord_core/msg/MessageTypeSerializer.java
index c70bc14..ca961b7 100644
--- a/core/src/main/java/nl/andrewl/concord_core/msg/MessageType.java
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/MessageTypeSerializer.java
@@ -19,24 +19,24 @@ import java.util.function.Function;
* @param reader A reader that can read messages from an input stream.
* @param writer A writer that write messages from an input stream.
*/
-public record MessageType(
+public record MessageTypeSerializer(
Class messageClass,
Function byteSizeFunction,
MessageReader reader,
MessageWriter writer
) {
- private static final Map, MessageType>> generatedMessageTypes = new HashMap<>();
+ private static final Map, MessageTypeSerializer>> generatedMessageTypes = new HashMap<>();
/**
- * Gets the {@link MessageType} instance for a given message class, and
+ * Gets the {@link MessageTypeSerializer} instance for a given message class, and
* generates a new implementation if none exists yet.
* @param messageClass The class of the message to get a type for.
* @param The type of the message.
* @return The message type.
*/
@SuppressWarnings("unchecked")
- public static MessageType get(Class messageClass) {
- return (MessageType) generatedMessageTypes.computeIfAbsent(messageClass, c -> generateForRecord((Class) c));
+ public static MessageTypeSerializer get(Class messageClass) {
+ return (MessageTypeSerializer) generatedMessageTypes.computeIfAbsent(messageClass, c -> generateForRecord((Class) c));
}
/**
@@ -49,7 +49,7 @@ public record MessageType(
* @param The type of the message.
* @return A message type instance.
*/
- public static MessageType generateForRecord(Class messageTypeClass) {
+ public static MessageTypeSerializer generateForRecord(Class messageTypeClass) {
RecordComponent[] components = messageTypeClass.getRecordComponents();
Constructor constructor;
try {
@@ -58,7 +58,7 @@ public record MessageType(
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException(e);
}
- return new MessageType<>(
+ return new MessageTypeSerializer<>(
messageTypeClass,
generateByteSizeFunction(components),
generateReader(constructor),
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/MessageUtils.java b/core/src/main/java/nl/andrewl/concord_core/msg/MessageUtils.java
index b813e14..61a3f87 100644
--- a/core/src/main/java/nl/andrewl/concord_core/msg/MessageUtils.java
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/MessageUtils.java
@@ -35,10 +35,14 @@ public class MessageUtils {
return size;
}
+ public static int getByteSize(Message msg) {
+ return 1 + (msg == null ? 0 : msg.byteSize());
+ }
+
public static int getByteSize(T[] items) {
int count = Integer.BYTES;
for (var item : items) {
- count += item.byteSize();
+ count += getByteSize(items);
}
return count;
}
@@ -59,7 +63,7 @@ public class MessageUtils {
} else if (o.getClass().isArray() && Message.class.isAssignableFrom(o.getClass().getComponentType())) {
return getByteSize((Message[]) o);
} else if (o instanceof Message) {
- return ((Message) o).byteSize();
+ return getByteSize((Message) o);
} else {
throw new IllegalArgumentException("Unsupported object type: " + o.getClass().getSimpleName());
}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/Serializer.java b/core/src/main/java/nl/andrewl/concord_core/msg/Serializer.java
index f7b8945..4030647 100644
--- a/core/src/main/java/nl/andrewl/concord_core/msg/Serializer.java
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/Serializer.java
@@ -3,15 +3,13 @@ package nl.andrewl.concord_core.msg;
import nl.andrewl.concord_core.msg.types.Error;
import nl.andrewl.concord_core.msg.types.ServerMetaData;
import nl.andrewl.concord_core.msg.types.ServerUsers;
+import nl.andrewl.concord_core.msg.types.UserData;
import nl.andrewl.concord_core.msg.types.channel.CreateThread;
import nl.andrewl.concord_core.msg.types.channel.MoveToChannel;
import nl.andrewl.concord_core.msg.types.chat.Chat;
import nl.andrewl.concord_core.msg.types.chat.ChatHistoryRequest;
import nl.andrewl.concord_core.msg.types.chat.ChatHistoryResponse;
-import nl.andrewl.concord_core.msg.types.client_setup.Identification;
-import nl.andrewl.concord_core.msg.types.client_setup.KeyData;
-import nl.andrewl.concord_core.msg.types.client_setup.Registration;
-import nl.andrewl.concord_core.msg.types.client_setup.ServerWelcome;
+import nl.andrewl.concord_core.msg.types.client_setup.*;
import nl.andrewl.concord_core.util.ChainedDataOutputStream;
import nl.andrewl.concord_core.util.ExtendedDataInputStream;
@@ -20,6 +18,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
/**
@@ -32,31 +31,36 @@ public class Serializer {
* The mapping which defines each supported message type and the byte value
* used to identify it when reading and writing messages.
*/
- private final Map> messageTypes = new HashMap<>();
+ private final Map> messageTypes = new HashMap<>();
/**
* An inverse of {@link Serializer#messageTypes} which is used to look up a
* message's byte value when you know the class of the message.
*/
- private final Map, Byte> inverseMessageTypes = new HashMap<>();
+ private final Map, Byte> inverseMessageTypes = new HashMap<>();
/**
* Constructs a new serializer instance, with a standard set of supported
* message types.
*/
public Serializer() {
- registerType(0, Identification.class);
- registerType(1, ServerWelcome.class);
- registerType(2, Chat.class);
- registerType(3, MoveToChannel.class);
- registerType(4, ChatHistoryRequest.class);
- registerType(5, ChatHistoryResponse.class);
- registerType(6, Registration.class);
- registerType(7, ServerUsers.class);
- registerType(8, ServerMetaData.class);
- registerType(9, Error.class);
- registerType(10, CreateThread.class);
- registerType(11, KeyData.class);
+ List> messageClasses = List.of(
+ // Utility messages.
+ Error.class,
+ UserData.class,
+ ServerUsers.class,
+ // Client setup messages.
+ KeyData.class, ClientRegistration.class, ClientLogin.class, ClientSessionResume.class,
+ RegistrationStatus.class, ServerWelcome.class, ServerMetaData.class,
+ // Chat messages.
+ Chat.class, ChatHistoryRequest.class, ChatHistoryResponse.class,
+ // Channel messages.
+ MoveToChannel.class,
+ CreateThread.class
+ );
+ for (int id = 0; id < messageClasses.size(); id++) {
+ registerType(id, messageClasses.get(id));
+ }
}
/**
@@ -67,7 +71,7 @@ public class Serializer {
* @param messageClass The type of message associated with the given id.
*/
private synchronized void registerType(int id, Class messageClass) {
- MessageType type = MessageType.get(messageClass);
+ MessageTypeSerializer type = MessageTypeSerializer.get(messageClass);
messageTypes.put((byte) id, type);
inverseMessageTypes.put(type, (byte) id);
}
@@ -104,12 +108,12 @@ public class Serializer {
*/
public void writeMessage(Message msg, OutputStream o) throws IOException {
DataOutputStream d = new DataOutputStream(o);
- Byte typeId = inverseMessageTypes.get(msg.getType());
+ Byte typeId = inverseMessageTypes.get(msg.getTypeSerializer());
if (typeId == null) {
throw new IOException("Unsupported message type: " + msg.getClass().getSimpleName());
}
d.writeByte(typeId);
- msg.getType().writer().write(msg, new ChainedDataOutputStream(d));
+ msg.getTypeSerializer().writer().write(msg, new ChainedDataOutputStream(d));
d.flush();
}
}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/ClientLogin.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/ClientLogin.java
new file mode 100644
index 0000000..6455aff
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/ClientLogin.java
@@ -0,0 +1,9 @@
+package nl.andrewl.concord_core.msg.types.client_setup;
+
+import nl.andrewl.concord_core.msg.Message;
+
+/**
+ * This message is sent by clients to log into a server that they have already
+ * registered with, but don't have a valid session token for.
+ */
+public record ClientLogin(String username, String password) implements Message {}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/Registration.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/ClientRegistration.java
similarity index 60%
rename from core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/Registration.java
rename to core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/ClientRegistration.java
index 4421dc5..a8a2f74 100644
--- a/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/Registration.java
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/ClientRegistration.java
@@ -6,4 +6,9 @@ import nl.andrewl.concord_core.msg.Message;
* The data that new users should send to a server in order to register in that
* server.
*/
-public record Registration (String username, String password) implements Message {}
+public record ClientRegistration(
+ String name,
+ String description,
+ String username,
+ String password
+) implements Message {}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/ClientSessionResume.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/ClientSessionResume.java
new file mode 100644
index 0000000..ee9881e
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/ClientSessionResume.java
@@ -0,0 +1,9 @@
+package nl.andrewl.concord_core.msg.types.client_setup;
+
+import nl.andrewl.concord_core.msg.Message;
+
+/**
+ * This message is sent by the client to log into a server using a session token
+ * instead of a username/password combination.
+ */
+public record ClientSessionResume(String sessionToken) implements Message {}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/Identification.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/Identification.java
deleted file mode 100644
index ed2d237..0000000
--- a/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/Identification.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package nl.andrewl.concord_core.msg.types.client_setup;
-
-import nl.andrewl.concord_core.msg.Message;
-
-/**
- * This message is sent from the client to a server, to provide identification
- * information about the client to the server when the connection is started.
- *
- * @param nickname
- */
-public record Identification(String nickname, String sessionToken) implements Message {}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/RegistrationStatus.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/RegistrationStatus.java
new file mode 100644
index 0000000..45d8159
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/RegistrationStatus.java
@@ -0,0 +1,15 @@
+package nl.andrewl.concord_core.msg.types.client_setup;
+
+import nl.andrewl.concord_core.msg.Message;
+
+/**
+ * A response from the server which indicates the current status of the client's
+ * registration request.
+ */
+public record RegistrationStatus (Type type) implements Message {
+ public enum Type {PENDING, ACCEPTED, REJECTED}
+
+ public static RegistrationStatus pending() {
+ return new RegistrationStatus(Type.PENDING);
+ }
+}
diff --git a/core/src/main/java/nl/andrewl/concord_core/util/ChainedDataOutputStream.java b/core/src/main/java/nl/andrewl/concord_core/util/ChainedDataOutputStream.java
index 48e0687..38df084 100644
--- a/core/src/main/java/nl/andrewl/concord_core/util/ChainedDataOutputStream.java
+++ b/core/src/main/java/nl/andrewl/concord_core/util/ChainedDataOutputStream.java
@@ -63,13 +63,16 @@ public class ChainedDataOutputStream {
public ChainedDataOutputStream writeArray(T[] array) throws IOException {
this.out.writeInt(array.length);
for (var item : array) {
- item.getType().writer().write(item, this);
+ writeMessage(item);
}
return this;
}
public ChainedDataOutputStream writeMessage(Message msg) throws IOException {
- msg.getType().writer().write(msg, this);
+ this.out.writeBoolean(msg != null);
+ if (msg != null) {
+ msg.getTypeSerializer().writer().write(msg, this);
+ }
return this;
}
diff --git a/core/src/main/java/nl/andrewl/concord_core/util/ExtendedDataInputStream.java b/core/src/main/java/nl/andrewl/concord_core/util/ExtendedDataInputStream.java
index aada77f..3470b36 100644
--- a/core/src/main/java/nl/andrewl/concord_core/util/ExtendedDataInputStream.java
+++ b/core/src/main/java/nl/andrewl/concord_core/util/ExtendedDataInputStream.java
@@ -1,7 +1,7 @@
package nl.andrewl.concord_core.util;
import nl.andrewl.concord_core.msg.Message;
-import nl.andrewl.concord_core.msg.MessageType;
+import nl.andrewl.concord_core.msg.MessageTypeSerializer;
import java.io.DataInputStream;
import java.io.IOException;
@@ -45,7 +45,7 @@ public class ExtendedDataInputStream extends DataInputStream {
}
@SuppressWarnings("unchecked")
- public T[] readArray(MessageType type) throws IOException {
+ public T[] readArray(MessageTypeSerializer type) throws IOException {
int length = super.readInt();
T[] array = (T[]) Array.newInstance(type.messageClass(), length);
for (int i = 0; i < length; i++) {
@@ -76,10 +76,10 @@ public class ExtendedDataInputStream extends DataInputStream {
int length = this.readInt();
return this.readNBytes(length);
} else if (type.isArray() && Message.class.isAssignableFrom(type.getComponentType())) {
- var messageType = MessageType.get((Class extends Message>) type.getComponentType());
+ var messageType = MessageTypeSerializer.get((Class extends Message>) type.getComponentType());
return this.readArray(messageType);
} else if (Message.class.isAssignableFrom(type)) {
- var messageType = MessageType.get((Class extends Message>) type);
+ var messageType = MessageTypeSerializer.get((Class extends Message>) type);
return messageType.reader().read(this);
} else {
throw new IOException("Unsupported object type: " + type.getSimpleName());
diff --git a/server/pom.xml b/server/pom.xml
index 70fb7bd..c64020a 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -42,6 +42,12 @@
jackson-annotations
2.12.4
+
+
+ at.favre.lib
+ bcrypt
+ 0.9.0
+
diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java
index 6552aac..a6eee84 100644
--- a/server/src/main/java/module-info.java
+++ b/server/src/main/java/module-info.java
@@ -4,6 +4,7 @@ module concord_server {
requires com.fasterxml.jackson.databind;
requires com.fasterxml.jackson.core;
requires com.fasterxml.jackson.annotation;
+ requires bcrypt;
requires java.base;
requires java.logging;
diff --git a/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java b/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java
index 9bc4e90..6b8756a 100644
--- a/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java
+++ b/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java
@@ -85,6 +85,8 @@ public class ConcordServer implements Runnable {
private final ClientManager clientManager;
private final DiscoveryServerPublisher discoveryServerPublisher;
+
+ @Getter
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
public ConcordServer() throws IOException {
diff --git a/server/src/main/java/nl/andrewl/concord_server/cli/command/ListClientsCommand.java b/server/src/main/java/nl/andrewl/concord_server/cli/command/ListClientsCommand.java
index e9e668e..3f1e152 100644
--- a/server/src/main/java/nl/andrewl/concord_server/cli/command/ListClientsCommand.java
+++ b/server/src/main/java/nl/andrewl/concord_server/cli/command/ListClientsCommand.java
@@ -9,7 +9,7 @@ import nl.andrewl.concord_server.cli.ServerCliCommand;
public class ListClientsCommand implements ServerCliCommand {
@Override
public void handle(ConcordServer server, String[] args) throws Exception {
- var users = server.getClientManager().getClients();
+ var users = server.getClientManager().getConnectedClients();
if (users.isEmpty()) {
System.out.println("There are no connected clients.");
} else {
diff --git a/server/src/main/java/nl/andrewl/concord_server/client/AuthenticationService.java b/server/src/main/java/nl/andrewl/concord_server/client/AuthenticationService.java
new file mode 100644
index 0000000..7a8f341
--- /dev/null
+++ b/server/src/main/java/nl/andrewl/concord_server/client/AuthenticationService.java
@@ -0,0 +1,114 @@
+package nl.andrewl.concord_server.client;
+
+import at.favre.lib.crypto.bcrypt.BCrypt;
+import nl.andrewl.concord_core.msg.types.client_setup.ClientLogin;
+import nl.andrewl.concord_core.msg.types.client_setup.ClientRegistration;
+import nl.andrewl.concord_core.msg.types.client_setup.ClientSessionResume;
+import nl.andrewl.concord_server.ConcordServer;
+import nl.andrewl.concord_server.util.CollectionUtils;
+import nl.andrewl.concord_server.util.StringUtils;
+import org.dizitart.no2.Document;
+import org.dizitart.no2.IndexType;
+import org.dizitart.no2.NitriteCollection;
+import org.dizitart.no2.filters.Filters;
+
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * This authentication service provides support for managing the client's
+ * authentication status, such as registering new clients, generating tokens,
+ * and logging in.
+ */
+public class AuthenticationService {
+ public static record ClientConnectionData(UUID id, String nickname, String sessionToken, boolean newClient) {}
+
+ private final NitriteCollection userCollection;
+ private final NitriteCollection sessionTokenCollection;
+ private final ConcordServer server;
+
+ public AuthenticationService(ConcordServer server, NitriteCollection userCollection) {
+ this.server = server;
+ this.userCollection = userCollection;
+ this.sessionTokenCollection = server.getDb().getCollection("session-tokens");
+ CollectionUtils.ensureIndexes(this.sessionTokenCollection, Map.of(
+ "sessionToken", IndexType.Unique,
+ "userId", IndexType.NonUnique,
+ "expiresAt", IndexType.NonUnique
+ ));
+ }
+
+ public ClientConnectionData registerNewClient(ClientRegistration registration) {
+ UUID id = this.server.getIdProvider().newId();
+ String sessionToken = this.generateSessionToken(id);
+ String passwordHash = BCrypt.withDefaults().hashToString(12, registration.password().toCharArray());
+ Document doc = new Document(Map.of(
+ "id", id,
+ "username", registration.username(),
+ "passwordHash", passwordHash,
+ "name", registration.name(),
+ "description", registration.description(),
+ "createdAt", System.currentTimeMillis(),
+ "pending", false
+ ));
+ this.userCollection.insert(doc);
+ return new ClientConnectionData(id, registration.username(), sessionToken, true);
+ }
+
+ public UUID registerPendingClient(ClientRegistration registration) {
+ UUID id = this.server.getIdProvider().newId();
+ String passwordHash = BCrypt.withDefaults().hashToString(12, registration.password().toCharArray());
+ Document doc = new Document(Map.of(
+ "id", id,
+ "username", registration.username(),
+ "passwordHash", passwordHash,
+ "name", registration.name(),
+ "description", registration.description(),
+ "createdAt", System.currentTimeMillis(),
+ "pending", true
+ ));
+ this.userCollection.insert(doc);
+ return id;
+ }
+
+ public Document findAndAuthenticateUser(ClientLogin login) {
+ Document userDoc = this.userCollection.find(Filters.eq("username", login.username())).firstOrDefault();
+ if (userDoc != null) {
+ byte[] passwordHash = userDoc.get("passwordHash", String.class).getBytes(StandardCharsets.UTF_8);
+ if (BCrypt.verifyer().verify(login.password().getBytes(StandardCharsets.UTF_8), passwordHash).verified) {
+ return userDoc;
+ }
+ }
+ return null;
+ }
+
+ public Document findAndAuthenticateUser(ClientSessionResume sessionResume) {
+ Document tokenDoc = this.sessionTokenCollection.find(Filters.and(
+ Filters.eq("sessionToken", sessionResume.sessionToken()),
+ Filters.gt("expiresAt", Instant.now().toEpochMilli())
+ )).firstOrDefault();
+ if (tokenDoc == null) return null;
+ UUID userId = tokenDoc.get("userId", UUID.class);
+ return this.userCollection.find(Filters.eq("id", userId)).firstOrDefault();
+ }
+
+ public String generateSessionToken(UUID userId) {
+ String sessionToken = StringUtils.random(128);
+ long expiresAt = Instant.now().plus(7, ChronoUnit.DAYS).toEpochMilli();
+ Document doc = new Document(Map.of(
+ "sessionToken", sessionToken,
+ "userId", userId,
+ "expiresAt", expiresAt
+ ));
+ this.sessionTokenCollection.insert(doc);
+ return sessionToken;
+ }
+
+ public void removeExpiredSessionTokens() {
+ long now = System.currentTimeMillis();
+ this.sessionTokenCollection.remove(Filters.lt("expiresAt", now));
+ }
+}
diff --git a/server/src/main/java/nl/andrewl/concord_server/client/ClientManager.java b/server/src/main/java/nl/andrewl/concord_server/client/ClientManager.java
index 7d4c6a7..10565cb 100644
--- a/server/src/main/java/nl/andrewl/concord_server/client/ClientManager.java
+++ b/server/src/main/java/nl/andrewl/concord_server/client/ClientManager.java
@@ -1,14 +1,11 @@
package nl.andrewl.concord_server.client;
import nl.andrewl.concord_core.msg.Message;
-import nl.andrewl.concord_core.msg.types.Error;
import nl.andrewl.concord_core.msg.types.ServerUsers;
import nl.andrewl.concord_core.msg.types.UserData;
-import nl.andrewl.concord_core.msg.types.client_setup.Identification;
-import nl.andrewl.concord_core.msg.types.client_setup.ServerWelcome;
+import nl.andrewl.concord_core.msg.types.client_setup.*;
import nl.andrewl.concord_server.ConcordServer;
import nl.andrewl.concord_server.util.CollectionUtils;
-import nl.andrewl.concord_server.util.StringUtils;
import org.dizitart.no2.Document;
import org.dizitart.no2.IndexType;
import org.dizitart.no2.NitriteCollection;
@@ -18,6 +15,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
@@ -27,58 +25,107 @@ import java.util.stream.Collectors;
public class ClientManager {
private final ConcordServer server;
private final Map clients;
+ private final Map pendingClients;
private final NitriteCollection userCollection;
+ private final AuthenticationService authService;
+
public ClientManager(ConcordServer server) {
this.server = server;
this.clients = new ConcurrentHashMap<>();
+ this.pendingClients = new ConcurrentHashMap<>();
this.userCollection = server.getDb().getCollection("users");
CollectionUtils.ensureIndexes(this.userCollection, Map.of(
"id", IndexType.Unique,
- "sessionToken", IndexType.Unique,
- "nickname", IndexType.Fulltext
+ "username", IndexType.Unique,
+ "pending", IndexType.NonUnique
));
+ this.authService = new AuthenticationService(server, this.userCollection);
+ // Start a daily scheduled removal of expired session tokens.
+ server.getScheduledExecutorService().scheduleAtFixedRate(this.authService::removeExpiredSessionTokens, 1, 1, TimeUnit.DAYS);
+ }
+
+ public void handleRegistration(ClientRegistration registration, ClientThread clientThread) throws InvalidIdentificationException {
+ Document userDoc = this.userCollection.find(Filters.eq("username", registration.username())).firstOrDefault();
+ if (userDoc != null) throw new InvalidIdentificationException("Username is taken.");
+ if (this.server.getConfig().isAcceptAllNewClients()) {
+ var clientData = this.authService.registerNewClient(registration);
+ this.initializeClientConnection(clientData, clientThread);
+ } else {
+ var clientId = this.authService.registerPendingClient(registration);
+ this.initializePendingClientConnection(clientId, registration.username(), clientThread);
+ }
+ }
+
+ public void handleLogin(ClientLogin login, ClientThread clientThread) throws InvalidIdentificationException {
+ Document userDoc = this.authService.findAndAuthenticateUser(login);
+ if (userDoc == null) throw new InvalidIdentificationException("Username or password is incorrect.");
+ UUID userId = userDoc.get("id", UUID.class);
+ String username = userDoc.get("username", String.class);
+ boolean pending = userDoc.get("pending", Boolean.class);
+ if (pending) {
+ this.initializePendingClientConnection(userId, username, clientThread);
+ } else {
+ String sessionToken = this.authService.generateSessionToken(userId);
+ this.initializeClientConnection(new AuthenticationService.ClientConnectionData(userId, username, sessionToken, false), clientThread);
+ }
+ }
+
+ public void handleSessionResume(ClientSessionResume sessionResume, ClientThread clientThread) throws InvalidIdentificationException {
+ Document userDoc = this.authService.findAndAuthenticateUser(sessionResume);
+ if (userDoc == null) throw new InvalidIdentificationException("Invalid session. Log in to obtain a new session token.");
+ UUID userId = userDoc.get("id", UUID.class);
+ String username = userDoc.get("username", String.class);
+ String sessionToken = this.authService.generateSessionToken(userId);
+ this.initializeClientConnection(new AuthenticationService.ClientConnectionData(userId, username, sessionToken, false), clientThread);
+ }
+
+ public void decidePendingUser(UUID userId, boolean accepted) {
+ Document userDoc = this.userCollection.find(Filters.and(Filters.eq("id", userId), Filters.eq("pending", true))).firstOrDefault();
+ if (userDoc != null) {
+ if (accepted) {
+ userDoc.put("pending", false);
+ this.userCollection.update(userDoc);
+ // If the pending user is still connected, upgrade them to a normal connected client.
+ var clientThread = this.pendingClients.remove(userId);
+ if (clientThread != null) {
+ clientThread.sendToClient(new RegistrationStatus(RegistrationStatus.Type.ACCEPTED));
+ String username = userDoc.get("username", String.class);
+ String sessionToken = this.authService.generateSessionToken(userId);
+ this.initializeClientConnection(new AuthenticationService.ClientConnectionData(userId, username, sessionToken, true), clientThread);
+ }
+ } else {
+ this.userCollection.remove(userDoc);
+ var clientThread = this.pendingClients.remove(userId);
+ if (clientThread != null) {
+ clientThread.sendToClient(new RegistrationStatus(RegistrationStatus.Type.REJECTED));
+ }
+ }
+ }
}
/**
- * Registers a new client as connected to the server. This is done once the
- * client thread has received the correct identification information from
- * the client. The server will register the client in its global set of
- * connected clients, and it will immediately move the client to the default
- * channel.
- *
- * If the client provides a session token with their identification
- * message, then we should load their data from our database, otherwise
- * we assume this is a new client.
- *
- * @param identification The client's identification data.
- * @param clientThread The client manager thread.
+ * Standard flow for initializing a connection to a client who has already
+ * sent their identification message, and that has been checked to be valid.
+ * @param clientData The data about the client that has connected.
+ * @param clientThread The thread managing the client's connection.
*/
- public void handleLogIn(Identification identification, ClientThread clientThread) {
- ClientConnectionData data;
- try {
- data = identification.sessionToken() == null ? getNewClientData(identification) : getClientDataFromDb(identification);
- } catch (InvalidIdentificationException e) {
- clientThread.sendToClient(Error.warning(e.getMessage()));
- return;
- }
-
- this.clients.put(data.id, clientThread);
- clientThread.setClientId(data.id);
- clientThread.setClientNickname(data.nickname);
+ private void initializeClientConnection(AuthenticationService.ClientConnectionData clientData, ClientThread clientThread) {
+ this.clients.put(clientData.id(), clientThread);
+ clientThread.setClientId(clientData.id());
+ clientThread.setClientNickname(clientData.nickname());
var defaultChannel = this.server.getChannelManager().getDefaultChannel().orElseThrow();
- clientThread.sendToClient(new ServerWelcome(data.id, data.sessionToken, defaultChannel.getId(), defaultChannel.getName(), this.server.getMetaData()));
- // It is important that we send the welcome message first. The client expects this as the initial response to their identification message.
+ clientThread.sendToClient(new ServerWelcome(clientData.id(), clientData.sessionToken(), defaultChannel.getId(), defaultChannel.getName(), this.server.getMetaData()));
defaultChannel.addClient(clientThread);
clientThread.setCurrentChannel(defaultChannel);
- System.out.printf(
- "Client %s(%s) joined%s, and was put into %s.\n",
- data.nickname,
- data.id,
- data.newClient ? " for the first time" : "",
- defaultChannel
- );
- this.broadcast(new ServerUsers(this.getClients().toArray(new UserData[0])));
+ this.broadcast(new ServerUsers(this.getConnectedClients().toArray(new UserData[0])));
+ }
+
+ private void initializePendingClientConnection(UUID clientId, String pendingUsername, ClientThread clientThread) {
+ this.pendingClients.put(clientId, clientThread);
+ clientThread.setClientId(clientId);
+ clientThread.setClientNickname(pendingUsername);
+ clientThread.sendToClient(RegistrationStatus.pending());
}
/**
@@ -87,12 +134,16 @@ public class ClientManager {
* @param clientId The id of the client to remove.
*/
public void handleLogOut(UUID clientId) {
+ var pendingClient = this.pendingClients.remove(clientId);
+ if (pendingClient != null) {
+ pendingClient.shutdown();
+ }
var client = this.clients.remove(clientId);
if (client != null) {
client.getCurrentChannel().removeClient(client);
client.shutdown();
System.out.println("Client " + client + " has disconnected.");
- this.broadcast(new ServerUsers(this.getClients().toArray(new UserData[0])));
+ this.broadcast(new ServerUsers(this.getConnectedClients().toArray(new UserData[0])));
}
}
@@ -114,13 +165,20 @@ public class ClientManager {
}
}
- public List getClients() {
+ public List getConnectedClients() {
return this.clients.values().stream()
.sorted(Comparator.comparing(ClientThread::getClientNickname))
.map(ClientThread::toData)
.collect(Collectors.toList());
}
+ public List getPendingClients() {
+ return this.pendingClients.values().stream()
+ .sorted(Comparator.comparing(ClientThread::getClientNickname))
+ .map(ClientThread::toData)
+ .collect(Collectors.toList());
+ }
+
public Set getConnectedIds() {
return this.clients.keySet();
}
@@ -129,42 +187,7 @@ public class ClientManager {
return Optional.ofNullable(this.clients.get(id));
}
- private static record ClientConnectionData(UUID id, String nickname, String sessionToken, boolean newClient) {}
-
- private ClientConnectionData getClientDataFromDb(Identification identification) throws InvalidIdentificationException {
- var cursor = this.userCollection.find(Filters.eq("sessionToken", identification.sessionToken()));
- Document doc = cursor.firstOrDefault();
- if (doc != null) {
- UUID id = doc.get("id", UUID.class);
- String nickname = identification.nickname();
- if (nickname != null) {
- doc.put("nickname", nickname);
- } else {
- nickname = doc.get("nickname", String.class);
- }
- String sessionToken = StringUtils.random(128);
- doc.put("sessionToken", sessionToken);
- this.userCollection.update(doc);
- return new ClientConnectionData(id, nickname, sessionToken, false);
- } else {
- throw new InvalidIdentificationException("Invalid session token.");
- }
- }
-
- private ClientConnectionData getNewClientData(Identification identification) throws InvalidIdentificationException {
- UUID id = this.server.getIdProvider().newId();
- String nickname = identification.nickname();
- if (nickname == null) {
- throw new InvalidIdentificationException("Missing nickname.");
- }
- String sessionToken = StringUtils.random(128);
- Document doc = new Document(Map.of(
- "id", id,
- "nickname", nickname,
- "sessionToken", sessionToken,
- "createdAt", System.currentTimeMillis()
- ));
- this.userCollection.insert(doc);
- return new ClientConnectionData(id, nickname, sessionToken, true);
+ public Optional getPendingClientById(UUID id) {
+ return Optional.ofNullable(this.pendingClients.get(id));
}
}
diff --git a/server/src/main/java/nl/andrewl/concord_server/client/ClientThread.java b/server/src/main/java/nl/andrewl/concord_server/client/ClientThread.java
index 2e580c9..19763e9 100644
--- a/server/src/main/java/nl/andrewl/concord_server/client/ClientThread.java
+++ b/server/src/main/java/nl/andrewl/concord_server/client/ClientThread.java
@@ -4,8 +4,11 @@ import lombok.Getter;
import lombok.Setter;
import nl.andrewl.concord_core.msg.Encryption;
import nl.andrewl.concord_core.msg.Message;
-import nl.andrewl.concord_core.msg.types.client_setup.Identification;
+import nl.andrewl.concord_core.msg.types.Error;
import nl.andrewl.concord_core.msg.types.UserData;
+import nl.andrewl.concord_core.msg.types.client_setup.ClientLogin;
+import nl.andrewl.concord_core.msg.types.client_setup.ClientRegistration;
+import nl.andrewl.concord_core.msg.types.client_setup.ClientSessionResume;
import nl.andrewl.concord_server.ConcordServer;
import nl.andrewl.concord_server.channel.Channel;
@@ -135,14 +138,25 @@ public class ClientThread extends Thread {
System.err.println("Could not establish end-to-end encryption with the client.");
return false;
}
+ final var clientManager = this.server.getClientManager();
int attempts = 0;
while (attempts < 5) {
try {
var msg = this.server.getSerializer().readMessage(this.in);
- if (msg instanceof Identification id) {
- this.server.getClientManager().handleLogIn(id, this);
+ if (msg instanceof ClientRegistration cr) {
+ clientManager.handleRegistration(cr, this);
return true;
+ } else if (msg instanceof ClientLogin cl) {
+ clientManager.handleLogin(cl, this);
+ return true;
+ } else if (msg instanceof ClientSessionResume csr) {
+ clientManager.handleSessionResume(csr, this);
+ return true;
+ } else {
+ this.sendToClient(Error.warning("Invalid identification message: " + msg.getClass().getSimpleName() + ", expected ClientRegistration, ClientLogin, or ClientSessionResume."));
}
+ } catch (InvalidIdentificationException e) {
+ this.sendToClient(Error.warning(e.getMessage()));
} catch (IOException e) {
e.printStackTrace();
}
diff --git a/server/src/main/java/nl/andrewl/concord_server/config/ServerConfig.java b/server/src/main/java/nl/andrewl/concord_server/config/ServerConfig.java
index 684ee52..e97eb5e 100644
--- a/server/src/main/java/nl/andrewl/concord_server/config/ServerConfig.java
+++ b/server/src/main/java/nl/andrewl/concord_server/config/ServerConfig.java
@@ -20,6 +20,7 @@ public final class ServerConfig {
private String name;
private String description;
private int port;
+ private boolean acceptAllNewClients;
private int chatHistoryMaxCount;
private int chatHistoryDefaultCount;
private int maxMessageLength;
@@ -51,6 +52,7 @@ public final class ServerConfig {
"My Concord Server",
"A concord server for my friends and I.",
8123,
+ false,
100,
50,
8192,
From fe4bda42faecdcdfab4baf7f6e66d53c1536d660 Mon Sep 17 00:00:00 2001
From: Andrew Lalis
Date: Sat, 25 Sep 2021 13:10:35 +0200
Subject: [PATCH 2/5] Added ACCEPTED response to registration in accept all
mode.
---
.../java/nl/andrewl/concord_server/client/ClientManager.java | 1 +
1 file changed, 1 insertion(+)
diff --git a/server/src/main/java/nl/andrewl/concord_server/client/ClientManager.java b/server/src/main/java/nl/andrewl/concord_server/client/ClientManager.java
index 10565cb..cf4bb37 100644
--- a/server/src/main/java/nl/andrewl/concord_server/client/ClientManager.java
+++ b/server/src/main/java/nl/andrewl/concord_server/client/ClientManager.java
@@ -50,6 +50,7 @@ public class ClientManager {
if (userDoc != null) throw new InvalidIdentificationException("Username is taken.");
if (this.server.getConfig().isAcceptAllNewClients()) {
var clientData = this.authService.registerNewClient(registration);
+ clientThread.sendToClient(new RegistrationStatus(RegistrationStatus.Type.ACCEPTED));
this.initializeClientConnection(clientData, clientThread);
} else {
var clientId = this.authService.registerPendingClient(registration);
From 1bb446ff5b17a320a51344855aabdb9411ad0abb Mon Sep 17 00:00:00 2001
From: Andrew Lalis
Date: Sat, 25 Sep 2021 13:12:45 +0200
Subject: [PATCH 3/5] Updated readme.
---
README.md | 8 ++------
1 file changed, 2 insertions(+), 6 deletions(-)
diff --git a/README.md b/README.md
index 80bd162..a77c9b1 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,7 @@ You probably want to customize your server a bit. To do so, first stop your serv
- `name` The name of the server.
- `description` A short description of what this server is for, or who it's run by.
- `port` The port on which the server accepts client connections.
+- `acceptAllNewClients` Whether to automatically accept any new client that registers to this server. Set to false by default, meaning an administrator needs to approve any pending registration before it is complete.
- `chatHistoryMaxCount` The maximum amount of chat messages that a client can request from the server at any given time. Decrease this to improve performance.
- `chatHistoryDefaultCount` The default number of chat messages that are provided to clients when they join a channel, if they don't explicitly request a certain amount. Decrease this to improve performance.
- `maxMessageLength` The maximum length of a message. Messages longer than this will be rejected.
@@ -46,11 +47,6 @@ You probably want to customize your server a bit. To do so, first stop your serv
## Server CLI
-As mentioned briefly, the server supports a basic command-line-interface with some commands. You can show which commands are available via the `help` command. The following is a list of some of the most useful commands and a description of their functionality:
-
-- `add-channel ` Adds a new channel to the server with the given name. Channel names cannot be blank, and they cannot be duplicates of an existing channel name.
-- `remove-channel ` Removes a channel.
-- `list-clients` Shows a list of all connected clients.
-- `stop` Stops the server, disconnecting all clients.
+As mentioned briefly, the server supports a basic command-line-interface with some commands. You can show the commands that are available via the `help` command.
Each server uses a single [Nitrite](https://www.dizitart.org/nitrite-database/#what-is-nitrite) database to hold messages and other information.
From 6b7712a9fb7da32296e0f082ef5a93bc2dd33ff2 Mon Sep 17 00:00:00 2001
From: Andrew Lalis
Date: Tue, 28 Sep 2021 23:42:54 +0200
Subject: [PATCH 4/5] Added tons of javadoc, finalized server-side registration
protocol.
---
.../andrewl/concord_client/ConcordClient.java | 139 ++++++++----------
.../concord_client/data/ClientDataStore.java | 13 ++
.../data/JsonClientDataStore.java | 42 ++++++
.../concord_client/gui/MainWindow.java | 2 +-
.../concord_client/model/ClientModel.java | 2 +-
core/src/main/java/module-info.java | 9 ++
.../concord_core/msg/package-info.java | 7 +
.../andrewl/concord_core/msg/types/Error.java | 17 ++-
.../msg/types/channel/package-info.java | 4 +
.../concord_core/msg/types/chat/Chat.java | 4 +-
.../msg/types/chat/ChatHistoryRequest.java | 2 +-
.../msg/types/chat/ChatHistoryResponse.java | 2 +
.../msg/types/chat/package-info.java | 5 +
.../msg/types/client_setup/KeyData.java | 3 +
.../client_setup/RegistrationStatus.java | 4 +-
.../msg/types/client_setup/package-info.java | 4 +
.../concord_core/msg/types/package-info.java | 10 ++
.../nl/andrewl/concord_core/util/Pair.java | 5 +
.../nl/andrewl/concord_core/util/Triple.java | 6 +
.../concord_core/util/package-info.java | 5 +
pom.xml | 19 ++-
.../andrewl/concord_server/ConcordServer.java | 3 +
.../concord_server/channel/Channel.java | 2 +-
.../client/AuthenticationService.java | 2 -
.../client/ClientConnectionData.java | 15 ++
.../concord_server/client/ClientManager.java | 105 +++++++++++--
26 files changed, 326 insertions(+), 105 deletions(-)
create mode 100644 client/src/main/java/nl/andrewl/concord_client/data/ClientDataStore.java
create mode 100644 client/src/main/java/nl/andrewl/concord_client/data/JsonClientDataStore.java
create mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/package-info.java
create mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/types/channel/package-info.java
create mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/types/chat/package-info.java
create mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/package-info.java
create mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/types/package-info.java
create mode 100644 core/src/main/java/nl/andrewl/concord_core/util/package-info.java
create mode 100644 server/src/main/java/nl/andrewl/concord_server/client/ClientConnectionData.java
diff --git a/client/src/main/java/nl/andrewl/concord_client/ConcordClient.java b/client/src/main/java/nl/andrewl/concord_client/ConcordClient.java
index 75720bb..15604c8 100644
--- a/client/src/main/java/nl/andrewl/concord_client/ConcordClient.java
+++ b/client/src/main/java/nl/andrewl/concord_client/ConcordClient.java
@@ -1,6 +1,5 @@
package nl.andrewl.concord_client;
-import com.fasterxml.jackson.databind.ObjectMapper;
import com.googlecode.lanterna.gui2.MultiWindowTextGUI;
import com.googlecode.lanterna.gui2.Window;
import com.googlecode.lanterna.gui2.WindowBasedTextGUI;
@@ -9,6 +8,8 @@ import com.googlecode.lanterna.screen.TerminalScreen;
import com.googlecode.lanterna.terminal.DefaultTerminalFactory;
import com.googlecode.lanterna.terminal.Terminal;
import lombok.Getter;
+import nl.andrewl.concord_client.data.ClientDataStore;
+import nl.andrewl.concord_client.data.JsonClientDataStore;
import nl.andrewl.concord_client.event.EventManager;
import nl.andrewl.concord_client.event.handlers.ChannelMovedHandler;
import nl.andrewl.concord_client.event.handlers.ChatHistoryResponseHandler;
@@ -19,42 +20,41 @@ import nl.andrewl.concord_client.model.ClientModel;
import nl.andrewl.concord_core.msg.Encryption;
import nl.andrewl.concord_core.msg.Message;
import nl.andrewl.concord_core.msg.Serializer;
-import nl.andrewl.concord_core.msg.types.*;
+import nl.andrewl.concord_core.msg.types.ServerMetaData;
+import nl.andrewl.concord_core.msg.types.ServerUsers;
import nl.andrewl.concord_core.msg.types.channel.MoveToChannel;
import nl.andrewl.concord_core.msg.types.chat.Chat;
import nl.andrewl.concord_core.msg.types.chat.ChatHistoryRequest;
import nl.andrewl.concord_core.msg.types.chat.ChatHistoryResponse;
-import nl.andrewl.concord_core.msg.types.client_setup.Identification;
-import nl.andrewl.concord_core.msg.types.client_setup.ServerWelcome;
+import nl.andrewl.concord_core.msg.types.client_setup.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
-import java.nio.file.Files;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
public class ConcordClient implements Runnable {
private final Socket socket;
private final InputStream in;
private final OutputStream out;
private final Serializer serializer;
+ private final ClientDataStore dataStore;
@Getter
- private final ClientModel model;
+ private ClientModel model;
private final EventManager eventManager;
private volatile boolean running;
- public ConcordClient(String host, int port, String nickname, Path tokensFile) throws IOException {
+ private ConcordClient(String host, int port) throws IOException {
this.eventManager = new EventManager(this);
this.socket = new Socket(host, port);
this.serializer = new Serializer();
+ this.dataStore = new JsonClientDataStore(Path.of("concord-session-tokens.json"));
try {
var streams = Encryption.upgrade(socket.getInputStream(), socket.getOutputStream(), this.serializer);
this.in = streams.first();
@@ -62,8 +62,6 @@ public class ConcordClient implements Runnable {
} catch (GeneralSecurityException e) {
throw new IOException("Could not establish secure connection to the server.", e);
}
- this.model = this.initializeConnectionToServer(nickname, tokensFile);
-
// Add event listeners.
this.eventManager.addHandler(MoveToChannel.class, new ChannelMovedHandler());
this.eventManager.addHandler(ServerUsers.class, new ServerUsersHandler());
@@ -72,32 +70,63 @@ public class ConcordClient implements Runnable {
this.eventManager.addHandler(ServerMetaData.class, new ServerMetaDataHandler());
}
- /**
- * Initializes the communication with the server by sending an {@link Identification}
- * message, and waiting for a {@link ServerWelcome} response from the
- * server. After that, we request some information about the channel we were
- * placed in by the server.
- * @param nickname The nickname to send to the server that it should know
- * us by.
- * @param tokensFile Path to the file where session tokens are stored.
- * @return The client model that contains the server's metadata and other
- * information that should be kept up-to-date at runtime.
- * @throws IOException If an error occurs while reading or writing the
- * messages, or if the server sends an unexpected response.
- */
- private ClientModel initializeConnectionToServer(String nickname, Path tokensFile) throws IOException {
- String token = this.getSessionToken(tokensFile);
- this.serializer.writeMessage(new Identification(nickname, token), this.out);
- Message reply = this.serializer.readMessage(this.in);
- if (reply instanceof ServerWelcome welcome) {
- var model = new ClientModel(welcome.clientId(), nickname, welcome.currentChannelId(), welcome.currentChannelName(), welcome.metaData());
- this.saveSessionToken(welcome.sessionToken(), tokensFile);
- // Start fetching initial data for the channel we were initially put into.
- this.sendMessage(new ChatHistoryRequest(model.getCurrentChannelId(), ""));
- return model;
+ public static ConcordClient register(String host, int port, String username, String password) throws IOException {
+ var client = new ConcordClient(host, port);
+ client.sendMessage(new ClientRegistration(null, null, username, password));
+ Message reply = client.serializer.readMessage(client.in);
+ if (reply instanceof RegistrationStatus status) {
+ if (status.type() == RegistrationStatus.Type.ACCEPTED) {
+ ServerWelcome welcomeData = (ServerWelcome) client.serializer.readMessage(client.in);
+ client.initializeClientModel(welcomeData, username);
+ } else if (status.type() == RegistrationStatus.Type.PENDING) {
+ System.out.println("Registration pending!");
+ }
} else {
- throw new IOException("Unexpected response from the server after sending identification message: " + reply);
+ System.out.println(reply);
}
+ return client;
+ }
+
+ public static ConcordClient login(String host, int port, String username, String password) throws IOException {
+ var client = new ConcordClient(host, port);
+ client.sendMessage(new ClientLogin(username, password));
+ Message reply = client.serializer.readMessage(client.in);
+ if (reply instanceof ServerWelcome welcome) {
+ client.initializeClientModel(welcome, username);
+ } else if (reply instanceof RegistrationStatus status && status.type() == RegistrationStatus.Type.PENDING) {
+ System.out.println("Registration pending!");
+ } else {
+ System.out.println(reply);
+ }
+ return client;
+ }
+
+ public static ConcordClient loginWithToken(String host, int port) throws IOException {
+ var client = new ConcordClient(host, port);
+ var token = client.dataStore.getSessionToken(client.socket.getInetAddress().getHostName() + ":" + client.socket.getPort());
+ if (token.isPresent()) {
+ client.sendMessage(new ClientSessionResume(token.get()));
+ Message reply = client.serializer.readMessage(client.in);
+ if (reply instanceof ServerWelcome welcome) {
+ client.initializeClientModel(welcome, "unknown");
+ }
+ } else {
+ System.err.println("No session token!");
+ }
+ return client;
+ }
+
+ private void initializeClientModel(ServerWelcome welcomeData, String username) throws IOException {
+ var model = new ClientModel(
+ welcomeData.clientId(),
+ username,
+ welcomeData.currentChannelId(),
+ welcomeData.currentChannelName(),
+ welcomeData.metaData()
+ );
+ this.dataStore.saveSessionToken(this.socket.getInetAddress().getHostName() + ":" + this.socket.getPort(), welcomeData.sessionToken());
+ // Start fetching initial data for the channel we were initially put into.
+ this.sendMessage(new ChatHistoryRequest(model.getCurrentChannelId(), ""));
}
public void sendMessage(Message message) throws IOException {
@@ -138,46 +167,6 @@ public class ConcordClient implements Runnable {
}
}
- /**
- * Fetches the session token that this client should use for its currently
- * configured server, according to the socket address and port.
- * @param tokensFile The file containing the session tokens.
- * @return The session token, or null if none was found.
- * @throws IOException If the tokens file could not be read.
- */
- @SuppressWarnings("unchecked")
- private String getSessionToken(Path tokensFile) throws IOException {
- String token = null;
- String address = this.socket.getInetAddress().getHostName() + ":" + this.socket.getPort();
- if (Files.exists(tokensFile)) {
- ObjectMapper mapper = new ObjectMapper();
- Map sessionTokens = mapper.readValue(Files.newBufferedReader(tokensFile), Map.class);
- token = sessionTokens.get(address);
- }
- return token;
- }
-
- /**
- * Saves a session token that this client should use the next time it
- * connects to the same server.
- * @param token The token to save.
- * @param tokensFile The file containing the session tokens.
- * @throws IOException If the tokens file could not be read or written to.
- */
- @SuppressWarnings("unchecked")
- private void saveSessionToken(String token, Path tokensFile) throws IOException {
- String address = this.socket.getInetAddress().getHostName() + ":" + this.socket.getPort();
- Map tokens = new HashMap<>();
- ObjectMapper mapper = new ObjectMapper();
- if (Files.exists(tokensFile)) {
- tokens = mapper.readValue(Files.newBufferedReader(tokensFile), Map.class);
- }
- tokens.put(address, token);
- mapper.writerWithDefaultPrettyPrinter().writeValue(Files.newBufferedWriter(tokensFile), tokens);
- }
-
-
-
public static void main(String[] args) throws IOException {
Terminal term = new DefaultTerminalFactory().createTerminal();
Screen screen = new TerminalScreen(term);
diff --git a/client/src/main/java/nl/andrewl/concord_client/data/ClientDataStore.java b/client/src/main/java/nl/andrewl/concord_client/data/ClientDataStore.java
new file mode 100644
index 0000000..cc85b7f
--- /dev/null
+++ b/client/src/main/java/nl/andrewl/concord_client/data/ClientDataStore.java
@@ -0,0 +1,13 @@
+package nl.andrewl.concord_client.data;
+
+import java.io.IOException;
+import java.util.Optional;
+
+/**
+ * A component which can store and retrieve persistent data which a client can
+ * use as part of its interaction with servers.
+ */
+public interface ClientDataStore {
+ Optional getSessionToken(String serverName) throws IOException;
+ void saveSessionToken(String serverName, String sessionToken) throws IOException;
+}
diff --git a/client/src/main/java/nl/andrewl/concord_client/data/JsonClientDataStore.java b/client/src/main/java/nl/andrewl/concord_client/data/JsonClientDataStore.java
new file mode 100644
index 0000000..3ac5796
--- /dev/null
+++ b/client/src/main/java/nl/andrewl/concord_client/data/JsonClientDataStore.java
@@ -0,0 +1,42 @@
+package nl.andrewl.concord_client.data;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+public class JsonClientDataStore implements ClientDataStore {
+ private final Path file;
+
+ public JsonClientDataStore(Path file) {
+ this.file = file;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Optional getSessionToken(String serverName) throws IOException {
+ String token = null;
+ if (Files.exists(file)) {
+ ObjectMapper mapper = new ObjectMapper();
+ Map sessionTokens = mapper.readValue(Files.newBufferedReader(file), Map.class);
+ token = sessionTokens.get(serverName);
+ }
+ return Optional.ofNullable(token);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void saveSessionToken(String serverName, String sessionToken) throws IOException {
+ Map tokens = new HashMap<>();
+ ObjectMapper mapper = new ObjectMapper();
+ if (Files.exists(file)) {
+ tokens = mapper.readValue(Files.newBufferedReader(file), Map.class);
+ }
+ tokens.put(serverName, sessionToken);
+ mapper.writerWithDefaultPrettyPrinter().writeValue(Files.newBufferedWriter(file), tokens);
+ }
+}
diff --git a/client/src/main/java/nl/andrewl/concord_client/gui/MainWindow.java b/client/src/main/java/nl/andrewl/concord_client/gui/MainWindow.java
index 67a3a9f..c0dddc0 100644
--- a/client/src/main/java/nl/andrewl/concord_client/gui/MainWindow.java
+++ b/client/src/main/java/nl/andrewl/concord_client/gui/MainWindow.java
@@ -50,7 +50,7 @@ public class MainWindow extends BasicWindow {
if (nickname == null) return;
try {
- var client = new ConcordClient(host, port, nickname, Path.of("concord-session-tokens.json"));
+ var client = ConcordClient.login(host, port, nickname, "testpass");
var chatPanel = new ServerPanel(client, this);
client.getModel().addListener(chatPanel);
new Thread(client).start();
diff --git a/client/src/main/java/nl/andrewl/concord_client/model/ClientModel.java b/client/src/main/java/nl/andrewl/concord_client/model/ClientModel.java
index bbba707..fe9dd25 100644
--- a/client/src/main/java/nl/andrewl/concord_client/model/ClientModel.java
+++ b/client/src/main/java/nl/andrewl/concord_client/model/ClientModel.java
@@ -12,7 +12,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
@Getter
public class ClientModel {
- private UUID id;
+ private final UUID id;
private String nickname;
private ServerMetaData serverMetaData;
diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java
index cbbf0fa..1fe7dc8 100644
--- a/core/src/main/java/module-info.java
+++ b/core/src/main/java/module-info.java
@@ -1,3 +1,12 @@
+/**
+ * The core components that are used by both the Concord server and the default
+ * client implementation. Includes record-based message serialization, and some
+ * utilities for message passing.
+ *
+ * This core module defines the message protocol that clients must use to
+ * communicate with any server.
+ *
+ */
module concord_core {
requires static lombok;
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/package-info.java b/core/src/main/java/nl/andrewl/concord_core/msg/package-info.java
new file mode 100644
index 0000000..15af159
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * Message components which are used by the server and the default client
+ * implementation. Notably, the {@link nl.andrewl.concord_core.msg.Serializer}
+ * within this package defines the set of supported message types, and provides
+ * the highest-level interface to client-server communication.
+ */
+package nl.andrewl.concord_core.msg;
\ No newline at end of file
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/Error.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/Error.java
index 6331bb4..1605114 100644
--- a/core/src/main/java/nl/andrewl/concord_core/msg/types/Error.java
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/Error.java
@@ -5,11 +5,10 @@ import nl.andrewl.concord_core.msg.Message;
/**
* Error message which can be sent between either the server or client to
* indicate an unsavory situation.
+ * @param level The severity level of the error.
+ * @param message A message indicating what went wrong.
*/
-public record Error (
- Level level,
- String message
-) implements Message {
+public record Error (Level level, String message) implements Message {
/**
* The error level gives an indication as to the severity of the error.
* Warnings indicate that a user has attempted to do something which they
@@ -18,10 +17,20 @@ public record Error (
*/
public enum Level {WARNING, ERROR}
+ /**
+ * Creates a warning message.
+ * @param message The message text.
+ * @return A warning-level error message.
+ */
public static Error warning(String message) {
return new Error(Level.WARNING, message);
}
+ /**
+ * Creates an error message.
+ * @param message The message text.
+ * @return An error-level error message.
+ */
public static Error error(String message) {
return new Error(Level.ERROR, message);
}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/channel/package-info.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/channel/package-info.java
new file mode 100644
index 0000000..071fc18
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/channel/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Messages pertaining to channel interaction and updates.
+ */
+package nl.andrewl.concord_core.msg.types.channel;
\ No newline at end of file
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/chat/Chat.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/chat/Chat.java
index f094ef7..cf172af 100644
--- a/core/src/main/java/nl/andrewl/concord_core/msg/types/chat/Chat.java
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/chat/Chat.java
@@ -8,9 +8,7 @@ import java.util.UUID;
/**
* This message contains information about a chat message that a user sent.
*/
-public record Chat (
- UUID id, UUID senderId, String senderNickname, long timestamp, String message
-) implements Message {
+public record Chat (UUID id, UUID senderId, String senderNickname, long timestamp, String message) implements Message {
public Chat(UUID senderId, String senderNickname, long timestamp, String message) {
this(null, senderId, senderNickname, timestamp, message);
}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/chat/ChatHistoryRequest.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/chat/ChatHistoryRequest.java
index 725b01b..b2db93a 100644
--- a/core/src/main/java/nl/andrewl/concord_core/msg/types/chat/ChatHistoryRequest.java
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/chat/ChatHistoryRequest.java
@@ -24,6 +24,7 @@ import java.util.stream.Collectors;
*
*
* The following query parameters are supported:
+ *
*
* count
- Fetch up to N messages. Minimum of 1, and
* a server-specific maximum count, usually no higher than 1000.
@@ -37,7 +38,6 @@ import java.util.stream.Collectors;
* is present, all others are ignored, and a list containing the single
* message is returned, if it could be found, otherwise an empty list.
*
- *
*
* Responses to this request are sent via {@link ChatHistoryResponse}, where
* the list of messages is always sorted by the timestamp.
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/chat/ChatHistoryResponse.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/chat/ChatHistoryResponse.java
index 4743bfa..629d856 100644
--- a/core/src/main/java/nl/andrewl/concord_core/msg/types/chat/ChatHistoryResponse.java
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/chat/ChatHistoryResponse.java
@@ -7,5 +7,7 @@ import java.util.UUID;
/**
* The response that a server sends to a {@link ChatHistoryRequest}. The list of
* messages is ordered by timestamp, with the newest messages appearing first.
+ * @param channelId The id of the channel that the chat messages belong to.
+ * @param messages The list of messages that comprises the history.
*/
public record ChatHistoryResponse (UUID channelId, Chat[] messages) implements Message {}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/chat/package-info.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/chat/package-info.java
new file mode 100644
index 0000000..da1115c
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/chat/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Messages pertaining to chat messages and other auxiliary messages regarding
+ * the management of chat information.
+ */
+package nl.andrewl.concord_core.msg.types.chat;
\ No newline at end of file
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/KeyData.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/KeyData.java
index f46d18f..feca228 100644
--- a/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/KeyData.java
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/KeyData.java
@@ -5,5 +5,8 @@ import nl.andrewl.concord_core.msg.Message;
/**
* This message is sent as the first message from both the server and the client
* to establish an end-to-end encryption via a key exchange.
+ * @param iv The initialization vector bytes.
+ * @param salt The salt bytes.
+ * @param publicKey The public key.
*/
public record KeyData (byte[] iv, byte[] salt, byte[] publicKey) implements Message {}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/RegistrationStatus.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/RegistrationStatus.java
index 45d8159..43b951d 100644
--- a/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/RegistrationStatus.java
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/RegistrationStatus.java
@@ -6,10 +6,10 @@ import nl.andrewl.concord_core.msg.Message;
* A response from the server which indicates the current status of the client's
* registration request.
*/
-public record RegistrationStatus (Type type) implements Message {
+public record RegistrationStatus (Type type, String reason) implements Message {
public enum Type {PENDING, ACCEPTED, REJECTED}
public static RegistrationStatus pending() {
- return new RegistrationStatus(Type.PENDING);
+ return new RegistrationStatus(Type.PENDING, null);
}
}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/package-info.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/package-info.java
new file mode 100644
index 0000000..f23e1e6
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Messages pertaining to the establishment of a connection with clients.
+ */
+package nl.andrewl.concord_core.msg.types.client_setup;
\ No newline at end of file
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/package-info.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/package-info.java
new file mode 100644
index 0000000..f060e4f
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/package-info.java
@@ -0,0 +1,10 @@
+/**
+ * Contains all the various message types which can be sent between the server
+ * and client.
+ *
+ * Note that not all message types defined here may be supported by the
+ * latest version of Concord. See {@link nl.andrewl.concord_core.msg.Serializer}
+ * for the definitive list.
+ *
+ */
+package nl.andrewl.concord_core.msg.types;
\ No newline at end of file
diff --git a/core/src/main/java/nl/andrewl/concord_core/util/Pair.java b/core/src/main/java/nl/andrewl/concord_core/util/Pair.java
index e3a14d7..4167ab2 100644
--- a/core/src/main/java/nl/andrewl/concord_core/util/Pair.java
+++ b/core/src/main/java/nl/andrewl/concord_core/util/Pair.java
@@ -1,3 +1,8 @@
package nl.andrewl.concord_core.util;
+/**
+ * Simple generic pair of two objects.
+ * @param The first object.
+ * @param The second object.
+ */
public record Pair(A first, B second) {}
diff --git a/core/src/main/java/nl/andrewl/concord_core/util/Triple.java b/core/src/main/java/nl/andrewl/concord_core/util/Triple.java
index 28bed57..6cca48e 100644
--- a/core/src/main/java/nl/andrewl/concord_core/util/Triple.java
+++ b/core/src/main/java/nl/andrewl/concord_core/util/Triple.java
@@ -1,3 +1,9 @@
package nl.andrewl.concord_core.util;
+/**
+ * Simple generic triple of objects.
+ * @param The first object.
+ * @param The second object.
+ * @param The third object.
+ */
public record Triple (A first, B second, C third) {}
diff --git a/core/src/main/java/nl/andrewl/concord_core/util/package-info.java b/core/src/main/java/nl/andrewl/concord_core/util/package-info.java
new file mode 100644
index 0000000..4fd7cd4
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/util/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Contains some useful one-off utility classes that any consumer of Concord
+ * messages could benefit from.
+ */
+package nl.andrewl.concord_core.util;
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 8e65f56..16c95bd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -16,9 +16,10 @@
- 16
- 16
- 16
+ 17
+ 17
+ 17
+ 17
UTF-8
@@ -47,6 +48,18 @@
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.3.1
+
+ false
+ false
+ false
+ false
+ private
+
+
\ No newline at end of file
diff --git a/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java b/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java
index 6b8756a..ebb67c2 100644
--- a/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java
+++ b/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java
@@ -124,6 +124,9 @@ public class ConcordServer implements Runnable {
}
}
+ /**
+ * @return The server's metadata.
+ */
public ServerMetaData getMetaData() {
return new ServerMetaData(
this.config.getName(),
diff --git a/server/src/main/java/nl/andrewl/concord_server/channel/Channel.java b/server/src/main/java/nl/andrewl/concord_server/channel/Channel.java
index d46ffab..7bf4a26 100644
--- a/server/src/main/java/nl/andrewl/concord_server/channel/Channel.java
+++ b/server/src/main/java/nl/andrewl/concord_server/channel/Channel.java
@@ -33,7 +33,7 @@ public class Channel implements Comparable {
/**
* A document collection which holds all messages created in this channel,
- * indexed on id, timestamp, message, and sender's nickname.
+ * indexed on id, timestamp, message, and sender's username.
*/
private final NitriteCollection messageCollection;
diff --git a/server/src/main/java/nl/andrewl/concord_server/client/AuthenticationService.java b/server/src/main/java/nl/andrewl/concord_server/client/AuthenticationService.java
index 7a8f341..9e5e66b 100644
--- a/server/src/main/java/nl/andrewl/concord_server/client/AuthenticationService.java
+++ b/server/src/main/java/nl/andrewl/concord_server/client/AuthenticationService.java
@@ -24,8 +24,6 @@ import java.util.UUID;
* and logging in.
*/
public class AuthenticationService {
- public static record ClientConnectionData(UUID id, String nickname, String sessionToken, boolean newClient) {}
-
private final NitriteCollection userCollection;
private final NitriteCollection sessionTokenCollection;
private final ConcordServer server;
diff --git a/server/src/main/java/nl/andrewl/concord_server/client/ClientConnectionData.java b/server/src/main/java/nl/andrewl/concord_server/client/ClientConnectionData.java
new file mode 100644
index 0000000..6097df6
--- /dev/null
+++ b/server/src/main/java/nl/andrewl/concord_server/client/ClientConnectionData.java
@@ -0,0 +1,15 @@
+package nl.andrewl.concord_server.client;
+
+import java.util.UUID;
+
+/**
+ * Some common data that's used when dealing with a client who has just joined
+ * the server.
+ * @param id The user's unique id.
+ * @param username The user's unique username.
+ * @param sessionToken The user's new session token that can be used the next
+ * time they want to log in.
+ * @param newClient True if this client is connecting for the first time, or
+ * false otherwise.
+ */
+public record ClientConnectionData(UUID id, String username, String sessionToken, boolean newClient) {}
diff --git a/server/src/main/java/nl/andrewl/concord_server/client/ClientManager.java b/server/src/main/java/nl/andrewl/concord_server/client/ClientManager.java
index cf4bb37..18581e5 100644
--- a/server/src/main/java/nl/andrewl/concord_server/client/ClientManager.java
+++ b/server/src/main/java/nl/andrewl/concord_server/client/ClientManager.java
@@ -27,9 +27,12 @@ public class ClientManager {
private final Map clients;
private final Map pendingClients;
private final NitriteCollection userCollection;
-
private final AuthenticationService authService;
+ /**
+ * Constructs a new client manager for the given server.
+ * @param server The server that the client manager is for.
+ */
public ClientManager(ConcordServer server) {
this.server = server;
this.clients = new ConcurrentHashMap<>();
@@ -45,12 +48,28 @@ public class ClientManager {
server.getScheduledExecutorService().scheduleAtFixedRate(this.authService::removeExpiredSessionTokens, 1, 1, TimeUnit.DAYS);
}
+ /**
+ * Handles an attempt by a new client to register as a user for this server.
+ * If the server is set to automatically accept all new clients, the new
+ * user is registered and the client is sent a {@link RegistrationStatus}
+ * with the {@link RegistrationStatus.Type#ACCEPTED} value, closely followed
+ * by a {@link ServerWelcome} message. Otherwise, the client is sent a
+ * {@link RegistrationStatus.Type#PENDING} response, which indicates that
+ * the client's registration is pending approval. The client can choose to
+ * remain connected and wait for approval, or disconnect and try logging in
+ * later.
+ *
+ * @param registration The client's registration information.
+ * @param clientThread The client thread.
+ * @throws InvalidIdentificationException If the user's registration info is
+ * not valid.
+ */
public void handleRegistration(ClientRegistration registration, ClientThread clientThread) throws InvalidIdentificationException {
Document userDoc = this.userCollection.find(Filters.eq("username", registration.username())).firstOrDefault();
if (userDoc != null) throw new InvalidIdentificationException("Username is taken.");
if (this.server.getConfig().isAcceptAllNewClients()) {
var clientData = this.authService.registerNewClient(registration);
- clientThread.sendToClient(new RegistrationStatus(RegistrationStatus.Type.ACCEPTED));
+ clientThread.sendToClient(new RegistrationStatus(RegistrationStatus.Type.ACCEPTED, null));
this.initializeClientConnection(clientData, clientThread);
} else {
var clientId = this.authService.registerPendingClient(registration);
@@ -58,6 +77,23 @@ public class ClientManager {
}
}
+ /**
+ * Handles an attempt by a new client to login as an existing user to the
+ * server. If the user's credentials are valid, then the following can
+ * result:
+ *
+ * - If the user's registration is still pending, they will be sent a
+ * {@link RegistrationStatus.Type#PENDING} response, to indicate that
+ * their registration is still pending approval.
+ * - For non-pending (normal) users, they will be logged into the
+ * server and sent a {@link ServerWelcome} message.
+ *
+ *
+ * @param login The client's login credentials.
+ * @param clientThread The client thread managing the connection.
+ * @throws InvalidIdentificationException If the client's credentials are
+ * incorrect.
+ */
public void handleLogin(ClientLogin login, ClientThread clientThread) throws InvalidIdentificationException {
Document userDoc = this.authService.findAndAuthenticateUser(login);
if (userDoc == null) throw new InvalidIdentificationException("Username or password is incorrect.");
@@ -68,20 +104,38 @@ public class ClientManager {
this.initializePendingClientConnection(userId, username, clientThread);
} else {
String sessionToken = this.authService.generateSessionToken(userId);
- this.initializeClientConnection(new AuthenticationService.ClientConnectionData(userId, username, sessionToken, false), clientThread);
+ this.initializeClientConnection(new ClientConnectionData(userId, username, sessionToken, false), clientThread);
}
}
+ /**
+ * Handles an attempt by a new client to login as an existing user to the
+ * server with a session token from their previous session. If the token is
+ * valid, the user will be logged in and sent a {@link ServerWelcome}
+ * response.
+ *
+ * @param sessionResume The session token data.
+ * @param clientThread The client thread managing the connection.
+ * @throws InvalidIdentificationException If the token is invalid or refers
+ * to a non-existent user.
+ */
public void handleSessionResume(ClientSessionResume sessionResume, ClientThread clientThread) throws InvalidIdentificationException {
Document userDoc = this.authService.findAndAuthenticateUser(sessionResume);
if (userDoc == null) throw new InvalidIdentificationException("Invalid session. Log in to obtain a new session token.");
UUID userId = userDoc.get("id", UUID.class);
String username = userDoc.get("username", String.class);
String sessionToken = this.authService.generateSessionToken(userId);
- this.initializeClientConnection(new AuthenticationService.ClientConnectionData(userId, username, sessionToken, false), clientThread);
+ this.initializeClientConnection(new ClientConnectionData(userId, username, sessionToken, false), clientThread);
}
- public void decidePendingUser(UUID userId, boolean accepted) {
+ /**
+ * Used to accept or reject a pending user's registration. If the given user
+ * is not pending approval, this method does nothing.
+ * @param userId The id of the pending user.
+ * @param accepted Whether to accept or reject.
+ * @param reason The reason for rejection (or acceptance). This may be null.
+ */
+ public void decidePendingUser(UUID userId, boolean accepted, String reason) {
Document userDoc = this.userCollection.find(Filters.and(Filters.eq("id", userId), Filters.eq("pending", true))).firstOrDefault();
if (userDoc != null) {
if (accepted) {
@@ -90,16 +144,16 @@ public class ClientManager {
// If the pending user is still connected, upgrade them to a normal connected client.
var clientThread = this.pendingClients.remove(userId);
if (clientThread != null) {
- clientThread.sendToClient(new RegistrationStatus(RegistrationStatus.Type.ACCEPTED));
+ clientThread.sendToClient(new RegistrationStatus(RegistrationStatus.Type.ACCEPTED, reason));
String username = userDoc.get("username", String.class);
String sessionToken = this.authService.generateSessionToken(userId);
- this.initializeClientConnection(new AuthenticationService.ClientConnectionData(userId, username, sessionToken, true), clientThread);
+ this.initializeClientConnection(new ClientConnectionData(userId, username, sessionToken, true), clientThread);
}
} else {
this.userCollection.remove(userDoc);
var clientThread = this.pendingClients.remove(userId);
if (clientThread != null) {
- clientThread.sendToClient(new RegistrationStatus(RegistrationStatus.Type.REJECTED));
+ clientThread.sendToClient(new RegistrationStatus(RegistrationStatus.Type.REJECTED, reason));
}
}
}
@@ -111,22 +165,30 @@ public class ClientManager {
* @param clientData The data about the client that has connected.
* @param clientThread The thread managing the client's connection.
*/
- private void initializeClientConnection(AuthenticationService.ClientConnectionData clientData, ClientThread clientThread) {
- this.clients.put(clientData.id(), clientThread);
+ private void initializeClientConnection(ClientConnectionData clientData, ClientThread clientThread) {
clientThread.setClientId(clientData.id());
- clientThread.setClientNickname(clientData.nickname());
+ clientThread.setClientNickname(clientData.username());
var defaultChannel = this.server.getChannelManager().getDefaultChannel().orElseThrow();
clientThread.sendToClient(new ServerWelcome(clientData.id(), clientData.sessionToken(), defaultChannel.getId(), defaultChannel.getName(), this.server.getMetaData()));
+ this.clients.put(clientData.id(), clientThread); // We only add the client after sending the welcome, to make sure that we send the welcome packet first.
defaultChannel.addClient(clientThread);
clientThread.setCurrentChannel(defaultChannel);
this.broadcast(new ServerUsers(this.getConnectedClients().toArray(new UserData[0])));
}
+ /**
+ * Initializes a connection to a client whose registration is pending, thus
+ * they should simply keep their connection alive, and receive a {@link RegistrationStatus.Type#PENDING}
+ * message, instead of a {@link ServerWelcome}.
+ * @param clientId The id of the client.
+ * @param pendingUsername The client's username.
+ * @param clientThread The thread managing the client's connection.
+ */
private void initializePendingClientConnection(UUID clientId, String pendingUsername, ClientThread clientThread) {
- this.pendingClients.put(clientId, clientThread);
clientThread.setClientId(clientId);
clientThread.setClientNickname(pendingUsername);
clientThread.sendToClient(RegistrationStatus.pending());
+ this.pendingClients.put(clientId, clientThread);
}
/**
@@ -166,6 +228,9 @@ public class ClientManager {
}
}
+ /**
+ * @return The list of connected clients.
+ */
public List getConnectedClients() {
return this.clients.values().stream()
.sorted(Comparator.comparing(ClientThread::getClientNickname))
@@ -173,6 +238,9 @@ public class ClientManager {
.collect(Collectors.toList());
}
+ /**
+ * @return The list of connected, pending clients.
+ */
public List getPendingClients() {
return this.pendingClients.values().stream()
.sorted(Comparator.comparing(ClientThread::getClientNickname))
@@ -180,14 +248,27 @@ public class ClientManager {
.collect(Collectors.toList());
}
+ /**
+ * @return The set of ids of all connected clients.
+ */
public Set getConnectedIds() {
return this.clients.keySet();
}
+ /**
+ * Tries to find a connected client with the given id.
+ * @param id The id to look for.
+ * @return An optional client thread.
+ */
public Optional getClientById(UUID id) {
return Optional.ofNullable(this.clients.get(id));
}
+ /**
+ * Tries to find a pending client with the given id.
+ * @param id The id to look for.
+ * @return An optional client thread.
+ */
public Optional getPendingClientById(UUID id) {
return Optional.ofNullable(this.pendingClients.get(id));
}
From d87577fc0bbef65c0397cd0f76f4d617c7856948 Mon Sep 17 00:00:00 2001
From: Andrew Lalis
Date: Tue, 28 Sep 2021 23:45:51 +0200
Subject: [PATCH 5/5] Added tons of javadoc, finalized server-side registration
protocol.
---
docs/.nojekyll | 0
docs/index.html | 15 +++++++++++++++
2 files changed, 15 insertions(+)
create mode 100644 docs/.nojekyll
create mode 100644 docs/index.html
diff --git a/docs/.nojekyll b/docs/.nojekyll
new file mode 100644
index 0000000..e69de29
diff --git a/docs/index.html b/docs/index.html
new file mode 100644
index 0000000..ea8117c
--- /dev/null
+++ b/docs/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Concord
+
+
+
+Concord
+
+ More content coming soon!
+
+
+
+
\ No newline at end of file