http_parserを乗せ変えた話
こんにちわ、情弱DQNです。
最近は寝かしていたpicowsを見直しています。
その時にとある事が気になったのでhttp_parserを乗せ変えました。
でその時の話。
以前まで
今まではmongrel, thinなどでも使われているZedの書いたragelベースのhttp parser を使用していました。
もちろんですが、python版に書き直しています。
(余談ですがeventletの作者も同じようなものを作ってますが、メモリリークしてます...)
ragelベースのparesrは記述が比較的楽だし、それなりに速いので便利です。
(ただコールバックスタイルが好きかどうかって話もあるかも知れません)
mongrelのパーサの処理の流れは大体こんな感じです。
- start actionで開始位置のポインタをマーク
- 読み進め、条件にマッチしてるか判断
- 条件にマッチしなくなるとendと解釈
- end actionでstart actionでマークしたポインタと長さをコールバックに渡して呼ぶ
parserに流し込む文字列はパースし終わるまで保持しておかないといけません。
(開始位置のポインタがわからなくなる)
なのでsocketから読み込んだ文字列を連結しながらparseしていく方式です。
このやり方の場合、endの解釈まで文字列を保持しているので長いデータが来ると問題がある可能性があります。
例えば1G超のhttp header valueを送った場合、何も考えてないとそれだけ文字列を確保してしまします。
(end が呼ばれるまで何も状態が変わらないし、コールバックも呼ばれない)
これは昔にApacheもやられた手法です。
(Long Header DoS)
もちろん多くの場合はフロントに別のhttp serverを置いたりするのでそこでガードする事はできます。
(nginxもapacheもヘッダ1行あたりのバッファサイズは8192byte(デフォルト)のはず)
現状
とはいえフロントのproxyがどこまでヘッダを検証してくれるのか、ヘッダをそのまま送ってくるのか等、イマイチ
信用できななかったので長い文字列をなるべく保持しないようにnode.jsでも使ってるhttp_parserを使用しました。
ry's http_parserの話
使用しているのはこれです。
http://github.com/ry/http-parser
このparserの元ネタはlibebbで、それを元にチューニングと機能追加しています。
特徴は以下
- 文字列を溜め込まないでパースする
まあ細かいxxxxに対応とかもありますが、これが一番大きいかと思います。
そして大きな制限としてはfield-valueがmultilineに対応していないという点です。
(これはid:malaも書いていますが。)
これに関しては僕がforkして対応したものを作ってます。
http://github.com/mopemope/http-parser
で使い方。
コールバックの仕込みなどはtest、ドキュメントなどを見てください。
size_t len = 8*1024, nparsed; char buf[len]; ssize_t recved; recved = recv(fd, buf, len, 0); if (recved < 0) { /* Handle error. */ } /* Start up / continue the parser. * Note we pass recved==0 to signal that EOF has been recieved. */ nparsed = http_parser_execute(parser, &settings, buf, recved); if (parser->upgrade) { /* handle new protocol */ } else if (nparsed != recved) { /* Handle error. Usually just close the connection. */ }
readしたものをそのままparseしています。
データがまたがったらどうするんじゃい!ってツッコミがあると思いますが、コールバック
の呼ばれ方に少し癖があります。
このhttp_parserは読み込むバッファがなくなった際もその状態のコールバックを呼びます。
状態はparesrが保持しているので、再度読み込むバッファを突っ込んでパースを継続します。
コールバックの例;
struct line { char *field; size_t field_len; char *value; size_t value_len; }; #define CURRENT_LINE (&header[nlines-1]) #define MAX_HEADER_LINES 2000 static struct line header[MAX_HEADER_LINES]; static int nlines = 0; static int last_was_value = 0; void on_header_field (http_parser *_, const char *at, size_t len) { if (last_was_value) { nlines++; if (nlines == MAX_HEADER_LINES) ;// error! CURRENT_LINE->value = NULL; CURRENT_LINE->value_len = 0; CURRENT_LINE->field_len = len; CURRENT_LINE->field = malloc(len+1); strncpy(CURRENT_LINE->field, at, len); } else { assert(CURRENT_LINE->value == NULL); assert(CURRENT_LINE->value_len == 0); CURRENT_LINE->field_len += len; CURRENT_LINE->field = realloc(CURRENT_LINE->field, CURRENT_LINE->field_len+1); strncat(CURRENT_LINE->field, at, len); } CURRENT_LINE->field[CURRENT_LINE->field_len] = '\0'; last_was_value = 0; } void on_header_value (http_parser *_, const char *at, size_t len) { if (!last_was_value) { CURRENT_LINE->value_len = len; CURRENT_LINE->value = malloc(len+1); strncpy(CURRENT_LINE->value, at, len); } else { CURRENT_LINE->value_len += len; CURRENT_LINE->value = realloc(CURRENT_LINE->value, CURRENT_LINE->value_len+1); strncat(CURRENT_LINE->value, at, len); } CURRENT_LINE->value[CURRENT_LINE->value_len] = '\0'; last_was_value = 1; }
途中で途切れる可能性があるのでstrncatで連結します。
また新しいヘッダかどうかの判断をフラグで管理します。
(last_was_value)
少し脱線しますが、この方式だとパフォーマンス的にどうなの?って話になると思います。
読み込みバッファは毎回捨て同然なので必要な値は、自前でコピーし、保持しておく必要があります。
最初memcpyが大量に発生するのが予想されたので少し気になりました。
実際ゴリゴリ組んでみるとそこまで問題ないようです。
(ココを気にするよりもっと気にすることがあるのが大半です)
で注目すべきは読み込むバッファがなくなった際もその状態のコールバックを呼ぶ点です。
ragelではendがくるまでールバックが呼ばれませんが、こいつはバッファが切れた時点でも呼ばれるので
そのタイミングでいろいろとチェックすることができます。
コールバックでは文字列の長さも渡されるので長すぎればその時点で弾くことできますし、
(最高でreadしたバッファサイズの文字列がコールバックで渡される)
既に保持している文字列と連結した結果長いと予想されるケースも検出できます。
(ragelでは何も考えないとずーとコールバックが呼ばれず下手するとクソでかいバッファを保持しかねない)