原生消息传递

原生消息使扩展能够与安装在用户计算机上的原生应用程序交换消息。原生消息服务于不需要额外 Web 访问的扩展。

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

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

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

扩展必须在 manifest.json 文件中请求 "nativeMessaging" 权限可选权限。此外,原生应用程序必须通过在应用程序清单的 "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": "ping_pong@example.org",
      "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": ["ping_pong@example.org"]
}

这允许 ID 为 "ping_pong@example.org" 的扩展通过将名称 "ping_pong" 传递给相关的 runtime API 函数来连接。应用程序本身位于 "/path/to/native-messaging/app/ping_pong.py"

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

Windows 设置

作为示例,您还可以参考 GitHub 上原生消息扩展的 readme。如果您在 Windows 机器上 fork 此存储库后想检查本地设置,可以运行 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": ["ping_pong@example.org"]
}

(请参阅上面关于 allowed_extensions 键及其在 Chrome 中的对应项的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 上的示例进行工作,请阅读readme 的这一部分,并在浏览器上安装 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 = Buffer.from(new Uint32Array([message.length]).buffer);
  const stdout = process.stdout;
  await stdout.write(header);
  await stdout.write(message);
}

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 等类 Unix 系统上,浏览器会向原生应用程序发送 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 不兼容性

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