メッセージ受け渡し

メッセージング API を使用すると、拡張機能に関連付けられたコンテキストで実行されているさまざまなスクリプト間で通信できます。これには、サービス ワーカー、chrome-extension://ページ、コンテンツ スクリプト間の通信が含まれます。たとえば、RSS リーダー拡張機能は、コンテンツ スクリプトを使用してページに RSS フィードがあるかどうかを検出し、サービス ワーカーに通知してそのページのアクション アイコンを更新します。

メッセージ パッシング API には、1 回限りのリクエスト用のものと、複数のメッセージを送信できる長期間の接続用の複雑なものの 2 種類があります。

拡張機能間でメッセージを送信する方法については、拡張機能間のメッセージをご覧ください。

1 回限りのリクエスト

拡張機能の別の部分に単一のメッセージを送信し、必要に応じてレスポンスを取得するには、runtime.sendMessage() または tabs.sendMessage() を呼び出します。これらのメソッドを使用すると、コンテンツ スクリプトから拡張機能へ、または拡張機能からコンテンツ スクリプトへ、1 回限りの JSON シリアル化可能なメッセージを送信できます。どちらの API も、受信者から提供されたレスポンスに解決される Promise を返します。

コンテンツ スクリプトからリクエストを送信するコードは次のようになります。

content-script.js:

(async () => {
  const response = await chrome.runtime.sendMessage({greeting: "hello"});
  // do something with response here, not outside the function
  console.log(response);
})();

回答

メッセージをリッスンするには、chrome.runtime.onMessage イベントを使用します。

// Event listener
function handleMessages(message, sender, sendResponse) {
  fetch(message.url)
    .then((response) => sendResponse({statusCode: response.status}))

  // Since `fetch` is asynchronous, must return an explicit `true`
  return true;
}

chrome.runtime.onMessage.addListener(handleMessages);

// From the sender's context...
const {statusCode} = await chrome.runtime.sendMessage({
  url: 'https://example.com'
});

イベント リスナーが呼び出されると、sendResponse 関数が 3 番目のパラメータとして渡されます。これは、レスポンスを提供するために呼び出すことができる関数です。デフォルトでは、sendResponse コールバックは同期的に呼び出す必要があります。sendResponse に渡される値を取得するために非同期処理を行う場合は、イベント リスナーからリテラル true(真値だけでなく)を返す必要があります。これにより、sendResponse が呼び出されるまで、メッセージ チャネルが相手側に開いたままになります。

パラメータなしで sendResponse を呼び出すと、null がレスポンスとして送信されます。

複数のページが onMessage イベントをリッスンしている場合、特定のイベントに対して sendResponse() を最初に呼び出したページのみがレスポンスの送信に成功します。そのイベントに対する他のすべてのレスポンスは無視されます。

長時間継続する接続

再利用可能な長寿命のメッセージ パス チャネルを作成するには、次を呼び出します。

  • コンテンツ スクリプトから拡張機能ページにメッセージを渡すための runtime.connect()
  • 拡張機能のページからコンテンツ スクリプトにメッセージを渡すための tabs.connect()

name キーを含むオプション パラメータを渡して、さまざまな種類の接続を区別することで、チャンネルに名前を付けることができます。

const port = chrome.runtime.connect({name: "example"});

長時間接続のユースケースの 1 つとして、フォームの自動入力拡張機能があります。コンテンツ スクリプトは、特定のログイン用の拡張機能ページへのチャネルを開き、ページ上の各入力要素の拡張機能にメッセージを送信して、入力するフォームデータをリクエストする場合があります。共有接続により、拡張機能は拡張機能コンポーネント間で状態を共有できます。

接続を確立するときに、各エンドに runtime.Port オブジェクトが割り当てられ、その接続を介してメッセージの送受信が行われます。

次のコードを使用して、コンテンツ スクリプトからチャンネルを開き、メッセージを送信してリッスンします。

content-script.js:

