OITA: Oika's Information Technological Activities

@oika 情報技術的活動日誌。

C# FiddlerのInspectorsプラグインを作成

FiddlerはHTTPのローカルプロキシツール(雑)です。

これのプラグインが C# で作れるんだけれど、Inspectorsの中のタブを自作するサンプルがあんまりなかった気がするのでメモ。

バージョンとか

  • Fiddler 4 (v5.0.20194.41348)
    • v5.0 だけど Fiddler 4 なの??
  • Visual Studio 2019 Community

作るもの

例として、Livedoorのお天気Webサービス APIコールにFiddlerを挟んでみる。

このAPIでは、URLのパラメータで1次細分区IDを指定すると、その地区の天気データをJSONで取得することができる。
「札幌」なら http://weather.livedoor.com/forecast/webservice/json/v1?city=016010 というのがリクエストURL。

このリクエスト/レスポンスデータを見やすくするプラグインを作ってみましょう。

作ります

プロジェクト作成

Fiddlerのプラグインは .NET Frameworkのクラスライブラリ(Forms)として作成する。
.NET Coreでも作れるのかな。未検証。

Visual Studioでプロジェクトをクラスライブラリとして新規作成した場合、Windows Forms 関係の参照を追加する必要があるが、Visual Studio 2019 Community だと「Windows フォーム コントロール ライブラリ (.NET Framework)」というテンプレートがあって、これを選択すると楽である。

コードからFiddlerのクラス群を使うために、参照に Fiddler.exe を追加する必要がある。

普通にインストールすると C:\Users\ユーザ名\AppData\Local\Programs\Fiddler\Fiddler.exe みたいなところにあるので、これをそのまま参照してもいい。
HintPathにへんなパス埋めるのが嫌だったら、プロジェクトフォルダにexeだけコピって置いておいて、それを参照させてもいい。

リクエストのInspector

リクエストのタブに表示するInspectorコントロールを作成します。

まずはタブに表示する部品をユーザーコントロールとして作成。

リクエストURLのcityパラメータの値を表示するだけのシンプルなコントロールを RequestPanel という名前で作りました。

続いてInspectorクラスを作成。
こちらは Inspector2 , IRequestInspector2 という2つのインターフェースを実装するpublicなクラスとして作成する。

[assembly: Fiddler.RequiredVersion("2.4.0.0")]

public class LwwsReqInspector : Inspector2, IRequestInspector2
{
    //パネルコントロールを作って保持しておく
    readonly RequestPanel reqPanel = new RequestPanel();

    public bool bDirty => false;
    public bool bReadOnly { get; set; }

    public override int GetOrder()
    {
        return 0;
    }

    //Inspectorsのタブに追加する
    public override void AddToTab(TabPage o)
    {
        o.Text = "天気";
        o.Controls.Add(this.reqPanel);
        this.reqPanel.Dock = DockStyle.Fill;
    }

    private HTTPRequestHeaders _hds;

    public HTTPRequestHeaders headers
    {
        get
        {
            return this._hds;
        }
        set
        {
            this._hds = value;

            var host = value?.FirstOrDefault(h => h.Name == "Host")?.Value;
            if (host != "weather.livedoor.com")
            {
                this.reqPanel.Clear();
                return;
            }
            //パネルに要求URLを受け渡す
            this.reqPanel.UpdateId(value.RequestPath);
        }
    }

    //今回はbodyは使わない
    public byte[] body { get; set; }

    public void Clear()
    {
        this.headers = null;
        this.body = null;
    }
}

AddToTab で先ほどの RequestPanel をタブページとして追加し、 headers , body プロパティで送信前のリクエストデータを受け取る。
GetOrder はタブの表示位置を制御するやつかな。
bDirty , bReadOnly の使い方はよくわからない。

RequestPanel側に対応するメソッドを作っておきます。

public partial class RequestPanel : UserControl
{
    public RequestPanel()
    {
        InitializeComponent();
    }

    internal void Clear()
    {
        this.lblAreaId.Text = "--";
    }

    internal void UpdateId(string requestPath)
    {
        if (requestPath == null)
        {
            Clear();
            return;
        }

        //cityパラメータの値を反映
        var idxParam = requestPath.IndexOf("?");
        if (idxParam < 0)
        {
            Clear();
            return;
        }
        var cityParam = requestPath.Substring(idxParam + 1)
                        .Split('&')
                        .FirstOrDefault(p => p.StartsWith("city="));
        if (cityParam == null)
        {
            Clear();
            return;
        }

        this.lblAreaId.Text = cityParam.Substring("city=".Length);
    }
}

レスポンスのInspector

おんなじようにしてレスポンス用のタブコントロールとInspectorを作成。
こちらは受信データから、発表時刻と概況文を抜き出して表示してみましょう。

Inspector

public class LwwsResInspector : Inspector2, IResponseInspector2
{
    readonly ResponsePanel resPanel = new ResponsePanel();

    public bool bDirty => false;
    public bool bReadOnly { get; set; }

    public override int GetOrder()
    {
        return 0;
    }

    public override void AddToTab(TabPage o)
    {
        o.Text = "天気";
        o.Controls.Add(this.resPanel);
        this.resPanel.Dock = DockStyle.Fill;
    }

    //ヘッダは使用しない
    public HTTPResponseHeaders headers { get; set; }

    private byte[] _bd;
    public byte[] body
    {
        get
        {
            return this._bd;
        }
        set
        {
            this._bd = value;
            this.resPanel.UpdateBody(value);
        }
    }

    public void Clear()
    {
        this.resPanel.Clear();
    }
}

ResponsePanel(※JSON.NETを使用)

public partial class ResponsePanel : UserControl
{
    public ResponsePanel()
    {
        InitializeComponent();
    }

    internal void Clear()
    {
        this.lblSummary.Text = "--";
        this.lblTime.Text = "--";
    }

    internal void UpdateBody(byte[] value)
    {
        if (value == null)
        {
            Clear();
            return;
        }

        var txt = Encoding.UTF8.GetString(value);
        txt = Regex.Unescape(txt)
                   .TrimStart((char)65279); //BOM削除

        try
        {
            var o = JObject.Parse(txt);
            if (DateTime.TryParse(o["publicTime"].Value<string>(), out DateTime tim))
            {
                this.lblTime.Text = tim.ToString("yyyy/MM/dd HH:mm:ss");
            }
            else
            {
                this.lblTime.Text = "--";
            }
            this.lblSummary.Text = o["description"]["text"].Value<string>();
        }
        catch (Exception ex)
        {
            if (ex is JsonReaderException
                || ex is KeyNotFoundException
                || ex is NullReferenceException)
            {
                Clear();
            }
            else
            {
                throw;
            }
        }
    }
}

配備

ビルドしてできたDLLファイルを、Fiddlerインストールフォルダの「Inspectors」フォルダ内に入れます。
ScriptsフォルダとかPluginsフォルダとか紛らわしいのがあるんだけど、Inspectorを自作したときはInspectorsフォルダが正解。

また、Fiddler以外に外部参照するdllがある場合(今回でいうとNewtonsoft.Json.dll)、それもあわせて同フォルダに入れておく必要があるので注意。

DLLを入れたらFiddlerを起動します。
Inspectorsタブを開くとリクエスト・レスポンスともに「天気」タブが追加されているはず。

Fiddlerを起動した状態でブラウザなどでAPIをコールし、左ペインから対象のログを選ぶと、ちゃんと表示される。

以上。