1. 9.3 異なる文書間のメッセージング
      1. 9.3.1 導入
      2. 9.3.2 セキュリティ
      3. 9.3.3 メッセージのポスト
    2. 9.4 チャンネルメッセージング
      1. 9.4.1 導入
        1. 9.4.1.1
        2. 9.4.1.2 ウェブ上のオブジェクト機能モデルの基礎としてのポート
        3. 9.4.1.3 サービス実装を抽象化する基礎としてのポート
      2. 9.4.2 メッセージチャンネル
      3. 9.4.3 MessagePortWorker、およびDedicatedWorkerGlobalScopeに存在するプロパティ
      4. 9.4.4 メッセージポート
      5. 9.4.5 ポートおよびガベージコレクション
    3. 9.5 他のブラウジングコンテキストへのブロードキャスト

9.3 異なる文書間のメッセージング

Window/postMessage

Support in all current engines.

Firefox3+Safari4+Chrome2+
Opera9.5+Edge79+
Edge (Legacy)12+Internet Explorer10+
Firefox Android?Safari iOS?Chrome Android?WebView Android≤37+Samsung Internet?Opera Android10.1+

ウェブブラウザーは、セキュリティおよびプライバシー上の理由から、異なるドメインの文書が相互に影響を与えないようにしている。つまり、クロスサイトスクリプティングは許可されてない。

これは重要なセキュリティ機能であるが、たとえ敵対的でないページであっても、異なるドメインにあるそれらのページの通信を妨げる。このセクションでは、クロスサイトスクリプティング攻撃を可能にしないように設計された、ソースドメインに関係なく文書が相互に通信を可能にするメッセージングシステムを紹介する。

postMessage() APIは、トラッキングベクターとして使用できる。

9.3.1 導入

たとえば、文書Aに文書Bを含むiframe要素が含まれ、文書Aのスクリプトが文書Bの WindowオブジェクトでpostMessage()を呼び出す場合、文書AのWindowから発信されたものとしてマークされたメッセージイベントがそのオブジェクトで発生する。文書Aのスクリプトは次のようになる;

var o = document.getElementsByTagName('iframe')[0];
o.contentWindow.postMessage('Hello world', 'https://b.example.org/');

着信イベントのイベントハンドラーを登録するために、スクリプトはaddEventListener(または類似のメカニズム)を使用する。たとえば、文書Bのスクリプトは次のようになる:

window.addEventListener('message', receiver, false);
function receiver(e) {
  if (e.origin == 'https://example.com') {
    if (e.data == 'Hello world') {
      e.source.postMessage('Hello', e.origin);
    } else {
      alert(e.data);
    }
  }
}

このスクリプトは、ドメインが予想されるドメインであることを最初に確認し、次にメッセージを調べる。メッセージは、ユーザーに表示されるまたは、最初にメッセージを送信した文書にメッセージを送り返すことによって応答される。

9.3.2 セキュリティ

このAPIを使用するには、サイトを悪用する悪意のある組織からユーザーを保護するために、特別な注意が必要である。

著者は、origin属性をチェックし、メッセージを受信すると予想されるドメインからのみメッセージが受け入れられるようにすべきである。さもなければ、著者のメッセージ処理コードのバグが悪意のあるサイトによって悪用される可能性がある。

さらに、origin属性をチェックした後でも、著者は問題のデータが期待されたフォーマットであることもチェックすべきである。さもなければ、イベントのソースがクロスサイトスクリプティングの欠陥を使用して攻撃された場合、 postMessage()メソッドを使用して送信された情報の処理がさらにチェックされないことで、攻撃が受信側に伝播する可能性がある。

著者は、機密情報を含むメッセージのtargetOrigin 引数にワイルドカードキーワード(*)を使用すべきではない。さもなければ、メッセージが意図した受信者にのみ配信されることを保証できなくなる。


