import {LogCategory, logDebug, logError, logWarn} from "../log"; import {BasicIPCHandler, ChannelMessage, IPCChannel} from "../ipc/BrowserIPC"; import {guid} from "../crypto/uid"; import {tr} from "tc-shared/i18n/localize"; export type ConnectRequestData = { address: string; profile?: string; username?: string; password?: { value: string; hashed: boolean; }; } export interface ConnectOffer { request_id: string; data: ConnectRequestData; } export interface ConnectOfferAnswer { request_id: string; accepted: boolean; } export interface ConnectExecute { request_id: string; } export interface ConnectExecuted { request_id: string; succeeded: boolean; message?: string; } /* The connect process: * 1. Broadcast an offer * 2. Wait 50ms for all offer responses or until the first one respond with "ok" * 3. Select (if possible) on accepted offer and execute the connect */ export class ConnectHandler { private static readonly CHANNEL_NAME = "connect"; readonly ipc_handler: BasicIPCHandler; private ipc_channel: IPCChannel; public callback_available: (data: ConnectRequestData) => boolean = () => false; public callback_execute: (data: ConnectRequestData) => boolean | string = () => false; private _pending_connect_offers: { id: string; data: ConnectRequestData; timeout: number; remote_handler: string; }[] = []; private _pending_connects_requests: { id: string; data: ConnectRequestData; timeout: number; callback_success: () => any; callback_failed: (message: string) => any; callback_avail: () => Promise; remote_handler?: string; }[] = []; constructor(ipc_handler: BasicIPCHandler) { this.ipc_handler = ipc_handler; } public setup() { this.ipc_channel = this.ipc_handler.createChannel(ConnectHandler.CHANNEL_NAME); this.ipc_channel.messageHandler = this.onMessage.bind(this); } private onMessage(sender: string, broadcast: boolean, message: ChannelMessage) { if(broadcast) { if(message.type == "offer") { const data = message.data as ConnectOffer; const response = { accepted: this.callback_available(data.data), request_id: data.request_id } as ConnectOfferAnswer; if(response.accepted) { logDebug(LogCategory.IPC, tr("Received new connect offer from %s: %s"), sender, data.request_id); const ld = { remote_handler: sender, data: data.data, id: data.request_id, timeout: 0 }; this._pending_connect_offers.push(ld); ld.timeout = setTimeout(() => { logDebug(LogCategory.IPC, tr("Dropping connect request %s, because we never received an execute."), ld.id); this._pending_connect_offers.remove(ld); }, 120 * 1000) as any; } this.ipc_channel.sendMessage("offer-answer", response, sender); } } else { if(message.type == "offer-answer") { const data = message.data as ConnectOfferAnswer; const request = this._pending_connects_requests.find(e => e.id === data.request_id); if(!request) { logWarn(LogCategory.IPC, tr("Received connect offer answer with unknown request id (%s)."), data.request_id); return; } if(!data.accepted) { logDebug(LogCategory.IPC, tr("Client %s rejected the connect offer (%s)."), sender, request.id); return; } if(request.remote_handler) { logDebug(LogCategory.IPC, tr("Client %s accepted the connect offer (%s), but offer has already been accepted."), sender, request.id); return; } logDebug(LogCategory.IPC, tr("Client %s accepted the connect offer (%s). Request local acceptance."), sender, request.id); request.remote_handler = sender; clearTimeout(request.timeout); request.callback_avail().then(flag => { if(!flag) { request.callback_failed("local avail rejected"); return; } logDebug(LogCategory.IPC, tr("Executing connect with client %s"), request.remote_handler); this.ipc_channel.sendMessage("execute", { request_id: request.id } as ConnectExecute, request.remote_handler); request.timeout = setTimeout(() => { request.callback_failed("connect execute timeout"); }, 1000) as any; }).catch(error => { logError(LogCategory.IPC, tr("Local avail callback caused an error: %o"), error); request.callback_failed(tr("local avail callback caused an error")); }); } else if(message.type == "executed") { const data = message.data as ConnectExecuted; const request = this._pending_connects_requests.find(e => e.id === data.request_id); if(!request) { logWarn(LogCategory.IPC, tr("Received connect executed with unknown request id (%s)."), data.request_id); return; } if(request.remote_handler != sender) { logWarn(LogCategory.IPC, tr("Received connect executed for request %s, but from wrong client: %s (expected %s)"), data.request_id, sender, request.remote_handler); return; } logDebug(LogCategory.IPC, tr("Received connect executed response from client %s for request %s. Succeeded: %o (%s)"), sender, data.request_id, data.succeeded, data.message); clearTimeout(request.timeout); if(data.succeeded) request.callback_success(); else request.callback_failed(data.message); } else if(message.type == "execute") { const data = message.data as ConnectExecute; const request = this._pending_connect_offers.find(e => e.id === data.request_id); if(!request) { logWarn(LogCategory.IPC, tr("Received connect execute with unknown request id (%s)."), data.request_id); return; } if(request.remote_handler != sender) { logWarn(LogCategory.IPC, tr("Received connect execute for request %s, but from wrong client: %s (expected %s)"), data.request_id, sender, request.remote_handler); return; } clearTimeout(request.timeout); this._pending_connect_offers.remove(request); logDebug(LogCategory.IPC, tr("Executing connect for %s"), data.request_id); const cr = this.callback_execute(request.data); const response = { request_id: data.request_id, succeeded: typeof(cr) !== "string" && cr, message: typeof(cr) === "string" ? cr : "", } as ConnectExecuted; this.ipc_channel.sendMessage("executed", response, request.remote_handler); } } } post_connect_request(data: ConnectRequestData, callback_avail: () => Promise) : Promise { return new Promise((resolve, reject) => { const pd = { data: data, id: guid(), timeout: 0, callback_success: () => { this._pending_connects_requests.remove(pd); clearTimeout(pd.timeout); resolve(); }, callback_failed: error => { this._pending_connects_requests.remove(pd); clearTimeout(pd.timeout); reject(error); }, callback_avail: callback_avail, }; this._pending_connects_requests.push(pd); this.ipc_channel.sendMessage("offer", { request_id: pd.id, data: pd.data } as ConnectOffer); pd.timeout = setTimeout(() => { pd.callback_failed("received no response to offer"); }, 50) as any; }) } }