aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2020-09-25 13:52:31 +0200
committerLoïc Hoguin <[email protected]>2020-10-03 17:30:36 +0200
commit2f42047d6cec210186d703e31e5fd970b1ea4e33 (patch)
tree30c529955a79df3cb0cb13021c951ce655617f95
parent3ce70c5d96902e0e2718447383992dc7d6232e3a (diff)
downloadgun-2f42047d6cec210186d703e31e5fd970b1ea4e33.tar.gz
gun-2f42047d6cec210186d703e31e5fd970b1ea4e33.tar.bz2
gun-2f42047d6cec210186d703e31e5fd970b1ea4e33.zip
Add tunnel_SUITE testing all 3-layer combinations
The test suite is 216 tests with a tunnel created via two proxies leading to one origin server. The tests are for example socks5_h2_https where socks5 identifies the first SOCKS5 proxy, h2 the second HTTP/2 CONNECT proxy and https the secure HTTP/1.1 origin server. The test not only sets up the tunnel and does a request (or sends/receives data in the case of raw origin servers) but also confirms that the stream_info and info data is correct.
-rw-r--r--src/gun.erl156
-rw-r--r--src/gun_http.erl1
-rw-r--r--src/gun_http2.erl30
-rw-r--r--src/gun_socks.erl1
-rw-r--r--src/gun_tunnel.erl90
-rw-r--r--test/handlers/proxied_h.erl10
-rw-r--r--test/rfc7540_SUITE.erl2
-rw-r--r--test/tunnel_SUITE.erl1124
8 files changed, 1367 insertions, 47 deletions
diff --git a/src/gun.erl b/src/gun.erl
index ea599bf..4a0df0a 100644
--- a/src/gun.erl
+++ b/src/gun.erl
@@ -457,7 +457,10 @@ info(ServerPid) ->
<<"http">> -> tcp;
<<"https">> -> tls
end,
- origin_scheme => OriginScheme,
+ origin_scheme => case Protocol of
+ gun_raw -> undefined;
+ _ -> OriginScheme
+ end,
origin_host => OriginHost,
origin_port => OriginPort,
intermediaries => intermediaries_info(Intermediaries, []),
@@ -1096,7 +1099,11 @@ tls_handshake(internal, {tls_handshake, HandshakeEvent, Protocols, ReplyTo},
StreamRef = maps:get(stream_ref, HandshakeEvent, undefined),
case normal_tls_handshake(Socket, State0, HandshakeEvent, Protocols) of
{ok, TLSSocket, NewProtocol0, State} ->
- NewProtocol = gun_protocols:add_stream_ref(NewProtocol0, StreamRef),
+ NewProtocol1 = gun_protocols:add_stream_ref(NewProtocol0, StreamRef),
+ NewProtocol = case NewProtocol1 of
+ {NewProtocolName, NewProtocolOpts} -> {NewProtocolName, NewProtocolOpts#{tunnel_transport => tls}};
+ NewProtocolName -> {NewProtocolName, #{tunnel_transport => tls}}
+ end,
Protocol = gun_protocols:handler(NewProtocol),
ReplyTo ! {gun_tunnel_up, self(), StreamRef, Protocol:name()},
commands([
@@ -1127,7 +1134,11 @@ tls_handshake(info, {gun_tls_proxy, Socket, {ok, Negotiated}, {HandshakeEvent, P
State0=#state{socket=Socket, event_handler=EvHandler, event_handler_state=EvHandlerState0}) ->
NewProtocol0 = gun_protocols:negotiated(Negotiated, Protocols),
StreamRef = maps:get(stream_ref, HandshakeEvent, undefined),
- NewProtocol = gun_protocols:add_stream_ref(NewProtocol0, StreamRef),
+ NewProtocol1 = gun_protocols:add_stream_ref(NewProtocol0, StreamRef),
+ NewProtocol = case NewProtocol1 of
+ {NewProtocolName, NewProtocolOpts} -> {NewProtocolName, NewProtocolOpts#{tunnel_transport => tls}};
+ NewProtocolName -> {NewProtocolName, #{tunnel_transport => tls}}
+ end,
Protocol = gun_protocols:handler(NewProtocol),
ReplyTo ! {gun_tunnel_up, self(), StreamRef, Protocol:name()},
EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{
@@ -1218,7 +1229,8 @@ connected(cast, {headers, ReplyTo, StreamRef, Method, Path, Headers0, InitialFlo
event_handler=EvHandler, event_handler_state=EvHandlerState0}) ->
{Headers, State} = add_cookie_header(Path, Headers0, State0),
{ProtoState2, EvHandlerState} = Protocol:headers(ProtoState,
- StreamRef, ReplyTo, Method, Host, Port, Path, Headers,
+ dereference_stream_ref(StreamRef, State), ReplyTo,
+ Method, Host, Port, Path, Headers,
InitialFlow, EvHandler, EvHandlerState0),
{keep_state, State#state{protocol_state=ProtoState2, event_handler_state=EvHandlerState}};
connected(cast, {request, ReplyTo, StreamRef, Method, Path, Headers0, Body, InitialFlow},
@@ -1227,14 +1239,16 @@ connected(cast, {request, ReplyTo, StreamRef, Method, Path, Headers0, Body, Init
event_handler=EvHandler, event_handler_state=EvHandlerState0}) ->
{Headers, State} = add_cookie_header(Path, Headers0, State0),
{ProtoState2, EvHandlerState} = Protocol:request(ProtoState,
- StreamRef, ReplyTo, Method, Host, Port, Path, Headers, Body,
+ dereference_stream_ref(StreamRef, State), ReplyTo,
+ Method, Host, Port, Path, Headers, Body,
InitialFlow, EvHandler, EvHandlerState0),
{keep_state, State#state{protocol_state=ProtoState2, event_handler_state=EvHandlerState}};
connected(cast, {connect, ReplyTo, StreamRef, Destination, Headers, InitialFlow},
State=#state{origin_host=Host, origin_port=Port,
protocol=Protocol, protocol_state=ProtoState}) ->
%% @todo No events are currently handled for the CONNECT request?
- ProtoState2 = Protocol:connect(ProtoState, StreamRef, ReplyTo,
+ ProtoState2 = Protocol:connect(ProtoState,
+ dereference_stream_ref(StreamRef, State), ReplyTo,
Destination, #{host => Host, port => Port},
Headers, InitialFlow),
{keep_state, State#state{protocol_state=ProtoState2}};
@@ -1302,6 +1316,24 @@ add_cookie_header(PathWithQs, Headers0, State=#state{
end,
{Headers, State#state{cookie_store=Store}}.
+%% When the origin is using raw we do not dereference the stream_ref
+%% because it expects the full stream_ref to function (there's no
+%% other stream involved for this connection).
+dereference_stream_ref(StreamRef, #state{protocol=gun_raw}) ->
+ StreamRef;
+dereference_stream_ref(StreamRef, #state{intermediaries=Intermediaries}) ->
+ %% @todo It would be better to validate with the intermediary's stream_refs.
+ case length([http || #{protocol := http} <- Intermediaries]) of
+ 0 ->
+ StreamRef;
+ N ->
+ {_, Tail} = lists:split(N, StreamRef),
+ case Tail of
+ [SR] -> SR;
+ _ -> Tail
+ end
+ end.
+
%% Switch to the graceful connection close state.
closing(State=#state{protocol=Protocol, protocol_state=ProtoState,
event_handler=EvHandler, event_handler_state=EvHandlerState0}, Reason) ->
@@ -1329,7 +1361,8 @@ handle_common_connected(cast, {data, ReplyTo, StreamRef, IsFin, Data}, _,
State=#state{protocol=Protocol, protocol_state=ProtoState,
event_handler=EvHandler, event_handler_state=EvHandlerState0}) ->
{ProtoState2, EvHandlerState} = Protocol:data(ProtoState,
- StreamRef, ReplyTo, IsFin, Data, EvHandler, EvHandlerState0),
+ dereference_stream_ref(StreamRef, State),
+ ReplyTo, IsFin, Data, EvHandler, EvHandlerState0),
{keep_state, State#state{protocol_state=ProtoState2, event_handler_state=EvHandlerState}};
handle_common_connected(info, {timeout, TRef, Name}, _,
State=#state{protocol=Protocol, protocol_state=ProtoState}) ->
@@ -1364,8 +1397,9 @@ handle_common_connected_no_input(info,
Msg={gun_tls_proxy, _, _, {handle_continue, StreamRef, _, _}}, _,
State0=#state{protocol=Protocol, protocol_state=ProtoState,
event_handler=EvHandler, event_handler_state=EvHandlerState0}) ->
- {Commands, EvHandlerState} = Protocol:handle_continue(StreamRef, Msg,
- ProtoState, EvHandler, EvHandlerState0),
+ {Commands, EvHandlerState} = Protocol:handle_continue(
+ dereference_stream_ref(StreamRef, State0),
+ Msg, ProtoState, EvHandler, EvHandlerState0),
case commands(Commands, State0#state{event_handler_state=EvHandlerState}) of
{keep_state, State} ->
{keep_state, active(State)};
@@ -1390,8 +1424,9 @@ handle_common_connected_no_input(info,
handle_common_connected_no_input(info, {handle_continue, StreamRef, Msg}, _,
State0=#state{protocol=Protocol, protocol_state=ProtoState,
event_handler=EvHandler, event_handler_state=EvHandlerState0}) ->
- {Commands, EvHandlerState} = Protocol:handle_continue(StreamRef, Msg,
- ProtoState, EvHandler, EvHandlerState0),
+ {Commands, EvHandlerState} = Protocol:handle_continue(
+ dereference_stream_ref(StreamRef, State0),
+ Msg, ProtoState, EvHandler, EvHandlerState0),
case commands(Commands, State0#state{event_handler_state=EvHandlerState}) of
{keep_state, State} ->
{keep_state, active(State)};
@@ -1426,11 +1461,93 @@ handle_common_connected_no_input(cast, {cancel, ReplyTo, StreamRef}, _,
StreamRef, ReplyTo, EvHandler, EvHandlerState0),
{keep_state, State#state{protocol_state=ProtoState2, event_handler_state=EvHandlerState}};
handle_common_connected_no_input({call, From}, {stream_info, StreamRef}, _,
- #state{protocol=Protocol, protocol_state=ProtoState}) ->
- {keep_state_and_data, {reply, From, Protocol:stream_info(ProtoState, StreamRef)}};
+ State=#state{intermediaries=Intermediaries0, protocol=Protocol, protocol_state=ProtoState}) ->
+ Intermediaries = [I || I=#{protocol := http} <- Intermediaries0],
+ {keep_state_and_data, {reply, From,
+ if
+ %% The stream_ref refers to an intermediary.
+ %% @todo This is probably wrong.
+ length(StreamRef) =< length(Intermediaries) ->
+ Intermediary = lists:nth(length(StreamRef), lists:reverse(Intermediaries)),
+ {Intermediaries1, Tail} = lists:splitwith(
+ fun(Int) -> Int =/= Intermediary end,
+ lists:reverse(Intermediaries0)),
+ Tunnel = tunnel_info_from_intermediaries(State, Tail),
+ {ok, #{
+ ref => StreamRef,
+ reply_to => undefined, %% @todo
+ state => running,
+ intermediaries => intermediaries_info(lists:reverse(Intermediaries1), []),
+ tunnel => Tunnel
+ }};
+ is_reference(StreamRef), Intermediaries =/= [] ->
+ %% We take all intermediaries up to the first CONNECT intermediary.
+ {Intermediaries1, Tail} = lists:splitwith(
+ fun(#{protocol := P}) -> P =:= socks end,
+ lists:reverse(Intermediaries0)),
+ Tunnel = tunnel_info_from_intermediaries(State, Tail),
+ {ok, #{
+ ref => StreamRef,
+ reply_to => undefined, %% @todo
+ state => running,
+ intermediaries => intermediaries_info(lists:reverse(Intermediaries1), []),
+ tunnel => Tunnel
+ }};
+ true ->
+ {ok, Info0} = Protocol:stream_info(ProtoState, dereference_stream_ref(StreamRef, State)),
+ Info = Info0#{ref => StreamRef},
+ case Intermediaries0 of
+ [] ->
+ {ok, Info};
+ _ ->
+ Tail = maps:get(intermediaries, Info, []),
+ {ok, Info#{
+ intermediaries => intermediaries_info(Intermediaries0, []) ++ Tail
+ }}
+ end
+ end
+ }};
handle_common_connected_no_input(Type, Event, StateName, State) ->
handle_common(Type, Event, StateName, State).
+tunnel_info_from_intermediaries(State, Tail) ->
+ case Tail of
+ %% If the next endpoint is an intermediary take its infos.
+ [_, Intermediary|_] ->
+ #{
+ host := IntermediaryHost,
+ port := IntermediaryPort,
+ transport := IntermediaryTransport,
+ protocol := IntermediaryProtocol
+ } = Intermediary,
+ #{
+ transport => IntermediaryTransport,
+ protocol => IntermediaryProtocol,
+ origin_scheme => case IntermediaryTransport of
+ tcp -> <<"http">>;
+ tls -> <<"https">>
+ end,
+ origin_host => IntermediaryHost,
+ origin_port => IntermediaryPort
+ };
+ %% Otherwise take the infos from the state.
+ _ ->
+ tunnel_info_from_state(State)
+ end.
+
+tunnel_info_from_state(#state{origin_scheme=OriginScheme,
+ origin_host=OriginHost, origin_port=OriginPort, protocol=Proto}) ->
+ #{
+ transport => case OriginScheme of
+ <<"http">> -> tcp;
+ <<"https">> -> tls
+ end,
+ protocol => Proto:name(),
+ origin_scheme => OriginScheme,
+ origin_host => OriginHost,
+ origin_port => OriginPort
+ }.
+
%% Common events.
handle_common(cast, {set_owner, CurrentOwner, NewOwner}, _,
State=#state{owner=CurrentOwner, status={up, CurrentOwnerRef}}) ->
@@ -1544,7 +1661,7 @@ commands([{set_cookie, Authority, PathWithQs, _, Headers}|Tail], State=#state{
%% the transport and/or protocol in order to keep track
%% of the intermediaries properly.
commands([{origin, Scheme, Host, Port, Type}|Tail],
- State=#state{transport=Transport, protocol=Protocol,
+ State=#state{protocol=Protocol, origin_scheme=IntermediateScheme,
origin_host=IntermediateHost, origin_port=IntermediatePort, intermediaries=Intermediaries,
event_handler=EvHandler, event_handler_state=EvHandlerState0}) ->
EvHandlerState = EvHandler:origin_changed(#{
@@ -1557,7 +1674,10 @@ commands([{origin, Scheme, Host, Port, Type}|Tail],
type => Type,
host => IntermediateHost,
port => IntermediatePort,
- transport => Transport:name(),
+ transport => case IntermediateScheme of
+ <<"http">> -> tcp;
+ <<"https">> -> tls
+ end,
protocol => Protocol:name()
},
commands(Tail, State#state{origin_scheme=Scheme,
@@ -1577,7 +1697,11 @@ commands([{switch_transport, Transport, Socket}|Tail], State=#state{
commands([{switch_protocol, NewProtocol, ReplyTo}], State0=#state{
opts=Opts, socket=Socket, transport=Transport,
event_handler=EvHandler, event_handler_state=EvHandlerState0}) ->
- {Protocol, ProtoOpts} = gun_protocols:handler_and_opts(NewProtocol, Opts),
+ {Protocol, ProtoOpts0} = gun_protocols:handler_and_opts(NewProtocol, Opts),
+ ProtoOpts = case ProtoOpts0 of
+ #{tunnel_transport := _} -> ProtoOpts0;
+ _ -> ProtoOpts0#{tunnel_transport => tcp}
+ end,
{StateName, ProtoState} = Protocol:init(ReplyTo, Socket, Transport, ProtoOpts),
EvHandlerState = EvHandler:protocol_changed(#{protocol => Protocol:name()}, EvHandlerState0),
%% We cancel the existing keepalive and, depending on the protocol,
diff --git a/src/gun_http.erl b/src/gun_http.erl
index 064bf04..d2a758b 100644
--- a/src/gun_http.erl
+++ b/src/gun_http.erl
@@ -557,6 +557,7 @@ headers(State=#http_state{opts=Opts, out=head},
{new_stream(State#http_state{connection=Conn, out=Out}, StreamRef, ReplyTo,
Method, Authority, Path, InitialFlow), EvHandlerState}.
+%% @todo I don't think this clause is hit anymore. Same in other related callbacks.
request(State, StreamRef, ReplyTo, Method, Host, Port,
Path, Headers, Body, InitialFlow, EvHandler, EvHandlerState)
when is_list(StreamRef) ->
diff --git a/src/gun_http2.erl b/src/gun_http2.erl
index 61b315b..f8b507d 100644
--- a/src/gun_http2.erl
+++ b/src/gun_http2.erl
@@ -90,6 +90,10 @@
%% inside an HTTP/2 CONNECT stream.
base_stream_ref = undefined :: undefined | gun:stream_ref(),
+ %% Real transport for the HTTP/2 layer, defined when we are
+ %% in a non-HTTP/2 tunnel.
+ tunnel_transport = undefined :: undefined | tcp | tls,
+
%% Current status of the connection. We use this to ensure we are
%% not sending the GOAWAY frame more than once, and to validate
%% the server connection preface.
@@ -172,10 +176,11 @@ init(_ReplyTo, Socket, Transport, Opts0) ->
{ok, Preface, HTTP2Machine} = cow_http2_machine:init(client, Opts),
Handlers = maps:get(content_handlers, Opts, [gun_data_h]),
BaseStreamRef = maps:get(stream_ref, Opts, undefined),
+ TunnelTransport = maps:get(tunnel_transport, Opts, undefined),
%% @todo Better validate the preface being received.
State = #http2_state{socket=Socket, transport=Transport, opts=Opts,
- base_stream_ref=BaseStreamRef, content_handlers=Handlers,
- http2_machine=HTTP2Machine},
+ base_stream_ref=BaseStreamRef, tunnel_transport=TunnelTransport,
+ content_handlers=Handlers, http2_machine=HTTP2Machine},
Transport:send(Socket, Preface),
{connected, State}.
@@ -350,7 +355,10 @@ tunnel_commands([SetCookie={set_cookie, _, _, _, _}|Tail], Stream, State=#http2_
tunnel_commands(Tail, Stream, State#http2_state{commands_queue=[SetCookie|Queue]}).
continue_stream_ref(#http2_state{socket=#{handle_continue_stream_ref := ContinueStreamRef}}, StreamRef) ->
- ContinueStreamRef ++ [StreamRef];
+ case ContinueStreamRef of
+ [_|_] -> ContinueStreamRef ++ [StreamRef];
+ _ -> [ContinueStreamRef|StreamRef]
+ end;
continue_stream_ref(State, StreamRef) ->
stream_ref(State, StreamRef).
@@ -391,8 +399,8 @@ data_frame(State0, StreamID, IsFin, Data, EvHandler, EvHandlerState0,
{maybe_delete_stream(State, StreamID, remote, IsFin), EvHandlerState}.
%% @todo Make separate functions for inform/connect/normal.
-headers_frame(State0=#http2_state{transport=Transport, opts=Opts,
- content_handlers=Handlers0, commands_queue=Commands},
+headers_frame(State0=#http2_state{socket=Socket, transport=Transport, opts=Opts,
+ tunnel_transport=TunnelTransport, content_handlers=Handlers0, commands_queue=Commands},
StreamID, IsFin, Headers, #{status := Status}, _BodyLen,
EvHandler, EvHandlerState0) ->
Stream = get_stream_by_id(State0, StreamID),
@@ -463,7 +471,10 @@ headers_frame(State0=#http2_state{transport=Transport, opts=Opts,
stream_ref => RealStreamRef,
tunnel => #{
type => connect,
- transport_name => Transport:name(),
+ transport_name => case TunnelTransport of
+ undefined -> Transport:name();
+ _ -> TunnelTransport
+ end,
protocol_name => http2,
info => TunnelInfo,
handshake_event => HandshakeEvent,
@@ -476,7 +487,10 @@ headers_frame(State0=#http2_state{transport=Transport, opts=Opts,
stream_ref => RealStreamRef,
tunnel => #{
type => connect,
- transport_name => Transport:name(),
+ transport_name => case TunnelTransport of
+ undefined -> Transport:name();
+ _ -> TunnelTransport
+ end,
protocol_name => http2,
info => TunnelInfo,
new_protocol => NewProtocol
@@ -1045,7 +1059,7 @@ stream_info(State, StreamRef) when is_reference(StreamRef) ->
state => running,
tunnel => #{
transport => Transport,
- protocol => Proto:tunneled_name(ProtoState),
+ protocol => Proto:tunneled_name(ProtoState, true),
origin_scheme => case Transport of
tcp -> <<"http">>;
tls -> <<"https">>
diff --git a/src/gun_socks.erl b/src/gun_socks.erl
index cf5c41c..752daec 100644
--- a/src/gun_socks.erl
+++ b/src/gun_socks.erl
@@ -141,6 +141,7 @@ handle(<<5, 0, 0, Rest0/bits>>, #socks_state{ref=StreamRef, reply_to=ReplyTo, op
case Opts of
#{transport := tls} ->
HandshakeEvent0 = #{
+ reply_to => ReplyTo,
tls_opts => maps:get(tls_opts, Opts, []),
timeout => maps:get(tls_handshake_timeout, Opts, infinity)
},
diff --git a/src/gun_tunnel.erl b/src/gun_tunnel.erl
index 588317b..71ad137 100644
--- a/src/gun_tunnel.erl
+++ b/src/gun_tunnel.erl
@@ -31,7 +31,7 @@
-export([cancel/5]).
-export([timeout/3]).
-export([stream_info/2]).
--export([tunneled_name/1]).
+-export([tunneled_name/2]).
-export([down/1]).
%-export([ws_upgrade/10]).
@@ -51,6 +51,9 @@
%% the stream_ref is the same as the HTTP/1.1 CONNECT one.
stream_ref = undefined :: gun:stream_ref(),
+ %% The pid we send messages to.
+ reply_to = undefined :: pid(),
+
%% When the tunnel is a 'connect' tunnel we must dereference the
%% stream_ref. When it is 'socks' we must not as there was no
%% stream involved in creating the tunnel.
@@ -101,7 +104,7 @@ init(ReplyTo, OriginSocket, OriginTransport, Opts=#{stream_ref := StreamRef, tun
protocol_name := TunnelProtocol,
info := TunnelInfo
} = Tunnel,
- State = #tunnel_state{stream_ref=StreamRef, type=TunnelType,
+ State = #tunnel_state{stream_ref=StreamRef, reply_to=ReplyTo, type=TunnelType,
tunnel_transport=TunnelTransport, tunnel_protocol=TunnelProtocol,
info=TunnelInfo, opts=maps:without([stream_ref, tunnel], Opts)},
case Tunnel of
@@ -109,9 +112,17 @@ init(ReplyTo, OriginSocket, OriginTransport, Opts=#{stream_ref := StreamRef, tun
#{new_protocol := NewProtocol} ->
{Proto, ProtoOpts} = gun_protocols:handler_and_opts(NewProtocol, Opts),
{_, ProtoState} = Proto:init(ReplyTo, OriginSocket, OriginTransport,
- ProtoOpts#{stream_ref => StreamRef}),
+ ProtoOpts#{stream_ref => StreamRef, tunnel_transport => tcp}),
%% @todo EvHandlerState = EvHandler:protocol_changed(#{protocol => Protocol:name()}, EvHandlerState0),
- ReplyTo ! {gun_tunnel_up, self(), StreamRef, Proto:name()},
+ %% When the tunnel protocol is HTTP/1.1 or SOCKS
+ %% the gun_tunnel_up message was already sent.
+ %%
+ %% @todo There's probably a better way.
+ _ = case TunnelProtocol of
+ http -> ok;
+ socks -> ok;
+ _ -> ReplyTo ! {gun_tunnel_up, self(), StreamRef, Proto:name()}
+ end,
{tunnel, State#tunnel_state{socket=OriginSocket, transport=OriginTransport,
protocol=Proto, protocol_state=ProtoState}};
%% We can't initialize the protocol until the TLS handshake has completed.
@@ -172,11 +183,10 @@ handle_continue(ContinueStreamRef, {gun_tls_proxy, ProxyPid, {ok, Negotiated},
OriginSocket = #{
gun_pid => self(),
reply_to => ReplyTo,
- stream_ref => StreamRef%,
-% handle_continue_stream_ref => ContinueStreamRef
+ stream_ref => StreamRef
},
{_, ProtoState} = Proto:init(ReplyTo, OriginSocket, gun_tcp_proxy,
- ProtoOpts#{stream_ref => StreamRef}),
+ ProtoOpts#{stream_ref => StreamRef, tunnel_transport => tls}),
ReplyTo ! {gun_tunnel_up, self(), StreamRef, Proto:name()},
{{state, State#tunnel_state{protocol=Proto, protocol_state=ProtoState}}, EvHandlerState0};
handle_continue(ContinueStreamRef, {gun_tls_proxy, ProxyPid, {error, _Reason},
@@ -269,7 +279,18 @@ request(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0,
InitialFlow, EvHandler, EvHandlerState0),
{State#tunnel_state{protocol_state=ProtoState}, EvHandlerState}.
-%% We pass the data forward and optionally dereference StreamRef.
+%% When the next tunnel is SOCKS we pass the data forward directly.
+%% This is needed because SOCKS has no StreamRef and the data cannot
+%% therefore be passed forward through the usual method.
+data(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0,
+ protocol_origin={origin, _, _, _, socks5}},
+ StreamRef, ReplyTo, IsFin, Data, EvHandler, EvHandlerState0) ->
+ {ProtoState, EvHandlerState} = Proto:data(ProtoState0, StreamRef,
+ ReplyTo, IsFin, Data, EvHandler, EvHandlerState0),
+ {State#tunnel_state{protocol_state=ProtoState}, EvHandlerState};
+%% CONNECT tunnels pass the data forward and dereference StreamRef
+%% unless they are the recipient of the callback, in which case the
+%% data is sent to the socket.
data(State=#tunnel_state{socket=Socket, transport=Transport,
stream_ref=TunnelStreamRef0, protocol=Proto, protocol_state=ProtoState0},
StreamRef0, ReplyTo, IsFin, Data, EvHandler, EvHandlerState0) ->
@@ -286,11 +307,12 @@ data(State=#tunnel_state{socket=Socket, transport=Transport,
end.
%% We pass the CONNECT request forward and optionally dereference StreamRef.
-connect(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0},
- StreamRef0, ReplyTo, Destination, TunnelInfo, Headers, InitialFlow) ->
+connect(State=#tunnel_state{info=#{origin_host := Host, origin_port := Port},
+ protocol=Proto, protocol_state=ProtoState0},
+ StreamRef0, ReplyTo, Destination, _, Headers, InitialFlow) ->
StreamRef = maybe_dereference(State, StreamRef0),
ProtoState = Proto:connect(ProtoState0, StreamRef,
- ReplyTo, Destination, TunnelInfo, Headers, InitialFlow),
+ ReplyTo, Destination, #{host => Host, port => Port}, Headers, InitialFlow),
State#tunnel_state{protocol_state=ProtoState}.
%% @todo ?
@@ -302,6 +324,33 @@ cancel(_State, _StreamRef, _ReplyTo, _EvHandler, _EvHandlerState) ->
timeout(_State, {cow_http2_machine, _Name}, _TRef) ->
todo.
+stream_info(#tunnel_state{transport=Transport0, stream_ref=TunnelStreamRef, reply_to=ReplyTo,
+ tunnel_protocol=TunnelProtocol,
+ info=#{origin_host := OriginHost, origin_port := OriginPort},
+ protocol=Proto, protocol_state=ProtoState}, StreamRef)
+ when is_reference(StreamRef), TunnelProtocol =/= socks ->
+ Transport = case Transport0 of
+ gun_tcp_proxy -> tcp;
+ gun_tls_proxy -> tls
+ end,
+ {ok, #{
+ ref => TunnelStreamRef,
+ reply_to => ReplyTo,
+ state => running,
+ tunnel => #{
+ transport => Transport,
+ protocol => case Proto of
+ gun_tunnel -> Proto:tunneled_name(ProtoState, false);
+ _ -> Proto:name()
+ end,
+ origin_scheme => case Transport of
+ tcp -> <<"http">>;
+ tls -> <<"https">>
+ end,
+ origin_host => OriginHost,
+ origin_port => OriginPort
+ }
+ }};
stream_info(State=#tunnel_state{type=Type,
tunnel_transport=IntermediaryTransport, tunnel_protocol=IntermediaryProtocol,
info=TunnelInfo, protocol=Proto, protocol_state=ProtoState}, StreamRef0) ->
@@ -327,9 +376,11 @@ stream_info(State=#tunnel_state{type=Type,
}}
end.
-tunneled_name(#tunnel_state{protocol=Proto=gun_tunnel, protocol_state=ProtoState}) ->
- Proto:tunneled_name(ProtoState);
-tunneled_name(#tunnel_state{protocol=Proto}) ->
+tunneled_name(#tunnel_state{protocol=Proto=gun_tunnel, protocol_state=ProtoState}, true) ->
+ Proto:tunneled_name(ProtoState, false);
+tunneled_name(#tunnel_state{tunnel_protocol=TunnelProto}, false) ->
+ TunnelProto;
+tunneled_name(#tunnel_state{protocol=Proto}, _) ->
Proto:name().
%% @todo ?
@@ -394,13 +445,18 @@ commands([{switch_protocol, NewProtocol, ReplyTo}|Tail],
{_, ProtoState} = Proto:init(ReplyTo, OriginSocket, gun_tcp_proxy, ProtoOpts),
%% @todo EvHandlerState = EvHandler:protocol_changed(#{protocol => Protocol:name()}, EvHandlerState0),
commands(Tail, State#tunnel_state{protocol=Proto, protocol_state=ProtoState});
-commands([{tls_handshake, HandshakeEvent, Protocols, ReplyTo}|Tail],
+commands([{tls_handshake, HandshakeEvent0, Protocols, ReplyTo}|Tail],
State=#tunnel_state{transport=Transport,
info=#{origin_host := Host, origin_port := Port}, opts=Opts, protocol=CurrentProto,
protocol_origin={origin, _Scheme, OriginHost, OriginPort, Type}}) ->
#{
- stream_ref := StreamRef
- } = HandshakeEvent,
+ stream_ref := StreamRef,
+ tls_opts := TLSOpts0
+ } = HandshakeEvent0,
+ TLSOpts = gun:ensure_alpn_sni(Protocols, TLSOpts0, OriginHost),
+ HandshakeEvent = HandshakeEvent0#{
+ tls_opts => TLSOpts
+ },
ContinueStreamRef0 = continue_stream_ref(State),
ContinueStreamRef = case Type of
socks5 -> ContinueStreamRef0 ++ [make_ref()];
diff --git a/test/handlers/proxied_h.erl b/test/handlers/proxied_h.erl
index d5d4690..818823c 100644
--- a/test/handlers/proxied_h.erl
+++ b/test/handlers/proxied_h.erl
@@ -4,8 +4,8 @@
-export([init/2]).
-init(Req, State) ->
- {ok, cowboy_req:reply(200,
- #{<<"content-type">> => <<"text/plain">>},
- <<"TODO">>,
- Req), State}.
+-spec init(cowboy_req:req(), _) -> no_return().
+init(Req, _) ->
+ _ = cowboy_req:stream_reply(200, #{<<"content-type">> => <<"text/plain">>}, Req),
+ %% We never return to allow querying the stream_info.
+ receive after infinity -> ok end.
diff --git a/test/rfc7540_SUITE.erl b/test/rfc7540_SUITE.erl
index 4116ef4..9cb4ca2 100644
--- a/test/rfc7540_SUITE.erl
+++ b/test/rfc7540_SUITE.erl
@@ -632,7 +632,7 @@ do_connect_cowboy(_OriginScheme, OriginTransport, OriginProtocol, _ProxyScheme,
{response, nofin, 200, _} = gun:await(ConnPid, StreamRef),
{up, OriginProtocol} = gun:await(ConnPid, StreamRef),
ProxiedStreamRef = gun:get(ConnPid, "/proxied", #{}, #{tunnel => StreamRef}),
- timer:sleep(1000),
+ timer:sleep(1000), %% @todo Why?
{response, nofin, 200, _} = gun:await(ConnPid, ProxiedStreamRef),
%% We can create more requests on the proxy as well.
ProxyStreamRef = gun:get(ConnPid, "/"),
diff --git a/test/tunnel_SUITE.erl b/test/tunnel_SUITE.erl
new file mode 100644
index 0000000..190eb17
--- /dev/null
+++ b/test/tunnel_SUITE.erl
@@ -0,0 +1,1124 @@
+%% Copyright (c) 2020, 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(tunnel_SUITE).
+-compile(export_all).
+-compile(nowarn_export_all).
+
+-import(ct_helper, [doc/1]).
+-import(gun_test, [receive_from/1]).
+
+all() ->
+ ct_helper:all(?MODULE).
+
+%% Tests.
+%%
+%% Test names list the endpoint in the order the connection
+%% goes through, with proxies first and the origin server last.
+%% Each endpoint is identified by one of the following identifiers:
+%%
+%% Identifier | Protocol | Transport
+%% ---------- |----------|--------
+%% http | HTTP/1.1 | TCP
+%% https | HTTP/1.1 | TLS
+%% h2c | HTTP/2 | TCP
+%% h2 | HTTP/2 | TLS
+%% socks5 | SOCKS5 | TCP
+%% socks5tls | SOCKS5 | TLS
+%% raw | Raw | TCP
+%% rawtls | Raw | TLS
+
+http_http_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_http_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_http_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_http_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_http_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_http_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+http_https_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_https_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_https_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_https_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_https_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_https_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+http_h2c_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_h2c_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_h2c_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_h2c_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_h2c_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_h2c_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+http_h2_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_h2_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_h2_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_h2_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_h2_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_h2_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+http_socks5_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_socks5_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_socks5_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_socks5_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_socks5_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_socks5_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+http_socks5tls_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_socks5tls_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_socks5tls_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_socks5tls_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_socks5tls_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+http_socks5tls_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+https_http_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_http_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_http_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_http_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_http_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_http_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+https_https_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_https_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_https_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_https_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_https_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_https_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+https_h2c_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_h2c_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_h2c_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_h2c_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_h2c_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_h2c_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+https_h2_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_h2_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_h2_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_h2_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_h2_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_h2_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+https_socks5_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_socks5_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_socks5_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_socks5_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_socks5_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_socks5_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+https_socks5tls_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_socks5tls_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_socks5tls_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_socks5tls_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_socks5tls_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+https_socks5tls_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+h2c_http_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_http_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_http_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_http_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_http_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_http_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+h2c_https_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_https_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_https_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_https_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_https_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_https_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+h2c_h2c_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_h2c_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_h2c_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_h2c_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_h2c_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_h2c_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+h2c_h2_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_h2_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_h2_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_h2_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_h2_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_h2_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+h2c_socks5_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_socks5_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_socks5_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_socks5_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_socks5_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_socks5_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+h2c_socks5tls_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_socks5tls_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_socks5tls_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_socks5tls_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_socks5tls_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2c_socks5tls_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+h2_http_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_http_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_http_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_http_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_http_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_http_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+h2_https_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_https_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_https_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_https_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_https_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_https_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+h2_h2c_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_h2c_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_h2c_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_h2c_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_h2c_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_h2c_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+h2_h2_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_h2_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_h2_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_h2_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_h2_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_h2_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+h2_socks5_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_socks5_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_socks5_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_socks5_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_socks5_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_socks5_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+h2_socks5tls_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_socks5tls_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_socks5tls_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_socks5tls_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_socks5tls_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+h2_socks5tls_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+socks5_http_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_http_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_http_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_http_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_http_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_http_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+socks5_https_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_https_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_https_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_https_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_https_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_https_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+socks5_h2c_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_h2c_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_h2c_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_h2c_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_h2c_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_h2c_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+socks5_h2_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_h2_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_h2_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_h2_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_h2_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_h2_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+socks5_socks5_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_socks5_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_socks5_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_socks5_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_socks5_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_socks5_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+socks5_socks5tls_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_socks5tls_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_socks5tls_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_socks5tls_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_socks5tls_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5_socks5tls_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+socks5tls_http_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_http_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_http_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_http_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_http_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_http_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+socks5tls_https_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_https_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_https_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_https_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_https_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_https_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+socks5tls_h2c_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_h2c_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_h2c_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_h2c_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_h2c_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_h2c_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+socks5tls_h2_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_h2_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_h2_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_h2_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_h2_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_h2_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+socks5tls_socks5_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_socks5_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_socks5_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_socks5_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_socks5_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_socks5_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%%
+
+socks5tls_socks5tls_http(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_socks5tls_https(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_socks5tls_h2c(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_socks5tls_h2(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_socks5tls_raw(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+socks5tls_socks5tls_rawtls(_) ->
+ do_tunnel(?FUNCTION_NAME).
+
+%% Common code for all the test cases.
+
+-record(st, {
+ proxy1,
+ proxy1_pid,
+ proxy1_port,
+
+ proxy2,
+ proxy2_pid,
+ proxy2_port,
+
+ origin,
+ origin_pid,
+ origin_port
+}).
+
+do_tunnel(FunctionName) ->
+
+%dbg:tracer(),
+%dbg:tpl(gun, []),
+%dbg:tpl(gun_http, []),
+%dbg:tpl(gun_http2, []),
+%dbg:tpl(gun_tunnel, []),
+%dbg:p(all, c),
+
+%dbg:tracer(),
+%dbg:tpl(?MODULE, []),
+%dbg:p(all, c),
+
+ [Proxy1, Proxy2, Origin] = [list_to_atom(Lex) || Lex <- string:lexemes(atom_to_list(FunctionName), "_")],
+ do_doc(Proxy1, Proxy2, Origin),
+ {ok, OriginPid, OriginPort} = do_origin_start(Origin),
+ {ok, Proxy1Pid, Proxy1Port} = do_proxy_start(Proxy1),
+ {ok, Proxy2Pid, Proxy2Port} = do_proxy_start(Proxy2),
+ State = #st{
+ proxy1=Proxy1, proxy1_pid=Proxy1Pid, proxy1_port=Proxy1Port,
+ proxy2=Proxy2, proxy2_pid=Proxy2Pid, proxy2_port=Proxy2Port,
+ origin=Origin, origin_pid=OriginPid, origin_port=OriginPort
+ },
+ ConnPid = do_proxy1(State),
+ StreamRef1 = do_proxy2(State, ConnPid),
+ StreamRef2 = do_origin(State, ConnPid, StreamRef1),
+ StreamRef3 = do_origin_stream(State, ConnPid, StreamRef2),
+ do_proxy1_stream_info(State, ConnPid, StreamRef1),
+ do_proxy2_stream_info(State, ConnPid, StreamRef2),
+ do_origin_stream_info(State, ConnPid, StreamRef3),
+ do_info(State, ConnPid).
+
+do_doc(Proxy1, Proxy2, Origin) ->
+ doc(do_doc(Proxy1, "proxy") ++ " -> " ++ do_doc(Proxy2, "proxy") ++ " -> " ++ do_doc(Origin, "origin")).
+
+do_doc(Type, Endpoint) ->
+ {Transport, Protocol} = do_type(Type),
+ case Protocol of
+ http -> "HTTP/1.1";
+ http2 -> "HTTP/2";
+ socks -> "SOCKS5";
+ raw -> "Raw"
+ end
+ ++ " " ++ Endpoint ++ " over " ++
+ case Transport of
+ tcp -> "TCP";
+ tls -> "TLS"
+ end.
+
+do_origin_start(Type) when Type =:= raw; Type =:= rawtls ->
+ {Transport, Protocol} = do_type(Type),
+ gun_test:init_origin(Transport, Protocol, fun raw_SUITE:do_echo/3);
+do_origin_start(Type) ->
+ {Transport, Protocol} = do_type(Type),
+ rfc7540_SUITE:do_cowboy_origin(Transport, Protocol).
+
+do_proxy_start(Type) when Type =:= http; Type =:= https ->
+ {Transport, _} = do_type(Type),
+ rfc7231_SUITE:do_proxy_start(Transport);
+do_proxy_start(Type) when Type =:= h2; Type =:= h2c ->
+ {Transport, _} = do_type(Type),
+ rfc7540_SUITE:do_proxy_start(Transport);
+do_proxy_start(Type) when Type =:= socks5; Type =:= socks5tls ->
+ {Transport, _} = do_type(Type),
+ socks_SUITE:do_proxy_start(Transport, none).
+
+do_proxy1(State=#st{proxy1=Type, proxy1_pid=Proxy1Pid, proxy1_port=Port}) ->
+ {Transport, Protocol} = do_type(Type),
+ {ok, ConnPid} = gun:open("localhost", Port, #{
+ transport => Transport,
+ protocols => [case Protocol of
+ socks ->
+ {Protocol, do_proxy2_socks_opts(State)};
+ _ ->
+ Protocol
+ end]
+ }),
+ {ok, Protocol} = gun:await_up(ConnPid),
+ do_handshake_completed(Protocol, Proxy1Pid),
+ ConnPid.
+
+do_proxy2_socks_opts(State=#st{proxy2=Type, proxy2_port=Port}) ->
+ {Transport, Protocol} = do_type(Type),
+ #{
+ host => "localhost",
+ port => Port,
+ transport => Transport,
+ protocols => [case Protocol of
+ socks ->
+ {Protocol, do_origin_socks_opts(State)};
+ _ ->
+ Protocol
+ end]
+ }.
+
+do_origin_socks_opts(#st{origin=Type, origin_port=Port}) ->
+ {Transport, Protocol} = do_type(Type),
+ #{
+ host => "localhost",
+ port => Port,
+ transport => Transport,
+ protocols => [Protocol]
+ }.
+
+%% When the first proxy was socks all we need to do is wait for
+%% the second proxy to be up.
+do_proxy2(#st{proxy1=Proxy1Type, proxy2=Proxy2Type, proxy2_pid=Proxy2Pid}, ConnPid)
+ when Proxy1Type =:= socks5; Proxy1Type =:= socks5tls ->
+ {_, Protocol} = do_type(Proxy2Type),
+ {up, Protocol} = gun:await(ConnPid, undefined),
+ do_handshake_completed(Protocol, Proxy2Pid),
+ undefined;
+do_proxy2(State=#st{proxy2=Type, proxy2_pid=Proxy2Pid, proxy2_port=Port}, ConnPid) ->
+ {Transport, Protocol} = do_type(Type),
+ StreamRef1 = gun:connect(ConnPid, #{
+ host => "localhost",
+ port => Port,
+ transport => Transport,
+ protocols => [case Protocol of
+ socks ->
+ {Protocol, do_origin_socks_opts(State)};
+ _ ->
+ Protocol
+ end]
+ }),
+ %% @todo _IsFin is 'fin' for HTTP and 'nofin' for HTTP/2...
+ {response, _IsFin, 200, _} = gun:await(ConnPid, StreamRef1),
+ {up, Protocol} = gun:await(ConnPid, StreamRef1),
+ do_handshake_completed(Protocol, Proxy2Pid),
+ StreamRef1.
+
+%% When the second proxy was socks all we need to do is wait for
+%% the origin to be up.
+do_origin(#st{proxy2=Proxy2Type, origin=OriginType}, ConnPid, StreamRef)
+ when Proxy2Type =:= socks5; Proxy2Type =:= socks5tls ->
+ {_, Protocol} = do_type(OriginType),
+ {up, Protocol} = gun:await(ConnPid, StreamRef),
+ StreamRef;
+%% We can't have a socks5/socks5tls origin.
+do_origin(#st{origin=Type, origin_port=Port}, ConnPid, StreamRef1) ->
+ {Transport, Protocol} = do_type(Type),
+ StreamRef2 = gun:connect(ConnPid, #{
+ host => "localhost",
+ port => Port,
+ transport => Transport,
+ protocols => [Protocol]
+ }, [], #{tunnel => StreamRef1}),
+ %% @todo _IsFin is 'fin' for HTTP and 'nofin' for HTTP/2...
+ {response, _IsFin, 200, _} = gun:await(ConnPid, StreamRef2),
+ {up, Protocol} = gun:await(ConnPid, StreamRef2),
+ StreamRef2.
+
+do_handshake_completed(http2, ProxyPid) ->
+ handshake_completed = receive_from(ProxyPid),
+ ok;
+do_handshake_completed(_, _) ->
+ ok.
+
+do_origin_stream(#st{origin=Type}, ConnPid, StreamRef2)
+ when Type =:= raw; Type =:= rawtls ->
+ gun:data(ConnPid, StreamRef2, nofin, <<"Hello world!">>),
+ {data, nofin, <<"Hello world!">>} = gun:await(ConnPid, StreamRef2),
+ StreamRef2;
+do_origin_stream(#st{}, ConnPid, StreamRef2) ->
+ StreamRef3 = gun:get(ConnPid, "/proxied", #{}, #{tunnel => StreamRef2}),
+ {response, nofin, 200, _} = gun:await(ConnPid, StreamRef3),
+ StreamRef3.
+
+do_proxy1_stream_info(#st{proxy1=Proxy1}, _, _)
+ when Proxy1 =:= socks5; Proxy1 =:= socks5tls ->
+ ok;
+do_proxy1_stream_info(#st{proxy1=Proxy1, proxy2=Proxy2, proxy2_port=Proxy2Port}, ConnPid, StreamRef1) ->
+ ct:log("1: ~p~n~p", [StreamRef1, gun:stream_info(ConnPid, StreamRef1)]),
+ Self = if
+ %% We do not currently keep reply_to after switch_protocol.
+ Proxy1 =:= http; Proxy1 =:= https ->
+ undefined;
+ true ->
+ self()
+ end,
+ {Proxy2Transport, Proxy2Protocol} = do_type(Proxy2),
+ Proxy2Scheme = case Proxy2Transport of
+ tcp -> <<"http">>;
+ tls -> <<"https">>
+ end,
+ {ok, #{
+ ref := StreamRef1,
+ reply_to := Self,
+ state := running,
+ tunnel := #{
+ transport := Proxy2Transport,
+ protocol := Proxy2Protocol,
+ origin_scheme := Proxy2Scheme,
+ origin_host := "localhost",
+ origin_port := Proxy2Port
+ }
+ }} = gun:stream_info(ConnPid, StreamRef1),
+ ok.
+
+do_proxy2_stream_info(#st{proxy2=Proxy2}, _, _)
+ when Proxy2 =:= socks5; Proxy2 =:= socks5tls ->
+ ok;
+do_proxy2_stream_info(#st{proxy1=Proxy1, proxy1_port=Proxy1Port, proxy2=Proxy2,
+ origin=Origin, origin_port=OriginPort}, ConnPid, StreamRef2) ->
+ ct:log("2: ~p~n~p", [StreamRef2, gun:stream_info(ConnPid, StreamRef2)]),
+ Self = if
+ %% We do not currently keep reply_to after switch_protocol.
+ Proxy1 =/= h2, Proxy1 =/= h2c, (Proxy2 =:= http) orelse (Proxy2 =:= https) ->
+ undefined;
+ true ->
+ self()
+ end,
+ {Proxy1Transport, Proxy1Protocol} = do_type(Proxy1),
+ Proxy1Type = case Proxy1 of
+ socks5 -> socks5;
+ socks5tls -> socks5;
+ _ -> connect
+ end,
+ {OriginTransport, OriginProtocol} = do_type(Origin),
+ OriginScheme = case OriginTransport of
+ tcp -> <<"http">>;
+ tls -> <<"https">>
+ end,
+ {ok, #{
+ ref := StreamRef2,
+ reply_to := Self,
+ state := running,
+ intermediaries := [#{
+ type := Proxy1Type,
+ host := "localhost",
+ port := Proxy1Port,
+ transport := Proxy1Transport,
+ protocol := Proxy1Protocol
+ }],
+ tunnel := #{
+ transport := OriginTransport,
+ protocol := OriginProtocol,
+ origin_scheme := OriginScheme,
+ origin_host := "localhost",
+ origin_port := OriginPort
+ }
+ }} = gun:stream_info(ConnPid, StreamRef2),
+ ok.
+
+do_origin_stream_info(#st{origin=Type}, _, _)
+ when Type =:= raw; Type =:= rawtls ->
+ ok;
+do_origin_stream_info(#st{proxy1=Proxy1, proxy1_port=Proxy1Port,
+ proxy2=Proxy2, proxy2_port=Proxy2Port}, ConnPid, StreamRef3) ->
+ ct:log("3: ~p~n~p", [StreamRef3, gun:stream_info(ConnPid, StreamRef3)]),
+ {Proxy1Transport, Proxy1Protocol} = do_type(Proxy1),
+ Proxy1Type = case Proxy1 of
+ socks5 -> socks5;
+ socks5tls -> socks5;
+ _ -> connect
+ end,
+ {Proxy2Transport, Proxy2Protocol} = do_type(Proxy2),
+ Proxy2Type = case Proxy2 of
+ socks5 -> socks5;
+ socks5tls -> socks5;
+ _ -> connect
+ end,
+ {ok, #{
+ ref := StreamRef3,
+ reply_to := _, %% @todo
+ state := running,
+ intermediaries := [#{
+ type := Proxy1Type,
+ host := "localhost",
+ port := Proxy1Port,
+ transport := Proxy1Transport,
+ protocol := Proxy1Protocol
+ }, #{
+ type := Proxy2Type,
+ host := "localhost",
+ port := Proxy2Port,
+ transport := Proxy2Transport,
+ protocol := Proxy2Protocol
+ }]
+ }} = gun:stream_info(ConnPid, StreamRef3),
+ ok.
+
+do_info(#st{
+ proxy1=Proxy1, proxy1_port=Proxy1Port,
+ proxy2=Proxy2, proxy2_port=Proxy2Port,
+ origin=Origin, origin_port=OriginPort
+ }, ConnPid) ->
+ {Proxy1Transport, Proxy1Protocol} = do_type(Proxy1),
+ Proxy1Type = case Proxy1Protocol of
+ socks -> socks5;
+ _ -> connect
+ end,
+ {Proxy2Transport, Proxy2Protocol} = do_type(Proxy2),
+ Proxy2Type = case Proxy2Protocol of
+ socks -> socks5;
+ _ -> connect
+ end,
+ Intermediary1 = #{
+ type => Proxy1Type,
+ host => "localhost",
+ port => Proxy1Port,
+ transport => Proxy1Transport,
+ protocol => Proxy1Protocol
+ },
+ Intermediary2 = #{
+ type => Proxy2Type,
+ host => "localhost",
+ port => Proxy2Port,
+ transport => Proxy2Transport,
+ protocol => Proxy2Protocol
+ },
+ %% There are no connection-wide intermediaries for HTTP/2.
+ Intermediaries = case {Proxy1Protocol, Proxy2Protocol} of
+ {http2, _} -> [];
+ {_, http2} -> [Intermediary1];
+ _ -> [Intermediary1, Intermediary2]
+ end,
+ %% The transport, protocol, scheme and port of the origin
+ %% will also vary depending on where we started using HTTP/2 CONNECT.
+ %% In that case the connection-wide origin is the first HTTP/2 endpoint.
+ {OriginTransport, OriginProtocol} = do_type(Origin),
+ {InfoTransport, InfoProtocol, InfoPort} = case {Proxy1Protocol, Proxy2Protocol} of
+ {http2, _} -> {Proxy1Transport, Proxy1Protocol, Proxy1Port};
+ {_, http2} -> {Proxy2Transport, Proxy2Protocol, Proxy2Port};
+ _ -> {OriginTransport, OriginProtocol, OriginPort}
+ end,
+ InfoScheme = case {InfoTransport, InfoProtocol} of
+ {_, raw} -> undefined;
+ {tcp, _} -> <<"http">>;
+ {tls, _} -> <<"https">>
+ end,
+ #{
+ transport := InfoTransport,
+ protocol := InfoProtocol,
+ origin_scheme := InfoScheme,
+ origin_host := "localhost",
+ origin_port := InfoPort,
+ intermediaries := Intermediaries
+ } = gun:info(ConnPid),
+ ok.
+
+do_type(http) -> {tcp, http};
+do_type(https) -> {tls, http};
+do_type(h2c) -> {tcp, http2};
+do_type(h2) -> {tls, http2};
+do_type(socks5) -> {tcp, socks};
+do_type(socks5tls) -> {tls, socks};
+do_type(raw) -> {tcp, raw};
+do_type(rawtls) -> {tls, raw}.