FiddlerCoreを使ってC#でhttp通信を覗き見しよう

突然ですが、HTTP通信を覗くことを考えます。

あ、艦これの専ブラを作った話からの続きです、はい。

要するにHTTPプロキシを作ればいいわけで、C#にはSystem.Net.HttpListenerというHTTPサーバクラスもあったりして自前で実装することもできますが、FiddlerCoreという.NET向けHTTPプロキシライブラリがあるのでこれを使うことにしました。車輪の再発明はやめて、枯れたライブラリをありがたく使わせてもらいましょう*1

FiddlerCoreとは

FiddlerというHTTPアナライザのHTTPプロキシ部分がライブラリFiddlerCoreとして提供されています。

なので、単にHTTPセッションを中継するだけではなく、以下の機能があります。
「HTTPセッションをハンドルし、適当なタイミングでイベントを呼び、HTTPリクエスト/レスポンスを適当に調理したものを渡してくれる。」
完璧じゃないですか。

というわけで、早速使ってみましょう。

ダウンロード

Webサイトからインストーラ落としてきて適当に実行すると、FiddlerCoreAPIみたいなディレクトリが作られ、その中にアセンブリFiddlerCore.dll*2があります。このアセンブリと一緒にサンプルソースがインストールされ、また同じexeインストーラ形式で、chm形式のヘルプでドキュメントがあります*3

サンプルソースとドキュメントを読んで分かる人は以下の内容は読まなくてもいいですね。お疲れ様でした。わかんない人は、もう少しお付き合いください。

ちなみに、いちおう調べた上で書いてますがなんか誤ってても責任は取れません。ツッコミは歓迎します。

ちなみに、作者のEric Lawrenceさんが書かれた本の邦訳が出てます。一部の大学図書館にもある*4ようなので、読んでみるのもあり*5

実践 Fiddler

実践 Fiddler

使ってみよう

適当にWindowsフォームプロジェクトを作って、さっき落としてきたインストーラの吐いたアセンブリを参照に追加します。
そして、フォームにWebBrowserコントロールとテキストボックスとボタンコントロールを貼ります。

こんな感じに。

で、ソースコードは以下。Form1_FormClosedとbutton1_Clickはそれぞれフォームが閉じる時とボタンコントロールが押された際に反応するようフォームとボタンのプロパティでバインドしてあげてください。

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            //クライアントへレスポンスを返した後に呼ばれるイベント
            Fiddler.FiddlerApplication.AfterSessionComplete 
                        += new Fiddler.SessionStateHandler(FiddlerApplication_AfterSessionComplete);
            
            //Fiddlerを開始。ポートは自動選択
            Fiddler.FiddlerApplication.Startup(0,Fiddler.FiddlerCoreStartupFlags.ChainToUpstreamGateway);

            //当該プロセスのプロキシを設定する。WebBroweserコントロールはこの設定を参照
            Fiddler.URLMonInterop.SetProxyInProcess(string.Format("127.0.0.1:{0}",
                        Fiddler.FiddlerApplication.oProxy.ListenPort), "<local>");

        }

        private void button1_Click(object sender, EventArgs e)
        {
            //WebBrowserコントロールに指定URIを開かせる
            webBrowser1.Navigate(textBox1.Text);
        }

        void FiddlerApplication_AfterSessionComplete(Fiddler.Session oSession)
        {
            //取り敢えずログを吐く
            System.Diagnostics.Debug.WriteLine(string.Format("Session {0}({3}):HTTP {1} for {2}",
                    oSession.id, oSession.responseCode, oSession.fullUrl, oSession.oResponse.MIMEType));
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            //プロキシ設定を外す
            Fiddler.URLMonInterop.ResetProxyInProcessToDefault();

            //Fiddlerを終了させる
            Fiddler.FiddlerApplication.Shutdown();
        }
    }

これを実行すると、こんな感じにアクセスしたURIがごりごりっとデバッグウィンドウに吐かれます。

ここまでの数行で、以下のことが実現できました

  1. Fiddlerの起動
  2. WebBrowserコントロールのアクセスをFiddlerへ流し込む
  3. HTTPセッションが完了するとFiddlerがイベントを呼び出す

というわけで、それぞれ説明していきます。


Fiddlerを起動しよう

Fiddlerを起動します。スタティックメソッドなので、ごりっと叩きます。

Fiddler.FiddlerApplication.Startup(0,Fiddler.FiddlerCoreStartupFlags.ChainToUpstreamGateway);

Fiddler.FiddlerApplication.Startupメソッドは、以下3つのオーバーロードを持ってます。

public static void Startup(
	int iListenPort,
	FiddlerCoreStartupFlags oFlags
);
public static void Startup(
	int iListenPort,
	bool bRegisterAsSystemProxy,
	bool bDecryptSSL
);
public static void Startup(
	int iListenPort,
	bool bRegisterAsSystemProxy,
	bool bDecryptSSL,
	bool bAllowRemote
)

Fiddlerアセンブリが持ってるコメントに「フラグで指定しろ。三引数と四引数のメソッドはおすすめしない」って書いてあるんで、フラグ引数のメソッドを中心に*6

iListenPortは接続を待機するポートを指定します。8080とかそんな数字を入れるわけですが、指定したポートが既に使われていて待機できない場合にFiddlerCoreはエラーだとか教えてくれないようです*7。困ったなと思って調べると、ポート0を指定すると適当に空きポートを探して占有し起動するようです*8。占有したポートはFiddler.FiddlerApplication.oProxy.ListenPortでわかるので、後でこのポートにつなぎに行くよう設定をします。

None
何もフラグを立ててない状態
RegisterAsSystemProxy
システムグローバルにプロキシとして登録する。bRegisterAsSystemProxy=trueと同じ
DecryptSSL
SSLを解読する。bDecryptSSL=trueと同じ
AllowRemoteClients
他マシンからの接続*9を受け入れる。bAllowRemote=trueと同じ
ChainToUpstreamGateway
外に出ていく際にシステムのプロキシ設定を使う
MonitorAllConnections
LANだけではなくVPNやPPP等あらゆるWinInet接続のプロキシとして登録する
CaptureLocalhostTraffic
ローカルホストでループバックする接続をFiddlerへ流してもらう
Default
上全部のフラグを立てる

というわけで、システム全体のHTTP通信を見るのでなければChainToUpstreamGatewayだけフラグを立てておけばいいはず。
ただ、ChainToUpstreamGatewayで使ってくれるのは「インターネットオプション/接続/LANの設定」にあるLANのプロキシ設定だけで、VPN接続する際のプロキシ設定*10は使ってくれませんVPNでプロキシ必須、みたいな環境下だとLANの設定としてVPNプロキシを指定してあげないとFiddlerさんは世界に出ていけないことになる。プロキシ設定に関しては後述します。


ちなみに、SSLの解読とか出来るわけねーだろっていうツッコミがあるわけですが、所謂中間者攻撃で解読する機能を持っているようです。

プロセスのプロキシを設定する

Fiddlerが接続を待っていても、Fiddlerにリクエストが飛んでいかなければ待ちぼうけしてるだけになってしまいます。

というわけで、Formに貼り付けたWebBrowserコントロールがインターネットへつなぐ際に経由するプロキシを指定するメソッドを呼びます。

Fiddler.URLMonInterop.SetProxyInProcess(string.Format("127.0.0.1:{0}",Fiddler.FiddlerApplication.oProxy.ListenPort), "<local>");

Fiddler.URLMonInterop.SetProxyInProcessを呼ぶことで当該プロセスのWinInetプロキシ設定をいじることが出来ます。

Q. WinInetとはなんぞや?

A. WinInetはInternet Explorerがインターネットへつなぐ際に使うライブラリです。で、WebBrowserコントロールの実体はInternet Explorer
よって、WinInetのプロキシ設定をすることはWebBrowserコントロールのプロキシ設定をすることになります。

要するに、WebBrowserコントロールのプロキシ設定をするメソッドです。

public static void SetProxyInProcess(
	string sProxy,
	string sBypassList
)
sProxy
プロキシホスト。「127.0.0.1:8081」でHTTP/HTTPSアクセスを全部127.0.0.1:8081経由で行います。
sBypassList
プロキシを経由しないアクセスパターン。空文字列だと全てのアクセスを上述のプロキシ経由で行います。って書くと、ローカルホスト*11へのアクセスは先述のプロキシを経由しないでアクセスします。

