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-06-17 06:28:00

            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           23 :     function perform_jump(child, control) {
      63            1 :         let str = control.location || "";
      64           23 :         if (str[0] != "/")
      65            6 :             str = "/" + str;
      66           23 :         if (control.host)
      67           15 :             str = "/@" + encodeURIComponent(control.host) + str;
      68              : 
      69           23 :         callbacks.perform_frame_jump_command(child.name, str);
      70           23 :     }
      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          106 :     function on_load(ev) {
      99          106 :         const source = source_by_name[ev.target.contentWindow.name];
     100          106 :         if (source)
     101          106 :             perform_track(source.window);
     102          106 :     }
     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           40 :             } else if (control.command === "jump") {
     225           40 :                 perform_jump(child, control);
     226           40 :                 return;
     227           34 :             } 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 : }
        

Generated by: LCOV version 2.0-1