httpcap-rbを公開しました
未練
本当はhttpcapっていう名前で公開したかったけど2年程前に既に名前が取られてたし、しかも内容が
module HTTPCap end
のみとかいう殆ど機能していないライブラリで、とても悔しかったんですがどうにもできないのでhttpcap-rbという名前にしました
機能
pcapファイルからTCPのsend/recvのパケットをよしなに結合したあと、HTTPのメッセージとしてパースして、HTTPリクエストとHTTPレスポンスを一つの纏まりとして固めてから返します
require 'httpcap' HTTPcap.http_flows('./http.pcap') do |flow| p flow.request.body # => "{\"userId\":12345}" p flow.request.headers['Authorization'] # => "Bearer hogehoge123455567890" p flow.response.http_status # => 200 p flow.response.body # => "{\"userId\":12345,\"name\":\"naari3\",\"author\":true}" p flow.request.headers['Content-Length'] # => "46" end
(これ、リクエストとレスポンスを1:1として考えてるんですけど、もしかして仕様的に間違ってたりしますか?)
なお、httpsとかの暗号化された通信については特に考慮されてません
そのうちしたいけど、平文の通信だけ捌けるようになった今、自分の要件を満たすことは出来たのであまりモチベがない
実装
TCPのパケットの結合と振り分け
tcpの結合や取り扱いは reassemble_tcp というgemをそのまま使用しました
wiresharkと同じ結合をしてくれます(synとackを見てbodyをくっつけるアレ)
READMEに書かれている ReassembleTcp.tcp_data_stream を使うとセッションが担保されなくなったり、pktじゃなくてbodyだけが返ってきたりするので ReassembleTcp.tcp_connections を使うと良いです (こっちはPacketFuでパースされたあとの各packetが返る)
というわけで ReassembleTcp.tcp_connections を使うとtcpの各セッションごとに格納、かつ結合されたパケットが返ってきます
各セッションの結合後のパケットを #each_slice(2) を使って回します
おそらくどちらか片方ががリクエストで、もう片方がレスポンスなのでそう思ってパースをします
HTTPメッセージのパース
HTTPのメッセージはリクエストとレスポンスの両方を捌く必要があります
PumaとかWEBrickみたいなよく使われているライブラリがパースした結果を使うことが出来ればそれなりにリッチなものが返せるかと期待したんですが、リクエストとレスポンスの両方を良い感じに扱った実装を見つけることが出来ませんでした
例えば WEBrick::HTTPRequest には #parse というのがいてIOを渡すとよしなにHTTPのパースをしてくれるらしいんですが、 WEBrick::HTTPResponse に #parse は存在しません
なぜならサーバーの実装としてHTTPのメッセージをパースする機能は、クライアントからのリクエストを解釈する時以外に必要ないからです
なので、CとかC++とかに存在する素朴なHTTPパーサー実装のRubyバインドを探すことにしました (つまり要素によしなにアクセスしたりする部分は自分で書かなければいけない…)
今回は http-parser-lite を使いました
httpの気持ちは知らないので何故httpのパーサーがおおよそオブザーバーパターンなのがよくわからないんですが、上のgemも例に漏れずオブザーバーパターンを採用したものになっています
他と違ってこういう書き方が出来るのが特徴とのことですが、
parser = HTTP::Parser.new parser.on_message_begin do puts "message begin" end parser.on_message_complete do puts "message complete" end parser.on_status_complete do puts "status complete" end
今回は上記の機能を使うことでうまいことリクエストとレスポンスのクラスの抽象化が出来ました
# リクエストとレスポンスの抽象概念 class Message attr_reader :body, :headers def initialize(type, data) @parser = HTTP::Parser.new(type) # 略 %i[on_message_complete on_url on_header_field on_header_value on_headers_complete on_body].each do |name| @parser.send(name, &method(name)) end receive_data(data) end def on_header_field(value) @headers.stream(Headers::TYPE_FIELD, value) end def on_header_value(value) @headers.stream(Headers::TYPE_VALUE, value) end def on_headers_complete @headers.stream_complete end # 略 end
ヘッダーが存在することとボディが存在することは共通なのでこのクラスで実装して、それ以外の差異を継承先のクラスで実装する形をとっています
ふあんなこと
- TCPのシーケンス順に2つずつ結合済みパケットを取得して 片方をリクエスト、片方をレスポンス、と組を雑に決めてしまったが、もしかしてもうちょっとまともにリクエストとレスポンスの組を特定する方法がある??
- 自由な内容のpcapファイルを作る方法がわからない…(調べていない)のでテストケースが不足している
- ノイズとして別のTCPパケットも紛れ込んできそうだよね
おわりに
今回は久々に自分の要望がメインとなってgemをつくることになったが、モチベの保持がとても楽でよかったです
自分の作りたいものが出てこないというのが失格感あって悲しいので、問題解決を頑張っていきたいと思います