使えなくなったiNicoを復活させた話
この記事は TCU-CTRL 場外乱闘 Advent Calendar 2018 8 日目の記事です。 また、本日 12/8 は著者りぶの誕生日です。プレゼントお待ちしています。
7 日目担当キンジーの記事はこちら。
「キンジー」の由来がよく分かる記事。昔は遊戯王のアニメをリアルタイムで観てました。
この記事は以下のツイートの詳細を記したものです。
iOS用ニコニコ動画非公式プレイヤーiNicoが新サーバー見に行かない仕様になってて、最近旧サーバーが403返すようになったからAPIのレスポンス書き換えて新サーバーに誘導するプロキシを作っていました そのうち記事書くかもしれない
— りぶ (@g_s_sequence) September 15, 2018
免責事項
この記事では、本来 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(中間者)の略です。インストール方法やプロキシの設定方法は以下の記事などに書いてありますが、この記事でも説明します。
前提条件
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 では上記の通信結果から、
video.info
API にアクセスgetthumbinfo
API にアクセス- 動画閲覧ページにアクセス
getflv
API にアクセス- 動画本体にアクセス
という通信を行っていることが分かります。動画本体の 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
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
は従来の動画サーバーに関する情報がそれぞれ格納されています。双方の特徴は以下の通りです。
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 件に増やす処理もオマケでつけました(プレミアム会員のみ?)。あわせてどうぞ。
〆
お気に入りのアプリが(非公式だから仕方ないとはいえ)いきなり使えなくなってしまったのはショックでしたが、意外となんとかなって安心しました。
スマートフォン上などで盛んに行われている HTTP 通信。どのような通信があるのかを眺めてみるだけでも楽しいですよ。ぜひ試してみてください2。
次回 9 日目の記事は たち (@Tachiazul) 君です。よろしくお願いします。