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 341 : import cockpit from "cockpit";
20 :
21 341 : export function Router(callbacks) {
22 341 : const self = this;
23 :
24 341 : let unique_id = 0;
25 341 : const origin = cockpit.transport.origin;
26 341 : const source_by_seed = { };
27 341 : const source_by_name = { };
28 :
29 338 : cockpit.transport.filter(function(message, channel, control) {
30 : /* Only control messages with a channel are forwardable */
31 338 : if (control) {
32 338 : if (control.channel !== undefined) {
33 338 : for (const seed in source_by_seed) {
34 338 : const source = source_by_seed[seed];
35 338 : if (!source.window.closed)
36 338 : source.window.postMessage(message, origin);
37 338 : }
38 63 : } 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 63 : }
43 :
44 : /* Forward message to relevant frame */
45 338 : } else if (channel) {
46 338 : const pos = channel.indexOf('!');
47 338 : if (pos !== -1) {
48 338 : const seed = channel.substring(0, pos + 1);
49 338 : const source = source_by_seed[seed];
50 338 : if (source) {
51 338 : if (!source.window.closed)
52 338 : source.window.postMessage(message, origin);
53 338 : return false; /* Stop delivery */
54 338 : }
55 338 : }
56 338 : }
57 :
58 : /* Still deliver the message locally */
59 338 : return true;
60 338 : }, false);
61 :
62 50 : function perform_jump(child, control) {
63 3 : let str = control.location || "";
64 50 : if (str[0] != "/")
65 8 : str = "/" + str;
66 50 : if (control.host)
67 26 : str = "/@" + encodeURIComponent(control.host) + str;
68 :
69 50 : callbacks.perform_frame_jump_command(child.name, str);
70 50 : }
71 :
72 338 : function perform_track(child) {
73 338 : let hash = child.location.hash;
74 338 : if (hash.indexOf("#") === 0)
75 338 : hash = hash.substring(1);
76 338 : if (hash === "/")
77 338 : hash = "";
78 :
79 338 : callbacks.perform_frame_hash_track(child.name, hash);
80 338 : }
81 :
82 66 : function on_unload(ev) {
83 66 : let source;
84 66 : if (ev.target.defaultView)
85 16 : source = source_by_name[ev.target.defaultView.name];
86 16 : else if (ev.view)
87 16 : source = source_by_name[ev.view.name];
88 66 : if (source)
89 66 : unregister(source);
90 66 : }
91 :
92 162 : function on_hashchange(ev) {
93 162 : const source = source_by_name[ev.target.name];
94 162 : if (source)
95 162 : perform_track(source.window);
96 162 : }
97 :
98 307 : function on_load(ev) {
99 307 : const source = source_by_name[ev.target.contentWindow.name];
100 307 : if (source)
101 307 : perform_track(source.window);
102 307 : }
103 :
104 66 : function unregister(source) {
105 66 : const child = source.window;
106 66 : cockpit.kill(null, child.name);
107 66 : const frame = child.frameElement;
108 66 : if (frame)
109 66 : frame.removeEventListener("load", on_load);
110 : /* This is often invalid when the window is closed */
111 66 : if (child.removeEventListener) {
112 66 : child.removeEventListener("unload", on_unload);
113 66 : child.removeEventListener("hashchange", on_hashchange);
114 66 : }
115 66 : delete source_by_seed[source.channel_seed];
116 66 : delete source_by_name[source.name];
117 66 : }
118 :
119 338 : function register(child) {
120 338 : let host;
121 338 : let page;
122 63 : const name = child.name || "";
123 338 : if (name.indexOf("cockpit1:") === 0) {
124 338 : const parts = name.substring(9).split("/");
125 338 : host = parts[0];
126 338 : page = parts.slice(1).join("/");
127 338 : }
128 63 : if (!name || !host || !page) {
129 63 : console.warn("invalid child window name", child, name);
130 63 : return;
131 63 : }
132 :
133 338 : unique_id += 1;
134 63 : const seed = (cockpit.transport.options["channel-seed"] || "undefined:") + unique_id.toString() + "!";
135 338 : const source = {
136 338 : name,
137 338 : window: child,
138 338 : channel_seed: seed,
139 338 : default_host: host,
140 338 : page,
141 338 : inited: false,
142 338 : };
143 338 : source_by_seed[seed] = source;
144 338 : source_by_name[name] = source;
145 :
146 338 : const frame = child.frameElement;
147 338 : frame.addEventListener("load", on_load);
148 338 : child.addEventListener("unload", on_unload);
149 338 : child.addEventListener("hashchange", on_hashchange);
150 :
151 338 : perform_track(child);
152 :
153 338 : return source;
154 338 : }
155 :
156 338 : function message_handler(event) {
157 338 : if (event.origin !== origin)
158 338 : return;
159 :
160 338 : let data = event.data;
161 338 : const child = event.source;
162 338 : if (!child)
163 338 : 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 65 : if (data instanceof window.ArrayBuffer) {
170 65 : cockpit.transport.inject(data, true);
171 65 : return;
172 65 : }
173 :
174 338 : if (typeof data !== "string")
175 338 : return;
176 :
177 338 : let source;
178 338 : 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 338 : try {
186 338 : source = source_by_name[child.name];
187 63 : } catch (ex) {
188 63 : console.log("received message from child with inaccessible name: ", ex);
189 63 : return;
190 63 : }
191 :
192 : /* Closing the transport */
193 68 : if (data.length === 0) {
194 68 : if (source)
195 63 : unregister(source);
196 68 : return;
197 68 : }
198 :
199 : /* A control message */
200 338 : if (data[0] == '\n') {
201 338 : control = JSON.parse(data.substring(1));
202 338 : if (control.command === "init") {
203 338 : if (source)
204 63 : unregister(source);
205 63 : if (control.problem) {
206 63 : console.warn("child frame failed to init: " + control.problem);
207 63 : source = null;
208 63 : } else {
209 338 : source = register(child);
210 338 : }
211 338 : if (source) {
212 338 : const reply = {
213 338 : ...cockpit.transport.options,
214 338 : command: "init",
215 338 : host: source.default_host,
216 338 : "channel-seed": source.channel_seed,
217 338 : };
218 338 : child.postMessage("\n" + JSON.stringify(reply), origin);
219 338 : source.inited = true;
220 :
221 338 : callbacks.frame_is_initialized(child.frameElement.name);
222 338 : }
223 338 : return;
224 106 : } else if (control.command === "jump") {
225 106 : perform_jump(child, control);
226 106 : return;
227 84 : } else if (control.command === "hint") {
228 64 : if (control.hint == "restart") {
229 : /* watchdog handles current host for now */
230 64 : if (control.host != cockpit.transport.host)
231 63 : callbacks.expect_restart(control.host);
232 64 : } else
233 208 : cockpit.hint(control.hint, control);
234 209 : return;
235 64 : } else if (control.command == "oops") {
236 64 : callbacks.show_oops();
237 64 : return;
238 63 : } else if (control.command == "notify") {
239 91 : if (source)
240 91 : callbacks.handle_notifications(source.default_host, source.page, control);
241 91 : return;
242 :
243 : /* Only control messages with a channel are forwardable */
244 63 : } else if (control.channel === undefined && (control.command !== "logout" && control.command !== "kill")) {
245 63 : return;
246 :
247 : /* Add the child's group to all open channel messages */
248 63 : } else if (control.command == "open") {
249 338 : control.group = child.name;
250 338 : data = "\n" + JSON.stringify(control);
251 338 : }
252 338 : }
253 :
254 65 : if (!source) {
255 65 : console.warn("child frame " + child.name + " sending data without init");
256 65 : return;
257 65 : }
258 :
259 : /* Everything else gets forwarded */
260 338 : cockpit.transport.inject(data, true);
261 338 : }
262 :
263 338 : self.hint = function hint(name, data) {
264 338 : const source = source_by_name[name];
265 : /* This is often invalid when the window is closed */
266 338 : if (source && source.inited && !source.window.closed) {
267 338 : data.command = "hint";
268 338 : const message = "\n" + JSON.stringify(data);
269 338 : source.window.postMessage(message, origin);
270 338 : }
271 338 : };
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 341 : cockpit.transport.wait(function() {
280 341 : window.addEventListener("message", message_handler, false);
281 341 : });
282 341 : }
|