この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
Introduction
Native messagingとは、ユーザーのPCにインストールされたアプリと
Webブラウザの拡張機能間でメッセージ交換を可能にする機能です。
この機能を使えば、ネイティブアプリケーションが
Web経由でアクセスできなくても拡張機能にサービスを提供できます。
例えば、ローカルアプリがパスワードの暗号化と保管を行い、
拡張機能へパスワードを送信してフォームに自動設定、みたいなことも可能です。
また、拡張機能からは通常アクセスできないリソースに対しても実質的にアクセス可能になります。
本稿ではGoogle Chromeの拡張機能(extention)と
ローカルに用意したNodeプログラムでプロセス間通信をためしてみます。
※現状ではChrome以外でもEdge/Firefox/Safariなど主要なプラウザで使用可能
Environment
- OS : MacOS 10.15.7
- Node : v18.3.0
- Chrome : 103.0.5060.114
Native Massage Architecture
ローカルアプリとextentionで通信するためのNative Message機能ですが、
仕組みは特に難しくありません。
↓の図のように、ブラウザの拡張機能とローカルアプリをmanifest.jsonで関連付け、
chromeのapiと標準入出力でメッセージのやりとりをします。
このmanifest.json(拡張機能のmanifest.jsonとは別)が重要で、
ここでアクセスを許可する拡張機能のIDを指定や、
拡張機能からアクセスするための名前を指定します。
ローカルのアプリはそのまま実行できる形式であればOKで、
exe(Windows)形式やpythonのファイルなどが指定可能です。
今回はshebangをつけたjsファイルを作成します。
Try
Chrome Extensionをつくる
まずはChrome拡張を作成します。
ここで作成するExtensionは、
service workerでネイティブアプリに対して
メッセージを送受信するだけのものです。
Extension用ディレクトリを作成し、
そこにChrome Extension用のmanifest.jsonを作成します。
{
"manifest_version": 3,
"name": "Native Messaging Example",
"description": "Native Messaging Example chrome extension using manifest v3",
"version": "0.0.1",
"action": {
"default_title": "Native Messaging Example",
"default_popup": "popup.html"
},
"permissions": ["nativeMessaging"],
"host_permissions": [
"*://*/*"
],
"background": {
"service_worker": "service-worker.js"
}
}
permissionsにnativeMessagingを指定しています。
これがないとNative Messageが使えません。
通信処理は後述するservice-worker.jsに記述します。
今回はとくに関係ないけど、
ボタン押したときに起動するpopup.htmlを作成。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Native Messaging Example</title>
</head>
<body>
This is Native Messaging Example.
</body>
</html>
Netive Messageを行うservice-worker.jsを
作成します。
//ローカルアプリの起動
var port = chrome.runtime.connectNative('my_native_message_example')
//ローカルアプリからメッセージ受信
port.onMessage.addListener((req) => {
if (chrome.runtime.lastError) {
console.log(chrome.runtime.lastError.message)
}
handleMessage(req)
})
//アプリから切断されたときの処理
port.onDisconnect.addListener(() => {
if (chrome.runtime.lastError) {
console.log(chrome.runtime.lastError.message)
}
console.log('Disconnected')
})
function handleMessage (req) {
console.log("req.message : " + req.message);
if (req.message === 'pong') {
console.log(req)
}
}
//ローカルアプリへメッセージ送信
port.postMessage({message: 'ping', body: 'hello from browser extension'})
chrome.runtime.connectNativeでアプリと接続し、
メッセージの送受信処理を記述します。
ここで指定している「my_native_message_example」という名前が、
後述するmanifestファイルで指定するプロセス通信名となります。
ここまでできたら、chrome://extensions/にアクセスし、
「パッケージ化されていない拡張機能を読み込む」ボタンを押して
Extentionを登録します。
すると、↓のようにIDが表示されるので、
控えておきましょう。
なお、現時点でService Workerのリンクをクリックしても
ホスト側のアプリがないのでエラーがでます。
manifestファイルの用意
Native Messageで通信するため、ローカルのアプリ本体とは別に
JSON形式のmanifestファイルが必要になります。
このマニフェストファイルの名前とnameの値は、
さきほどconnectNativeで指定した名前と同じにします。
my_native_message_example.jsonという名前をつけ、
下記内容を記述します。
{
"name": "my_native_message_example",
"description": "Native Message example",
"path":"<ローカルアプリへのフルパス>",
"type": "stdio",
"supports_native_initiated_connections": true,
"allowed_origins": ["chrome-extension://<Chrome拡張のID>/"]
}
pathはこのあと作成するローカルアプリのフルパスです。
また、allowed_originsにはさきほど控えたIDを指定します。
※最後の/がないとエラーになるっぽいので注意
manifestファイルを作成したら、決められた位置に置きます。
ここにあるように、
/<ユーザーディレクトリ>/Library/ApplicationSupport/Google/Chrome/NativeMessagingHosts
にmanifestファイル(↑の例だとmy_native_message_example.json)を
配置します。
このファイルを置く位置は、対象ブラウザやOSによって違うので
リンク先を確認してください。
ローカルアプリの作成
manifestで指定したpathの場所に実行可能なアプリを作成します。
今回はNodeで動くjsファイルを作成しました。
#!/usr/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 handleMessage (req) {
if (req.message === 'ping') {
sendMessage({message: 'pong', body: 'hello from nodejs app',ping_body:req.body})
}
}
//標準出力処理
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()})
})
1行目のshebangには、自身の環境のnodeのパスを指定してください。
アプリ側では、標準入力を用いてメッセージの受信、
標準出力でメッセージを送信しています。
メッセージはシリアライズ、UTF-8エンコード、
メッセージ長の付加をして使用しています。
動作確認
nodeアプリができて、manifestファイルのpathで指定した位置に配置したら
Chrome Extensionを更新(再度インストール)します。
Service Workerリンクを開いてみると、
↓のようにメッセージのやり取りができているのがわかります。
body: "hello from nodejs app"
message: "pong"
ping_body: "hello from browser extension"
また、このタイミングでpsコマンドでnodeのプロセスをみてみると、
起動されたプロセスがわかります。
% ps -ax | grep node
27211 /usr/bin/node /path/your/script.js chrome-extension://<chrome extension id>/
上記プロセスをkillすると、ウィンドウにDisconnectedメッセージが表示されます。
Summary
今回はChrome Extensionとローカルアプリの通信について紹介しました。
Extensionだけでは実現できない機能も、
この仕組みをつかえば実現できる(かもしれない)。