原生消息传递

原生消息传递使扩展能够与用户计算机上安装的原生应用程序交换消息。原生消息传递为扩展提供服务,无需通过 Web 进行其他访问。

密码管理器:原生应用程序管理、存储和加密密码。然后,原生应用程序与扩展通信以填充 Web 表单。

原生消息传递还使扩展能够访问 WebExtension API 无法访问的资源(例如,特定硬件)。

原生应用程序不是由浏览器安装或管理的。原生应用程序是使用底层操作系统的安装机制安装的。创建一个名为“主机清单”或“应用程序清单”的 JSON 文件。将 JSON 文件安装到定义的位置。应用程序清单文件将描述浏览器如何连接到原生应用程序。

扩展必须请求"nativeMessaging" 权限可选权限manifest.json文件中。此外,原生应用程序必须通过在应用程序清单的"allowed_extensions"字段中包含 ID 来授予扩展权限。

安装后,扩展可以与原生应用程序交换 JSON 消息。使用runtime API中的一组函数。在原生应用程序端,消息使用标准输入 (stdin) 接收,并使用标准输出 (stdout) 发送。

Application flow: the native app JSON file resides on the users computer, providing resource information to the native application. The read and write functions of the native application interact with the browser extension's runtime events.

扩展中对原生消息传递的支持与 Chrome 大致兼容,但存在两个主要差异

  • 应用程序清单将allowed_extensions列为应用程序 ID 的数组,而 Chrome 将allowed_origins列为"chrome-extension" URL 的数组。
  • 应用程序清单存储在不同的位置 与 Chrome 相比

在 GitHub 上的"webextensions-examples"存储库的"native-messaging"目录中有一个完整的示例。本文中的大多数示例代码都取自该示例。

设置

扩展清单

与原生应用程序通信的扩展

manifest.json文件示例

json
{
  "description": "Native messaging example add-on",
  "manifest_version": 2,
  "name": "Native messaging example",
  "version": "1.0",
  "icons": {
    "48": "icons/message.svg"
  },

  "browser_specific_settings": {
    "gecko": {
      "id": "[email protected]",
      "strict_min_version": "50.0"
    }
  },

  "background": {
    "scripts": ["background.js"]
  },

  "browser_action": {
    "default_icon": "icons/message.svg"
  },

  "permissions": ["nativeMessaging"]
}

注意:Chrome 不支持browser_specific_settings键。您需要使用没有此键的其他清单在 Chrome 上安装等效的 WebExtension。请参阅下面的 Chrome 不兼容性

注意:使用可选权限时,请检查权限是否已授予,并在必要时使用permissions API向用户请求权限,然后再与原生应用程序通信。

应用程序清单

应用程序清单向浏览器描述如何连接到原生应用程序。

应用程序清单文件必须与原生应用程序一起安装。浏览器读取和验证应用程序清单文件,但不会安装或管理它们。这些文件何时以及如何安装和更新的安全模型更类似于原生应用程序,而不是使用 WebExtension API 的扩展。

有关原生应用程序清单语法和位置的详细信息,请参阅原生清单

例如,以下是"ping_pong"原生应用程序的清单

json
{
  "name": "ping_pong",
  "description": "Example host for native messaging",
  "path": "/path/to/native-messaging/app/ping_pong.py",
  "type": "stdio",
  "allowed_extensions": ["[email protected]"]
}

这允许 ID 为"[email protected]"的扩展通过将名称"ping_pong"传递到相关的runtime API 函数进行连接。应用程序本身位于"/path/to/native-messaging/app/ping_pong.py"

注意:Chrome 使用另一个键识别允许的扩展:allowed_origins,使用 WebExtension 的 ID。请参阅Chrome 文档以了解更多详细信息,并参阅下面的 Chrome 不兼容性

Windows 设置

例如,您还可以参考GitHub 上原生消息传递扩展的自述文件。如果您想在 Windows 计算机上分叉此存储库后检查您的本地设置,您可以运行check_config_win.py以解决一些问题。

应用程序清单

在上面的示例中,原生应用程序是 Python 脚本。以这种方式让 Windows 可靠地运行 Python 脚本可能很困难,因此另一种方法是提供一个.bat文件,并从应用程序的清单链接到该文件