あらゆる生成元からのメッセージを受け入れる著者は、サービス拒否攻撃のリスクを考慮することが奨励される。攻撃者は大量のメッセージを送信する可能性がある。受信ページがコストのかかる計算を実行したり、そのようなメッセージごとにネットワークトラフィックが送信されたりすると、攻撃者のメッセージはサービス拒否攻撃に増幅される可能性がある。著者は、そのような攻撃を非現実的にするために、レート制限(1分あたり特定の数のメッセージのみを受け入れる)を採用することが奨励される。

9.3.3 メッセージのポスト

window.postMessage(message [, options ])

指定されたウィンドウにメッセージをポストする。メッセージは、ネストされたオブジェクト、配列などの構造化されたオブジェクトにすることができ、JavaScriptの値(文字列、数値、Dateオブジェクトなど)を含めることができ、 File BlobFileListArrayBufferオブジェクトなどの特定のデータオブジェクトを含めることができる。

optionstransferメンバーにリストされているオブジェクトは、複製されるだけでなく、転送される。つまり、送信側では使用できなくなる。

ターゲットの生成元は、optionstargetOriginメンバーを使用して指定できる。指定しない場合、デフォルトの"/"になる。このデフォルトでは、メッセージは生成元が同じターゲットだけに制限される。

ターゲットウィンドウの生成元が指定されたターゲットの生成元とマッチしない場合、情報の漏洩を避けるためにメッセージは破棄される。生成元に関係なく宛先にメッセージを送信するには、ターゲットの生成元を"*"に設定する。

transfer配列に重複するオブジェクトが含まれている場合、またはmessageをクローンできなかった場合、"DataCloneError" DOMExceptionを投げる。

window.postMessage(message, targetOrigin [, transfer ])

これはpostMessage()の代替バージョンで、ターゲットの生成元がパラメータとして指定される。window.postMessage(message, target, transfer)の呼び出しは、window.postMessage(message, {targetOrigin, transfer})と同じである。

新しいDocumentにナビゲートされたばかりのブラウジングコンテキストWindowにメッセージを投稿するとき、メッセージが意図した受信者を受信しない可能性がある。ターゲットブラウジングコンテキストのスクリプトには、メッセージのリスナーを設定する時間が必要である。したがって、たとえば、メッセージが新しく作成された子iframeWindowに送信される状況では、著者は、子Documentにメッセージを受信する準備ができていることを知らせるメッセージを親に投稿させ、親がこのメッセージを待ってからメッセージの投稿を開始するようにアドバイスされる。

9.4 チャンネルメッセージング

Channel_Messaging_API

Support in all current engines.

Firefox41+Safari5+Chrome2+
Opera10.6+Edge79+
Edge (Legacy)12+Internet Explorer10+
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android11+

Channel_Messaging_API/Using_channel_messaging

Support in all current engines.

Firefox41+Safari5+Chrome2+
Opera10.6+Edge79+
Edge (Legacy)12+Internet Explorer10+
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android11+

9.4.1 導入

独立したコード部分(たとえば、異なるブラウジングコンテキストで実行される )が直接通信できるようにするために、著者はチャンネルメッセージングを使用することができる。

このメカニズムの通信チャンネルは、両端にポートを持つ双方向パイプとして実装される。一方のポートで送信されたメッセージはもう一方のポートに配信され、その逆も同様である。メッセージはDOMイベントとして配信され、実行タスクを中断したりブロックしたりすることはない。

接続(2つの"もつれた"ポート)を作成するには、MessageChannel()コンストラクターを呼び出す:

var channel = new MessageChannel();

ポートの1つはローカルポートとして保持され、もう1つのポートはリモートコードに送信される。たとえば、postMessage()を使用する:

otherWindow.postMessage('hello', 'https://example.com', [channel.port2]);

メッセージを送信するには、ポートのpostMessage()メソッドを使用する:

channel.port1.postMessage('hello');

メッセージを受信するには、messageイベントをリッスンする:

channel.port1.onmessage = handleMessage;
function handleMessage(event) {
  // message is in event.data
  // ...
}

ポートで送信されるデータは構造化データである可能性がある。たとえば、ここでは文字列の配列がMessagePortで渡される:

