Future Convergence PRJ.

主にプログラミング関連で調べたことのメモ(趣味プログラムなので動作は保証しません)

VB.NETでパケットキャプチャ2~HTTPリクエストボディ・レスポンスボディの取得

VB.NETでパケットキャプチャ作成 - Future Convergence PRJ.の続き。

VB.NETのパケットキャプチャでHTTPリクエストボディ・レスポンスボディを取得するのが目標。utf8でエンコードしてファイルに書き込んだ場合、HTTPを取り扱ったパケットだと以下のように表示される。

(IPヘッダ・TCPヘッダ部分の文字化け)
HTTP/1.1 200 OK
Date: Sun, 04 Jan 2015 08:14:04 GMT
Server: Apache
Vary: Client-Version,Accept-Encoding
X-Powered-By: XXX Platform
server_version: 20120101
user_id: 99999
version_up: 0
status_code: 200
authorize: timeStamp=1420359244&version=1.0&token=xxxxxxxxxx&requestTimeStamp=1420359244
X-Message-Code: aaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Content-Encoding: gzip
Content-Length: 885
Content-Type: application/json; charset=utf-8
Connection: close
(gzipによる文字化け)


(gzipによる文字化け)と書いた部分がレスポンスボディにあたる部分で、ヘッダに「Content-Encoding: gzip」と記載がある通り、gzip圧縮されているために文字化けしており、解凍しないと内容を見ることができない。そこで、ボディ部分のみ取得してgzip解凍の処理をする必要がある。httpの決まりでヘッダとボディの間には改行コード(CRLF)が2回入る。これはバイナリ表記だと「0A 0D 0A 0D」なので、これを判別して以降をボディとして取得するようにする。

あとは、パケットの終わりまで取得すれば終わりといいたいところだけど残念ながらそうはいかず…1パケットにボディ部が全て入っていれば問題ないんだけど、たいてい複数パケットに分かれて送られてくるんで、パケットの最後までの取得が終わったら、取得したバイト数とヘッダの「Content-Length: 」に記載されているサイズを比較して、すべて取得できているか判別する必要がある。もしまだ「Content-Length: 」のサイズに達していないのなら次のパケットからボディ部の続きを取得するようにする。

この、次にくるパケット以降にはhttpヘッダがないので、「0A 0D 0A 0D」による判別は使えない。tcpヘッダの終わった直後からをボディ部として取得するようにする。これはパケットの先頭からIPヘッダサイズとtcpヘッダサイズの和の分を進んだ先からになる。パケットの最後まで取得が終わったらまた「Content-Length: 」のサイズと比較、「Content-Length: 」のサイズに達したらボディの取得は終了、まだなら次のパケットをまた待つ。
こうしてボディ部の取得が完了したら一度gzipファイルとして書き出し、出来たファイルを読み込み・gzip解凍すれば目的は達成。厳密にやるならTcpの順序制御とかを見てデータの結合しないとダメなんだろうけど、通信対象と1か所にしとけば今のところ問題なく動いたのでその処理は割愛。ソースは以下の通り。

Imports System.Net
Imports System.Net.Sockets
Imports System.Text
Imports System.IO.Compression
Imports System.IO