json
{
  "name": "ping_pong",
  "description": "Example host for native messaging",
  "path": "c:\\path\\to\\native-messaging\\app\\ping_pong_win.bat",
  "type": "stdio",
  "allowed_extensions": ["[email protected]"]
}

(请参阅上面关于Chrome 兼容性的注释,了解allowed_extensions键及其在 Chrome 中的对应项)。

然后,批处理文件调用 Python 脚本

bash
@echo off

python -u "c:\\path\\to\\native-messaging\\app\\ping_pong.py"

注册表

浏览器根据位于特定位置的注册表项查找扩展。您需要使用最终应用程序以编程方式添加它们,或者如果您使用的是 GitHub 中的示例,则手动添加它们。有关更多详细信息,请参阅清单位置

继续使用ping_pong示例,如果使用 Firefox(请参阅此 Chrome 页面),则应为消息传递创建两个注册表项之一以使其工作

  • HKEY_CURRENT_USER\Software\Mozilla\NativeMessagingHosts\ping_pong
  • HKEY_LOCAL_MACHINE\Software\Mozilla\NativeMessagingHosts\ping_pong

密钥的默认值应为应用程序清单的路径:例如C:\Users\<myusername>\webextensions-examples\native-messaging\app\ping_pong.json

注意:如果您基于 GitHub 上的示例进行工作,请阅读 自述文件中的这一部分,并在浏览器上安装 WebExtension 之前检查 check_config_win.py 的输出。

交换消息

在上述设置下,扩展程序可以与本机应用程序交换 JSON 消息。

扩展程序端

内容脚本无法直接使用本机消息传递。您必须 通过后台脚本间接进行操作

这里有两种模式可以使用:基于连接的消息传递无连接的消息传递

基于连接的消息传递

使用此模式,您调用 runtime.connectNative(),并传递应用程序的名称(应用程序清单中 "name" 属性的值)。如果应用程序尚未运行,则会启动它,并向扩展程序返回一个 runtime.Port 对象。

应用程序启动时会传递两个参数

  • 应用程序清单的完整路径。
  • (Firefox 55 中新增)启动它的加载项的 ID(如 browser_specific_settings manifest.json 键中所述)。