port1.postMessage(['hello', 'world']);
9.4.1.1

この例では、2つのJavaScriptライブラリがMessagePortを使用して相互に接続されている。これにより、APIを変更することなく、ライブラリを後で別のフレームまたはWorkerオブジェクトにホストできるようになる。

<script src="contacts.js"></script> <!-- exposes a contacts object -->
<script src="compose-mail.js"></script> <!-- exposes a composer object -->
<script>
 var channel = new MessageChannel();
 composer.addContactsProvider(channel.port1);
 contacts.registerConsumer(channel.port2);
</script>

"addContactsProvider()"関数の実装は次のようになる:

function addContactsProvider(port) {
  port.onmessage = function (event) {
    switch (event.data.messageType) {
      case 'search-result': handleSearchResult(event.data.results); break;
      case 'search-done': handleSearchDone(); break;
      case 'search-error': handleSearchError(event.data.message); break;
      // ...
    }
  };
};

あるいは、次のように実装することもできる:

function addContactsProvider(port) {
  port.addEventListener('message', function (event) {
    if (event.data.messageType == 'search-result')
      handleSearchResult(event.data.results);
  });
  port.addEventListener('message', function (event) {
    if (event.data.messageType == 'search-done')
      handleSearchDone();
  });
  port.addEventListener('message', function (event) {
    if (event.data.messageType == 'search-error')
      handleSearchError(event.data.message);
  });
  // ...
  port.start();
};

重要な違いは、addEventListener()を使用する場合、start()メソッドも呼び出さなければならないことである。onmessageを使用する場合、start()メソッドが暗黙的に呼び出される。

start()メソッドは、明示的に呼び出された場合でも、(onmessageを設定することで)暗黙的に呼び出された場合でも、メッセージのフローを開始する。メッセージポートにポストされたメッセージは、スクリプトがハンドラーをセットアップする前に破棄されないように、最初は一時停止される。

9.4.1.2 ウェブ上のオブジェクト機能モデルの基礎としてのポート

ポートは、システム内の他の行為者に対して、限定された機能(オブジェクト機能モデルの意味で)を公開する手段と捉えることができる。これは、ポートが特定の生成元内での便利なモデルとしてのみ使用される弱い機能システムと、別の生成元の消費者プロバイダーに変更を加えたり情報を取得したりするための唯一の手段として、ある生成元のプロバイダーによって提供される強い機能モデルのいずれかである。

たとえば、ソーシャルウェブサイトが、あるiframeにユーザーのメール連絡先プロバイダー(別の生成元のアドレス帳サイト)を埋め込み、別のiframeにゲーム(別の生成元のゲーム)を埋め込んでいる状況を考えてみる。外側のソーシャルサイトと2番目のiframe内のゲームは、1つ目のiframe内のいかなる機能にもアクセスできない。つまり、これら2つのサイトでできることは次のとおりである:

連絡先プロバイダーはこれらのメソッド、特に3番目のメソッドを使用することで、他の生成元がユーザーのアドレス帳を操作できるAPIを提供できる。たとえば、"add-contact Guillaume Tell <tell@pomme.example.net>"というメッセージに応答し、指定された人物およびメールアドレスをユーザーのアドレス帳に追加することができる。

ウェブ上のどのサイトもユーザーの連絡先を操作できないようにするために、連絡先プロバイダーは、ソーシャルサイトなどの特定の信頼できるサイトのみにこの操作を許可する場合がある。

さて、ゲームがユーザーのアドレス帳に連絡先を追加したいと考えており、ソーシャルサイトがゲームに代わってその操作を許可し、連絡先プロバイダーがソーシャルサイトと築いていた信頼を「共有」することを望んだとする。これにはいくつかの方法があるが、最も単純な方法は、ゲームサイトと連絡先サイトとの間のメッセージをプロキシすることである。しかし、この解決策にはいくつかの難点がある。ソーシャルサイトは、ゲームサイトが権限を乱用しないことを完全に信頼するか、ソーシャルサイトが各リクエストを検証して、許可したくないリクエスト(複数の連絡先の追加、連絡先の閲覧、削除など)ではないことを確認する必要がある。また、複数のゲームが同時に連絡先プロバイダーとやり取りする可能性がある場合、複雑さが増す。

