Added tons of javadoc, finalized server-side registration protocol.
This commit is contained in:
		
							parent
							
								
									1bb446ff5b
								
							
						
					
					
						commit
						6b7712a9fb
					
				| 
						 | 
					@ -1,6 +1,5 @@
 | 
				
			||||||
package nl.andrewl.concord_client;
 | 
					package nl.andrewl.concord_client;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import com.fasterxml.jackson.databind.ObjectMapper;
 | 
					 | 
				
			||||||
import com.googlecode.lanterna.gui2.MultiWindowTextGUI;
 | 
					import com.googlecode.lanterna.gui2.MultiWindowTextGUI;
 | 
				
			||||||
import com.googlecode.lanterna.gui2.Window;
 | 
					import com.googlecode.lanterna.gui2.Window;
 | 
				
			||||||
import com.googlecode.lanterna.gui2.WindowBasedTextGUI;
 | 
					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.DefaultTerminalFactory;
 | 
				
			||||||
import com.googlecode.lanterna.terminal.Terminal;
 | 
					import com.googlecode.lanterna.terminal.Terminal;
 | 
				
			||||||
import lombok.Getter;
 | 
					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.EventManager;
 | 
				
			||||||
import nl.andrewl.concord_client.event.handlers.ChannelMovedHandler;
 | 
					import nl.andrewl.concord_client.event.handlers.ChannelMovedHandler;
 | 
				
			||||||
import nl.andrewl.concord_client.event.handlers.ChatHistoryResponseHandler;
 | 
					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.Encryption;
 | 
				
			||||||
import nl.andrewl.concord_core.msg.Message;
 | 
					import nl.andrewl.concord_core.msg.Message;
 | 
				
			||||||
import nl.andrewl.concord_core.msg.Serializer;
 | 
					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.channel.MoveToChannel;
 | 
				
			||||||
import nl.andrewl.concord_core.msg.types.chat.Chat;
 | 
					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.ChatHistoryRequest;
 | 
				
			||||||
import nl.andrewl.concord_core.msg.types.chat.ChatHistoryResponse;
 | 
					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.*;
 | 
				
			||||||
import nl.andrewl.concord_core.msg.types.client_setup.ServerWelcome;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.io.IOException;
 | 
					import java.io.IOException;
 | 
				
			||||||
import java.io.InputStream;
 | 
					import java.io.InputStream;
 | 
				
			||||||
import java.io.OutputStream;
 | 
					import java.io.OutputStream;
 | 
				
			||||||
import java.net.Socket;
 | 
					import java.net.Socket;
 | 
				
			||||||
import java.nio.file.Files;
 | 
					 | 
				
			||||||
import java.nio.file.Path;
 | 
					import java.nio.file.Path;
 | 
				
			||||||
import java.security.GeneralSecurityException;
 | 
					import java.security.GeneralSecurityException;
 | 
				
			||||||
import java.util.HashMap;
 | 
					 | 
				
			||||||
import java.util.List;
 | 
					import java.util.List;
 | 
				
			||||||
