/** * 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, registeredPosts: Set, } // 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));