ngrokによるBasic認証M5Cameraサーバーの映像をGoogle Colaboratoryに表示させてOpenCVで画像処理させてみた

Google colabにBasic認証有りのngrokを使い、M5Cameraストリーミング表示させ、OpenCVもつかってみた。 M5Stack

今回はかなりスゴイです。
Google Colaboratory上でPythonとHTMLおよびJavaScriptを連携させて、M5Cameraの動画ストリーミングおよび遠隔操作ができました。しかもOpenCVによる画像処理がリアルタイムでできたんです。
そして、最も自慢したいことは、ngrokによるBasic認証下のM5Cameraサーバーでも、HTMLのCanvasに映像を投影できて、そのデータをOpenCVに送ることが出来たことです。

スポンサーリンク

今回は、異なるサーバーのオリジン間で画像データを共有するために、ブラウザのCORSエラーを解消するのにかなり苦労しました。
とにかく、以下の成果をご覧ください。

ついにやったぜ!!!

ローカルWi-FiサーバーのM5Cameraの映像がGoogle Colaboratory上にストリーミングできていますね。しかも、ngrokのBasic認証を通過できています。
そして、HTMLのCanvas要素の映像をリアルタイムでOpenCVに送って画像処理できていますね。

これをわざわざM5Cameraでやらなくても、ラズパイカメラでやった方が遙かに簡単かも知れません。
でも、Wi-Fi接続のM5Cameraでやることによって、ブラウザとサーバー間のやり取りが良くわかって、より理解が深まったと思います。

では、この方法を紹介していきます。
因みにこれは、Windows10環境で実験していますので、Macパソコンでできるかどうかは分かりません。
そして、私はPythonもJavaScriptも含めてプログラミング全般について独学の素人です。
何か誤り等がありましたら、コメント投稿でご連絡いただけると助かります。

    【目次】

  1. 事前準備
  2. ngrokのBasic認証下ではプリフライトリクエストを受け付けなかった
  3. ngrokのBasic認証下でCORSエラーを解消した経緯
  4. Google Colaboratory PythonおよびJavaScriptコード
  5. M5Camera側Arduinoスケッチ(プログラムソースコード)
  6. コンパイル書き込み実行、およびランタイム実行
  7. まとめ

事前準備

使ったもの

M5Camera

Wi-Fi&Bluetoothマイコンモジュール ESP32-WROVER搭載、OmniVision製イメージセンサOV2640搭載のM5Stack社製モジュールです。技適取得済みのものです。
手軽にイメージセンサをWi-Fiでリモート制御できるのでお勧めです。
スイッチサイエンスさんで販売しています。

https://www.switch-science.com/catalog/5207/

因みに、当ブログでレビューした記事もありますので、参考にしてみてください。
M5Camera をレビューしてみた。分解したり、Arduino IDE でスマホに映したりする実験

パソコンおよびUSBケーブル

ここでは、Windows10パソコンを使いました。
Macで動作するかどうかはわかりません。

USBケーブルはArduino IDEでプログラムをM5Cameraに書き込みする時に使います。
良質で太く短いケーブルを使う事をお勧めします。

Wi-Fiルーター環境

M5CameraのWi-Fiは、STAモードという外部ルーターネットワーク内で動作させますので、別途Wi-Fi環境が必要です。
予めファイアウォール設定やMACアドレスフィルタリング等の設定を確認しておき、M5Cameraが接続できるようにしておきます。

Google Colaboratoryを使えるようにしておく

Google Colaboratoryについては以下の記事を参照して、予め使える状態にしておきます。

ディープラーニングのお勉強~その8。Google ColaboratoryでPythonプログラミングして機械学習してみた~

ngrokをインストールし、M5CameraのローカルIPアドレスをBasic認証有りで公開しておく

