// Main interface with the servers import * as electronRemote from '@electron/remote'; const electronConsole = electronRemote.getGlobal('console') as Console; import Logger from '../../logger/logger'; const LOG = Logger.create(__filename, electronConsole); import { EventEmitter } from "stream"; import ClientController from "./client-controller"; import * as socketio from 'socket.io-client'; import * as crypto from 'crypto'; import DBCache from './db-cache'; import { ServerConfig } from './data-types'; import { IAddServerData } from './elements/overlay-add-server'; export default class Controller extends EventEmitter { public servers: ClientController[] = []; constructor() { super(); } async _connectFromConfig(serverConfig: ServerConfig): Promise { LOG.debug(`connecting to server#${serverConfig.serverId} at ${serverConfig.url}`); let server = new ClientController(serverConfig); await DBCache.clearAllMemberStatus(server.id); this.servers.push(server); // Forward server events through this event emitter let serverEvents = [ 'connected', 'disconnected', 'verified', 'new-message', 'update-server', 'deleted-members', 'updated-members', 'added-members', 'deleted-channels', 'updated-channels', 'added-channels', 'deleted-messages', 'updated-messages', 'added-messages', ]; for (let event of serverEvents) { server.on(event, (...args) => { this.emit(event, server, ...args); }); } return server; } async init(): Promise { this.servers = []; let serverConfigs = await DBCache.getServerConfigs(); // TODO: HTML prompt if no server configs if (serverConfigs.length == 0) { LOG.warn('no server configs found in client-side db'); } for (let serverConfig of serverConfigs) { await this._connectFromConfig(serverConfig); } } static _socketEmitTimeout(socket: socketio.Socket, ms: number, name: string, ...args: any[]) { // see also client-controller.js let socketArgs = args.slice(0, args.length - 1); let respond = args[args.length - 1]; let cutoff = false; let timeout = setTimeout(() => { cutoff = true; respond('emit timeout'); }, ms); socket.emit(name, ...socketArgs, (...respondArgs: any[]) => { if (cutoff) { return; } clearTimeout(timeout); respond(...respondArgs); }); } async addNewServer(serverConfig: IAddServerData, displayName: string, avatarBuff: Buffer): Promise { const { name, url, cert, token } = serverConfig; LOG.debug('Adding new server', { name, url, cert, token, displayName, avatarBuff }); let socket = await new Promise((resolve, reject) => { let socket = socketio.connect(url, { forceNew: true, ca: cert, // verifies the server's identity reconnection: false, // a bit stricter for registration }); socket.on('connect_error', (e) => { LOG.error('connect error', e); reject(new Error('unable to connect to server')); }); socket.on('connect', () => { resolve(socket); }); }); try { // Create a new Public/Private key pair to identify ourselves with this server let { publicKey, privateKey } = await new Promise((resolve, reject) => { crypto.generateKeyPair('rsa', { modulusLength: 4096 }, (err, publicKey, privateKey) => { if (err) { reject(err); } else { resolve({ publicKey, privateKey }); } }); }); return await new Promise((resolve, reject) => { let clientPublicKeyDerBuff = publicKey.export({ type: 'spki', format: 'der' }); Controller._socketEmitTimeout(socket, 5000, 'register-with-token', token, clientPublicKeyDerBuff, displayName, avatarBuff, async (errStr, member) => { if (errStr) { reject(new Error(errStr)); } else { try { let serverConfig: ServerConfig | null = null; await DBCache.queueTransaction(async () => { let publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }); let privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }); let identityId = await DBCache.addIdentity(publicKeyPem, privateKeyPem); let serverId = await DBCache.addServer(url, cert, name); await DBCache.addServerIdentity(serverId, identityId); serverConfig = await DBCache.getServerConfig(serverId, identityId); }); if (serverConfig == null) { throw new Error('unable to get server config'); } let server = await this._connectFromConfig(serverConfig); resolve(server); } catch (e) { reject(e); } } } ); }); } finally { socket.disconnect(); } } async removeServer(server: ClientController): Promise { await DBCache.removeServer(server.id); this.servers = this.servers.filter(s => s != server); } }