2017年4月5日水曜日

Chrome extentionを使い、iframe内外でデータ通信

◆ 目的
Webページ(親ページ)とそこからiframeで読み込んだ子ページ間でデータの授受を行う。
ただし、iframe内のコンテンツは親ページのドメイン外に存在する任意のサイトとする。



◆ 課題
解決する課題は2つある。
1. クロスドメイン制約への対応
XMLHttpRequestを投げる際などにも必ず気に掛ける点だろう。
Same Origin Policy(同一生成元ポリシー)を回避しなければならない。

今回は任意の外部サイトをiframeに取り込むことを前提としているため、
CORS(Cross-Origin Resource Sharing)もJSONP(JSON with padding)も今回は使えない。

2. iframe内での読み込み不許可ヘッダへの対応
HTTPのレスポンスヘッダに、iframe内からWebページが読み込まれるのを防止するオプション(X-Frame-Options)が付与されていればiframe内で表示はできない。



◆ 解決策
1. postMessageの利用
クロスドメイン制約への対応として、HTML5で用意されたpostMessageの利用を思いつく。

(親サイト)
<iframe id="ifrm" src="外部サイト"></iframe>

<script type="text/javascript">
window.onload = function() {
  var ifrm = document.getElementById('ifrm').contentWindow
  ifrm.postMessage("hello", '外部サイト')
};
</script>

(外部サイト)
<script type="text/javascript">
window.addEventListener('message', function(event) {
    alert(event.data)
}, false);
</script>

しかし、Chromeが許可しない。
※Safariでは警告なく、実施できた。

(エラー例)
Failed to execute 'postMessage' on 'DOMWindow': The target origin provided ('http://親サイト') does not match the recipient window's origin ('http://外部サイト').


2. iframeを使わない
iframeを使う前提を変え、コンテンツをダウンロードし、外部サイトを親側で再現させる。つまり、前提を変える。
が、しかし、JavaScriptが生成する動的ページのリソースの管理は困難があるため、やはり、iframeは使いたい。前提は戻す。


3. Google Chromeの拡張機能の利用
Chromeに限定されるが、Chromeの拡張機能を使えば対応ができそうである。
データの共有は、バックグラウンドで動作させるスクリプト内でセッションストレージを使えばいいだろう。
また、ヘッダの書き換えも実施できる。



◆ 拡張コード概要
chrome拡張を利用することにした。
拡張コードを有効にするには、最低限以下の3つのファイルを用意し、それらを適当なフォルダに入れ、Chromeブラウザの拡張機能からインポートすれば良い。

○ マニフェスト ファイル
拡張機能に関する情報を与える。

○ コンテンツ スクリプト
ブラウザで表示させるページで読み込むjsとは別空間で実行させるjsである。
このファイルは親サイトと、iframe内の外部サイト、両方に読み込まれる。
空間は分かれているため、コンテンツスクリプト内で利用しているjQueryなどのライブラリがサイトで利用しているバージョンと異なっていても問題は起きない。

○ バックグラウンド スクリプト
Chromeのバックエンド側で処理させるjsである。
表示コンテンツには取り込まれないが、コンテント ファイルとの間でメッセージ通信ができる。


簡易図で表すと下記のような感じである。
   parent
+----------+
|          |← contentScripts.js
|  iframe  |                       
|  +----+  |
|  |    |  |
|  |    |←-|-- contentScripts.js
|  +----+  |
|          |
+----------+
background.js

contentScripts.js、background.jsの名称はmanifest.json内で指定する。



◆ 試験
1. iframe内でマウスを操作。マウスオーバしたタグ要素に色がつくようにしている。
  そのタグ要素でクリックすると、タグ名がセッションストレージへ保存される。

2. 親側でiframe外の要素をクリックする。
  iframeで取得した要素がalert表示されれば成功である。



◆ コード例
○ manifest.json
{
"name": "TEST",
"manifest_version": 2,
"version": "1.3",
"description": "test",
"permissions": [
"tabs",
"storage",
"webRequest", "webRequestBlocking",
"*://*/*"
],
"background": {
"scripts": [ "background.js" ],
"persistent": true
},
"content_scripts": [{
"matches": [ "*://*/*"],
"all_frames": true,
"css": ["common.css", "jquery/jquery-ui-edit.css"],
"js": [ "jquery/jquery.js", "jquery/jquery-ui.min.js", "contentScripts.js" ]
}],
"web_accessible_resources": [
"jquery/images/*.png"
]
}
view raw manifest.json hosted with ❤ by GitHub


○ contentScripts.js
// iframe
if (window != parent) {
var target_tag = 'address, article, aside, blockquote, canvas, dd, div, dl, fieldset, figcaption, figure, footer, form, h1, h2, h3, h4, h5, h6, header, hgroup, hr, li, main, nav, noscript, ol, output, p, pre, section, table, tfoot, ul, video'
var elements = document.querySelectorAll(target_tag)
for (var i = 0; i < elements.length; i++) {
elements[i].addEventListener('mouseover', function(event) {
if (event.target == this) {
this.classList.add('iframe_hover')
}else{
this.classList.remove('iframe_hover')
}
})
elements[i].addEventListener('mouseleave', function(event) {
if (event.target == this) {
this.classList.remove('iframe_hover')
}
})
elements[i].addEventListener('click', function(event) {
if (event.target == this) {
setItem("key", event.target.tagName)
}
})
}
} else{
document.addEventListener('click', function(event) {
getItem("key")
})
}
function setItem(key, value) {
chrome.runtime.sendMessage({
method : 'setItem',
key : key,
value : value
},
function(response) {
alert("setted item")
})
}
function getItem(key) {
chrome.runtime.sendMessage({
method : 'getItem',
key : key
},
function(response) {
$('body').css('background', '#000')
alert("getted item")
alert(response.data)
})
}


○ background.js
chrome.webRequest.onHeadersReceived.addListener(function(details) {
return {
responseHeaders : details.responseHeaders.filter(function(header) {
return (header.name.toLowerCase() !== 'x-frame-options')
})
}
}, {
urls : [ "<all_urls>" ]
}, [ "blocking", "responseHeaders" ])
var ss = sessionStorage
chrome.runtime.onMessage.addListener(function(request, sender, callback) {
if (request) {
if (request.method == 'setItem') {
ss.setItem(request.key, request.value)
callback({
data : ss.getItem(request.key)
})
} else if (request.method == 'getItem') {
callback({
data : ss.getItem(request.key)
})
} else if (req.method == 'clear') {
ss.clear()
}
}
return true
})
view raw background.js hosted with ❤ by GitHub



◆ コード解説
1点、解説を加えておく。
contentScript.jsは親子両方に読み込まれるため、共通処理以外のjsコードは、親子用で条件を加えている。
// iframe用
if (window != parent) {
  ~snip~
} // 親用
else{
  ~snip~
}