aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2020-07-17 12:25:11 +0200
committerLoïc Hoguin <[email protected]>2020-09-21 15:51:56 +0200
commit510d49d8ef0a46e90374c3230f28b5354115293f (patch)
tree2260d16089803e9e6e9c3e9156e21f83b0eae81c
parenta093bf88e1740e4f89937d84cd4d5b26cb5b4e80 (diff)
downloadgun-510d49d8ef0a46e90374c3230f28b5354115293f.tar.gz
gun-510d49d8ef0a46e90374c3230f28b5354115293f.tar.bz2
gun-510d49d8ef0a46e90374c3230f28b5354115293f.zip
Make gun:stream_info/2 return intermediaries for HTTP/2 CONNECT
-rw-r--r--src/gun.erl11
-rw-r--r--src/gun_http.erl6
-rw-r--r--src/gun_http2.erl69
-rw-r--r--test/raw_SUITE.erl6
-rw-r--r--test/rfc7540_SUITE.erl16
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).