LCOV - code coverage report
Current view: top level - pkg/shell - router.jsx Coverage Total Hit
Test: cockpit Lines: 97.6 % 207 202
Test Date: 2026-07-03 07:31:16

            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          160 :     function on_hashchange(ev) {
      93          160 :         const source = source_by_name[ev.target.name];
      94          160 :         if (source)
      95          160 :             perform_track(source.window);
      96          160 :     }
      97              : 
      98          310 :     function on_load(ev) {
      99          310 :         const source = source_by_name[ev.target.contentWindow.name];
     100          310 :         if (source)
     101          310 :             perform_track(source.window);
     102          310 :     }
     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           87 :                 if (source)
     240           87 :                     callbacks.handle_notifications(source.default_host, source.page, control);
     241           87 :                 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 : }
        

Generated by: LCOV version 2.0-1