Module PakCap

    'IPヘッダ長
    Private Const IP_HEADER_LENGTH As Integer = 20

    Sub Main()

        '文字コードはUTF8にする
        Dim enc As Encoding = Encoding.UTF8
        '自分のIPアドレスを設定する
        Dim ip As String = "192.168.3.100"

        Dim socket As New Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.IP)
        socket.Bind(New IPEndPoint(IPAddress.Parse(ip), 0))
        socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AcceptConnection, True)
        socket.IOControl(IOControlCode.ReceiveAll, New Byte() {1, 0, 0, 0}, New Byte() {0, 0, 0, 0})

        Dim packetCounter As Integer = 0
        Dim fileCounter As Integer = 0
        Dim isBuffered As Boolean = False
        Dim contentSize As Integer = 0
        Dim contentCounter As Integer = 0
        Dim contentBuff As Byte() = Nothing
        Dim isGzip As Boolean = False

        Using ws As New System.IO.StreamWriter("C:\test\packet.txt", False, enc)

            Do
                Dim buff As Byte() = New Byte(4095) {}
                socket.Receive(buff)

                'IPパケットヘッダ情報の取得
                Dim packetSize As Integer = buff(2) * 256 + buff(3)
                Dim packetId As Integer = buff(4) * 256 + buff(5)
                Dim packetFlg As Integer = buff(6) * 256 + buff(7)
                Dim protocolNum As Integer = buff(9)

                '送信元IP
                Dim srcIP As String = String.Format("{0}.{1}.{2}.{3}", buff(12), buff(13), buff(14), buff(15))
                '送信先IP
                Dim dstIP As String = String.Format("{0}.{1}.{2}.{3}", buff(16), buff(17), buff(18), buff(19))

                '送信元Port
                Dim srcPort As Integer = buff(20) * 256 + buff(21)
                '送信先Port
                Dim dstPort As Integer = buff(22) * 256 + buff(23)

                'utf8の文字列
                Dim utf8Str As String = enc.GetString(buff, 0, packetSize)

                Console.WriteLine("packetSize={0}", packetSize)
                Console.WriteLine("送信元{0}/{1}  -  送信先{2}/{3}", srcIP, srcPort, dstIP, dstPort)
                Console.WriteLine()

                '特定サーバとの通信に絞って処理をする
                '他のサーバとのパケットが混ざるとボディの取得が失敗する可能性が高いと思われるため
                Dim targetIP as String = "210.xxx.xxx.xxx"
                If srcIP = targetIP Or dstIP = targetIP Then

                    If isBuffered Then
                        'バッファリング待機中の場合
                        If protocolNum = 6 Then
                            'TCPの場合ヘッダ長を取得
                            Dim tcpHeaderLength As Integer = Convert.ToInt32(Convert.ToString(buff(32), 2).PadLeft(8, "0").Substring(0, 4), 2) * 4
                            For i As Integer = IP_HEADER_LENGTH + tcpHeaderLength To packetSize - 1
                                contentBuff(contentCounter) = buff(i)
                                contentCounter = contentCounter + 1

                                'ContentSizeを超えるデータ量だった場合は中断(パケット取り違えの場合)
                                If contentCounter >= contentBuff.Length Then
                                    Exit For
                                End If
                            Next
                        End If

                        'もしパケットにContent-Lengthが含まれていたらバッファリングは中断する
                        If utf8Str.Contains("Content-Length:") Then
                            isBuffered = False
                        End If

                    End If

                    If Not isBuffered Then
                        'バッファリング待機なしの場合

                        'コンテンツバッファカウンターのクリア
                        contentCounter = 0
                        'Content-Lengthのクリア
                        contentSize = 0

                        'httpヘッダのContent-Lengthを取得
                        If utf8Str.Contains("Content-Length:") Then
                            Dim r As New System.Text.RegularExpressions.Regex("Content-Length: ([0-9]+)", System.Text.RegularExpressions.RegexOptions.IgnoreCase Or System.Text.RegularExpressions.RegexOptions.Singleline)
                            Dim mc As System.Text.RegularExpressions.MatchCollection = r.Matches(enc.GetString(buff))

                            For Each m As System.Text.RegularExpressions.Match In mc
                                '正規表現に一致したグループの文字列を表示 
                                contentSize = m.Groups(1).Value
                                Exit For
                            Next
                        End If

                        If contentSize > 0 Then

                            'gzipかどうかの判別
                            isGzip = utf8Str.Contains("Content-Encoding: gzip")

                            'Contentがスタートする場所を取得
                            Dim contentStart As Integer = packetSize
                            For i As Integer = 4 To packetSize - 1
                                If buff(i - 4) = 13 AndAlso buff(i - 3) = 10 AndAlso buff(i - 2) = 13 AndAlso buff(i - 1) = 10 Then
                                    contentStart = i
                                    Exit For
                                End If
                            Next

                            'Contentの取得
                            contentBuff = New Byte(contentSize - 1) {}
                            For i As Integer = contentStart To packetSize - 1
                                contentBuff(contentCounter) = buff(i)
                                contentCounter = contentCounter + 1
                            Next

                        End If

                    End If

                    If contentSize > 0 AndAlso contentBuff IsNot Nothing Then

                        If contentCounter >= contentSize - 1 Then
                            'コンテンツが全部取得できた
                            isBuffered = False

                            'ファイルに書き出す
                            Dim filePath As String = "C:\test\Contents[" & packetCounter.ToString.PadLeft(6, "0") & "]"
                            Dim thawFilePath As String = filePath
                            If isGzip Then
                                filePath = filePath & ".gz"
                            End If

                            Using fs As New System.IO.FileStream(filePath, System.IO.FileMode.Create, System.IO.FileAccess.Write)
                                fs.Write(contentBuff, 0, contentBuff.Length)
                            End Using

                            If isGzip Then
                                Dim gzipFileStrm As New System.IO.FileStream(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read)
                                Using gzipStrm As New System.IO.Compression.GZipStream(gzipFileStrm, System.IO.Compression.CompressionMode.Decompress)
                                    Using outFileStrm As New System.IO.FileStream(thawFilePath, System.IO.FileMode.Create, System.IO.FileAccess.Write)

                                        Dim gzipBuff(1024) As Byte
                                        While True
                                            '書庫から展開されたデータを読み込む
                                            Dim readSize As Integer = gzipStrm.Read(gzipBuff, 0, gzipBuff.Length)
                                            '最後まで読み込んだ時は、ループを抜ける
                                            If readSize = 0 Then
                                                Exit While
                                            End If '展開先のファイルに書き込む
                                            outFileStrm.Write(gzipBuff, 0, readSize)
                                        End While

                                    End Using
                                End Using

                                '圧縮ファイルを削除する
                                File.Delete(filePath)

                            End If
                            
                        Else
                            'コンテンツが全部取得できていないので次のパケットを待つ
                            isBuffered = True
                        End If

                    End If

                    ws.WriteLine("[PacketCount={0}]", packetCounter.ToString.PadLeft(6, "0"))
                    ws.WriteLine("送信元{0}/{1}  -  送信先{2}/{3}", srcIP, srcPort, dstIP, dstPort)
                    ws.WriteLine("packetSize={0}, protocolNum={1}, contentSize={2}", packetSize, protocolNum, contentSize)
                    ws.WriteLine(utf8Str)

                    ws.WriteLine()
                    ws.Write("----------------------- BINARY -----------------------")

                    'パケットの内容を16進数で表記
                    For i As Integer = 0 To packetSize - 1

                        If (i Mod 16) = 0 Then
                            ws.WriteLine()
                            ws.Write("{0}:", (i / 16).ToString().PadLeft(4, "0"))
                        End If

                        ws.Write(" {0}", Convert.ToString(buff(i), 16).PadLeft(2, "0"))

                    Next
                    ws.WriteLine()
                    ws.WriteLine("------------------------------------------------------")
                    ws.WriteLine()
                    ws.WriteLine()

                    packetCounter = packetCounter + 1

                End If

            Loop
        End Using

    End Sub
End Module