この記事は TCU-CTRL場外乱闘 Advent Calendar 2018 8日目の記事です。 また、本日 12/8 は著者りぶの誕生日です。プレゼントお待ちしています。

7日目担当キンジーの記事はこちら。

「キンジー」の由来がよく分かる記事。昔は遊戯王のアニメをリアルタイムで観てました。


この記事は以下のツイートの詳細を記したものです。

免責事項

この記事では、本来End-to-Endで暗号化されるHTTPS通信の内容を閲覧したり編集するといったことを紹介しています。HTTPS通信の内容を閲覧したり編集することによって生じた損害等の一切の責任を負いかねますので、ご了承ください。

iOS用ニコニコ動画プレイヤー

iOS端末でニコニコ動画を見るとき、通常は公式アプリを使いますが、一部の動画が再生できなかったり検索結果に現れないなどの仕様が盛り込んであるため、サードパーティー製アプリを使っている人もいるかと思います。

数多くあるサードパーティー製ニコニコ動画プレイヤーの中で自分はiNico 2を愛用しています(現在App Storeから削除されています)。UIの使いやすさや再生できる動画の種類の豊富さなどはもちろん、ローカルのお気に入りや正規表現パターンを用いたコメントNG、バックグラウンド再生など非常に多機能なアプリとなっています。

その時、悲劇は起こった

2018年9月11日の夕方あたりから、iNicoで動画が再生できなくなりました。

このエラーメッセージより、サーバーからHTTP 403が返ってきていることが分かります。おそらくニコニコ動画の仕様が変わったのでしょう。

エコノミーモードの動画は引き続き見ることができます。でもやっぱり高画質で見たい。どうしよう。

iNicoの通信を覗いてみる

まずはどの通信がHTTP 403となっているのかを調査しましょう。HTTPプロキシを立てて通信を観察することにします。

通常のHTTP通信は以下のように行われます。

今回は、通信内容を確認するため、以下のようにプロキシを経由するようにします。

プロキシソフトウェアは、インタラクティブな操作で通信内容を確認できるmitmproxyを使います。mitmはman-in-the-middle(中間者)の略です。インストール方法やプロキシの設定方法は以下の記事などに書いてありますが、この記事でも説明します。

https://qiita.com/wtotw/items/69290b178371c4d7cf76

前提条件

PCとiPhoneは同一ネットワークに接続しておいてください。iPhoneからPCに到達可能ならOKです。また、PCのIPアドレスは固定しておいたほうが良いです(必須ではありません)。

mitmproxyのインストール

Python 3.5以降がインストールされているUnix系OSなどで

$ pip3 install mitmproxy

でインストールできます。Arch Linuxを使っている人は

$ sudo pacman -S mitmproxy

でも入ります。

iPhoneのプロキシ設定

設定App→Wi-Fi→接続しているWi-Fiネットワークのiボタン→プロキシを構成 から「手動」を選んで以下のようにプロキシを設定します。

プロキシを設定したあとは必ず右上の「保存」ボタンを押しましょう。

mitmproxyの起動

PC上のターミナルで

$ mitmproxy

だけで起動します。終了するときは Ctrl-C を押しましょう。

証明書のインストールと有効化

ニコニコ動画の通信は最近HTTPS化されました。HTTPS通信の内容を見るためには、証明書のインストールと有効化が必要です。

PCでmitmproxyを起動している状態で、iPhoneから http://mitm.it/ にアクセスしてAppleのマークをタップすることで証明書をインストールします。

この証明書はインストールしただけでは使えません。証明書の有効化(信頼設定)が必要です。 設定App→一般→情報→証明書信頼設定→mitmproxy をオンにしておきましょう。

使い方

mitmproxyを起動している状態でiPhoneから適当なサイトを開いてみましょう。コンソールにHTTP通信のリストがずらずらと表示されるはずです。

上下キー(もしくはj/kキー)でカーソルを移動してEnterキーを押すとその通信の詳細を見ることができます。

qキーを押すと元の画面に戻ることができます。

iNicoの通信エラーを確認する

それでは、iNicoを開いて適当な動画を再生してみましょう。

動画本体にアクセスしようとするとHTTP 403が返ってくることが確認できました。

iNicoの動作

iNicoでは上記の通信結果から、

  1. video.info APIにアクセス
  2. getthumbinfo APIにアクセス
  3. 動画閲覧ページにアクセス
  4. getflv APIにアクセス
  5. 動画本体にアクセス

