aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2019-07-22 16:17:10 +0200
committerLoïc Hoguin <[email protected]>2019-07-22 16:17:10 +0200
commit516933f9dd2722329b3886c495d5242308958fe1 (patch)
tree60b16423184a4db405a65266c326b7944ce42256 /src
parent265ece680c53f77d1685434d0636216c94021497 (diff)
downloadgun-516933f9dd2722329b3886c495d5242308958fe1.tar.gz
gun-516933f9dd2722329b3886c495d5242308958fe1.tar.bz2
gun-516933f9dd2722329b3886c495d5242308958fe1.zip
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.
Diffstat (limited to 'src')
-rw-r--r--src/gun.erl179
-rw-r--r--src/gun_default_event_h.erl16
-rw-r--r--src/gun_event.erl42
-rw-r--r--src/gun_tcp.erl73
-rw-r--r--src/gun_tls.erl9
5 files changed, 240 insertions, 79 deletions
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).