2015年8月18日火曜日

バッファローのルーターをC#から再起動させる

さて、今回もニッチなテーマになりました。

バッファローのルーターは比較的安定しており、バグも少なく使いやすいです。VPN機能を内蔵しているのものも比較的安価に販売されているので、非常に重宝しています。

しかし、私の使っているWZR-600DHP2というモデルですが、よくわかりませんが、たま~にVPN接続を解除した時にPPPoE接続が切断され以後再起動をかけるまでインターネットにつながらないという状況が起こってしまうようです。そうすると、VPNもつながりませんし、そもそもメール通知やTwitter通知を仕込んでるソフトも機能しなくなるので外から見ていて自宅のPC群が生きているのか全く分からない状態になってしまいます。

WZR-600DHP2には定期的に自身を再起動させる機能を持っていますが、別に不調じゃないのに再起動させる意味もありませんし、ここは、宅鯖から再起動を自動的にかける方法を探ってみました。

さて、まずはどうやったら再起動をかけられるか見当をつけてみます。
ブラウザからルーターにアクセスし、再起動画面を出してみます。そうすると、再起動画面のフレームのURLが
http://192.168.***.1/cgi-bin/cgi?req=tfr&id=55

であることがわかります。(IPアドレスは適当に各自の環境に置き換えて考えてください)
さらに、そのソースを見てみると、再起動ボタンを押すと
http://192.168.***.1/cgi-bin/cgi?req=inp&res=waiting_page.html
に飛ぶことがわかります。
よっしゃ、ここのURLを開けば再起動がかけられる!


\デーン/
URLを直にたたいただけではだめでした…。

実は、バッファローのルーターではこのような不正(っぽい)アクセスを防止するために、POSTパラメーターに適当な値を付けてURLリクエストをかけなければいけないようです。

というわけで、じっくり再起動周りのソースを調べてみると、inputタグは「submit=再起動」以外にも隠しパラメーターとしてsWebSessionnumとsWebSessionidがあることがわかると思います。この番号もPOSTパラメーターとして送信してあげないと再起動ができないわけですね。

というわけで、流れが見えてきました。
  1. http://192.168.***.1/cgi-bin/cgi?req=tfr&id=55にアクセスし、sWebSessionnumとsWebSessionidの値をパースする
  2.  http://192.168.***.1/cgi-bin/cgi?req=inp&res=waiting_page.htmlを開くときのPOSTパラメーターに、「submit=再起動」のほか、sWebSessionnumとsWebSessionidも入れてあげる
これで、プログラムから再起動ができそうです。


というわけで、さっそくWebClientを使ってhttp://192.168.***.1/cgi-bin/cgi?req=tfr&id=55のHTMLをダウンロードしようとしたところ、例外が発生しました。
System.Net.WebException
サーバーによってプロトコル違反が発生しました. Section=ResponseStatusLine
これはどうもお行儀が悪いウェブサーバーにアクセスしたときに起こる例外みたいで、.NETが厳密にHTMLプロトコルを検証した結果、違反があったときに発生するみたいです。これを回避するにはApp.config内でUseUnsafeHeaderParsingプロパティをTrueにしてあげればいいみたいです。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
    </startup>
    <system.net>
        <settings>
            <httpWebRequest useUnsafeHeaderParsing="true" />
        </settings>
    </system.net>
</configuration>


さて、これで準備は整いました。実装していきましょう。

ちなみにですが、バッファローのルーターでは、ログインの認証にBASIC認証を用いています。BASIC認証は、WebClient.CredentialsプロパティにIDとパスワードを入れたうえで各種リクエストを送れば大丈夫です。

static Regex reg = new Regex(@"<input type=hidden name=sWebSessionnum value=(?<num>\d+)><input type=hidden name=sWebSessionid value=(?<id>-?\d+)>", RegexOptions.Compiled);
static Encoding encoding = Encoding.GetEncoding("EUC-JP");

public static async Task RebootRouter(string RouterUrl, string Password)
{
    await RebootRouter(new Uri(RouterUrl), Password);
}

public static async Task RebootRouter(Uri RouterUrl, string Password)
{
    const string user = "admin";

    using(WebClient wc = new WebClient()) {
        wc.Credentials = new NetworkCredential(user, Password);
        wc.Encoding = encoding;

        string init = await wc.DownloadStringTaskAsync(new Uri(RouterUrl, @"/cgi-bin/cgi?req=frm&frm=init.html"));

        Match m = reg.Match(init);
        if(m.Success) {
            string num = m.Groups["num"].Value;
            string id = m.Groups["id"].Value;

            string post = $"sWebSessionnum={ num }&sWebSessionid={ id }&reboot={ HttpUtility.UrlEncode("再起動", encoding) }";
            wc.Headers.Add("Content-Type", "application/x-www-form-urlencoded");
            string resText = await wc.UploadStringTaskAsync(new Uri(RouterUrl, @"/cgi-bin/cgi?req=inp&res=waiting_page.html"), post);

            if(!resText.Contains("再起動中です。"))
                new InvalidOperationException(resText);
        } else
            new FormatException("cannot parse sWebSessionnum/sWebSessionid");
    }
}

さて、こんな感じでどうでしょうかね。
結構適当な実装です。HTMLもまじめにパースせず、スペースの取り方とかは決め打ちで正規表現でsWebSessionnumとsWebSessionidを抽出しています。そして、抽出に成功したらPOSTパラメーターを作り、UploadStringTaskAsyncメソッドでそれを送信しています。

なお、バッファローのルーターのWebサーバーの文字コードはEUC-JPになっています。Encodingを指定しないと日本語で文字化けするので気を付けてください。

余談ですが、C#6.0の新機能をここでは使っています。$"~"の構文ですね。Visual Studio 2015じゃないとコンパイル通らないので、古い環境を使っている人がいたら適宜書き換えてください。

try {
    RebootRouter(@"http://192.168.***.1/", "****").Wait();
    Console.WriteLine("Router Reboot has successfully started.");
}
catch(AggregateException e) when (e.InnerExceptions.Any(p => p is InvalidOperationException || p is FormatException || p is WebException)) {
    foreach(var ie in e.InnerExceptions) {
        Console.WriteLine(ie.GetType());
        Console.WriteLine(ie.Message);
    }
}

このメソッドの呼び出しはこんな感じでいいですかね。1つ目の引数にルーターのURL、2つ目にパスワードです。非同期メソッドになっているので、例外が起きた時はAggregateExceptionになることに注意してください。ここもC#6.0になってから簡単に例外を振り分けられるようになりました。

このプログラムを実行して2分くらい待つと再起動の完了です。
めでたしめでたし。

0 件のコメント:

コメントを投稿