UNPKG

open-collaboration-protocol

Version:

Open Collaboration Protocol implementation, part of the Open Collaboration Tools project

334 lines 13.9 kB
// ****************************************************************************** // Copyright 2024 TypeFox GmbH // This program and the accompanying materials are made available under the // terms of the MIT License, which is available in the project root. // ****************************************************************************** import * as msg from './messages.js'; import { Emitter } from '../utils/event.js'; import { Deferred } from '../utils/promise.js'; import { Encryption } from './encryption.js'; import { Encoding } from './encoding.js'; export class AbstractBroadcastConnection { options; messageHandlers = new Map(); onErrorEmitter = new Emitter(); onDisconnectEmitter = new Emitter(); onConnectionErrorEmitter = new Emitter(); onReconnectEmitter = new Emitter(); onUnhandledRequestHandler; onUnhandledBroadcastHandler; onUnhandledNotificationHandler; get onError() { return this.onErrorEmitter.event; } get onDisconnect() { return this.onDisconnectEmitter.event; } get onReconnect() { return this.onReconnectEmitter.event; } get onConnectionError() { return this.onConnectionErrorEmitter.event; } requestMap = new Map(); requestId = 1; symKey = Encryption.generateSymKey(); encryptionKeyCache = {}; decryptionKeyCache = {}; maxCacheSize = 50; _ready = new Deferred(); constructor(options) { this.options = options; options.transport.read(data => this.handleMessage(new Uint8Array(data))); options.transport.onDisconnect(() => this.dispose()); options.transport.onError(message => { this.onConnectionErrorEmitter.fire(message); this.dispose(); }); options.transport.onReconnect(() => this.onReconnectEmitter.fire()); } dispose() { this.onDisconnectEmitter.fire(); this.onDisconnectEmitter.dispose(); this.onErrorEmitter.dispose(); this.messageHandlers.clear(); this.options.transport.dispose(); } ready() { this._ready.resolve(); } /** * Cleanup the encryption and decryption key caches if they exceed the maximum cache size. * This is to prevent memory leaks in case the cache grows due to symmetric key swapping * or many peers joining/leaving a room. */ cleanupCaches() { // Determine the maximum size of the cache based on the number of available peers/keys const maxSize = this.getPublicKeysLength() + this.maxCacheSize; if (Object.keys(this.encryptionKeyCache).length > maxSize) { this.encryptionKeyCache = {}; } if (Object.keys(this.decryptionKeyCache).length > maxSize) { this.decryptionKeyCache = {}; } } async handleMessage(data) { this.cleanupCaches(); let message; try { message = Encoding.decode(data); } catch (err) { console.error('Decoding message error', err); return; } if (msg.ResponseMessage.isAny(message)) { const request = this.requestMap.get(message.id); try { let response; if (msg.ResponseMessage.isEncrypted(message)) { response = await Encryption.decrypt(message, { privateKey: this.options.privateKey, cache: this.decryptionKeyCache }); } else if (msg.ResponseMessage.is(message)) { response = message; } else { console.error('Received invalid response message'); return; } if (request) { request.response.resolve(response.content.response); } } catch (err) { console.error('Failed to handle response message', err); request?.response.reject(err); } } else if (msg.ResponseErrorMessage.isAny(message)) { const request = this.requestMap.get(message.id); try { let response; if (msg.ResponseErrorMessage.isEncrypted(message)) { response = await Encryption.decrypt(message, { privateKey: this.options.privateKey, cache: this.decryptionKeyCache }); } else if (msg.ResponseErrorMessage.is(message)) { response = message; } else { console.error('Received invalid response error message'); return; } if (request) { request.response.reject(new Error(response.content.message)); } } catch (err) { console.error('Failed to handle response error message', err); request?.response.reject(err); } } else if (msg.RequestMessage.isAny(message)) { try { let decrypted; if (msg.RequestMessage.isEncrypted(message)) { decrypted = await Encryption.decrypt(message, { privateKey: this.options.privateKey, cache: this.decryptionKeyCache }); } else if (msg.RequestMessage.is(message)) { decrypted = message; } else { console.error('Received invalid request message'); return; } const handler = this.messageHandlers.get(decrypted.content.method) ?? this.onUnhandledRequestHandler?.(decrypted.content.method); if (!handler) { console.error(`No handler registered for ${decrypted.kind} method ${decrypted.content.method}.`); return; } await this._ready.promise; let response; try { const result = await handler(decrypted.origin, ...(decrypted.content.params ?? [])); response = msg.ResponseMessage.create(decrypted.id, result); } catch (error) { response = msg.ResponseErrorMessage.create(decrypted.id, String(error)); } if (decrypted.origin === '') { // Write server responses as they are, without encryption await this.write(response); } else { // Encrypt responses to other peers const publicKey = this.getPublicKey(decrypted.origin); const encryptedResponseMessage = await Encryption.encrypt(response, { symmetricKey: this.symKey, cache: this.encryptionKeyCache }, publicKey); await this.write(encryptedResponseMessage); } } catch (err) { console.error('Failed to handle request message', err); } } else if (msg.BroadcastMessage.isAny(message) || msg.NotificationMessage.isAny(message)) { try { let decrypted; if (msg.NotificationMessage.isEncrypted(message) || msg.BroadcastMessage.isEncrypted(message)) { decrypted = await Encryption.decrypt(message, { privateKey: this.options.privateKey, cache: this.decryptionKeyCache }); } else if (msg.NotificationMessage.is(message) || msg.BroadcastMessage.is(message)) { decrypted = message; } else { console.error(`Received invalid ${message.kind} message`); return; } const handler = this.messageHandlers.get(decrypted.content.method) ?? (msg.BroadcastMessage.is(decrypted) ? this.onUnhandledBroadcastHandler?.(decrypted.content.method) : this.onUnhandledNotificationHandler?.(decrypted.content.method)); if (!handler) { console.error(`No handler registered for ${message.kind} method ${decrypted.content.method}.`); return; } handler(message.origin, ...(decrypted.content.params ?? [])); } catch (err) { console.error(`Failed to handle ${message.kind} message`, err); } } else if (msg.ErrorMessage.isAny(message)) { try { let decrypted; if (msg.ErrorMessage.isBinary(message)) { decrypted = await Encryption.decrypt(message, { privateKey: this.options.privateKey, cache: this.decryptionKeyCache }); } else if (msg.ErrorMessage.is(message)) { decrypted = message; } else { console.error('Received invalid error message'); return; } this.onErrorEmitter.fire(decrypted.content.message); } catch (err) { console.error('Failed to handle error message', err); } } } async write(message) { await this.options.transport.write(Encoding.encode(message)); } onRequest(typeOrHandler, handler) { if (typeof typeOrHandler === 'function') { if (this.onUnhandledRequestHandler) { console.warn('Unhandled request handler already set. previous handler will be overwritten.'); } this.onUnhandledRequestHandler = (method) => (origin, ...params) => typeOrHandler(origin, method, ...params); } else { const method = typeof typeOrHandler === 'string' ? typeOrHandler : typeOrHandler.method; this.messageHandlers.set(method, handler); } } onNotification(typeOrHandler, handler) { if (typeof typeOrHandler === 'function') { if (this.onUnhandledNotificationHandler) { console.warn('Unhandled notification handler already set. previous handler will be overwritten.'); } this.onUnhandledNotificationHandler = (method) => (origin, ...params) => typeOrHandler(origin, method, ...params); } else { const method = typeof typeOrHandler === 'string' ? typeOrHandler : typeOrHandler.method; this.messageHandlers.set(method, handler); } } onBroadcast(typeOrHandler, handler) { if (typeof typeOrHandler === 'function') { if (this.onUnhandledNotificationHandler) { console.warn('Unhandled broadcast handler already set. previous handler will be overwritten.'); } this.onUnhandledBroadcastHandler = (method) => (origin, ...params) => typeOrHandler(origin, method, ...params); } else { const method = typeof typeOrHandler === 'string' ? typeOrHandler : typeOrHandler.method; this.messageHandlers.set(method, handler); } } async sendRequest(type, target, ...parameters) { await this._ready.promise; const id = this.requestId++; const deferred = new Deferred(); const dispose = () => { this.requestMap.delete(id); clearTimeout(timeout); deferred.reject(new Error('Request timed out')); }; const timeout = setTimeout(dispose, 60_000); // Timeout after one minute const relayedMessage = { id, response: deferred, dispose }; this.requestMap.set(id, relayedMessage); const message = msg.RequestMessage.create(type, id, '', target, parameters); if (target === '') { await this.write(message); } else { const encryptedMessage = await Encryption.encrypt(message, { symmetricKey: this.symKey, cache: this.encryptionKeyCache }, this.getPublicKey(target)); await this.write(encryptedMessage); } return deferred.promise; } async sendNotification(type, target, ...parameters) { await this._ready.promise; const message = msg.NotificationMessage.create(type, '', target, parameters); if (target === '') { await this.write(message); } else { const encryptedMessage = await Encryption.encrypt(message, { symmetricKey: this.symKey, cache: this.encryptionKeyCache }, this.getPublicKey(target)); await this.write(encryptedMessage); } } async sendBroadcast(type, ...parameters) { await this._ready.promise; const message = msg.BroadcastMessage.create(type, '', parameters); const publicKeys = this.getPublicKeys(); if (publicKeys.length > 0) { // Don't actually send the broadcast if there are no other peers // Encryption will fail if we don't provide at least one public key const encryptedMessage = await Encryption.encrypt(message, { symmetricKey: this.symKey, cache: this.encryptionKeyCache }, ...publicKeys); await this.write(encryptedMessage); } } } //# sourceMappingURL=abstract-connection.js.map