使えなくなったiNicoを復活させた話

この記事は 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 通信は以下のように行われます。

HTTP通信

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

HTTP通信(プロキシ経由)

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

前提条件

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 エラーとなってしまいます。

ニコニコ動画の新仕様

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

この記事によると、動画再生ページ(/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 は従来の動画サーバーに関する情報がそれぞれ格納されています。双方の特徴は以下の通りです。

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

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

通信内容を書き換える

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

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

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

script.py
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 チェインのルールは以下の通りとなります。

iptables.rules
-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 件に増やす処理もオマケでつけました(プレミアム会員のみ?)。あわせてどうぞ。

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

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

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

Footnotes

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

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