diff options
author | Loïc Hoguin <[email protected]> | 2020-09-25 13:52:31 +0200 |
---|---|---|
committer | Loïc Hoguin <[email protected]> | 2020-10-03 17:30:36 +0200 |
commit | 2f42047d6cec210186d703e31e5fd970b1ea4e33 (patch) | |
tree | 30c529955a79df3cb0cb13021c951ce655617f95 | |
parent | 3ce70c5d96902e0e2718447383992dc7d6232e3a (diff) | |
download | gun-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.erl | 156 | ||||
-rw-r--r-- | src/gun_http.erl | 1 | ||||
-rw-r--r-- | src/gun_http2.erl | 30 | ||||
-rw-r--r-- | src/gun_socks.erl | 1 | ||||
-rw-r--r-- | src/gun_tunnel.erl | 90 | ||||
-rw-r--r-- | test/handlers/proxied_h.erl | 10 | ||||
-rw-r--r-- | test/rfc7540_SUITE.erl | 2 | ||||
-rw-r--r-- | test/tunnel_SUITE.erl | 1124 |
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}. |