From e3d4a5f3b3b9dab600db1e39e47dbf19703c493c Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Wed, 11 Sep 2024 23:59:25 -0400 Subject: [PATCH] dev: multi-instance many-to-many app-to-app comms I'll elaborate here because the commit name is confusing. Any time an app gets a connection to another app, either because it launched that app (or was launched by that app) or requested a connection to that app, the ID the app gets to represent the app it's communicating with is now a pseudo app id rather than the app instance ID. This accomplishes two things: 1. It's more secure. There are multiple places where GUI assumes that knowing an app's instance ID means you can do things as that app. 2. Between the same two apps, there may now be more than one connection. This is useful for situations like Phoenix shell talking to the emulator in multiple separate instances to pipe commands. (this is coming later) --- src/gui/src/IPC.js | 16 +++++++++++ src/gui/src/definitions.js | 17 +++++++++--- src/gui/src/helpers/launch_app.js | 2 +- src/gui/src/services/ExecService.js | 29 ++++++++++++++++---- src/gui/src/services/IPCService.js | 42 ++++++++++++++++++++++++++++- src/phoenix/src/main_puter.js | 1 - src/puter-js/src/modules/UI.js | 6 ++++- 7 files changed, 100 insertions(+), 13 deletions(-) diff --git a/src/gui/src/IPC.js b/src/gui/src/IPC.js index beeb0f90..9a08a50e 100644 --- a/src/gui/src/IPC.js +++ b/src/gui/src/IPC.js @@ -1439,6 +1439,22 @@ window.addEventListener('message', async (event) => { const { appInstanceID, targetAppInstanceID, targetAppOrigin, contents } = event.data; // TODO: Determine if we should allow the message // TODO: Track message traffic between apps + const svc_ipc = globalThis.services.get('ipc'); + // const svc_exec = globalThis.services() + + const conn = svc_ipc.get_connection(targetAppInstanceID); + if ( conn ) { + conn.send(contents); + // conn.send({ + // msg: 'messageToApp', + // appInstanceID, + // targetAppInstanceID, + // contents, + // }, targetAppOrigin); + return; + } + + console.log(`🔒 App ${appInstanceID} is sending to ${targetAppInstanceID} insecurely`); // pass on the message const target_iframe = window.iframe_for_app_instance(targetAppInstanceID); diff --git a/src/gui/src/definitions.js b/src/gui/src/definitions.js index 5ef2f925..22c14966 100644 --- a/src/gui/src/definitions.js +++ b/src/gui/src/definitions.js @@ -35,6 +35,9 @@ export class Service extends concepts.Service { this.services = a[0].services; return this._init(...a) } + get context () { + return { services: this.services }; + } }; export const PROCESS_INITIALIZING = { i18n_key: 'initializing' }; @@ -139,16 +142,22 @@ export class PortalProcess extends Process { } } - send (channel, object, context) { + send (channel, data, context) { const target = this.references.iframe.contentWindow; - // NEXT: ... + target.postMessage({ + msg: 'messageToApp', + appInstanceID: channel.returnAddress, + targetAppInstanceID: this.uuid, + contents: data, + // }, new URL(this.references.iframe.src).origin); + }, '*'); } - handle_connection (other_process, args) { + handle_connection (connection, args) { const target = this.references.iframe.contentWindow; target.postMessage({ msg: 'connection', - appInstanceID: other_process.uuid, + appInstanceID: connection.uuid, args, }); } diff --git a/src/gui/src/helpers/launch_app.js b/src/gui/src/helpers/launch_app.js index 00e2d2a7..bbb47c2a 100644 --- a/src/gui/src/helpers/launch_app.js +++ b/src/gui/src/helpers/launch_app.js @@ -183,7 +183,7 @@ const launch_app = async (options)=>{ // add parent_app_instance_id to URL if (options.parent_instance_id) { - iframe_url.searchParams.append('puter.parent_instance_id', options.parent_instance_id); + iframe_url.searchParams.append('puter.parent_instance_id', options.parent_pseudo_id); } if(file_signature){ diff --git a/src/gui/src/services/ExecService.js b/src/gui/src/services/ExecService.js index 213705b9..386b6cac 100644 --- a/src/gui/src/services/ExecService.js +++ b/src/gui/src/services/ExecService.js @@ -20,19 +20,30 @@ export class ExecService extends Service { async launchApp ({ app_name, args }, { ipc_context, msg_id } = {}) { const app = ipc_context?.caller?.app; const process = ipc_context?.caller?.process; - + // This mechanism will be replated with xdrpc soon const child_instance_id = window.uuidv4(); + const svc_ipc = this.services.get('ipc'); + const connection = ipc_context ? svc_ipc.add_connection({ + source: process.uuid, + target: child_instance_id, + }) : undefined; + // The "body" of this method is in a separate file const child_process = await launch_app({ name: app_name, args: args ?? {}, parent_instance_id: app?.appInstanceID, uuid: child_instance_id, + ...(connection ? { + parent_pseudo_id: connection.backward.uuid, + } : {}), }); const send_child_launched_msg = (...a) => { + if ( ! process ) return; + // TODO: (maybe) message process instead of iframe const parent_iframe = process?.references?.iframe; parent_iframe.contentWindow.postMessage({ msg: 'childAppLaunched', @@ -67,9 +78,10 @@ export class ExecService extends Service { window.report_app_closed(child_process.uuid); } }); - + return { - appInstanceID: child_instance_id, + appInstanceID: connection ? + connection.forward.uuid : child_instance_id, usesSDK: true, }; } @@ -96,10 +108,17 @@ export class ExecService extends Service { const options = svc_process.select_by_name(app_name); const process = options[0]; - await process.handle_connection(caller_process, args); + const svc_ipc = this.services.get('ipc'); + const connection = svc_ipc.add_connection({ + source: caller_process.uuid, + target: process.uuid, + }); + + const response = await process.handle_connection( + connection.backward, args); return { - appInstanceID: process.uuid, + appInstanceID: connection.forward.uuid, response, }; } diff --git a/src/gui/src/services/IPCService.js b/src/gui/src/services/IPCService.js index 83ac0bf3..82f7c8f2 100644 --- a/src/gui/src/services/IPCService.js +++ b/src/gui/src/services/IPCService.js @@ -1,12 +1,52 @@ import { Service } from "../definitions.js"; +class InternalConnection { + constructor ({ source, target, uuid, reverse }, { services }) { + this.services = services; + this.source = source; + this.target = target; + this.uuid = uuid; + this.reverse = reverse; + } + + send (data) { + const svc_process = this.services.get('process'); + const process = svc_process.get_by_uuid(this.target); + const channel = { + returnAddress: this.reverse, + }; + process.send(channel, data); + } +} + export class IPCService extends Service { static description = ` Allows other services to expose methods to apps. ` async _init () { - // + this.connections_ = {}; + } + + add_connection ({ source, target }) { + const uuid = window.uuidv4(); + const r_uuid = window.uuidv4(); + const forward = this.connections_[uuid] = { + source, target, + uuid: uuid, reverse: r_uuid, + }; + const backward = this.connections_[r_uuid] = { + source: target, target: source, + uuid: r_uuid, reverse: uuid, + }; + return { forward, backward }; + } + + get_connection (uuid) { + const entry = this.connections_[uuid]; + if ( ! entry ) return; + if ( entry.object ) return entry.object; + return entry.object = new InternalConnection(entry, this.context); } register_ipc_handler (name, spec) { diff --git a/src/phoenix/src/main_puter.js b/src/phoenix/src/main_puter.js index 01e03391..d61ef04f 100644 --- a/src/phoenix/src/main_puter.js +++ b/src/phoenix/src/main_puter.js @@ -53,7 +53,6 @@ window.main_shell = async () => { } }); terminal.on('close', () => { - console.log('Terminal closed; exiting Phoenix...'); puter.exit(); }); diff --git a/src/puter-js/src/modules/UI.js b/src/puter-js/src/modules/UI.js index f8d9a794..59a832cc 100644 --- a/src/puter-js/src/modules/UI.js +++ b/src/puter-js/src/modules/UI.js @@ -47,6 +47,7 @@ class AppConnection extends EventListener { // Message is from a different AppConnection; ignore it. return; } + // TODO: does this check really make sense? if (event.data.targetAppInstanceID !== this.appInstanceID) { console.error(`AppConnection received message intended for wrong app! appInstanceID=${this.appInstanceID}, target=${event.data.targetAppInstanceID}`); return; @@ -89,7 +90,10 @@ class AppConnection extends EventListener { msg: 'messageToApp', appInstanceID: this.appInstanceID, targetAppInstanceID: this.targetAppInstanceID, - targetAppOrigin: '*', // TODO: Specify this somehow + // Note: there was a TODO comment here about specifying the origin, + // but this should not happen here; the origin should be specified + // on the other side where the expected origin for the app is known. + targetAppOrigin: '*', contents: message, }, this.#puterOrigin); }