diff --git a/src/fr/univ/lyon1/client/Client.java b/src/fr/univ/lyon1/client/Client.java index 36b4b73..791a586 100644 --- a/src/fr/univ/lyon1/client/Client.java +++ b/src/fr/univ/lyon1/client/Client.java @@ -6,6 +6,7 @@ import fr.univ.lyon1.common.Message; import fr.univ.lyon1.common.command.Command; import fr.univ.lyon1.common.command.CommandType; import fr.univ.lyon1.common.exception.ChatException; +import fr.univ.lyon1.common.exception.NotInChannel; import fr.univ.lyon1.common.exception.UnknownCommand; import javax.net.SocketFactory; @@ -17,8 +18,13 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.Socket; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; +/** + * The core of the client side + */ public class Client { private final int port; private final String address; @@ -27,10 +33,17 @@ public class Client { protected final Socket socket; protected final ObjectOutputStream out; private ObjectInputStream in; - private List channels = new ArrayList<>(); + private final List channels = new ArrayList<>(); protected boolean started = false; - + /** + * A client need a server and login + * @param address the server address + * @param port the server port + * @param username thr username + * @param password the password + * @throws IOException When the initial communication with the server fail + */ public Client(String address, int port, String username, String password) throws IOException { this.address = address; this.port = port; @@ -41,6 +54,11 @@ public class Client { getIn(); } + /** + * Initialise the SSL WebSocket connection with the server + * @return the socket + * @throws IOException when unable to connect with the server + */ private Socket initSSL() throws IOException { SSLContext ctx = ChatSSL.getSSLContext(); @@ -54,6 +72,10 @@ public class Client { return connection; } + /** + * Close all connection to the server + * @throws IOException if fail to close the connection to the server + */ public void disconnectedServer() throws IOException { socket.close(); out.close(); @@ -63,19 +85,69 @@ public class Client { System.exit(0); } - public String sendMessage(String content) { - + /** + * Send a command to the server + * @param cmd the command + */ + private void send(Command cmd) { try { - out.writeObject(new Command(CommandType.message, List.of(new Message(content, channels.get(0))))); + out.writeObject(cmd); out.flush(); } catch (IOException e) { - System.err.println("Fail to send message !"); + System.err.println("Fail to send command !"); e.printStackTrace(); } - - return content; } + /** + * Send a message in the first channel + * @param content the content of the message + */ + public void sendMessage(String content) { + send(new Command(CommandType.message, List.of(new Message(content, channels.get(0))))); + } + + /** + * Send a command + * @param content the command name an args (/commandName arg1 arh2 arg3) + * @throws UnknownCommand if the command can't be found + */ + public void sendCommand(String content) throws UnknownCommand { + List args = Arrays.asList(content.split(" ")); + String commandName = args.get(0).replace("/", ""); + CommandType commandType; + + try { + commandType = CommandType.valueOf(commandName); + } catch (IllegalArgumentException e) { + throw new UnknownCommand(commandName); + } + + send(new Command(commandType, new ArrayList<>(args.subList(1, args.size())))); + } + + /** + * Send a message with a specific channel + * @param content the channel name and the message content (#chanemName content of the message) + * @throws NotInChannel if the client isn't in the channel + */ + public void sendMessageChannel(String content) throws NotInChannel { + String[] args = content.split(" "); + String channelName = args[0].replace("#", ""); + content = String.join(" ", Arrays.stream(args).toList().subList(1, args.length)); + Channel channel = channels.stream().filter(c -> c.getName().equals(channelName)).findFirst().orElse(null); + + if (channel == null) + throw new NotInChannel(channelName); + + send(new Command(CommandType.message, List.of(new Message(content, channel)))); + } + + /** + * Manage income data from the server + * @param data the data + * @throws IOException if a connection error occur with the server + */ public void action(Object data) throws IOException { if (data instanceof Command) command((Command) data); @@ -87,40 +159,74 @@ public class Client { } } + /** + * Command manager + * @param cmd the command + * @throws IOException if a connection error occur with the server + */ private void command(Command cmd) throws IOException { switch (cmd.getType()) { case login -> commandLogin(); case message -> commandMessage(cmd); case list -> commandList(cmd); + case listChannels -> commandListChannels(cmd); case join -> commandJoin(cmd); } } + /** + * After login handler + * @throws IOException if a connection error occur with the server + */ private void commandLogin() throws IOException { out.writeObject(new Command(CommandType.list, null)); out.flush(); + out.writeObject(new Command(CommandType.listChannels, null)); + out.flush(); out.writeObject(new Command(CommandType.join, List.of("general"))); out.flush(); } + /** + * Receiving message from the server + * @param cmd the message command + */ protected void commandMessage(Command cmd) { System.out.println(); System.out.println(cmd.getArgs().get(0)); } + /** + * User list handler + * @param cmd the command list + */ private void commandList(Command cmd) { - List users = cmd.getArgs(); - for (Object u : users) { - System.out.println(u); - } + System.out.println("Users: "+cmd.getArgs().stream().map(Object::toString).collect(Collectors.joining(", "))); } + /** + * Channel list handler + * @param cmd the command channel list + */ + private void commandListChannels(Command cmd) { + System.out.println("Channels: "+cmd.getArgs().stream().map(Object::toString).collect(Collectors.joining(", "))); + } + + /** + * The channel join handler + * @param cmd the command join + */ private void commandJoin(Command cmd) { Channel chan = (Channel) cmd.getArgs().get(0); channels.add(chan); System.out.println("You join "+chan); } + /** + * Main thread function, creating sub thread for user input and output CLI management + * @throws InterruptedException If the client force exit + * @throws IOException if a connection error occur with the server + */ public void run() throws InterruptedException, IOException { if (started) return; @@ -141,6 +247,11 @@ public class Client { clientReceiveThread.interrupt(); } + /** + * Get the in stream of the WebSocket + * @return the in stream + * @throws IOException if a connection error occur with the server + */ public ObjectInputStream getIn() throws IOException { if (in == null) in = new ObjectInputStream(socket.getInputStream()); diff --git a/src/fr/univ/lyon1/client/ClientReceive.java b/src/fr/univ/lyon1/client/ClientReceive.java index 4f5aa9d..0124fb2 100644 --- a/src/fr/univ/lyon1/client/ClientReceive.java +++ b/src/fr/univ/lyon1/client/ClientReceive.java @@ -5,16 +5,27 @@ import java.io.ObjectInputStream; import java.net.Socket; import java.net.SocketException; +/** + * Clint message receiver manager + */ public class ClientReceive implements Runnable { private final Client client; private ObjectInputStream in; private final Socket socket; + /** + * Create a receiver manager from a client and a socket + * @param client the client + * @param socket the client socket to the server + */ public ClientReceive(Client client, Socket socket) { this.client = client; this.socket = socket; } + /** + * The mai thread + */ @Override public void run() { try { diff --git a/src/fr/univ/lyon1/client/ClientSend.java b/src/fr/univ/lyon1/client/ClientSend.java index d3d82fe..fb8f486 100644 --- a/src/fr/univ/lyon1/client/ClientSend.java +++ b/src/fr/univ/lyon1/client/ClientSend.java @@ -1,20 +1,33 @@ package fr.univ.lyon1.client; +import fr.univ.lyon1.common.exception.ChatException; + import java.io.ObjectOutputStream; import java.net.Socket; import java.util.Scanner; +/** + * Clint message sender manager + */ public class ClientSend implements Runnable { private final Client client; private final ObjectOutputStream out; private final Socket socket; + /** + * Create a sender manager from a client and a socket + * @param client the client + * @param socket the client socket to the server + */ public ClientSend(Client client, ObjectOutputStream out, Socket socket) { this.client = client; this.out = out; this.socket = socket; } + /** + * The mai thread + */ public void run() { Scanner sc = new Scanner(System.in); @@ -25,7 +38,16 @@ public class ClientSend implements Runnable { if (m.equals("exit")) break; - client.sendMessage(m); + try { + if (m.startsWith("/")) + client.sendCommand(m); + else if (m.startsWith("#")) + client.sendMessageChannel(m); + else + client.sendMessage(m); + } catch (ChatException e) { + e.printStackTrace(); + } } } } diff --git a/src/fr/univ/lyon1/client/MainClient.java b/src/fr/univ/lyon1/client/MainClient.java index afc4565..aa7bc43 100644 --- a/src/fr/univ/lyon1/client/MainClient.java +++ b/src/fr/univ/lyon1/client/MainClient.java @@ -1,5 +1,8 @@ package fr.univ.lyon1.client; +/** + * The main program for the client CLI + */ public class MainClient { public static void main(String[] args) { try { @@ -19,11 +22,14 @@ public class MainClient { } } + /** + * Help usage for arguments + */ private static void printUsage() { System.out.println("java client.Client
"); System.out.println("\t
: server's ip address"); System.out.println("\t: server's port"); - System.out.println("\t: user's UUID"); + System.out.println("\t: username"); System.out.println("\t: user's password"); } } diff --git a/src/fr/univ/lyon1/common/Channel.java b/src/fr/univ/lyon1/common/Channel.java index 1ef983d..497d00d 100644 --- a/src/fr/univ/lyon1/common/Channel.java +++ b/src/fr/univ/lyon1/common/Channel.java @@ -3,28 +3,52 @@ package fr.univ.lyon1.common; import java.io.Serializable; import java.util.UUID; +/** + * The base type of chat channel + */ public class Channel implements Serializable { private final UUID uuid; private String name; + /** + * Create an existing channel + * @param uuid the channel unique id + * @param name the channel name + */ public Channel(UUID uuid, String name) { this.uuid = uuid; this.name = name; } + /** + * Create a new channel + * @param name the new channel name + */ public Channel(String name) { this.uuid = UUID.randomUUID(); this.name = name; } + /** + * Get the channel unique id + * @return the channel unique id + */ public UUID getUUID() { return uuid; } + /** + * Get the channel name + * @return the channel name + */ public String getName() { return name; } + /** + * String version of the channel + * @return the channel name + */ @Override public String toString() { return name; diff --git a/src/fr/univ/lyon1/common/ChatSSL.java b/src/fr/univ/lyon1/common/ChatSSL.java index f00b7c1..803b433 100644 --- a/src/fr/univ/lyon1/common/ChatSSL.java +++ b/src/fr/univ/lyon1/common/ChatSSL.java @@ -10,12 +10,13 @@ import java.io.InputStream; import java.security.*; import java.security.cert.CertificateException; -/* -keytool -genkeypair -alias server -keyalg EC \ --sigalg SHA384withECDSA -keysize 256 -keystore servercert.p12 \ --storetype pkcs12 -v -storepass abc123 -validity 10000 -ext san=ip:127.0.0.1 +/** + * The SSL context for client/server communication + * A key store can be generated with this command: + * keytool -genkeypair -alias server -keyalg EC \ + * -sigalg SHA384withECDSA -keysize 256 -keystore servercert.p12 \ + * -storetype pkcs12 -v -storepass abc123 -validity 10000 -ext san=ip:127.0.0.1 */ - public class ChatSSL { public static String trustStoreName = "servercert.p12"; public static String keyStoreName = "servercert.p12"; @@ -23,25 +24,37 @@ public class ChatSSL { private static char[] trustStorePassword = "abc123".toCharArray(); private static char[] keyStorePassword = "abc123".toCharArray(); + /** + * Get the SSL context for communication + * @return the SSL context + */ public static SSLContext getSSLContext() { - try { + // Get the SSL trust store from package resources KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); InputStream tstore = Connection.class .getResourceAsStream("/" + trustStoreName); trustStore.load(tstore, trustStorePassword); tstore.close(); + + // Create a trust factory from the trust store TrustManagerFactory tmf = TrustManagerFactory .getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(trustStore); + // Get the SSL key store from package resources KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); InputStream kstore = Connection.class .getResourceAsStream("/" + keyStoreName); keyStore.load(kstore, keyStorePassword); + kstore.close(); + + // Create a key factory from the key store KeyManagerFactory kmf = KeyManagerFactory .getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(keyStore, keyStorePassword); + + // Generate the SSL context SSLContext ctx = SSLContext.getInstance("TLS"); ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), SecureRandom.getInstanceStrong()); diff --git a/src/fr/univ/lyon1/common/Message.java b/src/fr/univ/lyon1/common/Message.java index 29d6786..8546edd 100644 --- a/src/fr/univ/lyon1/common/Message.java +++ b/src/fr/univ/lyon1/common/Message.java @@ -3,13 +3,21 @@ package fr.univ.lyon1.common; import java.io.Serializable; import java.util.UUID; +/** + * The base type of message in client/server communication + */ public class Message implements Serializable { private Channel channel; private User sender; private final String content; private final UUID uuid; - + /** + * Create a new message with a specified sender + * @param channel the target channel + * @param sender the sender + * @param content the content + */ public Message(Channel channel, User sender, String content) { this.uuid = UUID.randomUUID(); this.channel = channel; @@ -17,6 +25,13 @@ public class Message implements Serializable { this.content = content; } + /** + * Create an existing message + * @param uuid the unique id + * @param channel the channel + * @param sender the sender + * @param content the content + */ public Message(UUID uuid, Channel channel, User sender, String content) { this.uuid = uuid; this.channel = channel; @@ -24,32 +39,64 @@ public class Message implements Serializable { this.content = content; } + /** + * Create a nex message without specifying the sender + * @param content the content + * @param channel the target channel + */ public Message(String content, Channel channel) { this.uuid = UUID.randomUUID(); this.content = content; this.channel = channel; } + /** + * Generate a reply if this message + * This use the same chanel of the original one + * @param user the sender + * @param content the content + * @return the message reply + */ public Message repley(User user, String content) { return new Message(this.channel, user, content); } + /** + * Change the sender + * @param sender the new sender + */ public void setSender(User sender) { this.sender = sender; } + /** + * Get the message channel + * @return the channel + */ public Channel getChannel() { return channel; } + /** + * Get the message sender + * @return the sender + */ public User getSender() { return sender; } + /** + * Get the content of the message + * @return the message content + */ public String getContent() { return content; } + /** + * Printable version of the message + * @return the channel, sender and the content + */ @Override public String toString() { if (channel != null) diff --git a/src/fr/univ/lyon1/common/ServerConfiguration.java b/src/fr/univ/lyon1/common/ServerConfiguration.java index 339cf0f..7ff6a51 100644 --- a/src/fr/univ/lyon1/common/ServerConfiguration.java +++ b/src/fr/univ/lyon1/common/ServerConfiguration.java @@ -2,19 +2,36 @@ package fr.univ.lyon1.common; import org.jetbrains.annotations.NotNull; -import java.io.*; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; import java.util.Properties; +/** + * The client server configuration + * ToDo should be in gui and not in common ? + */ public record ServerConfiguration(@NotNull String address, int port) { @NotNull private static final File file = new File("connection.properties"); + /** + * Create a configuration + * @param address the server address + * @param port the server port + */ public ServerConfiguration(@NotNull String address, int port) { this.address = address; this.port = port; } - public static ServerConfiguration load() throws IOException, NumberFormatException { + /** + * Load configuration from file + * @return the configuration + * @throws IOException if the file doesn't exist + */ + public static ServerConfiguration load() throws IOException { // Check if file non exists, return error to launch server configuration if (!file.exists()) { System.out.println("File not exists"); @@ -26,6 +43,10 @@ public record ServerConfiguration(@NotNull String address, int port) { return new ServerConfiguration(properties.getProperty("address"), Integer.parseInt(properties.getProperty("port"))); } + /** + * Save configuration to file + * @throws IOException if fail to write the file + */ public void save() throws IOException { @NotNull final Properties properties = new Properties(); properties.setProperty("address", this.address); @@ -33,10 +54,18 @@ public record ServerConfiguration(@NotNull String address, int port) { properties.store(new FileWriter(file), "Information needed to connect to the server"); } + /** + * Get the server address + * @return the server address + */ public @NotNull String getAddress() { return address; } + /** + * Get the server port + * @return the server port + */ public int getPort() { return port; } diff --git a/src/fr/univ/lyon1/common/User.java b/src/fr/univ/lyon1/common/User.java index dd0f0c8..c329c12 100644 --- a/src/fr/univ/lyon1/common/User.java +++ b/src/fr/univ/lyon1/common/User.java @@ -3,28 +3,52 @@ package fr.univ.lyon1.common; import java.io.Serializable; import java.util.UUID; +/** + * The base type of user for client/server communication + */ public class User implements Serializable { private final UUID uuid; private String username; + /** + * Create an existing user + * @param uuid the user unique id + * @param username the username + */ public User(UUID uuid, String username) { this.uuid = uuid; this.username = username; } + /** + * Create a new user + * @param username the username + */ public User(String username) { this.uuid = UUID.randomUUID(); this.username = username; } + /** + * Get the user unique id + * @return thee unique id + */ public UUID getUUID() { return uuid; } + /** + * Get the username + * @return the username + */ public String getUsername() { return username; } + /** + * The user as a string + * @return the username + */ @Override public String toString() { return username; diff --git a/src/fr/univ/lyon1/common/command/Command.java b/src/fr/univ/lyon1/common/command/Command.java index d3e5ea6..b4f0d75 100644 --- a/src/fr/univ/lyon1/common/command/Command.java +++ b/src/fr/univ/lyon1/common/command/Command.java @@ -4,19 +4,37 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; +/** + * Command is the base object transmitted between clients and server + */ public class Command implements Serializable { private final CommandType type; private final List args; - public Command(CommandType type, List args) { + /** + * Class need a type and a list of arguments + * @see CommandType + * @param type The type of the command + * @param args A list of arguments for the given command + */ + public Command(CommandType type, List args) { this.type = type; - this.args = args; + this.args = (List) args; } + /** + * Get the type of the command + * @see CommandType + * @return the command type + */ public CommandType getType() { return type; } + /** + * Get the arguments of the command + * @return A list of arguments + */ public List getArgs() { return new ArrayList<>(args); } diff --git a/src/fr/univ/lyon1/common/command/CommandType.java b/src/fr/univ/lyon1/common/command/CommandType.java index 6e36acc..415865e 100644 --- a/src/fr/univ/lyon1/common/command/CommandType.java +++ b/src/fr/univ/lyon1/common/command/CommandType.java @@ -2,6 +2,10 @@ package fr.univ.lyon1.common.command; import java.io.Serializable; +/** + * List of command types + * @see Command + */ public enum CommandType implements Serializable { login("login", "Login to the server"), message("message", "Send a message"), @@ -12,15 +16,29 @@ public enum CommandType implements Serializable { private final String name; private final String description; + + /** + * A command type is defined by a name and a description + * @param name the command name + * @param description the command description + */ CommandType(String name, String description) { this.name = name; this.description = description; } + /** + * Get the name of the command + * @return the command name + */ public String getName() { return name; } + /** + * Get the description of the command + * @return the command description + */ public String getDescription() { return description; } diff --git a/src/fr/univ/lyon1/common/exception/ChatException.java b/src/fr/univ/lyon1/common/exception/ChatException.java index 7749e46..7f53cab 100644 --- a/src/fr/univ/lyon1/common/exception/ChatException.java +++ b/src/fr/univ/lyon1/common/exception/ChatException.java @@ -1,6 +1,13 @@ package fr.univ.lyon1.common.exception; +/** + * Main exception of the chat system for client and server communication + */ public class ChatException extends Exception { + /** + * This exception need only a message + * @param message the message + */ public ChatException(String message) { super(message); } diff --git a/src/fr/univ/lyon1/common/exception/LoginInvalid.java b/src/fr/univ/lyon1/common/exception/LoginInvalid.java index b412ec0..4f03d31 100644 --- a/src/fr/univ/lyon1/common/exception/LoginInvalid.java +++ b/src/fr/univ/lyon1/common/exception/LoginInvalid.java @@ -1,5 +1,8 @@ package fr.univ.lyon1.common.exception; +/** + * Exception when a login/password is invalid + */ public class LoginInvalid extends ChatException { public LoginInvalid(String message) { super(message); diff --git a/src/fr/univ/lyon1/common/exception/LoginRequired.java b/src/fr/univ/lyon1/common/exception/LoginRequired.java index 72060ec..f12c7d6 100644 --- a/src/fr/univ/lyon1/common/exception/LoginRequired.java +++ b/src/fr/univ/lyon1/common/exception/LoginRequired.java @@ -1,5 +1,8 @@ package fr.univ.lyon1.common.exception; +/** + * Exception when the user is not logged + */ public class LoginRequired extends ChatException { public LoginRequired() { super("Login required"); diff --git a/src/fr/univ/lyon1/common/exception/NotInChannel.java b/src/fr/univ/lyon1/common/exception/NotInChannel.java index 5a2473f..3a0520d 100644 --- a/src/fr/univ/lyon1/common/exception/NotInChannel.java +++ b/src/fr/univ/lyon1/common/exception/NotInChannel.java @@ -2,8 +2,15 @@ package fr.univ.lyon1.common.exception; import fr.univ.lyon1.common.Channel; +/** + * Exception sent to a client when he is not in the target channel + */ public class NotInChannel extends ChatException { public NotInChannel(Channel channel) { super("Your not in channel "+channel); } + + public NotInChannel(String channel) { + super("Your not in channel "+channel); + } } diff --git a/src/fr/univ/lyon1/common/exception/UnknownCommand.java b/src/fr/univ/lyon1/common/exception/UnknownCommand.java index 32a7c29..2572877 100644 --- a/src/fr/univ/lyon1/common/exception/UnknownCommand.java +++ b/src/fr/univ/lyon1/common/exception/UnknownCommand.java @@ -1,9 +1,14 @@ package fr.univ.lyon1.common.exception; -import fr.univ.lyon1.common.command.Command; - +/** + * Exception when a command is not known by the client or server + */ public class UnknownCommand extends ChatException { public UnknownCommand() { super("Command unknown"); } + + public UnknownCommand(String commandName) { + super("Command " + commandName + " unknown"); + } } diff --git a/src/fr/univ/lyon1/server/ConnectedClient.java b/src/fr/univ/lyon1/server/ConnectedClient.java index 09f98c6..f6082b7 100644 --- a/src/fr/univ/lyon1/server/ConnectedClient.java +++ b/src/fr/univ/lyon1/server/ConnectedClient.java @@ -17,6 +17,9 @@ import java.net.Socket; import java.util.Collections; import java.util.List; +/** + * Server client connection management + */ public class ConnectedClient implements Runnable { private static int idCounter = 0; private final int id = idCounter++; @@ -26,6 +29,12 @@ public class ConnectedClient implements Runnable { private ObjectInputStream in; private UserModel user; + /** + * Create a client connection management + * @param server server socket + * @param socket client socket + * @throws IOException if a connection error occur with the client + */ ConnectedClient(Server server, Socket socket) throws IOException { this.server = server; this.socket = socket; @@ -33,12 +42,31 @@ public class ConnectedClient implements Runnable { this.in = new ObjectInputStream(socket.getInputStream()); } - public Message sendMessage(Message message) throws IOException { - out.writeObject(new Command(CommandType.message, List.of(message))); + /** + * Send command to the client + * @param cmd the command + * @throws IOException if a connection error occur with the client + */ + private void send(Command cmd) throws IOException { + out.writeObject(cmd); out.flush(); - return message; } + /** + * Send a message to the client + * @param message the message + * @throws IOException if a connection error occur with the client + */ + public void sendMessage(Message message) throws IOException { + send(new Command(CommandType.message, List.of(message))); + } + + /** + * Client command handler + * @param command the command + * @throws IOException if a connection error occur with the client + * @throws ChatException chat runtime error send to the user + */ private void actionCommand(Command command) throws IOException, ChatException { CommandType type = command.getType(); if (user == null && type != CommandType.login) @@ -48,11 +76,19 @@ public class ConnectedClient implements Runnable { case login -> commandLogin(command); case message -> commandMessage(command); case list -> commandList(); + case listChannels -> commandListChannels(); case join -> commandJoin(command); } } - private void commandLogin(Command cmd) throws IOException, ChatException { + /** + * Handel's user authentication + * ToDo avoid re auth + * @param cmd the login command + * @throws IOException if a connection error occur with the client + * @throws LoginInvalid if the user credentials are invalid + */ + private void commandLogin(Command cmd) throws IOException, LoginInvalid { List args = cmd.getArgs(); String username = (String) args.get(0); @@ -70,13 +106,17 @@ public class ConnectedClient implements Runnable { else if (!user.checkPassword(password)) throw new LoginInvalid("Password invalid"); else { - out.writeObject(new Command(CommandType.login, null)); - out.flush(); + send(new Command(CommandType.login, null)); this.user = user; System.out.println("Client "+user.getUsername()+" is connected !"); } } + /** + * Message receive handler + * @param cmd the message command + * @throws NotInChannel if the user is not in the channel + */ private void commandMessage(Command cmd) throws NotInChannel { Message msg = (Message) cmd.getArgs().get(0); msg.setSender(this.user); @@ -88,11 +128,27 @@ public class ConnectedClient implements Runnable { server.broadcastMessage(msg, id); } + /** + * Command user list handler + * @throws IOException if a connection error occur with the client + */ private void commandList() throws IOException { - out.writeObject(new Command(CommandType.list, Collections.singletonList(server.getUsers()))); - out.flush(); + send(new Command(CommandType.list, Collections.singletonList(server.getUsers()))); } + /** + * Command channel list handler + * @throws IOException if a connection error occur with the client + */ + private void commandListChannels() throws IOException { + send(new Command(CommandType.listChannels, Collections.singletonList((List)(List) ChannelModel.getAll()))); + } + + /** + * Channel join command handler + * @param cmd the join command + * @throws IOException if a connection error occur with the client + */ private void commandJoin(Command cmd) throws IOException { String name = (String) cmd.getArgs().get(0); ChannelModel chan = ChannelModel.get(name); @@ -104,12 +160,14 @@ public class ConnectedClient implements Runnable { if (!chan.have(user)) chan.addUser(user); - out.writeObject(new Command(CommandType.join, List.of((Channel) chan))); - out.flush(); + send(new Command(CommandType.join, List.of((Channel) chan))); server.broadcastMessage(new Message(chan, Server.getServerUser(), user.getUsername()+" joined the channel !"), -1); } + /** + * Man thread of user connection + */ public void run() { try { while (true) { @@ -137,6 +195,10 @@ public class ConnectedClient implements Runnable { } } + /** + * Close connection to client + * @throws IOException if a connection error occur with the client + */ public void closeClient() throws IOException { if (in != null) in.close(); @@ -144,10 +206,18 @@ public class ConnectedClient implements Runnable { socket.close(); } + /** + * Get the client connection id + * @return connection id + */ public int getId() { return id; } + /** + * Get the user + * @return the user + */ public User getUser() { return user; } diff --git a/src/fr/univ/lyon1/server/Connection.java b/src/fr/univ/lyon1/server/Connection.java index 251f763..4bafda9 100644 --- a/src/fr/univ/lyon1/server/Connection.java +++ b/src/fr/univ/lyon1/server/Connection.java @@ -7,15 +7,28 @@ import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; +/** + * Server connection manager + */ public class Connection implements Runnable { private final Server server; private final ServerSocket serverSocket; + /** + * Create a server connection manager + * @param server the server + * @throws IOException if a connection error occur with the client + */ Connection(Server server) throws IOException { this.server = server; this.serverSocket = initSSL(); } + /** + * Initialise the SSL client WebSocket connection + * @return the socket + * @throws IOException if a connection error occur with the client + */ private SSLServerSocket initSSL() throws IOException { SSLContext ctx = ChatSSL.getSSLContext(); @@ -29,6 +42,9 @@ public class Connection implements Runnable { return sslListener; } + /** + * Main thread + */ public void run() { while (true) { Socket clientSocket; diff --git a/src/fr/univ/lyon1/server/Database.java b/src/fr/univ/lyon1/server/Database.java index 8756122..cbd41ce 100644 --- a/src/fr/univ/lyon1/server/Database.java +++ b/src/fr/univ/lyon1/server/Database.java @@ -10,10 +10,16 @@ import java.sql.DriverManager; import java.sql.SQLException; import java.util.Properties; +/** + * Server database management + */ public class Database { private static Database database; private Connection connection; + /** + * Create database object and establish connection + */ private Database() { Database.database = this; try { @@ -26,7 +32,12 @@ public class Database { init(); } - private String[] getCredentials() throws NullPointerException, IOException { + /** + * Get database credentials + * @return credentials + * @throws IOException when an error occur with the configuration file + */ + private String[] getCredentials() throws IOException { Properties props = new Properties(); File f = new File("server.properties"); @@ -43,6 +54,12 @@ public class Database { return new String[]{props.getProperty("db.url"), props.getProperty("db.user"), props.getProperty("db.password")}; } + /** + * Get the database connection + * @return the connection + * @throws SQLException if a connection error occur with the database + * @throws IOException when failed to get the credentials + */ private Connection getConnexion() throws SQLException, IOException { String[] credentials = getCredentials(); @@ -56,16 +73,27 @@ public class Database { return DriverManager.getConnection(credentials[0], credentials[1], credentials[2]); } + /** + * Get the database connection + * @return the connection + */ public Connection getConnection() { return connection; } + /** + * Get the database instance + * @return the database + */ public static Database getDatabase() { if (Database.database == null) return new Database(); return Database.database; } + /** + * Initialise the database tables from models + */ private void init() { UserModel.generateTable(); ChannelModel.generateTable(); diff --git a/src/fr/univ/lyon1/server/MainServer.java b/src/fr/univ/lyon1/server/MainServer.java index 5809841..a479b62 100644 --- a/src/fr/univ/lyon1/server/MainServer.java +++ b/src/fr/univ/lyon1/server/MainServer.java @@ -2,6 +2,9 @@ package fr.univ.lyon1.server; import java.io.IOException; +/** + * Main server program + */ public class MainServer { public static void main(String[] args) { try { @@ -16,6 +19,9 @@ public class MainServer { } } + /** + * Help usage for arguments + */ private static void printUsage() { System.out.println("java server.Server "); System.out.println("\t: server's port"); diff --git a/src/fr/univ/lyon1/server/Server.java b/src/fr/univ/lyon1/server/Server.java index 8bf6058..411d6a2 100644 --- a/src/fr/univ/lyon1/server/Server.java +++ b/src/fr/univ/lyon1/server/Server.java @@ -9,11 +9,19 @@ import java.util.ArrayList; import java.util.List; import java.util.UUID; +/** + * Main server management + */ public class Server { private final int port; - private List clients = new ArrayList<>(); - private static User serverUser = new User(UUID.fromString("3539b6bf-5eb3-41d4-893f-cbf0caa9ca74"), "server"); + private final List clients = new ArrayList<>(); + private static final User serverUser = new User(UUID.fromString("3539b6bf-5eb3-41d4-893f-cbf0caa9ca74"), "server"); + /** + * Create server + * @param port the listening port + * @throws IOException if a connection error occur + */ Server(int port) throws IOException { this.port = port; Database.getDatabase(); @@ -21,12 +29,20 @@ public class Server { connection.start(); } - public ConnectedClient addClient(ConnectedClient newClient) { + /** + * Add client handler + * @param newClient the client + */ + public void addClient(ConnectedClient newClient) { clients.add(newClient); - return newClient; } - public int broadcastMessage(Message message, int id) { + /** + * Send a message to all clients + * @param message the message + * @param id the sender id + */ + public void broadcastMessage(Message message, int id) { List users = UserChannelModel.getUsers(message.getChannel()).stream().map(User::getUUID).toList(); for (ConnectedClient client : clients.stream().filter(connectedClient -> users.contains(connectedClient.getUser().getUUID())).toList()) { if (id == -1 || client.getId() != id) @@ -37,10 +53,13 @@ public class Server { e.printStackTrace(); } } - return id; } - public ConnectedClient disconnectedClient(ConnectedClient client) { + /** + * Close client connection + * @param client the client connection manager + */ + public void disconnectedClient(ConnectedClient client) { try { client.closeClient(); } catch (IOException e) { @@ -51,17 +70,28 @@ public class Server { clients.remove(client); System.out.println("Client "+client.getId()+" disconnected"); - return client; } + /** + * Get the server listening port + * @return the server listening port + */ public int getPort() { return port; } + /** + * Get the server user + * @return the server user + */ public static User getServerUser() { return serverUser; } + /** + * Get the list of connection client to the server + * @return list of connected client to the server + */ public List getUsers() { return clients.stream().map(ConnectedClient::getUser).toList(); } diff --git a/src/fr/univ/lyon1/server/models/ChannelModel.java b/src/fr/univ/lyon1/server/models/ChannelModel.java index a5d52fa..4429ffa 100644 --- a/src/fr/univ/lyon1/server/models/ChannelModel.java +++ b/src/fr/univ/lyon1/server/models/ChannelModel.java @@ -6,28 +6,57 @@ import fr.univ.lyon1.common.User; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; +/** + * Database model of a channel + */ public class ChannelModel extends Channel implements Model { private static final String TABLE_NAME = "Channel"; + /** + * Create a new channel from a name + * @param name the name + */ public ChannelModel(String name) { super(name); create(); } + /** + * Model from existing channel + * @param uuid + * @param name + */ private ChannelModel(UUID uuid, String name) { super(uuid, name); } + /** + * Add a user to the channel + * ToDo on user reconnection rejoin all connected channels + * @param user the user + */ public void addUser(User user) { new UserChannelModel(user, this); } + /** + * Check if a user is in this channel + * @param user the user + * @return true if is else false + */ public boolean have(User user) { return UserChannelModel.exist(user, this); } + /** + * Get a channel from a name + * @param name the name + * @return the channel or null if not found + */ public static ChannelModel get(String name) { try { PreparedStatement ps = database.getConnection().prepareStatement("SELECT UUID FROM "+TABLE_NAME+" WHERE name = ?"); @@ -44,6 +73,11 @@ public class ChannelModel extends Channel implements Model { return null; } + /** + * Get a channel from the unique id + * @param uuid the unique id + * @return the channel or null if not found + */ public static ChannelModel get(UUID uuid) { try { PreparedStatement ps = database.getConnection().prepareStatement("SELECT * FROM "+TABLE_NAME+" WHERE UUID = ?"); @@ -63,6 +97,33 @@ public class ChannelModel extends Channel implements Model { return null; } + /** + * Get all channels + * @return a list of channels + */ + public static List getAll() { + List channels = new ArrayList<>(); + try { + PreparedStatement ps = database.getConnection().prepareStatement("SELECT * FROM "+TABLE_NAME); + if (ps.execute()) { + ResultSet rs = ps.getResultSet(); + while (rs.next()) + channels.add(new ChannelModel( + UUID.fromString(rs.getString("UUID")), + rs.getString("NAME")) + ); + } + } catch (SQLException err) { + err.printStackTrace(); + return null; + } + return channels; + } + + /** + * Check of the channel exists in the database + * @return true if the channel exists else false + */ private boolean exist() { try { PreparedStatement ps = database.getConnection().prepareStatement("SELECT UUID FROM "+TABLE_NAME+" WHERE UUID = ?"); @@ -75,6 +136,10 @@ public class ChannelModel extends Channel implements Model { } } + /** + * Register the channel in the database + * @return true if the register is successful else false + */ private boolean create() { try { PreparedStatement ps = database.getConnection().prepareStatement("INSERT INTO "+TABLE_NAME+" (UUID, name) VALUES (?, ?)"); @@ -87,6 +152,10 @@ public class ChannelModel extends Channel implements Model { } } + /** + * Update the channel in the database + * @return true if the update is successful else false + */ public boolean save() { if (!exist()) return create(); @@ -101,6 +170,9 @@ public class ChannelModel extends Channel implements Model { } } + /** + * Generate the channel model table in the database + */ public static void generateTable() { try { PreparedStatement ps = database.getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS "+TABLE_NAME+" ( UUID varchar(40) primary key, name varchar(16) unique )"); diff --git a/src/fr/univ/lyon1/server/models/Model.java b/src/fr/univ/lyon1/server/models/Model.java index 5903bdb..26c6dc3 100644 --- a/src/fr/univ/lyon1/server/models/Model.java +++ b/src/fr/univ/lyon1/server/models/Model.java @@ -2,7 +2,9 @@ package fr.univ.lyon1.server.models; import fr.univ.lyon1.server.Database; - +/** + * Base model of a database type + */ public interface Model { Database database = Database.getDatabase(); } diff --git a/src/fr/univ/lyon1/server/models/UserChannelModel.java b/src/fr/univ/lyon1/server/models/UserChannelModel.java index 8e7b728..e5dbd8b 100644 --- a/src/fr/univ/lyon1/server/models/UserChannelModel.java +++ b/src/fr/univ/lyon1/server/models/UserChannelModel.java @@ -10,12 +10,20 @@ import java.util.ArrayList; import java.util.List; import java.util.UUID; +/** + * Database model of the relation between user and channel + */ public class UserChannelModel implements Model { private User user; private Channel channel; private static final String TABLE_NAME = "UserChannel"; + /** + * Create a user channel relation and save it in database if necessary + * @param user + * @param channel + */ public UserChannelModel(User user, Channel channel) { this.user = user; this.channel = channel; @@ -24,6 +32,11 @@ public class UserChannelModel implements Model { create(); } + /** + * Get the list of users in a specific channel + * @param channel the channel + * @return the list of users + */ public static List getUsers(Channel channel) { List users = new ArrayList<>(); @@ -42,6 +55,11 @@ public class UserChannelModel implements Model { return users; } + /** + * Get a list of channel where a user is in + * @param user the user + * @return the list of channels + */ public static List getChannels(User user) { List channels = new ArrayList<>(); @@ -60,6 +78,12 @@ public class UserChannelModel implements Model { return channels; } + /** + * Check if the relation exists in the database + * @param user the user + * @param channel the channel + * @return true if present else false + */ public static boolean exist(User user, Channel channel) { try { PreparedStatement ps = database.getConnection().prepareStatement("SELECT 1 FROM "+TABLE_NAME+" WHERE userUUID = ? AND channelUUID = ?"); @@ -73,6 +97,10 @@ public class UserChannelModel implements Model { } } + /** + * Save the ration in the database + * @return true if succeed else false + */ private boolean create() { try { PreparedStatement ps = database.getConnection().prepareStatement("INSERT INTO "+TABLE_NAME+" (userUUID, channelUUID) VALUES (?, ?)"); @@ -85,6 +113,9 @@ public class UserChannelModel implements Model { } } + /** + * Generate the user channel relation model table in the database + */ public static void generateTable() { try { PreparedStatement ps = database.getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS "+TABLE_NAME+" (userUUID varchar(40) not null references User(UUID), channelUUID varchar(40) not null references Channel(UUID), PRIMARY KEY (userUUID, channelUUID))"); diff --git a/src/fr/univ/lyon1/server/models/UserModel.java b/src/fr/univ/lyon1/server/models/UserModel.java index 4624640..9b720e5 100644 --- a/src/fr/univ/lyon1/server/models/UserModel.java +++ b/src/fr/univ/lyon1/server/models/UserModel.java @@ -17,6 +17,9 @@ import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; +/** + * Database model of a user type + */ public class UserModel extends User implements Model { private String passwordHash; @@ -28,17 +31,33 @@ public class UserModel extends User implements Model { private static final Pattern LAYOUT = Pattern.compile("\\$1\\$(\\d\\d?)\\$(.{43})"); private static final String TABLE_NAME = "User"; + /** + * Create a new user + * @param username the username + * @param password the password + */ public UserModel(String username, String password) { super(username); setPassword(password); create(); } + /** + * Create an existing user + * @param uuid the unique id + * @param username the username + * @param passwordHash the password hash + */ private UserModel(UUID uuid, String username, String passwordHash) { super(uuid, username); this.passwordHash = passwordHash; } + /** + * Get a user from his username + * @param username the username + * @return the user of null if not found + */ public static UserModel get(String username) { try { PreparedStatement ps = database.getConnection().prepareStatement("SELECT UUID FROM "+TABLE_NAME+" WHERE username = ?"); @@ -55,6 +74,11 @@ public class UserModel extends User implements Model { return null; } + /** + * Get a user from his unique id + * @param uuid the unique id + * @return the user of null if not found + */ public static UserModel get(UUID uuid) { try { PreparedStatement ps = database.getConnection().prepareStatement("SELECT * FROM "+TABLE_NAME+" WHERE UUID = ?"); @@ -75,6 +99,10 @@ public class UserModel extends User implements Model { return null; } + /** + * Check if the user exists in the database + * @return true if present else false + */ private boolean exist() { try { PreparedStatement ps = database.getConnection().prepareStatement("SELECT UUID FROM "+TABLE_NAME+" WHERE UUID = ?"); @@ -87,6 +115,10 @@ public class UserModel extends User implements Model { } } + /** + * Create a user in the database + * @return true if the update is successful else false + */ private boolean create() { try { PreparedStatement ps = database.getConnection().prepareStatement("INSERT INTO "+TABLE_NAME+" (UUID, username, password) VALUES (?, ?, ?)"); @@ -100,6 +132,10 @@ public class UserModel extends User implements Model { } } + /** + * Update a user in the database + * @return true if the update is successful else false + */ public boolean save() { if (!exist()) return create(); @@ -116,6 +152,9 @@ public class UserModel extends User implements Model { } } + /** + * Generate the channel model table in the database + */ public static void generateTable() { try { PreparedStatement ps = database.getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS "+TABLE_NAME+" ( UUID varchar(40) primary key, username varchar(16) unique, password varchar(256) )"); @@ -125,41 +164,77 @@ public class UserModel extends User implements Model { } } + /** + * Get the password hash + * @return password hash + */ public String getPasswordHash() { return passwordHash; } + /** + * Set the password as a hash + * @param password the plain password + */ public void setPassword(String password) { + // Generate a new salt byte[] passwordSalt = new byte[SIZE / 8]; random.nextBytes(passwordSalt); + + // Generate the hash from the password and the salt byte[] dk = pbkdf2(password.toCharArray(), passwordSalt, 1 << COST); byte[] hash = new byte[passwordSalt.length + dk.length]; System.arraycopy(passwordSalt, 0, hash, 0, passwordSalt.length); System.arraycopy(dk, 0, hash, passwordSalt.length, dk.length); Base64.Encoder enc = Base64.getUrlEncoder().withoutPadding(); + + // Format the password hash passwordHash = ID + COST + '$' + enc.encodeToString(hash); } + /** + * Check a password against the password hash + * @param password the plain password to test + * @return true if the password match else false + */ public boolean checkPassword(String password) { + // Check the password hash integrity Matcher m = LAYOUT.matcher(passwordHash); if (!m.matches()) throw new IllegalArgumentException("Invalid token format"); + + // Gather hash data int iterations = iterations(Integer.parseInt(m.group(1))); byte[] hash = Base64.getUrlDecoder().decode(m.group(2)); byte[] salt = Arrays.copyOfRange(hash, 0, SIZE / 8); byte[] check = pbkdf2(password.toCharArray(), salt, iterations); + + // Check if the password match the hash int zero = 0; for (int idx = 0; idx < check.length; ++idx) zero |= hash[salt.length + idx] ^ check[idx]; + return zero == 0; } + /** + * Get the hash iteration + * @param cost the has cost + * @return the iterations + */ private static int iterations(int cost) { if ((cost < 0) || (cost > 30)) throw new IllegalArgumentException("cost: " + cost); return 1 << cost; } + /** + * Generate the password encryption + * @param password the plain password + * @param salt the salt + * @param iterations the hash iterations + * @return the password encoded hash + */ private static byte[] pbkdf2(char[] password, byte[] salt, int iterations) { KeySpec spec = new PBEKeySpec(password, salt, iterations, SIZE); try {