Electronのプロセス間通信時の注意点と簡単な実装方法について

更新日時: July 18, 2020

はじめに

最近 Electron でOSのネイティブの通知APIを使った実装をする機会がありました。
実装する中、今まで知っていたプロセス間の通信方法が非推奨の方法であり、他に推奨される方法が別途あることが分かりました。

今回はプロセス間通信の推奨する実装方法をOSネイティブの通知APIを使う例で説明します。

実装した例は以下のリポジトリから確認できます。



Electronの2つのプロセス

Electron のプロセスには大きく2つがあります。

1つ目は メインプロセス で、2つ目は レンダラープロセス です。

それぞれのプロセスがどのように動いているかについて公式ドキュメントの説明によると、 メインプロセス は以下です。

Electronにおいて、package.json の main で指定されたスクリプトを実行するプロセスを メインプロセス (main process) と呼びます。 メインプロセスで実行されるスクリプトは、ウェブページを生成することで GUI を表示できます。 Electron アプリには常に1つのメインプロセスがありますが、これ以上はありません。

そして、 レンダラープロセス は以下です。

Electron はウェブページを表示するために Chromium を使用しているため、 Chromium のマルチプロセスアーキテクチャも使用されます。 Electronにおける各 Web ページはそれぞれのプロセスとして動作します。 これをレンダラプロセス (renderer process) と呼びます。

1つの メインプロセス に複数の レンダラープロセス が存在し得ることが特徴です。

Chrome ブラウザに例えると、起動しているブラウザが1つの メインプロセス で、表示されているそれぞれのタブが レンダラープロセス になります。


Electron - アプリケーションアーキテクチャ

また、それぞれのプロセスでは node.js のAPIへフルアクセスできます。
つまり、 レンダラープロセス でも require を使うことでモジュールの呼び出しができてしまいます。
これはリモートコンテンツをロードしようとする場合、セキュリティに気をつけないといけないことを意味します。
今回の記事でセキュリティに関することの中でも、プロセス間通信時に使う ipc通信 の利用で気をつけることを下記にまとめていきます。


実装時の注意点

プロセスをまたいだ呼び出しをしない

レンダラープロセス には remote という メインプロセス のモジュールを レンダラープロセス から使えるAPIがあります。

レンダラープロセス からプロセス間で通信するためのイベントを設定しなくても メインプロセス による処理(OSを参照したり、ファイルを書き込んだりするような)が使える点では便利です。


Electron - remote

ただし、これを使った実装にするとオブジェクトにアクセスする速さセキュリティ上の理由でこのAPIを使うことを非推奨する呼びかけもあります。


とくに、リモートコンテンツによる悪意のある行為があった場合、XSS対策がないとそのままスクリプトが実行される可能性もあります。


このようなことが起きないようにするための方法として、 メインプロセス から レンダラープロセス を生成する際に nodeIntegration を無効化するオプションがあります。

const mainWindow = new BrowserWindow({
  
  他のオプション
  
  webPreferences: {
    nodeIntegration: false, // このオプションを使う
  },
  
});

Electron - context-isolation

しかし、このオプションを使うと レンダラープロセス から require でモジュールを呼び出すことができなくなります。

Uncaught ReferenceError: require is not defined

プロセス間でコンテキストを共有する

では、XSS対策をしつつプロセス間通信を有効にするためにはどうすればいいかというと、以下の設定を加えることで可能になります。

  • レンダラープロセス を生成する時、 メインプロセス と通信できるAPIを限定的に公開するオプションをつける
  • レンダラープロセス で使うAPIを preload オプションに追加する
const mainWindow = new BrowserWindow({
  
  webPreferences: {
    nodeIntegration: false,
    contextIsolation: true, // 限定的なAPI公開のフラグ
    preload: (限定的に公開するAPIモジュールのパス), // レンダラープロセスに公開するAPIのファイル
  },
  
});

このように設定することでプロセス間の通信は以下のような形で実現できます。



実装の説明

