Setup model, user model and identification
This commit is contained in:
parent
2ebe7a68ae
commit
a6ec8e5676
17 changed files with 426 additions and 18 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
.idea/
|
||||
/target/
|
||||
/server.properties
|
||||
|
|
5
pom.xml
5
pom.xml
|
@ -43,6 +43,11 @@
|
|||
<version>5.7.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mariadb.jdbc</groupId>
|
||||
<artifactId>mariadb-java-client</artifactId>
|
||||
<version>2.7.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 <address> <port>");
|
||||
System.out.println("\t<address>: server's ip address");
|
||||
System.out.println("\t<port>: server's port");
|
||||
System.out.println("\t<UUID>: user's UUID");
|
||||
System.out.println("\t<Password>: user's password");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
41
src/fr/univ/lyon1/common/User.java
Normal file
41
src/fr/univ/lyon1/common/User.java
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
13
src/fr/univ/lyon1/common/channel/Channel.java
Normal file
13
src/fr/univ/lyon1/common/channel/Channel.java
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
10
src/fr/univ/lyon1/common/channel/PrivateChannel.java
Normal file
10
src/fr/univ/lyon1/common/channel/PrivateChannel.java
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
9
src/fr/univ/lyon1/common/channel/PublicChannel.java
Normal file
9
src/fr/univ/lyon1/common/channel/PublicChannel.java
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
62
src/fr/univ/lyon1/server/Database.java
Normal file
62
src/fr/univ/lyon1/server/Database.java
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<ConnectedClient> 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);
|
||||
|
||||
|
|
10
src/fr/univ/lyon1/server/models/Model.java
Normal file
10
src/fr/univ/lyon1/server/models/Model.java
Normal file
|
@ -0,0 +1,10 @@
|
|||
package fr.univ.lyon1.server.models;
|
||||
|
||||
import fr.univ.lyon1.server.Database;
|
||||
|
||||
|
||||
public interface Model {
|
||||
Database database = Database.getDatabase();
|
||||
|
||||
|
||||
}
|
176
src/fr/univ/lyon1/server/models/UserModel.java
Normal file
176
src/fr/univ/lyon1/server/models/UserModel.java
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
Reference in a new issue