open-collaboration-protocol
Version:
Open Collaboration Protocol implementation, part of the Open Collaboration Tools project
334 lines • 13.9 kB
JavaScript
// ******************************************************************************
// 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