blob: d33dd76c8368fa4103b82dabc879a73b69bb9e59 [file] [log] [blame]
Issam E. Maghni0ef4a362022-10-05 23:20:16 +00001/*
2 * Copyright (C) 2022 Savoir-faire Linux Inc.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Affero General Public License as
6 * published by the Free Software Foundation; either version 3 of the
7 * License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Affero General Public License for more details.
13 *
14 * You should have received a copy of the GNU Affero General Public
15 * License along with this program. If not, see
16 * <https://www.gnu.org/licenses/>.
17 */
18import { IncomingMessage } from 'node:http';
19import { Duplex } from 'node:stream';
20
Issam E. Maghni0432cb72022-11-12 06:09:26 +000021import { WebSocketCallbacks, WebSocketMessage, WebSocketMessageTable, WebSocketMessageType } from 'jami-web-common';
Issam E. Maghnif796a092022-10-09 20:25:26 +000022import { jwtVerify } from 'jose';
23import log from 'loglevel';
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000024import { Service } from 'typedi';
Issam E. Maghnif796a092022-10-09 20:25:26 +000025import { URL } from 'whatwg-url';
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000026import { WebSocket, WebSocketServer } from 'ws';
27
Issam E. Maghnif796a092022-10-09 20:25:26 +000028import { Vault } from './vault.js';
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000029
Issam E. Maghnif796a092022-10-09 20:25:26 +000030@Service()
31export class Ws {
Charlie6ddaefe2022-11-01 18:36:29 -040032 private sockets: Map<string, WebSocket[]>;
Issam E. Maghni0432cb72022-11-12 06:09:26 +000033 private callbacks: WebSocketCallbacks;
Charlie6ddaefe2022-11-01 18:36:29 -040034
35 constructor(private readonly vault: Vault) {
36 this.sockets = new Map();
Issam E. Maghni0432cb72022-11-12 06:09:26 +000037 this.callbacks = {
38 [WebSocketMessageType.ConversationMessage]: [],
39 [WebSocketMessageType.ConversationView]: [],
40 [WebSocketMessageType.WebRTCOffer]: [],
41 [WebSocketMessageType.WebRTCAnswer]: [],
42 [WebSocketMessageType.IceCandidate]: [],
43 };
Charlie6ddaefe2022-11-01 18:36:29 -040044 }
Issam E. Maghnif796a092022-10-09 20:25:26 +000045
46 async build() {
47 const wss = new WebSocketServer({ noServer: true });
Charlie6ddaefe2022-11-01 18:36:29 -040048
Issam E. Maghnif796a092022-10-09 20:25:26 +000049 wss.on('connection', (ws: WebSocket, _req: IncomingMessage, accountId: string) => {
50 log.info('New connection', accountId);
Charlie6ddaefe2022-11-01 18:36:29 -040051 const accountSockets = this.sockets.get(accountId);
52 if (accountSockets) {
53 accountSockets.push(ws);
54 } else {
55 this.sockets.set(accountId, [ws]);
56 }
Issam E. Maghnif796a092022-10-09 20:25:26 +000057
Issam E. Maghni0432cb72022-11-12 06:09:26 +000058 ws.on('message', <T extends WebSocketMessageType>(messageString: string) => {
59 const message: WebSocketMessage<T> = JSON.parse(messageString);
Charlie2bc0d672022-11-04 11:53:44 -040060 if (!message.type || !message.data) {
61 ws.send('Incorrect format (require type and data)');
62 return;
63 }
Issam E. Maghni0432cb72022-11-12 06:09:26 +000064 if (!Object.values(WebSocketMessageType).includes(message.type)) {
65 log.warn(`Unhandled account message type: ${message.type}`);
66 return;
67 }
68 const callbacks = this.callbacks[message.type];
69 for (const callback of callbacks) {
70 callback(message.data);
Charlie6ddaefe2022-11-01 18:36:29 -040071 }
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000072 });
Charlie7f9916b2022-11-08 18:26:22 -050073
74 ws.on('close', () => {
75 log.info('Connection close', accountId);
76 const accountSockets = this.sockets.get(accountId);
77 const index = accountSockets?.indexOf(ws);
78 if (index !== undefined) {
79 accountSockets?.splice(index, 1);
80 if (accountSockets?.length === 0) {
81 this.sockets.delete(accountId);
82 }
83 }
84 });
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000085 });
86
87 return (request: IncomingMessage, socket: Duplex, head: Buffer) => {
Issam E. Maghnif796a092022-10-09 20:25:26 +000088 // Do not use parseURL because it returns a URLRecord and not a URL.
89 const url = new URL(request.url ?? '/', 'http://localhost/');
90 const accessToken = url.searchParams.get('accessToken');
91 if (!accessToken) {
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040092 socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
Issam E. Maghnif796a092022-10-09 20:25:26 +000093 socket.destroy();
94 return;
95 }
96
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040097 jwtVerify(accessToken, this.vault.publicKey, {
Issam E. Maghnif796a092022-10-09 20:25:26 +000098 issuer: 'urn:example:issuer',
99 audience: 'urn:example:audience',
100 })
101 .then(({ payload }) => {
102 const id = payload.id as string;
103 log.info('Authentication successful', id);
104 wss.handleUpgrade(request, socket, head, (ws) => wss.emit('connection', ws, request, id));
105 })
106 .catch((reason) => {
107 log.debug('Authentication failed', reason);
108 socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
109 socket.destroy();
110 });
Issam E. Maghni0ef4a362022-10-05 23:20:16 +0000111 };
112 }
Charlie6ddaefe2022-11-01 18:36:29 -0400113
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000114 bind<T extends WebSocketMessageType>(type: T, callback: (data: WebSocketMessageTable[T]) => void): void {
115 this.callbacks[type].push(callback);
Charlie6ddaefe2022-11-01 18:36:29 -0400116 }
117
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000118 send<T extends WebSocketMessageType>(accountId: string, type: T, data: WebSocketMessageTable[T]): boolean {
Charlie6ddaefe2022-11-01 18:36:29 -0400119 const accountSockets = this.sockets.get(accountId);
120 if (!accountSockets) {
121 return false;
122 }
123 for (const accountSocket of accountSockets) {
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000124 accountSocket.send(JSON.stringify({ type, data }));
Charlie6ddaefe2022-11-01 18:36:29 -0400125 }
126 return true;
127 }
Issam E. Maghni0ef4a362022-10-05 23:20:16 +0000128}