◆ content script の空間なら cors 無視して fetch できるけど https/http の制限は受けた
◆ externally_connectable は全サイトで許可できない
◆ 手間だけど page ←→ content script ←→ background で通信するのがよさそう

CORS 制限ホントいらない

クロスオリジンだと fetch できないの不便すぎです
画像を canvas に貼ったらピクセルのデータ取り出せないとか CSS のスタイル定義を見れないとか 一々制限が多すぎます

セキュリティ対策といっても script タグで JavaScript ファイルはなんでも実行できるし JavaScript の中に DataURI 形式で任意のバイナリ仕込めるんだし 悪いことしようとしてる人からすれば 特に困るようには思えません
逆に 普通に何か作る人がサーバ分けた時などに苦労するだけです

ありえるのは CSRF (意図しないリクエスト送って ユーザが知らない間に何か操作したことになってる) かなと思いましたが これって CORS 制限じゃ防げてないですよね
クライアントが受け取れないだけで リクエストはサーバへ飛んでるから サーバではちゃんとリクエストどおりの処理がされています
リクエストの結果を受け取れないだけで ブラウザが CORS を禁止していてもいなくてもサーバの処理は一緒です
クロスドメイン制限邪魔!

他は 「勝手にウチのサイトの リソース持ってかないでよ」 っていうのでしょうか
しかしこれも 基本的な img タグなどは普通にクロスオリジンのリソースを見れます
あまり意味が感じられません
できないのは それらを JavaScript で処理 と言う部分だけです
しかも この場合はブラウザで対策することじゃなくて サーバ管理する側が個別に対策すべきのはずです

そんな感じでクロスオリジンの制限の利点が思いつきません

前に クロスオリジンでも通信できるように全部自サーバを経由させて クロスオリジンじゃなくすようにしました
cross origin な ajax 通信したい

でもこれはちょっと大変なので 今回はお手軽にできるように拡張機能をつくりました

cofetch1

使い方は fetch の代わりに cofetch という関数を使って
cofetch.asJson("http://fedora/sample.json").then(e => console.log(e))

となるようにします

fetch は background で行って cofetch というオブジェクトをタブ内に作るために content script も用意します

content script で直接関数などを作ってもページ内の JavaScript から使えないので script タグを経由してページ側の JavaScript 空間で関数定義するようにします

流れは

初期設定:
content script
↓ page に script を送って初期化処理を行う
page

cofetch:
page
↓ cofetch を実行
content script
↓ background へリクエスト情報を送る
background (ここで fetch する)
↓ 結果を content script へ返す
content script
↓ page へレスポンスを送る
page

と言う感じです


background は URL と メソッド (text / json / arraybuffer) を受け取って結果を返します

[background.js]
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
fetch(message.url)
.then(e => e[message.method]())
.catch(e => ({cofetch_error: e.message}))
.then(data => {
if(message.method === "arrayBuffer"){
sendResponse(Array.from(new Uint8Array(data)))
}else{
sendResponse(data)
}
})

return true
})

chrome extension のメッセージでは受け渡しできる型に制限があるので ArrayBuffer 型は普通の配列にしています

[cscript.js]
var s = document.createElement("script")
s.innerHTML = `
!function(){
window.cofetch = {
asText(url){return sendFromExtSpace(url, "text")},
asJson(url){return sendFromExtSpace(url, "json")},
asABuf(url){return sendFromExtSpace(url, "arrayBuffer")},
}

var fmap = {}

window.addEventListener("message", eve => {
if(eve.data && eve.data.cftv && eve.data.cftv.result){
var fn = fmap[eve.data.cftv.key]
fn && fn(eve.data.cftv.result)
}
}, false)

function sendFromExtSpace(url, method){
return new Promise((resolve, reject) => {
var key = Date.now() + Math.random() + ""
var data = {cftv: {key, url, method}}

fmap[key] = result => {
if(result.cofetch_error){
reject(result.cofetch_error)
}else{
resolve(result)
}
}

window.postMessage(data, "*")
})
}
}()
`

window.addEventListener("message", eve => {
if(eve.data && eve.data.cftv && eve.data.cftv.url){
var cftv = eve.data.cftv
var data = {cftv: {key: cftv.key}}

chrome.runtime.sendMessage(eve.data.cftv, result => {
if(cftv.method === "arrayBuffer"){
data.cftv.result = Uint8Array.from(result).buffer
}else{
data.cftv.result = result
}
window.postMessage(data, "*")
})
}
}, false)

document.head.appendChild(s)
s.remove()

前半で script タグを作っています
`` の内側は ページの JavaScript 空間で実行されます

後半の リスナ設定は content script が background と通信する処理です


これで cofetch で好きなデータ取得をできます

cofetch2

できはしたのですが 長いので改良したいと思います

