aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2019-07-26 12:35:38 +0200
committerLoïc Hoguin <[email protected]>2019-07-26 12:36:55 +0200
commite4df3bb7c726571640c2799bc7a6fbb687b3bdae (patch)
tree1f044ea876852c876846a9d518c97e35cb535e53
parentc2ba2258a0020d82faa3e79162f05fc67d61b53e (diff)
downloadgun-e4df3bb7c726571640c2799bc7a6fbb687b3bdae.tar.gz
gun-e4df3bb7c726571640c2799bc7a6fbb687b3bdae.tar.bz2
gun-e4df3bb7c726571640c2799bc7a6fbb687b3bdae.zip
Add tls_handshake events for CONNECT through TLS proxies
-rw-r--r--src/gun.erl24
-rw-r--r--src/gun_event.erl5
-rw-r--r--src/gun_http.erl12
-rw-r--r--src/gun_tls_proxy.erl42
-rw-r--r--test/event_SUITE.erl90
5 files changed, 146 insertions, 27 deletions
diff --git a/src/gun.erl b/src/gun.erl
index 9add685..ada6272 100644
--- a/src/gun.erl
+++ b/src/gun.erl
@@ -968,11 +968,25 @@ connected(cast, {connect, ReplyTo, StreamRef, Destination0, Headers},
ProtoState2 = Protocol:connect(ProtoState, StreamRef, ReplyTo, Destination, Headers),
{keep_state, State#state{protocol_state=ProtoState2}};
%% When using gun_tls_proxy we need a separate message to know whether
-%% we need to switch to a different protocol.
-connected(info, {connect_protocol, Protocol}, #state{protocol=Protocol}) ->
- keep_state_and_data;
-connected(info, {connect_protocol, Protocol}, State=#state{protocol_state=ProtoState}) ->
- commands([{switch_protocol, Protocol, ProtoState}], State);
+%% the handshake succeeded and whether we need to switch to a different protocol.
+connected(info, {gun_tls_proxy, Socket, {ok, NewProtocol}, HandshakeEvent},
+ State0=#state{socket=Socket, protocol=CurrentProtocol, protocol_state=ProtoState,
+ event_handler=EvHandler, event_handler_state=EvHandlerState0}) ->
+ EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{
+ socket => Socket,
+ protocol => NewProtocol:name()
+ }, EvHandlerState0),
+ State = State0#state{event_handler_state=EvHandlerState},
+ case NewProtocol of
+ CurrentProtocol -> {keep_state, State};
+ _ -> commands([{switch_protocol, NewProtocol, ProtoState}], State)
+ end;
+connected(info, {gun_tls_proxy, Socket, Error = {error, Reason}, HandshakeEvent},
+ State=#state{socket=Socket, event_handler=EvHandler, event_handler_state=EvHandlerState0}) ->
+ EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{
+ error => Reason
+ }, EvHandlerState0),
+ commands([Error], State#state{event_handler_state=EvHandlerState});
connected(cast, {cancel, ReplyTo, StreamRef}, State=#state{
protocol=Protocol, protocol_state=ProtoState,
event_handler=EvHandler, event_handler_state=EvHandlerState0}) ->
diff --git a/src/gun_event.erl b/src/gun_event.erl
index ed50fd7..8a40518 100644
--- a/src/gun_event.erl
+++ b/src/gun_event.erl
@@ -60,11 +60,6 @@
%% upgrading the connection to use TLS, for example using CONNECT.
%% The stream_ref/reply_to values are only present when the TLS
%% handshake occurs as a result of a request.
-%%
-%% @todo The current implementation of TLS over TLS will not result
-%% in an event being triggered when the TLS handshake fails. Instead
-%% the Gun process will exit because of the link to the gun_tls_proxy
-%% process.
-type tls_handshake_event() :: #{
stream_ref => reference(),
diff --git a/src/gun_http.erl b/src/gun_http.erl
index 113de0d..68f9e7d 100644
--- a/src/gun_http.erl
+++ b/src/gun_http.erl
@@ -289,14 +289,22 @@ handle_head(Data, State=#http_state{socket=Socket, transport=Transport,
#{transport := tls} when Transport =:= gun_tls ->
TLSOpts = maps:get(tls_opts, Destination, []),
TLSTimeout = maps:get(tls_handshake_timeout, Destination, infinity),
+ HandshakeEvent = #{
+ stream_ref => RealStreamRef,
+ reply_to => ReplyTo,
+ socket => Socket,
+ tls_opts => TLSOpts,
+ timeout => TLSTimeout
+ },
+ EvHandlerState = EvHandler:tls_handshake_start(HandshakeEvent, EvHandlerState1),
{ok, ProxyPid} = gun_tls_proxy:start_link(NewHost, NewPort,
- TLSOpts, TLSTimeout, Socket, gun_tls),
+ TLSOpts, TLSTimeout, Socket, gun_tls, HandshakeEvent),
%% In this case the switch_protocol is delayed and is handled by
%% a message sent from gun_tls_proxy once the connection is established,
%% and handled by the gun module directly.
{[{state, State2#http_state{socket=ProxyPid, transport=gun_tls_proxy}},
{origin, <<"https">>, NewHost, NewPort, connect},
- {switch_transport, gun_tls_proxy, ProxyPid}], EvHandlerState1};
+ {switch_transport, gun_tls_proxy, ProxyPid}], EvHandlerState};
#{transport := tls} ->
TLSOpts = maps:get(tls_opts, Destination, []),
TLSTimeout = maps:get(tls_handshake_timeout, Destination, infinity),
diff --git a/src/gun_tls_proxy.erl b/src/gun_tls_proxy.erl
index c2930f4..d0558b2 100644
--- a/src/gun_tls_proxy.erl
+++ b/src/gun_tls_proxy.erl
@@ -46,7 +46,7 @@
-endif.
%% Gun-specific interface.
--export([start_link/6]).
+-export([start_link/7]).
%% gun_tls_proxy_cb interface.
-export([cb_controlling_process/2]).
@@ -89,7 +89,10 @@
%% The socket or proxy process we are sending to.
out_socket :: any(),
out_transport :: module(),
- out_messages :: {atom(), atom(), atom()} %% @todo Missing passive.
+ out_messages :: {atom(), atom(), atom()}, %% @todo Missing passive.
+
+ %% Extra information to be sent to the owner when the handshake completes.
+ extra :: any()
}).
-ifdef(DEBUG_PROXY).
@@ -102,12 +105,11 @@
%% Gun-specific interface.
-start_link(Host, Port, Opts, Timeout, OutSocket, OutTransport) ->
+start_link(Host, Port, Opts, Timeout, OutSocket, OutTransport, Extra) ->
?DEBUG_LOG("host ~0p port ~0p opts ~0p timeout ~0p out_socket ~0p out_transport ~0p",
[Host, Port, Opts, Timeout, OutSocket, OutTransport]),
-
case gen_statem:start_link(?MODULE,
- {self(), Host, Port, Opts, Timeout, OutSocket, OutTransport},
+ {self(), Host, Port, Opts, Timeout, OutSocket, OutTransport, Extra},
[]) of
{ok, Pid} when is_port(OutSocket) ->
ok = gen_tcp:controlling_process(OutSocket, Pid),
@@ -176,13 +178,19 @@ sockname(Pid) ->
-spec close(pid()) -> ok.
close(Pid) ->
?DEBUG_LOG("pid ~0p", [Pid]),
- gen_statem:call(Pid, ?FUNCTION_NAME).
+ try
+ gen_statem:call(Pid, ?FUNCTION_NAME)
+ catch
+ %% May happen for example when the handshake fails.
+ exit:{noproc, _} ->
+ ok
+ end.
%% gen_statem.
callback_mode() -> state_functions.
-init({OwnerPid, Host, Port, Opts, Timeout, OutSocket, OutTransport}) ->
+init({OwnerPid, Host, Port, Opts, Timeout, OutSocket, OutTransport, Extra}) ->
if
is_pid(OutSocket) ->
gen_statem:cast(OutSocket, {set_owner, self()});
@@ -199,7 +207,8 @@ init({OwnerPid, Host, Port, Opts, Timeout, OutSocket, OutTransport}) ->
" out_socket ~0p out_transport ~0p proxy_pid ~0p",
[OwnerPid, Host, Port, Opts, Timeout, OutSocket, OutTransport, ProxyPid]),
{ok, not_connected, #state{owner_pid=OwnerPid, host=Host, port=Port, proxy_pid=ProxyPid,
- out_socket=OutSocket, out_transport=OutTransport, out_messages=Messages}}.
+ out_socket=OutSocket, out_transport=OutTransport, out_messages=Messages,
+ extra=Extra}}.
connect_proc(ProxyPid, Host, Port, Opts, Timeout) ->
?DEBUG_LOG("proxy_pid ~0p host ~0p port ~0p opts ~0p timeout ~0p",
@@ -226,20 +235,23 @@ not_connected({call, _}, Msg={send, _}, State) ->
not_connected(cast, Msg={setopts, _}, State) ->
?DEBUG_LOG("postpone ~0p state ~0p", [Msg, State]),
{keep_state_and_data, postpone};
-not_connected(cast, Msg={connect_proc, {ok, Socket}}, State=#state{owner_pid=OwnerPid}) ->
+not_connected(cast, Msg={connect_proc, {ok, Socket}}, State=#state{owner_pid=OwnerPid, extra=Extra}) ->
?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]),
Protocol = case ssl:negotiated_protocol(Socket) of
{ok, <<"h2">>} -> gun_http2;
_ -> gun_http
end,
- OwnerPid ! {connect_protocol, Protocol},
+ OwnerPid ! {?MODULE, self(), {ok, Protocol}, Extra},
%% We need to spawn this call before OTP-21.2 because it triggers
%% a cb_setopts call that blocks us. Might be OK to just leave it
%% like this once we support 21.2+ only.
spawn(fun() -> ok = ssl:setopts(Socket, [{active, true}]) end),
{next_state, connected, State#state{proxy_socket=Socket}};
-not_connected(cast, Msg={connect_proc, Error}, State) ->
+not_connected(cast, Msg={connect_proc, Error}, State=#state{owner_pid=OwnerPid, extra=Extra}) ->
?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]),
+ OwnerPid ! {?MODULE, self(), Error, Extra},
+ %% We unlink from the owner process to avoid taking it down with us.
+ unlink(OwnerPid),
{stop, Error, State};
not_connected(Type, Event, State) ->
handle_common(Type, Event, State).
@@ -387,7 +399,7 @@ proxy_active(State=#state{proxy_pid=ProxyPid, proxy_active=Active0, proxy_buffer
tcp_test() ->
ssl:start(),
{ok, Socket} = gen_tcp:connect("google.com", 443, [binary, {active, false}]),
- {ok, ProxyPid1} = start_link("google.com", 443, [], 5000, Socket, gen_tcp),
+ {ok, ProxyPid1} = start_link("google.com", 443, [], 5000, Socket, gen_tcp, #{}),
send(ProxyPid1, <<"GET / HTTP/1.1\r\nHost: google.com\r\n\r\n">>),
receive {tls_proxy, ProxyPid1, <<"HTTP/1.1 ", _/bits>>} -> ok after 1000 -> error(timeout) end.
@@ -396,7 +408,7 @@ ssl_test() ->
_ = (catch ct_helper:make_certs_in_ets()),
{ok, _, Port} = do_proxy_start("google.com", 443),
{ok, Socket} = ssl:connect("localhost", Port, [binary, {active, false}]),
- {ok, ProxyPid1} = start_link("google.com", 443, [], 5000, Socket, ssl),
+ {ok, ProxyPid1} = start_link("google.com", 443, [], 5000, Socket, ssl, #{}),
send(ProxyPid1, <<"GET / HTTP/1.1\r\nHost: google.com\r\n\r\n">>),
receive {tls_proxy, ProxyPid1, <<"HTTP/1.1 ", _/bits>>} -> ok after 1000 -> error(timeout) end.
@@ -406,8 +418,8 @@ ssl2_test() ->
{ok, _, Port1} = do_proxy_start("google.com", 443),
{ok, _, Port2} = do_proxy_start("localhost", Port1),
{ok, Socket} = ssl:connect("localhost", Port2, [binary, {active, false}]),
- {ok, ProxyPid1} = start_link("localhost", Port1, [], 5000, Socket, ssl),
- {ok, ProxyPid2} = start_link("google.com", 443, [], 5000, ProxyPid1, ?MODULE),
+ {ok, ProxyPid1} = start_link("localhost", Port1, [], 5000, Socket, ssl, #{}),
+ {ok, ProxyPid2} = start_link("google.com", 443, [], 5000, ProxyPid1, ?MODULE, #{}),
send(ProxyPid2, <<"GET / HTTP/1.1\r\nHost: google.com\r\n\r\n">>),
receive {tls_proxy, ProxyPid2, <<"HTTP/1.1 ", _/bits>>} -> ok after 1000 -> error(timeout) end.
diff --git a/test/event_SUITE.erl b/test/event_SUITE.erl
index fb45bc9..b1bfbcb 100644
--- a/test/event_SUITE.erl
+++ b/test/event_SUITE.erl
@@ -320,6 +320,96 @@ http1_tls_handshake_end_ok_connect(Config) ->
true = is_tuple(Socket),
gun:close(ConnPid).
+http1_tls_handshake_start_connect_over_https_proxy(Config) ->
+ doc("Confirm that the tls_handshake_start event callback is called "
+ "when using CONNECT to a TLS server via a TLS proxy."),
+ OriginPort = config(tls_origin_port, Config),
+ {ok, _, ProxyPort} = rfc7231_SUITE:do_proxy_start(tls),
+ {ok, ConnPid} = gun:open("localhost", ProxyPort, #{
+ event_handler => {?MODULE, self()},
+ protocols => [config(name, config(tc_group_properties, Config))],
+ transport => tls
+ }),
+ {ok, http} = gun:await_up(ConnPid),
+ %% We skip the TLS handshake event to the TLS proxy.
+ _ = do_receive_event(tls_handshake_start),
+ StreamRef = gun:connect(ConnPid, #{
+ host => "localhost",
+ port => OriginPort,
+ transport => tls
+ }),
+ ReplyTo = self(),
+ #{
+ stream_ref := StreamRef,
+ reply_to := ReplyTo,
+ socket := Socket,
+ tls_opts := _,
+ timeout := _
+ } = do_receive_event(tls_handshake_start),
+ true = is_tuple(Socket),
+ gun:close(ConnPid).
+
+http1_tls_handshake_end_error_connect_over_https_proxy(Config) ->
+ doc("Confirm that the tls_handshake_end event callback is called on TLS handshake error "
+ "when using CONNECT to a TLS server via a TLS proxy."),
+ %% We use the wrong port on purpose to trigger a handshake error.
+ OriginPort = config(tcp_origin_port, Config),
+ {ok, _, ProxyPort} = rfc7231_SUITE:do_proxy_start(tls),
+ {ok, ConnPid} = gun:open("localhost", ProxyPort, #{
+ event_handler => {?MODULE, self()},
+ protocols => [config(name, config(tc_group_properties, Config))],
+ transport => tls
+ }),
+ {ok, http} = gun:await_up(ConnPid),
+ %% We skip the TLS handshake event to the TLS proxy.
+ _ = do_receive_event(tls_handshake_end),
+ StreamRef = gun:connect(ConnPid, #{
+ host => "localhost",
+ port => OriginPort,
+ transport => tls
+ }),
+ ReplyTo = self(),
+ #{
+ stream_ref := StreamRef,
+ reply_to := ReplyTo,
+ socket := Socket,
+ tls_opts := _,
+ timeout := _,
+ error := {tls_alert, _}
+ } = do_receive_event(tls_handshake_end),
+ true = is_tuple(Socket),
+ gun:close(ConnPid).
+
+http1_tls_handshake_end_ok_connect_over_https_proxy(Config) ->
+ doc("Confirm that the tls_handshake_end event callback is called on TLS handshake success "
+ "when using CONNECT to a TLS server via a TLS proxy."),
+ OriginPort = config(tls_origin_port, Config),
+ {ok, _, ProxyPort} = rfc7231_SUITE:do_proxy_start(tls),
+ {ok, ConnPid} = gun:open("localhost", ProxyPort, #{
+ event_handler => {?MODULE, self()},
+ protocols => [config(name, config(tc_group_properties, Config))],
+ transport => tls
+ }),
+ {ok, http} = gun:await_up(ConnPid),
+ %% We skip the TLS handshake event to the TLS proxy.
+ _ = do_receive_event(tls_handshake_end),
+ StreamRef = gun:connect(ConnPid, #{
+ host => "localhost",
+ port => OriginPort,
+ transport => tls
+ }),
+ ReplyTo = self(),
+ #{
+ stream_ref := StreamRef,
+ reply_to := ReplyTo,
+ socket := Socket,
+ tls_opts := _,
+ timeout := _,
+ protocol := http
+ } = do_receive_event(tls_handshake_end),
+ true = is_pid(Socket),
+ gun:close(ConnPid).
+
request_start(Config) ->
doc("Confirm that the request_start event callback is called."),
do_request_event(Config, ?FUNCTION_NAME),