Native messaging
Native messaging はユーザーのコンピューターにインストールされたアプリケーションと拡張機能との間のメッセージ交換を可能にします。 Native messaging を利用すれば、ネイティブアプリケーションが Web を介してアクセスできなくても拡張機能にサービスを提供できます。典型的な利用例としてはパスワードマネージャーが挙げられます。ネイティブアプリケーションはパスワードの暗号化と保管を行い、拡張機能と通信して Web フォームに入力を行うといったことが可能です。さらに、Native messaging を用いることで、一部のハードウェア等の WebExtension API ではアクセスできないリソースに対してアドオンからアクセスできるようになります。
対象となるネイティブアプリケーションは、ブラウザーを使用してインストールや管理を行うわけではありません。OS のインストール機構を使ってインストールします。ネイティブアプリケーションそのものに加えて、「ホストマニフェスト」または「アプリマニフェスト」と呼ばれる JSON ファイルを用意しなければなりません。アプリマニフェストファイルにはブラウザーからネイティブアプリケーションにアクセスするための方法を記述します。
Native messaging を利用する拡張機能は manifest.json の中で "nativeMessaging" permission を要求する必要があります。反対に、ネイティブアプリケーション側ではアプリマニフェストの "allowed_extensions" フィールドに拡張機能の ID を含めることで permission を認める必要があります。
それで拡張機能はruntime
API の関数セットを用いてネイティブアプリケーションと JSON メッセージを交換することができます。ネイティブアプリケーション側では標準入力 (stdin) を介してメッセージを受信し、標準出力 (stdout) を介してメッセージを送信します。
Native messaging のサポートは Chrome とほぼ互換性がありますが、主に 2 つの違いがあります。
- アプリマニフェストには
allowed_extensions
にアプリの ID の配列を記述します。 Chrome ではallowed_origins
に "chrome-extension" URL の配列を記述します。 - アプリマニフェストが Chrome とは別の場所に保管されます。
GitHub の "webextensions-examples" リポジトリの "native-messaging" ディレクトリーに完全な例があります。この記事におけるサンプルコードの大半は、この例から直接持ち込んでいます。
セットアップ
拡張機能の manifest
もし拡張機能をネイティブアプリケーションと通信させたい場合、
- "nativeMessaging" permission を manifest.json ファイルに設定する必要があります
- applications manifest キーを使用してアドオン ID を明示的に設定すべきです (これはアプリマニフェストが、そのアプリケーションへのアクセスが許可されている拡張機能かどうかを識別するために、ID を利用するためです)
以下に manifest.json の例を示します。
{
"description": "Native messaging example add-on",
"manifest_version": 2,
"name": "Native messaging example",
"version": "1.0",
"icons": {
"48": "icons/message.svg"
},
"applications": {
"gecko": {
"id": "ping_pong@example.org",
"strict_min_version": "50.0"
}
},
"background": {
"scripts": ["background.js"]
},
"browser_action": {
"default_icon": "icons/message.svg"
},
"permissions": ["nativeMessaging"]
}
App manifest
アプリマニフェストに、ブラウザーがネイティブアプリケーションに接続する方法を記述します。
アプリマニフェストファイルはネイティブアプリケーションと一緒にインストールする必要があります。ブラウザーはアプリマニフェストファイルを読み込み、検証を行いますが、インストールや管理は行いません。したがって、app manifest ファイルがインストール・アップデートされた時期や方法についてのセキュリティモデルは、WebExtension を使う拡張機能に対してのものというよりはネイティブアプリケーションに対してのものです。
native アプリマニフェストの文法と場所については、Native manifests を見てください。
例として、"ping_pong"ネイティブアプリケーションの manifest を以下に示します。
{
"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" という ID の 拡張機能において"ping_pong" という名前を runtime
API等に渡すことによる接続が許可されます。 アプリケーション自体は "/path/to/native-messaging/app/ping_pong.py" です。
Note for Windows: 上記の例におけるネイティブアプリケーションは 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" ]
}
バッチファイルから Python スクリプトを起動します。
@echo off
python -u "c:\\path\\to\\native-messaging\\app\\ping_pong.py"
メッセージの交換
上記のセットアップにより、拡張機能はネイティブアプリケーションと JSON メッセージを交換することができます。
拡張機能側
ネイティブメッセージはコンテンツスクリプトで直接使うことはできません; バックグラウンドスクリプトで間接的にやりとりする必要があります。
これを使うには2つのパターンがあります:ネクションベースのメッセージングとコネクションレスメッセージングです。
コネクションベースのメッセージング
このパターンでは、 runtime.connectNative() (en-US) を呼びだし、その時にアプリケーションの名前(アプリマニフェストの "name" プロパティの値)を渡します。既にアプリケーションが起動済みでなかった場合、これによってアプリケーションが起動し、runtime.Port (en-US) オブジェクトを拡張機能に返します。
ネイティブアプリは起動時に次の 2 つの引数を取ります:
- アプリマニフェストの完全パス
- (Firefox 55 以降で) 起動元のアドオンの ID (manifest.json の applications キーにて指定)
Chrome では引数の扱いが異なります:
- Linux と Macでは、Chrome は引数を、拡張機能が開始するオリジンを次の形:
chrome-extension://[extensionID]
で渡します。これによりアプリは拡張機能を識別できます。 - Windowsでは、Chrome は2つの引数を渡します、最初は拡張機能のオリジンで、2つ目はアプリを開始するChrome ネイティブウィンドウのハンドルです。
アプリケーションは 拡張機能が Port.disconnect()
を呼び出すか、接続されたページが閉じられるまで実行し続けます。
Port
を使用してメッセージを送信するためには、postMessage()
関数を呼び出し、 送信する JSON メッセージを渡します。Port
を使用してメッセージを受信するためには、onMessage.addListener()
関数を使用してリスナーを追加します。
"ping_pong" アプリケーションとコネクションを確立するバックグラウンドスクリプトの例を示します。アプリケーションからのメッセージを受信し、ユーザーがブラウザーアクションをクリックするたびに "ping" メッセージを送信します。
/*
On startup, connect to the "ping_pong" app.
*/
var 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() (en-US) を呼び、以下を渡します。
- アプリケーションの名前
- 送信する JSON メッセージ
- コールバック(オプション)
それぞれのメッセージごとに新しいアプリケーションのインスタンスが作成されます。アプリの開始時に次の 2 つの引数が渡されます:
- アプリマニフェストの完全パス
- (Firefox 55 以降で) 起動元のアドオンの ID (manifest.json の applications キーにて指定)
アプリからの最初のメッセージは 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");
var sending = browser.runtime.sendNativeMessage(
"ping_pong",
"ping");
sending.then(onResponse, onError);
});
アプリ側
アプリケーション側では、標準入力を用いてメッセージを受信し、標準出力を用いてメッセージを送信します。
各メッセージは JSON でシリアライズされ、UTF-8 でエンコードされ、メッセージ長を表す 32-bit の値がネイティブのバイト順で先頭に付加されます。
アプリケーションからの一つのメッセージの最大サイズは 1MB です。アプリケーションへの一つのメッセージの最大サイズは 4GB です。
次の NodeJS コードですぐにメッセージを送受信できます:
#!/usr/local/bin/node
process.stdin.on('readable', () => {
var input = []
var chunk
while (chunk = process.stdin.read()) {
input.push(chunk)
}
input = Buffer.concat(input)
var msgLen = input.readUInt32LE(0)
var dataLen = msgLen + 4
if (input.length >= dataLen) {
var content = input.slice(4, dataLen)
var json = JSON.parse(content.toString())
handleMessage(json)
}
})
function sendMessage(msg) {
var buffer = Buffer.from(JSON.stringify(msg))
var header = Buffer.alloc(4)
header.writeUInt32LE(buffer.length, 0)
var data = Buffer.concat([header, buffer])
process.stdout.write(data)
}
process.on('uncaughtException', (err) => {
sendMessage({error: err.toString()})
})
もうひとつ、Python による例を示します。このアプリケーションはアドオンからのメッセージを受信します。Linuxでは、このファイルを実行可能にしてください。メッセージが "ping" であった場合、"pong" というメッセージを返します。これはPython 2のバージョンです:
#!/usr/bin/python -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):
encoded_content = json.dumps(message_content)
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/python -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.buffer.read(4)
if not raw_length:
sys.exit(0)
message_length = struct.unpack('=I', raw_length)[0]
message = sys.stdin.buffer.read(message_length).decode("utf-8")
return json.loads(message)
# Encode a message for transmission, given its content.
def encode_message(message_content):
encoded_content = json.dumps(message_content).encode("utf-8")
encoded_length = struct.pack('=I', len(encoded_content))
# use struct.pack("10s", bytes), to pack a string of the length of 10 characters
return {'length': encoded_length, 'content': struct.pack(str(len(encoded_content))+"s",encoded_content)}
# Send an encoded message to stdout.
def send_message(encoded_message):
sys.stdout.buffer.write(encoded_message['length'])
sys.stdout.buffer.write(encoded_message['content'])
sys.stdout.buffer.flush()
while True:
message = get_message()
if message == "ping":
send_message(encode_message("pong"))
ネイティブアプリを閉じる
runtime.connectNative()
を使用してネイティブアプリケーションに接続した場合、アプリケーションは拡張機能が Port.disconnect()
を呼び出すか接続したページが閉じられるまで実行されます。runtime.sendNativeMessage()
を使用してネイティブアプリケーションの実行を開始した場合、アプリケーションはメッセージを受信してレスポンスを送信した後閉じられます。
ネイティブアプリケーションを閉じるために
- OS X や Linux のような *nix システムでは、ブラウザーはネイティブアプリケーションが正しく終了する機会を与えるために SIGTERM を送信し、その後 SIGKILL を送信します。これらのシグナルは新しいプロセスグループを作成して分けない限りすべてのサブプロセスに伝播します。
- Windows では、ブラウザーはネイティブアプリケーションのプロセスを Job object とし、ジョブを kill します。 ネイティブアプリケーションが追加でプロセスを立ち上げ、アプリケーション自体が kill された後もそのままにしたい場合、ネイティブアプリケーションは追加のプロセスを
CREATE_BREAKAWAY_FROM_JOB
フラグを立てて立ち上げる必要があります。
トラブルシューティング
もしうまくいかない場合、ブラウザーコンソールをチェックしてください。ネイティブアプリケーションが何かしらの出力を stderr に送っていた場合、ブラウザーはそれをブラウザーのコンソールにリダイレクトします。そのため、ネイティブアプリケーションが起動できている限り、出力されたエラーメッセージを確認することができます。
アプリケーションが起動できていなかった場合、問題の手がかりとなるエラーメッセージを確認してください。
"No such native application <name>"
-
runtime.connectNative()
に渡した名前がアプリマニフェスト中の名前と一致しているか確認してください - OS X/Linux: アプリマニフェストのファイル名が <name>.json となっていることを確認してください
- OS X/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" permission を持っているか確認してください
"[object Object] NativeMessaging.jsm:218"
- アプリケーションの開始に問題が発生しました
Chrome での非互換性
Connection-based messaging arguments
On Linux and Mac: Chrome passes one argument to the native app, which is the origin of the extension that started it, in the form: chrome-extension://«extensionID/»
(trailing slash required). This enables the app to identify the extension.
On Windows: Chrome passes two arguments:
- The origin of the extension
- A handle to the Chrome native window that started the app
allowed_extensions
- In Firefox: The manifest key is called
allowed_extensions
. - In Chrome: The manifest key is called
allowed_origins
instead.
App manifest location
- In Chrome: The app manifest is expected in a different place. See Native messaging host location in the Chrome docs.