という通信を行っていることが分かります。動画本体のURLは、getflv APIのレスポンスから取得しています。getflvのレスポンス例は以下の通りです。

データサンプル 正常な場合 thread_id=1173108780&l=319&url=http://smile-pcm42.nicovideo.jp/smile?v=9.0468&link=http://www.smilevideo.jp/view/9/20929324&ms=http://msg.nicovideo.jp/10/api/&ms_sub=http://sub.msg.nicovideo.jp/10/api/&user_id=20929324&is_premium=1&nickname=ℳກ੮ວܬ୧&time=1390950769539&done=true&ng_rv=101&hms=hiroba.nicovideo.jp&hmsp=2536&hmst=120&hmstk=1390950829.8Pgecbv8FVdjB1b55BzBCF5i2Uw getflv - ニコニコAPIリストwiki - アットウィキ

iNicoはこのレスポンスの url= 以降のURLを動画URLとして再生しようとしているようです。しかし、ニコニコ動画の仕様変更によって、ほとんどの動画においてgetflvのレスポンスに載っているURLはHTTP 403エラーとなってしまいます。

ニコニコ動画の新仕様

ニコニコ動画の新しい仕様は以下の記事にまとまっています。

https://qiita.com/tor4kichi/items/91550a71119f3878bfba

この記事によると、動画再生ページ(/watch/smXXXXXXXX)の中に動画情報がJSONで埋め込まれているとの記述があるので、PCのウェブブラウザで確認してみましょう。適当な動画ページをPCで開いて開発者ツールを起動し、コンソールに以下のコードを打ち込みます。

JSON.parse(document.querySelector('#js-initial-watch-data').attributes['data-api-data'].value)

このコードはページに埋め込まれているJSONをパースしているだけです。実行すると、動画情報がツリー形式でコンソールに表示されるはずです。

動画本体の情報は .video.dmcInfo.video.smileInfo に格納されています。 dmcInfo はニコニコ動画の新しい動画サーバーに関する情報、 smileInfo は従来の動画サーバーに関する情報がそれぞれ格納されています。双方の特徴は以下の通りです。

dmcInfo smileInfo
再生手順 難しい 簡単
画質
備考 古い動画はnull

.video.smileInfo.url に格納されているURLをブラウザに打ち込むと、HTTP 403エラーにならず、正しく再生されることが分かります。おそらく getflv APIは仕様変更に追従していなかったのでしょう。

通信内容を書き換える

mitmproxyは通信内容を見るだけでなく書き換えることもできます。ということは、mitmproxy内で getflv のレスポンスを書き換えることで、iNicoで動画を再生できるようになると考えられます。

mitmproxyでレスポンスを書き換えるには、iキーを押して中断する通信のパターンを入れて通信を止めてから、Enterキーを押して通信を表示してeキーで入力を編集して……と手動で行うこともできますが、編集している間にタイムアウトして失敗してしまいます。

mitmproxyはPythonスクリプトで通信の書き換えなどを自動化することができるので、これを使いましょう。以下にコードを示します。

import html
import json
from re import match, search, sub
from urllib import parse

PAT_WATCH_PATH = r"^/watch/(.*?)\?watch_harmful=1$"
PAT_GETFLV_PATH = r"^/api/getflv\?v=(.*)"
PAT_WATCH_DATA = r'<div id="js-initial-watch-data" data-api-data="(.*?)"'
PAT_GETFLV_SUB = r"(?<=&url=)(.*?)(?=&ms=)"

smile_urls = {}


def response(flow):
    """Webサーバーからレスポンスが来たときに呼ばれる"""
    scheme = flow.request.scheme
    host = flow.request.host_header
    path = flow.request.path

    # URLで分岐
    if scheme == "https" and host == "www.nicovideo.jp":
        m = match(PAT_WATCH_PATH, path)
        if m:
            __watch_page(flow, m.group(1))
            return
    if scheme == "http" and host == "flapi.nicovideo.jp":
        m = match(PAT_GETFLV_PATH, path)
        if m:
            __getflv(flow, m.group(1))
            return


def __watch_page(flow, video_id):
    """動画閲覧ページを処理する"""
    m = html.unescape(search(PAT_WATCH_DATA, flow.response.text).group(1))
    j = json.loads(m)
    url = j["video"]["smileInfo"]["url"]
    smile_urls[video_id] = url