ngrokは、ローカルネットワークのサーバーを世界にWeb公開できるトンネリングツールです。
ngorkをパソコンおよびラズパイなどにインストールして置き、動かしておく必要があります。
今回は、Wi-FiローカルネットワークのM5CameraサーバーをBasic認証ありでIPアドレスを発行します。
インストール方法や簡単な使い方は以下の記事を参照してください。

ngrokインストール方法と簡単な使い方

M5CameraのWi-FiローカルIPアドレスについては、最後の方の章で紹介しているように、M5Cameraのスケッチをコンパイル書き込みした後、シリアルモニタに表示されます。

Arduino core for the ESP32をインストールしておく

ESP32-WROVER搭載のM5CameraをArduino IDEでプログラミングします。
予め、Arduino IDEとArduino core for the ESP32をインストールしておきます。
インストール方法は以下の記事を参照してください。

Arduino core for the ESP32 のインストール方法

動作確認済みバージョンは以下の通りです。

Arduino IDE 1.8.15
Arduino core for the ESP32 stable 1.0.6

Google Colaboratory上でHTMLおよびJavaScriptの動かし方を確認しておく

前回記事で紹介しましたが、Google Colaboratory上でHTMLおよびJavaScriptの動かし方を予め習得しておきます。
これができれば、Google Colaboratory上でWebと連携できます。

Google ColaboratoryのPythonでHTMLおよびJavaScriptを動かす

ngrokのBasic認証下ではプリフライトリクエストを受け付けなかった

では、プログラミングを紹介する前に、今回の実験で苦労したことを紹介していきます。
主にブラウザのエラーで散々悩まされました。
そんなわけで、まずはBasic認証サーバーへアクセスする時に、ブラウザの挙動を理解しておく必要があります。

今回、Google Colaboratory内にJavaScriptを組み、その中のFetch APIを使ってngrok経由でローカルのM5Cameraサーバーにアクセスします。
つまり、Google Colaboratoryサーバーが元のオリジンで、M5Cameraサーバーが別のオリジンとなり、異なるオリジン間で映像データのリソース共有をすることになります。以前のこちらの記事でも紹介した、CORS( Cross-Origin Resource Sharing)というやつです。

CORSについてネットで調べると、必ずと言っていいほどプリフライト(Preflight)という用語を目にしますね。
これがちょっと小難しいのです。
私自身、つい数日前に知ったばかりで、これに関してはド素人です。

プリフライトリクエストというのは、GETリクエストでデータをやり取りする前に、サーバー側がCORSポリシーに沿った約束事ができているかどうかをブラウザが事前にチェックするリクエストとのことだそうです。
詳しくは、MDNのPreflight request (プリフライトリクエスト)ドキュメントを参照してください。

単なるテキスト通信だけの単純リクエストの場合は、CORSでGETリクエストしても、ブラウザからはプリフライトリクエストは発生しません。
例えば、以下のような単純なFetch APIを使う場合です。

const uri = 'https://www.example.com/';
fetch(uri);

Basic認証サーバーへのアクセスではこれはエラーになります。

一方、CORSでリクエストヘッダに「単純でないもの」が入っていると、ブラウザからプリフライトリクエストが送信されます。
「単純でないもの」というと、headersにAuthorizationが入っていたり、application/jsonが入る場合です。
例えば、以下のようにBasic認証情報を加えてリクエストを送る場合です。

fetch(url, {
  method: 'GET',
  headers: {
    Authorization: 'Basic ' + btoa('user-name' + ':' + 'password'),
  },
  mode: 'cors'
})

こうすると、Google Chrome 91ではプリフライトリクエストを送信してきました。
デベロッパーツールで確認するとこんな感じです。

(図01)

プリフライトリクエストエラー

デベロッパーツールのプリフライトリクエストエラー

