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