UNPKG

open-collaboration-protocol

Version:

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

387 lines (357 loc) 18.7 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 { MessageTransport } from '../transport/transport.js'; import { Emitter, Event } from '../utils/event.js'; import { Deferred } from '../utils/promise.js'; import { Encryption } from './encryption.js'; import { Encoding } from './encoding.js'; export type Handler<P extends unknown[], R = void> = (origin: string, ...parameters: P) => (R | Promise<R>); export type UnhandledMessageHandler = (origin: string, method: string, ...parameters: unknown[]) => any | Promise<any>; export type ErrorHandler = (message: string) => void; export interface BroadcastConnection { onRequest(type: string, handler: Handler<any[], any>): void; onRequest<P extends unknown[], R>(type: msg.RequestType<P, R>, handler: Handler<P, R>): void; onRequest(handler: UnhandledMessageHandler): void; onNotification(type: string, handler: Handler<any[]>): void; onNotification<P extends unknown[]>(type: msg.NotificationType<P>, handler: Handler<P>): void; onNotification(handler: UnhandledMessageHandler): void; onBroadcast(type: string, handler: Handler<any[]>): void; onBroadcast<P extends unknown[]>(type: msg.BroadcastType<P>, handler: Handler<P>): void; onBroadcast(handler: UnhandledMessageHandler): void; onError(handler: ErrorHandler): void; sendRequest(type: string, ...parameters: any[]): Promise<any>; sendRequest<P extends unknown[], R>(type: msg.RequestType<P, R>, ...parameters: P): Promise<R>; sendNotification(type: string, ...parameters: any[]): void; sendNotification<P extends unknown[]>(type: msg.NotificationType<P>, ...parameters: P): void; sendBroadcast(type: string, ...parameters: any[]): void; sendBroadcast<P extends unknown[]>(type: msg.BroadcastType<P>, ...parameters: P): void; dispose(): void; onDisconnect: Event<void>; onReconnect: Event<void>; onConnectionError: Event<string>; } export interface RelayedRequest { id: string | number; response: Deferred<any> dispose(): void; } export interface AbstractBroadcastConnectionOptions { privateKey: string; transport: MessageTransport; } export abstract class AbstractBroadcastConnection implements BroadcastConnection { protected messageHandlers = new Map<string, Handler<any[], any>>(); protected onErrorEmitter = new Emitter<string>(); protected onDisconnectEmitter = new Emitter<void>(); protected onConnectionErrorEmitter = new Emitter<string>(); protected onReconnectEmitter = new Emitter<void>(); protected onUnhandledRequestHandler?: (method: string) => Handler<any[], any>; protected onUnhandledBroadcastHandler?: (method: string) => Handler<any[], any>; protected onUnhandledNotificationHandler?: (method: string) => Handler<any[], any>; get onError(): Event<string> { return this.onErrorEmitter.event; } get onDisconnect(): Event<void> { return this.onDisconnectEmitter.event; } get onReconnect(): Event<void> { return this.onReconnectEmitter.event; } get onConnectionError(): Event<string> { return this.onConnectionErrorEmitter.event; } protected requestMap = new Map<string | number, RelayedRequest>(); protected requestId = 1; protected symKey = Encryption.generateSymKey(); protected encryptionKeyCache: Record<string, string> = {}; protected decryptionKeyCache: Record<string, string> = {}; protected maxCacheSize = 50; protected _ready = new Deferred(); constructor(readonly options: AbstractBroadcastConnectionOptions) { 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(): void { this.onDisconnectEmitter.fire(); this.onDisconnectEmitter.dispose(); this.onErrorEmitter.dispose(); this.messageHandlers.clear(); this.options.transport.dispose(); } protected ready(): void { 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. */ protected cleanupCaches(): void { // 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 = {}; } } protected async handleMessage(data: Uint8Array): Promise<void> { this.cleanupCaches(); let message: unknown; 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: msg.ResponseMessage; 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: msg.ResponseErrorMessage; 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: msg.RequestMessage; 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: msg.ResponseMessage | msg.ResponseErrorMessage; 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: msg.BroadcastMessage | msg.NotificationMessage; 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: msg.ErrorMessage; 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); } } } protected abstract getPublicKey(origin: string | undefined): Encryption.AsymmetricKey; protected abstract getPublicKeys(): Encryption.AsymmetricKey[]; protected abstract getPublicKeysLength(): number; private async write(message: msg.Message): Promise<void> { await this.options.transport.write(Encoding.encode(message)); } onRequest(handler: UnhandledMessageHandler): void; onRequest(type: string, handler: Handler<any[], any>): void; onRequest<P extends unknown[], R>(type: msg.RequestType<P, R> | string, handler: Handler<P, R>): void; onRequest(typeOrHandler: msg.RequestType<any[], any> | string | UnhandledMessageHandler, handler?: Handler<any[], any>): void { 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(handler: UnhandledMessageHandler): void; onNotification(type: string, handler: Handler<any[]>): void onNotification<P extends unknown[]>(type: msg.NotificationType<P> | string, handler: Handler<P>): void onNotification(typeOrHandler: msg.NotificationType<any[]> | string | UnhandledMessageHandler, handler?: Handler<any[]>): void { 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(handler: UnhandledMessageHandler): void; onBroadcast(type: msg.BroadcastType<any[]> | string, handler: Handler<any[]>): void; onBroadcast(type: msg.BroadcastType<any[]> | string, handler: Handler<any[]>): void; onBroadcast(typeOrHandler: msg.BroadcastType<any[]> | string | UnhandledMessageHandler, handler?: Handler<any[]>): void { 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!); } } sendRequest(type: string, target: msg.MessageTarget, ...parameters: any[]): Promise<any>; sendRequest<P extends unknown[], R>(type: msg.RequestType<P, R> | string, target: msg.MessageTarget, ...parameters: P): Promise<R>; async sendRequest(type: msg.RequestType<any, any> | string, target: msg.MessageTarget, ...parameters: any[]): Promise<any> { await this._ready.promise; const id = this.requestId++; const deferred = new Deferred<any>(); 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: RelayedRequest = { 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; } sendNotification(type: string, target: msg.MessageTarget, ...parameters: any[]): Promise<void>; sendNotification<P extends unknown[]>(type: msg.NotificationType<P>, target: msg.MessageTarget, ...parameters: P): Promise<void>; async sendNotification(type: msg.NotificationType<any> | string, target: msg.MessageTarget, ...parameters: any[]): Promise<void> { 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); } } sendBroadcast(type: string, ...parameters: any[]): Promise<void>; sendBroadcast<P extends unknown[]>(type: msg.BroadcastType<P>, ...parameters: P): Promise<void>; async sendBroadcast(type: msg.BroadcastType<any> | string, ...parameters: any[]): Promise<void> { 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); } } }