From 510d49d8ef0a46e90374c3230f28b5354115293f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Fri, 17 Jul 2020 12:25:11 +0200 Subject: Make gun:stream_info/2 return intermediaries for HTTP/2 CONNECT --- src/gun.erl | 11 +++++--- src/gun_http.erl | 6 ++--- src/gun_http2.erl | 69 ++++++++++++++++++++++++++++++++++++-------------- test/raw_SUITE.erl | 6 ++++- test/rfc7540_SUITE.erl | 16 ++++++++++-- 5 files changed, 79 insertions(+), 29 deletions(-) diff --git a/src/gun.erl b/src/gun.erl index 0a0560d..8556b92 100644 --- a/src/gun.erl +++ b/src/gun.erl @@ -166,8 +166,8 @@ type := connect | socks5, host := inet:hostname() | inet:ip_address(), port := inet:port_number(), - transport := tcp | tls | tls_proxy, - protocol := http | http2 | raw | socks + transport := tcp | tls, + protocol := http | socks }. -type raw_opts() :: #{}. @@ -1212,9 +1212,12 @@ connected(cast, {request, ReplyTo, StreamRef, Method, Path, Headers0, Body, Init InitialFlow, EvHandler, EvHandlerState0), {keep_state, State#state{protocol_state=ProtoState2, event_handler_state=EvHandlerState}}; connected(cast, {connect, ReplyTo, StreamRef, Destination, Headers, InitialFlow}, - State=#state{protocol=Protocol, protocol_state=ProtoState}) -> + State=#state{origin_host=Host, origin_port=Port, + protocol=Protocol, protocol_state=ProtoState}) -> %% @todo Not events are currently handled for the request? - ProtoState2 = Protocol:connect(ProtoState, StreamRef, ReplyTo, Destination, Headers, InitialFlow), + ProtoState2 = Protocol:connect(ProtoState, StreamRef, ReplyTo, + Destination, #{host => Host, port => Port}, + Headers, InitialFlow), {keep_state, State#state{protocol_state=ProtoState2}}; %% Public Websocket interface. %% @todo Maybe make an interface in the protocol module instead of checking on protocol name. diff --git a/src/gun_http.erl b/src/gun_http.erl index b5ad751..aae47cf 100644 --- a/src/gun_http.erl +++ b/src/gun_http.erl @@ -29,7 +29,7 @@ -export([headers/11]). -export([request/12]). -export([data/7]). --export([connect/6]). +-export([connect/7]). -export([cancel/5]). -export([stream_info/2]). -export([down/1]). @@ -683,12 +683,12 @@ data(State=#http_state{socket=Socket, transport=Transport, version=Version, {error_stream_not_found(State, StreamRef, ReplyTo), EvHandlerState0} end. -connect(State=#http_state{streams=Streams}, StreamRef, ReplyTo, _, _, _) when Streams =/= [] -> +connect(State=#http_state{streams=Streams}, StreamRef, ReplyTo, _, _, _, _) when Streams =/= [] -> ReplyTo ! {gun_error, self(), StreamRef, {badstate, "CONNECT can only be used with HTTP/1.1 when no other streams are active."}}, State; connect(State=#http_state{socket=Socket, transport=Transport, opts=Opts, version=Version}, - StreamRef, ReplyTo, Destination=#{host := Host0}, Headers0, InitialFlow0) -> + StreamRef, ReplyTo, Destination=#{host := Host0}, _TunnelInfo, Headers0, InitialFlow0) -> Host = case Host0 of Tuple when is_tuple(Tuple) -> inet:ntoa(Tuple); _ -> Host0 diff --git a/src/gun_http2.erl b/src/gun_http2.erl index c4668d7..0dad3d9 100644 --- a/src/gun_http2.erl +++ b/src/gun_http2.erl @@ -29,13 +29,23 @@ -export([headers/11]). -export([request/12]). -export([data/7]). --export([connect/6]). +-export([connect/7]). -export([cancel/5]). -export([timeout/3]). -export([stream_info/2]). -export([down/1]). %-export([ws_upgrade/10]). +-type tunnel_info() :: #{ + %% Tunnel. + host := inet:hostname() | inet:ip_address(), + port := inet:port_number(), + + %% Origin. + origin_host => inet:hostname() | inet:ip_address(), + origin_port => inet:port_number() +}. + -record(stream, { id = undefined :: cow_http2:streamid(), @@ -56,7 +66,9 @@ handler_state :: undefined | gun_content_handler:state(), %% CONNECT tunnel. - tunnel :: {module(), any(), gun:connect_destination()} | {setup, gun:connect_destination()} | undefined + tunnel :: {module(), any(), tunnel_info()} + | {setup, gun:connect_destination(), tunnel_info()} + | undefined }). -record(http2_state, { @@ -300,15 +312,14 @@ maybe_ack(State=#http2_state{socket=Socket, transport=Transport}, Frame) -> end, State. -%% @todo CONNECT streams may need to pass data through TLS socket. data_frame(State, StreamID, IsFin, Data, EvHandler, EvHandlerState0) -> case get_stream_by_id(State, StreamID) of Stream=#stream{tunnel=undefined} -> data_frame(State, StreamID, IsFin, Data, EvHandler, EvHandlerState0, Stream); - Stream=#stream{tunnel={Protocol, ProtoState0, Destination}} -> + Stream=#stream{tunnel={Protocol, ProtoState0, TunnelInfo}} -> {ProtoState, EvHandlerState} = Protocol:handle(Data, ProtoState0, EvHandler, EvHandlerState0), - {store_stream(State, Stream#stream{tunnel={Protocol, ProtoState, Destination}}), + {store_stream(State, Stream#stream{tunnel={Protocol, ProtoState, TunnelInfo}}), EvHandlerState} end. @@ -380,7 +391,7 @@ headers_frame(State0=#http2_state{content_handlers=Handlers0, commands_queue=Com headers => Headers }, EvHandlerState0), %% @todo Handle TLS over TCP and TLS over TLS. - {setup, Destination} = Tunnel, + {setup, Destination=#{host := DestHost, port := DestPort}, TunnelInfo} = Tunnel, tcp = maps:get(transport, Destination, tcp), [Protocol0] = maps:get(protocols, Destination, [http]), %% Options are either passed directly or #{} is used. Since the @@ -399,7 +410,8 @@ headers_frame(State0=#http2_state{content_handlers=Handlers0, commands_queue=Com {_, ProtoState} = Protocol:init(ReplyTo, OriginSocket, OriginTransport, ProtoOpts), %% @todo EvHandlerState = EvHandler:protocol_changed(#{protocol => Protocol:name()}, EvHandlerState0), %% @todo What about keepalive? - {store_stream(State, Stream#stream{tunnel={Protocol, ProtoState, Destination}}), + {store_stream(State, Stream#stream{tunnel={Protocol, ProtoState, + TunnelInfo#{origin_host => DestHost, origin_port => DestPort}}}), EvHandlerState}; true -> ReplyTo ! {gun_response, self(), StreamRef, IsFin, Status, Headers}, @@ -698,12 +710,13 @@ request(State0=#http2_state{socket=Socket, transport=Transport, opts=Opts, request(State, [StreamRef|Tail], ReplyTo, Method, _Host, _Port, Path, Headers, Body, InitialFlow, EvHandler, EvHandlerState0) -> case get_stream_by_ref(State, StreamRef) of - Stream=#stream{tunnel={Proto, ProtoState0, Destination=#{host := OriginHost, port := OriginPort}}} -> + Stream=#stream{tunnel={Proto, ProtoState0, TunnelInfo=#{ + origin_host := OriginHost, origin_port := OriginPort}}} -> %% @todo So the event is probably not giving the right StreamRef? {ProtoState, EvHandlerState} = Proto:request(ProtoState0, normalize_stream_ref(Tail), ReplyTo, Method, OriginHost, OriginPort, Path, Headers, Body, InitialFlow, EvHandler, EvHandlerState0), - {store_stream(State, Stream#stream{tunnel={Proto, ProtoState, Destination}}), EvHandlerState}; + {store_stream(State, Stream#stream{tunnel={Proto, ProtoState, TunnelInfo}}), EvHandlerState}; #stream{tunnel=undefined} -> ReplyTo ! {gun_error, self(), StreamRef, {badstate, "The stream is not a tunnel."}}, @@ -774,10 +787,10 @@ data(State=#http2_state{http2_machine=HTTP2Machine}, StreamRef, ReplyTo, IsFin, %% Tunneled data. data(State, [StreamRef|Tail], ReplyTo, IsFin, Data, EvHandler, EvHandlerState0) -> case get_stream_by_ref(State, StreamRef) of - Stream=#stream{tunnel={Proto, ProtoState0, Destination}} -> + Stream=#stream{tunnel={Proto, ProtoState0, TunnelInfo}} -> {ProtoState, EvHandlerState} = Proto:data(ProtoState0, normalize_stream_ref(Tail), ReplyTo, IsFin, Data, EvHandler, EvHandlerState0), - {store_stream(State, Stream#stream{tunnel={Proto, ProtoState, Destination}}), EvHandlerState}; + {store_stream(State, Stream#stream{tunnel={Proto, ProtoState, TunnelInfo}}), EvHandlerState}; #stream{tunnel=undefined} -> ReplyTo ! {gun_error, self(), StreamRef, {badstate, "The stream is not a tunnel."}}, @@ -854,8 +867,8 @@ reset_stream(State0=#http2_state{socket=Socket, transport=Transport}, end. connect(State=#http2_state{socket=Socket, transport=Transport, opts=Opts, - http2_machine=HTTP2Machine0}, StreamRef, ReplyTo, Destination=#{host := Host0}, - Headers0, InitialFlow0) -> + http2_machine=HTTP2Machine0}, StreamRef, ReplyTo, + Destination=#{host := Host0}, TunnelInfo, Headers0, InitialFlow0) -> Host = case Host0 of Tuple when is_tuple(Tuple) -> inet:ntoa(Tuple); _ -> Host0 @@ -885,7 +898,7 @@ connect(State=#http2_state{socket=Socket, transport=Transport, opts=Opts, Transport:send(Socket, cow_http2:headers(StreamID, nofin, HeaderBlock)), InitialFlow = initial_flow(InitialFlow0, Opts), Stream = #stream{id=StreamID, ref=StreamRef, reply_to=ReplyTo, flow=InitialFlow, - authority=Authority, path= <<>>, tunnel={setup, Destination}}, + authority=Authority, path= <<>>, tunnel={setup, Destination, TunnelInfo}}, create_stream(State#http2_state{http2_machine=HTTP2Machine}, Stream). cancel(State=#http2_state{socket=Socket, transport=Transport, http2_machine=HTTP2Machine0}, @@ -917,7 +930,8 @@ timeout(State=#http2_state{http2_machine=HTTP2Machine0}, {cow_http2_machine, Nam stream_info(State, StreamRef) when is_reference(StreamRef) -> case get_stream_by_ref(State, StreamRef) of - #stream{reply_to=ReplyTo, tunnel={Protocol, _, #{host := OriginHost, port := OriginPort}}} -> + #stream{reply_to=ReplyTo, tunnel={Protocol, _, #{ + origin_host := OriginHost, origin_port := OriginPort}}} -> {ok, #{ ref => StreamRef, reply_to => ReplyTo, @@ -940,17 +954,34 @@ stream_info(State, StreamRef) when is_reference(StreamRef) -> {ok, undefined} end; %% Tunneled streams. -stream_info(State, StreamRefList=[StreamRef|Tail]) -> +stream_info(State=#http2_state{transport=Transport}, StreamRefList=[StreamRef|Tail]) -> case get_stream_by_ref(State, StreamRef) of - #stream{tunnel={Protocol, ProtoState, _}} -> + #stream{tunnel={Protocol, ProtoState, #{host := TunnelHost, port := TunnelPort}}} -> %% We must return the real StreamRef as seen by the user. %% We therefore set it on return, with the outer layer "winning". - %% @todo Would be well worth returning intermediaries as well. + %% + %% We also add intermediaries which are prepended to the list and + %% therefore are ultimately given from outer to inner layer just + %% like gun:info/1 intermediaries. case Protocol:stream_info(ProtoState, normalize_stream_ref(Tail)) of {ok, undefined} -> {ok, undefined}; {ok, Info} -> - {ok, Info#{ref => StreamRefList}} + Intermediaries = maps:get(intermediaries, Info, []), + {ok, Info#{ + ref => StreamRefList, + intermediaries => [#{ + type => connect, + host => TunnelHost, + port => TunnelPort, + transport => case Transport:name() of + tcp_proxy -> tcp; + tls_proxy -> tls; + TransportName -> TransportName + end, + protocol => http2 + }|Intermediaries] + }} end; error -> {ok, undefined} diff --git a/test/raw_SUITE.erl b/test/raw_SUITE.erl index 9b836c0..c3eec1d 100644 --- a/test/raw_SUITE.erl +++ b/test/raw_SUITE.erl @@ -187,9 +187,13 @@ connect_raw_reply_to(_) -> receive {ReplyTo, ok} -> gun:close(ConnPid) after 1000 -> error(timeout) end. h2_connect_tcp_raw_tcp(_) -> - doc("Use HTTP/2 CONNECT over TCP to connect to a remote endpoint using the raw protocol over TCP."), + doc("Use CONNECT over clear HTTP/2 to connect to a remote endpoint using the raw protocol over TCP."), do_h2_connect_raw(tcp, tcp). +h2_connect_tls_raw_tcp(_) -> + doc("Use CONNECT over secure HTTP/2 to connect to a remote endpoint using the raw protocol over TCP."), + do_h2_connect_raw(tcp, tls). + do_h2_connect_raw(OriginTransport, ProxyTransport) -> {ok, OriginPid, OriginPort} = init_origin(OriginTransport, raw, fun do_echo/3), {ok, ProxyPid, ProxyPort} = rfc7540_SUITE:do_proxy_start(ProxyTransport, [ diff --git a/test/rfc7540_SUITE.erl b/test/rfc7540_SUITE.erl index 09a0923..ac36ee6 100644 --- a/test/rfc7540_SUITE.erl +++ b/test/rfc7540_SUITE.erl @@ -62,7 +62,7 @@ do_proxy_init(Proxy=#proxy{parent=Parent, transport=Transport}) -> gen_tcp:listen(0, [binary, {active, false}]); gun_tls -> Opts = ct_helper:get_certs_from_ets(), - ssl:listen(0, [binary, {active, false}|Opts]) + ssl:listen(0, [binary, {active, false}, {alpn_preferred_protocols, [<<"h2">>]}|Opts]) end, {ok, {_, Port}} = Transport:sockname(ListenSocket), Parent ! {self(), Port}, @@ -435,6 +435,11 @@ connect_http(_) -> "to an HTTP/1.1 server via a TCP HTTP/2 proxy. (RFC7540 8.3)"), do_connect_http(<<"http">>, tcp, <<"http">>, tcp). +connect_https(_) -> + doc("CONNECT can be used to establish a TCP connection " + "to an HTTP/1.1 server via a TLS HTTP/2 proxy. (RFC7540 8.3)"), + do_connect_http(<<"http">>, tcp, <<"https">>, tls). + do_connect_http(OriginScheme, OriginTransport, ProxyScheme, ProxyTransport) -> {ok, OriginPid, OriginPort} = init_origin(OriginTransport, http), {ok, ProxyPid, ProxyPort} = do_proxy_start(ProxyTransport, [ @@ -485,6 +490,13 @@ do_connect_http(OriginScheme, OriginTransport, ProxyScheme, ProxyTransport) -> {ok, #{ ref := ProxiedStreamRef, reply_to := Self, - state := running + state := running, + intermediaries := [#{ + type := connect, + host := "localhost", + port := ProxyPort, + transport := ProxyTransport, + protocol := http2 + }] }} = gun:stream_info(ConnPid, ProxiedStreamRef), gun:close(ConnPid). -- cgit v1.2.3