aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/gun.erl18
-rw-r--r--src/gun_http.erl4
-rw-r--r--src/gun_http2.erl38
-rw-r--r--src/gun_http3.erl6
-rw-r--r--src/gun_raw.erl4
-rw-r--r--src/gun_socks.erl4
-rw-r--r--src/gun_tunnel.erl14
-rw-r--r--src/gun_ws.erl4
-rw-r--r--test/rfc7540_SUITE.erl97
9 files changed, 132 insertions, 57 deletions
diff --git a/src/gun.erl b/src/gun.erl
index ead6152..39b3b95 100644
--- a/src/gun.erl
+++ b/src/gun.erl
@@ -739,17 +739,17 @@ data(ServerPid, StreamRef, IsFin, Data) ->
%% User pings.
--spec ping(pid()) -> stream_ref().
+-spec ping(pid()) -> reference().
ping(ServerPid) ->
ping(ServerPid, #{}).
--spec ping(pid(), req_opts()) -> stream_ref().
+-spec ping(pid(), req_opts()) -> reference().
ping(ServerPid, ReqOpts) ->
Tunnel = get_tunnel(ReqOpts),
- StreamRef = make_stream_ref(Tunnel),
+ PingRef = make_ref(),
ReplyTo = maps:get(reply_to, ReqOpts, self()),
- gen_statem:cast(ServerPid, {ping, ReplyTo, StreamRef}),
- StreamRef.
+ gen_statem:cast(ServerPid, {ping, ReplyTo, Tunnel, PingRef}),
+ PingRef.
%% Tunneling.
@@ -1397,9 +1397,13 @@ connected_ws_only(Type, Event, State) ->
%%
%% @todo It might be better, internally, to pass around a URIMap
%% containing the target URI, instead of separate Host/Port/PathWithQs.
-connected(cast, {ping, ReplyTo, StreamRef},
+connected(cast, {ping, ReplyTo, Tunnel0, PingRef},
State=#state{protocol=Protocol, protocol_state=ProtoState}) ->
- Commands = Protocol:ping(ProtoState, dereference_stream_ref(StreamRef, State), ReplyTo),
+ Tunnel = case dereference_stream_ref(Tunnel0, State) of
+ [] -> undefined;
+ Tunnel1 -> Tunnel1
+ end,
+ Commands = Protocol:ping(ProtoState, Tunnel, ReplyTo, PingRef),
commands(Commands, State);
connected(cast, {headers, ReplyTo, StreamRef, Method, Path, Headers, InitialFlow},
State=#state{origin_host=Host, origin_port=Port,
diff --git a/src/gun_http.erl b/src/gun_http.erl
index d77d9b6..005f04b 100644
--- a/src/gun_http.erl
+++ b/src/gun_http.erl
@@ -26,7 +26,7 @@
-export([closing/4]).
-export([close/4]).
-export([keepalive/3]).
--export([ping/3]).
+-export([ping/4]).
-export([headers/12]).
-export([request/13]).
-export([data/7]).
@@ -561,7 +561,7 @@ keepalive(#http_state{socket=Socket, transport=Transport, out=head}, _, EvHandle
keepalive(_State, _, EvHandlerState) ->
{[], EvHandlerState}.
-ping(_State, _PingRef, _ReplyTo) ->
+ping(_State, undefined, _ReplyTo, _PingRef) ->
{error, unsupported_by_protocol}.
headers(State, StreamRef, ReplyTo, _, _, _, _, _, _, CookieStore, _, EvHandlerState)
diff --git a/src/gun_http2.erl b/src/gun_http2.erl
index bc100f5..a1d717e 100644
--- a/src/gun_http2.erl
+++ b/src/gun_http2.erl
@@ -27,7 +27,7 @@
-export([closing/4]).
-export([close/4]).
-export([keepalive/3]).
--export([ping/3]).
+-export([ping/4]).
-export([headers/12]).
-export([request/13]).
-export([data/7]).
@@ -389,9 +389,8 @@ maybe_ack_or_notify(State=#http2_state{reply_to=ReplyTo, socket=Socket,
{ping_ack, Payload} ->
%% User ping.
case lists:keytake(Payload, #user_ping.payload, UserPings0) of
- {value, #user_ping{ref=StreamRef, reply_to=PingReplyTo}, UserPings} ->
- RealStreamRef = stream_ref(State, StreamRef),
- PingReplyTo ! {gun_notify, self(), ping_ack, RealStreamRef},
+ {value, #user_ping{ref=PingRef, reply_to=PingReplyTo}, UserPings} ->
+ PingReplyTo ! {gun_notify, self(), ping_ack, PingRef},
{state, State#http2_state{user_pings=UserPings}};
false ->
%% Ignore unexpected ping ack. RFC 7540
@@ -971,15 +970,36 @@ keepalive(State=#http2_state{socket=Socket, transport=Transport, pings_unack=Pin
{Error, EvHandlerState}
end.
-ping(State=#http2_state{socket=Socket, transport=Transport, user_pings=UserPings}, StreamRef, ReplyTo) ->
+ping(State=#http2_state{socket=Socket, transport=Transport, user_pings=UserPings},
+ undefined, ReplyTo, PingRef) ->
%% Use non-zero 64-bit payload for user pings. 0 is reserved for keepalive.
Payload = erlang:unique_integer([monotonic, positive]),
case Transport:send(Socket, cow_http2:ping(Payload)) of
ok ->
- UserPing = #user_ping{ref = StreamRef, reply_to = ReplyTo, payload = Payload},
- {state, State#http2_state{user_pings = UserPings ++ [UserPing]}};
- Error={error, _} ->
- {ok, Error}
+ UserPing = #user_ping{ref = PingRef, reply_to = ReplyTo, payload = Payload},
+ {state, State#http2_state{user_pings = [UserPing|UserPings]}};
+ Error = {error, _} ->
+ Error
+ end;
+%% Tunneled ping.
+ping(State, TunnelRef=[StreamRef|_], ReplyTo, PingRef) ->
+ case get_stream_by_ref(State, StreamRef) of
+ %% @todo We should send an error to the user if the stream isn't ready.
+ Stream=#stream{tunnel=Tunnel=#tunnel{protocol=Proto, protocol_state=ProtoState0}} ->
+ case Proto:ping(ProtoState0, TunnelRef, ReplyTo, PingRef) of
+ {state, ProtoState} ->
+ {state, store_stream(State, Stream#stream{
+ tunnel=Tunnel#tunnel{protocol_state=ProtoState}})};
+ Error = {error, _} ->
+ Error
+ end;
+ #stream{tunnel=undefined} ->
+ gun:reply(ReplyTo, {gun_error, self(), stream_ref(State, StreamRef), {badstate,
+ "The stream is not a tunnel."}}),
+ {state, State};
+ error ->
+ error_stream_not_found(State, StreamRef, ReplyTo),
+ {state, State}
end.
headers(State, StreamRef, ReplyTo, Method, Host, Port, Path,
diff --git a/src/gun_http3.erl b/src/gun_http3.erl
index 5b95d4a..99f14cc 100644
--- a/src/gun_http3.erl
+++ b/src/gun_http3.erl
@@ -28,7 +28,7 @@
-export([closing/4]).
-export([close/4]).
-export([keepalive/3]).
--export([ping/3]).
+-export([ping/4]).
-export([headers/12]).
-export([request/13]).
-export([data/7]).
@@ -487,9 +487,9 @@ close(_Reason, _State, _, EvHandlerState) ->
keepalive(_State, _, _EvHandlerState) ->
error(todo).
--spec ping(_, _, _) -> {error, not_implemented}.
+-spec ping(_, _, _, _) -> {error, not_implemented}.
-ping(_State, _PingRef, _ReplyTo) ->
+ping(_State, _Tunnel, _ReplyTo, _PingRef) ->
{error, not_implemented}.
headers(State0=#http3_state{conn=Conn, transport=Transport,
diff --git a/src/gun_raw.erl b/src/gun_raw.erl
index 3c506b6..a3f1ef5 100644
--- a/src/gun_raw.erl
+++ b/src/gun_raw.erl
@@ -23,7 +23,6 @@
-export([update_flow/4]).
-export([closing/4]).
-export([close/4]).
--export([ping/3]).
-export([data/7]).
-export([down/1]).
@@ -85,9 +84,6 @@ closing(_, _, _, EvHandlerState) ->
close(_, _, _, EvHandlerState) ->
EvHandlerState.
-ping(_State, _PingRef, _ReplyTo) ->
- {error, unsupported_by_protocol}.
-
%% @todo Initiate closing on IsFin=fin.
data(#raw_state{ref=StreamRef, socket=Socket, transport=Transport}, StreamRef,
_ReplyTo, _IsFin, Data, _EvHandler, EvHandlerState) ->
diff --git a/src/gun_socks.erl b/src/gun_socks.erl
index 7a2bdd7..d29f906 100644
--- a/src/gun_socks.erl
+++ b/src/gun_socks.erl
@@ -23,7 +23,6 @@
-export([handle/5]).
-export([closing/4]).
-export([close/4]).
--export([ping/3]).
%% @todo down
-record(socks_state, {
@@ -206,6 +205,3 @@ closing(_, _, _, EvHandlerState) ->
close(_, _, _, EvHandlerState) ->
EvHandlerState.
-
-ping(_State, _PingRef, _ReplyTo) ->
- {error, unsupported_by_protocol}.
diff --git a/src/gun_tunnel.erl b/src/gun_tunnel.erl
index a6d51e3..d89b665 100644
--- a/src/gun_tunnel.erl
+++ b/src/gun_tunnel.erl
@@ -24,6 +24,7 @@
-export([closing/4]).
-export([close/4]).
-export([keepalive/3]).
+-export([ping/4]).
-export([headers/12]).
-export([request/13]).
-export([data/7]).
@@ -285,6 +286,19 @@ keepalive(_State, _EvHandler, EvHandlerState) ->
%% @todo Need to figure out how to handle keepalive for tunnels.
{[], EvHandlerState}.
+ping(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0},
+ TunnelRef0, ReplyTo, PingRef) ->
+ TunnelRef = case maybe_dereference(State, TunnelRef0) of
+ [] -> undefined;
+ TunnelRef1 -> TunnelRef1
+ end,
+ case Proto:ping(ProtoState0, TunnelRef, ReplyTo, PingRef) of
+ {state, ProtoState} ->
+ {state, State#tunnel_state{protocol_state=ProtoState}};
+ Error = {error, _} ->
+ Error
+ end.
+
%% We pass the headers forward and optionally dereference StreamRef.
headers(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0},
StreamRef0, ReplyTo, Method, Host, Port, Path, Headers,
diff --git a/src/gun_ws.erl b/src/gun_ws.erl
index 09e29aa..07df766 100644
--- a/src/gun_ws.erl
+++ b/src/gun_ws.erl
@@ -28,7 +28,7 @@
-export([closing/4]).
-export([close/4]).
-export([keepalive/3]).
--export([ping/3]).
+-export([ping/4]).
-export([ws_send/6]).
-export([down/1]).
@@ -308,7 +308,7 @@ close(_, _, _, EvHandlerState) ->
keepalive(State=#ws_state{reply_to=ReplyTo}, EvHandler, EvHandlerState0) ->
send(ping, State, ReplyTo, EvHandler, EvHandlerState0).
-ping(_State, _PingRef, _ReplyTo) ->
+ping(_State, undefined, _ReplyTo, _PingRef) ->
{error, not_implemented}.
%% Send one frame.
diff --git a/test/rfc7540_SUITE.erl b/test/rfc7540_SUITE.erl
index b629ec0..5d470eb 100644
--- a/test/rfc7540_SUITE.erl
+++ b/test/rfc7540_SUITE.erl
@@ -534,9 +534,28 @@ keepalive_tolerance_ping_ack_timeout(_) ->
error(timeout)
end.
-user_initiated_ping(_) ->
- doc("The PING frame allows a client to safely test whether a connection is"
- " still active without sending a request. (RFC7540 8.1.4)"),
+do_ping_ack_loop_fun() ->
+ %% Receive ping, sync with parent, send ping ack, loop.
+ fun Loop(Parent, ListenSocket, Socket, Transport) ->
+ {ok, Data} = Transport:recv(Socket, 9, infinity),
+ <<Len:24, 6:8, %% PING
+ 0:8, %% Flags
+ 0:1, 0:31>> = Data,
+ {ok, Payload} = Transport:recv(Socket, Len, 1000),
+ 8 = Len = byte_size(Payload),
+ Parent ! ping_received,
+ receive
+ send_ping_ack ->
+ Ack = <<8:24, 6:8, %% PING
+ 1:8, %% Ack flag
+ 0:1, 0:31, Payload/binary>>,
+ ok = Transport:send(Socket, Ack)
+ end,
+ Loop(Parent, ListenSocket, Socket, Transport)
+ end.
+
+user_ping(_) ->
+ doc("The PING frame may be used to easily test a connection. (RFC7540 8.1.4)"),
{ok, OriginPid, OriginPort} = init_origin(tcp, http2, fun (_, _, Socket, Transport) ->
{ok, Data} = Transport:recv(Socket, 9, infinity),
<<Len:24, 6:8, %% PING
@@ -549,39 +568,65 @@ user_initiated_ping(_) ->
0:1, 0:31, Payload/binary>>,
ok = Transport:send(Socket, Ack)
end),
- {ok, Pid} = gun:open("localhost", OriginPort, #{
+ {ok, ConnPid} = gun:open("localhost", OriginPort, #{
protocols => [http2]
}),
- {ok, http2} = gun:await_up(Pid),
+ {ok, http2} = gun:await_up(ConnPid),
handshake_completed = receive_from(OriginPid),
- PingRef = gun:ping(Pid),
- receive
- {gun_notify, Pid, ping_ack, PingRef} ->
- ok
- after 1000 ->
- error(timeout)
- end,
- gun:close(Pid).
+ PingRef = gun:ping(ConnPid),
+ {notify, ping_ack, PingRef} = gun:await(ConnPid, undefined),
+ gun:close(ConnPid).
-do_ping_ack_loop_fun() ->
- %% Receive ping, sync with parent, send ping ack, loop.
- fun Loop(Parent, ListenSocket, Socket, Transport) ->
+user_ping_via_http(_) ->
+ doc("The PING frame may be used to easily test a connection. (RFC7540 8.1.4)"),
+ do_user_ping_tunnel(http).
+
+user_ping_via_https(_) ->
+ doc("The PING frame may be used to easily test a connection. (RFC7540 8.1.4)"),
+ do_user_ping_tunnel(https).
+
+user_ping_via_h2c(_) ->
+ doc("The PING frame may be used to easily test a connection. (RFC7540 8.1.4)"),
+ do_user_ping_tunnel(h2c).
+
+user_ping_via_h2(_) ->
+ doc("The PING frame may be used to easily test a connection. (RFC7540 8.1.4)"),
+ do_user_ping_tunnel(h2).
+
+do_user_ping_tunnel(ProxyType) ->
+ {ok, OriginPid, OriginPort} = init_origin(tcp, http2, fun (_, _, Socket, Transport) ->
{ok, Data} = Transport:recv(Socket, 9, infinity),
<<Len:24, 6:8, %% PING
0:8, %% Flags
0:1, 0:31>> = Data,
{ok, Payload} = Transport:recv(Socket, Len, 1000),
8 = Len = byte_size(Payload),
- Parent ! ping_received,
- receive
- send_ping_ack ->
- Ack = <<8:24, 6:8, %% PING
- 1:8, %% Ack flag
- 0:1, 0:31, Payload/binary>>,
- ok = Transport:send(Socket, Ack)
- end,
- Loop(Parent, ListenSocket, Socket, Transport)
- end.
+ Ack = <<8:24, 6:8, %% PING
+ 1:8, %% Ack flag
+ 0:1, 0:31, Payload/binary>>,
+ ok = Transport:send(Socket, Ack)
+ end),
+ {ok, ProxyPid, ProxyPort} = tunnel_SUITE:do_proxy_start(ProxyType),
+ {ProxyTransport, ProxyProtocol} = tunnel_SUITE:do_type(ProxyType),
+ {ok, ConnPid} = gun:open("localhost", ProxyPort, #{
+ transport => ProxyTransport,
+ tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}],
+ protocols => [ProxyProtocol]
+ }),
+ {ok, ProxyProtocol} = gun:await_up(ConnPid),
+ tunnel_SUITE:do_handshake_completed(ProxyProtocol, ProxyPid),
+ StreamRef = gun:connect(ConnPid, #{
+ host => "localhost",
+ port => OriginPort,
+ transport => tcp,
+ protocols => [http2]
+ }),
+ {response, fin, 200, _} = gun:await(ConnPid, StreamRef),
+ handshake_completed = receive_from(OriginPid),
+ {up, http2} = gun:await(ConnPid, StreamRef),
+ PingRef = gun:ping(ConnPid, #{tunnel => StreamRef}),
+ {notify, ping_ack, PingRef} = gun:await(ConnPid, undefined),
+ gun:close(ConnPid).
connect_http_via_h2c(_) ->
doc("CONNECT can be used to establish a TCP connection "