diff --git a/CHANGELOG.md b/CHANGELOG.md index ac0d8fe..4a45413 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `nhttp_headers:get_exact/2,3`, a case-sensitive `get/2,3` for lists + with lowercase names. + ### Fixed - `nhttp_h2:recv/2` answers a connection preface out of place with a diff --git a/src/nhttp_h1.erl b/src/nhttp_h1.erl index 7f05d09..4283711 100644 --- a/src/nhttp_h1.erl +++ b/src/nhttp_h1.erl @@ -1078,10 +1078,7 @@ derive_authority(<<"http://", Rest/binary>>, Headers) -> derive_authority(<<"https://", Rest/binary>>, Headers) -> extract_authority_from_uri(Rest, Headers); derive_authority(_Path, Headers) -> - case nhttp_headers:get(<<"host">>, Headers) of - undefined -> <<>>; - Host -> Host - end. + nhttp_headers:get_exact(<<"host">>, Headers, <<>>). -spec detect_body_mode(nhttp_lib:headers()) -> body_mode() @@ -1316,10 +1313,7 @@ extract_authority_from_uri(Rest, Headers) -> nomatch -> Rest; {0, _} -> - case nhttp_headers:get(<<"host">>, Headers) of - undefined -> <<>>; - Host -> Host - end; + nhttp_headers:get_exact(<<"host">>, Headers, <<>>); {Pos, _} -> binary:part(Rest, 0, Pos) end. diff --git a/src/nhttp_headers.erl b/src/nhttp_headers.erl index 8641661..d76a1c1 100644 --- a/src/nhttp_headers.erl +++ b/src/nhttp_headers.erl @@ -13,7 +13,8 @@ Key invariants: - Header names compare case-insensitively (RFC 9110 ยง5.1). A lookup matches a stored name in any case, so `get/2` with - `<<"content-length">>` finds a stored `Content-Length`. + `<<"content-length">>` finds a stored `Content-Length`, except through + `get_exact/2,3`. - Multi-valued headers keep insertion order. `get/2,3` returns the first match. `delete/2` removes every occurrence. `append/3` appends and keeps existing entries. @@ -42,6 +43,8 @@ lowercasing of `lower_field_name/1`. filter/2, get/2, get/3, + get_exact/2, + get_exact/3, has/2, has_forbidden_value_octet/1, is_tchar/1, @@ -185,6 +188,19 @@ get(Name, Headers, Default) -> Lower = to_lower(Name), do_get(Lower, Headers, Default). +-doc "Like `get/2`, but case-sensitive. For lists with lowercase names only.". +-spec get_exact(binary(), nhttp_lib:headers()) -> binary() | undefined. +get_exact(Name, Headers) -> + get_exact(Name, Headers, undefined). + +-doc "Like `get/3`, but case-sensitive. For lists with lowercase names only.". +-spec get_exact(binary(), nhttp_lib:headers(), Default) -> binary() | Default. +get_exact(Name, Headers, Default) -> + case lists:keyfind(Name, 1, Headers) of + {_, Value} -> Value; + false -> Default + end. + -doc "True iff a header with the given name exists. Case-insensitive.". -spec has(binary(), nhttp_lib:headers()) -> boolean(). has(Name, Headers) -> diff --git a/test/nhttp_h1_SUITE.erl b/test/nhttp_h1_SUITE.erl index 50f03b8..96e1fa1 100644 --- a/test/nhttp_h1_SUITE.erl +++ b/test/nhttp_h1_SUITE.erl @@ -46,6 +46,7 @@ groups() -> parse_populates_default_scheme, parse_populates_scheme_from_opts, parse_populates_authority_from_host, + parse_populates_authority_from_host_in_any_case, parse_populates_authority_from_absolute_uri, parse_populates_authority_https_absolute_uri, parse_populates_authority_absolute_uri_no_path, @@ -315,6 +316,13 @@ parse_populates_authority_from_host(_Config) -> {ok, Req, _} = nhttp_h1:parse_request(Data), ?assertEqual(<<"example.com:8080">>, maps:get(authority, Req)). +parse_populates_authority_from_host_in_any_case(_Config) -> + Data = <<"GET /path HTTP/1.1\r\nHOST: Example.com\r\n\r\n">>, + {ok, Req, _} = nhttp_h1:parse_request(Data), + ?assertEqual(<<"Example.com">>, maps:get(authority, Req)), + {ok, Req2, _, _} = nhttp_h1:parse_request_headers(Data), + ?assertEqual(<<"Example.com">>, maps:get(authority, Req2)). + parse_populates_authority_from_absolute_uri(_Config) -> Data = <<"GET http://origin.example.com/foo HTTP/1.1\r\nHost: proxy.example.com\r\n\r\n">>, {ok, Req, _} = nhttp_h1:parse_request(Data), diff --git a/test/nhttp_headers_SUITE.erl b/test/nhttp_headers_SUITE.erl index 5298eae..efd7635 100644 --- a/test/nhttp_headers_SUITE.erl +++ b/test/nhttp_headers_SUITE.erl @@ -15,6 +15,7 @@ all() -> [ {group, get}, + {group, get_exact}, {group, has}, {group, mutation}, {group, filter}, @@ -36,6 +37,12 @@ groups() -> get_returns_first_when_duplicate, get_handles_empty_headers ]}, + {get_exact, [parallel], [ + get_exact_first_match, + get_exact_default_when_absent, + get_exact_returns_first_when_duplicate, + get_exact_misses_a_name_in_another_case + ]}, {has, [parallel], [ has_present, has_absent, @@ -158,6 +165,37 @@ get_handles_empty_headers(_Config) -> ?assertEqual(undefined, nhttp_headers:get(<<"x">>, [])), ?assertEqual(default, nhttp_headers:get(<<"x">>, [], default)). +%%%----------------------------------------------------------------------------- +%%% GET EXACT +%%%----------------------------------------------------------------------------- + +get_exact_first_match(_Config) -> + Headers = [ + {<<"content-type">>, <<"text/html">>}, + {<<"host">>, <<"localhost">>} + ], + ?assertEqual(<<"text/html">>, nhttp_headers:get_exact(<<"content-type">>, Headers)), + ?assertEqual(<<"localhost">>, nhttp_headers:get_exact(<<"host">>, Headers, default)). + +get_exact_default_when_absent(_Config) -> + ?assertEqual(undefined, nhttp_headers:get_exact(<<"x">>, [])), + ?assertEqual(undefined, nhttp_headers:get_exact(<<"missing">>, [{<<"x">>, <<"1">>}])), + ?assertEqual(default, nhttp_headers:get_exact(<<"x">>, [], default)), + ?assertEqual(custom, nhttp_headers:get_exact(<<"missing">>, [{<<"x">>, <<"1">>}], custom)). + +get_exact_returns_first_when_duplicate(_Config) -> + Headers = [ + {<<"set-cookie">>, <<"a=1">>}, + {<<"set-cookie">>, <<"b=2">>} + ], + ?assertEqual(<<"a=1">>, nhttp_headers:get_exact(<<"set-cookie">>, Headers)). + +get_exact_misses_a_name_in_another_case(_Config) -> + Headers = [{<<"Host">>, <<"a">>}, {<<"content-length">>, <<"5">>}], + ?assertEqual(undefined, nhttp_headers:get_exact(<<"host">>, Headers)), + ?assertEqual(undefined, nhttp_headers:get_exact(<<"Content-Length">>, Headers)), + ?assertEqual(<<"a">>, nhttp_headers:get(<<"host">>, Headers)). + %%%----------------------------------------------------------------------------- %%% HAS %%%----------------------------------------------------------------------------- diff --git a/test/nhttp_props_SUITE.erl b/test/nhttp_props_SUITE.erl index cc77647..d5a6567 100644 --- a/test/nhttp_props_SUITE.erl +++ b/test/nhttp_props_SUITE.erl @@ -199,7 +199,8 @@ groups() -> headers_field_name_accepts_only_the_rfc_octets, headers_field_value_agrees_with_the_octet_set, headers_lower_field_name_matches_byte_wise_lowercase, - headers_lower_field_name_does_not_copy_a_lowercase_name + headers_lower_field_name_does_not_copy_a_lowercase_name, + headers_get_exact_agrees_with_get_on_lowercase_names ]} ]. @@ -685,3 +686,8 @@ headers_lower_field_name_does_not_copy_a_lowercase_name(Config) -> run_property( nhttp_headers_props, prop_lower_field_name_does_not_copy_a_lowercase_name, Config ). + +headers_get_exact_agrees_with_get_on_lowercase_names(Config) -> + run_property( + nhttp_headers_props, prop_get_exact_agrees_with_get_on_lowercase_names, Config + ). diff --git a/test/property_test/nhttp_headers_props.erl b/test/property_test/nhttp_headers_props.erl index 0bbb0c1..c994cad 100644 --- a/test/property_test/nhttp_headers_props.erl +++ b/test/property_test/nhttp_headers_props.erl @@ -70,6 +70,16 @@ prop_lower_field_name_does_not_copy_a_lowercase_name() -> erts_debug:same(Name, nhttp_headers:lower_field_name(Name)) ). +-spec prop_get_exact_agrees_with_get_on_lowercase_names() -> triq:property(). +prop_get_exact_agrees_with_get_on_lowercase_names() -> + ?FORALL( + {Name, Headers, Default}, + ?LET(Name, lowercase_name_gen(), {Name, lowercase_headers_gen(Name), default_gen()}), + nhttp_headers:get_exact(Name, Headers, Default) =:= + nhttp_headers:get(Name, Headers, Default) andalso + nhttp_headers:get_exact(Name, Headers) =:= nhttp_headers:get(Name, Headers) + ). + %%%----------------------------------------------------------------------------- %%% GENERATORS %%%----------------------------------------------------------------------------- @@ -166,6 +176,14 @@ lowercase_name_gen() -> byte_wise_lower(Name) ). +-spec lowercase_headers_gen(binary()) -> triq_dom:domain(). +lowercase_headers_gen(Name) -> + list({oneof([Name, lowercase_name_gen()]), field_value_gen()}). + +-spec default_gen() -> triq_dom:domain(). +default_gen() -> + elements([undefined, missing, <<>>, <<"fallback">>]). + %%%----------------------------------------------------------------------------- %%% ORACLES %%%-----------------------------------------------------------------------------