/* * Copyright (C) 2024 Red Hat, Inc. * SPDX-License-Identifier: LGPL-2.1-or-later */ /* THE MESSAGE ROUTER * * The message router forwards Cockpit protocol messages between the * web socket and the frames. * * It automatically starts processing messages for a frame as soon as * it receives the "init" message from that frame. * * The router needs a "callback" object that it uses to hook into the * rest of the shell. This is provided by the ShellState instance, * and more documentation can be found there. */ import cockpit from "cockpit"; export function Router(callbacks) { const self = this; let unique_id = 0; const origin = cockpit.transport.origin; const source_by_seed = { }; const source_by_name = { }; cockpit.transport.filter(function(message, channel, control) { /* Only control messages with a channel are forwardable */ if (control) { if (control.channel !== undefined) { for (const seed in source_by_seed) { const source = source_by_seed[seed]; if (!source.window.closed) source.window.postMessage(message, origin); } } else if (control.command === "hint") { /* This is where we handle hint messages directed at * the shell. Right now, there aren't any. */ } /* Forward message to relevant frame */ } else if (channel) { const pos = channel.indexOf('!'); if (pos !== -1) { const seed = channel.substring(0, pos + 1); const source = source_by_seed[seed]; if (source) { if (!source.window.closed) source.window.postMessage(message, origin); return false; /* Stop delivery */ } } } /* Still deliver the message locally */ return true; }, false); function perform_jump(child, control) { let str = control.location || ""; if (str[0] !== "/") str = "/" + str; if (control.host) str = "/@" + encodeURIComponent(control.host) + str; callbacks.perform_frame_jump_command(child.name, str); } function perform_track(child) { let hash = child.location.hash; if (hash.indexOf("#") === 0) hash = hash.substring(1); if (hash === "/") hash = ""; callbacks.perform_frame_hash_track(child.name, hash); } function on_unload(ev) { let source; if (ev.target.defaultView) source = source_by_name[ev.target.defaultView.name]; else if (ev.view) source = source_by_name[ev.view.name]; if (source) unregister(source); } function on_hashchange(ev) { const source = source_by_name[ev.target.name]; if (source) perform_track(source.window); } function on_load(ev) { const source = source_by_name[ev.target.contentWindow.name]; if (source) perform_track(source.window); } function unregister(source) { const child = source.window; cockpit.kill(null, child.name); const frame = child.frameElement; if (frame) frame.removeEventListener("load", on_load); /* This is often invalid when the window is closed */ if (child.removeEventListener) { child.removeEventListener("unload", on_unload); child.removeEventListener("hashchange", on_hashchange); } delete source_by_seed[source.channel_seed]; delete source_by_name[source.name]; } function register(child) { let host; let page; const name = child.name || ""; if (name.indexOf("cockpit1:") === 0) { const parts = name.substring(9).split("/"); host = parts[0]; page = parts.slice(1).join("/"); } if (!name || !host || !page) { console.warn("invalid child window name", child, name); return; } unique_id += 1; const seed = (cockpit.transport.options["channel-seed"] || "undefined:") + unique_id.toString() + "!"; const source = { name, window: child, channel_seed: seed, default_host: host, page, inited: false, }; source_by_seed[seed] = source; source_by_name[name] = source; const frame = child.frameElement; frame.addEventListener("load", on_load); child.addEventListener("unload", on_unload); child.addEventListener("hashchange", on_hashchange); perform_track(child); return source; } function message_handler(event) { if (event.origin !== origin) return; let data = event.data; const child = event.source; if (!child) return; /* If it's binary data just send it. * TODO: Once we start restricting what frames can * talk to which hosts, we need to parse control * messages here, and cross check channels */ if (data instanceof window.ArrayBuffer) { cockpit.transport.inject(data, true); return; } if (typeof data !== "string") return; let source; let control; /* * On Internet Explorer we see Access Denied when non Cockpit * frames send messages (such as Javascript console). This also * happens when the window is closed. */ try { source = source_by_name[child.name]; } catch (ex) { console.log("received message from child with inaccessible name: ", ex); return; } /* Closing the transport */ if (data.length === 0) { if (source) unregister(source); return; } /* A control message */ if (data[0] === '\n') { control = JSON.parse(data.substring(1)); if (control.command === "init") { if (source) unregister(source); if (control.problem) { console.warn("child frame failed to init: " + control.problem); source = null; } else { source = register(child); } if (source) { const reply = { ...cockpit.transport.options, command: "init", host: source.default_host, "channel-seed": source.channel_seed, }; child.postMessage("\n" + JSON.stringify(reply), origin); source.inited = true; callbacks.frame_is_initialized(child.frameElement.name); } return; } else if (control.command === "jump") { perform_jump(child, control); return; } else if (control.command === "hint") { if (control.hint === "restart") { /* watchdog handles current host for now */ if (control.host !== cockpit.transport.host) callbacks.expect_restart(control.host); } else cockpit.hint(control.hint, control); return; } else if (control.command === "oops") { callbacks.show_oops(); return; } else if (control.command === "notify") { if (source) callbacks.handle_notifications(source.default_host, source.page, control); return; /* Only control messages with a channel are forwardable */ } else if (control.channel === undefined && (control.command !== "logout" && control.command !== "kill")) { return; /* Add the child's group to all open channel messages */ } else if (control.command === "open") { control.group = child.name; data = "\n" + JSON.stringify(control); } } if (!source) { console.warn("child frame " + child.name + " sending data without init"); return; } /* Everything else gets forwarded */ cockpit.transport.inject(data, true); } self.hint = function hint(name, data) { const source = source_by_name[name]; /* This is often invalid when the window is closed */ if (source && source.inited && !source.window.closed) { data.command = "hint"; const message = "\n" + JSON.stringify(data); source.window.postMessage(message, origin); } }; self.unregister_name = (name) => { const source = source_by_name[name]; if (source) unregister(source); }; cockpit.transport.wait(function() { window.addEventListener("message", message_handler, false); }); }