blob: 46d563ad52cf002e91812cab016066fbd048ad24 [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
Charlie6ddaefe2022-11-01 18:36:29 -040021import { WebSocketMessage, 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[]>;
33 private callbacks: Map<WebSocketMessageType, ((message: WebSocketMessage) => void)[]>;
34
35 constructor(private readonly vault: Vault) {
36 this.sockets = new Map();
37 this.callbacks = new Map();
38 }
Issam E. Maghnif796a092022-10-09 20:25:26 +000039
40 async build() {
41 const wss = new WebSocketServer({ noServer: true });
Charlie6ddaefe2022-11-01 18:36:29 -040042
Issam E. Maghnif796a092022-10-09 20:25:26 +000043 wss.on('connection', (ws: WebSocket, _req: IncomingMessage, accountId: string) => {
44 log.info('New connection', accountId);
Charlie6ddaefe2022-11-01 18:36:29 -040045 const accountSockets = this.sockets.get(accountId);
46 if (accountSockets) {
47 accountSockets.push(ws);
48 } else {
49 this.sockets.set(accountId, [ws]);
50 }
Issam E. Maghnif796a092022-10-09 20:25:26 +000051
Charlie6ddaefe2022-11-01 18:36:29 -040052 ws.on('message', (messageString: string) => {
53 const message: WebSocketMessage = JSON.parse(messageString);
Charlie2bc0d672022-11-04 11:53:44 -040054 if (!message.type || !message.data) {
55 ws.send('Incorrect format (require type and data)');
56 return;
57 }
Charlie6ddaefe2022-11-01 18:36:29 -040058 const callbacks = this.callbacks.get(message.type);
59 if (callbacks) {
60 for (const callback of callbacks) {
61 callback(message);
62 }
63 }
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000064 });
Charlie7f9916b2022-11-08 18:26:22 -050065
66 ws.on('close', () => {
67 log.info('Connection close', accountId);
68 const accountSockets = this.sockets.get(accountId);
69 const index = accountSockets?.indexOf(ws);
70 if (index !== undefined) {
71 accountSockets?.splice(index, 1);
72 if (accountSockets?.length === 0) {
73 this.sockets.delete(accountId);
74 }
75 }
76 });
Issam E. Maghni0ef4a362022-10-05 23:20:16 +000077 });
78
79 return (request: IncomingMessage, socket: Duplex, head: Buffer) => {
Issam E. Maghnif796a092022-10-09 20:25:26 +000080 // Do not use parseURL because it returns a URLRecord and not a URL.
81 const url = new URL(request.url ?? '/', 'http://localhost/');
82 const accessToken = url.searchParams.get('accessToken');
83 if (!accessToken) {
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040084 socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
Issam E. Maghnif796a092022-10-09 20:25:26 +000085 socket.destroy();
86 return;
87 }
88
Misha Krieger-Raynauld242560f2022-10-16 19:59:58 -040089 jwtVerify(accessToken, this.vault.publicKey, {
Issam E. Maghnif796a092022-10-09 20:25:26 +000090 issuer: 'urn:example:issuer',
91 audience: 'urn:example:audience',
92 })
93 .then(({ payload }) => {
94 const id = payload.id as string;
95 log.info('Authentication successful', id);
96 wss.handleUpgrade(request, socket, head, (ws) => wss.emit('connection', ws, request, id));
97 })
98 .catch((reason) => {
99 log.debug('Authentication failed', reason);
100 socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
101 socket.destroy();
102 });
Issam E. Maghni0ef4a362022-10-05 23:20:16 +0000103 };
104 }
Charlie6ddaefe2022-11-01 18:36:29 -0400105
106 bind(messageType: WebSocketMessageType, callback: (message: WebSocketMessage) => void): void {
107 const messageTypeCallbacks = this.callbacks.get(messageType);
108 if (messageTypeCallbacks) {
109 messageTypeCallbacks.push(callback);
110 } else {
111 this.callbacks.set(messageType, [callback]);
112 }
113 }
114
115 send(accountId: string, message: WebSocketMessage): boolean {
116 const accountSockets = this.sockets.get(accountId);
117 if (!accountSockets) {
118 return false;
119 }
120 for (const accountSocket of accountSockets) {
Charlie8f99f012022-11-12 17:34:56 -0500121 accountSocket.send(JSON.stringify(message));
Charlie6ddaefe2022-11-01 18:36:29 -0400122 }
123 return true;
124 }
Issam E. Maghni0ef4a362022-10-05 23:20:16 +0000125}