cordis/client/webapp/controller.ts
2021-11-07 17:04:04 -06:00

167 lines
5.0 KiB
TypeScript

// 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(
private dbCache: DBCache
) {
super();
}
async _connectFromConfig(serverConfig: ServerConfig): Promise<ClientController> {
LOG.debug(`connecting to server#${serverConfig.serverId} at ${serverConfig.url}`);
let server = new ClientController(this.dbCache, serverConfig);
await this.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<void> {
this.servers = [];
let serverConfigs = await this.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<ClientController> {
const { name, url, cert, token } = serverConfig;
LOG.debug('Adding new server', { name, url, cert, token, displayName, avatarBuff });
let socket = await new Promise<socketio.Socket>((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 this.dbCache.queueTransaction(async () => {
let publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' });
let privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' });
let identityId = await this.dbCache.addIdentity(publicKeyPem, privateKeyPem);
let serverId = await this.dbCache.addServer(url, cert, name);
await this.dbCache.addServerIdentity(serverId, identityId);
serverConfig = await this.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<void> {
await this.dbCache.removeServer(server.id);
this.servers = this.servers.filter(s => s != server);
}
}