はじめに
Reproでサーバーサイドやクライアント用SDKの開発を担当しているEdward Fox (@edwardkenfox) です。最近 Anyca というアプリを利用してTeslaのモデルSを運転したのですが、富士急のドドンパ並の加速性能で大変驚きました。
Reproは創業から約4年間モバイルアプリの成長支援パートナーとして事業を続けてきましたが、2018年10月からはRepro Webという名前でWebページやWebサービスの分析とマーケティング施策が実施できるツールの提供も行っています。本記事ではRepro Webの開発にも大活躍している mitmproxy というローカルプロキシツールとReproでの活用方法を紹介します。
そもそもRepro Webとは
先にも書いたように、Repro WebはWebサイトの分析やマーケティング施策が実施できるツールです。Repro Webの導入自体はWeb SDKと呼ばれるJavaScriptライブラリをインストールするタグを貼るだけで完了します。一度導入さえ済んでしまえば開発者の手を借りることなくマーケティング施策を実施したりA/Bテストを行えることがRepro Webの特長の1つです。
Web SDKの動作確認の難しさ
通常のサービス開発におけるフロントエンドと異なり、Web SDKはお客様のサイト上で動くライブラリであるため、種々様々なWebページ上で正しく動くことを求められます。一方で、広いインターネットの世界にはWeb SDK開発者が想定していなかったHTMLやJavaScriptによって構築されたページが存在するのも事実です。開発時のテストでは確認できることに限りがあるため、できることならWeb SDKが導入されているサイト上で動作確認をしたいものですが、お客様のWebページを自分たちで改修することは当然できません。
こういった状況下においてとても有用なのがローカルプロキシと呼ばれるツールです。ローカルプロキシを利用すれば、Webページのリクエストやレスポンスを好きなように改竄することができます。対象となるWebページ自体に手を入れることなくWebページのリソースを改竄できブラウザからするとあたかもWebページそのものが変わったかのように扱えるので、擬似的に、かつ必要十分な動作確認が本番環境上で安全に行えとても便利です。
実践 mitmproxy
ローカルプロキシツールには Charles など色々なものがありますが、今回は mitmproxy というOSSのプロキシツールを紹介します。またmitmproxyを利用して任意のJavaScriptを任意のWebページに挿入する方法を説明します。次の図に示す構成で example.com のレスポンスを改竄し、アラートを表示するJavaScriptを挿入する方法を説明します。
実施環境
本記事の内容は筆者が利用する下記の環境を前提に書かれています。後述の beautifulsoup4 のインストール方法などは自身の環境に合った方法に適宜読み替えてください。
mitmproxyのインストール
まずはmitmproxyをダウンロードし、必要なセットアップ作業を行いましょう。工程としては大きく次の通りです。
- mitmproxyを起動するマシン側の準備
- mitmproxyのインストール
- example.com にアクセスするクライアント端末側の準備
- ネットワークのプロキシ設定
- 証明書のインストール
なおこれらの準備作業についてはmitmproxyの公式ドキュメントや下記の記事に詳しく書いてあるので、本記事内での説明は割愛します。
レスポンスを改竄する
必要な設定が完了したらさっそく example.com へのリクエストを改竄し JavaScript を挿入してみましょう。mitmproxyには Scripting(Addons)という機能があり、独自のPythonスクリプトを書いてmitmproxyがプロキシするリクエストやレスポンスをハンドリングできるようになっています。加えて、今回はレスポンスボディのHTMLを改竄したいので、 beautifulsoup4 というHTML/XMLのパーサライブラリを利用します。
まずはレスポンスを改竄する前に素の状態で example.com を見てみましょう。クライアント端末で example.com を開くと、通常通りのコンテンツが表示されます。
ではレスポンスを改竄する準備を進めます。次のコマンドを実行して beautifulsoup4 をインストールします。
$(brew --prefix mitmproxy)/libexec/bin/pip install beautifulsoup4
今回レスポンスを改竄したいのはあくまでも example.com へのリクエストのみであり、 example.com のHTMLに含まれるサブリソースなどには手を付ける必要はありません(実際は example.com にはサブリソースを含みません)。後述のPythonスクリプトではレスポンスの改竄対象となるドメインを MITM_TARGET_DOMAIN
という環境変数で指定できるようにしてあるので、この環境変数に 'example.com'
をセットしましょう。
$ export MITM_TARGET_DOMAIN='example.com'
つづけて、次のコードを inject_alert.py
として保存します。
import os from bs4 import BeautifulSoup from mitmproxy import http host = os.environ["MITM_TARGET_DOMAIN"] class Injector: def load(self, loader): loader.add_option( "script", str, "", "My Script Tag" ) def response(self, flow: http.HTTPFlow) -> None: if flow.request.pretty_host == host and flow.response.headers.get("content-type").find("text/html") != -1: html = BeautifulSoup(flow.response.content, "html.parser") if html.head: script = html.new_tag("script", id="mitmproxy") script.string = 'alert("Hello from mitmproxy!")' html.head.insert(0, script) flow.response.content = str(html).encode("utf8") addons = [Injector()]
上記のコードを簡単に説明します。このPythonスクリプトで利用する各種ライブラリを import
します。つづけて、 MITM_TARGET_DOMAIN
という環境変数にセットされた値を host
という変数に保持しておきます。これは後にリクエスト先のドメインと照合する用途で利用します。その下に書かれている Injector
クラスというのがこのPythonスクリプトの本体で、レスポンスの改竄をするコードです。 Injector
クラスはmitmproxyのAddonの機構に則って書かれており、 load
と response
メソッドが定義されています。メソッドを1つずつ見ていきましょう。
Addonの機能を利用してmitmproxyを起動させると、まずはAddonのPythonスクリプトに用意された load
メソッドが実行されます。load
メソッドの第二引数にある loader
は
mitmproxy.addonmanager.Loader
オブジェクトで、Loader.add_option
を通して各種オプションをセットします。オプションの詳細については このあたりの実装 を参照してください。
実際に mitmproxy がHTTPトラフィックをプロキシし、クライアントにレスポンスを返す際に response
メソッドが実行されます。response
メソッドの第二引数にある flow
は http.HTTPFlow
オブジェクトで、 「An HTTPFlow is a collection of objects representing a single HTTP transaction.」 というものです。この flow
オブジェクトをいじることでレスポンスの改竄が実現できます。改竄の対象となるリソースは example.com のメインリソースとなるHTMLページだけで、そこから派生してリクエストされるサブリソースには何も手を加える必要がないため、先にセットしておいた host
(ここでは 'example.com'
)と flow.request.pretty_host
が一致するかを確認します。つづけて beautifulsoup を利用してレスポンスのHTMLを取得・パースし、 head
タグに任意の script
タグを挿入する、という流れです。
最後に、次のコマンドを実行してmitmproxyを起動してみましょう。
$ mitmdump -s inject_alert.py
エラーなく次のようなログが表示されれば mitmproxy の準備は完了です。
Loading script inject_alert.py Proxy server listening at http://*:8080
動作確認
それではいよいよネットワークのプロキシ設定をした端末でブラウザを開き、ふたたび example.com にアクセスしてみましょう。
inject_alert.py
で書いた alert()
がHTMLに挿入され、実際にアラートが表示されていますね! めでたしめでたし。
まとめ
本記事では mitmproxy を使って本番環境のWebページに手を加えることなくレスポンスを改竄し任意の JavaScript を挿入する方法を紹介しました。レスポンスだけでなくリクエストに手を入れたり、ボディだけでなくヘッダを改変することも可能です。mitmproxyを使いこなし、安全・快適に動作確認を行いましょう!
なお本記事で紹介したPythonスクリプトはmitmproxyのリポジトリにあるサンプルコードを一部改変したものです。これ以外にもたくさんのサンプルが用意してあるので興味のある人は見てみてください。