const port = chrome.runtime.connect({name: "knockknock"});
port.onMessage.addListener(function(msg) {
  if (msg.question === "Who's there?") {
    port.postMessage({answer: "Madame"});
  } else if (msg.question === "Madame who?") {
    port.postMessage({answer: "Madame... Bovary"});
  }
});
port.postMessage({joke: "Knock knock"});

拡張機能からコンテンツ スクリプトにリクエストを送信するには、前の例の runtime.connect() の呼び出しを tabs.connect() に置き換えます。

コンテンツ スクリプトまたは拡張機能ページのいずれかの受信接続を処理するには、runtime.onConnect イベント リスナーを設定します。拡張機能の別の部分が connect() を呼び出すと、このイベントと runtime.Port オブジェクトが有効になります。受信接続に応答するコードは次のようになります。

service-worker.js:

chrome.runtime.onConnect.addListener(function(port) {
  if (port.name !== "knockknock") {
    return;
  }
  port.onMessage.addListener(function(msg) {
    if (msg.joke === "Knock knock") {
      port.postMessage({question: "Who's there?"});
    } else if (msg.answer === "Madame") {
      port.postMessage({question: "Madame who?"});
    } else if (msg.answer === "Madame... Bovary") {
      port.postMessage({question: "I don't get it."});
    }
  });
});

シリアル化

Chrome では、メッセージ パッシング API は JSON シリアル化を使用します。つまり、メッセージ(および受信者から提供されたレスポンス)には、任意の有効な JSON 値(null、ブール値、数値、文字列、配列、オブジェクト)を含めることができます。他の値はシリアル化可能な値に強制変換されます。

なお、これは 構造化クローン アルゴリズムで同じ API を実装する他のブラウザとは異なります。

ポートの有効期間

ポートは、拡張機能のさまざまな部分間の双方向通信メカニズムとして設計されています。拡張機能の一部が tabs.connect()runtime.connect()、または runtime.connectNative() を呼び出すと、postMessage() を使用してすぐにメッセージを送信できる Port が作成されます。

タブに複数のフレームがある場合、tabs.connect() を呼び出すと、タブ内のフレームごとに runtime.onConnect イベントが 1 回呼び出されます。同様に、runtime.connect() が呼び出されると、拡張機能プロセスのフレームごとに 1 回 onConnect イベントが発生します。

たとえば、開いているポートごとに個別の状態を維持している場合、接続が閉じられたタイミングを確認することがあります。これを行うには、runtime.Port.onDisconnect イベントをリッスンします。このイベントは、チャネルのもう一方の端に有効なポートがない場合に発生します。これには、次のいずれかの原因が考えられます。

  • 相手側に runtime.onConnect のリスナーが存在しません。
  • ポートを含むタブがアンロードされる(たとえば、タブが移動された場合)。
  • connect() が呼び出されたフレームがアンロードされました。
  • ポートを受け取ったすべてのフレーム(runtime.onConnect 経由)がアンロードされました。
  • runtime.Port.disconnect()もう一方の端によって呼び出されます。connect() 呼び出しの結果、受信側の複数のポートが生成され、これらのポートのいずれかで disconnect() が呼び出された場合、onDisconnect イベントは送信ポートでのみ発生し、他のポートでは発生しません。

拡張機能間のメッセージング

拡張機能内のさまざまなコンポーネント間でメッセージを送信するだけでなく、メッセージング API を使用して他の拡張機能と通信することもできます。これにより、他の拡張機能で使用できる公開 API を公開できます。

他の拡張機能からの受信リクエストと接続をリッスンするには、runtime.onMessageExternal メソッドまたは runtime.onConnectExternal メソッドを使用します。次に例を示します。

service-worker.js

// For a single request:
chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.id !== allowlistedExtension) {
      return; // don't allow this extension access
    }
    if (request.getTargetData) {
      sendResponse({ targetData: targetData });
    } else if (request.activateLasers) {
      const success = activateLasers();
      sendResponse({ activateLasers: success });
    }
  }
);

