diff options
Diffstat (limited to 'src/cow_http.erl')
-rw-r--r-- | src/cow_http.erl | 681 |
1 files changed, 313 insertions, 368 deletions
diff --git a/src/cow_http.erl b/src/cow_http.erl index 93e9193..b4bc672 100644 --- a/src/cow_http.erl +++ b/src/cow_http.erl @@ -12,23 +12,35 @@ %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +%% This module contains functions and types common +%% to all or most HTTP versions. -module(cow_http). +%% The HTTP/1 functions have been moved to cow_http1. +%% In order to remain backward compatible we redirect +%% calls to cow_http1. The type version() was moved +%% and no fallback is provided. +%% +%% @todo Remove the aliases in Cowlib 3.0. -export([parse_request_line/1]). -export([parse_status_line/1]). -export([status_to_integer/1]). -export([parse_headers/1]). - -export([parse_fullpath/1]). -export([parse_version/1]). - -export([request/4]). -export([response/3]). -export([headers/1]). -export([version/1]). --type version() :: 'HTTP/1.0' | 'HTTP/1.1'. --export_type([version/0]). +%% Functions used by HTTP/2+. + +-export([format_semantic_error/1]). +-export([merge_pseudo_headers/2]). +-export([process_headers/5]). +-export([remove_http1_headers/1]). + +%% Types used by all versions of HTTP. -type status() :: 100..999. -export_type([status/0]). @@ -36,391 +48,324 @@ -type headers() :: [{binary(), iodata()}]. -export_type([headers/0]). --include("cow_inline.hrl"). - -%% @doc Parse the request line. - --spec parse_request_line(binary()) -> {binary(), binary(), version(), binary()}. -parse_request_line(Data) -> - {Pos, _} = binary:match(Data, <<"\r">>), - <<RequestLine:Pos/binary, "\r\n", Rest/bits>> = Data, - [Method, Target, Version0] = binary:split(RequestLine, <<$\s>>, [trim_all, global]), - Version = case Version0 of - <<"HTTP/1.1">> -> 'HTTP/1.1'; - <<"HTTP/1.0">> -> 'HTTP/1.0' - end, - {Method, Target, Version, Rest}. - --ifdef(TEST). -parse_request_line_test_() -> - Tests = [ - {<<"GET /path HTTP/1.0\r\nRest">>, - {<<"GET">>, <<"/path">>, 'HTTP/1.0', <<"Rest">>}}, - {<<"GET /path HTTP/1.1\r\nRest">>, - {<<"GET">>, <<"/path">>, 'HTTP/1.1', <<"Rest">>}}, - {<<"CONNECT proxy.example.org:1080 HTTP/1.1\r\nRest">>, - {<<"CONNECT">>, <<"proxy.example.org:1080">>, 'HTTP/1.1', <<"Rest">>}} - ], - [{V, fun() -> R = parse_request_line(V) end} - || {V, R} <- Tests]. - -parse_request_line_error_test_() -> - Tests = [ - <<>>, - <<"GET">>, - <<"GET /path\r\n">>, - <<"GET /path HTTP/1.1">>, - <<"GET /path HTTP/1.1\r">>, - <<"GET /path HTTP/1.1\n">>, - <<"GET /path HTTP/0.9\r\n">>, - <<"content-type: text/plain\r\n">>, - <<0:80, "\r\n">> - ], - [{V, fun() -> {'EXIT', _} = (catch parse_request_line(V)) end} - || V <- Tests]. - -horse_parse_request_line_get_path() -> - horse:repeat(200000, - parse_request_line(<<"GET /path HTTP/1.1\r\n">>) - ). --endif. - -%% @doc Parse the status line. - --spec parse_status_line(binary()) -> {version(), status(), binary(), binary()}. -parse_status_line(<< "HTTP/1.1 200 OK\r\n", Rest/bits >>) -> - {'HTTP/1.1', 200, <<"OK">>, Rest}; -parse_status_line(<< "HTTP/1.1 404 Not Found\r\n", Rest/bits >>) -> - {'HTTP/1.1', 404, <<"Not Found">>, Rest}; -parse_status_line(<< "HTTP/1.1 500 Internal Server Error\r\n", Rest/bits >>) -> - {'HTTP/1.1', 500, <<"Internal Server Error">>, Rest}; -parse_status_line(<< "HTTP/1.1 ", Status/bits >>) -> - parse_status_line(Status, 'HTTP/1.1'); -parse_status_line(<< "HTTP/1.0 ", Status/bits >>) -> - parse_status_line(Status, 'HTTP/1.0'). - -parse_status_line(<<H, T, U, " ", Rest/bits>>, Version) -> - Status = status_to_integer(H, T, U), - {Pos, _} = binary:match(Rest, <<"\r">>), - << StatusStr:Pos/binary, "\r\n", Rest2/bits >> = Rest, - {Version, Status, StatusStr, Rest2}. +%% Types used by HTTP/2+. + +-type pseudo_headers() :: #{} %% Trailers + | #{ %% Responses. + status := cow_http:status() + } | #{ %% Normal CONNECT requests. + method := binary(), + authority := binary() + } | #{ %% Extended CONNECT requests. + method := binary(), + scheme := binary(), + authority := binary(), + path := binary(), + protocol := binary() + } | #{ %% Other requests. + method := binary(), + scheme := binary(), + authority => binary(), + path := binary() + }. +-export_type([pseudo_headers/0]). + +-type fin() :: fin | nofin. +-export_type([fin/0]). + +%% HTTP/1 function aliases. + +-spec parse_request_line(binary()) -> {binary(), binary(), cow_http1:version(), binary()}. +parse_request_line(Data) -> cow_http1:parse_request_line(Data). + +-spec parse_status_line(binary()) -> {cow_http1:version(), status(), binary(), binary()}. +parse_status_line(Data) -> cow_http1:parse_status_line(Data). -spec status_to_integer(status() | binary()) -> status(). -status_to_integer(Status) when is_integer(Status) -> - Status; -status_to_integer(Status) -> - case Status of - <<H, T, U>> -> - status_to_integer(H, T, U); - <<H, T, U, " ", _/bits>> -> - status_to_integer(H, T, U) - end. - -status_to_integer(H, T, U) - when $0 =< H, H =< $9, $0 =< T, T =< $9, $0 =< U, U =< $9 -> - (H - $0) * 100 + (T - $0) * 10 + (U - $0). - --ifdef(TEST). -parse_status_line_test_() -> - Tests = [ - {<<"HTTP/1.1 200 OK\r\nRest">>, - {'HTTP/1.1', 200, <<"OK">>, <<"Rest">>}}, - {<<"HTTP/1.0 404 Not Found\r\nRest">>, - {'HTTP/1.0', 404, <<"Not Found">>, <<"Rest">>}}, - {<<"HTTP/1.1 500 Something very funny here\r\nRest">>, - {'HTTP/1.1', 500, <<"Something very funny here">>, <<"Rest">>}}, - {<<"HTTP/1.1 200 \r\nRest">>, - {'HTTP/1.1', 200, <<>>, <<"Rest">>}} - ], - [{V, fun() -> R = parse_status_line(V) end} - || {V, R} <- Tests]. - -parse_status_line_error_test_() -> - Tests = [ - <<>>, - <<"HTTP/1.1">>, - <<"HTTP/1.1 200\r\n">>, - <<"HTTP/1.1 200 OK">>, - <<"HTTP/1.1 200 OK\r">>, - <<"HTTP/1.1 200 OK\n">>, - <<"HTTP/0.9 200 OK\r\n">>, - <<"HTTP/1.1 42 Answer\r\n">>, - <<"HTTP/1.1 999999999 More than OK\r\n">>, - <<"content-type: text/plain\r\n">>, - <<0:80, "\r\n">> - ], - [{V, fun() -> {'EXIT', _} = (catch parse_status_line(V)) end} - || V <- Tests]. - -horse_parse_status_line_200() -> - horse:repeat(200000, - parse_status_line(<<"HTTP/1.1 200 OK\r\n">>) - ). +status_to_integer(Status) -> cow_http1:status_to_integer(Status). -horse_parse_status_line_404() -> - horse:repeat(200000, - parse_status_line(<<"HTTP/1.1 404 Not Found\r\n">>) - ). +-spec parse_headers(binary()) -> {[{binary(), binary()}], binary()}. +parse_headers(Data) -> cow_http1:parse_headers(Data). -horse_parse_status_line_500() -> - horse:repeat(200000, - parse_status_line(<<"HTTP/1.1 500 Internal Server Error\r\n">>) - ). +-spec parse_fullpath(binary()) -> {binary(), binary()}. +parse_fullpath(Fullpath) -> cow_http1:parse_fullpath(Fullpath). -horse_parse_status_line_other() -> - horse:repeat(200000, - parse_status_line(<<"HTTP/1.1 416 Requested range not satisfiable\r\n">>) - ). --endif. +-spec parse_version(binary()) -> cow_http1:version(). +parse_version(Data) -> cow_http1:parse_version(Data). -%% @doc Parse the list of headers. +-spec request(binary(), iodata(), cow_http1:version(), headers()) -> iodata(). +request(Method, Path, Version, Headers) -> cow_http1:request(Method, Path, Version, Headers). --spec parse_headers(binary()) -> {[{binary(), binary()}], binary()}. -parse_headers(Data) -> - parse_header(Data, []). - -parse_header(<< $\r, $\n, Rest/bits >>, Acc) -> - {lists:reverse(Acc), Rest}; -parse_header(Data, Acc) -> - parse_hd_name(Data, Acc, <<>>). - -parse_hd_name(<< C, Rest/bits >>, Acc, SoFar) -> - case C of - $: -> parse_hd_before_value(Rest, Acc, SoFar); - $\s -> parse_hd_name_ws(Rest, Acc, SoFar); - $\t -> parse_hd_name_ws(Rest, Acc, SoFar); - _ -> ?LOWER(parse_hd_name, Rest, Acc, SoFar) - end. +-spec response(status() | binary(), cow_http1:version(), headers()) -> iodata(). +response(Status, Version, Headers) -> cow_http1:response(Status, Version, Headers). -parse_hd_name_ws(<< C, Rest/bits >>, Acc, Name) -> - case C of - $: -> parse_hd_before_value(Rest, Acc, Name); - $\s -> parse_hd_name_ws(Rest, Acc, Name); - $\t -> parse_hd_name_ws(Rest, Acc, Name) +-spec headers(headers()) -> iodata(). +headers(Headers) -> cow_http1:headers(Headers). + +-spec version(cow_http1:version()) -> binary(). +version(Version) -> cow_http1:version(Version). + +%% Functions used by HTTP/2+. + +%% Semantic errors are common to all HTTP versions. + +-spec format_semantic_error(atom()) -> atom(). + +format_semantic_error(connect_invalid_content_length_2xx) -> + 'Content-length header received in a 2xx response to a CONNECT request. (RFC7230 3.3.2).'; +format_semantic_error(invalid_content_length_header) -> + 'The content-length header is invalid. (RFC7230 3.3.2)'; +format_semantic_error(invalid_content_length_header_1xx) -> + 'Content-length header received in a 1xx response. (RFC7230 3.3.2)'; +format_semantic_error(invalid_content_length_header_204) -> + 'Content-length header received in a 204 response. (RFC7230 3.3.2)'; +format_semantic_error(multiple_content_length_headers) -> + 'Multiple content-length headers were received. (RFC7230 3.3.2)'. + +%% Merge pseudo headers at the start of headers. + +-spec merge_pseudo_headers(pseudo_headers(), headers()) -> headers(). + +merge_pseudo_headers(PseudoHeaders, Headers0) -> + lists:foldl(fun + ({status, Status}, Acc) when is_integer(Status) -> + [{<<":status">>, integer_to_binary(Status)}|Acc]; + ({Name, Value}, Acc) -> + [{iolist_to_binary([$:, atom_to_binary(Name, latin1)]), Value}|Acc] + end, Headers0, maps:to_list(PseudoHeaders)). + +%% Process HTTP/2+ headers. This is done after decoding them. + +-spec process_headers(headers(), request | push_promise | response | trailers, + binary() | undefined, fin(), #{enable_connect_protocol => boolean(), any() => any()}) + -> {headers, headers(), pseudo_headers(), non_neg_integer() | undefined} + | {push_promise, headers(), pseudo_headers()} + | {trailers, headers()} + | {error, atom()}. + +process_headers(Headers0, Type, ReqMethod, IsFin, LocalSettings) + when Type =:= request; Type =:= push_promise -> + IsExtendedConnectEnabled = maps:get(enable_connect_protocol, LocalSettings, false), + case request_pseudo_headers(Headers0, #{}) of + %% Extended CONNECT method (HTTP/2: RFC8441, HTTP/3: RFC9220). + {ok, PseudoHeaders=#{method := <<"CONNECT">>, scheme := _, + authority := _, path := _, protocol := _}, Headers} + when IsExtendedConnectEnabled -> + regular_headers(Headers, Type, ReqMethod, IsFin, PseudoHeaders); + {ok, #{method := <<"CONNECT">>, scheme := _, + authority := _, path := _}, _} + when IsExtendedConnectEnabled -> + {error, extended_connect_missing_protocol}; + {ok, #{protocol := _}, _} -> + {error, invalid_protocol_pseudo_header}; + %% Normal CONNECT (no scheme/path). + {ok, PseudoHeaders = #{method := <<"CONNECT">>, authority := _}, Headers} + when map_size(PseudoHeaders) =:= 2 -> + regular_headers(Headers, Type, ReqMethod, IsFin, PseudoHeaders); + {ok, #{method := <<"CONNECT">>, authority := _}, _} -> + {error, connect_invalid_pseudo_header}; + {ok, #{method := <<"CONNECT">>}, _} -> + {error, connect_missing_authority}; + %% Other requests. + {ok, PseudoHeaders = #{method := _, scheme := _, path := _}, Headers} -> + regular_headers(Headers, Type, ReqMethod, IsFin, PseudoHeaders); + {ok, _, _} -> + {error, missing_pseudo_header}; + Error = {error, _} -> + Error + end; +process_headers(Headers0, Type = response, ReqMethod, IsFin, _LocalSettings) -> + case response_pseudo_headers(Headers0, #{}) of + {ok, PseudoHeaders=#{status := _}, Headers} -> + regular_headers(Headers, Type, ReqMethod, IsFin, PseudoHeaders); + {ok, _, _} -> + {error, missing_pseudo_header}; + Error = {error, _} -> + Error + end; +process_headers(Headers, Type = trailers, ReqMethod, IsFin, _LocalSettings) -> + case trailers_have_pseudo_headers(Headers) of + false -> + regular_headers(Headers, Type, ReqMethod, IsFin, #{}); + true -> + {error, trailer_invalid_pseudo_header} end. -parse_hd_before_value(<< $\s, Rest/bits >>, Acc, Name) -> - parse_hd_before_value(Rest, Acc, Name); -parse_hd_before_value(<< $\t, Rest/bits >>, Acc, Name) -> - parse_hd_before_value(Rest, Acc, Name); -parse_hd_before_value(Data, Acc, Name) -> - parse_hd_value(Data, Acc, Name, <<>>). - -parse_hd_value(<< $\r, Rest/bits >>, Acc, Name, SoFar) -> - case Rest of - << $\n, C, Rest2/bits >> when C =:= $\s; C =:= $\t -> - parse_hd_value(Rest2, Acc, Name, << SoFar/binary, C >>); - << $\n, Rest2/bits >> -> - Value = clean_value_ws_end(SoFar, byte_size(SoFar) - 1), - parse_header(Rest2, [{Name, Value}|Acc]) +request_pseudo_headers([{<<":method">>, _}|_], #{method := _}) -> + {error, multiple_method_pseudo_headers}; +request_pseudo_headers([{<<":method">>, Method}|Tail], PseudoHeaders) -> + request_pseudo_headers(Tail, PseudoHeaders#{method => Method}); +request_pseudo_headers([{<<":scheme">>, _}|_], #{scheme := _}) -> + {error, multiple_scheme_pseudo_headers}; +request_pseudo_headers([{<<":scheme">>, Scheme}|Tail], PseudoHeaders) -> + request_pseudo_headers(Tail, PseudoHeaders#{scheme => Scheme}); +request_pseudo_headers([{<<":authority">>, _}|_], #{authority := _}) -> + {error, multiple_authority_pseudo_headers}; +request_pseudo_headers([{<<":authority">>, Authority}|Tail], PseudoHeaders) -> + request_pseudo_headers(Tail, PseudoHeaders#{authority => Authority}); +request_pseudo_headers([{<<":path">>, _}|_], #{path := _}) -> + {error, multiple_path_pseudo_headers}; +request_pseudo_headers([{<<":path">>, Path}|Tail], PseudoHeaders) -> + request_pseudo_headers(Tail, PseudoHeaders#{path => Path}); +request_pseudo_headers([{<<":protocol">>, _}|_], #{protocol := _}) -> + {error, multiple_protocol_pseudo_headers}; +request_pseudo_headers([{<<":protocol">>, Protocol}|Tail], PseudoHeaders) -> + request_pseudo_headers(Tail, PseudoHeaders#{protocol => Protocol}); +request_pseudo_headers([{<<":", _/bits>>, _}|_], _) -> + {error, invalid_pseudo_header}; +request_pseudo_headers(Headers, PseudoHeaders) -> + {ok, PseudoHeaders, Headers}. + +response_pseudo_headers([{<<":status">>, _}|_], #{status := _}) -> + {error, multiple_status_pseudo_headers}; +response_pseudo_headers([{<<":status">>, Status}|Tail], PseudoHeaders) -> + try cow_http:status_to_integer(Status) of + IntStatus -> + response_pseudo_headers(Tail, PseudoHeaders#{status => IntStatus}) + catch _:_ -> + {error, invalid_status_pseudo_header} end; -parse_hd_value(<< C, Rest/bits >>, Acc, Name, SoFar) -> - parse_hd_value(Rest, Acc, Name, << SoFar/binary, C >>). - -%% This function has been copied from cowboy_http. -clean_value_ws_end(_, -1) -> - <<>>; -clean_value_ws_end(Value, N) -> - case binary:at(Value, N) of - $\s -> clean_value_ws_end(Value, N - 1); - $\t -> clean_value_ws_end(Value, N - 1); - _ -> - S = N + 1, - << Value2:S/binary, _/bits >> = Value, - Value2 +response_pseudo_headers([{<<":", _/bits>>, _}|_], _) -> + {error, invalid_pseudo_header}; +response_pseudo_headers(Headers, PseudoHeaders) -> + {ok, PseudoHeaders, Headers}. + +trailers_have_pseudo_headers([]) -> + false; +trailers_have_pseudo_headers([{<<":", _/bits>>, _}|_]) -> + true; +trailers_have_pseudo_headers([_|Tail]) -> + trailers_have_pseudo_headers(Tail). + +%% Rejecting invalid regular headers might be a bit too strong for clients. +regular_headers(Headers, Type, ReqMethod, IsFin, PseudoHeaders) -> + case regular_headers(Headers, Type) of + ok when Type =:= request -> + request_expected_size(Headers, IsFin, PseudoHeaders); + ok when Type =:= push_promise -> + return_push_promise(Headers, PseudoHeaders); + ok when Type =:= response -> + response_expected_size(Headers, ReqMethod, IsFin, PseudoHeaders); + ok when Type =:= trailers -> + return_trailers(Headers); + Error = {error, _} -> + Error end. --ifdef(TEST). -parse_headers_test_() -> - Tests = [ - {<<"\r\nRest">>, - {[], <<"Rest">>}}, - {<<"Server: Erlang/R17 \r\n\r\n">>, - {[{<<"server">>, <<"Erlang/R17">>}], <<>>}}, - {<<"Server: Erlang/R17\r\n" - "Date: Sun, 23 Feb 2014 09:30:39 GMT\r\n" - "Multiline-Header: why hello!\r\n" - " I didn't see you all the way over there!\r\n" - "Content-Length: 12\r\n" - "Content-Type: text/plain\r\n" - "\r\nRest">>, - {[{<<"server">>, <<"Erlang/R17">>}, - {<<"date">>, <<"Sun, 23 Feb 2014 09:30:39 GMT">>}, - {<<"multiline-header">>, - <<"why hello! I didn't see you all the way over there!">>}, - {<<"content-length">>, <<"12">>}, - {<<"content-type">>, <<"text/plain">>}], - <<"Rest">>}} - ], - [{V, fun() -> R = parse_headers(V) end} - || {V, R} <- Tests]. - -parse_headers_error_test_() -> - Tests = [ - <<>>, - <<"\r">>, - <<"Malformed\r\n\r\n">>, - <<"content-type: text/plain\r\nMalformed\r\n\r\n">>, - <<"HTTP/1.1 200 OK\r\n\r\n">>, - <<0:80, "\r\n\r\n">>, - <<"content-type: text/plain\r\ncontent-length: 12\r\n">> +regular_headers([{<<>>, _}|_], _) -> + {error, empty_header_name}; +regular_headers([{<<":", _/bits>>, _}|_], _) -> + {error, pseudo_header_after_regular}; +regular_headers([{<<"connection">>, _}|_], _) -> + {error, invalid_connection_header}; +regular_headers([{<<"keep-alive">>, _}|_], _) -> + {error, invalid_keep_alive_header}; +regular_headers([{<<"proxy-authenticate">>, _}|_], _) -> + {error, invalid_proxy_authenticate_header}; +regular_headers([{<<"proxy-authorization">>, _}|_], _) -> + {error, invalid_proxy_authorization_header}; +regular_headers([{<<"transfer-encoding">>, _}|_], _) -> + {error, invalid_transfer_encoding_header}; +regular_headers([{<<"upgrade">>, _}|_], _) -> + {error, invalid_upgrade_header}; +regular_headers([{<<"te">>, Value}|_], request) when Value =/= <<"trailers">> -> + {error, invalid_te_value}; +regular_headers([{<<"te">>, _}|_], Type) when Type =/= request -> + {error, invalid_te_header}; +regular_headers([{Name, _}|Tail], Type) -> + Pattern = [ + <<$A>>, <<$B>>, <<$C>>, <<$D>>, <<$E>>, <<$F>>, <<$G>>, <<$H>>, <<$I>>, + <<$J>>, <<$K>>, <<$L>>, <<$M>>, <<$N>>, <<$O>>, <<$P>>, <<$Q>>, <<$R>>, + <<$S>>, <<$T>>, <<$U>>, <<$V>>, <<$W>>, <<$X>>, <<$Y>>, <<$Z>> ], - [{V, fun() -> {'EXIT', _} = (catch parse_headers(V)) end} - || V <- Tests]. - -horse_parse_headers() -> - horse:repeat(50000, - parse_headers(<<"Server: Erlang/R17\r\n" - "Date: Sun, 23 Feb 2014 09:30:39 GMT\r\n" - "Multiline-Header: why hello!\r\n" - " I didn't see you all the way over there!\r\n" - "Content-Length: 12\r\n" - "Content-Type: text/plain\r\n" - "\r\nRest">>) - ). --endif. - -%% @doc Extract path and query string from a binary, -%% removing any fragment component. - --spec parse_fullpath(binary()) -> {binary(), binary()}. -parse_fullpath(Fullpath) -> - parse_fullpath(Fullpath, <<>>). - -parse_fullpath(<<>>, Path) -> {Path, <<>>}; -parse_fullpath(<< $#, _/bits >>, Path) -> {Path, <<>>}; -parse_fullpath(<< $?, Qs/bits >>, Path) -> parse_fullpath_query(Qs, Path, <<>>); -parse_fullpath(<< C, Rest/bits >>, SoFar) -> parse_fullpath(Rest, << SoFar/binary, C >>). - -parse_fullpath_query(<<>>, Path, Query) -> {Path, Query}; -parse_fullpath_query(<< $#, _/bits >>, Path, Query) -> {Path, Query}; -parse_fullpath_query(<< C, Rest/bits >>, Path, SoFar) -> - parse_fullpath_query(Rest, Path, << SoFar/binary, C >>). - --ifdef(TEST). -parse_fullpath_test() -> - {<<"*">>, <<>>} = parse_fullpath(<<"*">>), - {<<"/">>, <<>>} = parse_fullpath(<<"/">>), - {<<"/path/to/resource">>, <<>>} = parse_fullpath(<<"/path/to/resource#fragment">>), - {<<"/path/to/resource">>, <<>>} = parse_fullpath(<<"/path/to/resource">>), - {<<"/">>, <<>>} = parse_fullpath(<<"/?">>), - {<<"/">>, <<"q=cowboy">>} = parse_fullpath(<<"/?q=cowboy#fragment">>), - {<<"/">>, <<"q=cowboy">>} = parse_fullpath(<<"/?q=cowboy">>), - {<<"/path/to/resource">>, <<"q=cowboy">>} - = parse_fullpath(<<"/path/to/resource?q=cowboy">>), + case binary:match(Name, Pattern) of + nomatch -> regular_headers(Tail, Type); + _ -> {error, uppercase_header_name} + end; +regular_headers([], _) -> ok. --endif. -%% @doc Convert an HTTP version to atom. - --spec parse_version(binary()) -> version(). -parse_version(<<"HTTP/1.1">>) -> 'HTTP/1.1'; -parse_version(<<"HTTP/1.0">>) -> 'HTTP/1.0'. +request_expected_size(Headers, IsFin, PseudoHeaders) -> + case [CL || {<<"content-length">>, CL} <- Headers] of + [] when IsFin =:= fin -> + return_headers(Headers, PseudoHeaders, 0); + [] -> + return_headers(Headers, PseudoHeaders, undefined); + [<<"0">>] -> + return_headers(Headers, PseudoHeaders, 0); + [_] when IsFin =:= fin -> + {error, non_zero_length_with_fin_flag}; + [BinLen] -> + parse_expected_size(Headers, PseudoHeaders, BinLen); + _ -> + {error, multiple_content_length_headers} + end. --ifdef(TEST). -parse_version_test() -> - 'HTTP/1.1' = parse_version(<<"HTTP/1.1">>), - 'HTTP/1.0' = parse_version(<<"HTTP/1.0">>), - {'EXIT', _} = (catch parse_version(<<"HTTP/1.2">>)), - ok. --endif. +response_expected_size(Headers, ReqMethod, IsFin, PseudoHeaders = #{status := Status}) -> + case [CL || {<<"content-length">>, CL} <- Headers] of + [] when IsFin =:= fin -> + return_headers(Headers, PseudoHeaders, 0); + [] -> + return_headers(Headers, PseudoHeaders, undefined); + [_] when Status >= 100, Status =< 199 -> + {error, invalid_content_length_header_1xx}; + [_] when Status =:= 204 -> + {error, invalid_content_length_header_204}; + [_] when Status >= 200, Status =< 299, ReqMethod =:= <<"CONNECT">> -> + {error, connect_invalid_content_length_2xx}; + %% Responses to HEAD requests, and 304 responses may contain + %% a content-length header that must be ignored. (RFC7230 3.3.2) + [_] when ReqMethod =:= <<"HEAD">> -> + return_headers(Headers, PseudoHeaders, 0); + [_] when Status =:= 304 -> + return_headers(Headers, PseudoHeaders, 0); + [<<"0">>] when IsFin =:= fin -> + return_headers(Headers, PseudoHeaders, 0); + [_] when IsFin =:= fin -> + {error, non_zero_length_with_fin_flag}; + [BinLen] -> + parse_expected_size(Headers, PseudoHeaders, BinLen); + _ -> + {error, multiple_content_length_headers} + end. -%% @doc Return formatted request-line and headers. -%% @todo Add tests when the corresponding reverse functions are added. +parse_expected_size(Headers, PseudoHeaders, BinLen) -> + try cow_http_hd:parse_content_length(BinLen) of + Len -> + return_headers(Headers, PseudoHeaders, Len) + catch _:_ -> + {error, invalid_content_length_header} + end. --spec request(binary(), iodata(), version(), headers()) -> iodata(). -request(Method, Path, Version, Headers) -> - [Method, <<" ">>, Path, <<" ">>, version(Version), <<"\r\n">>, - [[N, <<": ">>, V, <<"\r\n">>] || {N, V} <- Headers], - <<"\r\n">>]. +return_headers(Headers, PseudoHeaders, Len) -> + {headers, Headers, PseudoHeaders, Len}. --spec response(status() | binary(), version(), headers()) -> iodata(). -response(Status, Version, Headers) -> - [version(Version), <<" ">>, status(Status), <<"\r\n">>, - headers(Headers), <<"\r\n">>]. +return_push_promise(Headers, PseudoHeaders) -> + {push_promise, Headers, PseudoHeaders}. --spec headers(headers()) -> iodata(). -headers(Headers) -> - [[N, <<": ">>, V, <<"\r\n">>] || {N, V} <- Headers]. +return_trailers(Headers) -> + {trailers, Headers}. -%% @doc Return the version as a binary. +%% Remove HTTP/1-specific headers. --spec version(version()) -> binary(). -version('HTTP/1.1') -> <<"HTTP/1.1">>; -version('HTTP/1.0') -> <<"HTTP/1.0">>. +-spec remove_http1_headers(headers()) -> headers(). --ifdef(TEST). -version_test() -> - <<"HTTP/1.1">> = version('HTTP/1.1'), - <<"HTTP/1.0">> = version('HTTP/1.0'), - {'EXIT', _} = (catch version('HTTP/1.2')), - ok. --endif. - -%% @doc Return the status code and string as binary. - --spec status(status() | binary()) -> binary(). -status(100) -> <<"100 Continue">>; -status(101) -> <<"101 Switching Protocols">>; -status(102) -> <<"102 Processing">>; -status(103) -> <<"103 Early Hints">>; -status(200) -> <<"200 OK">>; -status(201) -> <<"201 Created">>; -status(202) -> <<"202 Accepted">>; -status(203) -> <<"203 Non-Authoritative Information">>; -status(204) -> <<"204 No Content">>; -status(205) -> <<"205 Reset Content">>; -status(206) -> <<"206 Partial Content">>; -status(207) -> <<"207 Multi-Status">>; -status(208) -> <<"208 Already Reported">>; -status(226) -> <<"226 IM Used">>; -status(300) -> <<"300 Multiple Choices">>; -status(301) -> <<"301 Moved Permanently">>; -status(302) -> <<"302 Found">>; -status(303) -> <<"303 See Other">>; -status(304) -> <<"304 Not Modified">>; -status(305) -> <<"305 Use Proxy">>; -status(306) -> <<"306 Switch Proxy">>; -status(307) -> <<"307 Temporary Redirect">>; -status(308) -> <<"308 Permanent Redirect">>; -status(400) -> <<"400 Bad Request">>; -status(401) -> <<"401 Unauthorized">>; -status(402) -> <<"402 Payment Required">>; -status(403) -> <<"403 Forbidden">>; -status(404) -> <<"404 Not Found">>; -status(405) -> <<"405 Method Not Allowed">>; -status(406) -> <<"406 Not Acceptable">>; -status(407) -> <<"407 Proxy Authentication Required">>; -status(408) -> <<"408 Request Timeout">>; -status(409) -> <<"409 Conflict">>; -status(410) -> <<"410 Gone">>; -status(411) -> <<"411 Length Required">>; -status(412) -> <<"412 Precondition Failed">>; -status(413) -> <<"413 Request Entity Too Large">>; -status(414) -> <<"414 Request-URI Too Long">>; -status(415) -> <<"415 Unsupported Media Type">>; -status(416) -> <<"416 Requested Range Not Satisfiable">>; -status(417) -> <<"417 Expectation Failed">>; -status(418) -> <<"418 I'm a teapot">>; -status(421) -> <<"421 Misdirected Request">>; -status(422) -> <<"422 Unprocessable Entity">>; -status(423) -> <<"423 Locked">>; -status(424) -> <<"424 Failed Dependency">>; -status(425) -> <<"425 Unordered Collection">>; -status(426) -> <<"426 Upgrade Required">>; -status(428) -> <<"428 Precondition Required">>; -status(429) -> <<"429 Too Many Requests">>; -status(431) -> <<"431 Request Header Fields Too Large">>; -status(451) -> <<"451 Unavailable For Legal Reasons">>; -status(500) -> <<"500 Internal Server Error">>; -status(501) -> <<"501 Not Implemented">>; -status(502) -> <<"502 Bad Gateway">>; -status(503) -> <<"503 Service Unavailable">>; -status(504) -> <<"504 Gateway Timeout">>; -status(505) -> <<"505 HTTP Version Not Supported">>; -status(506) -> <<"506 Variant Also Negotiates">>; -status(507) -> <<"507 Insufficient Storage">>; -status(508) -> <<"508 Loop Detected">>; -status(510) -> <<"510 Not Extended">>; -status(511) -> <<"511 Network Authentication Required">>; -status(B) when is_binary(B) -> B. +remove_http1_headers(Headers) -> + RemoveHeaders0 = [ + <<"keep-alive">>, + <<"proxy-connection">>, + <<"transfer-encoding">>, + <<"upgrade">> + ], + RemoveHeaders = case lists:keyfind(<<"connection">>, 1, Headers) of + false -> + RemoveHeaders0; + {_, ConnHd} -> + %% We do not need to worry about any "close" header because + %% that header name is reserved. + Connection = cow_http_hd:parse_connection(ConnHd), + Connection ++ [<<"connection">>|RemoveHeaders0] + end, + lists:filter(fun({Name, _}) -> + not lists:member(Name, RemoveHeaders) + end, Headers). |