tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

sw_inter_sw_postmessage.js (6074B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 let bc = new BroadcastChannel("inter-sw-postmessage");
      5 let myId = /\/sw-(.)+$/.exec(registration.scope)[1];
      6 // If we are being imported by the generated script from
      7 // `sw_always_updating_inter_sw_postmessage.sjs`, there will be a "version"
      8 // global and it starts counting from 1.
      9 let myVersion = "version" in globalThis ? globalThis.version : 0;
     10 let myFullId = `${myId}#${myVersion}`;
     11 
     12 onactivate = function () {
     13  bc.postMessage(`${myId}:version-activated:${myVersion}`);
     14 };
     15 
     16 function extractId(urlStr) {
     17  if (!urlStr) {
     18    return urlStr;
     19  }
     20  const qIndex = urlStr.indexOf("?");
     21  if (qIndex >= 0) {
     22    return urlStr.substring(qIndex + 1);
     23  }
     24  if (urlStr.endsWith("/empty_with_utils.html")) {
     25    return "helper";
     26  }
     27  return urlStr;
     28 }
     29 
     30 function describeSource(source) {
     31  // Note that WindowProxy is impossible here, so we don't check it.
     32  if (source === null) {
     33    return "null";
     34  } else if (source instanceof MessagePort) {
     35    return "port";
     36  } else if (source instanceof WindowClient) {
     37    return `wc-${extractId(source.url)}`;
     38  } else if (source instanceof Client) {
     39    return `c-${extractId(source.url)}`;
     40  } else if (source instanceof ServiceWorker) {
     41    return `sw-${extractId(source.scriptURL)}`;
     42  } else {
     43    return "unexpected";
     44  }
     45 }
     46 
     47 let lastPostMessageSource = null;
     48 globalThis.onmessage = async function handle_message(evt) {
     49  console.log(myId, "received postMessage");
     50  lastPostMessageSource = evt.source;
     51  bc.postMessage(
     52    `${myId}:received-post-message-from:${describeSource(evt.source)}`
     53  );
     54 };
     55 
     56 /**
     57 * Map a target descriptor onto something we can postMessage.  Possible options
     58 * and the resulting target:
     59 * - `last-source`: The `.source` property of the most recent event received via
     60 *   `globalThis.onmessage`.
     61 * - `reg-sw-ID`: The active ServiceWorker found on a registration whose
     62 *   scriptURL ends with `?ID`.  This allows us to distinguish between multiple
     63 *   (non-self-updating) ServiceWorkers on the same registration because each SW
     64 *   can be given a distinct script path via the `?ID` suffix.  But it does not
     65 *   work for self-updating ServiceWorkers where the only difference is the
     66 *   version identifier embedded in the script itself.
     67 */
     68 async function resolveTarget(descriptor) {
     69  if (descriptor === "last-source") {
     70    return lastPostMessageSource;
     71  } else if (descriptor.startsWith("reg-")) {
     72    const registrations = await navigator.serviceWorker.getRegistrations();
     73    let filterFunc;
     74    if (descriptor.startsWith("reg-sw-")) {
     75      const descriptorId = /^reg-sw-(.+)$/.exec(descriptor)[1];
     76      console.log(
     77        "Looking for registration with id",
     78        descriptorId,
     79        "across",
     80        registrations.length,
     81        "registrations"
     82      );
     83      filterFunc = sw => {
     84        if (sw) {
     85          console.log("checking SW", sw.scriptURL);
     86        }
     87        return extractId(sw?.scriptURL) === descriptorId;
     88      };
     89    } else {
     90      throw new Error(`Target selector '${descriptor}' not understood`);
     91    }
     92 
     93    for (const reg of registrations) {
     94      console.log("Reg scriptURL", reg.active?.scriptURL);
     95      if (filterFunc(reg.active)) {
     96        return reg.active;
     97      } else if (filterFunc(reg.waiting)) {
     98        return reg.waiting;
     99      } else if (filterFunc(reg.installing)) {
    100        return reg.installing;
    101      }
    102    }
    103    throw new Error("No registration matches found!");
    104  }
    105  throw new Error(`Target selector '${descriptor}' not understood`);
    106 }
    107 
    108 /**
    109 * Map a registration descriptor onto a registration.  Options:
    110 * - `scope-ID`: The registration with a scope ending with `/sw-ID`.
    111 */
    112 async function resolveRegistration(descriptor) {
    113  if (descriptor.startsWith("sw-")) {
    114    const registrations = await navigator.serviceWorker.getRegistrations();
    115 
    116    const scopeSuffix = `/${descriptor}`;
    117    for (const reg of registrations) {
    118      if (reg.scope.endsWith(scopeSuffix)) {
    119        return reg;
    120      }
    121    }
    122 
    123    throw new Error("No registration matches found!");
    124  }
    125  throw new Error(`Registration selector '${descriptor}' not understood`);
    126 }
    127 
    128 bc.onmessage = async function handle_bc(evt) {
    129  // Split the message into colon-delimited commands of the form:
    130  // <who should do the thing>:<the command>:<the target of the command>
    131  if (typeof evt?.data !== "string") {
    132    return;
    133  }
    134  const pieces = evt?.data?.split(":");
    135  if (
    136    !pieces ||
    137    pieces.length < 2 ||
    138    (pieces[0] !== myId && pieces[0] !== myFullId)
    139  ) {
    140    return;
    141  }
    142 
    143  const cmd = pieces[1];
    144  try {
    145    if (cmd === "post-message-to") {
    146      const target = await resolveTarget(pieces[2]);
    147      target.postMessage("yo!");
    148    } else if (cmd === "update-reg") {
    149      const reg = await resolveRegistration(pieces[2]);
    150      reg.update();
    151    } else if (cmd === "install-reg") {
    152      const installId = pieces[2];
    153      const scope = `sw-${installId}`;
    154      const script = `sw_inter_sw_postmessage.js?${installId}`;
    155      await navigator.serviceWorker.register(script, {
    156        scope,
    157      });
    158      bc.postMessage(`${myId}:registered:${installId}`);
    159    } else if (cmd === "workerref-hang") {
    160      const topic = pieces[2];
    161      globalThis.WorkerTestUtils.holdStrongWorkerRefUntilMainThreadObserverNotified(
    162        topic
    163      );
    164      bc.postMessage(`${myId}:workerref-hung:${topic}`);
    165    } else if (cmd === "block") {
    166      const topic = pieces[2];
    167      globalThis.WorkerTestUtils.blockUntilMainThreadObserverNotified(
    168        topic,
    169        // This callback is invoked once the observer has been registered.
    170        () => {
    171          bc.postMessage(`${myId}:blocking:${topic}`);
    172        }
    173      );
    174    } else if (cmd === "notify-observer") {
    175      const topic = pieces[2];
    176      globalThis.WorkerTestUtils.notifyObserverOnMainThread(topic);
    177      bc.postMessage(`${myId}:notified-observer:${topic}`);
    178    }
    179  } catch (ex) {
    180    console.error(ex);
    181    bc.postMessage({
    182      error: ex + "",
    183      myId,
    184      processing: evt?.data,
    185    });
    186  }
    187 };