cross origin fetch 拡張機能つくった
- カテゴリ:
- ChromeExtension
- コメント数:
- Comments: 0
◆ content script の空間なら cors 無視して fetch できるけど https/http の制限は受けた
◆ externally_connectable は全サイトで許可できない
◆ 手間だけど page ←→ content script ←→ background で通信するのがよさそう
◆ 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
↓ page に script を送って初期化処理を行う
page
cofetch:
page
↓ cofetch を実行
content script
↓ background へリクエスト情報を送る
background (ここで fetch する)
↓ 結果を content script へ返す
content script
↓ page へレスポンスを送る
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
})
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()
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()
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": [ "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()
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
})
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 開き直して とやってたのですが それも頻繁にあると面倒でした
この拡張機能あるとそういうことを気にしなくていいのでかなーり捗ります
ただ慣れてしまうと 普段の公開するところで不便と感じるレベルが上がりますのでちょっと注意です