fetch が background でしないとダメだと思っていました
ですが content script 空間なら クロスオリジンでも fetch 可能でした

なので background は消してしまい content script だけにおさめてしまいます

[cscript.js]
var s = document.createElement("script")
s.innerHTML = `
!function(){
window.cofetch = {
asText(url){return sendFromExtSpace(url, "text")},
asJson(url){return sendFromExtSpace(url, "json")},
asABuf(url){return sendFromExtSpace(url, "arrayBuffer")},
}

var fmap = {}

window.addEventListener("message", eve => {
if(eve.data && eve.data.cftv && eve.data.cftv.result){
var fn = fmap[eve.data.cftv.key]
fn && fn(eve.data.cftv.result)
}
}, false)

function sendFromExtSpace(url, method){
return new Promise((resolve, reject) => {
var key = Date.now() + Math.random() + ""
var data = {cftv: {key, url, method}}

fmap[key] = result => {
if(result.cofetch_error){
reject(result.cofetch_error)
}else{
resolve(result)
}
}

window.postMessage(data, "*")
})
}
}()
`

window.addEventListener("message", async eve => {
if(eve.data && eve.data.cftv && eve.data.cftv.url){
var cftv = eve.data.cftv
var data = {cftv: {key: cftv.key}}

var result = await fetch(cftv.url).then(e => e[cftv.method](), err => {cofetch_error: err.message})
data.cftv.result = result

window.postMessage(data, "*")
}
}, false)

document.head.appendChild(s)
s.remove()

前半は一緒です
content script 側で行う addEventListener の中が変わりました
background への通信はせずにその場で fetch して結果を返します

window.postMessage では ArrayBuffer をそのまま送れるので分岐も減ってシンプルです


ただ 後から気づいたのですが 重大な問題がありました


https から http へアクセスできない!


クロスオリジンはいけても content script 空間では https/http の違いはそのページと同じになるようです

cofetch3

せっかく短くしたのに https/http で通信できないのは不便なので他の方法を探してると
"externally_connectable": {
"matches": [ "http://server/*" ]
}

というのがあるみたいです

拡張機能が外部のものと通信するときの許可を設定します
matches に URL を書くとそのページから 拡張機能にアクセスできます

これまで
page←→ content script ←→ background
だったのは
page ←→ background
の直接通信ができなかったからです

ですが このオプションで通信できそうです

content script では直接 background に対して chrome.runtime.sendMessage でメッセージを送ります

[cscript.js]
var s = document.createElement("script")
s.innerHTML = `
!function(){
window.cofetch = {
asText(url){return send(url, "text")},
asJson(url){return send(url, "json")},
asABuf(url){return send(url, "arrayBuffer")},
}

function send(url, method){
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage("${chrome.runtime.id}", {url, method}, result => {
if(result.cofetch_error){
reject(result.cofetch_error)
}else if(method === "arrayBuffer"){
resolve(Uint8Array.from(result).buffer)
}else{
resolve(result)
}
})
})
}
}()
`
document.head.appendChild(s)
s.remove()

background では 受け取る方法が chrome.runtime.onMessageExternal.addListener に変わります

[background.js]
chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => {
fetch(message.url)
.then(e => e[message.method]())
.then(data => {
if(message.method === "arrayBuffer"){
sendResponse(Array.from(new Uint8Array(data)))
}else{
sendResponse(data)
}
}, err => sendResponse({cofetch_error: err.message}))

return true
})

これで 許可されてる URL からは直接通信ができて cofetch が動きます


ですが

またも問題がありました
URL にいつもどおり <all_urls> を指定しようとしたらエラーになりました

こういう制限があるようです
Patterns cannot include wildcard domains nor subdomains of (effective) top level domains;

<all_urls> や *://*/* などはできないです

また effective top level domains はダメのようで co.jp だけや com だけの指定はだめになります
"*://*.co.jp/*"
"*://*.com/*"

これならエラーになりません
"*://*.abcde.co.jp/*"

ただ これだと全部の URL を許可というのは難しそうです
やっぱり最初の page/content script/background の 3 層になってるのが万能みたいです

さいごに

これはあくまで 拡張機能入れてる人だけが使える機能です

見る人全員にいれてください というのは無理がありますし 基本は個人的に使うものです


私の場合は 基本そのとき見てる URL のページで F12 キーで devtools 出して JavaScript コード動かしてみてるのが多いので ページによって https/http の通信だとか クロスオリジンだから fetch できなかったとか言われるのがすごく鬱陶しいです

これまでは fetch したいページと同じオリジンのタブを開いてそっちで devtools 開き直して とやってたのですが それも頻繁にあると面倒でした

この拡張機能あるとそういうことを気にしなくていいのでかなーり捗ります

ただ慣れてしまうと 普段の公開するところで不便と感じるレベルが上がりますのでちょっと注意です