Method欄に”Preflight”という文字が見えますね。そして、CORSエラーになっています。
そして、2行目には、Method欄に”OPTIONS”とあります。
つまり、プリフライトリクエストでは、GETリクエストではなくて、OPTIONSリクエストになっているわけです。
ただ、M5Camera側のArduino IDE シリアルモニタには、OPTIONSリクエストを受信している様子はなく、ウンともスンとも言いませんでした。

この原因はなかなか分らなかったのですが、ようやく以下のngrok公式ドキュメントで答えを見つけました。
CORS with HTTP basic authentication

そこに書いてあることをDeepLで翻訳して見ると、こんな感じです。

ngrok の http トンネルでは、トンネルを保護するためにBasic認証情報を指定することができます。しかし ngrok は、CORS 仕様で要求されているプリフライト OPTIONS リクエストを含め、「すべての」リクエストに対してこのポリシーを適用します。この場合、お客様のアプリケーションで独自のBasic認証を実装する必要があります。詳細については、こちらの github issue をご覧ください。

ということで、そこで紹介してある以下のGitHub issueを見てみます。
https://github.com/inconshreveable/ngrok/issues/196

自分流に要約すると、CORSの仕様として定められているのは、本来、Basic認証サーバーへのプリフライトリクエストは認証を回避できるようにすべきであるが、ngrokの場合はプリフライトリクエストでもBasic認証を適用してしまうとのこと。
プリフライトリクエストは、Fetch APIでヘッダにAuthorization項目をセットしたとしても、それを送信しない仕様のため、ngrokでBasic認証有りにした場合は、プリフライトリクエストは通過できない、ということのようです。

ならば、Basic認証無しにして、プリフライトリクエストを許可させて、あとは自分自身でアプリ開発して新たなパスワード認証を設定するしかないと書いてありますが、それはあまり好ましくなく、予期しないセキュリティ脆弱性を招く恐れがあるとも書いてあります。

ところで、Basic認証についてよくよく調べてみると、MDNのHTTP 認証ドキュメントにBasic認証手順が記載されていました。
Basic認証サイトにアクセスする時、1回目のアクセスでサーバーからステータスコード401を返し、それによってブラウザが相手先がBasic認証有りのサイトであることを認識します。
そして、2回目のアクセス時にAuthorization項目をサーバーに送信すると書いてあります。

これは、私がつい数日前に初めて知ったことでした。
「へぇ~、そうだったんだ!」
って感じですね。
思わず自分のTwitterでもつぶやいてしまいました。
ブラウザはとっても複雑なことやってるんですね。

この件については、@ITサイトの以下の記事がとても分かり易かったので、参考になると思います。
「HTTP」の仕組みをおさらいしよう(その4)

なら、Fetchを2回に分けてheadersにAuthorization項目を含めてみることを試してみました。
しかし、2回とも同じ401エラーで終わってしまいました。
つまり、1回目のプリフライトで弾かれてしまい、それで終わりなのです。
プリフライト無しの単純リクエストにしても、1回目のリクエストでCORSエラーになって終わりでした。
結局、Basic認証有りのngrokサーバーへFetch APIでアクセスする場合、headersにAuthorizationを含めるのは無駄ということが分かりました。

ということで、今までBasic認証について個人的に軽く見ていましたが、今回の実験でブラウザというものは思っていたよりも複雑で緻密なやり取りをしていることがわかりました。セキュリティを高めるための先人の知恵が凝縮されているんですね。勉強になりました。

では、このことを踏まえて、次はブラウザのCORSエラーを解消していった経緯を紹介して行きます。

ngrokのBasic認証下でCORSエラーを解消した経緯

では、Basic認証とプリフライトリクエストについて少しは理解したところで、Google Colaboratory上のHTMLおよびJavaScriptのCanvasでCORSエラーを解消していった経緯を紹介したいと思います。
自分の備忘録も兼ねているため、ダラダラ長くなっています。

