Implemented session tokens for persistent users.
This commit is contained in:
parent
97e886f0f6
commit
6770418c66
|
@ -10,7 +10,7 @@ This platform will be organized by many independent servers, each of which will
|
||||||
- [x] Private message between users in a server. **No support for private messaging users outside the context of a server.**
|
- [x] Private message between users in a server. **No support for private messaging users outside the context of a server.**
|
||||||
- [ ] Banning users from the server.
|
- [ ] Banning users from the server.
|
||||||
- [ ] Voice channels.
|
- [ ] Voice channels.
|
||||||
- [ ] Persistent users. Connect and disconnect from a server multiple times, while keeping your information intact.
|
- [x] Persistent users. Connect and disconnect from a server multiple times, while keeping your information intact.
|
||||||
|
|
||||||
|
|
||||||
Here's a short demonstration of its current features:
|
Here's a short demonstration of its current features:
|
||||||
|
|
|
@ -33,6 +33,25 @@
|
||||||
<artifactId>jna-platform</artifactId>
|
<artifactId>jna-platform</artifactId>
|
||||||
<version>5.9.0</version>
|
<version>5.9.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>2.12.4</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-core</artifactId>
|
||||||
|
<version>2.12.4</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-annotations</artifactId>
|
||||||
|
<version>2.12.4</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
|
@ -4,4 +4,7 @@ module concord_client {
|
||||||
requires com.sun.jna;
|
requires com.sun.jna;
|
||||||
requires com.sun.jna.platform;
|
requires com.sun.jna.platform;
|
||||||
requires static lombok;
|
requires static lombok;
|
||||||
|
requires com.fasterxml.jackson.databind;
|
||||||
|
requires com.fasterxml.jackson.core;
|
||||||
|
requires com.fasterxml.jackson.annotation;
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package nl.andrewl.concord_client;
|
package nl.andrewl.concord_client;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.googlecode.lanterna.gui2.MultiWindowTextGUI;
|
import com.googlecode.lanterna.gui2.MultiWindowTextGUI;
|
||||||
import com.googlecode.lanterna.gui2.Window;
|
import com.googlecode.lanterna.gui2.Window;
|
||||||
import com.googlecode.lanterna.gui2.WindowBasedTextGUI;
|
import com.googlecode.lanterna.gui2.WindowBasedTextGUI;
|
||||||
|
@ -23,7 +24,11 @@ import java.io.DataInputStream;
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class ConcordClient implements Runnable {
|
public class ConcordClient implements Runnable {
|
||||||
private final Socket socket;
|
private final Socket socket;
|
||||||
|
@ -38,13 +43,13 @@ public class ConcordClient implements Runnable {
|
||||||
|
|
||||||
private volatile boolean running;
|
private volatile boolean running;
|
||||||
|
|
||||||
public ConcordClient(String host, int port, String nickname) throws IOException {
|
public ConcordClient(String host, int port, String nickname, Path tokensFile) throws IOException {
|
||||||
this.eventManager = new EventManager(this);
|
this.eventManager = new EventManager(this);
|
||||||
this.socket = new Socket(host, port);
|
this.socket = new Socket(host, port);
|
||||||
this.in = new DataInputStream(this.socket.getInputStream());
|
this.in = new DataInputStream(this.socket.getInputStream());
|
||||||
this.out = new DataOutputStream(this.socket.getOutputStream());
|
this.out = new DataOutputStream(this.socket.getOutputStream());
|
||||||
this.serializer = new Serializer();
|
this.serializer = new Serializer();
|
||||||
this.model = this.initializeConnectionToServer(nickname);
|
this.model = this.initializeConnectionToServer(nickname, tokensFile);
|
||||||
|
|
||||||
// Add event listeners.
|
// Add event listeners.
|
||||||
this.eventManager.addHandler(MoveToChannel.class, new ChannelMovedHandler());
|
this.eventManager.addHandler(MoveToChannel.class, new ChannelMovedHandler());
|
||||||
|
@ -61,16 +66,19 @@ public class ConcordClient implements Runnable {
|
||||||
* placed in by the server.
|
* placed in by the server.
|
||||||
* @param nickname The nickname to send to the server that it should know
|
* @param nickname The nickname to send to the server that it should know
|
||||||
* us by.
|
* us by.
|
||||||
|
* @param tokensFile Path to the file where session tokens are stored.
|
||||||
* @return The client model that contains the server's metadata and other
|
* @return The client model that contains the server's metadata and other
|
||||||
* information that should be kept up-to-date at runtime.
|
* information that should be kept up-to-date at runtime.
|
||||||
* @throws IOException If an error occurs while reading or writing the
|
* @throws IOException If an error occurs while reading or writing the
|
||||||
* messages, or if the server sends an unexpected response.
|
* messages, or if the server sends an unexpected response.
|
||||||
*/
|
*/
|
||||||
private ClientModel initializeConnectionToServer(String nickname) throws IOException {
|
private ClientModel initializeConnectionToServer(String nickname, Path tokensFile) throws IOException {
|
||||||
this.serializer.writeMessage(new Identification(nickname), this.out);
|
String token = this.getSessionToken(tokensFile);
|
||||||
|
this.serializer.writeMessage(new Identification(nickname, token), this.out);
|
||||||
Message reply = this.serializer.readMessage(this.in);
|
Message reply = this.serializer.readMessage(this.in);
|
||||||
if (reply instanceof ServerWelcome welcome) {
|
if (reply instanceof ServerWelcome welcome) {
|
||||||
var model = new ClientModel(welcome.getClientId(), nickname, welcome.getCurrentChannelId(), welcome.getCurrentChannelName(), welcome.getMetaData());
|
var model = new ClientModel(welcome.getClientId(), nickname, welcome.getCurrentChannelId(), welcome.getCurrentChannelName(), welcome.getMetaData());
|
||||||
|
this.saveSessionToken(welcome.getSessionToken(), tokensFile);
|
||||||
// Start fetching initial data for the channel we were initially put into.
|
// Start fetching initial data for the channel we were initially put into.
|
||||||
this.sendMessage(new ChatHistoryRequest(model.getCurrentChannelId(), ""));
|
this.sendMessage(new ChatHistoryRequest(model.getCurrentChannelId(), ""));
|
||||||
return model;
|
return model;
|
||||||
|
@ -117,6 +125,44 @@ public class ConcordClient implements Runnable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the session token that this client should use for its currently
|
||||||
|
* configured server, according to the socket address and port.
|
||||||
|
* @param tokensFile The file containing the session tokens.
|
||||||
|
* @return The session token, or null if none was found.
|
||||||
|
* @throws IOException If the tokens file could not be read.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private String getSessionToken(Path tokensFile) throws IOException {
|
||||||
|
String token = null;
|
||||||
|
String address = this.socket.getInetAddress().getHostName() + ":" + this.socket.getPort();
|
||||||
|
if (Files.exists(tokensFile)) {
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
Map<String, String> sessionTokens = mapper.readValue(Files.newBufferedReader(tokensFile), Map.class);
|
||||||
|
token = sessionTokens.get(address);
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves a session token that this client should use the next time it
|
||||||
|
* connects to the same server.
|
||||||
|
* @param token The token to save.
|
||||||
|
* @param tokensFile The file containing the session tokens.
|
||||||
|
* @throws IOException If the tokens file could not be read or written to.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private void saveSessionToken(String token, Path tokensFile) throws IOException {
|
||||||
|
String address = this.socket.getInetAddress().getHostName() + ":" + this.socket.getPort();
|
||||||
|
Map<String, String> tokens = new HashMap<>();
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
if (Files.exists(tokensFile)) {
|
||||||
|
tokens = mapper.readValue(Files.newBufferedReader(tokensFile), Map.class);
|
||||||
|
}
|
||||||
|
tokens.put(address, token);
|
||||||
|
mapper.writerWithDefaultPrettyPrinter().writeValue(Files.newBufferedWriter(tokensFile), tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static void main(String[] args) throws IOException {
|
public static void main(String[] args) throws IOException {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import com.googlecode.lanterna.input.KeyStroke;
|
||||||
import nl.andrewl.concord_client.ConcordClient;
|
import nl.andrewl.concord_client.ConcordClient;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
public class MainWindow extends BasicWindow {
|
public class MainWindow extends BasicWindow {
|
||||||
|
@ -49,7 +50,7 @@ public class MainWindow extends BasicWindow {
|
||||||
if (nickname == null) return;
|
if (nickname == null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var client = new ConcordClient(host, port, nickname);
|
var client = new ConcordClient(host, port, nickname, Path.of("concord-session-tokens.json"));
|
||||||
var chatPanel = new ServerPanel(client, this);
|
var chatPanel = new ServerPanel(client, this);
|
||||||
client.getModel().addListener(chatPanel);
|
client.getModel().addListener(chatPanel);
|
||||||
new Thread(client).start();
|
new Thread(client).start();
|
||||||
|
|
|
@ -1,39 +1,56 @@
|
||||||
package nl.andrewl.concord_core.msg.types;
|
package nl.andrewl.concord_core.msg.types;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import nl.andrewl.concord_core.msg.Message;
|
import nl.andrewl.concord_core.msg.Message;
|
||||||
import nl.andrewl.concord_core.msg.MessageUtils;
|
|
||||||
|
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static nl.andrewl.concord_core.msg.MessageUtils.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This message is sent from the client to a server, to provide identification
|
* This message is sent from the client to a server, to provide identification
|
||||||
* information about the client to the server when the connection is started.
|
* information about the client to the server when the connection is started.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class Identification implements Message {
|
public class Identification implements Message {
|
||||||
|
/**
|
||||||
|
* The nickname that a client wants to be identified by when in the server.
|
||||||
|
* If a valid session token is provided, this can be left as null, and the
|
||||||
|
* user will be given the same nickname they had in their previous session.
|
||||||
|
*/
|
||||||
private String nickname;
|
private String nickname;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A session token that's used to uniquely identify this client as the same
|
||||||
|
* as one who has previously connected to the server. If this is null, the
|
||||||
|
* client is indicating that they have not connected to this server before.
|
||||||
|
*/
|
||||||
|
private String sessionToken;
|
||||||
|
|
||||||
public Identification(String nickname) {
|
public Identification(String nickname) {
|
||||||
this.nickname = nickname;
|
this.nickname = nickname;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getByteCount() {
|
public int getByteCount() {
|
||||||
return MessageUtils.getByteSize(this.nickname);
|
return getByteSize(this.nickname) + getByteSize(sessionToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(DataOutputStream o) throws IOException {
|
public void write(DataOutputStream o) throws IOException {
|
||||||
MessageUtils.writeString(this.nickname, o);
|
writeString(this.nickname, o);
|
||||||
|
writeString(this.sessionToken, o);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void read(DataInputStream i) throws IOException {
|
public void read(DataInputStream i) throws IOException {
|
||||||
this.nickname = MessageUtils.readString(i);
|
this.nickname = readString(i);
|
||||||
|
this.sessionToken = readString(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,19 +20,41 @@ import static nl.andrewl.concord_core.msg.MessageUtils.*;
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class ServerWelcome implements Message {
|
public class ServerWelcome implements Message {
|
||||||
|
/**
|
||||||
|
* The unique id of this client.
|
||||||
|
*/
|
||||||
private UUID clientId;
|
private UUID clientId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The token which this client can use to reconnect to the server later and
|
||||||
|
* still be recognized as the same user.
|
||||||
|
*/
|
||||||
|
private String sessionToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the channel that the user has been placed in.
|
||||||
|
*/
|
||||||
private UUID currentChannelId;
|
private UUID currentChannelId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the channel that the user has been placed in.
|
||||||
|
*/
|
||||||
private String currentChannelName;
|
private String currentChannelName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about the server's structure.
|
||||||
|
*/
|
||||||
private ServerMetaData metaData;
|
private ServerMetaData metaData;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getByteCount() {
|
public int getByteCount() {
|
||||||
return 2 * UUID_BYTES + getByteSize(this.currentChannelName) + this.metaData.getByteCount();
|
return 2 * UUID_BYTES + getByteSize(this.sessionToken) + getByteSize(this.currentChannelName) + this.metaData.getByteCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(DataOutputStream o) throws IOException {
|
public void write(DataOutputStream o) throws IOException {
|
||||||
writeUUID(this.clientId, o);
|
writeUUID(this.clientId, o);
|
||||||
|
writeString(this.sessionToken, o);
|
||||||
writeUUID(this.currentChannelId, o);
|
writeUUID(this.currentChannelId, o);
|
||||||
writeString(this.currentChannelName, o);
|
writeString(this.currentChannelName, o);
|
||||||
this.metaData.write(o);
|
this.metaData.write(o);
|
||||||
|
@ -41,6 +63,7 @@ public class ServerWelcome implements Message {
|
||||||
@Override
|
@Override
|
||||||
public void read(DataInputStream i) throws IOException {
|
public void read(DataInputStream i) throws IOException {
|
||||||
this.clientId = readUUID(i);
|
this.clientId = readUUID(i);
|
||||||
|
this.sessionToken = readString(i);
|
||||||
this.currentChannelId = readUUID(i);
|
this.currentChannelId = readUUID(i);
|
||||||
this.metaData = new ServerMetaData();
|
this.metaData = new ServerMetaData();
|
||||||
this.currentChannelName = readString(i);
|
this.currentChannelName = readString(i);
|
||||||
|
|
|
@ -49,31 +49,19 @@ public class Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a client to this channel. Also sends an update to all clients,
|
* Adds a client to this channel.
|
||||||
* including the new one, telling them that a user has joined.
|
|
||||||
* @param clientThread The client to add.
|
* @param clientThread The client to add.
|
||||||
*/
|
*/
|
||||||
public void addClient(ClientThread clientThread) {
|
public void addClient(ClientThread clientThread) {
|
||||||
this.connectedClients.add(clientThread);
|
this.connectedClients.add(clientThread);
|
||||||
// try {
|
|
||||||
// this.sendMessage(new ChannelUsersResponse(this.getUserData()));
|
|
||||||
// } catch (IOException e) {
|
|
||||||
// e.printStackTrace();
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a client from this channel. Also sends an update to all the
|
* Removes a client from this channel.
|
||||||
* clients that are still connected, telling them that a user has left.
|
|
||||||
* @param clientThread The client to remove.
|
* @param clientThread The client to remove.
|
||||||
*/
|
*/
|
||||||
public void removeClient(ClientThread clientThread) {
|
public void removeClient(ClientThread clientThread) {
|
||||||
this.connectedClients.remove(clientThread);
|
this.connectedClients.remove(clientThread);
|
||||||
// try {
|
|
||||||
// this.sendMessage(new ChannelUsersResponse(this.getUserData()));
|
|
||||||
// } catch (IOException e) {
|
|
||||||
// e.printStackTrace();
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
package nl.andrewl.concord_server.client;
|
package nl.andrewl.concord_server.client;
|
||||||
|
|
||||||
import nl.andrewl.concord_core.msg.Message;
|
import nl.andrewl.concord_core.msg.Message;
|
||||||
import nl.andrewl.concord_core.msg.types.Identification;
|
import nl.andrewl.concord_core.msg.types.Error;
|
||||||
import nl.andrewl.concord_core.msg.types.ServerUsers;
|
import nl.andrewl.concord_core.msg.types.*;
|
||||||
import nl.andrewl.concord_core.msg.types.ServerWelcome;
|
|
||||||
import nl.andrewl.concord_core.msg.types.UserData;
|
|
||||||
import nl.andrewl.concord_server.ConcordServer;
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
|
import nl.andrewl.concord_server.util.CollectionUtils;
|
||||||
|
import nl.andrewl.concord_server.util.StringUtils;
|
||||||
|
import org.dizitart.no2.Document;
|
||||||
|
import org.dizitart.no2.IndexType;
|
||||||
|
import org.dizitart.no2.NitriteCollection;
|
||||||
|
import org.dizitart.no2.filters.Filters;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -20,10 +24,17 @@ import java.util.stream.Collectors;
|
||||||
public class ClientManager {
|
public class ClientManager {
|
||||||
private final ConcordServer server;
|
private final ConcordServer server;
|
||||||
private final Map<UUID, ClientThread> clients;
|
private final Map<UUID, ClientThread> clients;
|
||||||
|
private final NitriteCollection userCollection;
|
||||||
|
|
||||||
public ClientManager(ConcordServer server) {
|
public ClientManager(ConcordServer server) {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.clients = new ConcurrentHashMap<>();
|
this.clients = new ConcurrentHashMap<>();
|
||||||
|
this.userCollection = server.getDb().getCollection("users");
|
||||||
|
CollectionUtils.ensureIndexes(this.userCollection, Map.of(
|
||||||
|
"id", IndexType.Unique,
|
||||||
|
"sessionToken", IndexType.Unique,
|
||||||
|
"nickname", IndexType.Fulltext
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,18 +47,29 @@ public class ClientManager {
|
||||||
* @param clientThread The client manager thread.
|
* @param clientThread The client manager thread.
|
||||||
*/
|
*/
|
||||||
public void registerClient(Identification identification, ClientThread clientThread) {
|
public void registerClient(Identification identification, ClientThread clientThread) {
|
||||||
var id = this.server.getIdProvider().newId();
|
ClientConnectionData data;
|
||||||
System.out.printf("Client \"%s\" joined with id %s.\n", identification.getNickname(), id);
|
try {
|
||||||
this.clients.put(id, clientThread);
|
data = identification.getSessionToken() == null ? getNewClientData(identification) : getClientDataFromDb(identification);
|
||||||
clientThread.setClientId(id);
|
} catch (InvalidIdentificationException e) {
|
||||||
clientThread.setClientNickname(identification.getNickname());
|
clientThread.sendToClient(Error.warning(e.getMessage()));
|
||||||
// Immediately add the client to the default channel and send the initial welcome message.
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clients.put(data.id, clientThread);
|
||||||
|
clientThread.setClientId(data.id);
|
||||||
|
clientThread.setClientNickname(data.nickname);
|
||||||
var defaultChannel = this.server.getChannelManager().getDefaultChannel().orElseThrow();
|
var defaultChannel = this.server.getChannelManager().getDefaultChannel().orElseThrow();
|
||||||
clientThread.sendToClient(new ServerWelcome(id, defaultChannel.getId(), defaultChannel.getName(), this.server.getMetaData()));
|
clientThread.sendToClient(new ServerWelcome(data.id, data.sessionToken, defaultChannel.getId(), defaultChannel.getName(), this.server.getMetaData()));
|
||||||
// It is important that we send the welcome message first. The client expects this as the initial response to their identification message.
|
// It is important that we send the welcome message first. The client expects this as the initial response to their identification message.
|
||||||
defaultChannel.addClient(clientThread);
|
defaultChannel.addClient(clientThread);
|
||||||
clientThread.setCurrentChannel(defaultChannel);
|
clientThread.setCurrentChannel(defaultChannel);
|
||||||
System.out.println("Moved client " + clientThread + " to " + defaultChannel);
|
System.out.printf(
|
||||||
|
"Client %s(%s) joined%s, and was put into %s.\n",
|
||||||
|
data.nickname,
|
||||||
|
data.id,
|
||||||
|
data.newClient ? " for the first time" : "",
|
||||||
|
defaultChannel
|
||||||
|
);
|
||||||
this.broadcast(new ServerUsers(this.getClients()));
|
this.broadcast(new ServerUsers(this.getClients()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,4 +120,43 @@ public class ClientManager {
|
||||||
public Optional<ClientThread> getClientById(UUID id) {
|
public Optional<ClientThread> getClientById(UUID id) {
|
||||||
return Optional.ofNullable(this.clients.get(id));
|
return Optional.ofNullable(this.clients.get(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static record ClientConnectionData(UUID id, String nickname, String sessionToken, boolean newClient) {}
|
||||||
|
|
||||||
|
private ClientConnectionData getClientDataFromDb(Identification identification) throws InvalidIdentificationException {
|
||||||
|
var cursor = this.userCollection.find(Filters.eq("sessionToken", identification.getSessionToken()));
|
||||||
|
Document doc = cursor.firstOrDefault();
|
||||||
|
if (doc != null) {
|
||||||
|
UUID id = doc.get("id", UUID.class);
|
||||||
|
String nickname = identification.getNickname();
|
||||||
|
if (nickname != null) {
|
||||||
|
doc.put("nickname", nickname);
|
||||||
|
} else {
|
||||||
|
nickname = doc.get("nickname", String.class);
|
||||||
|
}
|
||||||
|
String sessionToken = StringUtils.random(128);
|
||||||
|
doc.put("sessionToken", sessionToken);
|
||||||
|
this.userCollection.update(doc);
|
||||||
|
return new ClientConnectionData(id, nickname, sessionToken, false);
|
||||||
|
} else {
|
||||||
|
throw new InvalidIdentificationException("Invalid session token.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClientConnectionData getNewClientData(Identification identification) throws InvalidIdentificationException {
|
||||||
|
UUID id = this.server.getIdProvider().newId();
|
||||||
|
String nickname = identification.getNickname();
|
||||||
|
if (nickname == null) {
|
||||||
|
throw new InvalidIdentificationException("Missing nickname.");
|
||||||
|
}
|
||||||
|
String sessionToken = StringUtils.random(128);
|
||||||
|
Document doc = new Document(Map.of(
|
||||||
|
"id", id,
|
||||||
|
"nickname", nickname,
|
||||||
|
"sessionToken", sessionToken,
|
||||||
|
"createdAt", System.currentTimeMillis()
|
||||||
|
));
|
||||||
|
this.userCollection.insert(doc);
|
||||||
|
return new ClientConnectionData(id, nickname, sessionToken, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package nl.andrewl.concord_server.client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception that's thrown when a client's identification information is invalid.
|
||||||
|
*/
|
||||||
|
public class InvalidIdentificationException extends Exception {
|
||||||
|
public InvalidIdentificationException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
package nl.andrewl.concord_server.util;
|
||||||
|
|
||||||
|
public record Pair<A, B>(A first, B second) {}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package nl.andrewl.concord_server.util;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class StringUtils {
|
||||||
|
|
||||||
|
public static String random(int length) {
|
||||||
|
Random random = new SecureRandom();
|
||||||
|
final String alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-=+[]{}()<>";
|
||||||
|
StringBuilder sb = new StringBuilder(length);
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
sb.append(alphabet.charAt(random.nextInt(alphabet.length())));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue