%% Copyright (c) 2013-2023, Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% 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]). %% 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]). -type headers() :: [{binary(), iodata()}]. -export_type([headers/0]). %% 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) -> cow_http1:status_to_integer(Status). -spec parse_headers(binary()) -> {[{binary(), binary()}], binary()}. parse_headers(Data) -> cow_http1:parse_headers(Data). -spec parse_fullpath(binary()) -> {binary(), binary()}. parse_fullpath(Fullpath) -> cow_http1:parse_fullpath(Fullpath). -spec parse_version(binary()) -> cow_http1:version(). parse_version(Data) -> cow_http1:parse_version(Data). -spec request(binary(), iodata(), cow_http1:version(), headers()) -> iodata(). request(Method, Path, Version, Headers) -> cow_http1:request(Method, Path, Version, Headers). -spec response(status() | binary(), cow_http1:version(), headers()) -> iodata(). response(Status, Version, Headers) -> cow_http1:response(Status, Version, Headers). -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. 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; 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. 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>> ], case binary:match(Name, Pattern) of nomatch -> regular_headers(Tail, Type); _ -> {error, uppercase_header_name} end; regular_headers([], _) -> ok. 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. 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. 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. return_headers(Headers, PseudoHeaders, Len) -> {headers, Headers, PseudoHeaders, Len}. return_push_promise(Headers, PseudoHeaders) -> {push_promise, Headers, PseudoHeaders}. return_trailers(Headers) -> {trailers, Headers}. %% Remove HTTP/1-specific headers. -spec remove_http1_headers(headers()) -> headers(). 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).