Line data Source code
1 : /*
2 : * Copyright (C) 2024 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 : /* THE MESSAGE ROUTER
7 : *
8 : * The message router forwards Cockpit protocol messages between the
9 : * web socket and the frames.
10 : *
11 : * It automatically starts processing messages for a frame as soon as
12 : * it receives the "init" message from that frame.
13 : *
14 : * The router needs a "callback" object that it uses to hook into the
15 : * rest of the shell. This is provided by the ShellState instance,
16 : * and more documentation can be found there.
17 : */
18 :
19 123 : import cockpit from "cockpit";
20 :
21 123 : export function Router(callbacks) {
22 123 : const self = this;
23 :
24 123 : let unique_id = 0;
25 123 : const origin = cockpit.transport.origin;
26 123 : const source_by_seed = { };
27 123 : const source_by_name = { };
28 :
29 120 : cockpit.transport.filter(function(message, channel, control) {
30 : /* Only control messages with a channel are forwardable */
31 120 : if (control) {
32 120 : if (control.channel !== undefined) {
33 120 : for (const seed in source_by_seed) {
34 120 : const source = source_by_seed[seed];
35 120 : if (!source.window.closed)
36 120 : source.window.postMessage(message, origin);
37 120 : }
38 19 : } else if (control.command == "hint") {
39 : /* This is where we handle hint messages directed at
40 : * the shell. Right now, there aren't any.
41 : */
42 19 : }
43 :
44 : /* Forward message to relevant frame */
45 120 : } else if (channel) {
46 120 : const pos = channel.indexOf('!');
47 120 : if (pos !== -1) {
48 120 : const seed = channel.substring(0, pos + 1);
49 120 : const source = source_by_seed[seed];
50 120 : if (source) {
51 120 : if (!source.window.closed)
52 120 : source.window.postMessage(message, origin);
53 120 : return false; /* Stop delivery */
54 120 : }
55 120 : }
56 120 : }
57 :
58 : /* Still deliver the message locally */
59 120 : return true;
60 120 : }, false);
61 :
62 24 : function perform_jump(child, control) {
63 1 : let str = control.location || "";
64 24 : if (str[0] != "/")
65 6 : str = "/" + str;
66 24 : if (control.host)
67 16 : str = "/@" + encodeURIComponent(control.host) + str;
68 :
69 24 : callbacks.perform_frame_jump_command(child.name, str);
70 24 : }
71 :
72 120 : function perform_track(child) {
73 120 : let hash = child.location.hash;
74 120 : if (hash.indexOf("#") === 0)
75 120 : hash = hash.substring(1);
76 120 : if (hash === "/")
77 120 : hash = "";
78 :
79 120 : callbacks.perform_frame_hash_track(child.name, hash);
80 120 : }
81 :
82 20 : function on_unload(ev) {
83 20 : let source;
84 20 : if (ev.target.defaultView)
85 6 : source = source_by_name[ev.target.defaultView.name];
86 6 : else if (ev.view)
87 6 : source = source_by_name[ev.view.name];
88 20 : if (source)
89 20 : unregister(source);
90 20 : }
91 :
92 44 : function on_hashchange(ev) {
93 44 : const source = source_by_name[ev.target.name];
94 44 : if (source)
95 44 : perform_track(source.window);
96 44 : }
97 :
98 105 : function on_load(ev) {
99 105 : const source = source_by_name[ev.target.contentWindow.name];
100 105 : if (source)
101 105 : perform_track(source.window);
102 105 : }
103 :
104 20 : function unregister(source) {
105 20 : const child = source.window;
106 20 : cockpit.kill(null, child.name);
107 20 : const frame = child.frameElement;
108 20 : if (frame)
109 20 : frame.removeEventListener("load", on_load);
110 : /* This is often invalid when the window is closed */
111 20 : if (child.removeEventListener) {
112 20 : child.removeEventListener("unload", on_unload);
113 20 : child.removeEventListener("hashchange", on_hashchange);
114 20 : }
115 20 : delete source_by_seed[source.channel_seed];
116 20 : delete source_by_name[source.name];
117 20 : }
118 :
119 120 : function register(child) {
120 120 : let host;
121 120 : let page;
122 19 : const name = child.name || "";
123 120 : if (name.indexOf("cockpit1:") === 0) {
124 120 : const parts = name.substring(9).split("/");
125 120 : host = parts[0];
126 120 : page = parts.slice(1).join("/");
127 120 : }
128 19 : if (!name || !host || !page) {
129 19 : console.warn("invalid child window name", child, name);
130 19 : return;
131 19 : }
132 :
133 120 : unique_id += 1;
134 19 : const seed = (cockpit.transport.options["channel-seed"] || "undefined:") + unique_id.toString() + "!";
135 120 : const source = {
136 120 : name,
137 120 : window: child,
138 120 : channel_seed: seed,
139 120 : default_host: host,
140 120 : page,
141 120 : inited: false,
142 120 : };
143 120 : source_by_seed[seed] = source;
144 120 : source_by_name[name] = source;
145 :
146 120 : const frame = child.frameElement;
147 120 : frame.addEventListener("load", on_load);
148 120 : child.addEventListener("unload", on_unload);
149 120 : child.addEventListener("hashchange", on_hashchange);
150 :
151 120 : perform_track(child);
152 :
153 120 : return source;
154 120 : }
155 :
156 120 : function message_handler(event) {
157 120 : if (event.origin !== origin)
158 120 : return;
159 :
160 120 : let data = event.data;
161 120 : const child = event.source;
162 120 : if (!child)
163 120 : return;
164 :
165 : /* If it's binary data just send it.
166 : * TODO: Once we start restricting what frames can
167 : * talk to which hosts, we need to parse control
168 : * messages here, and cross check channels */
169 19 : if (data instanceof window.ArrayBuffer) {
170 19 : cockpit.transport.inject(data, true);
171 19 : return;
172 19 : }
173 :
174 120 : if (typeof data !== "string")
175 120 : return;
176 :
177 120 : let source;
178 120 : let control;
179 :
180 : /*
181 : * On Internet Explorer we see Access Denied when non Cockpit
182 : * frames send messages (such as Javascript console). This also
183 : * happens when the window is closed.
184 : */
185 120 : try {
186 120 : source = source_by_name[child.name];
187 19 : } catch (ex) {
188 19 : console.log("received message from child with inaccessible name: ", ex);
189 19 : return;
190 19 : }
191 :
192 : /* Closing the transport */
193 19 : if (data.length === 0) {
194 19 : if (source)
195 19 : unregister(source);
196 19 : return;
197 19 : }
198 :
199 : /* A control message */
200 120 : if (data[0] == '\n') {
201 120 : control = JSON.parse(data.substring(1));
202 120 : if (control.command === "init") {
203 120 : if (source)
204 19 : unregister(source);
205 19 : if (control.problem) {
206 19 : console.warn("child frame failed to init: " + control.problem);
207 19 : source = null;
208 19 : } else {
209 120 : source = register(child);
210 120 : }
211 120 : if (source) {
212 120 : const reply = {
213 120 : ...cockpit.transport.options,
214 120 : command: "init",
215 120 : host: source.default_host,
216 120 : "channel-seed": source.channel_seed,
217 120 : };
218 120 : child.postMessage("\n" + JSON.stringify(reply), origin);
219 120 : source.inited = true;
220 :
221 120 : callbacks.frame_is_initialized(child.frameElement.name);
222 120 : }
223 120 : return;
224 41 : } else if (control.command === "jump") {
225 41 : perform_jump(child, control);
226 41 : return;
227 35 : } else if (control.command === "hint") {
228 20 : if (control.hint == "restart") {
229 : /* watchdog handles current host for now */
230 20 : if (control.host != cockpit.transport.host)
231 19 : callbacks.expect_restart(control.host);
232 20 : } else
233 59 : cockpit.hint(control.hint, control);
234 60 : return;
235 20 : } else if (control.command == "oops") {
236 20 : callbacks.show_oops();
237 20 : return;
238 19 : } else if (control.command == "notify") {
239 26 : if (source)
240 26 : callbacks.handle_notifications(source.default_host, source.page, control);
241 26 : return;
242 :
243 : /* Only control messages with a channel are forwardable */
244 19 : } else if (control.channel === undefined && (control.command !== "logout" && control.command !== "kill")) {
245 19 : return;
246 :
247 : /* Add the child's group to all open channel messages */
248 19 : } else if (control.command == "open") {
249 120 : control.group = child.name;
250 120 : data = "\n" + JSON.stringify(control);
251 120 : }
252 120 : }
253 :
254 19 : if (!source) {
255 19 : console.warn("child frame " + child.name + " sending data without init");
256 19 : return;
257 19 : }
258 :
259 : /* Everything else gets forwarded */
260 120 : cockpit.transport.inject(data, true);
261 120 : }
262 :
263 120 : self.hint = function hint(name, data) {
264 120 : const source = source_by_name[name];
265 : /* This is often invalid when the window is closed */
266 120 : if (source && source.inited && !source.window.closed) {
267 120 : data.command = "hint";
268 120 : const message = "\n" + JSON.stringify(data);
269 120 : source.window.postMessage(message, origin);
270 120 : }
271 120 : };
272 :
273 0 : self.unregister_name = (name) => {
274 0 : const source = source_by_name[name];
275 0 : if (source)
276 0 : unregister(source);
277 0 : };
278 :
279 123 : cockpit.transport.wait(function() {
280 123 : window.addEventListener("message", message_handler, false);
281 123 : });
282 123 : }
|