From 516933f9dd2722329b3886c495d5242308958fe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Mon, 22 Jul 2019 16:17:10 +0200 Subject: Split domain lookup/connect/TLS handshake and add events This changes the way we connect to servers entirely. We now have three states when connecting (domain_lookup, connect and tls_handshake when applicable) and as a result three corresponding timeout options. Each state has a start/end event associated and the event data was tweaked to best match each event. Since the TLS handshake is separate, the transport_opts option was also split into two: tcp_opts and tls_opts. --- src/gun.erl | 179 +++++++++++++++++++++++++++++++------------- src/gun_default_event_h.erl | 16 ++++ src/gun_event.erl | 42 ++++++++--- src/gun_tcp.erl | 73 ++++++++++++++++-- src/gun_tls.erl | 9 --- 5 files changed, 240 insertions(+), 79 deletions(-) (limited to 'src') diff --git a/src/gun.erl b/src/gun.erl index 12e4ae6..b55d3a1 100644 --- a/src/gun.erl +++ b/src/gun.erl @@ -93,6 +93,9 @@ -export([callback_mode/0]). -export([init/1]). -export([not_connected/3]). +-export([domain_lookup/3]). +-export([connecting/3]). +-export([tls_handshake/3]). -export([connected/3]). -export([terminate/3]). @@ -107,17 +110,20 @@ -type opts() :: #{ connect_timeout => timeout(), - event_handler => {module(), any()}, - http_opts => http_opts(), - http2_opts => http2_opts(), - protocols => [http | http2], - retry => non_neg_integer(), - retry_timeout => pos_integer(), - supervise => boolean(), - trace => boolean(), - transport => tcp | tls | ssl, - transport_opts => [gen_tcp:connect_option()] | [ssl:connect_option()], - ws_opts => ws_opts() + domain_lookup_timeout => timeout(), + event_handler => {module(), any()}, + http_opts => http_opts(), + http2_opts => http2_opts(), + protocols => [http | http2], + retry => non_neg_integer(), + retry_timeout => pos_integer(), + supervise => boolean(), + tcp_opts => [gen_tcp:connect_option()], + tls_handshake_timeout => timeout(), + tls_opts => [ssl:connect_option()], + trace => boolean(), + transport => tcp | tls | ssl, + ws_opts => ws_opts() }. -export_type([opts/0]). %% @todo Add an option to disable/enable the notowner behavior. @@ -237,6 +243,10 @@ check_options([{connect_timeout, infinity}|Opts]) -> check_options(Opts); check_options([{connect_timeout, T}|Opts]) when is_integer(T), T >= 0 -> check_options(Opts); +check_options([{domain_lookup_timeout, infinity}|Opts]) -> + check_options(Opts); +check_options([{domain_lookup_timeout, T}|Opts]) when is_integer(T), T >= 0 -> + check_options(Opts); check_options([{event_handler, {Mod, _}}|Opts]) when is_atom(Mod) -> check_options(Opts); check_options([{http_opts, ProtoOpts}|Opts]) when is_map(ProtoOpts) -> @@ -273,12 +283,18 @@ check_options([{retry_timeout, T}|Opts]) when is_integer(T), T >= 0 -> check_options(Opts); check_options([{supervise, B}|Opts]) when B =:= true; B =:= false -> check_options(Opts); +check_options([{tcp_opts, L}|Opts]) when is_list(L) -> + check_options(Opts); +check_options([{tls_handshake_timeout, infinity}|Opts]) -> + check_options(Opts); +check_options([{tls_handshake_timeout, T}|Opts]) when is_integer(T), T >= 0 -> + check_options(Opts); +check_options([{tls_opts, L}|Opts]) when is_list(L) -> + check_options(Opts); check_options([{trace, B}|Opts]) when B =:= true; B =:= false -> check_options(Opts); check_options([{transport, T}|Opts]) when T =:= tcp; T =:= tls -> check_options(Opts); -check_options([{transport_opts, L}|Opts]) when is_list(L) -> - check_options(Opts); check_options([{ws_opts, ProtoOpts}|Opts]) when is_map(ProtoOpts) -> case gun_ws:check_options(ProtoOpts) of ok -> @@ -745,66 +761,125 @@ init({Owner, Host, Port, Opts}) -> origin_host=Host, origin_port=Port, opts=Opts, transport=Transport, messages=Transport:messages(), event_handler=EvHandler, event_handler_state=EvHandlerState}, - {ok, not_connected, State, - {next_event, internal, {retries, Retry}}}. + {ok, domain_lookup, State, + {next_event, internal, {retries, Retry, not_connected}}}. default_transport(443) -> tls; default_transport(_) -> tcp. -not_connected(_, {retries, Retries}, State0=#state{host=Host, port=Port, opts=Opts, +%% @todo This is where we would implement the backoff mechanism presumably. +not_connected(_, {retries, 0, Reason}, State) -> + {stop, {shutdown, Reason}, State}; +not_connected(_, {retries, Retries, _}, State=#state{opts=Opts}) -> + Timeout = maps:get(retry_timeout, Opts, 5000), + {next_state, domain_lookup, State, + {state_timeout, Timeout, {retries, Retries - 1, not_connected}}}; +not_connected({call, From}, {stream_info, _}, _) -> + {keep_state_and_data, {reply, From, {error, not_connected}}}; +not_connected(Type, Event, State) -> + handle_common(Type, Event, ?FUNCTION_NAME, State). + +domain_lookup(_, {retries, Retries, _}, State=#state{host=Host, port=Port, opts=Opts, + event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> + TransOpts = maps:get(tcp_opts, Opts, []), + DomainLookupTimeout = maps:get(domain_lookup_timeout, Opts, infinity), + DomainLookupEvent = #{ + host => Host, + port => Port, + tcp_opts => TransOpts, + timeout => DomainLookupTimeout + }, + EvHandlerState1 = EvHandler:domain_lookup_start(DomainLookupEvent, EvHandlerState0), + case gun_tcp:domain_lookup(Host, Port, TransOpts, DomainLookupTimeout) of + {ok, LookupInfo} -> + EvHandlerState = EvHandler:domain_lookup_end(DomainLookupEvent#{ + lookup_info => LookupInfo + }, EvHandlerState1), + {next_state, connecting, State#state{event_handler_state=EvHandlerState}, + {next_event, internal, {retries, Retries, LookupInfo}}}; + {error, Reason} -> + EvHandlerState = EvHandler:domain_lookup_end(DomainLookupEvent#{ + error => Reason + }, EvHandlerState1), + {next_state, not_connected, State#state{event_handler_state=EvHandlerState}, + {next_event, internal, {retries, Retries, Reason}}} + end; +domain_lookup({call, From}, {stream_info, _}, _) -> + {keep_state_and_data, {reply, From, {error, not_connected}}}; +domain_lookup(Type, Event, State) -> + handle_common(Type, Event, ?FUNCTION_NAME, State). + +connecting(_, {retries, Retries, LookupInfo}, State=#state{opts=Opts, transport=Transport, event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> - TransOpts0 = maps:get(transport_opts, Opts, []), - TransOpts1 = case Transport of - gun_tcp -> TransOpts0; - gun_tls -> ensure_alpn(maps:get(protocols, Opts, [http2, http]), TransOpts0) - end, - TransOpts = [binary, {active, false}|TransOpts1], ConnectTimeout = maps:get(connect_timeout, Opts, infinity), ConnectEvent = #{ - host => Host, - port => Port, - transport => Transport:name(), - transport_opts => TransOpts, + lookup_info => LookupInfo, timeout => ConnectTimeout }, EvHandlerState1 = EvHandler:connect_start(ConnectEvent, EvHandlerState0), - case Transport:connect(Host, Port, TransOpts, ConnectTimeout) of - {ok, Socket} -> - Protocol = case Transport of - gun_tcp -> - case maps:get(protocols, Opts, [http]) of - [http] -> gun_http; - [http2] -> gun_http2 - end; - gun_tls -> - case ssl:negotiated_protocol(Socket) of - {ok, <<"h2">>} -> gun_http2; - _ -> gun_http - end + case gun_tcp:connect(LookupInfo, ConnectTimeout) of + {ok, Socket} when Transport =:= gun_tcp -> + Protocol = case maps:get(protocols, Opts, [http]) of + [http] -> gun_http; + [http2] -> gun_http2 end, EvHandlerState = EvHandler:connect_end(ConnectEvent#{ socket => Socket, protocol => Protocol:name() }, EvHandlerState1), - {next_state, connected, State0#state{event_handler_state=EvHandlerState}, + {next_state, connected, State#state{event_handler_state=EvHandlerState}, {next_event, internal, {connected, Socket, Protocol}}}; + {ok, Socket} when Transport =:= gun_tls -> + EvHandlerState = EvHandler:connect_end(ConnectEvent#{ + socket => Socket + }, EvHandlerState1), + {next_state, tls_handshake, State#state{event_handler_state=EvHandlerState}, + {next_event, internal, {retries, Retries, Socket}}}; {error, Reason} -> EvHandlerState = EvHandler:connect_end(ConnectEvent#{ error => Reason }, EvHandlerState1), - State = State0#state{event_handler_state=EvHandlerState}, - case Retries of - 0 -> - {stop, {shutdown, Reason}, State}; - _ -> - Timeout = maps:get(retry_timeout, Opts, 5000), - {keep_state, State, - {state_timeout, Timeout, {retries, Retries - 1}}} - end + {next_state, not_connected, State#state{event_handler_state=EvHandlerState}, + {next_event, internal, {retries, Retries, Reason}}} end; -not_connected({call, From}, {stream_info, _}, _) -> +connecting({call, From}, {stream_info, _}, _) -> {keep_state_and_data, {reply, From, {error, not_connected}}}; -not_connected(Type, Event, State) -> +connecting(Type, Event, State) -> + handle_common(Type, Event, ?FUNCTION_NAME, State). + +tls_handshake(_, {retries, Retries, Socket0}, State=#state{opts=Opts, + event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> + TransOpts0 = maps:get(tls_opts, Opts, []), + TransOpts = ensure_alpn(maps:get(protocols, Opts, [http2, http]), TransOpts0), + HandshakeTimeout = maps:get(tls_handshake_timeout, Opts, infinity), + HandshakeEvent = #{ + socket => Socket0, + tls_opts => TransOpts, + timeout => HandshakeTimeout + }, + EvHandlerState1 = EvHandler:tls_handshake_start(HandshakeEvent, EvHandlerState0), + case gun_tls:connect(Socket0, TransOpts, HandshakeTimeout) of + {ok, Socket} -> + Protocol = case ssl:negotiated_protocol(Socket) of + {ok, <<"h2">>} -> gun_http2; + _ -> gun_http + end, + EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{ + socket => Socket, + protocol => Protocol:name() + }, EvHandlerState1), + {next_state, connected, State#state{event_handler_state=EvHandlerState}, + {next_event, internal, {connected, Socket, Protocol}}}; + {error, Reason} -> + EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{ + error => Reason + }, EvHandlerState1), + {next_state, not_connected, State#state{event_handler_state=EvHandlerState}, + {next_event, internal, {retries, Retries, Reason}}} + end; +tls_handshake({call, From}, {stream_info, _}, _) -> + {keep_state_and_data, {reply, From, {error, not_connected}}}; +tls_handshake(Type, Event, State) -> handle_common(Type, Event, ?FUNCTION_NAME, State). ensure_alpn(Protocols0, TransOpts) -> @@ -1048,7 +1123,7 @@ disconnect(State=#state{owner=Owner, opts=Opts, keepalive_cancel(State#state{socket=undefined, protocol=undefined, protocol_state=undefined, event_handler_state=EvHandlerState}), - {next_event, internal, {retries, Retry - 1}}} + {next_event, internal, {retries, Retry - 1, Reason}}} end. disconnect_flush(State=#state{socket=Socket, messages={OK, Closed, Error}}) -> diff --git a/src/gun_default_event_h.erl b/src/gun_default_event_h.erl index de63f17..9b56e71 100644 --- a/src/gun_default_event_h.erl +++ b/src/gun_default_event_h.erl @@ -16,8 +16,12 @@ -behavior(gun_event). -export([init/2]). +-export([domain_lookup_start/2]). +-export([domain_lookup_end/2]). -export([connect_start/2]). -export([connect_end/2]). +-export([tls_handshake_start/2]). +-export([tls_handshake_end/2]). -export([request_start/2]). -export([request_headers/2]). -export([request_end/2]). @@ -39,12 +43,24 @@ init(_EventData, State) -> State. +domain_lookup_start(_EventData, State) -> + State. + +domain_lookup_end(_EventData, State) -> + State. + connect_start(_EventData, State) -> State. connect_end(_EventData, State) -> State. +tls_handshake_start(_EventData, State) -> + State. + +tls_handshake_end(_EventData, State) -> + State. + request_start(_EventData, State) -> State. diff --git a/src/gun_event.erl b/src/gun_event.erl index 5332568..4716f47 100644 --- a/src/gun_event.erl +++ b/src/gun_event.erl @@ -27,22 +27,46 @@ -callback init(init_event(), State) -> State. -%% connect_start/connect_end. +%% domain_lookup_start/domain_lookup_end. --type connect_event() :: #{ +-type domain_lookup_event() :: #{ host := inet:hostname() | inet:ip_address(), port := inet:port_number(), - transport := tcp | tls, - transport_opts := [gen_tcp:connect_option()] | [ssl:connect_option()], + tcp_opts := [gen_tcp:connect_option()], timeout := timeout(), - socket => inet:socket() | ssl:sslsocket() | pid(), - protocol => http | http2, + lookup_info => gun_tcp:lookup_info(), + error => any() +}. + +-callback domain_lookup_start(domain_lookup_event(), State) -> State. +-callback domain_lookup_end(domain_lookup_event(), State) -> State. + +%% connect_start/connect_end. + +-type connect_event() :: #{ + lookup_info := gun_tcp:lookup_info(), + timeout := timeout(), + socket => inet:socket(), + protocol => http | http2, %% Only when transport is tcp. error => any() }. -callback connect_start(connect_event(), State) -> State. -callback connect_end(connect_event(), State) -> State. +%% tls_handshake_start/tls_handshake_end. + +-type tls_handshake_event() :: #{ + socket := inet:socket() | ssl:sslsocket(), %% The socket before/after will be different. + tls_opts := [ssl:connect_option()], + timeout := timeout(), + protocol => http | http2, + error => any() +}. + +-callback tls_handshake_start(tls_handshake_event(), State) -> State. +-callback tls_handshake_end(tls_handshake_event(), State) -> State. + %% request_start/request_headers. -type request_start_event() :: #{ @@ -201,16 +225,12 @@ %% terminate. -type terminate_event() :: #{ - state := not_connected | connected, + state := not_connected | domain_lookup | connecting | tls_handshake | connected, reason := normal | shutdown | {shutdown, any()} | any() }. -callback terminate(terminate_event(), State) -> State. -%% @todo domain_lookup_start -%% @todo domain_lookup_end -%% @todo tls_handshake_start -%% @todo tls_handshake_end %% @todo origin_changed %% @todo transport_changed %% @todo push_promise_start diff --git a/src/gun_tcp.erl b/src/gun_tcp.erl index 72e5681..2d091a0 100644 --- a/src/gun_tcp.erl +++ b/src/gun_tcp.erl @@ -16,23 +16,82 @@ -export([name/0]). -export([messages/0]). --export([connect/4]). +-export([domain_lookup/4]). +-export([connect/2]). -export([send/2]). -export([setopts/2]). -export([sockname/1]). -export([close/1]). +-type lookup_info() :: #{ + ip_addresses := [inet:ip_address()], + port := inet:port_number(), + tcp_module := module(), + tcp_opts := [gen_tcp:connect_option()] | [ssl:connect_option()] +}. +-export_type([lookup_info/0]). + name() -> tcp. messages() -> {tcp, tcp_closed, tcp_error}. --spec connect(inet:ip_address() | inet:hostname(), - inet:port_number(), any(), timeout()) +%% The functions domain_lookup/4 and connect/2 are very similar +%% to gen_tcp:connect/4 except the logic is split in order to +%% be able to trigger events between the domain lookup step +%% and the actual connect step. + +-spec domain_lookup(inet:ip_address() | inet:hostname(), + inet:port_number(), [gen_tcp:connect_option()] | [ssl:connect_option()], timeout()) + -> {ok, lookup_info()} | {error, atom()}. +domain_lookup(Address, Port0, Opts0, Timeout) -> + {Mod, Opts} = inet:tcp_module(Opts0, Address), + Timer = inet:start_timer(Timeout), + try Mod:getaddrs(Address, Timer) of + {ok, IPs} -> + case Mod:getserv(Port0) of + {ok, Port} -> + {ok, #{ + ip_addresses => IPs, + port => Port, + tcp_module => Mod, + tcp_opts => Opts ++ [binary, {active, false}, {packet, raw}] + }}; + Error -> + maybe_exit(Error) + end; + Error -> + maybe_exit(Error) + after + _ = inet:stop_timer(Timer) + end. + +-spec connect(lookup_info(), timeout()) -> {ok, inet:socket()} | {error, atom()}. -connect(Host, Port, Opts, Timeout) when is_integer(Port) -> - gen_tcp:connect(Host, Port, - Opts ++ [binary, {active, false}, {packet, raw}], - Timeout). +connect(#{ip_addresses := IPs, port := Port, tcp_module := Mod, tcp_opts := Opts}, Timeout) -> + Timer = inet:start_timer(Timeout), + Res = try + try_connect(IPs, Port, Opts, Timer, Mod, {error, einval}) + after + _ = inet:stop_timer(Timer) + end, + case Res of + {ok, S} -> {ok, S}; + Error -> maybe_exit(Error) + end. + +try_connect([IP|IPs], Port, Opts, Timer, Mod, _) -> + Timeout = inet:timeout(Timer), + case Mod:connect(IP, Port, Opts, Timeout) of + {ok, S} -> {ok, S}; + {error, einval} -> {error, einval}; + {error, timeout} -> {error, timeout}; + Error -> try_connect(IPs, Port, Opts, Timer, Mod, Error) + end; +try_connect([], _, _, _, _, Error) -> + Error. + +maybe_exit({error, einval}) -> exit(badarg); +maybe_exit(Error) -> Error. -spec send(inet:socket(), iodata()) -> ok | {error, atom()}. send(Socket, Packet) -> diff --git a/src/gun_tls.erl b/src/gun_tls.erl index 256b881..3f8d1d6 100644 --- a/src/gun_tls.erl +++ b/src/gun_tls.erl @@ -17,7 +17,6 @@ -export([name/0]). -export([messages/0]). -export([connect/3]). --export([connect/4]). -export([send/2]). -export([setopts/2]). -export([sockname/1]). @@ -32,14 +31,6 @@ messages() -> {ssl, ssl_closed, ssl_error}. connect(Socket, Opts, Timeout) -> ssl:connect(Socket, Opts, Timeout). --spec connect(inet:ip_address() | inet:hostname(), - inet:port_number(), any(), timeout()) - -> {ok, ssl:sslsocket()} | {error, atom()}. -connect(Host, Port, Opts, Timeout) when is_integer(Port) -> - ssl:connect(Host, Port, - Opts ++ [binary, {active, false}, {packet, raw}], - Timeout). - -spec send(ssl:sslsocket(), iodata()) -> ok | {error, atom()}. send(Socket, Packet) -> ssl:send(Socket, Packet). -- cgit v1.2.3