Fiddlerを起動した際にFiddlerが選んだポート番号をFiddler.FiddlerApplication.oProxy.ListenPortで取ってきて、そこへつなぎに行くようにすると先述のコードになります。

sProxyやsBypassListは単純にホスト名を書く以外に多少フォーマットがあり、詳細はInternetOpenの"lpszProxyName"や"lpszProxyBypass"パラメタの説明、およびEnabling Internet Functionalityの"Listing Proxy Servers"及び"Listing the Proxy Bypass"を参照してくださいな。

ちなみにWinInetがインターネットアクセスをする際、プロキシ設定はこの順で拾っていくようです。

  1. ハンドル*12単位
  2. プロセス単位
  3. システム*13単位
セッションイベントの処理
Fiddler.FiddlerApplication.AfterSessionComplete 
                        += new Fiddler.SessionStateHandler(FiddlerApplication_AfterSessionComplete);
void FiddlerApplication_AfterSessionComplete(Fiddler.Session oSession)
{
	System.Diagnostics.Debug.WriteLine(string.Format("Session {0}({3}):HTTP {1} for {2}",
		oSession.id, oSession.responseCode, oSession.fullUrl, oSession.oResponse.MIMEType));
}

通知して欲しいタイミングのイベントを登録すると、そのタイミングでイベントを呼び出してくれます。イベントの引数に、セッション情報のオブジェクトが渡されます。このオブジェクトから、レスポンスやらリクエストなどを取得することが出来ます。やったね。

イベントはいくつかありますが、主に使うのはこれぐらいでしょうか

BeforeRequest
クライアントからのリクエストをFiddlerが受信した際に呼ばれます。
BeforeResponse
サーバからのレスポンスをFiddlerが受信した際に呼ばれます。レスポンスを書き換える際に使うんですが、微妙に罠があるので後述します。
AfterSessionComplete
Fiddlerがクライアントへレスポンスを返し終えて、全てが終了した後に呼ばれます。

ちなみに、このイベントはUIスレッドとは別のスレッドから呼ばれています*14。なので、イベントハンドラ内でUIをつつく際はControl.Invoke()を呼ぶ必要があります。

おまけ

前述の情報でFiddlerCoreから情報を取ってくることは出来ました。やったね。

ということで、ちまちまとおまけを書いていきます。

上流プロキシの設定方法

FiddlerApplication.StartupでフラグChainToUpstreamGatewayを立てたらインターネットオプションからLANのプロキシ情報を拾ってきて、そこ経由で世界に出ていきます。

しかし、先述したようにインターネットオプションからLANのプロキシ情報がない場合*15はプロキシ情報を拾ってくれません。Fiddlerに上流プロキシをコードで設定させる方法を調べると、みんな大好きStackOverflowに作者からの答えがありました。

If you want to send each request to a proxy, and that proxy isn't the system's default: Before each request is sent, specify the X-OverrideGateway flag on the Session. Inside your BeforeRequest handler, add the following line:

oSession["X-OverrideGateway"] = "someProxy:1234";

How to manually set upstream proxy for fiddler core? - Stack Overflow

BeforeRequestイベントでサーバへリクエストを投げる前に、セッションオブジェクトのゲートウェイ情報を書き換えてくれ、とな。
ちなみに、「proxy:1234」だとhttpプロキシを想定してつなぎに行きますが、「socks=socksproxy:1080」という感じに"socks="というプレフィクスをつけると指定されたホストをSOCKS4aプロキシ*16とみなして繋ぎに行きます。

Using the X-OverrideGateway flag, use the socks= prefix to indicate that Fiddler should use the SOCKS v4a protocol when speaking to the upstream server.

HOWTO: Use Fiddler with a SOCKS proxy - Google グループ

レスポンス情報の取得について

BeforeRequestイベントなど、サーバからの返事がまだもらえてない状態では、セッション情報のレスポンスは当然に空っぽです。AfterSessionCompleteないしBeforeResponseでの話をします。

