ErlangでHTTPリクエストをパースする

どうもこんにちは。前回に引き続きErlangでProxyを作っています。HTTP ProxyでURIごとにルーティングをさせる。さらにProxyを経由してHTTPリクエストをするので「X-Forwarded-For」をHeaderに追加させる必要があります。そのためHTTPリクエストの簡単なパース処理を実装しました。その内容をまとめていきます。

前回の内容

kobatako.hatenablog.com

動作環境

Erlangで文字列処理

まずはErlangでの文字列処理について。そもそもErlangでの文字列は他の言語の文字とは少し違い、リスト、バイナリになります。Erlangで文字列を表現するときは以下のようになります。

> [97, 98, 99].
"abc"
> is_list("abc").
true
> is_binary("abc").
false
> is_binary(<<"abc">>).
true

is_list("abc").の結果がtrueっということは"abc"はリストになります。つまり文字列はリストで表現されてます。 そしてもう一つの表現方法としてバイナリ文字列として文字列を表現することもできます。 Erlangでのリストとバイナリ、2つの文字列の違いはメモリ空間の使い方が大きく違います。リストは単方向リストとなりますがバイナリ文字列は一つの塊としてメモリに保存されます。

HTTPリクエストのフォーマット

HTTPのパケットは基本的に文字列になります。ヘッダー情報もすべて文字列となるので文字列を操作する必要があります。 まずはHTTPリクエストの内容を見てみましょう。

<<"GET /hello?hoe=test HTTP/1.1\r\nHost: 127.0.0.1:5555\r\nUser-Agent: curl/7.58.0\r\nAccept: */*\r\n\r\n">>

これはErlangでキャプチャしたものになります。バイナリ文字としてパケットを取得しています。各要素について見ていきましょう。

GET はHTTPリクエストのメソッドですね。 /hello?hoe=testっというURIでリクエストが来ています。 HTTP/1.1 はHTTPのバージョンになります。

そして残りの部分がHeader部分になります。 これをErlangでパースします。

パース処理

全体的なパース処理を見てみましょう。

parse(Buffer) ->
  {ok, Method, Res0} = parse_method(Buffer, <<>>),
  {ok, Uri, Res1} = parse_uri(Res0, <<>>),
  {ok, Ver, Res2} = parse_version(Res1),
  {ok, Header} = parse_header(Res2, []),
  #{method => Method, uri => Uri, version => Ver, header => Header}.

こんな感じでパース処理を実装しました。それぞれのパース処理は以下のようになります。

parse_method : メソッド部分のパース parse_uri : URI部分のパース parse_version : バージョン部分のパース parse_header : ヘッダー部分のパース

そしてすべての結果をマップとして返します。 特に実装は複雑にはしてなく、エラー処理等は省いています。

メソッド部分

まずは

  {ok, Method, Res0} = parse_method(Buffer, <<>>),

の部分を見ていきます。

parse_method(<<?BLANK, Res/binary>>, Method) when ?IS_HTTP_METHOD(Method) ->
  {ok, Method, Res};
parse_method(<<C, Res/binary>>, Method) ->
  parse_method(Res, <<Method/binary, C>>).

これだけで実装しました。 ?BLANKはdefineで空白文字を定義しておきます。

-define(BLANK, $\s).

$\sアスキーコードで32を表してます。アスキーコードの32は空白文字になるので空白文字が出るまで第二引数のバイナリに先頭の文字を追加していきます。 そして、空白文字が出たときに第二引数の文字、つまりメソッドがちゃんとHTTPのメソッドなのか IS_HTTP_METHOD で確認します。

-define(GET, <<"GET">>).
-define(POST, <<"POST">>).
-define(PUT, <<"PUT">>).
-define(DELETE, <<"DELETE">>).
-define(IS_HTTP_METHOD(H),
              (H =:= ?GET) or (H =:= ?POST)
           or (H =:= ?PUT) or (H =:= ?DELETE)).

IS_HTTP_METHODdefineで定義し、ガードの中で使っています。 これでメソッドが正しいかどうかチェックをしています。

URIのパース処理

先程のメソッドと同じようにURIもパースします。

parse_uri(<<?BLANK, Res/binary>>, Uri) ->
  {ok, Uri, Res};
parse_uri(<<C, Res/binary>>, Uri) ->
  parse_uri(Res, <<Uri/binary, C>>).

バージョンのパース処理

バージョンだけは長さが固定長になっているのでもっと楽に取り出しましょう。

parse_version(<<"HTTP/1.0\r\n", Res/binary>>) ->
  {ok, http_1_0, Res};
parse_version(<<"HTTP/1.1\r\n", Res/binary>>) ->
  {ok, http_1_1, Res};
parse_version(<<"HTTP/2.0\r\n", Res/binary>>) ->
  {ok, http_2_0, Res};
parse_version(Res) ->
  {error, not_match_http_version, Res}.

そのままパターンマッチで処理をします。 それぞれアトムを定義して、後ほどこれをいい感じに処理をできるようにします。

ヘッダーのパース処理

今回で一番めんどくさいヘッダーのパース処理です。 では、ヘッダーのパース処理を見ていきましょう。

parse_header(<<$\r, $\n, $\r, $\n>>, Headers) ->
  {ok, Headers};
parse_header(<<$\r, $\n, _/binary>>, Headers) ->
  {ok, Headers};
parse_header(<<Res0/binary>>, Headers) ->
  {ok, Header, Res} = parse_header_field(Res0, <<>>),
  parse_header(Res, [Header| Headers]).

parse_header_field(<<>>, _) ->
  {error, not_header_field};
parse_header_field(<<$:, Res0/binary>>, Field) ->
  {ok, Value, Res} = parse_header_value(Res0, <<>>),
  {ok, {Field, string:trim(Value)}, Res};
parse_header_field(<<C, Res/binary>>, Field) ->
  parse_header_field(Res, <<Field/binary, C>>).

parse_header_value(<<>>, _) ->
  {error, not_header_value};
parse_header_value(<<$\r, $\n, Res/binary>>, Value) ->
  {ok, Value, Res};
parse_header_value(<<C, Res/binary>>, Value) ->
  parse_header_value(Res, <<Value/binary, C>>).

全体のヘッダーのパースをするparse_headerと各ヘッダー項目をパースするためフィールドと値を取り出す関数を別々にして実装しています。 parse_header_field でヘッダーのフィールド部分を取り出し、 parse_header_value でフィールドの値を取り出します。

最終的なパースした結果はこうなります。

#{header =>
      [{<<"Accept">>,<<"*/*">>},
       {<<"User-Agent">>,<<"curl/7.58.0">>},
       {<<"Host">>,<<"127.0.0.1:5555">>}],
  method => <<"GET">>,uri => <<"/hello?hoe=test">>,version => http_1_1}

ヘッダー部分もフィールドと値でタプルの配列として取れましたし、メソッド、URIバージョンも無事にマップとして取り出せました。

まとめ

  • バイナリ文字列をそのままパースしていきました。パース自体も簡単なものだったのでサクサクっと作ることができました。
  • 今回のような簡単な文字列操作なら自前で実装できますが、もう少し複雑なものとなるとBIFでstringパッケージがあるのでそれを使えばいいかと。
  • まだパースしただけなのでURIでのルーティング処理を実装できてないんで実装を進めていきます。
  • ここまででバックグランド側のサーバには「X-Forwarded-For」がついてる状態でリクエストが行くようになりました。次はURIでのルーティング処理を実装していきたいと思います。