注意:Chrome 对传递的参数处理方式不同

  • 在 Linux 和 Mac 上,Chrome 传递 一个 参数:启动它的扩展程序的来源(格式为 chrome-extension://[extensionID])。这使应用程序能够识别扩展程序。
  • 在 Windows 上,Chrome 传递 两个 参数:第一个是扩展程序的来源,第二个是启动应用程序的 Chrome 本机窗口的句柄。

应用程序将一直运行,直到扩展程序调用 Port.disconnect() 或连接到它的页面关闭。

要使用 Port 发送消息,请调用其 postMessage() 函数,并传递要发送的 JSON 消息。要使用 Port 监听消息,请使用其 onMessage.addListener() 函数添加监听器。

以下是一个后台脚本示例,它与 "ping_pong" 应用程序建立连接,监听来自它的消息,并在用户点击浏览器操作时向它发送 "ping" 消息

js
/*
On startup, connect to the "ping_pong" app.
*/
let port = browser.runtime.connectNative("ping_pong");

/*
Listen for messages from the app.
*/
port.onMessage.addListener((response) => {
  console.log(`Received: ${response}`);
});

/*
On a click on the browser action, send the app a message.
*/
browser.browserAction.onClicked.addListener(() => {
  console.log("Sending:  ping");
  port.postMessage("ping");
});

无连接的消息传递

使用此模式,您调用 runtime.sendNativeMessage(),并传递以下内容:

  • 应用程序的名称
  • 要发送的 JSON 消息
  • 可选地,回调函数。

每个消息都会创建一个新的应用程序实例。应用程序在启动时传递两个参数

  • 应用程序清单的完整路径
  • (Firefox 55 中新增)启动它的加载项的 ID(如 browser_specific_settings manifest.json 键中所述)。

应用程序发送的第一条消息将被视为对 sendNativeMessage() 调用的响应,并将传递到回调函数中。

以下是上面示例的重写版本,使用 runtime.sendNativeMessage()

js
function onResponse(response) {
  console.log(`Received ${response}`);
}

function onError(error) {
  console.log(`Error: ${error}`);
}

/*
On a click on the browser action, send the app a message.
*/
browser.browserAction.onClicked.addListener(() => {
  console.log("Sending:  ping");
  let sending = browser.runtime.sendNativeMessage("ping_pong", "ping");
  sending.then(onResponse, onError);
});

应用程序端

在应用程序端,您使用标准输入接收消息,并使用标准输出发送消息。

每条消息都使用 JSON 进行序列化,UTF-8 编码,并在前面加上一个包含消息长度(以本机字节序表示)的无符号 32 位值。

应用程序发送的单个消息的最大大小为 1 MB。发送到应用程序的消息的最大大小为 4 GB。

您可以使用此 NodeJS 代码 nm_nodejs.mjs 快速开始发送和接收消息。

js
#!/usr/bin/env -S /full/path/to/node

import fs from "node:fs/promises";

async function getMessage() {
  const header = new Uint32Array(1);
  await readFullAsync(1, header);
  const message = await readFullAsync(header[0]);
  return message;
}

async function readFullAsync(length, buffer = new Uint8Array(65536)) {
  const data = [];
  while (data.length < length) {
    const input = await fs.open("/dev/stdin");
    const { bytesRead } = await input.read({ buffer });
    await input.close();
    if (bytesRead === 0) {
      break;
    }
    data.push(...buffer.subarray(0, bytesRead));
  }
  return new Uint8Array(data);
}

async function sendMessage(message) {
  const header = new Uint32Array([message.length]);
  const stdout = await fs.open(`/proc/${process.pid}/fd/1`, "w");
  await stdout.write(header);
  await stdout.write(message);
  await stdout.close();
}

while (true) {
  try {
    const message = await getMessage();
    await sendMessage(message);
  } catch (e) {
    console.error(e);
    process.exit(1);
  }
}

以下是用 Python 编写的另一个示例。它监听来自扩展程序的消息。请注意,该文件必须在 Linux 上可执行。如果消息为 "ping",则它将回复消息 "pong"

这是 Python 2 版本

python
#!/usr/bin/env -S python2 -u

# Note that running python with the `-u` flag is required on Windows,
# in order to ensure that stdin and stdout are opened in binary, rather
# than text, mode.

import json
import sys
import struct

# Read a message from stdin and decode it.
def get_message():
    raw_length = sys.stdin.read(4)
    if not raw_length:
        sys.exit(0)
    message_length = struct.unpack('=I', raw_length)[0]
    message = sys.stdin.read(message_length)
    return json.loads(message)

# Encode a message for transmission, given its content.
def encode_message(message_content):
    # https://docs.pythonlang.cn/3/library/json.html#basic-usage
    # To get the most compact JSON representation, you should specify
    # (',', ':') to eliminate whitespace.
    # We want the most compact representation because the browser rejects
    # messages that exceed 1 MB.
    encoded_content = json.dumps(message_content, separators=(',', ':'))
    encoded_length = struct.pack('=I', len(encoded_content))
    return {'length': encoded_length, 'content': encoded_content}

# Send an encoded message to stdout.
def send_message(encoded_message):
    sys.stdout.write(encoded_message['length'])
    sys.stdout.write(encoded_message['content'])
    sys.stdout.flush()

while True:
    message = get_message()
    if message == "ping":
        send_message(encode_message("pong"))

在 Python 3 中,接收到的二进制数据必须解码为字符串。要发送回加载项的内容必须使用结构体编码为二进制数据。

python
#!/usr/bin/env -S python3 -u

# Note that running python with the `-u` flag is required on Windows,
# in order to ensure that stdin and stdout are opened in binary, rather
# than text, mode.

import sys
import json
import struct

# Read a message from stdin and decode it.
def getMessage():
    rawLength = sys.stdin.buffer.read(4)
    if len(rawLength) == 0:
        sys.exit(0)
    messageLength = struct.unpack('@I', rawLength)[0]
    message = sys.stdin.buffer.read(messageLength).decode('utf-8')
    return json.loads(message)

# Encode a message for transmission,
# given its content.
def encodeMessage(messageContent):
    # https://docs.pythonlang.cn/3/library/json.html#basic-usage
    # To get the most compact JSON representation, you should specify
    # (',', ':') to eliminate whitespace.
    # We want the most compact representation because the browser rejects # messages that exceed 1 MB.
    encodedContent = json.dumps(messageContent, separators=(',', ':')).encode('utf-8')
    encodedLength = struct.pack('@I', len(encodedContent))
    return {'length': encodedLength, 'content': encodedContent}

# Send an encoded message to stdout
def sendMessage(encodedMessage):
    sys.stdout.buffer.write(encodedMessage['length'])
    sys.stdout.buffer.write(encodedMessage['content'])
    sys.stdout.buffer.flush()

while True:
    receivedMessage = getMessage()
    if receivedMessage == "ping":
        sendMessage(encodeMessage("pong"))

关闭原生应用程序

如果您使用 runtime.connectNative() 连接到本机应用程序,则它将一直运行,直到扩展程序调用 Port.disconnect() 或连接到它的页面关闭。如果您通过发送 runtime.sendNativeMessage() 启动了本机应用程序,则在它接收消息并发送响应后,它将关闭。

关闭本机应用程序

  • 在 macOS 和 Linux 等 *nix 系统上,浏览器会向本机应用程序发送 SIGTERM,然后在应用程序有机会优雅地退出后发送 SIGKILL。除非这些信号断开到新的进程组,否则它们会传播到任何子进程。
  • 在 Windows 上,浏览器会将本机应用程序的进程放入 作业对象 中并杀死该作业。如果本机应用程序启动了其他进程,并且希望在杀死本机应用程序后这些进程保持打开状态,则本机应用程序必须使用 CREATE_BREAKAWAY_FROM_JOB 标志启动其他进程,例如使用 CreateProcess

故障排除

如果出现问题,请检查 浏览器控制台。如果本机应用程序向 stderr 发送任何输出,浏览器将将其重定向到浏览器控制台。因此,如果您已经启动了本机应用程序,您将看到它发出的任何错误消息。

如果您未能运行应用程序,您应该会看到一条错误消息,提示您有关问题的信息。

"No such native application <name>"
  • 检查传递给 runtime.connectNative() 的名称是否与应用程序清单中的名称匹配
  • macOS/Linux:检查应用程序清单的名称是否为 <name>.json
  • macOS/Linux:检查本机应用程序的清单文件位置,如 此处 所述。
  • Windows:检查注册表项是否位于正确的位置,以及其名称是否与应用程序清单中的名称匹配。
  • Windows:检查注册表项中给出的路径是否指向应用程序清单。
    "Error: Invalid application <name>"
    
  • 检查应用程序的名称是否不包含无效字符。
    "'python' is not recognized as an internal or external command, ..."
    
  • Windows:如果您的应用程序是 Python 脚本,请检查您是否已安装 Python 并为其设置了路径。
    "File at path <path> does not exist, or is not executable"
    
  • 如果您看到此信息,则表示已成功找到应用程序清单。
  • 检查应用程序清单中的“path”是否正确。
  • Windows:检查您是否已转义路径分隔符("c:\\path\\to\\file")。
  • 检查应用程序是否位于应用程序清单中 "path" 属性指向的位置。
  • 检查应用程序是否可执行。
    "This extension does not have permission to use native application <name>"
    
  • 检查应用程序清单中的 "allowed_extensions" 键是否包含加载项的 ID。
        "TypeError: browser.runtime.connectNative is not a function"
    
  • 检查扩展程序是否具有 "nativeMessaging" 权限。
    "[object Object]       NativeMessaging.jsm:218"
    
  • 启动应用程序时出现问题。

Chrome 不兼容性

浏览器之间存在许多差异会影响 WebExtension 中的本机消息传递,包括传递给本机应用程序的参数、清单文件的位置等。这些差异在 Chrome 不兼容性 > 本机消息传递 中进行了讨论。