レスポンスを取ってくる場合、Fiddler.Session.ResponseBodyプロパティでバイナリ配列として取ってくる場合とFiddler.Session.GetResponseBodyAsString()メソッドから文字列で取ってくる二種類が考えられますが、これら二つは同一のデータを返さない事があります。具体的には、ResponseBodyはレスポンスそのまんまを持っていますが、GetResponseBodyAsString()はレスポンスデータを文字列に適切にデコードして返してきます。単純に考えると、文字列を取る場合はGetResponseBodyAsString()でも、ResponseBodyをSystem.Text.Encoding.UTF8.GetString()あたりに渡して文字列を貰ってもおんなじ結果になるはず。しかし、同じ結果にならないことがあります。

HTTPレスポンスがchunked transferされて複数のchunkで返ってきたりgzip圧縮されて返ってきた場合、ResponseBodyは複数chunkのままだったりgzip圧縮されたストリームのままになっています。

なので、ResponseBodyプロパティで期待するレスポンスデータを確実に取ってくるためには、oSession.utilDecodeResponse()を呼んでセッション内のレスポンス情報をデコードする必要*17があります。

文字列を取得する場合はGetResponseBodyAsString()を呼んでおけば良くてutilDecodeResponse()処理は内部で勝手にしてくれますが、文字列でない画像などバイトデータを扱う場合はここに注意する必要があります。

リクエストを握りつぶす

サーバへのリクエストを握りつぶし、クライアントへ適当なレスポンスを返したい場合があります。

BeforeRequestの段階でFiddler.Session.utilCreateResponseAndBypassServer();を呼ぶと、Fiddlerはレスポンスを生成してサーバへリクエストを投げること無くクライアントへ返事を返してくれます。

例えば、全リクエストを403に偽装する場合。

void FiddlerApplication_BeforeRequest(Fiddler.Session oSession)
{
         oSession.utilCreateResponseAndBypassServer();
         oSession.responseCode = 403;
         oSession.oResponse.headers.HTTPResponseStatus = "403 Forbidden";
         oSession.oResponse.headers["Date"] = DateTime.Now.ToString("R");
}

あるいは、適当なテキストを返す場合

void FiddlerApplication_BeforeRequest(Fiddler.Session oSession)
{
        oSession.utilCreateResponseAndBypassServer();
        oSession.responseCode = 200;
        oSession.oResponse.headers.HTTPResponseStatus = "200 OK";
        oSession.oResponse.headers["Content-Type"] = "text/plain";
        oSession.oResponse.headers["Date"] = DateTime.Now.ToString("R");
        oSession.ResponseBody = Encoding.ASCII.GetBytes("hogehoge");
}

レスポンスはオブジェクトが生成されるだけなので、中身は自分で書いてやる必要があります。

レスポンスを書き換える

サーバから返ってきたレスポンスを適当に書き換える事を考えます。

基本的にはModifying a Request or Responseを参照すればいいのですが、FiddlerがBeforeResponseを呼ぶ際にバッファリングしてもらわないとBeforeResponseでデータをいじれないので、BeforeRequestあたり*18でSession.bBufferResponseをtrueにしておく必要があります。

void FiddlerApplication_BeforeRequest(Fiddler.Session oSession)
{
        oSession.bBufferResponse = true;
}
void FiddlerApplication_BeforeResponse(Fiddler.Session oSession)
{
        //置換
        oSession.utilDecodeResponse(); oSession.utilReplaceInResponse("hoge", "fuga");

    //設定
        oSession.utilSetResponseBody("aaa");
}

参照ドキュメントにもありますが、Session.utilReplaceInResponse()などでレスポンスを書き換える前にはSession.utilDecodeResponse()でレスポンス情報をデコードしておく必要があります。

ちなみに、Session.bBufferResponse=falseのとき、サーバから来たレスポンスをクライアントへストリーミングで返してからBeforeResponseを呼ぶようです。Beforeちゃうやんけ。Session.ResponseBodyは空っぽかというとそんなこともなくちゃんとデータが入ってるので、ResponseBodyへデータを放り込みながらクライアントへ返事を返し、それが終わったところでBeforeResponseが呼ばれるんでしょうなたぶん。

