Cloudflare Streamでプレイヤーの埋め込みドメインを制限してみた
はじめに
清水です。Cloudflareの動画配信プラットフォームのサービスCloudflare Streamを試しています。Cloudflare Streamの動画コンテンツ保護機能の1つとして、プレイヤーの埋め込み先ドメインを指定する機能があります。許可されたドメインのページに埋め込まれたStream Playerだけが対象の動画コンテンツを再生できる、というわけです。ドキュメントでは「Hotlinking Protection」と呼ばれ、またダッシュボードなど設定画面では「許可されたオリジン/Allowed Origins」と表記されています。本エントリではこのHotlinking Protection/許可対象のオリジン指定について、実際に試しつつ、どのような仕組みで制限しているのか推測してみたのでまとめてみたいと思います。Allowed Originsの名称からも推測できるように、おそらくCORSのためのAccess-Control-Allow-Originヘッダを利用する、というような仕組みで制限していると推測しています。
Hotlinking Protectionの有効化
まずはダッシュボードからHotlinking Protectionを有効にします。Hotlinking Protectionはビデオごとに設定していきます。ダッシュボードでビデオの詳細ページに進み、「設定」タブの「許可されたオリジン」の箇所に、プレイヤーやマニフェストURLの埋め込みを許可するドメインを記載します。ドキュメント「Hotlinking Protection - Securing your Stream · Cloudflare Stream docs」には記載の際の注意事項として以下が挙げられます。
*.example.com
ではexample.com
(ネイキッドドメイン)とwww.example.com
(サブドメイン)、そしてaa.bb.example.com
(サブサブドメイン)がサポートされるexample.com
ではwww.example.com
などのサブドメインはサポートされない- パスはサポートされない。
example.com
を指定した場合はexample.com/*
として扱われる
今回はstream-player.example.net
を指定しました。
またAPIでもHotlinking Protection、埋め込みを許可するドメイン(オリジン)の設定は可能です。以下の形式でJSONデータをPOSTします。
curl -X POST \ -H "Authorization: Bearer ${TOKEN}" \ --data "{\"uid\":\"${VIDEO_UID}\", \"allowedOrigins\": [\"stream-player.example.net\"]}" \ "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT}/stream/${VIDEO_UID}"
以下が実際の実行例です。設定後、"requireSignedURLs": true
となっていることが確認できます。
##### 設定前の状態を確認 % curl -X GET -H "Authorization: Bearer ${TOKEN}" "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT}/stream/${VIDEO_UID}" { "result": { "uid": "bbd3xxxxxxxxxxxxxxxxxxxxxxxxxxxx", "creator": "user-musashino", "thumbnail": "https://cloudflarestream.com/bbd3xxxxxxxxxxxxxxxxxxxxxxxxxxxx/thumbnails/thumbnail.jpg", "thumbnailTimestampPct": 0, "readyToStream": true, "status": { "state": "ready", "pctComplete": "100.000000", "errorReasonCode": "", "errorReasonText": "" }, "meta": { "downloaded-from": "https://mybucket.s3.ap-northeast-1.amazonaws.com/IMG_3435.MOV", "name": "吉祥寺の駅" }, "created": "2022-04-30T10:53:00.119086Z", "modified": "2022-06-29T13:37:08.702328Z", "size": 336065892, "preview": "https://watch.videodelivery.net/bbd3xxxxxxxxxxxxxxxxxxxxxxxxxxxx", "allowedOrigins": [], "requireSignedURLs": false, "uploaded": "2022-04-30T10:53:00.119078Z", "uploadExpiry": null, "maxSizeBytes": null, "maxDurationSeconds": null, "duration": 53.8, "input": { "width": 3840, "height": 2160 }, "playback": { "hls": "https://cloudflarestream.com/bbd3xxxxxxxxxxxxxxxxxxxxxxxxxxxx/manifest/video.m3u8", "dash": "https://cloudflarestream.com/bbd3xxxxxxxxxxxxxxxxxxxxxxxxxxxx/manifest/video.mpd" }, "watermark": null }, "success": true, "errors": [], "messages": [] } ##### Hotlinking Protectionを有効化 % curl -X POST \ -H "Authorization: Bearer ${TOKEN}" \ --data "{\"uid\":\"${VIDEO_UID}\", \"allowedOrigins\": [\"stream-player.example.net\"]}" \ "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT}/stream/${VIDEO_UID}" { "result": { "uid": "bbd3xxxxxxxxxxxxxxxxxxxxxxxxxxxx", "creator": "user-musashino", "thumbnail": "https://cloudflarestream.com/bbd3xxxxxxxxxxxxxxxxxxxxxxxxxxxx/thumbnails/thumbnail.jpg", "thumbnailTimestampPct": 0, "readyToStream": true, "status": { "state": "ready", "pctComplete": "100.000000", "errorReasonCode": "", "errorReasonText": "" }, "meta": { "downloaded-from": "https://mybucket.s3.ap-northeast-1.amazonaws.com/IMG_3435.MOV", "name": "吉祥寺の駅" }, "created": "2022-04-30T10:53:00.119086Z", "modified": "2022-06-29T13:40:34.063972Z", "size": 336065892, "preview": "https://watch.videodelivery.net/bbd3xxxxxxxxxxxxxxxxxxxxxxxxxxxx", "allowedOrigins": [ "stream-player.example.net" ], "requireSignedURLs": false, "uploaded": "2022-04-30T10:53:00.119078Z", "uploadExpiry": null, "maxSizeBytes": null, "maxDurationSeconds": null, "duration": 53.8, "input": { "width": 3840, "height": 2160 }, "playback": { "hls": "https://cloudflarestream.com/bbd3xxxxxxxxxxxxxxxxxxxxxxxxxxxx/manifest/video.m3u8", "dash": "https://cloudflarestream.com/bbd3xxxxxxxxxxxxxxxxxxxxxxxxxxxx/manifest/video.mpd" }, "watermark": null }, "success": true, "errors": [], "messages": [] } ##### 設定後の状態を確認 % curl -X GET -H "Authorization: Bearer ${TOKEN}" "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT}/stream/${VIDEO_UID}" { "result": { "uid": "bbd3xxxxxxxxxxxxxxxxxxxxxxxxxxxx", "creator": "user-musashino", "thumbnail": "https://cloudflarestream.com/bbd3xxxxxxxxxxxxxxxxxxxxxxxxxxxx/thumbnails/thumbnail.jpg", "thumbnailTimestampPct": 0, "readyToStream": true, "status": { "state": "ready", "pctComplete": "100.000000", "errorReasonCode": "", "errorReasonText": "" }, "meta": { "downloaded-from": "https://mybucket.s3.ap-northeast-1.amazonaws.com/IMG_3435.MOV", "name": "吉祥寺の駅" }, "created": "2022-04-30T10:53:00.119086Z", "modified": "2022-06-29T13:40:34.063972Z", "size": 336065892, "preview": "https://watch.videodelivery.net/bbd3xxxxxxxxxxxxxxxxxxxxxxxxxxxx", "allowedOrigins": [ "stream-player.example.net" ], "requireSignedURLs": false, "uploaded": "2022-04-30T10:53:00.119078Z", "uploadExpiry": null, "maxSizeBytes": null, "maxDurationSeconds": null, "duration": 53.8, "input": { "width": 3840, "height": 2160 }, "playback": { "hls": "https://cloudflarestream.com/bbd3xxxxxxxxxxxxxxxxxxxxxxxxxxxx/manifest/video.m3u8", "dash": "https://cloudflarestream.com/bbd3xxxxxxxxxxxxxxxxxxxxxxxxxxxx/manifest/video.mpd" }, "watermark": null }, "success": true, "errors": [], "messages": [] }
指定したドメインに埋め込んだプレイヤーで再生できることの確認
Hotlinking Protection設定後、まずは実際に許可したドメインのページで埋め込んだStream Playerにて動画が再生できることを確認してみましょう。stream-player.example.net
のドメイン配下に、以下のstream-player.html
のページを作成しました。
<html> <head> <title>Stream Player</title> </head> <body> <div style="position: relative; padding-top: 56.25%"> <iframe src="https://iframe.videodelivery.net/bbd3xxxxxxxxxxxxxxxxxxxxxxxxxxxx" style="border: none; position: absolute; top: 0; height: 100%; width: 100%" allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;" allowfullscreen="true" ></iframe> </div> </body> </html>
いざ、Google Chromeブラウザでhttps://stream-player.example.net/stream-player.html
を開いて再生してみます。問題なく再生ができていますね!
指定していないドメインでは再生できないことの確認
続いて、埋め込みを許可していないドメインページで確認してみましょう。not-allowed-origin.example.info
というドメインを準備、配下に先程と同様、stream-player.html
のページを配置しました。https://not-allowed-origin.example.info/stream-player.html
にアクセスしてみます。「This video has not been configured to be allowed on this domain.」と表示され、動画は再生できませんね。
またこのとき、iframe.videodelivery.net
からは403のステータスコードが返ってきていました。
もう一つ、Stream Playerのiframeのドメインiframe.videodelivery.net
での再生についても確認しておきましょう。https://iframe.videodelivery.net/$VIDEOID
のURLを直接ブラウザで開いてみます。こちらも再生できません。先ほどと同じく「This video has not been configured to be allowed on this domain.」とメッセージが表示されています。
こちらも確認すると、403 Forbiddenが返ってきていました。
HLSマニフェストURLを使用した場合の挙動の確認
HLSマニフェストURL使用時の再生可否
設定したとおり、許可したドメインではStream Playerでの再生ができ、それ以外のドメインでは再生ができないという状況でした。ここで、Stream PlayerではなくマニフェストURLを使って独自プレイヤーで再生する場合の挙動も確認してみましょう。例として、VideoJS HTTP Streaming (VHS)でHLSマニフェストURLを入力して再生しようと試みます。が、videojs.github.io
ドメインでの再生になることからか再生ができません。確認してみるとCORSエラーが発生している状況でした。
CORSエラーが発生している、ということはJavaScriptを使用しないケースでは再生できるかもしれませんね。ためしにHLSのネイティブ再生(JavaScript製プレイヤーではなく、ブラウザのプレイヤーで再生)に対応しているmacOS上のSafariブラウザで確認してみたところ、こちらは再生が可能でした。
ということで、ドメインの制御としてCORSで許可する・しないを制限している、という推測ができます。
HLSマニフェストURLのAccess-Control-Allow-Originヘッダの確認
CORSでの制御という推測を裏付けるため、Access-Control-Allow-Originヘッダについても確認してみます。まずはHLSマニフェストURLに対してcurl
コマンドでヘッダ情報だけ確認してみます。こちらはaccess-control-allow-origin: *
ということで、予想と異なりどんなドメイン(オリジン)からでもCORSが許可されていそうです。
% curl -I https://cloudflarestream.com/bbd3xxxxxxxxxxxxxxxxxxxxxxxxxxxx/manifest/video.m3u8 HTTP/2 200 date: Wed, 29 Jun 2022 14:04:47 GMT content-type: application/x-mpegURL access-control-allow-origin: * cache-control: public, max-age=600 vary: origin, referer access-control-allow-headers: range access-control-expose-header: cf-ray stream-dw-version: 2022.6.2 expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" server: cloudflare cf-ray: 722f369d6b8a344b-NRT
それでは、と試しに許可されていないドメインをリクエスト時のOrigin
ヘッダに指定してみます。今度はレスポンスにaccess-control-allow-origin: https://videodelivery.net
というヘッダが付与されました。このvideodelivery.net
ドメインはCloudflare Streamで利用されているものです。結果、Originヘッダに指定したドメインからはCORSエラーが発生することになります。
% curl -I -H "Origin: not-allowed-origin.example.info" https://cloudflarestream.com/bbd3xxxxxxxxxxxxxxxxxxxxxxxxxxxx/manifest/video.m3u8 HTTP/2 200 date: Wed, 29 Jun 2022 14:08:56 GMT content-type: application/x-mpegURL access-control-allow-origin: https://videodelivery.net cache-control: public, max-age=600 vary: origin, referer access-control-allow-headers: range access-control-expose-header: cf-ray stream-dw-version: 2022.6.2 expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" server: cloudflare cf-ray: 722f3caba93caf81-NRT
許可したドメインでも試してみましょう。"Origin: stream-player.example.net"をリクエスト時のヘッダに付与してみます。レスポンスにはこのドメインがaccess-control-allow-origin
ヘッダに含まれる結果となりました。これならCORSに対応し動画の再生が可能になります。
% curl -I -H "Origin: stream-player.example.net" https://cloudflarestream.com/bbd3xxxxxxxxxxxxxxxxxxxxxxxxxxxx/manifest/video.m3u8 HTTP/2 200 date: Wed, 29 Jun 2022 14:09:22 GMT content-type: application/x-mpegURL access-control-allow-origin: stream-player.example.net cache-control: public, max-age=600 vary: origin, referer access-control-allow-headers: range access-control-expose-header: cf-ray stream-dw-version: 2022.6.2 expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" server: cloudflare cf-ray: 722f3d4fcd19afe1-NRT
あくまでHLSマニフェストURLを対象にした挙動ですが、リクエスト時のOriginヘッダをもとにレスポンスのAccess-Control-Allow-Originヘッダを調整し、再生(CORS)可能なドメインを制限している、と推測しています。マニフェストURLではなくStream Playerの場合は403エラーということでまったく同じではないかと思いますが、このようにリクエスト時の(iframe埋め込みもと?)ドメインを確認してレスポンスのステータスや内容を変更しているのでは、と推測しています。
まとめ
Cloudflare StreamのHotlinking Protection機能について実際に試しつつ、挙動などからその仕組みを推測してみました。ドキュメントに具体的な記載はありませんでしたが、HLSマニフェストURLの場合はレスポンスのAccess-Control-Allow-Originヘッダの挙動からCORSを利用して許可したドメインのみから再生可能にしているようです。Stream Playerの場合もリクエスト時の埋め込みもとドメインの情報からレスポンスのステータスを決定、再生制限していると予想しています。
ドキュメント「Securing your Stream」の項目には、このHotlinking Protection、Playerの埋め込み可能なドメインの指定とあわせて署名付きURLを利用することで、よりセキュアに動画の視聴制限を実現できるとしています。実際の本番運用にあたってはこのHotlinking Protectionも是非とも設定しておきましょう。