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