mitmproxyでhttps対応Data Compression Proxyを作る

 こんにちは。

 今回はhttps通信に対応したデータ圧縮プロキシを自前で作成します。

動機

  • 4月の連休で帰省したが、データ通信量が契約を超過し低速モード突入し重い
  • 特に画像が出てくるまでが遅い
  • 画像を読み込みつつcssとかjsが読み込まれないと白紙のサイトってなんなの?
  • 古くからある圧縮プロキシであるziproxyではhttpsサイトの圧縮に対応できないため、昨今メジャーなhttpsサイトでは圧縮されない
  • そこで、なんとかしてhttpsサイトに対応したデータ削減プロキシを作れないか?

mitmproxyとは

  • https://mitmproxy.org/
  • MITM = Man-In-The-Middle : 中間者
  • 本来、通常のSSL通信はサーバとクライアント間で暗号化と復号がされるため、中継する装置ではその内容を見ることはできない
  • squid等のProxyも暗号化されたままデータを中継する
  • データ圧縮に対応したziproxyもhttpsについては中継するのみ
  • しかし、mitmproxyは、サーバとクライアントの間のプロキシ側で一旦復号・暗号化をする
  • そのため、データの中を見てデバッグしたり、スクリプトによって処理を変更することができる

仕組み

https://docs.mitmproxy.org/stable/concepts-howmitmproxyworks/

HTTPプロキシとしての動作


how-mitmproxy-works-explicit.png
1. クライアントはプロキシに接続し、要求を出します。
2. Mitmproxyは上流のサーバーに接続し、単に要求を転送します。

HTTPSプロキシとしての動作


how-mitmproxy-works-explicit-https.png
1. クライアントはmitmproxyに接続し、HTTP CONNECT要求を発行します。
2. Mitmproxyは200 Connection Established、CONNECTパイプを設定したかのように応答します。
3. クライアントは、リモートサーバーと通信していると考え、TLS接続を開始します。SNIを使用して、接続先のホスト名を示します。
4. Mitmproxyはサーバーに接続し、クライアントが指定したSNIホスト名を使用してTLS接続を確立します。
5. サーバは、傍受証明書を生成するために必要なCNとSANの値を含む、一致する証明書で応答します。
6. Mitmproxyはインターセプト証明書を生成し、手順3で一時停止したクライアントTLSハンドシェイクを続行します。
7. クライアントは、確立されたTLS接続を介して要求を送信します。
8. Mitmproxyは、手順4で開始したTLS接続を介してサーバーに要求を渡します。

作り方

  1. Python環境構築用のシェルをダウンロード wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh
  2. シェル実行 sh ./Miniconda3-latest-Linux-x86_64.sh
  3. バージョン3.6以上であることを確認 python -V
  4. Pythonパッケージ管理システムでmitmproxyとpillow-simdをインストール pip install mitmproxy pillow-simd
  5. mitmproxy用のデータ圧縮スクリプトを作成(内容は後述) vi flows.py
  6. mitmproxyのCUIバージョンを起動 nohup mitmdump --host :3129 --no-http2 -s flows.py &
  7. ポート3129でLISTENしていることを確認 lsof -i | grep mitm
  8. ログ確認 tail -f nohup.out
  9. スマホメールにプロキシサーバを追記したプロファイルを送付し導入するか、wifiにプロキシを設定する
  10. ブラウザから証明書サイトにアクセス http://mitm.it
  11. 証明書を信頼する (iPhone: 設定 -> 一般 -> 情報 -> 証明書信頼設定 -> ルート証明書を全面的に信頼する)
  12. 通信の最適化確認サイトでテスト、元画像の約20パーセントの容量になる http://horobi.com/Saiteika/
flows.py
from PIL import Image
import io, time

def response(flow):
  if "content-type" in flow.response.headers and "content-length" in flow.response.headers:
    ct = str(flow.response.headers["content-type"])
    cl = int(flow.response.headers["content-length"])
    if not len(ct) == 0:
      if str(flow.response.headers["content-type"]) [0:6] == ("image/") and int(flow.response.headers["content-length"]) > 1000 and str(flow.response.headers["content-type"]) [0:9] != ("image/svg"):
        start = time.time()
        img = Image.open(s).convert("RGB")
        s2 = io.BytesIO()
        img.save(s2, "jpeg", quality=10, optimize=True)
        flow.response.content = s2.getvalue()
        flow.response.headers["content-type"] = "image/jpeg"
        after = int(flow.response.headers["content-length"])
        print("Before: %s / %s bytes" % (ct, cl))
        ct = str(flow.response.headers["content-type"])
        cl = int(flow.response.headers["content-length"])
        print("After : %s / %s bytes" % (ct, cl))
        i = int(after / before * 100)
        print("<<< Compressed %s percent >>>" % i)
        elapsed_time = time.time() - start
        print("elapsed_time:i {0}".format(elapsed_time))

注意点

  • セキュリティのリスクは高まるので、自分専用で実験用に使う
  • 透過gif,pngは背景が黒になる、アニメーションはしなくなるのは割り切る
  • Certificate pinning対応のサイトにはつながらない → --ignore-hostsオプションで復号せずに中継させる https://docs.mitmproxy.org/stable/howto-ignoredomains/

おわりに

 この方法はリスクと制限がありますので、パケットが枯渇した月末などに、一時的に利用するなどの使い方がよいかと思います。というか、時間もかかるしより大容量のデータ通信に対応したプランを契約するなどお金で解決すること推奨です。
 今回Pythonのコードを生まれて初めて書きました。GW中の暇を利用して、iPhoneからTermiusというアプリでサーバにssh接続、safariで検索という環境でトライアンドエラーしました。
 コード書きにあたっては、QiitaのPython先輩方、オライリー・ジャパン発刊「Pythonチュートリアル 第3版」を参考にさせていただきました。

リファレンス

https://mitmproxy.org/
https://docs.mitmproxy.org/stable/
http://pillow.readthedocs.io/en/5.1.x/handbook/image-file-formats.html
http://pillow.readthedocs.io/en/latest/reference/Image.html
http://yalis.fr/cms/index.php/post/2014/01/23/Compress-the-mobile-web-even-further-both-HTTP-and-HTTPS
https://conda.io/miniconda.html

スクリーンショット

左:未圧縮 右:圧縮
D9ABC87F-792D-4AA4-B0E4-34CF7991C977.png ED4BF124-5CAB-4086-9D58-852BDC2538F2.png
131E5D93-7045-4014-888E-4523FA679E83.png C6144B07-3060-44CD-BCA1-6C5E9AC9605D.png
2B0BA4B7-D847-44AB-A328-E6126CE8A3A3.png 47F9198E-863A-4E21-98A8-4C7A193C3342.png

ログ
B6DD1BDA-5227-4E3D-BD72-2030DAC0AA52.png

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.