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)
This commit is contained in:
KernelDeimos 2024-09-11 23:59:25 -04:00
parent 62634b0afe
commit e3d4a5f3b3
7 changed files with 100 additions and 13 deletions

View File

@ -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);

View File

@ -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,
});
}

View File

@ -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){

View File

@ -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,
};
}

View File

@ -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) {

View File

@ -53,7 +53,6 @@ window.main_shell = async () => {
}
});
terminal.on('close', () => {
console.log('Terminal closed; exiting Phoenix...');
puter.exit();
});

View File

@ -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);
}