aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ebin/gun.app2
-rw-r--r--src/gun.erl80
-rw-r--r--src/gun_default_event_h.erl29
-rw-r--r--src/gun_event.erl72
-rw-r--r--test/event_SUITE.erl124
5 files changed, 284 insertions, 23 deletions
diff --git a/ebin/gun.app b/ebin/gun.app
index 40fa4ca..e3ad981 100644
--- a/ebin/gun.app
+++ b/ebin/gun.app
@@ -1,7 +1,7 @@
{application, 'gun', [
{description, "HTTP/1.1, HTTP/2 and Websocket client for Erlang/OTP."},
{vsn, "1.3.0"},
- {modules, ['gun','gun_app','gun_content_handler','gun_data_h','gun_http','gun_http2','gun_sse_h','gun_sup','gun_tcp','gun_tls','gun_tls_proxy','gun_tls_proxy_cb','gun_ws','gun_ws_h']},
+ {modules, ['gun','gun_app','gun_content_handler','gun_data_h','gun_default_event_h','gun_event','gun_http','gun_http2','gun_sse_h','gun_sup','gun_tcp','gun_tls','gun_tls_proxy','gun_tls_proxy_cb','gun_ws','gun_ws_h']},
{registered, [gun_sup]},
{applications, [kernel,stdlib,ssl,cowlib]},
{mod, {gun_app, []}},
diff --git a/src/gun.erl b/src/gun.erl
index 7376a33..e958d2e 100644
--- a/src/gun.erl
+++ b/src/gun.erl
@@ -106,6 +106,7 @@
-type opts() :: #{
connect_timeout => timeout(),
+ event_handler => {module(), any()},
http_opts => http_opts(),
http2_opts => http2_opts(),
protocols => [http | http2],
@@ -184,7 +185,9 @@
transport :: module(),
messages :: {atom(), atom(), atom()},
protocol :: module(),
- protocol_state :: any()
+ protocol_state :: any(),
+ event_handler :: module(),
+ event_handler_state :: any()
}).
%% Connection.
@@ -233,6 +236,8 @@ 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([{event_handler, {Mod, _}}|Opts]) when is_atom(Mod) ->
+ check_options(Opts);
check_options([{http_opts, ProtoOpts}|Opts]) when is_map(ProtoOpts) ->
case gun_http:check_options(ProtoOpts) of
ok ->
@@ -724,18 +729,29 @@ init({Owner, Host, Port, Opts}) ->
tls -> {<<"https">>, gun_tls}
end,
OwnerRef = monitor(process, Owner),
+ {EventHandler, EventHandlerState0} = maps:get(event_handler, Opts,
+ {gun_default_event_h, undefined}),
+ EventHandlerState = EventHandler:init(#{
+ owner => Owner,
+ transport => OriginTransport,
+ origin_scheme => OriginScheme,
+ origin_host => Host,
+ origin_port => Port,
+ opts => Opts
+ }, EventHandlerState0),
State = #state{owner=Owner, owner_ref=OwnerRef,
host=Host, port=Port, origin_scheme=OriginScheme,
origin_host=Host, origin_port=Port, opts=Opts,
- transport=Transport, messages=Transport:messages()},
+ transport=Transport, messages=Transport:messages(),
+ event_handler=EventHandler, event_handler_state=EventHandlerState},
{ok, not_connected, State,
{next_event, internal, {retries, Retry}}}.
default_transport(443) -> tls;
default_transport(_) -> tcp.
-not_connected(_, {retries, Retries},
- State=#state{host=Host, port=Port, opts=Opts, transport=Transport}) ->
+not_connected(_, {retries, Retries}, State0=#state{host=Host, port=Port, opts=Opts,
+ transport=Transport, event_handler=EventHandler, event_handler_state=EventHandlerState0}) ->
TransOpts0 = maps:get(transport_opts, Opts, []),
TransOpts1 = case Transport of
gun_tcp -> TransOpts0;
@@ -743,27 +759,47 @@ not_connected(_, {retries, Retries},
end,
TransOpts = [binary, {active, false}|TransOpts1],
ConnectTimeout = maps:get(connect_timeout, Opts, infinity),
+ ConnectEvent = #{
+ host => Host,
+ port => Port,
+ transport => Transport:name(),
+ transport_opts => TransOpts,
+ timeout => ConnectTimeout
+ },
+ EventHandlerState1 = EventHandler:connect_start(ConnectEvent, EventHandlerState0),
case Transport:connect(Host, Port, TransOpts, ConnectTimeout) of
- {ok, Socket} when Transport =:= gun_tcp ->
- Protocol = case maps:get(protocols, Opts, [http]) of
- [http] -> gun_http;
- [http2] -> gun_http2
+ {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
end,
- {next_state, connected, State,
+ EventHandlerState = EventHandler:connect_end(ConnectEvent#{
+ socket => Socket,
+ protocol => Protocol:name()
+ }, EventHandlerState1),
+ {next_state, connected, State0#state{event_handler_state=EventHandlerState},
{next_event, internal, {connected, Socket, Protocol}}};
- {ok, Socket} when Transport =:= gun_tls ->
- Protocol = case ssl:negotiated_protocol(Socket) of
- {ok, <<"h2">>} -> gun_http2;
- _ -> gun_http
- end,
- {next_state, connected, State,
- {next_event, internal, {connected, Socket, Protocol}}};
- {error, Reason} when Retries =:= 0 ->
- {stop, {shutdown, Reason}};
- {error, _Reason} ->
- Timeout = maps:get(retry_timeout, Opts, 5000),
- {keep_state, State,
- {state_timeout, Timeout, {retries, Retries - 1}}}
+ {error, Reason} ->
+ EventHandlerState = EventHandler:connect_end(ConnectEvent#{
+ error => Reason
+ }, EventHandlerState1),
+ State = State0#state{event_handler_state=EventHandlerState},
+ 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
end;
not_connected({call, From}, {stream_info, _}, _) ->
{keep_state_and_data, {reply, From, {error, not_connected}}};
diff --git a/src/gun_default_event_h.erl b/src/gun_default_event_h.erl
new file mode 100644
index 0000000..e242e84
--- /dev/null
+++ b/src/gun_default_event_h.erl
@@ -0,0 +1,29 @@
+%% Copyright (c) 2019, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+-module(gun_default_event_h).
+-behavior(gun_event).
+
+-export([init/2]).
+-export([connect_start/2]).
+-export([connect_end/2]).
+
+init(_EventData, State) ->
+ State.
+
+connect_start(_EventData, State) ->
+ State.
+
+connect_end(_EventData, State) ->
+ State.
diff --git a/src/gun_event.erl b/src/gun_event.erl
new file mode 100644
index 0000000..fffcafe
--- /dev/null
+++ b/src/gun_event.erl
@@ -0,0 +1,72 @@
+%% Copyright (c) 2019, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+-module(gun_event).
+
+%% init.
+
+-type init_event() :: #{
+ owner := pid(),
+ transport := tcp | tls,
+ origin_scheme := binary(),
+ origin_host := inet:hostname() | inet:ip_address(),
+ origin_port := inet:port_number(),
+ opts := gun:opts()
+}.
+
+-callback init(init_event(), State) -> State.
+
+%% connect_start/connect_end.
+
+-type connect_event() :: #{
+ host := inet:hostname() | inet:ip_address(),
+ port := inet:port_number(),
+ transport := tcp | tls,
+ transport_opts := [gen_tcp:connect_option()] | [ssl:connect_option()],
+ timeout := timeout(),
+ socket => inet:socket() | ssl:sslsocket() | pid(),
+ protocol => http | http2,
+ error => any()
+}.
+
+-callback connect_start(connect_event(), State) -> State.
+-callback connect_end(connect_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 protocol_changed
+%% @todo disconnected
+%% @todo terminate
+%% @todo stream_start
+%% @todo stream_end
+%% @todo request_start
+%% @todo request_headers
+%% @todo request_end
+%% @todo response_start (call it once per inform + one for the response)
+%% @todo response_inform
+%% @todo response_headers
+%% @todo response_end
+%% @todo push_promise_start
+%% @todo push_promise_end
+%% @todo ws_upgrade_start
+%% @todo ws_upgrade_end
+%% @todo ws_frame_read_start
+%% @todo ws_frame_read_header
+%% @todo ws_frame_read_end
+%% @todo ws_frame_write_start
+%% @todo ws_frame_write_end
diff --git a/test/event_SUITE.erl b/test/event_SUITE.erl
new file mode 100644
index 0000000..383ac46
--- /dev/null
+++ b/test/event_SUITE.erl
@@ -0,0 +1,124 @@
+%% Copyright (c) 2019, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+-module(event_SUITE).
+-compile(export_all).
+-compile(nowarn_export_all).
+
+-behavior(gun_event).
+
+-import(ct_helper, [config/2]).
+-import(ct_helper, [doc/1]).
+
+all() ->
+ ct_helper:all(?MODULE).
+
+init_per_suite(Config) ->
+ {ok, _} = cowboy:start_clear(?MODULE, [], #{env => #{
+ dispatch => cowboy_router:compile([{'_', [{"/", ws_echo, []}]}])
+ }}),
+ OriginPort = ranch:get_port(?MODULE),
+ [{origin_port, OriginPort}|Config].
+
+end_per_suite(_) ->
+ ok = cowboy:stop_listener(?MODULE).
+
+%% init.
+
+init(_) ->
+ doc("Confirm that the init event callback is called."),
+ Self = self(),
+ Opts = #{event_handler => {?MODULE, Self}},
+ {ok, Pid} = gun:open("localhost", 12345, Opts),
+ #{
+ owner := Self,
+ transport := tcp,
+ origin_scheme := <<"http">>,
+ origin_host := "localhost",
+ origin_port := 12345,
+ opts := Opts
+ } = do_receive_event(?FUNCTION_NAME),
+ gun:close(Pid).
+
+%% connect_start/connect_end.
+
+connect_start(_) ->
+ doc("Confirm that the connect_start event callback is called."),
+ Self = self(),
+ Opts = #{event_handler => {?MODULE, Self}},
+ {ok, Pid} = gun:open("localhost", 12345, Opts),
+ #{
+ host := "localhost",
+ port := 12345,
+ transport := tcp,
+ transport_opts := _,
+ timeout := _
+ } = do_receive_event(?FUNCTION_NAME),
+ gun:close(Pid).
+
+connect_end_error(_) ->
+ doc("Confirm that the connect_end event callback is called on connect failure."),
+ Self = self(),
+ Opts = #{event_handler => {?MODULE, Self}},
+ {ok, Pid} = gun:open("localhost", 12345, Opts),
+ #{
+ host := "localhost",
+ port := 12345,
+ transport := tcp,
+ transport_opts := _,
+ timeout := _,
+ error := _
+ } = do_receive_event(connect_end),
+ gun:close(Pid).
+
+connect_end_ok(Config) ->
+ doc("Confirm that the connect_end event callback is called on connect success."),
+ Self = self(),
+ Opts = #{event_handler => {?MODULE, Self}},
+ OriginPort = config(origin_port, Config),
+ {ok, Pid} = gun:open("localhost", OriginPort, Opts),
+ #{
+ host := "localhost",
+ port := OriginPort,
+ transport := tcp,
+ transport_opts := _,
+ timeout := _,
+ socket := _,
+ protocol := http
+ } = do_receive_event(connect_end),
+ gun:close(Pid).
+
+%% Internal.
+
+do_receive_event(Event) ->
+ receive
+ {Event, EventData} ->
+ EventData
+ after 5000 ->
+ error(timeout)
+ end.
+
+%% gun_event callbacks.
+
+init(EventData, Pid) ->
+ Pid ! {?FUNCTION_NAME, EventData},
+ Pid.
+
+connect_start(EventData, Pid) ->
+ Pid ! {?FUNCTION_NAME, EventData},
+ Pid.
+
+connect_end(EventData, Pid) ->
+ Pid ! {?FUNCTION_NAME, EventData},
+ Pid.