以前、こちらの記事で紹介した時もCORSでHTML Canvas画像を表示させていましたが、その時のCORSエラーは、img要素にcrossorigin=’anonymous’を設定して、M5Cameraサーバーからのレスポンスヘッダは、Access-Control-Allow-Origin: * を付加するだけで済みました。
しかし、今回はBasic認証下でデータをやり取りするため、その方式では通用しませんでした。

単純FetchによるBasic認証有りサーバーへのアクセスはCORSエラーになる

Google ColaboratoryからBasic認証有りのngrokを使ってM5CameraサーバーへGETリクエストを送る方法は、JavaScriptのFetch APIを使いました。
単純に何も設定しないFetchの場合、以下になります。

const url = ‘https://xxxxxxxx.jp.ngrok.io/’
fetch(url);

これでは残念ながらM5Cameraサーバー側にはリクエストが到達しません。
Basic認証ポップアップウィンドウも表示されません。

ブラウザGoogle Chromeのデベロッパーツールには、以下のエラーメッセージが出ました。

Access to fetch at ‘https://xxxxxxxx.jp.ngrok.io/’ from origin ‘https://xxxx-xxxx-x-colab.googleusercontent.com’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.

 

DeepLで日本語訳すると、こんな感じです。

オリジン「https://xxxx-xxxx-x-colab.googleusercontent.com」からの「https://xxxxxxxx.jp.ngrok.io/」でのフェッチへのアクセスは、CORSポリシーによってブロックされています。リクエストされたリソースに ‘Access-Control-Allow-Origin’ ヘッダがありません。不透明な応答が必要な場合は、リクエストのモードを ‘no-cors’ に設定して、CORS を無効にしてリソースを取得してください。

このメッセージを受けても、CORS無効にはできません。
ならば、M5Cameraサーバー側にAccess-Control-Allow-Originヘッダを追加してみました。

レスポンスヘッダに Access-Control-Allow-Origin: * を入れてもダメだった

では、先のデベロッパーツールのエラーメッセージを受けて、M5Cameraサーバーのレスポンスヘッダに、
Access-Control-Allow-Origin: *
を追加してみました。

でも、同じエラーで、全然ダメでした。
以前のこちらの記事の実験のセオリーでは、これでいけるかなと思ったのですが、的外れでした。

Fetch APIでheadersにユーザー名やパスワードを設定しても意味無し

もしかしたら、Fetchにユーザー名やパスワードが無いのが原因かと思い、以下のようにユーザー名とパスワードをBase64エンコードして、headersに含めてみました。

fetch(url, {
  method: 'GET',
  headers: {
    Authorization: 'Basic ' + btoa('user-name' + ':' + 'password'),
  },
  mode: 'cors'
});

これでも、M5Cameraサーバー側はウンともスンとも言わず、無反応でした。
実は、前章で説明したように、ブラウザからプリフライトリクエストが送信されて無視されていました。

Fetch APIでcredentials:’include’を設定するだけではダメ

前章でも述べたように、Fetch APIのheadersにBasic認証情報を載せても、ngrokサーバーには無意味でした。
そういえば、その方法はセキュリティ的にも問題ありでしたね。
JavaScriptコード中にパスワードが明記されている事自体、第三者に見られる危険性があって大問題ですからね。

ならば、ブラウザ側のBasic認証ポップアップウィンドウで手入力したユーザー名やパスワードをサーバー側に送ることができれば良いわけです。
それは、credentials: 'include'というオプション設定すればよいことが分かりました。
それについては、MDNのこちらのドキュメントを参照してください。
以下の感じです。

fetch(url, {
  credentials: 'include',
  mode: 'cors'
});

こうすると、ブラウザ内に保存されたユーザー名やパスワードを含めて、サーバー側へリクエストを送信してくれます。
しかし、残念ながらこれだけでは以下のエラーが出ました。

Access to fetch at ‘https://xxxxxxxx.jp.ngrok.io/’ from origin ‘https://xxxx-xxxx-x-colab.googleusercontent.com’ has been blocked by CORS policy: The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ‘*’ when the request’s credentials mode is ‘include’.

 

