aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2020-08-27 17:17:00 +0200
committerLoïc Hoguin <[email protected]>2020-09-21 15:51:58 +0200
commite740356b5881c39a95715d6081689802edf469a0 (patch)
treee87c317b75c881f8368ce9944650c1a5054177c1
parent839f31e97504e10b8eef4897aa1271a53137cc29 (diff)
downloadgun-e740356b5881c39a95715d6081689802edf469a0.tar.gz
gun-e740356b5881c39a95715d6081689802edf469a0.tar.bz2
gun-e740356b5881c39a95715d6081689802edf469a0.zip
Add test HTTP/2 CONNECT -> HTTP/1.1 CONNECT -> origin
Implements gun:connect with a tunnel.
-rw-r--r--src/gun_http2.erl25
-rw-r--r--test/rfc7540_SUITE.erl52
2 files changed, 72 insertions, 5 deletions
diff --git a/src/gun_http2.erl b/src/gun_http2.erl
index 3687946..252aaec 100644
--- a/src/gun_http2.erl
+++ b/src/gun_http2.erl
@@ -362,14 +362,13 @@ tunnel_commands([{origin, _, NewHost, NewPort, Type}|Tail], Stream, Protocol, Tu
tunnel_commands([{switch_protocol, NewProtocol, ReplyTo}|Tail], Stream=#stream{ref=StreamRef},
_CurrentProtocol, TunnelInfo, State=#http2_state{opts=Opts}) ->
{Protocol, ProtoOpts} = gun_protocols:handler_and_opts(NewProtocol, Opts),
- RealStreamRef = stream_ref(State, StreamRef),
OriginSocket = #{
gun_pid => self(),
reply_to => ReplyTo,
- stream_ref => RealStreamRef
+ stream_ref => stream_ref(State, StreamRef)
},
OriginTransport = gun_tcp_proxy,
- {_, ProtoState} = Protocol:init(ReplyTo, OriginSocket, OriginTransport, ProtoOpts#{stream_ref => RealStreamRef}),
+ {_, ProtoState} = Protocol:init(ReplyTo, OriginSocket, OriginTransport, ProtoOpts),
%% @todo EvHandlerState = EvHandler:protocol_changed(#{protocol => Protocol:name()}, EvHandlerState0),
tunnel_commands([{state, ProtoState}|Tail], Stream, Protocol, TunnelInfo, State);
tunnel_commands([{active, true}|Tail], Stream, Protocol, TunnelInfo, State) ->
@@ -1043,7 +1042,8 @@ reset_stream(State0=#http2_state{socket=Socket, transport=Transport},
connect(State=#http2_state{socket=Socket, transport=Transport, opts=Opts,
http2_machine=HTTP2Machine0}, StreamRef, ReplyTo,
- Destination=#{host := Host0}, TunnelInfo, Headers0, InitialFlow0) ->
+ Destination=#{host := Host0}, TunnelInfo, Headers0, InitialFlow0)
+ when is_reference(StreamRef) ->
Host = case Host0 of
Tuple when is_tuple(Tuple) -> inet:ntoa(Tuple);
_ -> Host0
@@ -1074,7 +1074,22 @@ connect(State=#http2_state{socket=Socket, transport=Transport, opts=Opts,
InitialFlow = initial_flow(InitialFlow0, Opts),
Stream = #stream{id=StreamID, ref=StreamRef, reply_to=ReplyTo, flow=InitialFlow,
authority=Authority, path= <<>>, tunnel={setup, Destination, TunnelInfo}},
- create_stream(State#http2_state{http2_machine=HTTP2Machine}, Stream).
+ create_stream(State#http2_state{http2_machine=HTTP2Machine}, Stream);
+%% Tunneled request.
+connect(State, [StreamRef|Tail], ReplyTo, Destination, TunnelInfo, Headers0, InitialFlow) ->
+ case get_stream_by_ref(State, StreamRef) of
+ %% @todo We should send an error to the user if the stream isn't ready.
+ Stream=#stream{tunnel={Proto, ProtoState0, ProtoTunnelInfo}} ->
+ ProtoState = Proto:connect(ProtoState0, normalize_stream_ref(Tail),
+ ReplyTo, Destination, TunnelInfo, Headers0, InitialFlow),
+ store_stream(State, Stream#stream{tunnel={Proto, ProtoState, ProtoTunnelInfo}});
+ #stream{tunnel=undefined} ->
+ ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef), {badstate,
+ "The stream is not a tunnel."}},
+ State;
+ error ->
+ error_stream_not_found(State, StreamRef, ReplyTo)
+ end.
cancel(State=#http2_state{socket=Socket, transport=Transport, http2_machine=HTTP2Machine0},
StreamRef, ReplyTo, EvHandler, EvHandlerState0) ->
diff --git a/test/rfc7540_SUITE.erl b/test/rfc7540_SUITE.erl
index 8669b13..c8aff46 100644
--- a/test/rfc7540_SUITE.erl
+++ b/test/rfc7540_SUITE.erl
@@ -648,3 +648,55 @@ do_cowboy_origin(OriginTransport, OriginProtocol) ->
tls -> gun_test:init_cowboy_tls(Ref, ProtoOpts, [])
end,
{ok, Ref, Port}.
+
+connect_http_via_http_via_h2c(_) ->
+ doc("CONNECT can be used to establish a TCP connection "
+ "to an HTTP/1.1 server via a tunnel going through both "
+ "an HTTP/2 and an HTTP/1.1 proxy. (RFC7231 4.3.6)"),
+ do_connect_via_multiple_proxies(tcp, http, tcp, http, tcp).
+
+do_connect_via_multiple_proxies(OriginTransport, OriginProtocol,
+ Proxy2Transport, Proxy2Protocol, Proxy1Transport) ->
+ {ok, Ref, OriginPort} = do_cowboy_origin(OriginTransport, OriginProtocol),
+ try
+ {ok, Proxy1Pid, Proxy1Port} = do_proxy_start(Proxy1Transport, [
+ #proxy_stream{id=1, status=200}
+ ]),
+ {ok, Proxy2Pid, Proxy2Port} = rfc7231_SUITE:do_proxy_start(Proxy2Transport),
+ %% First proxy.
+ {ok, ConnPid} = gun:open("localhost", Proxy1Port, #{
+ protocols => [http2]
+ }),
+ {ok, http2} = gun:await_up(ConnPid),
+ %% Second proxy.
+ StreamRef1 = gun:connect(ConnPid, #{
+ host => "localhost",
+ port => Proxy2Port,
+ protocols => [Proxy2Protocol]
+ }, []),
+ handshake_completed = receive_from(Proxy1Pid),
+ Authority1 = iolist_to_binary(["localhost:", integer_to_binary(Proxy2Port)]),
+ {request, #{
+ <<":method">> := <<"CONNECT">>,
+ <<":authority">> := Authority1
+ }} = receive_from(Proxy1Pid),
+ {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1),
+ {up, Proxy2Protocol} = gun:await(ConnPid, StreamRef1),
+ %% Origin.
+ StreamRef2 = gun:connect(ConnPid, #{
+ host => "localhost",
+ port => OriginPort,
+ protocols => [OriginProtocol]
+ }, [], #{tunnel => StreamRef1}),
+ Authority2 = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]),
+ {request, <<"CONNECT">>, Authority2, 'HTTP/1.1', _} = receive_from(Proxy2Pid),
+ %% @todo OK there's a mismatch between HTTP/1.1 (fin) and HTTP/2 (nofin).
+ {response, fin, 200, _} = gun:await(ConnPid, StreamRef2),
+ {up, OriginProtocol} = gun:await(ConnPid, StreamRef2),
+ %% Tunneled request to the origin.
+ ProxiedStreamRef = gun:get(ConnPid, "/proxied", [], #{tunnel => StreamRef2}),
+ {response, nofin, 200, _} = gun:await(ConnPid, ProxiedStreamRef),
+ gun:close(ConnPid)
+ after
+ cowboy:stop_listener(Ref)
+ end.