UNPKG

open-collaboration-server

Version:

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

193 lines (168 loc) 7.05 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 { inject, injectable, postConstruct } from 'inversify'; import { nanoid } from 'nanoid'; import * as protocol from 'open-collaboration-protocol'; import { Channel } from './channel.js'; import { MessageRelay } from './message-relay.js'; import { RoomManager } from './room-manager.js'; import { Peer, PeerInfo, Room, User } from './types.js'; import { Logger } from './utils/logging.js'; import { parse } from 'semver'; export const PeerFactory = Symbol('PeerFactory'); export type PeerFactory = (info: PeerInfo) => Peer; @injectable() export class PeerImpl implements Peer { @inject(Logger) protected logger: Logger; readonly id = nanoid(24); get jwt(): string { return this.peerInfo.jwt; } get publicKey(): string { return this.peerInfo.publicKey; } get client(): string { return this.peerInfo.client; } get supportedCompression(): string[] { return this.peerInfo.supportedCompression; } get user(): User { return this.peerInfo.user; } get host(): boolean { return this.peerInfo.host; } get room(): Room { const value = this.roomManager.getRoomByPeerId(this.id); if (!value) { throw this.logger.createErrorAndLog(`Peer '${this.id}' does not belong to any room`); } return value; } @inject(MessageRelay) private readonly messageRelay: MessageRelay; @inject(PeerInfo) private readonly peerInfo: PeerInfo; @inject(RoomManager) private readonly roomManager: RoomManager; private _channel?: Channel; private disposed = false; private readonly onDisposeEmitter = new protocol.Emitter<void>(); get onDispose(): protocol.Event<void> { return this.onDisposeEmitter.event; } get channel(): Channel { if (!this._channel) { throw new Error('Not initialized'); } return this._channel; } @postConstruct() protected initialize(): void { this._channel = new Channel(this.peerInfo.channel); this._channel.onMessage(message => this.receiveMessage(message)); this._channel.onClose(() => this.dispose()); } private async receiveMessage(message: protocol.Message): Promise<void> { const messageVersion = parse(message.version); if (!messageVersion) { this.logger.warn(`Received message from '${this.id}' with invalid version: ${message.version}. Ignoring message.`); return; } if (!protocol.compatibleVersions(messageVersion)) { this.logger.warn(`Received message from '${this.id}' with incompatible version: ${message.version}; expected: ${protocol.VERSION}. Ignoring message.`); return; } if (protocol.ResponseMessage.isAny(message) || protocol.ResponseErrorMessage.isAny(message)) { this.messageRelay.pushResponse(this, message); } else if (protocol.RequestMessage.isAny(message)) { if (!protocol.Message.isEncrypted(message)) { this.logger.warn(`Received unencrypted request from: '${this.id}'`); return; } // Override whatever we know about the origin of the message message.origin = this.id; try { const response = await this.messageRelay.sendRequest(this.getTargetPeer(message.target), message); // Adjust the response to the original message id response.id = message.id; this.channel.sendMessage(response); } catch (_error) { const errorResponseMessage = protocol.ResponseErrorMessage.create(message.id, 'Failed to retrieve the requested data.'); this.channel.sendMessage(errorResponseMessage); } } else if (protocol.NotificationMessage.isAny(message)) { message.origin = this.id; if (message.target === '') { this.handleServerMessage(message); return; } else if (!protocol.Message.isEncrypted(message)) { this.logger.warn(`Received unencrypted notification from: '${this.id}'`); return; } try { this.messageRelay.sendNotification(this.getTargetPeer(message.target), message); } catch (_error) { this.logger.error(`Failed sending notification to: ${message.target || '<empty>'}`); } } else if (protocol.BroadcastMessage.isAny(message)) { if (!protocol.Message.isEncrypted(message)) { this.logger.warn(`Received unencrypted broadcast from: '${this.id}'`); return; } this.messageRelay.sendBroadcast(this, message); } } private async handleServerMessage(notification: protocol.UnknownNotificationMessage): Promise<void> { if (!protocol.NotificationMessage.is(notification)) { this.logger.error('Received encrypted server message'); return; } try { if (notification.content.method === protocol.Messages.Room.Leave.method) { this.dispose(); } else { throw new Error('Unknown server message method: ' + notification.content.method); } } catch (err) { this.logger.error('Failed to handle server message', err); } } private getTargetPeer(targetId: string | undefined): Peer { const peer = targetId ? this.room.getPeer(targetId) : undefined; if (!peer) { throw this.logger.createErrorAndLog(`Could not find the target peer: ${targetId || '<empty>'}`); } return peer; } toProtocol(): protocol.Peer { return { id: this.id, host: this.host, name: this.user.name, email: this.user.email, metadata: { compression: { supported: this.peerInfo.supportedCompression }, encryption: { publicKey: this.publicKey } } }; } dispose(): void { if (this.disposed) { return; } this.disposed = true; this.onDisposeEmitter.fire(undefined); this.onDisposeEmitter.dispose(); this._channel?.close(); this.logger.info(`Peer '${this.id}' disposed`); } }