DeepLで翻訳すると、

オリジン「https://xxxx-xxxx-x-colab.googleusercontent.com」から「https://xxxxxxxx.jp.ngrok.io/」でのフェッチへのアクセスは、CORSポリシーによってブロックされました。リクエストの資格モードが「include」の場合、レスポンスの「Access-Control-Allow-Origin」ヘッダの値は、ワイルドカードの「*」であってはなりません。

これは、先ほど試したようにM5Cameraサーバー側のレスポンスヘッダに
Access-Control-Allow-Origin: *
としてしまったことによるものです。

ならば、Access-Control-Allow-Originに、Google ColaboratoryのオリジンURLをデベロッパーツールのログから探し出し、以下のように入れてみました。

Access-Control-Allow-Origin: https://xxxx-xxxx-x-colab.googleusercontent.com/

これでもまだ以下のエラーが出ました。

Access to fetch at ‘https://xxxxxxxx.jp.ngrok.io/control?var=first&val=0’ from origin ‘https://xxxx-xxxx-x-colab.googleusercontent.com’ has been blocked by CORS policy: The value of the ‘Access-Control-Allow-Credentials’ header in the response is ” which must be ‘true’ when the request’s credentials mode is ‘include’.

 

DeepLで翻訳すると、

オリジン「https://xxxx-xxxx-x-colab.googleusercontent.com」からの「https://xxxxxxxx.jp.ngrok.io/control?var=first&val=0」でのフェッチへのアクセスは、CORSポリシーによってブロックされています。レスポンスの ‘Access-Control-Allow-Credentials’ ヘッダの値は ‘空’ で、リクエストの資格情報モードが ‘include’ の場合は ‘true’ でなければなりません。

ならば、そのメッセージ通りにやってみました。
そうしたら、ようやく解決できたんです。

これでCORSエラー全面解決!

さて、これでM5Cameraサーバーのレスポンスヘッダに、
Access-Control-Allow-Credentials: true
を含めることにしました。
ただ、MDNのこちらのドキュメントを読んでいろいろ調べると、この他に以下の5種のヘッダを含める必要があると書いてあります。

Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Content-Type, origin, authorization
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Origin: https://xxxx-xxxx-x-colab.googleusercontent.com
Access-Control-Max-Age: 600

ただし、これはブラウザからプリフライトリクエストが有った場合限定のお話だということが要注意です。
プリフライトについては前章で述べましたが、プリフライトリクエストが飛ばなければこの5つのヘッダ情報は不要です。

結局、いろいろ試行錯誤した結果、上記の5つのヘッダのうち、Access-Control-Allow-CredentialsAccess-Control-Allow-Originの2つがあれば良いことが分かりました

ということで、結論!

Google Colaboratory側Fetch APIでは、

fetch(url, {
  credentials: 'include',
  mode: 'cors'
});

サーバー側レスポンスヘッダに以下を追加

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://xxxx-xxxx-x-colab.googleusercontent.com

これで決まりです。
これでCORSエラーが解消しました!

あと、もう一つ。
img要素のMotion JPEG動画ですが、これには以下の属性を加えればOKです。

crossOrigin='use-credentials'

これで、src属性にURLを与えてGETリクエストされても、ブラウザ内のBasic認証データが送られてきます。
当然、M5Cameraサーバーのレスポンスヘッダにも

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://xxxx-xxxx-x-colab.googleusercontent.com

を含めるようにすればOKです。

これで、Google Colaboratory上のJavaScriptでM5Cameraの動画ストリーミングができて、Canvas要素からデータを取得できました。
この結論が出るまで長~い時間がかかりましたよ~…。

では、次のページでは、実際のGoogle Colaboratoryのコードと、M5CameraのArduinoスケッチ(プログラムソースコード)を紹介します。

コメント

タイトルとURLをコピーしました