diff --git a/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChannelUsersResponseHandler.java b/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChannelUsersResponseHandler.java deleted file mode 100644 index 5cbe745..0000000 --- a/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChannelUsersResponseHandler.java +++ /dev/null @@ -1,16 +0,0 @@ -package nl.andrewl.concord_client.event.handlers; - -import nl.andrewl.concord_client.ConcordClient; -import nl.andrewl.concord_client.event.MessageHandler; -import nl.andrewl.concord_core.msg.types.ChannelUsersResponse; - -/** - * When the client receives information about the list of known users, it will - * update its model to show the new list. - */ -public class ChannelUsersResponseHandler implements MessageHandler { - @Override - public void handle(ChannelUsersResponse msg, ConcordClient client) throws Exception { - client.getModel().setKnownUsers(msg.getUsers()); - } -} diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/Encryption.java b/core/src/main/java/nl/andrewl/concord_core/msg/Encryption.java index ebc5610..2590c60 100644 --- a/core/src/main/java/nl/andrewl/concord_core/msg/Encryption.java +++ b/core/src/main/java/nl/andrewl/concord_core/msg/Encryption.java @@ -23,6 +23,26 @@ import java.util.List; * Utility class for handling the establishment of encrypted communication. */ public class Encryption { + /** + * Upgrades the given input and output streams to a pair of cipher input and + * output streams. This upgrade follows the following steps: + *
    + *
  1. Generate an elliptic curve key pair, and send the public key to the output stream.
  2. + *
  3. Read the public key that the other person has sent, from the input stream.
  4. + *
  5. Compute a shared private key using the ECDH key exchange, with our private key and their public key.
  6. + *
  7. Create the cipher streams from the shared private key.
  8. + *
+ * @param in The unencrypted input stream. + * @param out The unencrypted output stream. + * @param serializer The message serializer that is used to read and write + * messages according to the standard Concord protocol. + * @return The pair of cipher streams, which can be used to send encrypted + * messages. + * @throws GeneralSecurityException If an error occurs while generating keys + * or preparing the cipher streams. + * @throws IOException If an error occurs while reading or writing data on + * the streams. + */ public static Pair upgrade( InputStream in, OutputStream out, 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 813c79f..263a089 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 @@ -36,7 +36,7 @@ public class Serializer { registerType(3, MoveToChannel.class); registerType(4, ChatHistoryRequest.class); registerType(5, ChatHistoryResponse.class); - registerType(6, ChannelUsersRequest.class); + // Type id 6 removed due to deprecation. registerType(7, ServerUsers.class); registerType(8, ServerMetaData.class); registerType(9, Error.class); diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/ChannelUsersRequest.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/ChannelUsersRequest.java deleted file mode 100644 index 2696441..0000000 --- a/core/src/main/java/nl/andrewl/concord_core/msg/types/ChannelUsersRequest.java +++ /dev/null @@ -1,36 +0,0 @@ -package nl.andrewl.concord_core.msg.types; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import nl.andrewl.concord_core.msg.Message; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.util.UUID; - -import static nl.andrewl.concord_core.msg.MessageUtils.*; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@Deprecated -public class ChannelUsersRequest implements Message { - private UUID channelId; - - @Override - public int getByteCount() { - return UUID_BYTES; - } - - @Override - public void write(DataOutputStream o) throws IOException { - writeUUID(this.channelId, o); - } - - @Override - public void read(DataInputStream i) throws IOException { - this.channelId = readUUID(i); - } -} diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/ChannelUsersResponse.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/ChannelUsersResponse.java deleted file mode 100644 index 80ab9c9..0000000 --- a/core/src/main/java/nl/andrewl/concord_core/msg/types/ChannelUsersResponse.java +++ /dev/null @@ -1,43 +0,0 @@ -package nl.andrewl.concord_core.msg.types; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import nl.andrewl.concord_core.msg.Message; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.util.List; - -import static nl.andrewl.concord_core.msg.MessageUtils.*; - -/** - * This message is sent from the server to the client when the information about - * the users in the channel that a client is in has changed. For example, when - * a user leaves a channel, all others in that channel will be sent this message - * to indicate that update. - * @deprecated Clients will be updated via a {@link ServerUsers} message. - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -@Deprecated -public class ChannelUsersResponse implements Message { - private List users; - - @Override - public int getByteCount() { - return getByteSize(this.users); - } - - @Override - public void write(DataOutputStream o) throws IOException { - writeList(this.users, o); - } - - @Override - public void read(DataInputStream i) throws IOException { - this.users = readList(UserData.class, i); - } -} diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/ChatHistoryRequest.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/ChatHistoryRequest.java index c872556..f45f61a 100644 --- a/core/src/main/java/nl/andrewl/concord_core/msg/types/ChatHistoryRequest.java +++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/ChatHistoryRequest.java @@ -77,6 +77,10 @@ public class ChatHistoryRequest implements Message { .collect(Collectors.joining(";")); } + /** + * Utility method to extract the query string's values as a key-value map. + * @return A map of the query parameters. + */ public Map getQueryAsMap() { String[] pairs = this.query.split(";"); if (pairs.length == 0) return Map.of(); 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 4c16b60..d599f3c 100644 --- a/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java +++ b/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java @@ -145,7 +145,7 @@ public class ConcordServer implements Runnable { private void shutdown() { System.out.println("Shutting down the server."); for (var clientId : this.clientManager.getConnectedIds()) { - this.clientManager.deregisterClient(clientId); + this.clientManager.handleLogOut(clientId); } this.scheduledExecutorService.shutdown(); this.executorService.shutdown(); @@ -161,13 +161,7 @@ public class ConcordServer implements Runnable { public void run() { this.running = true; this.scheduledExecutorService.scheduleAtFixedRate(this.discoveryServerPublisher::publish, 0, 1, TimeUnit.MINUTES); - StringBuilder startupMessage = new StringBuilder(); - startupMessage.append("Opened server on port ").append(config.getPort()).append("\n") - .append("The following channels are available:\n"); - for (var channel : this.channelManager.getChannels()) { - startupMessage.append("\tChannel \"").append(channel).append('\n'); - } - System.out.println(startupMessage); + System.out.printf("Opened server on port %d.\n", config.getPort()); while (this.running) { try { Socket socket = this.serverSocket.accept(); 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 50b9ae3..777030f 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 @@ -21,11 +21,14 @@ import java.util.concurrent.ConcurrentHashMap; * clients in a server. */ @Getter -public class Channel { +public class Channel implements Comparable { private final ConcordServer server; private final UUID id; private String name; + /** + * The set of clients that are connected to this channel. + */ private final Set connectedClients; /** @@ -94,6 +97,10 @@ public class Channel { return users; } + public String getAsTag() { + return "#" + this.name; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -111,4 +118,9 @@ public class Channel { public String toString() { return this.name + " (" + this.id + ")"; } + + @Override + public int compareTo(Channel o) { + return this.getName().compareTo(o.getName()); + } } diff --git a/server/src/main/java/nl/andrewl/concord_server/cli/ServerCli.java b/server/src/main/java/nl/andrewl/concord_server/cli/ServerCli.java index 4aca00a..b36631a 100644 --- a/server/src/main/java/nl/andrewl/concord_server/cli/ServerCli.java +++ b/server/src/main/java/nl/andrewl/concord_server/cli/ServerCli.java @@ -1,9 +1,8 @@ package nl.andrewl.concord_server.cli; import nl.andrewl.concord_server.ConcordServer; -import nl.andrewl.concord_server.cli.command.AddChannelCommand; +import nl.andrewl.concord_server.cli.command.ChannelCommand; import nl.andrewl.concord_server.cli.command.ListClientsCommand; -import nl.andrewl.concord_server.cli.command.RemoveChannelCommand; import nl.andrewl.concord_server.cli.command.StopCommand; import java.io.BufferedReader; @@ -12,6 +11,10 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; +/** + * Simple command-line interface that's available when running the server. This + * accepts commands and various space-separated arguments. + */ public class ServerCli implements Runnable { private final ConcordServer server; private final Map commands; @@ -21,8 +24,7 @@ public class ServerCli implements Runnable { this.commands = new HashMap<>(); this.commands.put("list-clients", new ListClientsCommand()); - this.commands.put("add-channel", new AddChannelCommand()); - this.commands.put("remove-channel", new RemoveChannelCommand()); + this.commands.put("channel", new ChannelCommand()); this.commands.put("stop", new StopCommand()); this.commands.put("help", (s, args) -> { @@ -35,7 +37,7 @@ public class ServerCli implements Runnable { @Override public void run() { - System.out.println("Server command-line-interface initialized. Type \"help\" for a list of available commands."); + System.out.println("Server command-line-interface initialized.\n\tType \"help\" for a list of available commands.\n\tType \"stop\" to stop the server."); BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); String line; try { diff --git a/server/src/main/java/nl/andrewl/concord_server/cli/command/AddChannelCommand.java b/server/src/main/java/nl/andrewl/concord_server/cli/command/AddChannelCommand.java deleted file mode 100644 index 6213121..0000000 --- a/server/src/main/java/nl/andrewl/concord_server/cli/command/AddChannelCommand.java +++ /dev/null @@ -1,41 +0,0 @@ -package nl.andrewl.concord_server.cli.command; - -import nl.andrewl.concord_server.ConcordServer; -import nl.andrewl.concord_server.channel.Channel; -import nl.andrewl.concord_server.cli.ServerCliCommand; -import nl.andrewl.concord_server.config.ServerConfig; - -import java.util.UUID; - -/** - * This command adds a new channel to the server. - */ -public class AddChannelCommand implements ServerCliCommand { - @Override - public void handle(ConcordServer server, String[] args) throws Exception { - if (args.length < 1) { - System.err.println("Missing required name argument."); - return; - } - String name = args[0].trim().toLowerCase().replaceAll("\\s+", "-"); - if (name.isBlank()) { - System.err.println("Cannot create channel with blank name."); - return; - } - if (server.getChannelManager().getChannelByName(name).isPresent()) { - System.err.println("Channel with that name already exists."); - return; - } - String description = null; - if (args.length > 1) { - description = args[1].trim(); - } - UUID id = server.getIdProvider().newId(); - var channelConfig = new ServerConfig.ChannelConfig(id.toString(), name, description); - server.getConfig().getChannels().add(channelConfig); - server.getConfig().save(); - - server.getChannelManager().addChannel(new Channel(server, id, name)); - server.getClientManager().broadcast(server.getMetaData()); - } -} diff --git a/server/src/main/java/nl/andrewl/concord_server/cli/command/ChannelCommand.java b/server/src/main/java/nl/andrewl/concord_server/cli/command/ChannelCommand.java new file mode 100644 index 0000000..b93c625 --- /dev/null +++ b/server/src/main/java/nl/andrewl/concord_server/cli/command/ChannelCommand.java @@ -0,0 +1,105 @@ +package nl.andrewl.concord_server.cli.command; + +import nl.andrewl.concord_server.ConcordServer; +import nl.andrewl.concord_server.channel.Channel; +import nl.andrewl.concord_server.cli.ServerCliCommand; +import nl.andrewl.concord_server.config.ServerConfig; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Optional; +import java.util.UUID; + +/** + * Command for interacting with channels on the server. + */ +public class ChannelCommand implements ServerCliCommand { + @Override + public void handle(ConcordServer server, String[] args) throws Exception { + if (args.length < 1) { + System.err.println("Missing required subcommand. Valid subcommands are: add, remove, list"); + return; + } + String subcommand = args[0]; + args = Arrays.copyOfRange(args, 1, args.length); + switch (subcommand) { + case "add" -> addChannel(server, args); + case "remove" -> removeChannel(server, args); + case "list" -> listChannels(server); + default -> System.err.println("Unknown subcommand."); + } + } + + private void addChannel(ConcordServer server, String[] args) throws IOException { + if (args.length < 1) { + System.err.println("Missing required name argument."); + return; + } + String name = args[0].trim().toLowerCase().replaceAll("\\s+", "-"); + if (name.isBlank()) { + System.err.println("Cannot create channel with blank name."); + return; + } + if (server.getChannelManager().getChannelByName(name).isPresent()) { + System.err.println("Channel with that name already exists."); + return; + } + String description = null; + if (args.length > 1) { + description = args[1].trim(); + } + UUID id = server.getIdProvider().newId(); + var channelConfig = new ServerConfig.ChannelConfig(id.toString(), name, description); + server.getConfig().getChannels().add(channelConfig); + server.getConfig().save(); + + var channel = new Channel(server, id, name); + server.getChannelManager().addChannel(channel); + server.getClientManager().broadcast(server.getMetaData()); + System.out.println("Added channel " + channel.getAsTag() + "."); + } + + private void removeChannel(ConcordServer server, String[] args) throws IOException { + if (args.length != 1) { + System.err.println("Missing required channel name."); + return; + } + String name = args[0].trim().toLowerCase(); + Optional optionalChannel = server.getChannelManager().getChannelByName(name); + if (optionalChannel.isEmpty()) { + System.err.println("No channel with that name exists."); + return; + } + Channel channelToRemove = optionalChannel.get(); + Channel alternative = null; + for (var c : server.getChannelManager().getChannels()) { + if (!c.equals(channelToRemove)) { + alternative = c; + break; + } + } + if (alternative == null) { + System.err.println("No alternative channel could be found. A server must always have at least one channel."); + return; + } + for (var client : channelToRemove.getConnectedClients()) { + server.getChannelManager().moveToChannel(client, alternative); + } + server.getChannelManager().removeChannel(channelToRemove); + server.getDb().getContext().dropCollection(channelToRemove.getMessageCollection().getName()); + server.getConfig().getChannels().removeIf(channelConfig -> channelConfig.getName().equals(channelToRemove.getName())); + server.getConfig().save(); + server.getClientManager().broadcast(server.getMetaData()); + System.out.println("Removed the channel " + channelToRemove); + } + + private void listChannels(ConcordServer server) { + StringBuilder sb = new StringBuilder(); + server.getChannelManager().getChannels().stream().sorted() + .forEachOrdered(channel -> sb.append(channel.getAsTag()) + .append(" - ").append(channel.getConnectedClients().size()) + .append(" users").append("\n") + ); + System.out.print(sb); + } +} diff --git a/server/src/main/java/nl/andrewl/concord_server/cli/command/RemoveChannelCommand.java b/server/src/main/java/nl/andrewl/concord_server/cli/command/RemoveChannelCommand.java deleted file mode 100644 index df66e33..0000000 --- a/server/src/main/java/nl/andrewl/concord_server/cli/command/RemoveChannelCommand.java +++ /dev/null @@ -1,44 +0,0 @@ -package nl.andrewl.concord_server.cli.command; - -import nl.andrewl.concord_server.channel.Channel; -import nl.andrewl.concord_server.ConcordServer; -import nl.andrewl.concord_server.cli.ServerCliCommand; - -import java.util.Optional; - -public class RemoveChannelCommand implements ServerCliCommand { - @Override - public void handle(ConcordServer server, String[] args) throws Exception { - if (args.length != 1) { - System.err.println("Missing required channel name."); - return; - } - String name = args[0].trim().toLowerCase(); - Optional optionalChannel = server.getChannelManager().getChannelByName(name); - if (optionalChannel.isEmpty()) { - System.err.println("No channel with that name exists."); - return; - } - Channel channelToRemove = optionalChannel.get(); - Channel alternative = null; - for (var c : server.getChannelManager().getChannels()) { - if (!c.equals(channelToRemove)) { - alternative = c; - break; - } - } - if (alternative == null) { - System.err.println("No alternative channel could be found. A server must always have at least one channel."); - return; - } - for (var client : channelToRemove.getConnectedClients()) { - server.getChannelManager().moveToChannel(client, alternative); - } - server.getChannelManager().removeChannel(channelToRemove); - server.getDb().getContext().dropCollection(channelToRemove.getMessageCollection().getName()); - server.getConfig().getChannels().removeIf(channelConfig -> channelConfig.getName().equals(channelToRemove.getName())); - server.getConfig().save(); - server.getClientManager().broadcast(server.getMetaData()); - System.out.println("Removed the channel " + channelToRemove); - } -} 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 0eda45c..6907f59 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 @@ -43,10 +43,15 @@ public class ClientManager { * 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. */ - public void registerClient(Identification identification, ClientThread clientThread) { + public void handleLogIn(Identification identification, ClientThread clientThread) { ClientConnectionData data; try { data = identification.getSessionToken() == null ? getNewClientData(identification) : getClientDataFromDb(identification); @@ -78,7 +83,7 @@ public class ClientManager { * they're currently in. * @param clientId The id of the client to remove. */ - public void deregisterClient(UUID clientId) { + public void handleLogOut(UUID clientId) { var client = this.clients.remove(clientId); if (client != null) { client.getCurrentChannel().removeClient(client); 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 ca2e855..29589d9 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 @@ -112,7 +112,7 @@ public class ClientThread extends Thread { } if (this.clientId != null) { - this.server.getClientManager().deregisterClient(this.clientId); + this.server.getClientManager().handleLogOut(this.clientId); } try { if (!this.socket.isClosed()) { @@ -140,7 +140,7 @@ public class ClientThread extends Thread { try { var msg = this.server.getSerializer().readMessage(this.in); if (msg instanceof Identification id) { - this.server.getClientManager().registerClient(id, this); + this.server.getClientManager().handleLogIn(id, this); return true; } } catch (IOException e) { diff --git a/server/src/main/java/nl/andrewl/concord_server/event/ChannelUsersRequestHandler.java b/server/src/main/java/nl/andrewl/concord_server/event/ChannelUsersRequestHandler.java deleted file mode 100644 index e78835c..0000000 --- a/server/src/main/java/nl/andrewl/concord_server/event/ChannelUsersRequestHandler.java +++ /dev/null @@ -1,17 +0,0 @@ -package nl.andrewl.concord_server.event; - -import nl.andrewl.concord_core.msg.types.ChannelUsersRequest; -import nl.andrewl.concord_core.msg.types.ChannelUsersResponse; -import nl.andrewl.concord_server.client.ClientThread; -import nl.andrewl.concord_server.ConcordServer; - -public class ChannelUsersRequestHandler implements MessageHandler { - @Override - public void handle(ChannelUsersRequest msg, ClientThread client, ConcordServer server) throws Exception { - var optionalChannel = server.getChannelManager().getChannelById(msg.getChannelId()); - if (optionalChannel.isPresent()) { - var channel = optionalChannel.get(); - client.sendToClient(new ChannelUsersResponse(channel.getUserData())); - } - } -} diff --git a/server/src/main/java/nl/andrewl/concord_server/event/EventListener.java b/server/src/main/java/nl/andrewl/concord_server/event/EventListener.java deleted file mode 100644 index 3b9f2af..0000000 --- a/server/src/main/java/nl/andrewl/concord_server/event/EventListener.java +++ /dev/null @@ -1,9 +0,0 @@ -package nl.andrewl.concord_server.event; - -import nl.andrewl.concord_core.msg.types.Chat; -import nl.andrewl.concord_server.client.ClientThread; -import nl.andrewl.concord_server.ConcordServer; - -public interface EventListener { - default void chatMessageReceived(ConcordServer server, Chat chat, ClientThread client) {} -} diff --git a/server/src/main/java/nl/andrewl/concord_server/event/MessageHandler.java b/server/src/main/java/nl/andrewl/concord_server/event/MessageHandler.java index bc540e4..1222d37 100644 --- a/server/src/main/java/nl/andrewl/concord_server/event/MessageHandler.java +++ b/server/src/main/java/nl/andrewl/concord_server/event/MessageHandler.java @@ -4,6 +4,11 @@ import nl.andrewl.concord_core.msg.Message; import nl.andrewl.concord_server.client.ClientThread; import nl.andrewl.concord_server.ConcordServer; +/** + * Defines a component which can handle messages of a certain type which were + * received from a client. + * @param The type of message to be handled. + */ public interface MessageHandler { void handle(T msg, ClientThread client, ConcordServer server) throws Exception; } diff --git a/server/src/main/java/nl/andrewl/concord_server/util/Pair.java b/server/src/main/java/nl/andrewl/concord_server/util/Pair.java deleted file mode 100644 index ddce77d..0000000 --- a/server/src/main/java/nl/andrewl/concord_server/util/Pair.java +++ /dev/null @@ -1,3 +0,0 @@ -package nl.andrewl.concord_server.util; - -public record Pair(A first, B second) {}