diff --git a/src/hackney_http.erl b/src/hackney_http.erl index 1f6fc1d8..63e1b588 100644 --- a/src/hackney_http.erl +++ b/src/hackney_http.erl @@ -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) -> @@ -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. @@ -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, @@ -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, <<>>]; @@ -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); diff --git a/test/hackney_http_nl_tests.erl b/test/hackney_http_nl_tests.erl new file mode 100644 index 00000000..4b14979b --- /dev/null +++ b/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">>)).