231 lines
8.7 KiB
TypeScript
231 lines
8.7 KiB
TypeScript
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<boolean>;
|
|
|
|
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<boolean>) : Promise<void> {
|
|
return new Promise<void>((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;
|
|
})
|
|
}
|
|
} |