diff --git a/.gitignore b/.gitignore
index 87afada..4d66140 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
.idea/
/target/
+/server.properties
diff --git a/pom.xml b/pom.xml
index b0e06c3..2a98067 100644
--- a/pom.xml
+++ b/pom.xml
@@ -43,6 +43,11 @@
5.7.1
test
+
+ org.mariadb.jdbc
+ mariadb-java-client
+ 2.7.1
+
diff --git a/src/fr/univ/lyon1/client/Client.java b/src/fr/univ/lyon1/client/Client.java
index 19718cc..b5f33cb 100644
--- a/src/fr/univ/lyon1/client/Client.java
+++ b/src/fr/univ/lyon1/client/Client.java
@@ -15,11 +15,31 @@ public class Client {
private ObjectInputStream in;
protected boolean started = false;
- public Client(String address, int port) throws IOException, InterruptedException {
+ public Client(String address, int port, String uuid, String password) throws IOException, InterruptedException, Exception {
this.address = address;
this.port = port;
socket = new Socket(address, port);
out = new ObjectOutputStream(socket.getOutputStream());
+ while (!this.auth(uuid, password));
+ }
+
+ public boolean auth(String uuid, String password) throws IOException {
+ getIn();
+
+ out.writeUTF(uuid);
+ out.flush();
+ out.writeUTF(password);
+ out.flush();
+
+ String response = in.readUTF();
+ System.out.println(response);
+
+ if (response.startsWith("err:"))
+ return false;
+ else if (response.equals("logged"))
+ return true;
+ else
+ throw new IOException("Uk message");
}
public void disconnectedServer() throws IOException {
@@ -66,4 +86,10 @@ public class Client {
socket.close();
clientReceiveThread.interrupt();
}
+
+ public ObjectInputStream getIn() throws IOException {
+ if (in == null)
+ in = new ObjectInputStream(socket.getInputStream());
+ return in;
+ }
}
diff --git a/src/fr/univ/lyon1/client/ClientReceive.java b/src/fr/univ/lyon1/client/ClientReceive.java
index ddccb12..4a906b2 100644
--- a/src/fr/univ/lyon1/client/ClientReceive.java
+++ b/src/fr/univ/lyon1/client/ClientReceive.java
@@ -20,7 +20,7 @@ public class ClientReceive implements Runnable {
@Override
public void run() {
try {
- in = new ObjectInputStream(socket.getInputStream());
+ in = client.getIn();
} catch (IOException e) {
e.printStackTrace();
return;
diff --git a/src/fr/univ/lyon1/client/MainClient.java b/src/fr/univ/lyon1/client/MainClient.java
index 82c088b..afc4565 100644
--- a/src/fr/univ/lyon1/client/MainClient.java
+++ b/src/fr/univ/lyon1/client/MainClient.java
@@ -1,20 +1,21 @@
package fr.univ.lyon1.client;
-import java.io.IOException;
-
public class MainClient {
public static void main(String[] args) {
try {
- if (args.length != 2) {
+ if (args.length != 4) {
printUsage();
} else {
String address = args[0];
int port = Integer.parseInt(args[1]);
- Client c = new Client(address, port);
+ String uuid = args[2];
+ String password = args[3];
+ Client c = new Client(address, port, uuid, password);
c.run();
}
- } catch (IOException|InterruptedException e) {
+ } catch (Exception e) {
e.printStackTrace();
+ System.exit(1);
}
}
@@ -22,5 +23,7 @@ public class MainClient {
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: user's password");
}
}
diff --git a/src/fr/univ/lyon1/common/Message.java b/src/fr/univ/lyon1/common/Message.java
index 15a8638..c8e5b8f 100644
--- a/src/fr/univ/lyon1/common/Message.java
+++ b/src/fr/univ/lyon1/common/Message.java
@@ -1,22 +1,31 @@
package fr.univ.lyon1.common;
import java.io.Serializable;
+import java.util.UUID;
public class Message implements Serializable {
- private String sender;
+ private User sender;
private final String content;
+ private final UUID uuid;
- public Message(String sender, String content) {
+ public Message(User sender, String content) {
+ this.uuid = UUID.randomUUID();
this.sender = sender;
this.content = content;
}
- public void setSender(String sender) {
+ public Message(UUID uuid, User sender, String content) {
+ this.uuid = uuid;
+ this.sender = sender;
+ this.content = content;
+ }
+
+ public void setSender(User sender) {
this.sender = sender;
}
- public String getSender() {
+ public User getSender() {
return sender;
}
diff --git a/src/fr/univ/lyon1/common/User.java b/src/fr/univ/lyon1/common/User.java
new file mode 100644
index 0000000..5a14351
--- /dev/null
+++ b/src/fr/univ/lyon1/common/User.java
@@ -0,0 +1,41 @@
+package fr.univ.lyon1.common;
+
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import java.io.Serializable;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.UUID;
+import java.util.regex.Matcher;
+
+public class User implements Serializable {
+ private final UUID uuid;
+ private String username;
+
+ public User(UUID uuid, String username) {
+ this.uuid = uuid;
+ this.username = username;
+ }
+
+ public User(String username) {
+ this.uuid = UUID.randomUUID();
+ this.username = username;
+ }
+
+ public UUID getUUID() {
+ return uuid;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ @Override
+ public String toString() {
+ return username;
+ }
+}
diff --git a/src/fr/univ/lyon1/common/channel/Channel.java b/src/fr/univ/lyon1/common/channel/Channel.java
new file mode 100644
index 0000000..fce3126
--- /dev/null
+++ b/src/fr/univ/lyon1/common/channel/Channel.java
@@ -0,0 +1,13 @@
+package fr.univ.lyon1.common.channel;
+
+import java.util.UUID;
+
+public abstract class Channel {
+ private final UUID uuid;
+ private String name;
+
+ public Channel(UUID uuid, String name) {
+ this.uuid = uuid;
+ this.name = name;
+ }
+}
diff --git a/src/fr/univ/lyon1/common/channel/PrivateChannel.java b/src/fr/univ/lyon1/common/channel/PrivateChannel.java
new file mode 100644
index 0000000..92ecbd8
--- /dev/null
+++ b/src/fr/univ/lyon1/common/channel/PrivateChannel.java
@@ -0,0 +1,10 @@
+package fr.univ.lyon1.common.channel;
+
+import java.util.UUID;
+
+public class PrivateChannel extends Channel {
+
+ public PrivateChannel(UUID uuid, String name) {
+ super(uuid, name);
+ }
+}
diff --git a/src/fr/univ/lyon1/common/channel/PublicChannel.java b/src/fr/univ/lyon1/common/channel/PublicChannel.java
new file mode 100644
index 0000000..068dec2
--- /dev/null
+++ b/src/fr/univ/lyon1/common/channel/PublicChannel.java
@@ -0,0 +1,9 @@
+package fr.univ.lyon1.common.channel;
+
+import java.util.UUID;
+
+public class PublicChannel extends Channel {
+ public PublicChannel(UUID uuid, String name) {
+ super(uuid, name);
+ }
+}
diff --git a/src/fr/univ/lyon1/gui/ClientGUI.java b/src/fr/univ/lyon1/gui/ClientGUI.java
index 634aefc..cb19ab2 100644
--- a/src/fr/univ/lyon1/gui/ClientGUI.java
+++ b/src/fr/univ/lyon1/gui/ClientGUI.java
@@ -9,8 +9,8 @@ import java.io.IOException;
public class ClientGUI extends Client {
private final MainGui gui;
- public ClientGUI(MainGui gui, String address, int port) throws IOException, InterruptedException {
- super(address, port);
+ public ClientGUI(MainGui gui, String address, int port) throws IOException, InterruptedException, Exception {
+ super(address, port, null, null);
this.gui = gui;
}
diff --git a/src/fr/univ/lyon1/server/ConnectedClient.java b/src/fr/univ/lyon1/server/ConnectedClient.java
index 39d8d0b..8f3de7c 100644
--- a/src/fr/univ/lyon1/server/ConnectedClient.java
+++ b/src/fr/univ/lyon1/server/ConnectedClient.java
@@ -1,6 +1,8 @@
package fr.univ.lyon1.server;
import fr.univ.lyon1.common.Message;
+import fr.univ.lyon1.common.User;
+import fr.univ.lyon1.server.models.UserModel;
import java.io.EOFException;
import java.io.IOException;
@@ -15,11 +17,47 @@ public class ConnectedClient implements Runnable {
private final Socket socket;
private final ObjectOutputStream out;
private ObjectInputStream in;
+ private User user;
ConnectedClient(Server server, Socket socket) throws IOException {
this.server = server;
this.socket = socket;
this.out = new ObjectOutputStream(socket.getOutputStream());
+
+ System.out.println("New user try to auth");
+ while (!this.auth());
+ }
+
+ private boolean auth() throws IOException {
+ if (in == null)
+ in = new ObjectInputStream(socket.getInputStream());
+
+ String username = in.readUTF();
+ System.out.println("username: "+username);
+ String password = in.readUTF();
+ System.out.println("Pass: "+password);
+
+ if (username.isEmpty() || password.isEmpty()) {
+ out.writeUTF("err: Login required");
+ out.flush();
+ return false;
+ }
+
+ UserModel user = UserModel.get(username);
+
+ if (user == null)
+ out.writeUTF("err: Username not found !");
+ else if (!user.checkPassword(password))
+ out.writeUTF("err: Password invalid !");
+ else {
+ out.writeUTF("logged");
+ out.flush();
+ this.user = user;
+ return true;
+ }
+ out.flush();
+
+ return false;
}
public Message sendMessage(Message message) throws IOException {
@@ -30,15 +68,13 @@ public class ConnectedClient implements Runnable {
public void run() {
try {
- in = new ObjectInputStream(socket.getInputStream());
-
while (true) {
Message msg = (Message) in.readObject();
if (msg == null)
break;
- msg.setSender(String.valueOf(id));
+ msg.setSender(this.user);
server.broadcastMessage(msg, id);
}
} catch (IOException | ClassNotFoundException e) {
diff --git a/src/fr/univ/lyon1/server/Database.java b/src/fr/univ/lyon1/server/Database.java
new file mode 100644
index 0000000..b3651ab
--- /dev/null
+++ b/src/fr/univ/lyon1/server/Database.java
@@ -0,0 +1,62 @@
+package fr.univ.lyon1.server;
+
+import java.io.*;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.util.Properties;
+
+public class Database {
+ private static Database database;
+ private Connection connection;
+
+ private Database() {
+ Database.database = this;
+ try {
+ this.connection = getConnexion();
+ } catch (IOException | SQLException err) {
+ err.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private String[] getCredentials() throws NullPointerException, IOException {
+ Properties props = new Properties();
+ File f = new File("server.properties");
+
+ if (!f.exists()) {
+ props.setProperty("db.url", "jdbc:mariadb://localhost:3306/chat");
+ props.setProperty("db.user", "chat");
+ props.setProperty("db.password", "password");
+
+ props.store(new FileWriter(f), "");
+ } else {
+ props.load(new FileReader(f));
+ }
+
+ return new String[]{props.getProperty("db.url"), props.getProperty("db.user"), props.getProperty("db.password")};
+ }
+
+ private Connection getConnexion() throws SQLException, IOException {
+ String[] credentials = getCredentials();
+
+ try {
+ Class.forName("org.mariadb.jdbc.Driver");
+ } catch (ClassNotFoundException err) {
+ System.err.println("MariaDB driver not found !");
+ System.exit(1);
+ }
+
+ return DriverManager.getConnection(credentials[0], credentials[1], credentials[2]);
+ }
+
+ public Connection getConnection() {
+ return connection;
+ }
+
+ public static Database getDatabase() {
+ if (Database.database == null)
+ return new Database();
+ return Database.database;
+ }
+}
diff --git a/src/fr/univ/lyon1/server/Server.java b/src/fr/univ/lyon1/server/Server.java
index ca711d6..3e7d64f 100644
--- a/src/fr/univ/lyon1/server/Server.java
+++ b/src/fr/univ/lyon1/server/Server.java
@@ -1,23 +1,27 @@
package fr.univ.lyon1.server;
import fr.univ.lyon1.common.Message;
+import fr.univ.lyon1.common.User;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
+import java.util.UUID;
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");
Server(int port) throws IOException {
this.port = port;
+ Database.getDatabase();
Thread connection = new Thread(new Connection(this));
connection.start();
}
public ConnectedClient addClient(ConnectedClient newClient) {
- Message msg = new Message( "Server", newClient.getId() + " is connected !");
+ Message msg = new Message( serverUser, newClient.getId() + " is connected !");
clients.add(newClient);
@@ -51,7 +55,7 @@ public class Server {
clients.remove(client);
- Message msg = new Message("Server", "Client "+client.getId()+" is disconnected");
+ Message msg = new Message(serverUser, "Client "+client.getId()+" is disconnected");
broadcastMessage(msg, -1);
diff --git a/src/fr/univ/lyon1/server/models/Model.java b/src/fr/univ/lyon1/server/models/Model.java
new file mode 100644
index 0000000..aca3034
--- /dev/null
+++ b/src/fr/univ/lyon1/server/models/Model.java
@@ -0,0 +1,10 @@
+package fr.univ.lyon1.server.models;
+
+import fr.univ.lyon1.server.Database;
+
+
+public interface Model {
+ Database database = Database.getDatabase();
+
+
+}
diff --git a/src/fr/univ/lyon1/server/models/UserModel.java b/src/fr/univ/lyon1/server/models/UserModel.java
new file mode 100644
index 0000000..95280db
--- /dev/null
+++ b/src/fr/univ/lyon1/server/models/UserModel.java
@@ -0,0 +1,176 @@
+package fr.univ.lyon1.server.models;
+
+import fr.univ.lyon1.common.User;
+
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.UUID;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class UserModel extends User implements Model {
+ private String passwordHash;
+
+ private final SecureRandom random = new SecureRandom();
+ public static final String ID = "$1$";
+ private static final int SIZE = 128;
+ private static final int COST = 16;
+ private static final String ALGORITHM = "PBKDF2WithHmacSHA1";
+ private static final Pattern LAYOUT = Pattern.compile("\\$1\\$(\\d\\d?)\\$(.{43})");
+ private static final String TABLE_NAME = "User";
+
+ public UserModel(String username, String password) {
+ super(username);
+ setPassword(password);
+ create();
+ }
+
+ private UserModel(UUID uuid, String username, String passwordHash) {
+ super(uuid, username);
+ this.passwordHash = passwordHash;
+ }
+
+ public static UserModel get(String username) {
+ try {
+ PreparedStatement ps = database.getConnection().prepareStatement("SELECT UUID FROM "+TABLE_NAME+" WHERE username = ?");
+ ps.setString(1, username);
+ if (ps.execute()) {
+ ResultSet rs = ps.getResultSet();
+ if (rs.next())
+ return get(UUID.fromString(rs.getString("UUID")));
+ }
+ } catch (SQLException err) {
+ err.printStackTrace();
+ return null;
+ }
+ return null;
+ }
+
+ public static UserModel get(UUID uuid) {
+ try {
+ PreparedStatement ps = database.getConnection().prepareStatement("SELECT * FROM "+TABLE_NAME+" WHERE UUID = ?");
+ ps.setString(1, uuid.toString());
+ if (ps.execute()) {
+ ResultSet rs = ps.getResultSet();
+ if (rs.next())
+ return new UserModel(
+ UUID.fromString(rs.getString("UUID")),
+ rs.getString("USERNAME"),
+ rs.getString("PASSWORD")
+ );
+ }
+ } catch (SQLException err) {
+ err.printStackTrace();
+ return null;
+ }
+ return null;
+ }
+
+ private boolean exist() {
+ try {
+ PreparedStatement ps = database.getConnection().prepareStatement("SELECT UUID FROM "+TABLE_NAME+" WHERE UUID = ?");
+ ps.setString(1, super.getUUID().toString());
+ ps.execute();
+ return ps.getResultSet().next();
+ } catch (SQLException err) {
+ err.printStackTrace();
+ return false;
+ }
+ }
+
+ private boolean create() {
+ try {
+ PreparedStatement ps = database.getConnection().prepareStatement("INSERT INTO "+TABLE_NAME+" (UUID, username, password) VALUES (?, ?, ?)");
+ ps.setString(1, super.getUUID().toString());
+ ps.setString(2, super.getUsername());
+ ps.setString(3, getPasswordHash());
+ return ps.executeUpdate() > 0;
+ } catch (SQLException err) {
+ err.printStackTrace();
+ return false;
+ }
+ }
+
+ public boolean save() {
+ if (!exist())
+ return create();
+
+ try {
+ PreparedStatement ps = database.getConnection().prepareStatement("UPDATE "+TABLE_NAME+" SET username = ?, password = ? WHERE UUID = ?");
+ ps.setString(1, super.getUsername());
+ ps.setString(2, getPasswordHash());
+ ps.setString(3, super.getUUID().toString());
+ return ps.executeUpdate() > 0;
+ } catch (SQLException err) {
+ err.printStackTrace();
+ return false;
+ }
+ }
+
+ public static void generateTable() {
+ try {
+ PreparedStatement ps = database.getConnection().prepareStatement("CREATE TABLE "+TABLE_NAME+" ( UUID varchar(40) primary key, username varchar(16) unique, password varchar(256) )");
+ ps.executeUpdate();
+ } catch (SQLException err) {
+ err.printStackTrace();
+ }
+ }
+
+ public String getPasswordHash() {
+ return passwordHash;
+ }
+
+ public void setPassword(String password) {
+ byte[] passwordSalt = new byte[SIZE / 8];
+ random.nextBytes(passwordSalt);
+ 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();
+ passwordHash = ID + COST + '$' + enc.encodeToString(hash);
+ }
+
+ public boolean checkPassword(String password) {
+ Matcher m = LAYOUT.matcher(passwordHash);
+ if (!m.matches())
+ throw new IllegalArgumentException("Invalid token format");
+ 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);
+ int zero = 0;
+ for (int idx = 0; idx < check.length; ++idx)
+ zero |= hash[salt.length + idx] ^ check[idx];
+ return zero == 0;
+ }
+
+ private static int iterations(int cost) {
+ if ((cost < 0) || (cost > 30))
+ throw new IllegalArgumentException("cost: " + cost);
+ return 1 << cost;
+ }
+
+ private static byte[] pbkdf2(char[] password, byte[] salt, int iterations) {
+ KeySpec spec = new PBEKeySpec(password, salt, iterations, SIZE);
+ try {
+ SecretKeyFactory f = SecretKeyFactory.getInstance(ALGORITHM);
+ return f.generateSecret(spec).getEncoded();
+ }
+ catch (NoSuchAlgorithmException ex) {
+ throw new IllegalStateException("Missing algorithm: " + ALGORITHM, ex);
+ }
+ catch (InvalidKeySpecException ex) {
+ throw new IllegalStateException("Invalid SecretKeyFactory", ex);
+ }
+ }
+}
diff --git a/src/module-info.java b/src/module-info.java
index 39f3612..b6f9ef6 100644
--- a/src/module-info.java
+++ b/src/module-info.java
@@ -3,6 +3,9 @@ module fr.univ.lyon1.gui {
requires javafx.fxml;
requires org.kordamp.bootstrapfx.core;
+ requires java.sql;
+
+ requires org.mariadb.jdbc;
opens fr.univ.lyon1.gui to javafx.fxml;
exports fr.univ.lyon1.gui;