summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js')
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js1111
1 files changed, 1111 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js
new file mode 100644
index 0000000000..cb08a70151
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js
@@ -0,0 +1,1111 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* globals chrome */
+
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+const PREF_MAX_READ = "webextensions.native-messaging.max-input-message-bytes";
+const PREF_MAX_WRITE =
+ "webextensions.native-messaging.max-output-message-bytes";
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "42"
+);
+
+const server = createHttpServer({ hosts: ["example.com"] });
+
+server.registerPathHandler("/dummy", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ response.write("<!DOCTYPE html><html></html>");
+});
+
+const ECHO_BODY = String.raw`
+ import struct
+ import sys
+
+ stdin = getattr(sys.stdin, 'buffer', sys.stdin)
+ stdout = getattr(sys.stdout, 'buffer', sys.stdout)
+
+ while True:
+ rawlen = stdin.read(4)
+ if len(rawlen) == 0:
+ sys.exit(0)
+ msglen = struct.unpack('@I', rawlen)[0]
+ msg = stdin.read(msglen)
+
+ stdout.write(struct.pack('@I', msglen))
+ stdout.write(msg)
+`;
+
+const INFO_BODY = String.raw`
+ import json
+ import os
+ import struct
+ import sys
+
+ msg = json.dumps({"args": sys.argv, "cwd": os.getcwd()})
+ if sys.version_info >= (3,):
+ sys.stdout.buffer.write(struct.pack('@I', len(msg)))
+ else:
+ sys.stdout.write(struct.pack('@I', len(msg)))
+ sys.stdout.write(msg)
+ sys.exit(0)
+`;
+
+const DELAYED_ECHO_BODY = String.raw`
+ import atexit
+ import json
+ import os
+ import struct
+ import sys
+ import time
+
+ stdin = getattr(sys.stdin, 'buffer', sys.stdin)
+ stdout = getattr(sys.stdout, 'buffer', sys.stdout)
+ pid = os.getpid()
+
+ sys.stderr.write("nativeapp with pid %d is running\n" % pid)
+
+ def onexit():
+ sys.stderr.write("nativeapp with pid %d is exiting\n" % pid)
+
+ atexit.register(onexit)
+
+ while True:
+ rawlen = stdin.read(4)
+ if len(rawlen) == 0:
+ sys.exit(0)
+ msglen = struct.unpack('@I', rawlen)[0]
+ msg = stdin.read(msglen)
+
+ sys.stderr.write(
+ "nativeapp with pid %d delaying echoing message '%s'\n" %
+ (pid, str(msg, 'utf-8'))
+ )
+
+ time.sleep(5)
+ stdout.write(struct.pack('@I', msglen))
+ stdout.write(msg)
+
+ sys.stderr.write(
+ "nativeapp with pid %d replied to message '%s'\n" %
+ (pid, str(msg, 'utf-8'))
+ )
+`;
+
+const STDERR_LINES = ["hello stderr", "this should be a separate line"];
+let STDERR_MSG = STDERR_LINES.join("\\n");
+
+const STDERR_BODY = String.raw`
+ import sys
+ sys.stderr.write("${STDERR_MSG}")
+`;
+
+const PLATFORM_PATH_SEP = AppConstants.platform == "win" ? "\\" : "/";
+
+let SCRIPTS = [
+ {
+ name: "echo",
+ description: "a native app that echoes back messages it receives",
+ script: ECHO_BODY.replace(/^ {2}/gm, ""),
+ },
+ {
+ name: "relative.echo",
+ description: "a native app that echoes; relative path instead of absolute",
+ script: ECHO_BODY.replace(/^ {2}/gm, ""),
+ _hookModifyManifest(manifest) {
+ manifest.path = PathUtils.filename(manifest.path);
+ },
+ },
+ {
+ name: "relative_dotdot.echo",
+ description: "a native app that echos; relative path with dot dot",
+ script: ECHO_BODY.replace(/^ {2}/gm, ""),
+ _hookModifyManifest(manifest) {
+ // Set to ..\NativeMessagingHosts\relative_dotdot.bat (Windows)
+ manifest.path = [
+ "..",
+ ...manifest.path.split(PLATFORM_PATH_SEP).slice(-2),
+ ].join(PLATFORM_PATH_SEP);
+ },
+ },
+ {
+ name: "renamed.echo",
+ description: "invalid manifest due to name mismatch",
+ script: ECHO_BODY.replace(/^ {2}/gm, ""),
+ _hookModifyManifest(manifest) {
+ manifest.name = "renamed_name_mismatch";
+ },
+ },
+ {
+ name: "nonstdio.echo",
+ description: "invalid manifest due to non-stdio type",
+ script: ECHO_BODY.replace(/^ {2}/gm, ""),
+ _hookModifyManifest(manifest) {
+ // schema only permits "stdio" or "pkcs11". Change from "stdio":
+ manifest.type = "pkcs11";
+ },
+ },
+ {
+ name: "forwardslash.echo",
+ description: "a native app that echos; with forward slash in path",
+ script: ECHO_BODY.replace(/^ {2}/gm, ""),
+ _hookModifyManifest(manifest) {
+ // On Linux/macOS, this doesn't change anything.
+ // On Windows, this turns C:\Program Files\... in C:/Program Files/...
+ manifest.path = manifest.path.replaceAll("\\", "/");
+ },
+ },
+ {
+ name: "dot.echo",
+ description: "a native app that echos; with dot slash in path",
+ script: ECHO_BODY.replace(/^ {2}/gm, ""),
+ _hookModifyManifest(manifest) {
+ // Replace / with /./ (or \ with \.\ on Windows).
+ manifest.path = manifest.path.replaceAll(
+ PLATFORM_PATH_SEP,
+ PLATFORM_PATH_SEP + "." + PLATFORM_PATH_SEP
+ );
+ },
+ },
+ {
+ name: "dotdot.echo",
+ description: "a native app that echos; with dot dot slash in path",
+ script: ECHO_BODY.replace(/^ {2}/gm, ""),
+ _hookModifyManifest(manifest) {
+ // The binary is in a directory called "TYPE_SLUG". Turn
+ // /TYPE_SLUG/ in /TYPE_SLUG/../TYPE_SLUG/ to have equivalent directories
+ // that ought to be considered a valid absolute path.
+ const dirWithSlashes = PLATFORM_PATH_SEP + TYPE_SLUG + PLATFORM_PATH_SEP;
+ manifest.path = manifest.path.replace(
+ dirWithSlashes,
+ dirWithSlashes + ".." + dirWithSlashes
+ );
+ },
+ },
+ {
+ name: "delayedecho",
+ description:
+ "a native app that echo messages received with a small artificial delay",
+ script: DELAYED_ECHO_BODY.replace(/^ {2}/gm, ""),
+ },
+ {
+ name: "info",
+ description: "a native app that gives some info about how it was started",
+ script: INFO_BODY.replace(/^ {2}/gm, ""),
+ },
+ {
+ name: "stderr",
+ description: "a native app that writes to stderr and then exits",
+ script: STDERR_BODY.replace(/^ {2}/gm, ""),
+ },
+];
+
+if (AppConstants.platform == "win") {
+ SCRIPTS.push({
+ name: "echocmd",
+ description: "echo but using a .cmd file",
+ scriptExtension: "cmd",
+ script: ECHO_BODY.replace(/^ {2}/gm, ""),
+ });
+}
+
+add_setup(async function setup() {
+ optionalPermissionsPromptHandler.init();
+ optionalPermissionsPromptHandler.acceptPrompt = true;
+ await AddonTestUtils.promiseStartupManager();
+
+ await setupHosts(SCRIPTS);
+});
+
+// Test the basic operation of native messaging with a simple
+// script that echoes back whatever message is sent to it.
+add_task(async function test_happy_path() {
+ async function background() {
+ let port;
+ browser.test.onMessage.addListener(async (what, payload) => {
+ if (what == "request") {
+ await browser.permissions.request({ permissions: ["nativeMessaging"] });
+ // connectNative requires permission
+ port = browser.runtime.connectNative("echo");
+ port.onMessage.addListener(msg => {
+ browser.test.sendMessage("message", msg);
+ });
+ browser.test.sendMessage("ready");
+ } else if (what == "send") {
+ if (payload._json) {
+ let json = payload._json;
+ payload.toJSON = () => json;
+ delete payload._json;
+ }
+ port.postMessage(payload);
+ }
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ browser_specific_settings: { gecko: { id: ID } },
+ optional_permissions: ["nativeMessaging"],
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ await withHandlingUserInput(extension, async () => {
+ extension.sendMessage("request");
+ await extension.awaitMessage("ready");
+ });
+ const tests = [
+ {
+ data: "this is a string",
+ what: "simple string",
+ },
+ {
+ data: "Это юникода",
+ what: "unicode string",
+ },
+ {
+ data: { test: "hello" },
+ what: "simple object",
+ },
+ {
+ data: {
+ what: "An object with a few properties",
+ number: 123,
+ bool: true,
+ nested: { what: "another object" },
+ },
+ what: "object with several properties",
+ },
+
+ {
+ data: {
+ ignoreme: true,
+ _json: { data: "i have a tojson method" },
+ },
+ expected: { data: "i have a tojson method" },
+ what: "object with toJSON() method",
+ },
+ ];
+ for (let test of tests) {
+ extension.sendMessage("send", test.data);
+ let response = await extension.awaitMessage("message");
+ let expected = test.expected || test.data;
+ deepEqual(response, expected, `Echoed a message of type ${test.what}`);
+ }
+
+ let procCount = await getSubprocessCount();
+ equal(procCount, 1, "subprocess is still running");
+ let exitPromise = waitForSubprocessExit();
+ await extension.unload();
+ await exitPromise;
+});
+
+// Just test that the given app (which should be the echo script above)
+// can be started. Used to test corner cases in how the native application
+// is located/launched.
+async function simpleTest(app) {
+ function background(appname) {
+ let port = browser.runtime.connectNative(appname);
+ let MSG = "test";
+ port.onMessage.addListener(msg => {
+ browser.test.assertEq(MSG, msg, "Got expected message back");
+ browser.test.sendMessage("done");
+ });
+ port.postMessage(MSG);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: `(${background})(${JSON.stringify(app)});`,
+ manifest: {
+ browser_specific_settings: { gecko: { id: ID } },
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+
+ let procCount = await getSubprocessCount();
+ equal(procCount, 1, "subprocess is still running");
+ let exitPromise = waitForSubprocessExit();
+ await extension.unload();
+ await exitPromise;
+}
+
+async function testBrokenApp({
+ extensionId = ID,
+ appname,
+ expectedError,
+ expectedConsoleMessages,
+}) {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.test.onMessage.addListener(async (appname, expectedError) => {
+ await browser.test.assertRejects(
+ browser.runtime.sendNativeMessage(appname, "dummymsg"),
+ expectedError,
+ "Expected sendNativeMessage error"
+ );
+ browser.test.sendMessage("done");
+ });
+ },
+ manifest: {
+ browser_specific_settings: { gecko: { id: extensionId } },
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ await extension.startup();
+ let { messages } = await promiseConsoleOutput(async () => {
+ extension.sendMessage(appname, expectedError);
+ await extension.awaitMessage("done");
+ });
+ await extension.unload();
+
+ let procCount = await getSubprocessCount();
+ equal(procCount, 0, "No child process was started");
+
+ // Because we're using forbidUnexpected:true below, we have to account for
+ // all logged messages. RemoteSettings may try (and fail) to load remote
+ // settings - ignore the "NetworkError: Network request failed" error.
+ // To avoid having to update this filter all the time, select the specific
+ // modules relevant to native messaging from where we expect errors.
+ messages = messages.filter(m => {
+ return /NativeMessaging|NativeManifests|Subprocess/.test(m.message);
+ });
+
+ // On Linux/macOS, the setupHosts helper registers the same manifest file in
+ // multiple locations, which can result in the same error being printed
+ // multiple times. We de-duplicate that here.
+ let deduplicatedMessages = messages.filter(
+ (msg, i) => i === messages.findIndex(m => m.message === msg.message)
+ );
+
+ // Now check that all the log messages exist, in the expected order too.
+ AddonTestUtils.checkMessages(
+ deduplicatedMessages,
+ {
+ expected: expectedConsoleMessages.map(message => ({ message })),
+ forbidUnexpected: true,
+ },
+ "Expected messages in the console"
+ );
+}
+
+if (AppConstants.platform == "win") {
+ // "relative.echo" has a relative path in the host manifest.
+ add_task(function test_relative_path() {
+ // Note: relative paths only supported on Windows.
+ // For non-Windows, see test_relative_path_unsupported instead.
+ return simpleTest("relative.echo");
+ });
+
+ add_task(function test_relative_dotdot_path() {
+ // Note: relative paths only supported on Windows.
+ // For non-Windows, see test_relative_dotdot_path_unsupported instead.
+ return simpleTest("relative_dotdot.echo");
+ });
+
+ // "echocmd" uses a .cmd file instead of a .bat file
+ add_task(function test_cmd_file() {
+ return simpleTest("echocmd");
+ });
+} else {
+ // On non-Windows, relative paths are not supported.
+ add_task(function test_relative_path_unsupported() {
+ return testBrokenApp({
+ appname: "relative.echo",
+ expectedError: "An unexpected error occurred",
+ expectedConsoleMessages: [
+ /NativeApp requires absolute path to command on this platform/,
+ ],
+ });
+ });
+ add_task(function test_relative_dotdot_path_unsupported() {
+ return testBrokenApp({
+ appname: "relative_dotdot.echo",
+ expectedError: "An unexpected error occurred",
+ expectedConsoleMessages: [
+ /NativeApp requires absolute path to command on this platform/,
+ ],
+ });
+ });
+}
+
+add_task(async function test_absolute_path_dot_one() {
+ return simpleTest("dot.echo");
+});
+
+add_task(async function test_absolute_path_dotdot() {
+ return simpleTest("dotdot.echo");
+});
+
+add_task(async function test_error_name_mismatch() {
+ await testBrokenApp({
+ appname: "renamed.echo",
+ expectedError: "No such native application renamed.echo",
+ expectedConsoleMessages: [
+ /Native manifest .+ has name property renamed_name_mismatch \(expected renamed\.echo\)/,
+ /No such native application renamed\.echo/,
+ ],
+ });
+});
+
+add_task(async function test_invalid_manifest_type_not_stdio() {
+ await testBrokenApp({
+ appname: "nonstdio.echo",
+ expectedError: "No such native application nonstdio.echo",
+ expectedConsoleMessages: [
+ /Native manifest .+ has type property pkcs11 \(expected stdio\)/,
+ /No such native application nonstdio\.echo/,
+ ],
+ });
+});
+
+add_task(async function test_forward_slashes_in_path_works() {
+ await simpleTest("forwardslash.echo");
+});
+
+// Test sendNativeMessage()
+add_task(async function test_sendNativeMessage() {
+ async function background() {
+ let MSG = { test: "hello world" };
+
+ // Check error handling
+ await browser.test.assertRejects(
+ browser.runtime.sendNativeMessage("nonexistent", MSG),
+ "No such native application nonexistent",
+ "sendNativeMessage() to a nonexistent app failed"
+ );
+
+ // Check regular message exchange
+ let reply = await browser.runtime.sendNativeMessage("echo", MSG);
+
+ let expected = JSON.stringify(MSG);
+ let received = JSON.stringify(reply);
+ browser.test.assertEq(expected, received, "Received echoed native message");
+
+ browser.test.sendMessage("finished");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ browser_specific_settings: { gecko: { id: ID } },
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("finished");
+
+ // With sendNativeMessage(), the subprocess should be disconnected
+ // after exchanging a single message.
+ await waitForSubprocessExit();
+
+ await extension.unload();
+});
+
+// Test calling Port.disconnect()
+add_task(async function test_disconnect() {
+ function background() {
+ let port = browser.runtime.connectNative("echo");
+ port.onMessage.addListener((msg, msgPort) => {
+ browser.test.assertEq(
+ port,
+ msgPort,
+ "onMessage handler should receive the port as the second argument"
+ );
+ browser.test.sendMessage("message", msg);
+ });
+ port.onDisconnect.addListener(msgPort => {
+ browser.test.fail("onDisconnect should not be called for disconnect()");
+ });
+ browser.test.onMessage.addListener((what, payload) => {
+ if (what == "send") {
+ if (payload._json) {
+ let json = payload._json;
+ payload.toJSON = () => json;
+ delete payload._json;
+ }
+ port.postMessage(payload);
+ } else if (what == "disconnect") {
+ try {
+ port.disconnect();
+ browser.test.assertThrows(
+ () => port.postMessage("void"),
+ "Attempt to postMessage on disconnected port"
+ );
+ browser.test.sendMessage("disconnect-result", { success: true });
+ } catch (err) {
+ browser.test.sendMessage("disconnect-result", {
+ success: false,
+ errmsg: err.message,
+ });
+ }
+ }
+ });
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ browser_specific_settings: { gecko: { id: ID } },
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ extension.sendMessage("send", "test");
+ let response = await extension.awaitMessage("message");
+ equal(response, "test", "Echoed a string");
+
+ let procCount = await getSubprocessCount();
+ equal(procCount, 1, "subprocess is running");
+
+ extension.sendMessage("disconnect");
+ response = await extension.awaitMessage("disconnect-result");
+ equal(response.success, true, "disconnect succeeded");
+
+ info("waiting for subprocess to exit");
+ await waitForSubprocessExit();
+ procCount = await getSubprocessCount();
+ equal(procCount, 0, "subprocess is no longer running");
+
+ extension.sendMessage("disconnect");
+ response = await extension.awaitMessage("disconnect-result");
+ equal(response.success, true, "second call to disconnect silently ignored");
+
+ await extension.unload();
+});
+
+// Test the limit on message size for writing
+add_task(async function test_write_limit() {
+ Services.prefs.setIntPref(PREF_MAX_WRITE, 10);
+ function clearPref() {
+ Services.prefs.clearUserPref(PREF_MAX_WRITE);
+ }
+ registerCleanupFunction(clearPref);
+
+ function background() {
+ const PAYLOAD = "0123456789A";
+ let port = browser.runtime.connectNative("echo");
+ try {
+ port.postMessage(PAYLOAD);
+ browser.test.sendMessage("result", null);
+ } catch (ex) {
+ browser.test.sendMessage("result", ex.message);
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ browser_specific_settings: { gecko: { id: ID } },
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ await extension.startup();
+
+ let errmsg = await extension.awaitMessage("result");
+ notEqual(
+ errmsg,
+ null,
+ "native postMessage() failed for overly large message"
+ );
+
+ await extension.unload();
+ await waitForSubprocessExit();
+
+ clearPref();
+});
+
+// Test the limit on message size for reading
+add_task(async function test_read_limit() {
+ Services.prefs.setIntPref(PREF_MAX_READ, 10);
+ function clearPref() {
+ Services.prefs.clearUserPref(PREF_MAX_READ);
+ }
+ registerCleanupFunction(clearPref);
+
+ function background() {
+ const PAYLOAD = "0123456789A";
+ let port = browser.runtime.connectNative("echo");
+ port.onDisconnect.addListener(msgPort => {
+ browser.test.assertEq(
+ port,
+ msgPort,
+ "onDisconnect handler should receive the port as the first argument"
+ );
+ browser.test.assertEq(
+ "Native application tried to send a message of 13 bytes, which exceeds the limit of 10 bytes.",
+ port.error && port.error.message
+ );
+ browser.test.sendMessage("result", "disconnected");
+ });
+ port.onMessage.addListener(msg => {
+ browser.test.sendMessage("result", "message");
+ });
+ port.postMessage(PAYLOAD);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ browser_specific_settings: { gecko: { id: ID } },
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ await extension.startup();
+
+ let result = await extension.awaitMessage("result");
+ equal(
+ result,
+ "disconnected",
+ "native port disconnected on receiving large message"
+ );
+
+ await extension.unload();
+ await waitForSubprocessExit();
+
+ clearPref();
+});
+
+// Test that an extension without the nativeMessaging permission cannot
+// use native messaging.
+add_task(async function test_ext_permission() {
+ function background() {
+ browser.test.assertEq(
+ chrome.runtime.connectNative,
+ undefined,
+ "chrome.runtime.connectNative does not exist without nativeMessaging permission"
+ );
+ browser.test.assertEq(
+ browser.runtime.connectNative,
+ undefined,
+ "browser.runtime.connectNative does not exist without nativeMessaging permission"
+ );
+ browser.test.assertEq(
+ chrome.runtime.sendNativeMessage,
+ undefined,
+ "chrome.runtime.sendNativeMessage does not exist without nativeMessaging permission"
+ );
+ browser.test.assertEq(
+ browser.runtime.sendNativeMessage,
+ undefined,
+ "browser.runtime.sendNativeMessage does not exist without nativeMessaging permission"
+ );
+ browser.test.sendMessage("finished");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {},
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("finished");
+ await extension.unload();
+});
+
+// Test that an extension that is not listed in allowed_extensions for
+// a native application cannot use that application.
+add_task(async function test_app_permission() {
+ await testBrokenApp({
+ extensionId: "@id-that-is-not-in-the-allowed_extensions-list",
+ appname: "echo",
+ expectedError: "No such native application echo",
+ expectedConsoleMessages: [
+ /This extension does not have permission to use native manifest .+echo\.json/,
+ /No such native application echo/,
+ ],
+ });
+});
+
+// Test that the command-line arguments and working directory for the
+// native application are as expected.
+add_task(async function test_child_process() {
+ function background() {
+ let port = browser.runtime.connectNative("info");
+ port.onMessage.addListener(msg => {
+ browser.test.sendMessage("result", msg);
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ browser_specific_settings: { gecko: { id: ID } },
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ await extension.startup();
+
+ let msg = await extension.awaitMessage("result");
+ equal(msg.args.length, 3, "Received two command line arguments");
+ equal(
+ msg.args[1],
+ getPath("info.json"),
+ "Command line argument is the path to the native host manifest"
+ );
+ equal(
+ msg.args[2],
+ ID,
+ "Second command line argument is the ID of the calling extension"
+ );
+ equal(
+ msg.cwd.replace(/^\/private\//, "/"),
+ PathUtils.join(tmpDir.path, TYPE_SLUG),
+ "Working directory is the directory containing the native appliation"
+ );
+
+ let exitPromise = waitForSubprocessExit();
+ await extension.unload();
+ await exitPromise;
+});
+
+add_task(async function test_stderr() {
+ function background() {
+ let port = browser.runtime.connectNative("stderr");
+ port.onDisconnect.addListener(msgPort => {
+ browser.test.assertEq(
+ port,
+ msgPort,
+ "onDisconnect handler should receive the port as the first argument"
+ );
+ browser.test.assertEq(
+ null,
+ port.error,
+ "Normal application exit is not an error"
+ );
+ browser.test.sendMessage("finished");
+ });
+ }
+
+ let { messages } = await promiseConsoleOutput(async function () {
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ browser_specific_settings: { gecko: { id: ID } },
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("finished");
+ await extension.unload();
+
+ await waitForSubprocessExit();
+ });
+
+ let lines = STDERR_LINES.map(line =>
+ messages.findIndex(msg => msg.message.includes(line))
+ );
+ notEqual(lines[0], -1, "Saw first line of stderr output on the console");
+ notEqual(lines[1], -1, "Saw second line of stderr output on the console");
+ notEqual(
+ lines[0],
+ lines[1],
+ "Stderr output lines are separated in the console"
+ );
+});
+
+// Test that calling connectNative() multiple times works
+// (see bug 1313980 for a previous regression in this area)
+add_task(async function test_multiple_connects() {
+ async function background() {
+ function once() {
+ return new Promise(resolve => {
+ let MSG = "hello";
+ let port = browser.runtime.connectNative("echo");
+
+ port.onMessage.addListener(msg => {
+ browser.test.assertEq(MSG, msg, "Got expected message back");
+ port.disconnect();
+ resolve();
+ });
+ port.postMessage(MSG);
+ });
+ }
+
+ await once();
+ await once();
+ browser.test.notifyPass("multiple-connect");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ browser_specific_settings: { gecko: { id: ID } },
+ permissions: ["nativeMessaging"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("multiple-connect");
+ await extension.unload();
+});
+
+// Test that native messaging is always rejected on content scripts
+add_task(async function test_connect_native_from_content_script() {
+ async function testScript() {
+ let port = browser.runtime.connectNative("echo");
+ port.onDisconnect.addListener(msgPort => {
+ browser.test.assertEq(
+ port,
+ msgPort,
+ "onDisconnect handler should receive the port as the first argument"
+ );
+ browser.test.assertEq(
+ "An unexpected error occurred",
+ port.error && port.error.message
+ );
+ browser.test.sendMessage("result", "disconnected");
+ });
+ port.onMessage.addListener(msg => {
+ browser.test.sendMessage("result", "message");
+ });
+ port.postMessage({ test: "test" });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ run_at: "document_end",
+ js: ["test.js"],
+ matches: ["http://example.com/dummy"],
+ },
+ ],
+ browser_specific_settings: { gecko: { id: ID } },
+ permissions: ["nativeMessaging"],
+ },
+ files: {
+ "test.js": testScript,
+ },
+ });
+
+ await extension.startup();
+
+ const page = await ExtensionTestUtils.loadContentPage(
+ "http://example.com/dummy"
+ );
+
+ let result = await extension.awaitMessage("result");
+ equal(result, "disconnected", "connectNative() failed from content script");
+
+ await page.close();
+ await extension.unload();
+
+ let procCount = await getSubprocessCount();
+ equal(procCount, 0, "No child process was started");
+});
+
+// Testing native app messaging against idle timeout.
+async function startupExtensionAndRequestPermission() {
+ const extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ browser_specific_settings: { gecko: { id: ID } },
+ optional_permissions: ["nativeMessaging"],
+ background: { persistent: false },
+ },
+ async background() {
+ browser.runtime.onSuspend.addListener(() => {
+ browser.test.sendMessage("bgpage:suspending");
+ });
+
+ let port;
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ switch (msg) {
+ case "request-permission": {
+ await browser.permissions.request({
+ permissions: ["nativeMessaging"],
+ });
+ break;
+ }
+ case "delayedecho-sendmessage": {
+ browser.runtime
+ .sendNativeMessage("delayedecho", args[0])
+ .then(msg =>
+ browser.test.sendMessage(
+ `delayedecho-sendmessage:got-reply`,
+ msg
+ )
+ );
+ break;
+ }
+ case "connectNative": {
+ if (port) {
+ browser.test.fail(`Unexpected already connected NativeApp port`);
+ } else {
+ port = browser.runtime.connectNative("echo");
+ }
+ break;
+ }
+ case "disconnectNative": {
+ if (!port) {
+ browser.test.fail(`Unexpected undefined NativeApp port`);
+ }
+ port?.disconnect();
+ break;
+ }
+ default:
+ browser.test.fail(`Got an unexpected test message: ${msg}`);
+ }
+
+ browser.test.sendMessage(`${msg}:done`);
+ });
+ browser.test.sendMessage("bg:ready");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("bg:ready");
+ const contextId = extension.extension.backgroundContext.contextId;
+ notEqual(contextId, undefined, "Got a contextId for the background context");
+
+ await withHandlingUserInput(extension, async () => {
+ extension.sendMessage("request-permission");
+ await extension.awaitMessage("request-permission:done");
+ });
+
+ return { extension, contextId };
+}
+
+async function expectTerminateBackgroundToResetIdle({ extension, contextId }) {
+ info("Wait for hasActiveNativeAppPorts to become true");
+ await TestUtils.waitForCondition(
+ () => extension.extension.backgroundContext,
+ "Parent proxy context should be active"
+ );
+
+ await TestUtils.waitForCondition(
+ () => extension.extension.backgroundContext?.hasActiveNativeAppPorts,
+ "Parent proxy context should have active native app ports tracked"
+ );
+
+ clearHistograms();
+ assertHistogramEmpty(WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT);
+ assertKeyedHistogramEmpty(WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT_BY_ADDONID);
+
+ info("Trigger background script idle timeout and expect to be reset");
+ const promiseResetIdle = promiseExtensionEvent(
+ extension,
+ "background-script-reset-idle"
+ );
+ await extension.terminateBackground();
+ info("Wait for 'background-script-reset-idle' event to be emitted");
+ await promiseResetIdle;
+ equal(
+ extension.extension.backgroundContext.contextId,
+ contextId,
+ "Initial background context is still available as expected"
+ );
+
+ assertHistogramCategoryNotEmpty(WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT, {
+ category: "reset_nativeapp",
+ categories: HISTOGRAM_EVENTPAGE_IDLE_RESULT_CATEGORIES,
+ });
+
+ assertHistogramCategoryNotEmpty(
+ WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT_BY_ADDONID,
+ {
+ keyed: true,
+ key: extension.id,
+ category: "reset_nativeapp",
+ categories: HISTOGRAM_EVENTPAGE_IDLE_RESULT_CATEGORIES,
+ }
+ );
+}
+
+async function testSendNativeMessage({ extension, contextId }) {
+ extension.sendMessage("delayedecho-sendmessage", "delayed-echo");
+ await extension.awaitMessage("delayedecho-sendmessage:done");
+
+ await expectTerminateBackgroundToResetIdle({ extension, contextId });
+
+ // We expect exactly two replies (one for the previous queued message
+ // and one more for the last message sent right above).
+ equal(
+ await extension.awaitMessage("delayedecho-sendmessage:got-reply"),
+ "delayed-echo",
+ "Got the expected reply for the first message sent"
+ );
+
+ await TestUtils.waitForCondition(
+ () => !extension.extension.backgroundContext?.hasActiveNativeAppPorts,
+ "Parent proxy context should not have any active native app ports tracked"
+ );
+
+ info("terminating the background script");
+ await extension.terminateBackground();
+ info("wait for runtime.onSuspend listener to have been called");
+ await extension.awaitMessage("bgpage:suspending");
+}
+
+async function testConnectNative({ extension, contextId }) {
+ extension.sendMessage("connectNative");
+ await extension.awaitMessage("connectNative:done");
+
+ await expectTerminateBackgroundToResetIdle({ extension, contextId });
+
+ // Disconnect the NativeApp and confirm that the background page
+ // will be suspending as expected.
+ extension.sendMessage("disconnectNative");
+ await extension.awaitMessage("disconnectNative:done");
+
+ await TestUtils.waitForCondition(
+ () => !extension.extension.backgroundContext?.hasActiveNativeAppPorts,
+ "Parent proxy context should not have any active native app ports tracked"
+ );
+
+ info("terminating the background script");
+ await extension.terminateBackground();
+ info("wait for runtime.onSuspend listener to have been called");
+ await extension.awaitMessage("bgpage:suspending");
+}
+
+add_task(
+ {
+ pref_set: [["extensions.eventPages.enabled", true]],
+ },
+ async function test_pending_sendNativeMessageReply_resets_bgscript_idle_timeout() {
+ const { extension, contextId } =
+ await startupExtensionAndRequestPermission();
+ await testSendNativeMessage({ extension, contextId });
+ await waitForSubprocessExit();
+ await extension.unload();
+ }
+);
+
+add_task(
+ {
+ pref_set: [["extensions.eventPages.enabled", true]],
+ },
+ async function test_open_connectNativePort_resets_bgscript_idle_timeout() {
+ const { extension, contextId } =
+ await startupExtensionAndRequestPermission();
+ await testConnectNative({ extension, contextId });
+ await waitForSubprocessExit();
+ await extension.unload();
+ }
+);