1
0
mirror of https://gitlab.com/comunic/comunicapiv2 synced 2024-11-22 21:39:22 +00:00
comunicapiv2/src/controllers/UserWebSocketController.ts

257 lines
5.9 KiB
TypeScript

/**
* User websocket controller
*
* @author Pierre Hubert
*/
import * as ws from 'ws';
import { Request } from 'express';
import { RequestHandler } from '../entities/RequestHandler';
import { time } from '../utils/DateUtils';
import { randomStr } from '../utils/CryptUtils';
import { EventsHelper } from '../helpers/EventsHelper';
import { UserWebSocketRoutes } from './UserWebSocketRoutes';
import { UserWebSocketRequestsHandler } from '../entities/WebSocketRequestHandler';
import { WsMessage } from '../entities/WsMessage';
interface PendingRequests {
time: number,
clientID: number,
userID: number,
token: string,
incognito: boolean
}
export interface ActiveClient {
socketID: string,
clientID: number,
userID: number,
ws: ws,
incognito: boolean,
registeredConversations: Set<number>,
registeredPosts: Set<number>,
}
// Tokens are valid only 10 seconds after they are generated
const TOKENS_DURATION = 10
const TOKEN_LENGTH = 20
export class UserWebSocketController {
/**
* The list of pending connections
*/
static pending_list: PendingRequests[] = []
/**
* The list of active clients
*/
static active_clients: ActiveClient[] = []
/**
* Clean the list of tokens
*/
private static CleanList() {
// Clean the list
this.pending_list = this.pending_list
.filter((l) => l.time + TOKENS_DURATION + 1 > time())
}
/**
* Get a websocket access token
*
* @param h Request handler
*/
public static async GetToken(h: RequestHandler) {
this.CleanList();
// Generate a temporary token
const token = randomStr(TOKEN_LENGTH);
// Add the token to the list
this.pending_list.push({
time: time(),
clientID: h.getClientInfo().id,
userID: h.getUserId(),
token: token,
incognito: h.postBool("incognito", false)
});
h.send({
token: token
});
}
/**
* Handler user websocket request
*
* @param req Associated request
* @param ws The socket
*/
public static async UserWS(req: Request, ws: ws) {
this.CleanList();
// First, check for token
if(!req.query.hasOwnProperty("token")
|| String(req.query.token).length != TOKEN_LENGTH) {
ws.send("Missing token!");
ws.close();
return;
}
// Search appropriate connection
const token = req.query.token;
const entryIndex = this.pending_list.findIndex((el) => el.token == token);
if(entryIndex == -1) {
ws.send("Invalid token!");
ws.close();
return;
}
// Remove the entry from the array
const entry = this.pending_list[entryIndex];
this.pending_list.splice(entryIndex, 1);
// Add the client to the list of active clients
const client: ActiveClient = {
socketID: randomStr(30),
clientID: entry.clientID,
userID: entry.userID,
ws: ws,
incognito: entry.incognito,
registeredConversations: new Set(),
registeredPosts: new Set(),
}
this.active_clients.push(client);
// Remove the client for the list as soon as the
// socket is closed
ws.addEventListener("close", () => {
this.active_clients.splice(this.active_clients.indexOf(client), 1);
})
// Handles error
ws.addEventListener("error", (e) => {
if(ws.readyState == ws.OPEN)
ws.close();
console.log("WebSocket error", e)
})
// Handles incoming messages
ws.addEventListener("message", async (msg) => {
// Only accept text messages
if(msg.type != "message") {
console.error("Received a non-text messsage through a WebSocket !")
ws.close();
return;
}
// Check if the data are valid
let wsMsg : WsMessage;
try {
wsMsg = new WsMessage(JSON.parse(msg.data));
if(!wsMsg.isValidRequest)
throw new Error("Requested message is invalid!");
} catch(e) {
console.error(e);
ws.close();
return;
}
// Create request handler
const handler = new UserWebSocketRequestsHandler(client, wsMsg);
try {
// Check if we support this kind of message
const route = UserWebSocketRoutes.find((el) => el.title == wsMsg.title);
if(route == undefined) {
handler.error(404, "Method not found!");
return;
}
else
await route.handler(handler);
} catch(e) {
console.error(e);
// Try to send a server error response
if(!handler.isResponseSent) {
try {
handler.sendResponse("error", {
code: 500,
message: "Server error"
});
} catch(e) {
console.error(e);
}
}
}
})
}
/**
* Close a specific user websocket
*
* @param clientID Target client ID
* @param userID Target user ID
*/
public static async CloseClientSockets(clientID: number, userID: number) {
for(const entry of this.active_clients.filter((f) => f.clientID == clientID && f.userID == userID))
entry.ws.close();
}
/**
* Send a message to a socket
*
* @param userID Target user ID
* @param message The message to send
*/
public static Send(userID: number, message: WsMessage) {
for(const entry of this.active_clients.filter((e) => e.userID == userID))
{
this.SendToClient(entry, message);
}
}
/**
* Send a message to a specific client
*
* @param client Target client
* @param message The message to send
*/
public static SendToClient(client: ActiveClient, message: WsMessage) {
if(client.ws.readyState == ws.OPEN)
client.ws.send(JSON.stringify(message));
}
/**
* Check out whether a user has an active websocket or not
*
* @param userID Target user ID
*/
public static IsConnected(userID: number) : boolean {
return this.active_clients.find((e) => e.userID == userID) != undefined;
}
/**
* Check out whether a has all its connections marked as incognito
*
* @param userID Target user ID
*/
public static IsIcognito(userID: number) : boolean {
return this.IsConnected(userID)
&& this.active_clients.find(e => e.userID == userID && !e.incognito) == undefined;
}
}
// When user sign out
EventsHelper.Listen("destroyed_login_tokens", (e) => UserWebSocketController.CloseClientSockets(e.client.id, e.userID));