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
+ * This core module defines the message protocol that clients must use to
+ * communicate with any server.
+ *
* 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.* 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