diff --git a/src/fr/univ/lyon1/server/ConnectedClient.java b/src/fr/univ/lyon1/server/ConnectedClient.java index cb3e226..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) @@ -53,7 +81,14 @@ public class ConnectedClient implements Runnable { } } - 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); @@ -71,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); @@ -89,16 +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 { - out.writeObject(new Command(CommandType.listChannels, Collections.singletonList((List)(List) ChannelModel.getAll()))); - out.flush(); + 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); @@ -110,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) { @@ -143,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(); @@ -150,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 111ee31..4429ffa 100644 --- a/src/fr/univ/lyon1/server/models/ChannelModel.java +++ b/src/fr/univ/lyon1/server/models/ChannelModel.java @@ -10,26 +10,53 @@ 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 = ?"); @@ -46,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 = ?"); @@ -65,6 +97,10 @@ 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 { @@ -84,6 +120,10 @@ public class ChannelModel extends Channel implements Model { 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 = ?"); @@ -96,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 (?, ?)"); @@ -108,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(); @@ -122,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 {