Note to FiddlerCore Developers:When the bBufferResponse property on a session is set to false, that session's response is streamed to the client as it is being read from the server. As a consequence, the client has already received the response before the BeforeResponse event has fired. In contrast, if that property is set to true, that Session's response will be buffered until complete, then the BeforeResponse event fires, then the response is sent to the client. The last event guaranteed to fire before the client starts getting response bytes is FiddlerApplication.ResponseHeadersAvailable. You can use that event handler to set the bBufferResponse property if you need to modify the response bytes before the client receives them.

Fiddler Web Debugger - Streaming Mode

セッションを無視する

全部のHTTPセッションを見たい、というのは割と例外的で、殆どの場合は一部のHTTPセッションだけ相手することになります。

そのためか、Fiddler.SessionクラスにはIgnore()というメソッドがあります。これを呼ぶと、以降のバッファリングをせずイベントも呼ばないとある。

Calling Ignore()on a Session disables buffering of the Session’s response, sets its SessionFlags.Ignored bitflag, and sets the log-drop-request-body and log-drop-response-body flags. With the Ignored flag set, FiddlerCore will no longer fire events as the Session is processed. For instance, none of the BeforeRequest, BeforeResponse, StateChanged, etc, handlers will be fired.

FiddlerCore Updated


しかし、実際にBeforeRequestなどでSession.Ignore()を呼んで試してみる*19と、確かにSession.ResponseBodyは空っぽなんですがBeforeResponse*20はしっかり呼ばれる。

なので、イベント内ではセッションのフラグを確認してSessionFlags.Ignoredが立っていれば処理をしないコードが必要になります。

void FiddlerApplication_BeforeResponse(Fiddler.Session oSession)
{
        //フラグを確認
        if (oSession.isFlagSet(SessionFlags.Ignored))
                return;
        
        ...
}

v4.4.5.6にupdateした時点で、BeforeResponseは呼ばれなくなりました(5 May 2013追記)。

次回予告:「JSONC#で読む」→書きました:C#でJSONを読み込むメモ - (。・ω・。)ノ・☆':*;':*

*1:自分の書いたコードが一番信用ならんかったりするよね。

*2:.NET 4ではDotNet4ディレクトリにFiddlerCore4.dllがあります

*3:このヘルプ、ソースコードから自動生成っぽいんですが、一部のメソッド説明がMissing documentationになってたり、対象のFiddlerが2.3.8.5で、最新版2.4.5.3からちょっと離れてるとか微妙に残念。VSがアセンブリから自動生成するツールヒントも参考になります。

*4:ちなみに本稿執筆時点ではわたしの所属する京大の図書館にないので、近所で所蔵してる京産大図書館までチャリでえっちらおっちら行ってきました。

*5:というか読まないとわからない情報もちらほら。あと、検索するとこの本の原本プレビューがGoogleBooksで引っかかったりすることもある。

*6:フラグ引数の縮小版が三引数と四引数のメソッドです。

*7:現状、エラーを検出する手段を発見できてない。

*8:本質的な解決じゃないですが…。また、後から使いたいポートを勝手に占有される可能性もあります。

*9:つまりlocalhost以外からの接続。

*10:京大KUINSのpptp接続時に使うプロキシ設定など。

*11:ドメインにドットがない、という極めてアバウトな判断みたいですが。

*12:WinInetを使う際にInternetOpen APIで取得する。プロセス内に複数存在し得る。

*13:IEのインターネットオプションで指定するアレ。ちなみに、Chromeもこの設定を使います。

*14:セッションを処理するワーカースレッドっぽい気がする。

*15:先述したVPNでのみプロキシを使う場合など。

*16:4aなので、多分名前解決もSOCKSサーバに任せてくれると信じたいけどまだ未検証。

*17:ちなみにこのメソッドは引数がなく、呼ぶとセッション内のレスポンス情報が書き換えられます。

*18:レスポンス受信前に呼べばいいので、ResponseHeadersAvailableでもいいらしい。サンプルコードではBeforeRequestで呼んでる

*19:クライアントからのリクエスト読み込み後に呼ばれるBeforeRequestより先に呼ばれるRequestHeadersAvailableでも試したけど同じ結果。

*20:AfterSessionCompleteも呼ばれる。