Added the possibility to connect within an already running TeaWeb instance

canary
WolverinDEV 2019-11-06 14:27:29 +01:00
parent 9326572342
commit 27e3967ffa
4 changed files with 550 additions and 234 deletions

View File

@ -1,4 +1,7 @@
# Changelog:
* **06.10.19**
- Added the possibility to connect within an already running TeaWeb instance
* **30.10.19**
- Removed old `files.php` script and replaced it with a more modern `file.ts` script to generate the environment
- Some small UI fixed

View File

@ -28,8 +28,8 @@ namespace bipc {
export interface ChannelMessage {
channel_id: string;
key: string;
message: any;
type: string;
data: any;
}
export interface ProcessQueryResponse {
@ -63,7 +63,7 @@ namespace bipc {
abstract send_message(type: string, data: any, target?: string);
protected handle_message(message: BroadcastMessage) {
log.trace(LogCategory.IPC, tr("Received message %o"), message);
//log.trace(LogCategory.IPC, tr("Received message %o"), message);
if(message.receiver === BasicIPCHandler.BROADCAST_UNIQUE_ID) {
if(message.type == "process-query") {
@ -86,7 +86,8 @@ namespace bipc {
log.warn(LogCategory.IPC, tr("Received a query response for an unknown request."));
}
return;
} else if(message.type == "certificate-accept-callback") {
}
else if(message.type == "certificate-accept-callback") {
const data: CertificateAcceptCallback = message.data;
if(!this._cert_accept_callbacks[data.request_id]) {
log.warn(LogCategory.IPC, tr("Received certificate accept callback for an unknown request ID."));
@ -99,21 +100,24 @@ namespace bipc {
} as CertificateAcceptSucceeded, message.sender);
return;
} else if(message.type == "certificate-accept-succeeded") {
}
else if(message.type == "certificate-accept-succeeded") {
if(!this._cert_accept_succeeded[message.sender]) {
log.warn(LogCategory.IPC, tr("Received certificate accept succeeded, but haven't a callback."));
return;
}
this._cert_accept_succeeded[message.sender]();
return;
} else if(message.type === "channel") {
}
}
if(message.type === "channel") {
const data: ChannelMessage = message.data;
let channel_invoked = false;
for(const channel of this._channels)
if(channel.channel_id === data.channel_id && (typeof(channel.target_id) === "undefined" || channel.target_id === message.sender)) {
if(channel.message_handler)
channel.message_handler(message.sender, data);
channel.message_handler(message.sender, message.receiver === BasicIPCHandler.BROADCAST_UNIQUE_ID, data);
channel_invoked = true;
}
if(!channel_invoked) {
@ -121,22 +125,23 @@ namespace bipc {
}
}
}
}
create_channel(target_id?: string, channel_id?: string) {
let channel: Channel = {
target_id: target_id,
channel_id: channel_id || uuidv4(),
message_handler: undefined,
send_message: (key: string, message: any) => {
if(!channel.target_id)
throw "channel has no target!";
send_message: (type: string, data: any, target?: string) => {
if(typeof target !== "undefined") {
if(typeof channel.target_id === "string" && target != channel.target_id)
throw "target id does not match channel target";
}
this.send_message("channel", {
key: key,
message: message,
type: type,
data: data,
channel_id: channel.channel_id
} as ChannelMessage, channel.target_id)
} as ChannelMessage, target || channel.target_id || BasicIPCHandler.BROADCAST_UNIQUE_ID);
}
};
@ -198,8 +203,8 @@ namespace bipc {
readonly channel_id: string;
target_id?: string;
message_handler: (remote_id: string, message: ChannelMessage) => any;
send_message(key: string, message: any);
message_handler: (remote_id: string, broadcast: boolean, message: ChannelMessage) => any;
send_message(type: string, message: any, target?: string);
}
class BroadcastChannelIPC extends BasicIPCHandler {
@ -252,6 +257,224 @@ namespace bipc {
}
}
export namespace connect {
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: Channel;
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.create_channel(undefined, ConnectHandler.CHANNEL_NAME);
this.ipc_channel.message_handler = this.on_message.bind(this);
}
private on_message(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) {
log.debug(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(() => {
log.debug(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.send_message("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) {
log.warn(LogCategory.IPC, tr("Received connect offer answer with unknown request id (%s)."), data.request_id);
return;
}
if(!data.accepted) {
log.debug(LogCategory.IPC, tr("Client %s rejected the connect offer (%s)."), sender, request.id);
return;
}
if(request.remote_handler) {
log.debug(LogCategory.IPC, tr("Client %s accepted the connect offer (%s), but offer has already been accepted."), sender, request.id);
return;
}
log.debug(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;
}
log.debug(LogCategory.IPC, tr("Executing connect with client %s"), request.remote_handler);
this.ipc_channel.send_message("execute", {
request_id: request.id
} as ConnectExecute, request.remote_handler);
request.timeout = setTimeout(() => {
request.callback_failed("connect execute timeout");
}, 1000) as any;
}).catch(error => {
log.error(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) {
log.warn(LogCategory.IPC, tr("Received connect executed with unknown request id (%s)."), data.request_id);
return;
}
if(request.remote_handler != sender) {
log.warn(LogCategory.IPC, tr("Received connect executed for request %s, but from wrong client: %s (expected %s)"), data.request_id, sender, request.remote_handler);
return;
}
log.debug(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) {
log.warn(LogCategory.IPC, tr("Received connect execute with unknown request id (%s)."), data.request_id);
return;
}
if(request.remote_handler != sender) {
log.warn(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);
log.debug(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.send_message("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: uuidv4(),
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.send_message("offer", {
request_id: pd.id,
data: pd.data
} as ConnectOffer);
pd.timeout = setTimeout(() => {
pd.callback_failed("received no response to offer");
}, 50) as any;
})
}
}
}
export namespace mproxy {
export interface MethodProxyInvokeData {
method_name: string;
arguments: any[];
@ -364,15 +587,15 @@ namespace bipc {
}
}
private _handle_message(remote_id: string, message: ChannelMessage) {
if(message.key === "finalize") {
private _handle_message(remote_id: string, boradcast: boolean, message: ChannelMessage) {
if(message.type === "finalize") {
this._handle_finalize();
} else if(message.key === "initialize") {
} else if(message.type === "initialize") {
this._handle_remote_callback(remote_id);
} else if(message.key === "invoke") {
this._handle_invoke(message.message);
} else if(message.key === "result") {
this._handle_result(message.message);
} else if(message.type === "invoke") {
this._handle_invoke(message.data);
} else if(message.type === "result") {
this._handle_result(message.data);
}
}
@ -464,20 +687,30 @@ namespace bipc {
protected abstract on_connected();
protected abstract on_disconnected();
}
}
let handler: BasicIPCHandler;
let connect_handler: connect.ConnectHandler;
export function setup() {
if(!supported())
return;
/* TODO: test for support */
handler = new BroadcastChannelIPC();
handler.setup();
connect_handler = new connect.ConnectHandler(handler);
connect_handler.setup();
}
export function get_handler() {
return handler;
}
export function get_connect_handler() {
return connect_handler;
}
export function supported() {
/* ios does not support this */
return typeof(window.BroadcastChannel) !== "undefined";

View File

@ -8,6 +8,8 @@
/// <reference path="log.ts" />
/// <reference path="PPTListener.ts" />
import spawnYesNo = Modals.spawnYesNo;
const js_render = window.jsrender || $;
const native_client = window.require !== undefined;
@ -288,35 +290,42 @@ interface Window {
}
*/
function execute_default_connect() {
if(settings.static(Settings.KEY_FLAG_CONNECT_DEFAULT, false) && settings.static(Settings.KEY_CONNECT_ADDRESS, "")) {
const profile_uuid = settings.static(Settings.KEY_CONNECT_PROFILE, (profiles.default_profile() || {id: 'default'}).id);
const profile = profiles.find_profile(profile_uuid) || profiles.default_profile();
const address = settings.static(Settings.KEY_CONNECT_ADDRESS, "");
const username = settings.static(Settings.KEY_CONNECT_USERNAME, "Another TeaSpeak user");
type ConnectRequestData = {
address: string;
const password = settings.static(Settings.KEY_CONNECT_PASSWORD, "");
const password_hashed = settings.static(Settings.KEY_FLAG_CONNECT_PASSWORD, false);
profile?: string;
username?: string;
password?: {
value: string;
hashed: boolean;
};
}
function handle_connect_request(properties: ConnectRequestData, connection: ConnectionHandler) {
const profile_uuid = properties.profile || (profiles.default_profile() || {id: 'default'}).id;
const profile = profiles.find_profile(profile_uuid) || profiles.default_profile();
const username = properties.username || profile.connect_username();
const password = properties.password ? properties.password.value : "";
const password_hashed = properties.password ? properties.password.hashed : false;
if(profile && profile.valid()) {
const connection = server_connections.active_connection_handler() || server_connections.spawn_server_connection_handler();
connection.startConnection(address, profile, true, {
connection.startConnection(properties.address, profile, true, {
nickname: username,
password: password.length > 0 ? {
password: password,
hashed: password_hashed
} : undefined
});
server_connections.set_active_connection_handler(connection);
} else {
Modals.spawnConnectModal({},{
url: address,
url: properties.address,
enforce: true
}, {
profile: profile,
enforce: true
});
}
}
}
function main() {
@ -416,7 +425,6 @@ function main() {
};
/* schedule it a bit later then the main because the main function is still within the loader */
setTimeout(execute_default_connect, 5);
setTimeout(() => {
const connection = server_connections.active_connection_handler();
/*
@ -499,6 +507,74 @@ const task_teaweb_starter: loader.Task = {
priority: 10
};
const task_connect_handler: loader.Task = {
name: "Connect handler",
function: async () => {
const address = settings.static(Settings.KEY_CONNECT_ADDRESS, "");
const chandler = bipc.get_connect_handler();
if(settings.static(Settings.KEY_FLAG_CONNECT_DEFAULT, false) && address) {
const connect_data = {
address: address,
profile: settings.static(Settings.KEY_CONNECT_PROFILE, ""),
username: settings.static(Settings.KEY_CONNECT_USERNAME, ""),
password: {
value: settings.static(Settings.KEY_CONNECT_PASSWORD, ""),
hashed: settings.static(Settings.KEY_FLAG_CONNECT_PASSWORD, false)
}
};
if(chandler) {
try {
await chandler.post_connect_request(connect_data, () => new Promise<boolean>((resolve, reject) => {
spawnYesNo(tr("Another TeaWeb instance is already running"), tra("Another TeaWeb instance is already running.{:br:}Would you like to connect there?"), response => {
resolve(response);
}, {
closeable: false
}).open();
}));
log.info(LogCategory.CLIENT, tr("Executed connect successfully in another browser window. Closing this window"));
const message =
"You're connecting to {0} within the other TeaWeb instance.{:br:}" +
"You could now close this page.";
createInfoModal(
tr("Connecting successfully within other instance"),
MessageHelper.formatMessage(tr(message), connect_data.address),
{
closeable: false,
footer: undefined
}
).open();
return;
} catch(error) {
log.info(LogCategory.CLIENT, tr("Failed to execute connect within other TeaWeb instance. Using this one. Error: %o"), error);
}
}
loader.register_task(loader.Stage.LOADED, {
priority: 0,
function: async () => handle_connect_request(connect_data, server_connections.active_connection_handler() || server_connections.spawn_server_connection_handler()),
name: tr("default url connect")
});
}
if(chandler) {
/* no instance avail, so lets make us avail */
chandler.callback_available = data => {
return !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION);
};
chandler.callback_execute = data => {
handle_connect_request(data, server_connections.spawn_server_connection_handler());
return true;
}
}
loader.register_task(loader.Stage.LOADED, task_teaweb_starter);
},
priority: 10
};
const task_certificate_callback: loader.Task = {
name: "certificate accept tester",
function: async () => {
@ -546,7 +622,7 @@ const task_certificate_callback: loader.Task = {
log.info(LogCategory.IPC, tr("We're not used to accept certificated. Booting app."));
}
loader.register_task(loader.Stage.LOADED, task_teaweb_starter);
loader.register_task(loader.Stage.LOADED, task_connect_handler);
},
priority: 10
};
@ -574,8 +650,9 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
if(app.is_web()) {
loader.register_task(loader.Stage.LOADED, task_certificate_callback);
} else
} else {
loader.register_task(loader.Stage.LOADED, task_teaweb_starter);
}
} catch (ex) {
if(ex instanceof Error || typeof(ex.stack) !== "undefined")
console.error((tr || (msg => msg))("Critical error stack trace: %o"), ex.stack);

View File

@ -3,7 +3,9 @@
namespace Modals {
export function spawnYesNo(header: BodyCreator, body: BodyCreator, callback: (_: boolean) => any, properties?: {
text_yes?: string,
text_no?: string
text_no?: string,
closeable?: boolean;
}) {
properties = properties || {};
@ -16,6 +18,7 @@ namespace Modals {
props.header = header;
props.template_properties.question = ModalFunctions.jqueriefy(body);
props.closeable = typeof(properties.closeable) !== "boolean" || properties.closeable;
const modal = createModal(props);
let submited = false;
const button_yes = modal.htmlTag.find(".button-yes");