しかし、メッセージチャンネルおよびMessagePortオブジェクトを使用すれば、これらの問題はすべて解消する。ゲームがソーシャルサイトに連絡先を追加したいと伝えると、ソーシャルサイトは連絡先プロバイダーに、連絡先を1つ追加するのではなく、1つの連絡先を追加する機能を要求できる。連絡先プロバイダーはMessagePortオブジェクトのペアを作成し、そのうちの1つをソーシャルサイトに送り返し、ソーシャルサイトはそれをゲームに転送する。こうしてゲームと連絡先プロバイダーは直接接続され、連絡先プロバイダーは"連絡先の追加"リクエスト1件のみを承認すればよく、それ以外のリクエストは受け付けない。つまり、ゲームには1つの連絡先を追加する機能が付与されたことになる。

9.4.1.3 サービス実装を抽象化する基礎としてのポート

前のセクションからの例に続いて、特に連絡先プロバイダーについて考えてみる。初期の実装では、サービスのiframeで単にXMLHttpRequestオブジェクトを使用していたかもしれないが、サービスの進化により、代わりに単一のWebSocket接続で共有ワーカーを使用したい場合がある。

最初の設計でMessagePortオブジェクトを使用して機能を付与した場合、または単に複数の同時独立セッションを許可した場合でも、サービス実装は、APIをまったく変更することなく、 XMLHttpRequestの各iframeモデルから共有WebSocketモデルに切り替えることができる。サービスプロバイダー側のポートはすべて、APIのユーザーに少しも影響を与えることなく、共有ワーカーに転送できる。

9.4.2 メッセージチャンネル

MessageChannel

Support in all current engines.

Firefox41+Safari5+Chrome2+
Opera10.6+Edge79+
Edge (Legacy)12+Internet Explorer10+
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android11+
channel = new MessageChannel()

2つの新しいMessagePortオブジェクトをもつ新しいMessageChannelオブジェクトを返す。

channel.port1

1つ目のMessagePortオブジェクトを返す。

channel.port2

2つ目のMessagePortオブジェクトを返す。

9.4.3 MessagePortWorker、およびDedicatedWorkerGlobalScopeに存在するプロパティ

下記は、MessagePortWorkerおよびDedicatedWorkerGlobalScopeオブジェクトによってイベントハンドラーIDL属性としてサポートされるイベントハンドラー(および対応するイベントハンドラーイベントタイプ)である:

イベントハンドラーイベントハンドラーイベント型
onmessage

MessagePort/message_event

Support in all current engines.

Firefox41+Safari5+Chrome2+
Opera10.6+Edge79+
Edge (Legacy)12+Internet Explorer10+
Firefox Android?Safari iOS?Chrome Android?WebView Android37+Samsung Internet?Opera Android11.5+

DedicatedWorkerGlobalScope/message_event

Support in all current engines.

Firefox3.5+Safari4+Chrome4+
Opera10.6+Edge79+
Edge (Legacy)12+Internet Explorer10+
Firefox Android?Safari iOS5+Chrome Android?WebView Android37+Samsung Internet?Opera Android11.5+
message
onmessageerror

MessagePort/messageerror_event

Support in all current engines.

Firefox57+Safari16.4+Chrome60+
Opera?Edge79+
Edge (Legacy)18Internet ExplorerNo
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android47+

DedicatedWorkerGlobalScope/messageerror_event

Support in all current engines.

Firefox57+Safari16.4+Chrome60+
Opera?Edge79+
Edge (Legacy)18Internet ExplorerNo
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android47+
messageerror

9.4.4 メッセージポート

MessagePort

Support in all current engines.

Firefox41+Safari5+Chrome2+
Opera10.6+Edge79+
Edge (Legacy)12+Internet Explorer10+
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android11+

