Merge branch 'flifloo' into 'main'
Merging flifloo See merge request p1905458/java-tp!4
This commit is contained in:
commit
1d46eda1ba
25 changed files with 725 additions and 47 deletions
|
@ -6,6 +6,7 @@ import fr.univ.lyon1.common.Message;
|
||||||
import fr.univ.lyon1.common.command.Command;
|
import fr.univ.lyon1.common.command.Command;
|
||||||
import fr.univ.lyon1.common.command.CommandType;
|
import fr.univ.lyon1.common.command.CommandType;
|
||||||
import fr.univ.lyon1.common.exception.ChatException;
|
import fr.univ.lyon1.common.exception.ChatException;
|
||||||
|
import fr.univ.lyon1.common.exception.NotInChannel;
|
||||||
import fr.univ.lyon1.common.exception.UnknownCommand;
|
import fr.univ.lyon1.common.exception.UnknownCommand;
|
||||||
|
|
||||||
import javax.net.SocketFactory;
|
import javax.net.SocketFactory;
|
||||||
|
@ -17,8 +18,13 @@ import java.io.ObjectInputStream;
|
||||||
import java.io.ObjectOutputStream;
|
import java.io.ObjectOutputStream;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The core of the client side
|
||||||
|
*/
|
||||||
public class Client {
|
public class Client {
|
||||||
private final int port;
|
private final int port;
|
||||||
private final String address;
|
private final String address;
|
||||||
|
@ -27,10 +33,17 @@ public class Client {
|
||||||
protected final Socket socket;
|
protected final Socket socket;
|
||||||
protected final ObjectOutputStream out;
|
protected final ObjectOutputStream out;
|
||||||
private ObjectInputStream in;
|
private ObjectInputStream in;
|
||||||
private List<Channel> channels = new ArrayList<>();
|
private final List<Channel> channels = new ArrayList<>();
|
||||||
protected boolean started = false;
|
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 {
|
public Client(String address, int port, String username, String password) throws IOException {
|
||||||
this.address = address;
|
this.address = address;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
|
@ -41,6 +54,11 @@ public class Client {
|
||||||
getIn();
|
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 {
|
private Socket initSSL() throws IOException {
|
||||||
SSLContext ctx = ChatSSL.getSSLContext();
|
SSLContext ctx = ChatSSL.getSSLContext();
|
||||||
|
|
||||||
|
@ -54,6 +72,10 @@ public class Client {
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close all connection to the server
|
||||||
|
* @throws IOException if fail to close the connection to the server
|
||||||
|
*/
|
||||||
public void disconnectedServer() throws IOException {
|
public void disconnectedServer() throws IOException {
|
||||||
socket.close();
|
socket.close();
|
||||||
out.close();
|
out.close();
|
||||||
|
@ -63,19 +85,69 @@ public class Client {
|
||||||
System.exit(0);
|
System.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String sendMessage(String content) {
|
/**
|
||||||
|
* Send a command to the server
|
||||||
|
* @param cmd the command
|
||||||
|
*/
|
||||||
|
private void send(Command cmd) {
|
||||||
try {
|
try {
|
||||||
out.writeObject(new Command(CommandType.message, List.of(new Message(content, channels.get(0)))));
|
out.writeObject(cmd);
|
||||||
out.flush();
|
out.flush();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
System.err.println("Fail to send message !");
|
System.err.println("Fail to send command !");
|
||||||
e.printStackTrace();
|
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<String> 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 {
|
public void action(Object data) throws IOException {
|
||||||
if (data instanceof Command)
|
if (data instanceof Command)
|
||||||
command((Command) data);
|
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 {
|
private void command(Command cmd) throws IOException {
|
||||||
switch (cmd.getType()) {
|
switch (cmd.getType()) {
|
||||||
case login -> commandLogin();
|
case login -> commandLogin();
|
||||||
case message -> commandMessage(cmd);
|
case message -> commandMessage(cmd);
|
||||||
case list -> commandList(cmd);
|
case list -> commandList(cmd);
|
||||||
|
case listChannels -> commandListChannels(cmd);
|
||||||
case join -> commandJoin(cmd);
|
case join -> commandJoin(cmd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After login handler
|
||||||
|
* @throws IOException if a connection error occur with the server
|
||||||
|
*/
|
||||||
private void commandLogin() throws IOException {
|
private void commandLogin() throws IOException {
|
||||||
out.writeObject(new Command(CommandType.list, null));
|
out.writeObject(new Command(CommandType.list, null));
|
||||||
out.flush();
|
out.flush();
|
||||||
|
out.writeObject(new Command(CommandType.listChannels, null));
|
||||||
|
out.flush();
|
||||||
out.writeObject(new Command(CommandType.join, List.of("general")));
|
out.writeObject(new Command(CommandType.join, List.of("general")));
|
||||||
out.flush();
|
out.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receiving message from the server
|
||||||
|
* @param cmd the message command
|
||||||
|
*/
|
||||||
protected void commandMessage(Command cmd) {
|
protected void commandMessage(Command cmd) {
|
||||||
System.out.println();
|
System.out.println();
|
||||||
System.out.println(cmd.getArgs().get(0));
|
System.out.println(cmd.getArgs().get(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User list handler
|
||||||
|
* @param cmd the command list
|
||||||
|
*/
|
||||||
private void commandList(Command cmd) {
|
private void commandList(Command cmd) {
|
||||||
List<Object> users = cmd.getArgs();
|
System.out.println("Users: "+cmd.getArgs().stream().map(Object::toString).collect(Collectors.joining(", ")));
|
||||||
for (Object u : users) {
|
|
||||||
System.out.println(u);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
private void commandJoin(Command cmd) {
|
||||||
Channel chan = (Channel) cmd.getArgs().get(0);
|
Channel chan = (Channel) cmd.getArgs().get(0);
|
||||||
channels.add(chan);
|
channels.add(chan);
|
||||||
System.out.println("You join "+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 {
|
public void run() throws InterruptedException, IOException {
|
||||||
if (started)
|
if (started)
|
||||||
return;
|
return;
|
||||||
|
@ -141,6 +247,11 @@ public class Client {
|
||||||
clientReceiveThread.interrupt();
|
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 {
|
public ObjectInputStream getIn() throws IOException {
|
||||||
if (in == null)
|
if (in == null)
|
||||||
in = new ObjectInputStream(socket.getInputStream());
|
in = new ObjectInputStream(socket.getInputStream());
|
||||||
|
|
|
@ -5,16 +5,27 @@ import java.io.ObjectInputStream;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.net.SocketException;
|
import java.net.SocketException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clint message receiver manager
|
||||||
|
*/
|
||||||
public class ClientReceive implements Runnable {
|
public class ClientReceive implements Runnable {
|
||||||
private final Client client;
|
private final Client client;
|
||||||
private ObjectInputStream in;
|
private ObjectInputStream in;
|
||||||
private final Socket socket;
|
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) {
|
public ClientReceive(Client client, Socket socket) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mai thread
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,20 +1,33 @@
|
||||||
package fr.univ.lyon1.client;
|
package fr.univ.lyon1.client;
|
||||||
|
|
||||||
|
import fr.univ.lyon1.common.exception.ChatException;
|
||||||
|
|
||||||
import java.io.ObjectOutputStream;
|
import java.io.ObjectOutputStream;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.util.Scanner;
|
import java.util.Scanner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clint message sender manager
|
||||||
|
*/
|
||||||
public class ClientSend implements Runnable {
|
public class ClientSend implements Runnable {
|
||||||
private final Client client;
|
private final Client client;
|
||||||
private final ObjectOutputStream out;
|
private final ObjectOutputStream out;
|
||||||
private final Socket socket;
|
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) {
|
public ClientSend(Client client, ObjectOutputStream out, Socket socket) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.out = out;
|
this.out = out;
|
||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mai thread
|
||||||
|
*/
|
||||||
public void run() {
|
public void run() {
|
||||||
Scanner sc = new Scanner(System.in);
|
Scanner sc = new Scanner(System.in);
|
||||||
|
|
||||||
|
@ -25,7 +38,16 @@ public class ClientSend implements Runnable {
|
||||||
if (m.equals("exit"))
|
if (m.equals("exit"))
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (m.startsWith("/"))
|
||||||
|
client.sendCommand(m);
|
||||||
|
else if (m.startsWith("#"))
|
||||||
|
client.sendMessageChannel(m);
|
||||||
|
else
|
||||||
client.sendMessage(m);
|
client.sendMessage(m);
|
||||||
|
} catch (ChatException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
package fr.univ.lyon1.client;
|
package fr.univ.lyon1.client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main program for the client CLI
|
||||||
|
*/
|
||||||
public class MainClient {
|
public class MainClient {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
try {
|
try {
|
||||||
|
@ -19,11 +22,14 @@ public class MainClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Help usage for arguments
|
||||||
|
*/
|
||||||
private static void printUsage() {
|
private static void printUsage() {
|
||||||
System.out.println("java client.Client <address> <port>");
|
System.out.println("java client.Client <address> <port>");
|
||||||
System.out.println("\t<address>: server's ip address");
|
System.out.println("\t<address>: server's ip address");
|
||||||
System.out.println("\t<port>: server's port");
|
System.out.println("\t<port>: server's port");
|
||||||
System.out.println("\t<UUID>: user's UUID");
|
System.out.println("\t<username>: username");
|
||||||
System.out.println("\t<Password>: user's password");
|
System.out.println("\t<Password>: user's password");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,28 +3,52 @@ package fr.univ.lyon1.common;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base type of chat channel
|
||||||
|
*/
|
||||||
public class Channel implements Serializable {
|
public class Channel implements Serializable {
|
||||||
private final UUID uuid;
|
private final UUID uuid;
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an existing channel
|
||||||
|
* @param uuid the channel unique id
|
||||||
|
* @param name the channel name
|
||||||
|
*/
|
||||||
public Channel(UUID uuid, String name) {
|
public Channel(UUID uuid, String name) {
|
||||||
this.uuid = uuid;
|
this.uuid = uuid;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new channel
|
||||||
|
* @param name the new channel name
|
||||||
|
*/
|
||||||
public Channel(String name) {
|
public Channel(String name) {
|
||||||
this.uuid = UUID.randomUUID();
|
this.uuid = UUID.randomUUID();
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the channel unique id
|
||||||
|
* @return the channel unique id
|
||||||
|
*/
|
||||||
public UUID getUUID() {
|
public UUID getUUID() {
|
||||||
return uuid;
|
return uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the channel name
|
||||||
|
* @return the channel name
|
||||||
|
*/
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* String version of the channel
|
||||||
|
* @return the channel name
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return name;
|
return name;
|
||||||
|
|
|
@ -10,12 +10,13 @@ import java.io.InputStream;
|
||||||
import java.security.*;
|
import java.security.*;
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
|
|
||||||
/*
|
/**
|
||||||
keytool -genkeypair -alias server -keyalg EC \
|
* The SSL context for client/server communication
|
||||||
-sigalg SHA384withECDSA -keysize 256 -keystore servercert.p12 \
|
* A key store can be generated with this command:
|
||||||
-storetype pkcs12 -v -storepass abc123 -validity 10000 -ext san=ip:127.0.0.1
|
* 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 class ChatSSL {
|
||||||
public static String trustStoreName = "servercert.p12";
|
public static String trustStoreName = "servercert.p12";
|
||||||
public static String keyStoreName = "servercert.p12";
|
public static String keyStoreName = "servercert.p12";
|
||||||
|
@ -23,25 +24,37 @@ public class ChatSSL {
|
||||||
private static char[] trustStorePassword = "abc123".toCharArray();
|
private static char[] trustStorePassword = "abc123".toCharArray();
|
||||||
private static char[] keyStorePassword = "abc123".toCharArray();
|
private static char[] keyStorePassword = "abc123".toCharArray();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the SSL context for communication
|
||||||
|
* @return the SSL context
|
||||||
|
*/
|
||||||
public static SSLContext getSSLContext() {
|
public static SSLContext getSSLContext() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Get the SSL trust store from package resources
|
||||||
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||||
InputStream tstore = Connection.class
|
InputStream tstore = Connection.class
|
||||||
.getResourceAsStream("/" + trustStoreName);
|
.getResourceAsStream("/" + trustStoreName);
|
||||||
trustStore.load(tstore, trustStorePassword);
|
trustStore.load(tstore, trustStorePassword);
|
||||||
tstore.close();
|
tstore.close();
|
||||||
|
|
||||||
|
// Create a trust factory from the trust store
|
||||||
TrustManagerFactory tmf = TrustManagerFactory
|
TrustManagerFactory tmf = TrustManagerFactory
|
||||||
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||||
tmf.init(trustStore);
|
tmf.init(trustStore);
|
||||||
|
|
||||||
|
// Get the SSL key store from package resources
|
||||||
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||||
InputStream kstore = Connection.class
|
InputStream kstore = Connection.class
|
||||||
.getResourceAsStream("/" + keyStoreName);
|
.getResourceAsStream("/" + keyStoreName);
|
||||||
keyStore.load(kstore, keyStorePassword);
|
keyStore.load(kstore, keyStorePassword);
|
||||||
|
kstore.close();
|
||||||
|
|
||||||
|
// Create a key factory from the key store
|
||||||
KeyManagerFactory kmf = KeyManagerFactory
|
KeyManagerFactory kmf = KeyManagerFactory
|
||||||
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
||||||
kmf.init(keyStore, keyStorePassword);
|
kmf.init(keyStore, keyStorePassword);
|
||||||
|
|
||||||
|
// Generate the SSL context
|
||||||
SSLContext ctx = SSLContext.getInstance("TLS");
|
SSLContext ctx = SSLContext.getInstance("TLS");
|
||||||
ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(),
|
ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(),
|
||||||
SecureRandom.getInstanceStrong());
|
SecureRandom.getInstanceStrong());
|
||||||
|
|
|
@ -3,13 +3,21 @@ package fr.univ.lyon1.common;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base type of message in client/server communication
|
||||||
|
*/
|
||||||
public class Message implements Serializable {
|
public class Message implements Serializable {
|
||||||
private Channel channel;
|
private Channel channel;
|
||||||
private User sender;
|
private User sender;
|
||||||
private final String content;
|
private final String content;
|
||||||
private final UUID uuid;
|
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) {
|
public Message(Channel channel, User sender, String content) {
|
||||||
this.uuid = UUID.randomUUID();
|
this.uuid = UUID.randomUUID();
|
||||||
this.channel = channel;
|
this.channel = channel;
|
||||||
|
@ -17,6 +25,13 @@ public class Message implements Serializable {
|
||||||
this.content = content;
|
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) {
|
public Message(UUID uuid, Channel channel, User sender, String content) {
|
||||||
this.uuid = uuid;
|
this.uuid = uuid;
|
||||||
this.channel = channel;
|
this.channel = channel;
|
||||||
|
@ -24,32 +39,64 @@ public class Message implements Serializable {
|
||||||
this.content = content;
|
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) {
|
public Message(String content, Channel channel) {
|
||||||
this.uuid = UUID.randomUUID();
|
this.uuid = UUID.randomUUID();
|
||||||
this.content = content;
|
this.content = content;
|
||||||
this.channel = channel;
|
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) {
|
public Message repley(User user, String content) {
|
||||||
return new Message(this.channel, user, content);
|
return new Message(this.channel, user, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the sender
|
||||||
|
* @param sender the new sender
|
||||||
|
*/
|
||||||
public void setSender(User sender) {
|
public void setSender(User sender) {
|
||||||
this.sender = sender;
|
this.sender = sender;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the message channel
|
||||||
|
* @return the channel
|
||||||
|
*/
|
||||||
public Channel getChannel() {
|
public Channel getChannel() {
|
||||||
return channel;
|
return channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the message sender
|
||||||
|
* @return the sender
|
||||||
|
*/
|
||||||
public User getSender() {
|
public User getSender() {
|
||||||
return sender;
|
return sender;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the content of the message
|
||||||
|
* @return the message content
|
||||||
|
*/
|
||||||
public String getContent() {
|
public String getContent() {
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Printable version of the message
|
||||||
|
* @return the channel, sender and the content
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
if (channel != null)
|
if (channel != null)
|
||||||
|
|
|
@ -2,19 +2,36 @@ package fr.univ.lyon1.common;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
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;
|
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) {
|
public record ServerConfiguration(@NotNull String address, int port) {
|
||||||
@NotNull
|
@NotNull
|
||||||
private static final File file = new File("connection.properties");
|
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) {
|
public ServerConfiguration(@NotNull String address, int port) {
|
||||||
this.address = address;
|
this.address = address;
|
||||||
this.port = port;
|
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
|
// Check if file non exists, return error to launch server configuration
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
System.out.println("File not 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")));
|
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 {
|
public void save() throws IOException {
|
||||||
@NotNull final Properties properties = new Properties();
|
@NotNull final Properties properties = new Properties();
|
||||||
properties.setProperty("address", this.address);
|
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");
|
properties.store(new FileWriter(file), "Information needed to connect to the server");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the server address
|
||||||
|
* @return the server address
|
||||||
|
*/
|
||||||
public @NotNull String getAddress() {
|
public @NotNull String getAddress() {
|
||||||
return address;
|
return address;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the server port
|
||||||
|
* @return the server port
|
||||||
|
*/
|
||||||
public int getPort() {
|
public int getPort() {
|
||||||
return port;
|
return port;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,28 +3,52 @@ package fr.univ.lyon1.common;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base type of user for client/server communication
|
||||||
|
*/
|
||||||
public class User implements Serializable {
|
public class User implements Serializable {
|
||||||
private final UUID uuid;
|
private final UUID uuid;
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an existing user
|
||||||
|
* @param uuid the user unique id
|
||||||
|
* @param username the username
|
||||||
|
*/
|
||||||
public User(UUID uuid, String username) {
|
public User(UUID uuid, String username) {
|
||||||
this.uuid = uuid;
|
this.uuid = uuid;
|
||||||
this.username = username;
|
this.username = username;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new user
|
||||||
|
* @param username the username
|
||||||
|
*/
|
||||||
public User(String username) {
|
public User(String username) {
|
||||||
this.uuid = UUID.randomUUID();
|
this.uuid = UUID.randomUUID();
|
||||||
this.username = username;
|
this.username = username;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user unique id
|
||||||
|
* @return thee unique id
|
||||||
|
*/
|
||||||
public UUID getUUID() {
|
public UUID getUUID() {
|
||||||
return uuid;
|
return uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the username
|
||||||
|
* @return the username
|
||||||
|
*/
|
||||||
public String getUsername() {
|
public String getUsername() {
|
||||||
return username;
|
return username;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user as a string
|
||||||
|
* @return the username
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return username;
|
return username;
|
||||||
|
|
|
@ -4,19 +4,37 @@ import java.io.Serializable;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command is the base object transmitted between clients and server
|
||||||
|
*/
|
||||||
public class Command implements Serializable {
|
public class Command implements Serializable {
|
||||||
private final CommandType type;
|
private final CommandType type;
|
||||||
private final List<Object> args;
|
private final List<Object> args;
|
||||||
|
|
||||||
public Command(CommandType type, List<Object> 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.type = type;
|
||||||
this.args = args;
|
this.args = (List<Object>) args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the type of the command
|
||||||
|
* @see CommandType
|
||||||
|
* @return the command type
|
||||||
|
*/
|
||||||
public CommandType getType() {
|
public CommandType getType() {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the arguments of the command
|
||||||
|
* @return A list of arguments
|
||||||
|
*/
|
||||||
public List<Object> getArgs() {
|
public List<Object> getArgs() {
|
||||||
return new ArrayList<>(args);
|
return new ArrayList<>(args);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,10 @@ package fr.univ.lyon1.common.command;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of command types
|
||||||
|
* @see Command
|
||||||
|
*/
|
||||||
public enum CommandType implements Serializable {
|
public enum CommandType implements Serializable {
|
||||||
login("login", "Login to the server"),
|
login("login", "Login to the server"),
|
||||||
message("message", "Send a message"),
|
message("message", "Send a message"),
|
||||||
|
@ -12,15 +16,29 @@ public enum CommandType implements Serializable {
|
||||||
|
|
||||||
private final String name;
|
private final String name;
|
||||||
private final String description;
|
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) {
|
CommandType(String name, String description) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of the command
|
||||||
|
* @return the command name
|
||||||
|
*/
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the description of the command
|
||||||
|
* @return the command description
|
||||||
|
*/
|
||||||
public String getDescription() {
|
public String getDescription() {
|
||||||
return description;
|
return description;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
package fr.univ.lyon1.common.exception;
|
package fr.univ.lyon1.common.exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main exception of the chat system for client and server communication
|
||||||
|
*/
|
||||||
public class ChatException extends Exception {
|
public class ChatException extends Exception {
|
||||||
|
/**
|
||||||
|
* This exception need only a message
|
||||||
|
* @param message the message
|
||||||
|
*/
|
||||||
public ChatException(String message) {
|
public ChatException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
package fr.univ.lyon1.common.exception;
|
package fr.univ.lyon1.common.exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception when a login/password is invalid
|
||||||
|
*/
|
||||||
public class LoginInvalid extends ChatException {
|
public class LoginInvalid extends ChatException {
|
||||||
public LoginInvalid(String message) {
|
public LoginInvalid(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
package fr.univ.lyon1.common.exception;
|
package fr.univ.lyon1.common.exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception when the user is not logged
|
||||||
|
*/
|
||||||
public class LoginRequired extends ChatException {
|
public class LoginRequired extends ChatException {
|
||||||
public LoginRequired() {
|
public LoginRequired() {
|
||||||
super("Login required");
|
super("Login required");
|
||||||
|
|
|
@ -2,8 +2,15 @@ package fr.univ.lyon1.common.exception;
|
||||||
|
|
||||||
import fr.univ.lyon1.common.Channel;
|
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 class NotInChannel extends ChatException {
|
||||||
public NotInChannel(Channel channel) {
|
public NotInChannel(Channel channel) {
|
||||||
super("Your not in channel "+channel);
|
super("Your not in channel "+channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public NotInChannel(String channel) {
|
||||||
|
super("Your not in channel "+channel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
package fr.univ.lyon1.common.exception;
|
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 class UnknownCommand extends ChatException {
|
||||||
public UnknownCommand() {
|
public UnknownCommand() {
|
||||||
super("Command unknown");
|
super("Command unknown");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UnknownCommand(String commandName) {
|
||||||
|
super("Command " + commandName + " unknown");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,9 @@ import java.net.Socket;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server client connection management
|
||||||
|
*/
|
||||||
public class ConnectedClient implements Runnable {
|
public class ConnectedClient implements Runnable {
|
||||||
private static int idCounter = 0;
|
private static int idCounter = 0;
|
||||||
private final int id = idCounter++;
|
private final int id = idCounter++;
|
||||||
|
@ -26,6 +29,12 @@ public class ConnectedClient implements Runnable {
|
||||||
private ObjectInputStream in;
|
private ObjectInputStream in;
|
||||||
private UserModel user;
|
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 {
|
ConnectedClient(Server server, Socket socket) throws IOException {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
|
@ -33,12 +42,31 @@ public class ConnectedClient implements Runnable {
|
||||||
this.in = new ObjectInputStream(socket.getInputStream());
|
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();
|
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 {
|
private void actionCommand(Command command) throws IOException, ChatException {
|
||||||
CommandType type = command.getType();
|
CommandType type = command.getType();
|
||||||
if (user == null && type != CommandType.login)
|
if (user == null && type != CommandType.login)
|
||||||
|
@ -48,11 +76,19 @@ public class ConnectedClient implements Runnable {
|
||||||
case login -> commandLogin(command);
|
case login -> commandLogin(command);
|
||||||
case message -> commandMessage(command);
|
case message -> commandMessage(command);
|
||||||
case list -> commandList();
|
case list -> commandList();
|
||||||
|
case listChannels -> commandListChannels();
|
||||||
case join -> commandJoin(command);
|
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<Object> args = cmd.getArgs();
|
List<Object> args = cmd.getArgs();
|
||||||
|
|
||||||
String username = (String) args.get(0);
|
String username = (String) args.get(0);
|
||||||
|
@ -70,13 +106,17 @@ public class ConnectedClient implements Runnable {
|
||||||
else if (!user.checkPassword(password))
|
else if (!user.checkPassword(password))
|
||||||
throw new LoginInvalid("Password invalid");
|
throw new LoginInvalid("Password invalid");
|
||||||
else {
|
else {
|
||||||
out.writeObject(new Command(CommandType.login, null));
|
send(new Command(CommandType.login, null));
|
||||||
out.flush();
|
|
||||||
this.user = user;
|
this.user = user;
|
||||||
System.out.println("Client "+user.getUsername()+" is connected !");
|
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 {
|
private void commandMessage(Command cmd) throws NotInChannel {
|
||||||
Message msg = (Message) cmd.getArgs().get(0);
|
Message msg = (Message) cmd.getArgs().get(0);
|
||||||
msg.setSender(this.user);
|
msg.setSender(this.user);
|
||||||
|
@ -88,11 +128,27 @@ public class ConnectedClient implements Runnable {
|
||||||
server.broadcastMessage(msg, id);
|
server.broadcastMessage(msg, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command user list handler
|
||||||
|
* @throws IOException if a connection error occur with the client
|
||||||
|
*/
|
||||||
private void commandList() throws IOException {
|
private void commandList() throws IOException {
|
||||||
out.writeObject(new Command(CommandType.list, Collections.singletonList(server.getUsers())));
|
send(new Command(CommandType.list, Collections.singletonList(server.getUsers())));
|
||||||
out.flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<Channel>)(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 {
|
private void commandJoin(Command cmd) throws IOException {
|
||||||
String name = (String) cmd.getArgs().get(0);
|
String name = (String) cmd.getArgs().get(0);
|
||||||
ChannelModel chan = ChannelModel.get(name);
|
ChannelModel chan = ChannelModel.get(name);
|
||||||
|
@ -104,12 +160,14 @@ public class ConnectedClient implements Runnable {
|
||||||
if (!chan.have(user))
|
if (!chan.have(user))
|
||||||
chan.addUser(user);
|
chan.addUser(user);
|
||||||
|
|
||||||
out.writeObject(new Command(CommandType.join, List.of((Channel) chan)));
|
send(new Command(CommandType.join, List.of((Channel) chan)));
|
||||||
out.flush();
|
|
||||||
|
|
||||||
server.broadcastMessage(new Message(chan, Server.getServerUser(), user.getUsername()+" joined the channel !"), -1);
|
server.broadcastMessage(new Message(chan, Server.getServerUser(), user.getUsername()+" joined the channel !"), -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Man thread of user connection
|
||||||
|
*/
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
while (true) {
|
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 {
|
public void closeClient() throws IOException {
|
||||||
if (in != null)
|
if (in != null)
|
||||||
in.close();
|
in.close();
|
||||||
|
@ -144,10 +206,18 @@ public class ConnectedClient implements Runnable {
|
||||||
socket.close();
|
socket.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the client connection id
|
||||||
|
* @return connection id
|
||||||
|
*/
|
||||||
public int getId() {
|
public int getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user
|
||||||
|
* @return the user
|
||||||
|
*/
|
||||||
public User getUser() {
|
public User getUser() {
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,15 +7,28 @@ import java.io.IOException;
|
||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server connection manager
|
||||||
|
*/
|
||||||
public class Connection implements Runnable {
|
public class Connection implements Runnable {
|
||||||
private final Server server;
|
private final Server server;
|
||||||
private final ServerSocket serverSocket;
|
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 {
|
Connection(Server server) throws IOException {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.serverSocket = initSSL();
|
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 {
|
private SSLServerSocket initSSL() throws IOException {
|
||||||
|
|
||||||
SSLContext ctx = ChatSSL.getSSLContext();
|
SSLContext ctx = ChatSSL.getSSLContext();
|
||||||
|
@ -29,6 +42,9 @@ public class Connection implements Runnable {
|
||||||
return sslListener;
|
return sslListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main thread
|
||||||
|
*/
|
||||||
public void run() {
|
public void run() {
|
||||||
while (true) {
|
while (true) {
|
||||||
Socket clientSocket;
|
Socket clientSocket;
|
||||||
|
|
|
@ -10,10 +10,16 @@ import java.sql.DriverManager;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server database management
|
||||||
|
*/
|
||||||
public class Database {
|
public class Database {
|
||||||
private static Database database;
|
private static Database database;
|
||||||
private Connection connection;
|
private Connection connection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create database object and establish connection
|
||||||
|
*/
|
||||||
private Database() {
|
private Database() {
|
||||||
Database.database = this;
|
Database.database = this;
|
||||||
try {
|
try {
|
||||||
|
@ -26,7 +32,12 @@ public class Database {
|
||||||
init();
|
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();
|
Properties props = new Properties();
|
||||||
File f = new File("server.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")};
|
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 {
|
private Connection getConnexion() throws SQLException, IOException {
|
||||||
String[] credentials = getCredentials();
|
String[] credentials = getCredentials();
|
||||||
|
|
||||||
|
@ -56,16 +73,27 @@ public class Database {
|
||||||
return DriverManager.getConnection(credentials[0], credentials[1], credentials[2]);
|
return DriverManager.getConnection(credentials[0], credentials[1], credentials[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the database connection
|
||||||
|
* @return the connection
|
||||||
|
*/
|
||||||
public Connection getConnection() {
|
public Connection getConnection() {
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the database instance
|
||||||
|
* @return the database
|
||||||
|
*/
|
||||||
public static Database getDatabase() {
|
public static Database getDatabase() {
|
||||||
if (Database.database == null)
|
if (Database.database == null)
|
||||||
return new Database();
|
return new Database();
|
||||||
return Database.database;
|
return Database.database;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise the database tables from models
|
||||||
|
*/
|
||||||
private void init() {
|
private void init() {
|
||||||
UserModel.generateTable();
|
UserModel.generateTable();
|
||||||
ChannelModel.generateTable();
|
ChannelModel.generateTable();
|
||||||
|
|
|
@ -2,6 +2,9 @@ package fr.univ.lyon1.server;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main server program
|
||||||
|
*/
|
||||||
public class MainServer {
|
public class MainServer {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
try {
|
try {
|
||||||
|
@ -16,6 +19,9 @@ public class MainServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Help usage for arguments
|
||||||
|
*/
|
||||||
private static void printUsage() {
|
private static void printUsage() {
|
||||||
System.out.println("java server.Server <port>");
|
System.out.println("java server.Server <port>");
|
||||||
System.out.println("\t<port>: server's port");
|
System.out.println("\t<port>: server's port");
|
||||||
|
|
|
@ -9,11 +9,19 @@ import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main server management
|
||||||
|
*/
|
||||||
public class Server {
|
public class Server {
|
||||||
private final int port;
|
private final int port;
|
||||||
private List<ConnectedClient> clients = new ArrayList<>();
|
private final List<ConnectedClient> clients = new ArrayList<>();
|
||||||
private static User serverUser = new User(UUID.fromString("3539b6bf-5eb3-41d4-893f-cbf0caa9ca74"), "server");
|
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 {
|
Server(int port) throws IOException {
|
||||||
this.port = port;
|
this.port = port;
|
||||||
Database.getDatabase();
|
Database.getDatabase();
|
||||||
|
@ -21,12 +29,20 @@ public class Server {
|
||||||
connection.start();
|
connection.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ConnectedClient addClient(ConnectedClient newClient) {
|
/**
|
||||||
|
* Add client handler
|
||||||
|
* @param newClient the client
|
||||||
|
*/
|
||||||
|
public void addClient(ConnectedClient newClient) {
|
||||||
clients.add(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<UUID> users = UserChannelModel.getUsers(message.getChannel()).stream().map(User::getUUID).toList();
|
List<UUID> users = UserChannelModel.getUsers(message.getChannel()).stream().map(User::getUUID).toList();
|
||||||
for (ConnectedClient client : clients.stream().filter(connectedClient -> users.contains(connectedClient.getUser().getUUID())).toList()) {
|
for (ConnectedClient client : clients.stream().filter(connectedClient -> users.contains(connectedClient.getUser().getUUID())).toList()) {
|
||||||
if (id == -1 || client.getId() != id)
|
if (id == -1 || client.getId() != id)
|
||||||
|
@ -37,10 +53,13 @@ public class Server {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ConnectedClient disconnectedClient(ConnectedClient client) {
|
/**
|
||||||
|
* Close client connection
|
||||||
|
* @param client the client connection manager
|
||||||
|
*/
|
||||||
|
public void disconnectedClient(ConnectedClient client) {
|
||||||
try {
|
try {
|
||||||
client.closeClient();
|
client.closeClient();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -51,17 +70,28 @@ public class Server {
|
||||||
clients.remove(client);
|
clients.remove(client);
|
||||||
|
|
||||||
System.out.println("Client "+client.getId()+" disconnected");
|
System.out.println("Client "+client.getId()+" disconnected");
|
||||||
return client;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the server listening port
|
||||||
|
* @return the server listening port
|
||||||
|
*/
|
||||||
public int getPort() {
|
public int getPort() {
|
||||||
return port;
|
return port;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the server user
|
||||||
|
* @return the server user
|
||||||
|
*/
|
||||||
public static User getServerUser() {
|
public static User getServerUser() {
|
||||||
return serverUser;
|
return serverUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of connection client to the server
|
||||||
|
* @return list of connected client to the server
|
||||||
|
*/
|
||||||
public List<User> getUsers() {
|
public List<User> getUsers() {
|
||||||
return clients.stream().map(ConnectedClient::getUser).toList();
|
return clients.stream().map(ConnectedClient::getUser).toList();
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,28 +6,57 @@ import fr.univ.lyon1.common.User;
|
||||||
import java.sql.PreparedStatement;
|
import java.sql.PreparedStatement;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database model of a channel
|
||||||
|
*/
|
||||||
public class ChannelModel extends Channel implements Model {
|
public class ChannelModel extends Channel implements Model {
|
||||||
private static final String TABLE_NAME = "Channel";
|
private static final String TABLE_NAME = "Channel";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new channel from a name
|
||||||
|
* @param name the name
|
||||||
|
*/
|
||||||
public ChannelModel(String name) {
|
public ChannelModel(String name) {
|
||||||
super(name);
|
super(name);
|
||||||
create();
|
create();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model from existing channel
|
||||||
|
* @param uuid
|
||||||
|
* @param name
|
||||||
|
*/
|
||||||
private ChannelModel(UUID uuid, String name) {
|
private ChannelModel(UUID uuid, String name) {
|
||||||
super(uuid, 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) {
|
public void addUser(User user) {
|
||||||
new UserChannelModel(user, this);
|
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) {
|
public boolean have(User user) {
|
||||||
return UserChannelModel.exist(user, this);
|
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) {
|
public static ChannelModel get(String name) {
|
||||||
try {
|
try {
|
||||||
PreparedStatement ps = database.getConnection().prepareStatement("SELECT UUID FROM "+TABLE_NAME+" WHERE name = ?");
|
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;
|
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) {
|
public static ChannelModel get(UUID uuid) {
|
||||||
try {
|
try {
|
||||||
PreparedStatement ps = database.getConnection().prepareStatement("SELECT * FROM "+TABLE_NAME+" WHERE UUID = ?");
|
PreparedStatement ps = database.getConnection().prepareStatement("SELECT * FROM "+TABLE_NAME+" WHERE UUID = ?");
|
||||||
|
@ -63,6 +97,33 @@ public class ChannelModel extends Channel implements Model {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all channels
|
||||||
|
* @return a list of channels
|
||||||
|
*/
|
||||||
|
public static List<ChannelModel> getAll() {
|
||||||
|
List<ChannelModel> 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() {
|
private boolean exist() {
|
||||||
try {
|
try {
|
||||||
PreparedStatement ps = database.getConnection().prepareStatement("SELECT UUID FROM "+TABLE_NAME+" WHERE UUID = ?");
|
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() {
|
private boolean create() {
|
||||||
try {
|
try {
|
||||||
PreparedStatement ps = database.getConnection().prepareStatement("INSERT INTO "+TABLE_NAME+" (UUID, name) VALUES (?, ?)");
|
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() {
|
public boolean save() {
|
||||||
if (!exist())
|
if (!exist())
|
||||||
return create();
|
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() {
|
public static void generateTable() {
|
||||||
try {
|
try {
|
||||||
PreparedStatement ps = database.getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS "+TABLE_NAME+" ( UUID varchar(40) primary key, name varchar(16) unique )");
|
PreparedStatement ps = database.getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS "+TABLE_NAME+" ( UUID varchar(40) primary key, name varchar(16) unique )");
|
||||||
|
|
|
@ -2,7 +2,9 @@ package fr.univ.lyon1.server.models;
|
||||||
|
|
||||||
import fr.univ.lyon1.server.Database;
|
import fr.univ.lyon1.server.Database;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base model of a database type
|
||||||
|
*/
|
||||||
public interface Model {
|
public interface Model {
|
||||||
Database database = Database.getDatabase();
|
Database database = Database.getDatabase();
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,12 +10,20 @@ import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database model of the relation between user and channel
|
||||||
|
*/
|
||||||
public class UserChannelModel implements Model {
|
public class UserChannelModel implements Model {
|
||||||
private User user;
|
private User user;
|
||||||
private Channel channel;
|
private Channel channel;
|
||||||
|
|
||||||
private static final String TABLE_NAME = "UserChannel";
|
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) {
|
public UserChannelModel(User user, Channel channel) {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.channel = channel;
|
this.channel = channel;
|
||||||
|
@ -24,6 +32,11 @@ public class UserChannelModel implements Model {
|
||||||
create();
|
create();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of users in a specific channel
|
||||||
|
* @param channel the channel
|
||||||
|
* @return the list of users
|
||||||
|
*/
|
||||||
public static List<User> getUsers(Channel channel) {
|
public static List<User> getUsers(Channel channel) {
|
||||||
List<User> users = new ArrayList<>();
|
List<User> users = new ArrayList<>();
|
||||||
|
|
||||||
|
@ -42,6 +55,11 @@ public class UserChannelModel implements Model {
|
||||||
return users;
|
return users;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of channel where a user is in
|
||||||
|
* @param user the user
|
||||||
|
* @return the list of channels
|
||||||
|
*/
|
||||||
public static List<Channel> getChannels(User user) {
|
public static List<Channel> getChannels(User user) {
|
||||||
List<Channel> channels = new ArrayList<>();
|
List<Channel> channels = new ArrayList<>();
|
||||||
|
|
||||||
|
@ -60,6 +78,12 @@ public class UserChannelModel implements Model {
|
||||||
return channels;
|
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) {
|
public static boolean exist(User user, Channel channel) {
|
||||||
try {
|
try {
|
||||||
PreparedStatement ps = database.getConnection().prepareStatement("SELECT 1 FROM "+TABLE_NAME+" WHERE userUUID = ? AND channelUUID = ?");
|
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() {
|
private boolean create() {
|
||||||
try {
|
try {
|
||||||
PreparedStatement ps = database.getConnection().prepareStatement("INSERT INTO "+TABLE_NAME+" (userUUID, channelUUID) VALUES (?, ?)");
|
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() {
|
public static void generateTable() {
|
||||||
try {
|
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))");
|
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))");
|
||||||
|
|
|
@ -17,6 +17,9 @@ import java.util.UUID;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database model of a user type
|
||||||
|
*/
|
||||||
public class UserModel extends User implements Model {
|
public class UserModel extends User implements Model {
|
||||||
private String passwordHash;
|
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 Pattern LAYOUT = Pattern.compile("\\$1\\$(\\d\\d?)\\$(.{43})");
|
||||||
private static final String TABLE_NAME = "User";
|
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) {
|
public UserModel(String username, String password) {
|
||||||
super(username);
|
super(username);
|
||||||
setPassword(password);
|
setPassword(password);
|
||||||
create();
|
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) {
|
private UserModel(UUID uuid, String username, String passwordHash) {
|
||||||
super(uuid, username);
|
super(uuid, username);
|
||||||
this.passwordHash = passwordHash;
|
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) {
|
public static UserModel get(String username) {
|
||||||
try {
|
try {
|
||||||
PreparedStatement ps = database.getConnection().prepareStatement("SELECT UUID FROM "+TABLE_NAME+" WHERE username = ?");
|
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;
|
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) {
|
public static UserModel get(UUID uuid) {
|
||||||
try {
|
try {
|
||||||
PreparedStatement ps = database.getConnection().prepareStatement("SELECT * FROM "+TABLE_NAME+" WHERE UUID = ?");
|
PreparedStatement ps = database.getConnection().prepareStatement("SELECT * FROM "+TABLE_NAME+" WHERE UUID = ?");
|
||||||
|
@ -75,6 +99,10 @@ public class UserModel extends User implements Model {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the user exists in the database
|
||||||
|
* @return true if present else false
|
||||||
|
*/
|
||||||
private boolean exist() {
|
private boolean exist() {
|
||||||
try {
|
try {
|
||||||
PreparedStatement ps = database.getConnection().prepareStatement("SELECT UUID FROM "+TABLE_NAME+" WHERE UUID = ?");
|
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() {
|
private boolean create() {
|
||||||
try {
|
try {
|
||||||
PreparedStatement ps = database.getConnection().prepareStatement("INSERT INTO "+TABLE_NAME+" (UUID, username, password) VALUES (?, ?, ?)");
|
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() {
|
public boolean save() {
|
||||||
if (!exist())
|
if (!exist())
|
||||||
return create();
|
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() {
|
public static void generateTable() {
|
||||||
try {
|
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) )");
|
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() {
|
public String getPasswordHash() {
|
||||||
return passwordHash;
|
return passwordHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the password as a hash
|
||||||
|
* @param password the plain password
|
||||||
|
*/
|
||||||
public void setPassword(String password) {
|
public void setPassword(String password) {
|
||||||
|
// Generate a new salt
|
||||||
byte[] passwordSalt = new byte[SIZE / 8];
|
byte[] passwordSalt = new byte[SIZE / 8];
|
||||||
random.nextBytes(passwordSalt);
|
random.nextBytes(passwordSalt);
|
||||||
|
|
||||||
|
// Generate the hash from the password and the salt
|
||||||
byte[] dk = pbkdf2(password.toCharArray(), passwordSalt, 1 << COST);
|
byte[] dk = pbkdf2(password.toCharArray(), passwordSalt, 1 << COST);
|
||||||
byte[] hash = new byte[passwordSalt.length + dk.length];
|
byte[] hash = new byte[passwordSalt.length + dk.length];
|
||||||
System.arraycopy(passwordSalt, 0, hash, 0, passwordSalt.length);
|
System.arraycopy(passwordSalt, 0, hash, 0, passwordSalt.length);
|
||||||
System.arraycopy(dk, 0, hash, passwordSalt.length, dk.length);
|
System.arraycopy(dk, 0, hash, passwordSalt.length, dk.length);
|
||||||
Base64.Encoder enc = Base64.getUrlEncoder().withoutPadding();
|
Base64.Encoder enc = Base64.getUrlEncoder().withoutPadding();
|
||||||
|
|
||||||
|
// Format the password hash
|
||||||
passwordHash = ID + COST + '$' + enc.encodeToString(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) {
|
public boolean checkPassword(String password) {
|
||||||
|
// Check the password hash integrity
|
||||||
Matcher m = LAYOUT.matcher(passwordHash);
|
Matcher m = LAYOUT.matcher(passwordHash);
|
||||||
if (!m.matches())
|
if (!m.matches())
|
||||||
throw new IllegalArgumentException("Invalid token format");
|
throw new IllegalArgumentException("Invalid token format");
|
||||||
|
|
||||||
|
// Gather hash data
|
||||||
int iterations = iterations(Integer.parseInt(m.group(1)));
|
int iterations = iterations(Integer.parseInt(m.group(1)));
|
||||||
byte[] hash = Base64.getUrlDecoder().decode(m.group(2));
|
byte[] hash = Base64.getUrlDecoder().decode(m.group(2));
|
||||||
byte[] salt = Arrays.copyOfRange(hash, 0, SIZE / 8);
|
byte[] salt = Arrays.copyOfRange(hash, 0, SIZE / 8);
|
||||||
byte[] check = pbkdf2(password.toCharArray(), salt, iterations);
|
byte[] check = pbkdf2(password.toCharArray(), salt, iterations);
|
||||||
|
|
||||||
|
// Check if the password match the hash
|
||||||
int zero = 0;
|
int zero = 0;
|
||||||
for (int idx = 0; idx < check.length; ++idx)
|
for (int idx = 0; idx < check.length; ++idx)
|
||||||
zero |= hash[salt.length + idx] ^ check[idx];
|
zero |= hash[salt.length + idx] ^ check[idx];
|
||||||
|
|
||||||
return zero == 0;
|
return zero == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the hash iteration
|
||||||
|
* @param cost the has cost
|
||||||
|
* @return the iterations
|
||||||
|
*/
|
||||||
private static int iterations(int cost) {
|
private static int iterations(int cost) {
|
||||||
if ((cost < 0) || (cost > 30))
|
if ((cost < 0) || (cost > 30))
|
||||||
throw new IllegalArgumentException("cost: " + cost);
|
throw new IllegalArgumentException("cost: " + cost);
|
||||||
return 1 << 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) {
|
private static byte[] pbkdf2(char[] password, byte[] salt, int iterations) {
|
||||||
KeySpec spec = new PBEKeySpec(password, salt, iterations, SIZE);
|
KeySpec spec = new PBEKeySpec(password, salt, iterations, SIZE);
|
||||||
try {
|
try {
|
||||||
|
|
Reference in a new issue