open-collaboration-server
Version:
Open Collaboration Server implementation, part of the Open Collaboration Tools project
193 lines (168 loc) • 7.05 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 { 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;
export class PeerImpl implements Peer {
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;
}
private readonly messageRelay: MessageRelay;
private readonly peerInfo: PeerInfo;
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;
}
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`);
}
}