pcapファイルからhttpリクエストとレスポンスを扱えるように色々するgem httpcap-rbを公開した

httpcap-rbを公開しました

rubygems.org

github.com

未練

本当は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をつくることになったが、モチベの保持がとても楽でよかったです

自分の作りたいものが出てこないというのが失格感あって悲しいので、問題解決を頑張っていきたいと思います