open-collaboration-protocol
Version:
Open Collaboration Protocol implementation, part of the Open Collaboration Tools project
387 lines (357 loc) • 18.7 kB
text/typescript
// ******************************************************************************
// 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);
}
}
}