def __getflv(flow, video_id):
    """getflvの内容を書き換える処理をする"""
    if not video_id in smile_urls:
        return

    encoded = parse.quote(smile_urls[video_id])
    flow.response.text = sub(PAT_GETFLV_SUB, encoded, flow.response.text)

Webサーバーからレスポンスを受け取ったとき、mitmproxyから response() が呼び出されます。 flow は通信1件に関するやり取り(リクエストやレスポンス)が格納されています。 __watch_page() は、iNicoが動画再生直前にアクセスする動画閲覧ページから smileInfo 内のURLをスクレイピングして辞書に格納する関数です。 __getflv() は、 __watch_page() で格納したURLを使って getflv APIのレスポンスを書き換える関数です。Pythonで正規表現やJSONを扱う程度のコードなのでそこまで難しいことはやっていないはず。

このスクリプトのファイル名を script.py としたとき、mitmproxyを以下のように起動すると自動でレスポンスが書き換わります。

$ mitmproxy -s script.py

また、動画本体はHTTPSの暗号化および復号化処理の負荷が大きく、そもそも不要なので、動画を配信するホストはプロキシを無視するオプションを加えておきましょう。

$ mitmproxy -s script.py --ignore-hosts ".sv.nicovideo.jp:443"

これでiNico上で動画が正常に再生されるようになりました。

VPNに透過プロキシを置く

しかしまだまだ問題点はあります。iOSはWi-Fi接続下でないとプロキシを使えません1。電車通勤・通学中など外出中に動画を見ることができません。

一方、VPNはWi-Fi接続かどうかにかかわらず使用することができます。そこで自分は、自前VPN接続の経路の後にmitmproxyを透過プロキシモードにして通信を通すようにしました。VPNの構成は千差万別ですのでVPNサーバーの構築方法はこの記事では割愛しますが、iptablesの設定によるルーティングは参考になりそうなので共有しておきます。

ここでは、VPNクライアントに割り当てられるIPアドレスの範囲を 10.1.2.0/24 、mitmproxyを動作させるポートを 12345 とします。

iptablesの nat テーブルにおける PREROUTING チェインのルールは以下の通りとなります。

-A PREROUTING -s 10.1.2.0/24 -p tcp --dport 80 -j REDIRECT --to-port 12345
-A PREROUTING -s 10.1.2.0/24 -p tcp --dport 443 -j REDIRECT --to-port 12345

mitmproxyのコマンドラインは以下の通りとなります。

$ mitmdump -p 12345 --mode transparent --showhost -s script.py --ignore-hosts ".sv.nicovideo.jp:443" 

VPNサーバーでの動作を想定しているので、インタラクティブUIを搭載した mitmproxy コマンドではなくシンプルな mitmdump コマンドを使用しています。systemdなどでデーモン化しておくと良いでしょう。

これで外出先からでも簡単に動画を楽しむことができます。

さらなる拡張

本記事では smileInfo を用いてURLを書き換えました。 dmcInfo を用いたURL書き換えはハートビート処理など複雑な処理が多いので本記事では割愛させていただきますが、スクリプトはGistに全部載せましたので参考にどうぞ。自分はこれを実際に使っています。

また、30件ほどしか表示されない視聴履歴の件数を100件に増やす処理もオマケでつけました(プレミアム会員のみ?)。あわせてどうぞ。

https://gist.github.com/gssequence/c7c90d0c16a3e7a30fa5c27cbd7ac5e9

お気に入りのアプリが(非公式だから仕方ないとはいえ)いきなり使えなくなってしまったのはショックでしたが、意外となんとかなって安心しました。

スマートフォン上などで盛んに行われているHTTP通信。どのような通信があるのかを眺めてみるだけでも楽しいですよ。ぜひ試してみてください2

次回 9 日目の記事は たち (@Tachiazul) 君です。よろしくお願いします。


  1. Apple Configuratorを使えばグローバルHTTPプロキシ設定で接続できるっぽいですが公開プロキシを立てるのもアレなので。

  2. HTTP Public Key Pinningによって、不正な証明書によるHTTPS通信を拒否するプログラムも存在します。主に通信セキュリティを十分に保つために使用されます。Twitter for iPhoneの通信はこの手法を用いているため内容を見ることができません。生意気な