各チャネルには2つのメッセージポートがある。一方のポートから送信されたデータはもう一方のポートで受信され、その逆も同様である。

port.postMessage(message [, transfer])
port.postMessage(message [, { transfer }])

チャンネルを通じてメッセージをポストする。Objects listed in transferにリストされたオブジェクトは、単に複製されるのではなく転送される。つまり、送信側では使用できなくなる。

transferが重複するオブジェクトもしくはportを含む場合、またはmessageを複製できなかった場合、"DataCloneError" DOMExceptionを投げる。

port.start()

ポートで受信したメッセージの発送を開始する。

port.close()

ポートを切断し、アクティブでなくなる。

9.4.5 ポートおよびガベージコレクション

著者は、リソースを再収集できるように、MessagePortオブジェクトを明示的に閉じて、それらを分離することを強く勧める。多くのMessagePortオブジェクトを作成し、それらを閉じずに破棄すると、ガベージコレクションが必ずしも迅速に実行されるとは限らないため、一時的にメモリーの使用率が高くなる可能性がある。特に、ガベージコレクションにプロセス間の調整が含まれるMessagePortの場合はそうなる。

9.5 他のブラウジングコンテキストへのブロードキャスト

BroadcastChannel

Support in all current engines.

Firefox38+Safari15.4+Chrome54+
Opera?Edge79+
Edge (Legacy)?Internet ExplorerNo
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android?

Broadcast_Channel_API

Support in all current engines.

Firefox38+Safari15.4+Chrome54+
Opera?Edge79+
Edge (Legacy)?Internet ExplorerNo
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android?

同じユーザーエージェント内の同じユーザーによって開かれたが、異なる関連のないブラウジングコンテキストの単一の生成元上のページは、時には互いに通知を送信する必要がある。例えば、「ユーザーはここにログインしました、あなたの認証情報をもう一度確認してください」。

共有状態のロックの管理、サーバーと複数のローカルクライアント間のリソースの同期の管理、リモートホストとのWebSocket接続の共有など、複雑なケースでは、共有ワーカーが最も適切なソリューションである。

しかし、共有ワーカーが過度のオーバーヘッドとなるような単純な場合には、著者はこのセクションで説明する単純なチャンネルベースのブロードキャストメカニズムを使用することができる。

broadcastChannel = new BroadcastChannel(name)

指定されたチャンネル名のメッセージを送受信できる新しいBroadcastChannelオブジェクトを返す。

broadcastChannel.name

(コンストラクターに渡された)チャンネル名を返す。

broadcastChannel.postMessage(message)

指定されたメッセージを、このチャンネルに設定されている他のBroadcastChannelオブジェクトに送信する。メッセージは、ネストされたオブジェクトや配列などの構造化オブジェクトにすることができる。

broadcastChannel.close()

BroadcastChannelオブジェクトを閉じ、ガベージコレクションに対して開く。

著者は、不要になったときにBroadcastChannelオブジェクトを明示的に閉じることを強く勧める。これにより、オブジェクトをガベージコレクションすることができる。多くのBroadcastChannelオブジェクトを作成し、それらをイベントリスナーに残したまま、それらを閉じずに破棄すると、オブジェクトはイベントリスナーがある限り(またはそれらのページまたはワーカーが閉じられるまで)存続し続けるため、明らかなメモリリークが発生する可能性がある。

ユーザーが同じサイトの別のタブからログアウトした場合でも、ユーザーがいつログアウトしたかをページが知りたいとする:

var authChannel = new BroadcastChannel('auth');
authChannel.onmessage = function (event) {
  if (event.data == 'logout')
    showLogout();
}

function logoutRequested() {
  // called when the user asks us to log them out
  doLogout();
  showLogout();
  authChannel.postMessage('logout');
}

function doLogout() {
  // actually log the user out (e.g. clearing cookies)
  // ...
}

function showLogout() {
  // update the UI to indicate we're logged out
  // ...
}