Skip to content

Commit

Permalink
(feat) Support LF separators since rfc7230-3.5 allows for LF-only (#706)
Browse files Browse the repository at this point in the history
  • Loading branch information
leonardb committed Oct 10, 2023
1 parent bbe73c8 commit 992cc36
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 13 deletions.
53 changes: 40 additions & 13 deletions src/hackney_http.erl
Expand Up @@ -156,10 +156,12 @@ execute(#hparser{state=Status, buffer=Buffer}=St, Bin) ->
end.

%% Empty lines must be using \r\n.
parse_first_line(<< $\n, _/binary >>, _St, _) ->
{error, badarg};
parse_first_line(<< $\n, Buffer/binary >>, #hparser{empty_lines = Empty0} = St, Empty) ->
parse_first_line(Buffer, St#hparser{buffer = Buffer, empty_lines = Empty0 + 1}, Empty + 1);
%% We limit the length of the first-line to MaxLength to avoid endlessly
%% reading from the socket and eventually crashing.
parse_first_line(_Buffer, #hparser{max_empty_lines=MaxEmpty}, Empty) when Empty >= MaxEmpty ->
{error, bad_request};
parse_first_line(Buffer, St=#hparser{type=Type,
max_line_length=MaxLength,
max_empty_lines=MaxEmpty}, Empty) ->
Expand Down Expand Up @@ -192,12 +194,17 @@ match_eol(_, _) ->
nomatch.

%% @doc parse status
parse_response_line(#hparser{buffer=Buf}=St) ->
case binary:split(Buf, <<"\r\n">>) of
parse_response_line(#hparser{}=St) ->
parse_response_line([<<"\r\n">>, <<"\n">>], St).

parse_response_line([], _St) ->
{error, bad_request};
parse_response_line([Sep | SepRest], #hparser{buffer=Buf}=St) ->
case binary:split(Buf, Sep) of
[Line, Rest] ->
parse_response_version(Line, St#hparser{buffer=Rest});
_ ->
{error, bad_request}
_Other ->
parse_response_line(SepRest, #hparser{buffer=Buf}=St)
end.


Expand Down Expand Up @@ -251,10 +258,15 @@ parse_uri_path(<< C, Rest/bits >>, St, Method, Acc) ->
_ -> parse_uri_path(Rest, St, Method, << Acc/binary, C >>)
end.

parse_version(<< "HTTP/", High, ".", Low, $\r , $\n, Rest/binary >>, St, Method, URI)
parse_version(<< "HTTP/", High, ".", Low, Rest0/binary >>, St, Method, URI)
when High >= $0, High =< $9, Low >= $0, Low =< $9 ->
Version = { High -$0, Low - $0},

Rest = case Rest0 of
<<"\r\n", Rest1/binary>> ->
Rest1;
<<"\n", Rest1/binary>> ->
Rest1
end,
NState = St#hparser{type=request,
version=Version,
method=Method,
Expand All @@ -270,24 +282,36 @@ parse_headers(#hparser{}=St) ->
parse_header(St).


parse_header(#hparser{buffer=Buf}=St) ->
case binary:split(Buf, <<"\r\n">>) of
parse_header(#hparser{}=St) ->
parse_header_sep([<<"\r\n">>, <<"\n">>], St).

parse_header_sep([], St) ->
{more, St};
parse_header_sep([Sep | SepRest], #hparser{buffer=Buf}=St) ->
case binary:split(Buf, Sep) of
[_, _] ->
parse_header_(Sep, St);
[Buf] ->
parse_header_sep(SepRest, St)
end.

parse_header_(Sep, #hparser{buffer=Buf}=St) ->
case binary:split(Buf, Sep) of
[<<>>, Rest] ->
{headers_complete, St#hparser{buffer=Rest,
state=on_body}};
[Line, << " ", Rest/binary >> ] ->
NewBuf = iolist_to_binary([Line, " ", Rest]),
parse_header(St#hparser{buffer=NewBuf});
parse_header_(Sep, St#hparser{buffer=NewBuf});
[Line, << "\t", Rest/binary >> ] ->
NewBuf = iolist_to_binary([Line, " ", Rest]),
parse_header(St#hparser{buffer=NewBuf});
parse_header_(Sep, St#hparser{buffer=NewBuf});
[Line, Rest]->
parse_header(Line, St#hparser{buffer=Rest});
[Buf] ->
{more, St}
end.


parse_header(Line, St) ->
[Key, Value] = case binary:split(Line, <<":">>, [trim]) of
[K] -> [K, <<>>];
Expand Down Expand Up @@ -468,6 +492,9 @@ read_size(<<>>, _, _) ->
read_size(<<"\r\n", Rest/binary>>, Acc, _) ->
{ok, lists:reverse(Acc), Rest};

read_size(<<"\n", Rest/binary>>, Acc, _) ->
{ok, lists:reverse(Acc), Rest};

read_size(<<$;, Rest/binary>>, Acc, _) ->
read_size(Rest, Acc, false);

Expand Down
47 changes: 47 additions & 0 deletions test/hackney_http_nl_tests.erl
@@ -0,0 +1,47 @@
-module(hackney_http_nl_tests).
-include_lib("eunit/include/eunit.hrl").
-include("hackney_lib.hrl").

parse_response_header_with_continuation_line_test() ->
Response = <<"HTTP/1.1 200\nContent-Type: multipart/related;\n\tboundary=\"--:\"\nOther-Header: test\n\n">>,
ST1 = #hparser{},
{response, _Version, _StatusInt, _Reason, ST2} = hackney_http:execute(ST1, Response),
{header, Header, ST3} = hackney_http:execute(ST2),
?assertEqual({<<"Content-Type">>, <<"multipart/related; boundary=\"--:\"">>}, Header),
{header, Header1, ST4} = hackney_http:execute(ST3),
?assertEqual({<<"Other-Header">>, <<"test">>}, Header1),
{headers_complete, _ST5} = hackney_http:execute(ST4).

parse_request_correct_leading_newlines_test() ->
Request = <<"\nGET / HTTP/1.1\n\n">>,
ST1 = #hparser{},
{request, Verb, Resource, Version, _ST2} = hackney_http:execute(ST1, Request),
?assertEqual(Verb, <<"GET">>),
?assertEqual(Resource, <<"/">>),
?assertEqual(Version, {1,1}).

parse_request_error_too_many_newlines_test() ->
Request = <<"\nGET / HTTP/1.1\n\n">>,
St = #hparser{max_empty_lines = 0},
{error, bad_request} = hackney_http:execute(St, Request).

parse_chunked_response_crlf_test() ->
P0 = hackney_http:parser([response]),
{_, _, _, _, P1} = hackney_http:execute(P0, <<"HTTP/1.1 200 OK\n">>),
{_, _, P2} = hackney_http:execute(P1, <<"Transfer-Encoding: chunked\n">>),
{_, P3} = hackney_http:execute(P2, <<"\n">>),

?assertEqual({done, <<>>}, hackney_http:execute(P3, <<"0\n\n">>)),
?assertEqual({done, <<"a">>}, hackney_http:execute(P3, <<"0\n\na">>)),
{more, P4_1} = hackney_http:execute(P3, <<"0\n">>),
?assertEqual({done, <<>>}, hackney_http:execute(P4_1, <<"\n">>)),
{more, P4_2} = hackney_http:execute(P3, <<"0\n\r">>),
?assertEqual({done, <<>>}, hackney_http:execute(P4_2, <<"\n">>)).

parse_chunked_response_trailers_test() ->
P0 = hackney_http:parser([response]),
{_, _, _, _, P1} = hackney_http:execute(P0, <<"HTTP/1.1 200 OK\n">>),
{_, _, P2} = hackney_http:execute(P1, <<"Transfer-Encoding: chunked\n">>),
{_, P3} = hackney_http:execute(P2, <<"\n">>),
{more, P4} = hackney_http:execute(P3, <<"0\nFoo: ">>),
?assertEqual({done, <<>>}, hackney_http:execute(P4, <<"Bar\n\n">>)).

0 comments on commit 992cc36

Please sign in to comment.