aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/gun_http.erl26
-rw-r--r--test/rfc7230_SUITE.erl127
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).