tor-browser

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

test_ext_native_messaging_geckoview.js (12600B)


      1 "use strict";
      2 
      3 const server = createHttpServer({ hosts: ["example.com"] });
      4 server.registerPathHandler("/", (request, response) => {
      5  response.setStatusLine(request.httpVersion, 200, "OK");
      6  response.setHeader("Content-Type", "text/html; charset=utf-8", false);
      7  response.write("<!DOCTYPE html><html></html>");
      8 });
      9 
     10 ChromeUtils.defineESModuleGetters(this, {
     11  GeckoViewConnection: "resource://gre/modules/GeckoViewWebExtension.sys.mjs",
     12 });
     13 
     14 // Save reference to original implementations to restore later.
     15 const { sendMessage, onConnect } = GeckoViewConnection.prototype;
     16 add_setup(async () => {
     17  // This file replaces the implementation of GeckoViewConnection;
     18  // make sure that it is restored upon test completion.
     19  registerCleanupFunction(() => {
     20    GeckoViewConnection.prototype.sendMessage = sendMessage;
     21    GeckoViewConnection.prototype.onConnect = onConnect;
     22  });
     23 });
     24 
     25 // Mock the embedder communication port
     26 class EmbedderPort {
     27  constructor(portId, messenger) {
     28    this.id = portId;
     29    this.messenger = messenger;
     30  }
     31  close() {
     32    Assert.ok(false, "close not expected to be called");
     33  }
     34  onPortDisconnect() {
     35    Assert.ok(false, "onPortDisconnect not expected to be called");
     36  }
     37  onPortMessage() {
     38    Assert.ok(false, "onPortMessage not expected to be called");
     39  }
     40  triggerPortDisconnect() {
     41    this.messenger.sendPortDisconnect(this.id);
     42  }
     43 }
     44 
     45 function stubConnectNative() {
     46  let port;
     47  const firstCallPromise = new Promise(resolve => {
     48    let callCount = 0;
     49    GeckoViewConnection.prototype.onConnect = (portId, messenger) => {
     50      Assert.equal(++callCount, 1, "onConnect called once");
     51      port = new EmbedderPort(portId, messenger);
     52      resolve();
     53      return port;
     54    };
     55  });
     56  const triggerPortDisconnect = () => {
     57    if (!port) {
     58      Assert.ok(false, "Undefined port, connection must be established first");
     59    }
     60    port.triggerPortDisconnect();
     61  };
     62  const restore = () => {
     63    GeckoViewConnection.prototype.onConnect = onConnect;
     64  };
     65  return { firstCallPromise, triggerPortDisconnect, restore };
     66 }
     67 
     68 function stubSendNativeMessage() {
     69  let sendResponse;
     70  const returnPromise = new Promise(resolve => {
     71    sendResponse = resolve;
     72  });
     73  const firstCallPromise = new Promise(resolve => {
     74    let callCount = 0;
     75    GeckoViewConnection.prototype.sendMessage = data => {
     76      Assert.equal(++callCount, 1, "sendMessage called once");
     77      resolve(data);
     78      return returnPromise;
     79    };
     80  });
     81  const restore = () => {
     82    GeckoViewConnection.prototype.sendMessage = sendMessage;
     83  };
     84  return { firstCallPromise, sendResponse, restore };
     85 }
     86 
     87 function promiseExtensionEvent(wrapper, event) {
     88  return new Promise(resolve => {
     89    wrapper.extension.once(event, (...args) => resolve(args));
     90  });
     91 }
     92 
     93 // verify that when background sends a native message,
     94 // the background will not be terminated to allow native messaging
     95 add_task(async function test_sendNativeMessage_event_page() {
     96  const extension = ExtensionTestUtils.loadExtension({
     97    isPrivileged: true,
     98    manifest: {
     99      permissions: ["geckoViewAddons", "nativeMessaging"],
    100      background: { persistent: false },
    101    },
    102    async background() {
    103      const res = await browser.runtime.sendNativeMessage("fake", "msg");
    104      browser.test.assertEq("myResp", res, "expected response");
    105      browser.test.sendMessage("done");
    106      browser.runtime.onSuspend.addListener(async () => {
    107        browser.test.assertFail("unexpected onSuspend");
    108      });
    109    },
    110  });
    111 
    112  const stub = stubSendNativeMessage();
    113  await extension.startup();
    114  info("Wait for sendNativeMessage to be received");
    115  Assert.equal(
    116    (await stub.firstCallPromise).deserialize({}),
    117    "msg",
    118    "expected message"
    119  );
    120 
    121  info("Trigger background script idle timeout and expect to be reset");
    122  const promiseResetIdle = promiseExtensionEvent(
    123    extension,
    124    "background-script-reset-idle"
    125  );
    126  await extension.terminateBackground({ expectStopped: false });
    127  info("Wait for 'background-script-reset-idle' event to be emitted");
    128  await promiseResetIdle;
    129 
    130  stub.sendResponse("myResp");
    131 
    132  info("Wait for extension to verify sendNativeMessage response");
    133  await extension.awaitMessage("done");
    134  await extension.unload();
    135 
    136  stub.restore();
    137 });
    138 
    139 // verify that when an extension tab sends a native message,
    140 // the background will terminate as expected
    141 add_task(async function test_sendNativeMessage_tab() {
    142  const extension = ExtensionTestUtils.loadExtension({
    143    isPrivileged: true,
    144    manifest: {
    145      permissions: ["geckoViewAddons", "nativeMessaging"],
    146      background: { persistent: false },
    147    },
    148    async background() {
    149      browser.runtime.onSuspend.addListener(async () => {
    150        browser.test.sendMessage("onSuspend_called");
    151      });
    152    },
    153    files: {
    154      "tab.html": `
    155        <!DOCTYPE html><meta charset="utf-8">
    156        <script src="tab.js"></script>
    157      `,
    158      "tab.js": async () => {
    159        const res = await browser.runtime.sendNativeMessage("fake", "msg");
    160        browser.test.assertEq("myResp", res, "expected response");
    161        browser.test.sendMessage("content_done");
    162      },
    163    },
    164  });
    165 
    166  const stub = stubSendNativeMessage();
    167  await extension.startup();
    168 
    169  const tab = await ExtensionTestUtils.loadContentPage(
    170    `moz-extension://${extension.uuid}/tab.html?tab`,
    171    { extension }
    172  );
    173 
    174  info("Wait for sendNativeMessage to be received");
    175  Assert.equal(
    176    (await stub.firstCallPromise).deserialize({}),
    177    "msg",
    178    "expected message"
    179  );
    180 
    181  info("Terminate extension");
    182  await extension.terminateBackground();
    183  await extension.awaitMessage("onSuspend_called");
    184 
    185  stub.sendResponse("myResp");
    186 
    187  info("Wait for extension to verify sendNativeMessage response");
    188  await extension.awaitMessage("content_done");
    189  await tab.close();
    190  await extension.unload();
    191 
    192  stub.restore();
    193 });
    194 
    195 // verify that when a content script sends a native message,
    196 // the background will terminate as expected
    197 add_task(async function test_sendNativeMessage_content_script() {
    198  const extension = ExtensionTestUtils.loadExtension({
    199    isPrivileged: true,
    200    manifest: {
    201      permissions: [
    202        "geckoViewAddons",
    203        "nativeMessaging",
    204        "nativeMessagingFromContent",
    205      ],
    206      background: { persistent: false },
    207      content_scripts: [
    208        {
    209          run_at: "document_end",
    210          js: ["test.js"],
    211          matches: ["http://example.com/"],
    212        },
    213      ],
    214    },
    215    files: {
    216      "test.js": async () => {
    217        const res = await browser.runtime.sendNativeMessage("fake", "msg");
    218        browser.test.assertEq("myResp", res, "expected response");
    219        browser.test.sendMessage("content_done");
    220      },
    221    },
    222    async background() {
    223      browser.runtime.onSuspend.addListener(async () => {
    224        browser.test.sendMessage("onSuspend_called");
    225      });
    226    },
    227  });
    228 
    229  const stub = stubSendNativeMessage();
    230  await extension.startup();
    231 
    232  info("Load content page");
    233  const page = await ExtensionTestUtils.loadContentPage("http://example.com/");
    234 
    235  info("Wait for message from extension");
    236  Assert.equal(
    237    (await stub.firstCallPromise).deserialize({}),
    238    "msg",
    239    "expected message"
    240  );
    241 
    242  info("Terminate extension");
    243  await extension.terminateBackground();
    244  await extension.awaitMessage("onSuspend_called");
    245 
    246  stub.sendResponse("myResp");
    247 
    248  info("Wait for extension to verify sendNativeMessage response");
    249  await extension.awaitMessage("content_done");
    250  await page.close();
    251  await extension.unload();
    252 
    253  stub.restore();
    254 });
    255 
    256 // verify that when native messaging ports are open, the background will not be terminated
    257 // and once the ports disconnect, onSuspend can be called
    258 add_task(async function test_connectNative_event_page() {
    259  const extension = ExtensionTestUtils.loadExtension({
    260    isPrivileged: true,
    261    manifest: {
    262      permissions: ["geckoViewAddons", "nativeMessaging"],
    263      background: { persistent: false },
    264    },
    265    async background() {
    266      const port = browser.runtime.connectNative("test");
    267      port.onDisconnect.addListener(() => {
    268        browser.test.assertEq(
    269          null,
    270          port.error,
    271          "port should be disconnected without errors"
    272        );
    273        browser.test.sendMessage("port_disconnected");
    274      });
    275 
    276      browser.runtime.onSuspend.addListener(async () => {
    277        browser.test.sendMessage("onSuspend_called");
    278      });
    279    },
    280  });
    281 
    282  const stub = stubConnectNative();
    283  await extension.startup();
    284  info("Waiting for connectNative request");
    285  await stub.firstCallPromise;
    286 
    287  info("Trigger background script idle timeout and expect to be reset");
    288  const promiseResetIdle = promiseExtensionEvent(
    289    extension,
    290    "background-script-reset-idle"
    291  );
    292 
    293  await extension.terminateBackground({ expectStopped: false });
    294  info("Wait for 'background-script-reset-idle' event to be emitted");
    295  await promiseResetIdle;
    296 
    297  info("Trigger port disconnect, terminate background, and expect onSuspend()");
    298  stub.triggerPortDisconnect();
    299  await extension.awaitMessage("port_disconnected");
    300 
    301  info("Terminate extension");
    302  await extension.terminateBackground();
    303  await extension.awaitMessage("onSuspend_called");
    304 
    305  await extension.unload();
    306  stub.restore();
    307 });
    308 
    309 // verify that when an extension tab opens native messaging ports,
    310 // the background will terminate as expected
    311 add_task(async function test_connectNative_tab() {
    312  const extension = ExtensionTestUtils.loadExtension({
    313    isPrivileged: true,
    314    manifest: {
    315      permissions: ["geckoViewAddons", "nativeMessaging"],
    316      background: { persistent: false },
    317    },
    318    async background() {
    319      browser.runtime.onSuspend.addListener(async () => {
    320        browser.test.sendMessage("onSuspend_called");
    321      });
    322    },
    323    files: {
    324      "tab.html": `
    325        <!DOCTYPE html><meta charset="utf-8">
    326        <script src="tab.js"></script>
    327      `,
    328      "tab.js": async () => {
    329        const port = browser.runtime.connectNative("test");
    330        port.onDisconnect.addListener(() => {
    331          browser.test.assertEq(
    332            null,
    333            port.error,
    334            "port should be disconnected without errors"
    335          );
    336          browser.test.sendMessage("port_disconnected");
    337        });
    338        browser.test.sendMessage("content_done");
    339      },
    340    },
    341  });
    342 
    343  const stub = stubConnectNative();
    344  await extension.startup();
    345 
    346  const tab = await ExtensionTestUtils.loadContentPage(
    347    `moz-extension://${extension.uuid}/tab.html?tab`,
    348    { extension }
    349  );
    350  await extension.awaitMessage("content_done");
    351  await stub.firstCallPromise;
    352 
    353  info("Terminate extension");
    354  await extension.terminateBackground();
    355  await extension.awaitMessage("onSuspend_called");
    356 
    357  stub.triggerPortDisconnect();
    358  await extension.awaitMessage("port_disconnected");
    359  await tab.close();
    360  await extension.unload();
    361 
    362  stub.restore();
    363 });
    364 
    365 // verify that when a content script opens native messaging ports,
    366 // the background will terminate as expected
    367 add_task(async function test_connectNative_content_script() {
    368  const extension = ExtensionTestUtils.loadExtension({
    369    isPrivileged: true,
    370    manifest: {
    371      permissions: [
    372        "geckoViewAddons",
    373        "nativeMessaging",
    374        "nativeMessagingFromContent",
    375      ],
    376      background: { persistent: false },
    377      content_scripts: [
    378        {
    379          run_at: "document_end",
    380          js: ["test.js"],
    381          matches: ["http://example.com/"],
    382        },
    383      ],
    384    },
    385    files: {
    386      "test.js": async () => {
    387        const port = browser.runtime.connectNative("test");
    388        port.onDisconnect.addListener(() => {
    389          browser.test.assertEq(
    390            null,
    391            port.error,
    392            "port should be disconnected without errors"
    393          );
    394          browser.test.sendMessage("port_disconnected");
    395        });
    396        browser.test.sendMessage("content_done");
    397      },
    398    },
    399    async background() {
    400      browser.runtime.onSuspend.addListener(async () => {
    401        browser.test.sendMessage("onSuspend_called");
    402      });
    403    },
    404  });
    405 
    406  const stub = stubConnectNative();
    407  await extension.startup();
    408 
    409  info("Load content page");
    410  const page = await ExtensionTestUtils.loadContentPage("http://example.com/");
    411  await extension.awaitMessage("content_done");
    412  await stub.firstCallPromise;
    413 
    414  info("Terminate extension");
    415  await extension.terminateBackground();
    416  await extension.awaitMessage("onSuspend_called");
    417 
    418  stub.triggerPortDisconnect();
    419  await extension.awaitMessage("port_disconnected");
    420  await page.close();
    421  await extension.unload();
    422 
    423  stub.restore();
    424 });