#Overview
セキュリティ上の観点から、通常別のサイトからのデータは取得できない。
しかし、許可されている別のサイトならデータを取得を可能にするCORSという仕組みがある。
これのおかげでセキュアながら、連携したい部分とは連携できるようになっている。
しかし、全く関係のないサイトにはCORSの設定ができないため、データが取得できないという厳しい制約がある。
いろんなサイトのOGP画像をサーバーを介さずに取得してプレビューを作ろうと考えた。
HTMLファイルの取得で以下のよく見かけるエラーに遭遇。
Access to fetch at '<another site url>' from origin 'http://localhost:3000' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
HTMLファイルだけならどうにか取得する方法あるんじゃないか?
そもそもどこかに穴があれば狙われるからまず考えられないが、テキストとして取れないか?という希望があると思わないとどうにかなりそうで
半分は敗北の未来をわかっていながら、もしかしたら?と浅はかながらもいくつか試したことを残しておく。
結論としてはタイトル通り取得できなかったので、この先に何一つ希望はないということは断っておきたい。
#Target reader
- CORSを使わずに何を試したのか興味のある方。
#Prerequisite
- CORSが何かを理解している方。
- Reactアプリに実装しているため、コードはすべてReactのコードの抜粋になります。
- ブラウザはWindowsのChromeを使いました。
#Body
Fetch API
CORSのことが完全に頭になかった時に普通に別サイトのHTMLを取得しようとしたコード。
fetch()
で冒頭に掲載したAccess-Control-Allow-Origin
がないと怒られる。
XMLHttpRequest
についてはMDNにfetch()
と同様と読み取れたため試していない。
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS
const fetchHtml = async (url) => {
const response = await fetch(url);
if (!response.ok) {
const description = `status code:${response.status} , text:${response.statusText}`;
throw new Error(description);
}
return await response.text();
}
iframe
TwitterやGoogle Mapや広告等はiframeで自分のサイトに埋め込むことができる。
実際デベロッパーツールではiframe内のDOMにアクセスできるため、これならいけるかも?とiframeに表示してそこにアクセスできないか試してみた。
const onLoadIframe = () => {
const iframe = document.querySelector("#inlineFrameExample");
const iframeDocument = iframe.contentWindow.document;
const a = iframeDocument.querySelector("a");
console.log("a:", a);
}
onLoadIframe
をiframe
のonload
イベントに接続して実施。
結果としては、HTMLは表示されるもののiframeDocument
の行でSecurityError
なるものが発生しアクセス不可。
デベロッパーツールならみれるのにコードからは見れないのか
object
iframeの派生形でobjectタグを使ってテキストとして読み込ませたらどうだろうか?
残念ながらtext/plain
でテキスト指定しても、丁寧にHTMLとして読み込んでiframeと表示は変化なし。
結果としてもiframe同様SecurityError
なるものが発生しアクセス不可。
<object id="inlineFrameExample"
width="100%"
height="100%"
data="https://sample.com/"
type="text/plain"
onLoad={handleiFrameonLoad}
>
</object>
winow.open()
iframeはダメだったが、winow.open()
はどうだろうか?
半分駄目だろうなと思っていたが、やることに意味があると思う
winow.open()
ってこんなやつだったなぁ~と懐かしみつつコードを書いてみた。
しかし、情けないことにaddEventListener
やonload
でFunctionを着火させられなかったため、window.open()
の5秒後にquerySelector()
を実行するコードになっている
const openWindow = (url) => {
const w = window.open("");
// w.addEventListener("load", (event) => {
// const a = w.document.querySelector("a");
// console.log("a:", a);
// })
// w.onload = (event) => {
// const a = w.document.querySelector("a");
// console.log("a:", a);
// }
w.location = url;
setTimeout(() => {
const a = w.document.querySelector("a");
console.log("a:", a);
}, 5000)
}
結論としてはw.document.querySelector("a")
が動作したところで、iframe同様SecurityError
なるものが発生しアクセス不可。
これは想定していた…うん…
Web Worker
色々調べていたらWeb Workerなるものが結構前からいることを知る!
https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Using_web_workers
Service Workerしか知らなかったので、ブラウザで以前から複数スレッド使えるのには驚いた。
サーバーサイドでは普通に取得できるし、それを受け取るのも問題ないから、DOMも扱えないバックグラウンドで動くこいつならCORS無視できるのかも?という淡い期待でトライしてみる。
基本的にはこのコードを拝借させていただいた。
https://github.com/facebook/create-react-app/issues/1277#issuecomment-345516463
MyWorkerの部分がエラーになったのでエラー箇所だけ修正した。
export default class WebWorker {
constructor(worker) {
let code = worker.toString();
code = code.substring(code.indexOf("{") + 1, code.lastIndexOf("}"));
// console.log("code:", code)
const blob = new Blob([code], { type: "application/javascript" });
return new Worker(URL.createObjectURL(blob));
}
}
export default function MyWorker(args) {
onmessage = (e) => {
const { url } = e.data;
fetch(url)
.then(response => response.text())
.then(html => console.log(html));
postMessage("Response");
};
}
import WebWorker from './utils/webworker';
import MyWorker from './utils/myWorker';
const fetchHtml = async (url) => {
// Worker initialisation
const workerInstance = new WebWorker(MyWorker);
// Communication with worker
workerInstance.addEventListener("message", e => console.log(e.data), false);
workerInstance.postMessage({url});
}
注意したいのがmyWorker.js
。
fetch()
の部分はawait
したくなるが、onmessage
にasync
を付与するとnew Worker
でエラーが発生する。
webWorker.js
でcode
の出力を見たが、async
の影響で出力されたコードが結構変わるのでそこが影響しているのかもしれない。
await
はあきらめてthen()
でつないでいるが、postMessage("Response")
をHTMLを取得後に出すならthen()
でつなごう
実行結果としてはFetch API
同様Access-Control-Allow-Origin
がないエラーだった気がする。
リダイレクト
視点を変えてCORSのプリフライト後に別サイトにリダイレクトしたらもしかしたら行けないだろうか?と思いつく。
リダイレクトは許可していない旨をMDNで見たような気もするが、この際やれることはやろう。
ソースコードはCloud Functions(サーバーサイド)のコード。
プリプライトは通常通り許可し、本番のGETできたときに302応答でフロントエンドが取得要求しているurlにリダイレクトさせる。
exports.fetchHtml = async (req, res) => {
res.set('Access-Control-Allow-Origin', '*');
if (req.method === 'OPTIONS') {
// Send response to OPTIONS requests
res.set('Access-Control-Allow-Methods', 'GET,POST');
res.set('Access-Control-Allow-Headers', 'Content-Type');
res.set('Access-Control-Max-Age', '-1'); // disable cache
res.status(204).send('');
} else {
res.redirect(302, res.body.url);
}
};
結果としてはFetch API
と同様で、リダイレクトの効果はなかった。
WebAssembly
執筆後にWebAssemblyならもしかしてCORSが必要ないとかいうことないだろうか?と思いつく。
簡単に調べてみたところ、BlazorでもCORSが必要とドキュメントにあるためトライせずに終了
Blazor WebAssembly サンプル アプリ (BlazorWebAssemblySample) は、呼び出し Web API コンポーネント (Pages/CallWebAPI.razor) で CORS を使用する具体的な方法を示しています。
#Conclusion
比較的簡単に思いつく方法で試してみましたが全てNGで、結局はサーバーで取得してフロントに渡すしかないかなという結論です。
ブラウザで表示はできるし、デベロッパーツールでDOMにもアクセスできるけど、プログラムとしては取得不可…(゜-゜)
仮に方法があったとしても全てのブラウザで動作しないかもしれないし、いずれ塞がれるかもしれないと前途多難。
それでもなお挑戦する方の時間節約になればこれ幸い
モバイルアプリなら、モバイルアプリでテキスト取得してWebViewにデータ渡せそうな気がする(確証なし)…まあそれだけのためにモバイルアプリは作らないけど
Have a great day!
#References
Create React AppのWeb Workerサポートのissue
https://github.com/facebook/create-react-app/issues/3660