From c2ba2258a0020d82faa3e79162f05fc67d61b53e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Thu, 25 Jul 2019 14:14:34 +0200 Subject: Add tls_handshake events for CONNECT through TCP proxies --- src/gun_event.erl | 12 +++++++ src/gun_http.erl | 37 ++++++++++++++++----- test/event_SUITE.erl | 94 +++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 129 insertions(+), 14 deletions(-) diff --git a/src/gun_event.erl b/src/gun_event.erl index c87af58..ed50fd7 100644 --- a/src/gun_event.erl +++ b/src/gun_event.erl @@ -55,8 +55,20 @@ -callback connect_end(connect_event(), State) -> State. %% tls_handshake_start/tls_handshake_end. +%% +%% These events occur when connecting to a TLS server or when +%% 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(), + reply_to => pid(), socket := inet:socket() | ssl:sslsocket(), %% The socket before/after will be different. tls_opts := [ssl:connect_option()], timeout := timeout(), diff --git a/src/gun_http.erl b/src/gun_http.erl index 08a287c..113de0d 100644 --- a/src/gun_http.erl +++ b/src/gun_http.erl @@ -274,8 +274,8 @@ handle_head(Data, State=#http_state{socket=Socket, transport=Transport, ok end, %% @todo Figure out whether the event should trigger if the stream was cancelled. - EvHandlerState = EvHandler:response_headers(#{ - stream_ref => StreamRef, + EvHandlerState1 = EvHandler:response_headers(#{ + stream_ref => RealStreamRef, reply_to => ReplyTo, status => Status, headers => Headers @@ -296,33 +296,52 @@ handle_head(Data, State=#http_state{socket=Socket, transport=Transport, %% 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}], EvHandlerState}; + {switch_transport, gun_tls_proxy, ProxyPid}], EvHandlerState1}; #{transport := 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 + }, + EvHandlerState2 = EvHandler:tls_handshake_start(HandshakeEvent, EvHandlerState1), case gun_tls:connect(Socket, TLSOpts, TLSTimeout) of {ok, TLSSocket} -> - case ssl:negotiated_protocol(TLSSocket) of - {ok, <<"h2">>} -> + Protocol = case ssl:negotiated_protocol(TLSSocket) of + {ok, <<"h2">>} -> gun_http2; + _ -> gun_http + end, + EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{ + socket => TLSSocket, + protocol => Protocol:name() + }, EvHandlerState2), + case Protocol of + gun_http2 -> {[{origin, <<"https">>, NewHost, NewPort, connect}, {switch_transport, gun_tls, TLSSocket}, {switch_protocol, gun_http2, State2}], EvHandlerState}; - _ -> + gun_http -> {[{state, State2#http_state{socket=TLSSocket, transport=gun_tls}}, {origin, <<"https">>, NewHost, NewPort, connect}, {switch_transport, gun_tls, TLSSocket}], EvHandlerState} end; - Error -> + Error = {error, Reason} -> + EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{ + error => Reason + }, EvHandlerState2), {Error, EvHandlerState} end; _ -> case maps:get(protocols, Destination, [http]) of [http] -> {[{state, State2}, - {origin, <<"http">>, NewHost, NewPort, connect}], EvHandlerState}; + {origin, <<"http">>, NewHost, NewPort, connect}], EvHandlerState1}; [http2] -> {[{origin, <<"http">>, NewHost, NewPort, connect}, - {switch_protocol, gun_http2, State2}], EvHandlerState} + {switch_protocol, gun_http2, State2}], EvHandlerState1} end end; {_, _} when Status >= 100, Status =< 199 -> diff --git a/test/event_SUITE.erl b/test/event_SUITE.erl index 79416f3..fb45bc9 100644 --- a/test/event_SUITE.erl +++ b/test/event_SUITE.erl @@ -30,15 +30,15 @@ all() -> groups() -> Tests = ct_helper:all(?MODULE), - %% Some tests are written only for HTTP/1.0. - HTTP10Tests = [T || T <- Tests, lists:sublist(atom_to_list(T), 7) =:= "http10_"], + %% Some tests are written only for HTTP/1.0 or HTTP/1.1. + HTTP1Tests = [T || T <- Tests, lists:sublist(atom_to_list(T), 6) =:= "http1_"], %% Push is not possible over HTTP/1.1. PushTests = [T || T <- Tests, lists:sublist(atom_to_list(T), 5) =:= "push_"], %% We currently do not support Websocket over HTTP/2. WsTests = [T || T <- Tests, lists:sublist(atom_to_list(T), 3) =:= "ws_"], [ {http, [parallel], Tests -- [cancel_remote|PushTests]}, - {http2, [parallel], (Tests -- [protocol_changed|WsTests]) -- HTTP10Tests} + {http2, [parallel], (Tests -- [protocol_changed|WsTests]) -- HTTP1Tests} ]. init_per_suite(Config) -> @@ -233,9 +233,93 @@ tls_handshake_end_ok(Config) -> timeout := _, protocol := Protocol } = do_receive_event(tls_handshake_end), - false = is_port(Socket), + true = is_tuple(Socket), gun:close(Pid). +http1_tls_handshake_start_connect(Config) -> + doc("Confirm that the tls_handshake_start event callback is called " + "when using CONNECT to a TLS server via a TCP proxy."), + OriginPort = config(tls_origin_port, Config), + {ok, _, ProxyPort} = rfc7231_SUITE:do_proxy_start(tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [config(name, config(tc_group_properties, Config))], + transport => tcp + }), + {ok, http} = gun:await_up(ConnPid), + 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_port(Socket), + gun:close(ConnPid). + +http1_tls_handshake_end_error_connect(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 TCP 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(tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [config(name, config(tc_group_properties, Config))], + transport => tcp + }), + {ok, http} = gun:await_up(ConnPid), + 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_port(Socket), + gun:close(ConnPid). + +http1_tls_handshake_end_ok_connect(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 TCP proxy."), + OriginPort = config(tls_origin_port, Config), + {ok, _, ProxyPort} = rfc7231_SUITE:do_proxy_start(tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [config(name, config(tc_group_properties, Config))], + transport => tcp + }), + {ok, http} = gun:await_up(ConnPid), + 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_tuple(Socket), + gun:close(ConnPid). + request_start(Config) -> doc("Confirm that the request_start event callback is called."), do_request_event(Config, ?FUNCTION_NAME), @@ -477,7 +561,7 @@ do_response_end(Config, EventName, Path) -> } = do_receive_event(EventName), gun:close(Pid). -http10_response_end_body_close(Config) -> +http1_response_end_body_close(Config) -> doc("Confirm that the request_headers event callback is called " "when using HTTP/1.0 and the content-length header is not set."), OriginPort = config(tcp_origin_port, Config), -- cgit v1.2.3