forked from mirrors/gecko-dev
1111 lines
32 KiB
JavaScript
1111 lines
32 KiB
JavaScript
/* -*- 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(() => {
|
|
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(() => {
|
|
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(() => {
|
|
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();
|
|
}
|
|
);
|