import java.util.Map;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class ConcordClient implements Runnable {
 | 
					public class ConcordClient implements Runnable {
 | 
				
			||||||
	private final Socket socket;
 | 
						private final Socket socket;
 | 
				
			||||||
	private final InputStream in;
 | 
						private final InputStream in;
 | 
				
			||||||
	private final OutputStream out;
 | 
						private final OutputStream out;
 | 
				
			||||||
	private final Serializer serializer;
 | 
						private final Serializer serializer;
 | 
				
			||||||
 | 
						private final ClientDataStore dataStore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@Getter
 | 
						@Getter
 | 
				
			||||||
	private final ClientModel model;
 | 
						private ClientModel model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private final EventManager eventManager;
 | 
						private final EventManager eventManager;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private volatile boolean running;
 | 
						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.eventManager = new EventManager(this);
 | 
				
			||||||
		this.socket = new Socket(host, port);
 | 
							this.socket = new Socket(host, port);
 | 
				
			||||||
		this.serializer = new Serializer();
 | 
							this.serializer = new Serializer();
 | 
				
			||||||
 | 
							this.dataStore = new JsonClientDataStore(Path.of("concord-session-tokens.json"));
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			var streams = Encryption.upgrade(socket.getInputStream(), socket.getOutputStream(), this.serializer);
 | 
								var streams = Encryption.upgrade(socket.getInputStream(), socket.getOutputStream(), this.serializer);
 | 
				
			||||||
			this.in = streams.first();
 | 
								this.in = streams.first();
 | 
				
			||||||
| 
						 | 
					@ -62,8 +62,6 @@ public class ConcordClient implements Runnable {
 | 
				
			||||||
		} catch (GeneralSecurityException e) {
 | 
							} catch (GeneralSecurityException e) {
 | 
				
			||||||
			throw new IOException("Could not establish secure connection to the server.", e);
 | 
								throw new IOException("Could not establish secure connection to the server.", e);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		this.model = this.initializeConnectionToServer(nickname, tokensFile);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Add event listeners.
 | 
							// Add event listeners.
 | 
				
			||||||
		this.eventManager.addHandler(MoveToChannel.class, new ChannelMovedHandler());
 | 
							this.eventManager.addHandler(MoveToChannel.class, new ChannelMovedHandler());
 | 
				
			||||||
		this.eventManager.addHandler(ServerUsers.class, new ServerUsersHandler());
 | 
							this.eventManager.addHandler(ServerUsers.class, new ServerUsersHandler());
 | 
				
			||||||
| 
						 | 
					@ -72,32 +70,63 @@ public class ConcordClient implements Runnable {
 | 
				
			||||||
		this.eventManager.addHandler(ServerMetaData.class, new ServerMetaDataHandler());
 | 
							this.eventManager.addHandler(ServerMetaData.class, new ServerMetaDataHandler());
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						public static ConcordClient register(String host, int port, String username, String password) throws IOException {
 | 
				
			||||||
	 * Initializes the communication with the server by sending an {@link Identification}
 | 
							var client = new ConcordClient(host, port);
 | 
				
			||||||
	 * message, and waiting for a {@link ServerWelcome} response from the
 | 
							client.sendMessage(new ClientRegistration(null, null, username, password));
 | 
				
			||||||
	 * server. After that, we request some information about the channel we were
 | 
							Message reply = client.serializer.readMessage(client.in);
 | 
				
			||||||
	 * placed in by the server.
 | 
							if (reply instanceof RegistrationStatus status) {
 | 
				
			||||||
	 * @param nickname The nickname to send to the server that it should know
 | 
								if (status.type() == RegistrationStatus.Type.ACCEPTED) {
 | 
				
			||||||
	 *                 us by.
 | 
									ServerWelcome welcomeData = (ServerWelcome) client.serializer.readMessage(client.in);
 | 
				
			||||||
	 * @param tokensFile Path to the file where session tokens are stored.
 | 
									client.initializeClientModel(welcomeData, username);
 | 
				
			||||||
	 * @return The client model that contains the server's metadata and other
 | 
								} else if (status.type() == RegistrationStatus.Type.PENDING) {
 | 
				
			||||||
	 * information that should be kept up-to-date at runtime.
 | 
									System.out.println("Registration pending!");
 | 
				
			||||||
	 * @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;
 | 
					 | 
				
			||||||
		} else {
 | 
							} 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 {
 | 
						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<String, String> 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<String, String> 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 {
 | 
						public static void main(String[] args) throws IOException {
 | 
				
			||||||
		Terminal term = new DefaultTerminalFactory().createTerminal();
 | 
							Terminal term = new DefaultTerminalFactory().createTerminal();
 | 
				
			||||||
		Screen screen = new TerminalScreen(term);
 | 
							Screen screen = new TerminalScreen(term);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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<String> getSessionToken(String serverName) throws IOException;
 | 
				
			||||||
 | 
						void saveSessionToken(String serverName, String sessionToken) throws IOException;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -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<String> getSessionToken(String serverName) throws IOException {
 | 
				
			||||||
 | 
							String token = null;
 | 
				
			||||||
 | 
							if (Files.exists(file)) {
 | 
				
			||||||
 | 
								ObjectMapper mapper = new ObjectMapper();
 | 
				
			||||||
 | 
								Map<String, String> 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<String, String> 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);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -50,7 +50,7 @@ public class MainWindow extends BasicWindow {
 | 
				
			||||||
		if (nickname == null) return;
 | 
							if (nickname == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		try {
 | 
							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);
 | 
								var chatPanel = new ServerPanel(client, this);
 | 
				
			||||||
			client.getModel().addListener(chatPanel);
 | 
								client.getModel().addListener(chatPanel);
 | 
				
			||||||
			new Thread(client).start();
 | 
								new Thread(client).start();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,7 +12,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Getter
 | 
					@Getter
 | 
				
			||||||
public class ClientModel {
 | 
					public class ClientModel {
 | 
				
			||||||
	private UUID id;
 | 
						private final UUID id;
 | 
				
			||||||
	private String nickname;
 | 
						private String nickname;
 | 
				
			||||||
	private ServerMetaData serverMetaData;
 | 
						private ServerMetaData serverMetaData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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.
 | 
				
			||||||
 | 
					 * <p>
 | 
				
			||||||
 | 
					 *     This core module defines the message protocol that clients must use to
 | 
				
			||||||
 | 
					 *     communicate with any server.
 | 
				
			||||||
 | 
					 * </p>
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
module concord_core {
 | 
					module concord_core {
 | 
				
			||||||
	requires static lombok;
 | 
						requires static lombok;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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;
 | 
				
			||||||
| 
						 | 
					@ -5,11 +5,10 @@ import nl.andrewl.concord_core.msg.Message;
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Error message which can be sent between either the server or client to
 | 
					 * Error message which can be sent between either the server or client to
 | 
				
			||||||
 * indicate an unsavory situation.
 | 
					 * indicate an unsavory situation.
 | 
				
			||||||
 | 
					 * @param level The severity level of the error.
 | 
				
			||||||
 | 
					 * @param message A message indicating what went wrong.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
public record Error (
 | 
					public record Error (Level level, String message) implements Message {
 | 
				
			||||||
		Level level,
 | 
					 | 
				
			||||||
		String message
 | 
					 | 
				
			||||||
) implements Message {
 | 
					 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * The error level gives an indication as to the severity of the error.
 | 
						 * 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
 | 
						 * Warnings indicate that a user has attempted to do something which they
 | 
				
			||||||
| 
						 | 
					@ -18,10 +17,20 @@ public record Error (
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public enum Level {WARNING, 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) {
 | 
						public static Error warning(String message) {
 | 
				
			||||||
		return new Error(Level.WARNING, 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) {
 | 
						public static Error error(String message) {
 | 
				
			||||||
		return new Error(Level.ERROR, message);
 | 
							return new Error(Level.ERROR, message);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Messages pertaining to channel interaction and updates.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					package nl.andrewl.concord_core.msg.types.channel;
 | 
				
			||||||
| 
						 | 
					@ -8,9 +8,7 @@ import java.util.UUID;
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * This message contains information about a chat message that a user sent.
 | 
					 * This message contains information about a chat message that a user sent.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
public record Chat (
 | 
					public record Chat (UUID id, UUID senderId, String senderNickname, long timestamp, String message) implements Message {
 | 
				
			||||||
		UUID id, UUID senderId, String senderNickname, long timestamp, String message
 | 
					 | 
				
			||||||
) implements Message {
 | 
					 | 
				
			||||||
	public Chat(UUID senderId, String senderNickname, long timestamp, String message) {
 | 
						public Chat(UUID senderId, String senderNickname, long timestamp, String message) {
 | 
				
			||||||
		this(null, senderId, senderNickname, timestamp, message);
 | 
							this(null, senderId, senderNickname, timestamp, message);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,6 +24,7 @@ import java.util.stream.Collectors;
 | 
				
			||||||
 * </p>
 | 
					 * </p>
 | 
				
			||||||
 * <p>
 | 
					 * <p>
 | 
				
			||||||
 *     The following query parameters are supported:
 | 
					 *     The following query parameters are supported:
 | 
				
			||||||
 | 
					 * </p>
 | 
				
			||||||
 *     <ul>
 | 
					 *     <ul>
 | 
				
			||||||
 *         <li><code>count</code> - Fetch up to N messages. Minimum of 1, and
 | 
					 *         <li><code>count</code> - Fetch up to N messages. Minimum of 1, and
 | 
				
			||||||
 *         a server-specific maximum count, usually no higher than 1000.</li>
 | 
					 *         a server-specific maximum count, usually no higher than 1000.</li>
 | 
				
			||||||
| 
						 | 
					@ -37,7 +38,6 @@ import java.util.stream.Collectors;
 | 
				
			||||||
 *         is present, all others are ignored, and a list containing the single
 | 
					 *         is present, all others are ignored, and a list containing the single
 | 
				
			||||||
 *         message is returned, if it could be found, otherwise an empty list.</li>
 | 
					 *         message is returned, if it could be found, otherwise an empty list.</li>
 | 
				
			||||||
 *     </ul>
 | 
					 *     </ul>
 | 
				
			||||||
 * </p>
 | 
					 | 
				
			||||||
 * <p>
 | 
					 * <p>
 | 
				
			||||||
 *     Responses to this request are sent via {@link ChatHistoryResponse}, where
 | 
					 *     Responses to this request are sent via {@link ChatHistoryResponse}, where
 | 
				
			||||||
 *     the list of messages is always sorted by the timestamp.
 | 
					 *     the list of messages is always sorted by the timestamp.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,5 +7,7 @@ import java.util.UUID;
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * The response that a server sends to a {@link ChatHistoryRequest}. The list of
 | 
					 * The response that a server sends to a {@link ChatHistoryRequest}. The list of
 | 
				
			||||||
 * messages is ordered by timestamp, with the newest messages appearing first.
 | 
					 * 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 {}
 | 
					public record ChatHistoryResponse (UUID channelId, Chat[] messages) implements Message {}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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;
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
					 * 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.
 | 
					 * 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 {}
 | 
					public record KeyData (byte[] iv, byte[] salt, byte[] publicKey) implements Message {}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
					 * A response from the server which indicates the current status of the client's
 | 
				
			||||||
 * registration request.
 | 
					 * 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 enum Type {PENDING, ACCEPTED, REJECTED}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static RegistrationStatus pending() {
 | 
						public static RegistrationStatus pending() {
 | 
				
			||||||
		return new RegistrationStatus(Type.PENDING);
 | 
							return new RegistrationStatus(Type.PENDING, null);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Messages pertaining to the establishment of a connection with clients.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					package nl.andrewl.concord_core.msg.types.client_setup;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,10 @@
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Contains all the various message types which can be sent between the server
 | 
				
			||||||
 | 
					 * and client.
 | 
				
			||||||
 | 
					 * <p>
 | 
				
			||||||
 | 
					 *     <em>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.</em>
 | 
				
			||||||
 | 
					 * </p>
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					package nl.andrewl.concord_core.msg.types;
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,8 @@
 | 
				
			||||||
package nl.andrewl.concord_core.util;
 | 
					package nl.andrewl.concord_core.util;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Simple generic pair of two objects.
 | 
				
			||||||
 | 
					 * @param <A> The first object.
 | 
				
			||||||
 | 
					 * @param <B> The second object.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
public record Pair<A, B>(A first, B second) {}
 | 
					public record Pair<A, B>(A first, B second) {}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,9 @@
 | 
				
			||||||
package nl.andrewl.concord_core.util;
 | 
					package nl.andrewl.concord_core.util;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Simple generic triple of objects.
 | 
				
			||||||
 | 
					 * @param <A> The first object.
 | 
				
			||||||
 | 
					 * @param <B> The second object.
 | 
				
			||||||
 | 
					 * @param <C> The third object.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
public record Triple<A, B, C> (A first, B second, C third) {}
 | 
					public record Triple<A, B, C> (A first, B second, C third) {}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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;
 | 
				
			||||||
							
								
								
									
										19
									
								
								pom.xml
								
								
								
								
							
							
						
						
									
										19
									
								
								pom.xml
								
								
								
								
							| 
						 | 
					@ -16,9 +16,10 @@
 | 
				
			||||||
    </modules>
 | 
					    </modules>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <properties>
 | 
					    <properties>
 | 
				
			||||||
        <maven.compiler.source>16</maven.compiler.source>
 | 
					        <maven.compiler.source>17</maven.compiler.source>
 | 
				
			||||||
        <maven.compiler.target>16</maven.compiler.target>
 | 
					        <maven.compiler.target>17</maven.compiler.target>
 | 
				
			||||||
        <java.version>16</java.version>
 | 
					        <maven.compiler.release>17</maven.compiler.release>
 | 
				
			||||||
 | 
					        <java.version>17</java.version>
 | 
				
			||||||
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 | 
					        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 | 
				
			||||||
    </properties>
 | 
					    </properties>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -47,6 +48,18 @@
 | 
				
			||||||
                    </annotationProcessorPaths>
 | 
					                    </annotationProcessorPaths>
 | 
				
			||||||
                </configuration>
 | 
					                </configuration>
 | 
				
			||||||
            </plugin>
 | 
					            </plugin>
 | 
				
			||||||
 | 
					            <plugin>
 | 
				
			||||||
 | 
					                <groupId>org.apache.maven.plugins</groupId>
 | 
				
			||||||
 | 
					                <artifactId>maven-javadoc-plugin</artifactId>
 | 
				
			||||||
 | 
					                <version>3.3.1</version>
 | 
				
			||||||
 | 
					                <configuration>
 | 
				
			||||||
 | 
					                    <detectLinks>false</detectLinks>
 | 
				
			||||||
 | 
					                    <detectOfflineLinks>false</detectOfflineLinks>
 | 
				
			||||||
 | 
					                    <failOnError>false</failOnError>
 | 
				
			||||||
 | 
					                    <failOnWarnings>false</failOnWarnings>
 | 
				
			||||||
 | 
					                    <show>private</show>
 | 
				
			||||||
 | 
					                </configuration>
 | 
				
			||||||
 | 
					            </plugin>
 | 
				
			||||||
        </plugins>
 | 
					        </plugins>
 | 
				
			||||||
    </build>
 | 
					    </build>
 | 
				
			||||||
</project>
 | 
					</project>
 | 
				
			||||||
| 
						 | 
					@ -124,6 +124,9 @@ public class ConcordServer implements Runnable {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * @return The server's metadata.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
	public ServerMetaData getMetaData() {
 | 
						public ServerMetaData getMetaData() {
 | 
				
			||||||
		return new ServerMetaData(
 | 
							return new ServerMetaData(
 | 
				
			||||||
				this.config.getName(),
 | 
									this.config.getName(),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,7 +33,7 @@ public class Channel implements Comparable<Channel> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * A document collection which holds all messages created in this channel,
 | 
						 * 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;
 | 
						private final NitriteCollection messageCollection;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,8 +24,6 @@ import java.util.UUID;
 | 
				
			||||||
 * and logging in.
 | 
					 * and logging in.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
public class AuthenticationService {
 | 
					public class AuthenticationService {
 | 
				
			||||||
	public static record ClientConnectionData(UUID id, String nickname, String sessionToken, boolean newClient) {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private final NitriteCollection userCollection;
 | 
						private final NitriteCollection userCollection;
 | 
				
			||||||
	private final NitriteCollection sessionTokenCollection;
 | 
						private final NitriteCollection sessionTokenCollection;
 | 
				
			||||||
	private final ConcordServer server;
 | 
						private final ConcordServer server;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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) {}
 | 
				
			||||||
| 
						 | 
					@ -27,9 +27,12 @@ public class ClientManager {
 | 
				
			||||||
	private final Map<UUID, ClientThread> clients;
 | 
						private final Map<UUID, ClientThread> clients;
 | 
				
			||||||
	private final Map<UUID, ClientThread> pendingClients;
 | 
						private final Map<UUID, ClientThread> pendingClients;
 | 
				
			||||||
	private final NitriteCollection userCollection;
 | 
						private final NitriteCollection userCollection;
 | 
				
			||||||
 | 
					 | 
				
			||||||
	private final AuthenticationService authService;
 | 
						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) {
 | 
						public ClientManager(ConcordServer server) {
 | 
				
			||||||
		this.server = server;
 | 
							this.server = server;
 | 
				
			||||||
		this.clients = new ConcurrentHashMap<>();
 | 
							this.clients = new ConcurrentHashMap<>();
 | 
				
			||||||
| 
						 | 
					@ -45,12 +48,28 @@ public class ClientManager {
 | 
				
			||||||
		server.getScheduledExecutorService().scheduleAtFixedRate(this.authService::removeExpiredSessionTokens, 1, 1, TimeUnit.DAYS);
 | 
							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 {
 | 
						public void handleRegistration(ClientRegistration registration, ClientThread clientThread) throws InvalidIdentificationException {
 | 
				
			||||||
		Document userDoc = this.userCollection.find(Filters.eq("username", registration.username())).firstOrDefault();
 | 
							Document userDoc = this.userCollection.find(Filters.eq("username", registration.username())).firstOrDefault();
 | 
				
			||||||
		if (userDoc != null) throw new InvalidIdentificationException("Username is taken.");
 | 
							if (userDoc != null) throw new InvalidIdentificationException("Username is taken.");
 | 
				
			||||||
		if (this.server.getConfig().isAcceptAllNewClients()) {
 | 
							if (this.server.getConfig().isAcceptAllNewClients()) {
 | 
				
			||||||
			var clientData = this.authService.registerNewClient(registration);
 | 
								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);
 | 
								this.initializeClientConnection(clientData, clientThread);
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			var clientId = this.authService.registerPendingClient(registration);
 | 
								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:
 | 
				
			||||||
 | 
						 * <ul>
 | 
				
			||||||
 | 
						 *     <li>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.</li>
 | 
				
			||||||
 | 
						 *     <li>For non-pending (normal) users, they will be logged into the
 | 
				
			||||||
 | 
						 *     server and sent a {@link ServerWelcome} message.</li>
 | 
				
			||||||
 | 
						 * </ul>
 | 
				
			||||||
 | 
						 *
 | 
				
			||||||
 | 
						 * @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 {
 | 
						public void handleLogin(ClientLogin login, ClientThread clientThread) throws InvalidIdentificationException {
 | 
				
			||||||
		Document userDoc = this.authService.findAndAuthenticateUser(login);
 | 
							Document userDoc = this.authService.findAndAuthenticateUser(login);
 | 
				
			||||||
		if (userDoc == null) throw new InvalidIdentificationException("Username or password is incorrect.");
 | 
							if (userDoc == null) throw new InvalidIdentificationException("Username or password is incorrect.");
 | 
				
			||||||
| 
						 | 
					@ -68,20 +104,38 @@ public class ClientManager {
 | 
				
			||||||
			this.initializePendingClientConnection(userId, username, clientThread);
 | 
								this.initializePendingClientConnection(userId, username, clientThread);
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			String sessionToken = this.authService.generateSessionToken(userId);
 | 
								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 {
 | 
						public void handleSessionResume(ClientSessionResume sessionResume, ClientThread clientThread) throws InvalidIdentificationException {
 | 
				
			||||||
		Document userDoc = this.authService.findAndAuthenticateUser(sessionResume);
 | 
							Document userDoc = this.authService.findAndAuthenticateUser(sessionResume);
 | 
				
			||||||
		if (userDoc == null) throw new InvalidIdentificationException("Invalid session. Log in to obtain a new session token.");
 | 
							if (userDoc == null) throw new InvalidIdentificationException("Invalid session. Log in to obtain a new session token.");
 | 
				
			||||||
		UUID userId = userDoc.get("id", UUID.class);
 | 
							UUID userId = userDoc.get("id", UUID.class);
 | 
				
			||||||
		String username = userDoc.get("username", String.class);
 | 
							String username = userDoc.get("username", String.class);
 | 
				
			||||||
		String sessionToken = this.authService.generateSessionToken(userId);
 | 
							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();
 | 
							Document userDoc = this.userCollection.find(Filters.and(Filters.eq("id", userId), Filters.eq("pending", true))).firstOrDefault();
 | 
				
			||||||
		if (userDoc != null) {
 | 
							if (userDoc != null) {
 | 
				
			||||||
			if (accepted) {
 | 
								if (accepted) {
 | 
				
			||||||
| 
						 | 
					@ -90,16 +144,16 @@ public class ClientManager {
 | 
				
			||||||
				// If the pending user is still connected, upgrade them to a normal connected client.
 | 
									// If the pending user is still connected, upgrade them to a normal connected client.
 | 
				
			||||||
				var clientThread = this.pendingClients.remove(userId);
 | 
									var clientThread = this.pendingClients.remove(userId);
 | 
				
			||||||
				if (clientThread != null) {
 | 
									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 username = userDoc.get("username", String.class);
 | 
				
			||||||
					String sessionToken = this.authService.generateSessionToken(userId);
 | 
										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 {
 | 
								} else {
 | 
				
			||||||
				this.userCollection.remove(userDoc);
 | 
									this.userCollection.remove(userDoc);
 | 
				
			||||||
				var clientThread = this.pendingClients.remove(userId);
 | 
									var clientThread = this.pendingClients.remove(userId);
 | 
				
			||||||
				if (clientThread != null) {
 | 
									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 clientData The data about the client that has connected.
 | 
				
			||||||
	 * @param clientThread The thread managing the client's connection.
 | 
						 * @param clientThread The thread managing the client's connection.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	private void initializeClientConnection(AuthenticationService.ClientConnectionData clientData, ClientThread clientThread) {
 | 
						private void initializeClientConnection(ClientConnectionData clientData, ClientThread clientThread) {
 | 
				
			||||||
		this.clients.put(clientData.id(), clientThread);
 | 
					 | 
				
			||||||
		clientThread.setClientId(clientData.id());
 | 
							clientThread.setClientId(clientData.id());
 | 
				
			||||||
		clientThread.setClientNickname(clientData.nickname());
 | 
							clientThread.setClientNickname(clientData.username());
 | 
				
			||||||
		var defaultChannel = this.server.getChannelManager().getDefaultChannel().orElseThrow();
 | 
							var defaultChannel = this.server.getChannelManager().getDefaultChannel().orElseThrow();
 | 
				
			||||||
		clientThread.sendToClient(new ServerWelcome(clientData.id(), clientData.sessionToken(), defaultChannel.getId(), defaultChannel.getName(), this.server.getMetaData()));
 | 
							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);
 | 
							defaultChannel.addClient(clientThread);
 | 
				
			||||||
		clientThread.setCurrentChannel(defaultChannel);
 | 
							clientThread.setCurrentChannel(defaultChannel);
 | 
				
			||||||
		this.broadcast(new ServerUsers(this.getConnectedClients().toArray(new UserData[0])));
 | 
							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) {
 | 
						private void initializePendingClientConnection(UUID clientId, String pendingUsername, ClientThread clientThread) {
 | 
				
			||||||
		this.pendingClients.put(clientId, clientThread);
 | 
					 | 
				
			||||||
		clientThread.setClientId(clientId);
 | 
							clientThread.setClientId(clientId);
 | 
				
			||||||
		clientThread.setClientNickname(pendingUsername);
 | 
							clientThread.setClientNickname(pendingUsername);
 | 
				
			||||||
		clientThread.sendToClient(RegistrationStatus.pending());
 | 
							clientThread.sendToClient(RegistrationStatus.pending());
 | 
				
			||||||
 | 
							this.pendingClients.put(clientId, clientThread);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
| 
						 | 
					@ -166,6 +228,9 @@ public class ClientManager {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * @return The list of connected clients.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
	public List<UserData> getConnectedClients() {
 | 
						public List<UserData> getConnectedClients() {
 | 
				
			||||||
		return this.clients.values().stream()
 | 
							return this.clients.values().stream()
 | 
				
			||||||
				.sorted(Comparator.comparing(ClientThread::getClientNickname))
 | 
									.sorted(Comparator.comparing(ClientThread::getClientNickname))
 | 
				
			||||||
| 
						 | 
					@ -173,6 +238,9 @@ public class ClientManager {
 | 
				
			||||||
				.collect(Collectors.toList());
 | 
									.collect(Collectors.toList());
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * @return The list of connected, pending clients.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
	public List<UserData> getPendingClients() {
 | 
						public List<UserData> getPendingClients() {
 | 
				
			||||||
		return this.pendingClients.values().stream()
 | 
							return this.pendingClients.values().stream()
 | 
				
			||||||
				.sorted(Comparator.comparing(ClientThread::getClientNickname))
 | 
									.sorted(Comparator.comparing(ClientThread::getClientNickname))
 | 
				
			||||||
| 
						 | 
					@ -180,14 +248,27 @@ public class ClientManager {
 | 
				
			||||||
				.collect(Collectors.toList());
 | 
									.collect(Collectors.toList());
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * @return The set of ids of all connected clients.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
	public Set<UUID> getConnectedIds() {
 | 
						public Set<UUID> getConnectedIds() {
 | 
				
			||||||
		return this.clients.keySet();
 | 
							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<ClientThread> getClientById(UUID id) {
 | 
						public Optional<ClientThread> getClientById(UUID id) {
 | 
				
			||||||
		return Optional.ofNullable(this.clients.get(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<ClientThread> getPendingClientById(UUID id) {
 | 
						public Optional<ClientThread> getPendingClientById(UUID id) {
 | 
				
			||||||
		return Optional.ofNullable(this.pendingClients.get(id));
 | 
							return Optional.ofNullable(this.pendingClients.get(id));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue