diff options
-rw-r--r-- | src/gun_http2.erl | 25 | ||||
-rw-r--r-- | test/rfc7540_SUITE.erl | 52 |
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. |