diff options
author | Loïc Hoguin <[email protected]> | 2019-01-02 14:10:01 +0100 |
---|---|---|
committer | Loïc Hoguin <[email protected]> | 2019-01-02 14:10:01 +0100 |
commit | ca0db57a30c4978f0291ef7900962ed9b8de005d (patch) | |
tree | 46ba3e7a0fbefac7f13f906d24190a0f86d920f1 | |
parent | 228879bfc4eda8bac1919bab176c859c64ac54c2 (diff) | |
download | gun-ca0db57a30c4978f0291ef7900962ed9b8de005d.tar.gz gun-ca0db57a30c4978f0291ef7900962ed9b8de005d.tar.bz2 gun-ca0db57a30c4978f0291ef7900962ed9b8de005d.zip |
Don't send the default port in the host header for HTTP/1.1
-rw-r--r-- | src/gun_http.erl | 26 | ||||
-rw-r--r-- | test/rfc7230_SUITE.erl | 127 |
2 files changed, 141 insertions, 12 deletions
diff --git a/src/gun_http.erl b/src/gun_http.erl index 5229a6d..29ba758 100644 --- a/src/gun_http.erl +++ b/src/gun_http.erl @@ -332,14 +332,9 @@ keepalive(State) -> headers(State=#http_state{socket=Socket, transport=Transport, version=Version, out=head}, StreamRef, ReplyTo, Method, Host, Port, Path, Headers) -> - Host2 = case Host of - {local, _SocketPath} -> <<>>; - Tuple when is_tuple(Tuple) -> inet:ntoa(Tuple); - _ -> Host - end, Headers2 = lists:keydelete(<<"transfer-encoding">>, 1, Headers), Headers3 = case lists:keymember(<<"host">>, 1, Headers) of - false -> [{<<"host">>, [Host2, $:, integer_to_binary(Port)]}|Headers2]; + false -> [{<<"host">>, host_header(Transport, Host, Port)}|Headers2]; true -> Headers2 end, %% We use Headers2 because this is the smallest list. @@ -356,15 +351,10 @@ headers(State=#http_state{socket=Socket, transport=Transport, version=Version, request(State=#http_state{socket=Socket, transport=Transport, version=Version, out=head}, StreamRef, ReplyTo, Method, Host, Port, Path, Headers, Body) -> - Host2 = case Host of - {local, _SocketPath} -> <<>>; - Tuple when is_tuple(Tuple) -> inet:ntoa(Tuple); - _ -> Host - end, Headers2 = lists:keydelete(<<"content-length">>, 1, lists:keydelete(<<"transfer-encoding">>, 1, Headers)), Headers3 = case lists:keymember(<<"host">>, 1, Headers) of - false -> [{<<"host">>, [Host2, $:, integer_to_binary(Port)]}|Headers2]; + false -> [{<<"host">>, host_header(Transport, Host, Port)}|Headers2]; true -> Headers2 end, Headers4 = transform_header_names(State, Headers3), @@ -377,6 +367,18 @@ request(State=#http_state{socket=Socket, transport=Transport, version=Version, Body]), new_stream(State#http_state{connection=Conn}, StreamRef, ReplyTo, Method). +host_header(Transport, Host0, Port) -> + Host = case Host0 of + {local, _SocketPath} -> <<>>; + Tuple when is_tuple(Tuple) -> inet:ntoa(Tuple); + _ -> Host0 + end, + case {Transport:name(), Port} of + {tcp, 80} -> Host; + {tls, 443} -> Host; + _ -> [Host, $:, integer_to_binary(Port)] + end. + transform_header_names(#http_state{transform_header_name = Fun}, Headers) -> lists:keymap(Fun, 1, Headers). diff --git a/test/rfc7230_SUITE.erl b/test/rfc7230_SUITE.erl new file mode 100644 index 0000000..517e2d5 --- /dev/null +++ b/test/rfc7230_SUITE.erl @@ -0,0 +1,127 @@ +%% Copyright (c) 2019, Loïc Hoguin <[email protected]> +%% +%% 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. + +-module(rfc7230_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-ifdef(OTP_RELEASE). +-compile({nowarn_deprecated_function, [{ssl, ssl_accept, 2}]}). +-endif. + +-import(ct_helper, [doc/1]). + +all() -> + ct_helper:all(?MODULE). + +%% Server helpers. (Taken from rfc7231_SUITE.) + +do_origin_start(Transport) -> + do_origin_start(Transport, http). + +do_origin_start(Transport, Protocol) -> + Self = self(), + Pid = spawn_link(fun() -> + case Transport of + tcp -> + do_origin_init_tcp(Self); + tls when Protocol =:= http -> + do_origin_init_tls(Self); + tls when Protocol =:= http2 -> + do_origin_init_tls_h2(Self) + end + end), + Port = do_receive(Pid), + {ok, Pid, Port}. + +do_origin_init_tcp(Parent) -> + {ok, ListenSocket} = gen_tcp:listen(0, [binary, {active, false}]), + {ok, {_, Port}} = inet:sockname(ListenSocket), + Parent ! {self(), Port}, + {ok, ClientSocket} = gen_tcp:accept(ListenSocket, 5000), + do_origin_loop(Parent, ClientSocket, gen_tcp). + +do_origin_init_tls(Parent) -> + Opts = ct_helper:get_certs_from_ets(), + {ok, ListenSocket} = ssl:listen(0, [binary, {active, false}|Opts]), + {ok, {_, Port}} = ssl:sockname(ListenSocket), + Parent ! {self(), Port}, + {ok, ClientSocket} = ssl:transport_accept(ListenSocket, 5000), + ok = ssl:ssl_accept(ClientSocket, 5000), + do_origin_loop(Parent, ClientSocket, ssl). + +do_origin_init_tls_h2(Parent) -> + Opts = ct_helper:get_certs_from_ets(), + {ok, ListenSocket} = ssl:listen(0, [binary, {active, false}, + {alpn_preferred_protocols, [<<"h2">>]}|Opts]), + {ok, {_, Port}} = ssl:sockname(ListenSocket), + Parent ! {self(), Port}, + {ok, ClientSocket} = ssl:transport_accept(ListenSocket, 5000), + ok = ssl:ssl_accept(ClientSocket, 5000), + {ok, <<"h2">>} = ssl:negotiated_protocol(ClientSocket), + do_origin_loop(Parent, ClientSocket, ssl). + +do_origin_loop(Parent, ClientSocket, ClientTransport) -> + case ClientTransport:recv(ClientSocket, 0, 1000) of + {ok, Data} -> + Parent ! {self(), Data}, + do_origin_loop(Parent, ClientSocket, ClientTransport); + {error, closed} -> + ok + end. + +do_receive(Pid) -> + do_receive(Pid, 1000). + +do_receive(Pid, Timeout) -> + receive + {Pid, Msg} -> + Msg + after Timeout -> + error(timeout) + end. + +%% Tests. + +host_default_port_http(_) -> + doc("The default port for http should not be sent in the host header. (RFC7230 2.7.1)"), + do_host_port(tcp, 80, <<>>). + +host_default_port_https(_) -> + doc("The default port for https should not be sent in the host header. (RFC7230 2.7.2)"), + do_host_port(tls, 443, <<>>). + +host_other_port_http(_) -> + doc("Non-default ports for http must be sent in the host header. (RFC7230 2.7.1)"), + do_host_port(tcp, 443, <<":443">>). + +host_other_port_https(_) -> + doc("Non-default ports for https must be sent in the host header. (RFC7230 2.7.2)"), + do_host_port(tls, 80, <<":80">>). + +do_host_port(Transport, DefaultPort, HostHeaderPort) -> + {ok, OriginPid, OriginPort} = do_origin_start(Transport, http), + {ok, ConnPid} = gun:open("localhost", OriginPort, #{transport => Transport}), + {ok, http} = gun:await_up(ConnPid), + %% Change the port in the state to trigger the default port behavior. + _ = sys:replace_state(ConnPid, fun({StateName, StateData}) -> + {StateName, setelement(7, StateData, DefaultPort)} + end, 5000), + %% Confirm the default port is not sent in the request. + _ = gun:get(ConnPid, "/"), + Data = do_receive(OriginPid), + Lines = binary:split(Data, <<"\r\n">>, [global]), + [<<"host: localhost", Rest/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines], + HostHeaderPort = Rest, + gun:close(ConnPid). |