簡単ですが、今回実装したサンプルの構造は以下になります。

.
├── index.html // レンダラープロセスで表示するファイル
├── main.js // メインプロセスファイル
├── preload.js // レンダラープロセスに限定的に公開するAPIのファイル
└── renderer.js // レンダラープロセスファイル

また、コードは以下の順で説明します。

  • メインプロセスレンダラープロセス を生成する書き方
  • 限定公開用のAPIモジュールの書き方
  • レンダラープロセス でイベントコールする書き方

メインプロセスレンダラープロセス を生成する書き方

main.js


const mainWindow = new BrowserWindow({
  
  webPreferences: {
    nodeIntegration: false, // XSS対策としてnodeモジュールを使わないように設定
    contextIsolation: true, // 限定的にAPIを公開する設定
    preload: path.resolve("./preload.js"), // レンダラープロセスに公開するAPIのファイル
  },
  
});

上記で説明を書いた通り、 レンダラープロセス を生成する時のフラグと限定公開するAPIのモジュールを設定します。


// レンダラープロセスに公開するAPI
ipcMain.on("require-send-notice", (e) => {
  // OSネイティブの通知機能インスタンスを生成
  const notification = new Notification({
    title: "基本的な通知",
    body: "簡単なメッセージ",
    silent: false,
  });

  // 通知表示
  notification.show();

  // 一定時間が経つと自動で消えるように設定
  setTimeout(
    (notification) => {
      notification.close();
    },
    5000,
    notification
  );
});

ipcMain.handle("is-notification-supported", (event) => {
  // OSがネイティブの通知機能をサポートしているかどうかを確認する
  return Notification.isSupported();
});

今回、OSネイティブの通知機能を使うためのAPIを定義します。
通知機能に関する仕様は以下のドキュメントから確認できます。

Electron - 通知

また、 レンダラープロセス と通信する際に ipcMain というAPIを使って同期/非同期的に扱います。
扱い方に関しては以下のドキュメントから確認ができます。

Electron - ipcMain

限定公開用のAPIモジュールの書き方

preload.js

const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld("electron", {
  isSupportedNotice: () => ipcRenderer.invoke("is-notification-supported").then(result => result).catch(err => console.log(err)),
  noSupportedNotice: () => false,
  notice: () => ipcRenderer.send("require-send-notice"),
});

contextBridge を使って、 メインプロセスレンダラープロセス 間で限定的に扱うAPIを定義します。

exposeInMainWorld で第1引数に レンダラープロセス で扱うJavaScriptの window から参照するキーを指定します。
第2引数には、 レンダラープロセス で呼び出し時に扱うAPIのオブジェクトを設定します。

contextBridge と扱えるAPIオブジェクトの仕様に関しては以下のドキュメントから確認できます。

Electron - contextBridge

また、 メインプロセス と通信する際に ipcRenderer というAPIを使って同期/非同期的に扱います。
扱い方に関しては以下のドキュメントから確認ができます。

Electron - ipcRenderer

レンダラープロセス でイベントコールする書き方

renderer.js

const notification = document.getElementById("notice");

notification.addEventListener("click", async () => {
  const result = await window.electron.isSupportedNotice();
  console.log("Notification: ", result);
  if (result) {
    window.electron.notice();
    return;
  }
});

preload.js で定義したAPIをイベント発火させたい要素に登録することで画面から呼び出すことができます。
呼び出し方は、 window.レンダラープロセスで扱うAPIキー、オブジェクトの呼び出しに使うキー() で使えます。

このように設定することで、 Electron のアプリでは以下のように使えます。



最後に

今回たまたま実務で触って見たことがきっかけでして、 レンダラープロセス 生成時の注意点について説明しました。

あくまでもセキュリティ面で気をつけることの1つとしてXSS対策を兼ねたイベントの定義/登録方法について触れました。
他にも Electron のセキュリティに関して興味がある方は以下の公式ドキュメントを参考にすることをオススメします。

Electron - セキュリティ、ネイティブ機能、あなたの責任

コメントする