Сообщения по родной схеме
Сообщения по родной схеме позволяют расширению обмениваться сообщениями с родным приложением, установленным на компьютере пользователя. Родные сообщения обслуживают расширения без дополнительного доступа к веб-ресурсам.
Управляющие паролями: родное приложение управляет, хранит и шифрует пароли. Затем родное приложение взаимодействует с расширением, чтобы заполнить веб-формы.
Сообщения по родной схеме также позволяют расширениям получать доступ к ресурсам, недоступным через API WebExtension (например, к определённому оборудованию).
Родное приложение не устанавливается и не управляется браузером. Родное приложение устанавливается с помощью механизмов установки операционной системы. Создайте JSON-файл, называемый «манифестом хоста» или «манифестом приложения». Установите JSON-файл в определённом месте. Файл манифеста приложения опишет, как браузер может подключиться к родному приложению.
Расширение должно запросить "nativeMessaging" разрешение или дополнительное разрешение в файле manifest.json. Кроме того, родное приложение должно предоставить разрешение для расширения, включив его идентификатор в поле "allowed_extensions" манифеста приложения.
После установки расширение может обмениваться JSON-сообщениями с родным приложением. Используйте набор функций в API runtime. В приложении на стороне родного приложения сообщения принимаются через стандартный ввод (stdin) и отправляются через стандартный вывод (stdout).
Поддержка родных сообщений в расширениях в основном совместима с Chrome, с двумя основными отличиями:
- В манифесте приложения перечислены
allowed_extensionsкак массив идентификаторов приложений, в то время как Chrome перечисляетallowed_origins, как массив"chrome-extension"URL-адресов. - Файл манифеста приложения хранится в другом месте по сравнению с Chrome.
Полный пример находится в «native-messaging» каталоге репозитория "webextensions-examples" на GitHub. Большая часть примеров кода в этой статье взята из этого примера.
Настройка
Манифест расширения
Расширение, взаимодействующее с родным приложением:
- Установите
"nativeMessaging"разрешение или дополнительное разрешение в файлеmanifest.json. - Явно укажите идентификатор своего дополнения. Используйте ключ манифеста
browser_specific_settings. (В манифесте приложения будет указан набор расширений, разрешающих подключение к идентификаторам).
Пример файла manifest.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. Для установки эквивалентного расширения WebExtension в Chrome вам потребуется использовать другой манифест без этого ключа. См. Несовместимость с Chrome ниже.
Примечание: При использовании дополнительного разрешения убедитесь, что разрешение предоставлено и при необходимости запросите разрешение у пользователя с помощью API permissions перед взаимодействием с родным приложением.
Манифест приложения
Манифест приложения описывает браузеру, как он может подключиться к родному приложению.
Файл манифеста приложения должен быть установлен вместе с родным приложением. Браузер читает и проверяет файлы манифеста приложения, но не устанавливает и не управляет ими. Модель безопасности для того, когда и как эти файлы устанавливаются и обновляются, больше похожа на модель для родных приложений, чем для расширений, использующих API WebExtension.
Подробную информацию о синтаксисе и расположении манифеста родного приложения см. в разделе Манифесты родных приложений.
Например, вот манифест для "ping_pong" родного приложения:
{ "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" ] }
Это позволяет расширению, идентификатор которого "ping_pong@example.org", подключаться, передавая имя "ping_pong" в соответствующую функцию API runtime. Само приложение находится по адресу "/path/to/native-messaging/app/ping_pong.py".
Примечание: Chrome определяет разрешённые расширения с другим ключом: allowed_origins, используя идентификатор WebExtension. Обратитесь к документации Chrome для получения дополнительной информации и см. Несовместимость с Chrome ниже.
Настройка Windows
В качестве примера вы также можете обратиться к файлу readme по расширению родных сообщений на GitHub. Если вы хотите проверить локальную настройку после того, как вы разделили этот репозиторий на компьютере под управлением Windows, вы можете запустить check_config_win.py для устранения неполадок.
Манифест приложения
В приведённом выше примере родное приложение представляет собой скрипт Python. Сложно заставить Windows надёжно выполнять скрипты Python таким образом, поэтому альтернативой является предоставление файла .bat и ссылка на него из манифеста приложения:
{ "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" ] }
(См. примечание выше о совместимости с Chrome относительно ключа allowed_extensions и его аналога в Chrome).
Файл пакетной обработки затем вызывает скрипт Python:
@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- Значение по умолчанию для этого ключа должно быть путём к файлу манифеста приложения: например,
C:\Users\<myusername>\webextensions-examples\native-messaging\app\ping_pong.json
- Значение по умолчанию для этого ключа должно быть путём к файлу манифеста приложения: например,
-
HKEY_LOCAL_MACHINE\Software\Mozilla\NativeMessagingHosts\ping_pong- Аналогично, значение по умолчанию для этого ключа должно быть путём к файлу манифеста приложения.
Примечание: Если вы используете пример с GitHub, пожалуйста, прочтите эту часть файла readme и проверьте вывод check_config_win.py перед установкой WebExtension в браузере.
Обмен сообщениями
При указанной выше настройке расширение может обмениваться JSON-сообщениями с родным приложением.
Сторона расширения
Родные сообщения напрямую нельзя использовать в скриптах контента. Вам необходимо сделать это косвенно через фоновые скрипты.
Здесь используются два подхода: обмен сообщениями на основе соединения и бесконнектный обмен сообщениями.
Обмен сообщениями на основе соединения
В этом подходе вы вызываете runtime.connectNative(), передавая имя приложения (значение свойства "name" в манифесте приложения). Это запускает приложение, если оно ещё не запущено, и возвращает расширению объект runtime.Port.
При запуске родному приложению передаются два аргумента:
- Полный путь к манифесту приложения.
- (новое в Firefox 55) идентификатор (как указано в ключе browser_specific_settings
manifest.jsonманифеста) дополнения, которое его запустило.
Примечание: Chrome обрабатывает переданные аргументы по-другому:
- В Linux и macOS Chrome передаёт один аргумент: происхождение расширения, которое его запустило (в виде
chrome-extension://[extensionID]). Это позволяет приложению идентифицировать расширение. - В Windows Chrome передаёт два аргумента: первый — происхождение расширения, второй — дескриптор родного окна Chrome, которое запустило приложение.
Приложение остаётся активным до тех пор, пока расширение не вызовет Port.disconnect() или страница, с которой оно было подключено, не будет закрыта.
Для отправки сообщений с помощью Port, вызовите его функцию postMessage(), передав JSON-сообщение для отправки. Для прослушивания сообщений с помощью Port, добавьте прослушиватель с помощью его функции onMessage.addListener().
Вот пример фонового скрипта, который устанавливает соединение с приложением "ping_pong", прослушивает сообщения от него, а затем отправляет ему сообщение "ping" всякий раз, когда пользователь нажимает на действие браузера:
/* 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) идентификатор (как указано в ключе browser_specific_settings манифеста) дополнения, которое его запустило.
Первое сообщение, отправленное приложением, обрабатывается как ответ на вызов sendNativeMessage() и будет передано в обратный вызов.
Вот пример выше, переписанный для использования runtime.sendNativeMessage():
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 МБ. Максимальный размер сообщения, отправленного приложению, составляет 4 ГБ.
Вы можете быстро начать отправлять и получать сообщения с помощью этого кода NodeJS:
#!/usr/local/bin/node (() => { let payloadSize = null; // A queue to store the chunks as we read them from stdin. // This queue can be flushed when `payloadSize` data has been read let chunks = []; // Only read the size once for each payload const sizeHasBeenRead = () => Boolean(payloadSize); // All the data has been read, reset everything for the next message const flushChunksQueue = () => { payloadSize = null; chunks.splice(0); }; const processData = () => { // Create one big buffer with all the chunks const stringData = Buffer.concat(chunks); // The browser will emit the size as a header of the payload, // if it hasn't been read yet, do it. // The next time we'll need to read the payload size is when all of the data // of the current payload has been read (i.e. data.length >= payloadSize + 4) if (!sizeHasBeenRead()) { payloadSize = stringData.readUInt32LE(0); } // If the data we have read so far is >= to the size advertised in the header, // it means we have all of the data sent. // We add 4 here because that's the size of the bytes that hold the payloadSize if (stringData.length >= (payloadSize + 4)) { // Remove the header const contentWithoutSize = stringData.slice(4, (payloadSize + 4)); // Reset the read size and the queued chunks flushChunksQueue(); const json = JSON.parse(contentWithoutSize); // Do something with the data… } }; process.stdin.on('readable', () => { // A temporary variable holding the nodejs.Buffer of each // chunk of data read off stdin let chunk = null; // Read all of the available data while ((chunk = process.stdin.read()) !== null) { chunks.push(chunk); } processData(); }); })();
Вот еще один пример, написанный на Python. Он слушает сообщения из расширения. Обратите внимание, что файл должен быть исполняемым в Linux. Если сообщение "ping", то он отвечает сообщением "pong".
Это версия Python 2:
#!/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.python.org/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 полученные двоичные данные должны быть декодированы в строку. Содержимое, которое должно быть отправлено обратно дополнению, должно быть закодировано в двоичные данные с помощью структуры:
#!/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.python.org/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(), то оно закрывается после получения сообщения и отправки ответа.
Чтобы закрыть приложение нативной платформе:
- В системах *nix, таких как macOS и Linux, браузер отправляет
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"
- Если вы видите это, значит, манифест приложения был найден успешно.
- Проверьте, что "путь" в манифесте приложения правильный.
- Windows: проверьте, что вы экранировали разделители путей (
"c:\\path\\to\\file"). - Проверьте, что приложение находится в месте, указанном свойством
"path"в манифесте приложения. - Проверьте, что приложение исполняемо.
"This extension does not have permission to use native application <name>"
- Проверьте, что ключ
"allowed_extensions"в манифесте приложения содержит идентификатор расширения."TypeError: browser.runtime.connectNative is not a function"
- Проверьте, что у расширения есть разрешение
"nativeMessaging"."[object Object] NativeMessaging.jsm:218"
- Возникла проблема при запуске приложения.
Несовместимости Chrome
Существует ряд различий между браузерами, которые влияют на нативное сообщение в расширениях веб-приложений, включая аргументы, передаваемые нативному приложению, местоположение файла манифеста и т. д. Эти различия обсуждаются в Несовместимости Chrome > Нативное сообщение.
© 2005–2023 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging