ErlangでHTTPリクエストをパースする
どうもこんにちは。前回に引き続きErlangでProxyを作っています。HTTP ProxyでURIごとにルーティングをさせる。さらにProxyを経由してHTTPリクエストをするので「X-Forwarded-For」をHeaderに追加させる必要があります。そのためHTTPリクエストの簡単なパース処理を実装しました。その内容をまとめていきます。
前回の内容
動作環境
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_METHOD
を define
で定義し、ガードの中で使っています。
これでメソッドが正しいかどうかチェックをしています。
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、バージョンも無事にマップとして取り出せました。