// For long-lived connections:
chrome.runtime.onConnectExternal.addListener(function(port) {
  port.onMessage.addListener(function(msg) {
    // See other examples for sample onMessage handlers.
  });
});

別の拡張機能にメッセージを送信するには、次のように通信する拡張機能の ID を渡します。

service-worker.js

// The ID of the extension we want to talk to.
const laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// For a minimal request:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
  function(response) {
    if (targetInRange(response.targetData))
      chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
  }
);

// For a long-lived connection:
const port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);

ウェブページからメッセージを送信する

拡張機能は、ウェブページからのメッセージを受信して応答することもできます。ウェブページから拡張機能にメッセージを送信するには、"externally_connectable" マニフェスト キーを使用して、メッセージを許可するウェブサイトを manifest.json で指定します。次に例を示します。

manifest.json

"externally_connectable": {
  "matches": ["https://*.example.com/*"]
}

これにより、指定した URL パターンに一致するすべてのページでメッセージング API が公開されます。URL パターンには、少なくともセカンドレベル ドメインを含める必要があります。つまり、「*」、「*.com」、「*.co.uk」、「*.appspot.com」などのホスト名パターンはサポートされていません。<all_urls> を使用して、すべてのドメインにアクセスできます。

runtime.sendMessage() API または runtime.connect() API を使用して、特定の拡張機能にメッセージを送信します。次に例を示します。

webpage.js

// The ID of the extension we want to talk to.
const editorExtensionId = 'abcdefghijklmnoabcdefhijklmnoabc';

// Check if extension is installed
if (chrome && chrome.runtime) {
  // Make a request:
  chrome.runtime.sendMessage(
    editorExtensionId,
    {
      openUrlInEditor: url
    },
    (response) => {
      if (!response.success) handleError(url);
    }
  );
}

拡張機能から、クロス拡張機能メッセージングのように、runtime.onMessageExternal または runtime.onConnectExternal API を使用してウェブページのメッセージをリッスンします。次の例をご覧ください。

service-worker.js

chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.url === blocklistedWebsite)
      return;  // don't allow this web page access
    if (request.openUrlInEditor)
      openUrl(request.openUrlInEditor);
  });

拡張機能からウェブページにメッセージを送信することはできません。

ネイティブ メッセージング

拡張機能は、ネイティブ メッセージング ホストとして登録されているネイティブ アプリケーションとメッセージを交換できます。この機能の詳細については、ネイティブ メッセージングをご覧ください。

セキュリティ上の考慮事項

メッセージングに関連するセキュリティ上の考慮事項をいくつかご紹介します。

コンテンツ スクリプトの信頼性は低い

コンテンツ スクリプトは、拡張機能サービス ワーカーよりも信頼性が低くなります。たとえば、悪意のあるウェブページが、コンテンツ スクリプトを実行するレンダリング プロセスを侵害する可能性があります。コンテンツ スクリプトからのメッセージは攻撃者によって作成された可能性があると想定し、すべての入力を検証してサニタイズしてください。コンテンツ スクリプトに送信されたデータはウェブページに漏洩する可能性があると想定してください。コンテンツ スクリプトから受信したメッセージによってトリガーできる特権アクションのスコープを制限します。

クロスサイト スクリプティング

スクリプトをクロスサイト スクリプティングから保護してください。ユーザー入力、コンテンツ スクリプトを介した他のウェブサイト、API などの信頼できないソースからデータを受け取る場合は、HTML として解釈したり、予期しないコードの実行を許すような方法で使用したりしないように注意してください。

より安全な方法

可能な限り、スクリプトを実行しない API を使用します。

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // JSON.parse doesn't evaluate the attacker's scripts.
  const resp = JSON.parse(response.farewell);
});

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // innerText does not let the attacker inject HTML elements.
  document.getElementById("resp").innerText = response.farewell;
});
安全でないメソッド

拡張機能が脆弱になる次の方法を使用しないでください。

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be evaluating a malicious script!
  const resp = eval(`(${response.farewell})`);
});

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be injecting a malicious script!
  document.getElementById("resp").innerHTML = response.farewell;
});