はじめに
前回の 「前編」 では、JSを用いてCross-Domain通信路(Bridge)を確保し、Heartbeatで生存確認を行う方法を解説しました。
シリーズ記事
| № | 記事 | 内容 |
|---|---|---|
| 1 | [悪用禁止] 外部サイトのDOMを自前SPAで操る(前編):Cross-Domain通信の仕組み | postMessageによるセキュアな通信基盤とHeartbeatの実装 |
| 2 | [悪用禁止] 外部サイトのDOMを自前SPAで操る(中編):RPCによる関数の遠隔実行 | 本編 |
| 3 | [悪用禁止] 外部サイトのDOMを自前SPAで操る(後編):Node.jsとTerserによる自動化と配備 | Terserを利用したビルド自動化とBookmarkletによる配備 |
開発者自身がコンソールでデバッグする分には「繋がっていること」が確認できれば十分ですが、高度なツールを作るには「Parent側のDOMをChildから自在に操る」という一歩踏み込んだ機能が欲しくなります。
今回は、あたかもParent側の関数を直接呼んでいるかのような 「RPC(Remote Procedure Call)」 の実装について詳しく見ていきます。
方法:
postMessageを「関数実行」として扱う仕組み
postMessage は本来「データ(文字列やオブジェクト)」しか送れません。
これを「関数実行」に変換する技術的な仕組みは、JavaScriptの .toString() と eval() の組み合わせによるものです。
SPA側 (Child) のRPCロジック統合
前編で解説した「疎通確認(Heartbeat)」のロジックに、今回の「RPC実行」のロジックを統合した全量コードです。
// index.js (Child)
let isConnected = false;
let lastPongTime = Date.now();
let rpcCallbacks = {}; // レスポンス待機中のリクエストを管理
const targetOrigin = new URLSearchParams(window.location.hash.slice(1)).get('parentDomain');
// Parent側で関数を実行するためのPromiseラッパー
const execInParent = (executorFn, ...args) => {
return new Promise((resolve, reject) => {
if (!isConnected) return reject("Not connected");
const timestamp = Date.now();
rpcCallbacks[timestamp] = { resolve, reject };
// 関数を文字列化して送信
window.opener.postMessage({
type: 'RPC_REQUEST',
functionStr: executorFn.toString(),
args: args,
timestamp: timestamp
}, targetOrigin);
});
};
// メッセージハンドラ(Heartbeat + RPCレスポンスの両方を処理)
window.addEventListener("message", (event) => {
if (event.origin !== targetOrigin) return;
const { type, timestamp, data } = event.data;
if (type === 'HEARTBEAT_PONG') {
isConnected = true;
lastPongTime = Date.now();
}
else if (type === 'RPC_RESPONSE' && rpcCallbacks[timestamp]) {
rpcCallbacks[timestamp].resolve(data);
delete rpcCallbacks[timestamp];
}
});
// 1秒ごとにPING送信および死活監視
setInterval(() => {
if (Date.now() - lastPongTime > 5000) {
isConnected = false;
console.warn("Status: Disconnected (Timeout)");
}
if (window.opener && targetOrigin) {
window.opener.postMessage({ type: 'HEARTBEAT_PING' }, targetOrigin);
}
}, 1000);
// 使用例:Parent側のタイトルを取得し、引数のメッセージと共に返す関数
// ※ この関数の中身が文字列化され、Parent(外部サイト)側で実行されます
function getParentTitleAndLog(message) {
const currentTitle = document.title;
// このログはParent(外部サイト)側のコンソールに出力されます
console.log(`[RPC Environment] Title: ${currentTitle}, Message: ${message}`);
return {
title: currentTitle,
text: message
};
}
// 第一引数に関数、第二引数以降に任意の引数を渡して実行
execInParent(getParentTitleAndLog, "Hello from Child!").then(result => {
// 実行結果はChild(SPA)側のコンソールに出力されます
console.log("RPC Result received in Child:", result);
});
Parent側 (Snippet内) の実行ロジック
前編で解説した「ウィンドウ起動」と「Heartbeat応答」のコードに、今回の「RPC実行」のロジックを統合した全量コードです。このスニペットをParent側(操作対象のサイト)のコンソールに貼り付けることで、通信と実行の準備が整います。
// Parent (Console)
const parentDomain = window.location.origin;
const spaUrl = `http://localhost:3000/#parentDomain=${encodeURIComponent(parentDomain)}`;
// 1. SPAを子画面として開く
const subWin = window.open(spaUrl, "Cross-Domain-RPC-Demo");
if (subWin) subWin.focus();
// 2. メッセージハンドラ(Heartbeat応答 + RPC実行)
window.addEventListener("message", (event) => {
// セキュリティチェック:許可したオリジン(localhost:3000)以外は無視
if (event.origin !== "http://localhost:3000") return;
const { type, functionStr, timestamp, args } = event.data;
// Heartbeat (PING) への応答
if (type === 'HEARTBEAT_PING') {
event.source.postMessage({ type: 'HEARTBEAT_PONG' }, event.origin);
}
// RPCリクエスト(関数実行命令)の処理
else if (type === "RPC_REQUEST") {
try {
// 文字列化された関数を復元してParentのコンテキストで実行
const executor = eval('(' + functionStr + ')');
const result = executor(...args);
// 実行結果をChild側へ返信
event.source.postMessage({
type: "RPC_RESPONSE",
timestamp,
data: result
}, event.origin);
} catch (e) {
console.error("RPC Error:", e);
}
}
});
この仕組みの特筆すべきメリットは、Parent側のDOMを操作するロジックを Child側のエディタで管理しつつ、実行自体はParent側のコンテキストで行える 点にあります。
おわりに(中編)
ここまでの手順で、ドメインの壁を超えて「関数」を送り込み、その実行結果を受け取ることが可能になりました。
しかし、開発のたびにこの長いスクリプトをコンソールへ貼り付けるのは非効率ですし、一般ユーザーに「開発者ツールを開いてください」と頼むのも現実的ではありません。
次回の 「後編」 では、この仕組みを自動化し、誰でもワンクリックで導入できる 「Bookmarklet」 として配布・運用する方法について詳しく解説します。
余談: CSP(Content Security Policy)による制限と回避策
本手法は、postMessage によってブラウザの同一生成元ポリシー(Same-Origin Policy)を回避できますが、Parent側(操作対象のサイト)の CSP(Content Security Policy) による制限は依然として受けます。
具体的には、対象サイトの CSP ヘッダーの script-src ディレクティブに 'unsafe-eval' が明示的に許可されていない場合、関数をシリアライズして送り込もうとすると、ブラウザ側で実行が拒否され EvalError が発生します。
もしこの制限を回避して、より高度な自動化やデータ抽出を実現したい場合は、ページ側の CSP に縛られない特権を持つ ブラウザ拡張機能(Content Script) として開発するか、Tampermonkey などのユーザースクリプトマネージャを活用するアプローチを検討してください。