diff options
author | Loïc Hoguin <[email protected]> | 2019-07-26 12:35:38 +0200 |
---|---|---|
committer | Loïc Hoguin <[email protected]> | 2019-07-26 12:36:55 +0200 |
commit | e4df3bb7c726571640c2799bc7a6fbb687b3bdae (patch) | |
tree | 1f044ea876852c876846a9d518c97e35cb535e53 | |
parent | c2ba2258a0020d82faa3e79162f05fc67d61b53e (diff) | |
download | gun-e4df3bb7c726571640c2799bc7a6fbb687b3bdae.tar.gz gun-e4df3bb7c726571640c2799bc7a6fbb687b3bdae.tar.bz2 gun-e4df3bb7c726571640c2799bc7a6fbb687b3bdae.zip |
Add tls_handshake events for CONNECT through TLS proxies
-rw-r--r-- | src/gun.erl | 24 | ||||
-rw-r--r-- | src/gun_event.erl | 5 | ||||
-rw-r--r-- | src/gun_http.erl | 12 | ||||
-rw-r--r-- | src/gun_tls_proxy.erl | 42 | ||||
-rw-r--r-- | test/event_SUITE.erl | 90 |
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), |