aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2018-09-20 15:06:42 +0200
committerLoïc Hoguin <[email protected]>2018-09-20 15:09:10 +0200
commitbaf0e420917ca1cb2806f8594a6cdb4710d2793d (patch)
treec8416139c514688bab05c86be3a5380476c7ca85
parentea0296d1560557ce45cdfc197a0254fb15bd75f8 (diff)
downloadgun-baf0e420917ca1cb2806f8594a6cdb4710d2793d.tar.gz
gun-baf0e420917ca1cb2806f8594a6cdb4710d2793d.tar.bz2
gun-baf0e420917ca1cb2806f8594a6cdb4710d2793d.zip
Use ALPN when proxying TLS connections using CONNECT
This fixes HTTP/2 over TLS connections. The protocol destination option has been deprecated in favor of a protocols option.
-rw-r--r--doc/src/manual/gun.asciidoc2
-rw-r--r--doc/src/manual/gun.connect.asciidoc4
-rw-r--r--src/gun.erl44
-rw-r--r--src/gun_http.erl31
-rw-r--r--test/rfc7231_SUITE.erl26
5 files changed, 76 insertions, 31 deletions
diff --git a/doc/src/manual/gun.asciidoc b/doc/src/manual/gun.asciidoc
index 29ca3ea..9f753b4 100644
--- a/doc/src/manual/gun.asciidoc
+++ b/doc/src/manual/gun.asciidoc
@@ -93,7 +93,7 @@ connect_destination() :: #{
username => iodata(),
password => iodata(),
- protocol => http | http2,
+ protocols => [http | http2],
transport => tcp | tls,
tls_opts => [ssl:connect_option()],
diff --git a/doc/src/manual/gun.connect.asciidoc b/doc/src/manual/gun.connect.asciidoc
index a1acaa5..483b24d 100644
--- a/doc/src/manual/gun.connect.asciidoc
+++ b/doc/src/manual/gun.connect.asciidoc
@@ -85,8 +85,8 @@ StreamRef = gun:connect(ConnPid, #{
{ok, http} = gun:await_up(ConnPid),
StreamRef = gun:connect(ConnPid, #{
host => "origin-server.example.org",
- port => 80,
- protocol => http2,
+ port => 443,
+ protocols => [http2],
transport => tls
}),
{response, fin, 200, _} = gun:await(ConnPid, StreamRef),
diff --git a/src/gun.erl b/src/gun.erl
index af576fb..9c92281 100644
--- a/src/gun.erl
+++ b/src/gun.erl
@@ -120,7 +120,8 @@
port := inet:port_number(),
username => iodata(),
password => iodata(),
- protocol => http | http2,
+ protocol => http | http2, %% @todo Remove in Gun 2.0.
+ protocols => [http | http2],
transport => tcp | tls,
tls_opts => [ssl:connect_option()],
tls_handshake_timeout => timeout()
@@ -652,14 +653,9 @@ default_transport(443) -> tls;
default_transport(_) -> tcp.
transport_connect(State=#state{host=Host, port=Port, opts=Opts, transport=Transport=gun_tls}, Retries) ->
- Protocols = [case P of
- http -> <<"http/1.1">>;
- http2 -> <<"h2">>
- end || P <- maps:get(protocols, Opts, [http2, http])],
- TransportOpts = [binary, {active, false},
- {alpn_advertised_protocols, Protocols},
- {client_preferred_next_protocols, {client, Protocols, <<"http/1.1">>}}
- |maps:get(transport_opts, Opts, [])],
+ TransportOpts = [binary, {active, false}|ensure_alpn(
+ maps:get(protocols, Opts, [http2, http]),
+ maps:get(transport_opts, Opts, []))],
case Transport:connect(Host, Port, TransportOpts, maps:get(connect_timeout, Opts, infinity)) of
{ok, Socket} ->
{Protocol, ProtoOptsKey} = case ssl:negotiated_protocol(Socket) of
@@ -684,6 +680,16 @@ transport_connect(State=#state{host=Host, port=Port, opts=Opts, transport=Transp
retry(State#state{last_error=Reason}, Retries)
end.
+ensure_alpn(Protocols0, TransportOpts) ->
+ Protocols = [case P of
+ http -> <<"http/1.1">>;
+ http2 -> <<"h2">>
+ end || P <- Protocols0],
+ [
+ {alpn_advertised_protocols, Protocols},
+ {client_preferred_next_protocols, {client, Protocols, <<"http/1.1">>}}
+ |TransportOpts].
+
up(State=#state{owner=Owner, opts=Opts, transport=Transport}, Socket, Protocol, ProtoOptsKey) ->
ProtoOpts = maps:get(ProtoOptsKey, Opts, #{}),
ProtoState = Protocol:init(Owner, Socket, Transport, ProtoOpts),
@@ -778,7 +784,25 @@ loop(State=#state{parent=Parent, owner=Owner, owner_ref=OwnerRef,
ProtoState2 = Protocol:data(ProtoState,
StreamRef, ReplyTo, IsFin, Data),
loop(State#state{protocol_state=ProtoState2});
- {connect, ReplyTo, StreamRef, Destination, Headers} ->
+ {connect, ReplyTo, StreamRef, Destination0, Headers} ->
+ %% The protocol option has been deprecated in favor of the protocols option.
+ %% Nobody probably ended up using it, but let's not break the interface.
+ Destination1 = case Destination0 of
+ #{protocols := _} ->
+ Destination0;
+ #{protocol := DestProto} ->
+ Destination0#{protocols => [DestProto]};
+ _ ->
+ Destination0
+ end,
+ Destination = case Destination1 of
+ #{transport := tls} ->
+ Destination1#{tls_opts => ensure_alpn(
+ maps:get(protocols, Destination1, [http]),
+ maps:get(tls_opts, Destination1, []))};
+ _ ->
+ Destination1
+ end,
ProtoState2 = Protocol:connect(ProtoState, StreamRef, ReplyTo, Destination, Headers),
loop(State#state{protocol_state=ProtoState2});
{cancel, ReplyTo, StreamRef} ->
diff --git a/src/gun_http.erl b/src/gun_http.erl
index ee9d04f..c2b0ed6 100644
--- a/src/gun_http.erl
+++ b/src/gun_http.erl
@@ -221,29 +221,34 @@ handle_head(Data, State=#http_state{socket=Socket, version=ClientVersion,
State2 = end_stream(State#http_state{streams=[Stream|Tail]}),
NewHost = maps:get(host, Destination),
NewPort = maps:get(port, Destination),
- DestProtocol = maps:get(protocol, Destination, http),
case Destination of
#{transport := tls} ->
TLSOpts = maps:get(tls_opts, Destination, []),
TLSTimeout = maps:get(tls_handshake_timeout, Destination, infinity),
case gun_tls:connect(Socket, TLSOpts, TLSTimeout) of
- {ok, TLSSocket} when DestProtocol =:= http2 ->
- [{switch_transport, gun_tls, TLSSocket},
- {switch_protocol, gun_http2, State2},
- {origin, <<"https">>, NewHost, NewPort}];
{ok, TLSSocket} ->
- [{state, State2#http_state{socket=TLSSocket, transport=gun_tls}},
- {switch_transport, gun_tls, TLSSocket},
- {origin, <<"https">>, NewHost, NewPort}];
+ case ssl:negotiated_protocol(TLSSocket) of
+ {ok, <<"h2">>} ->
+ [{switch_transport, gun_tls, TLSSocket},
+ {switch_protocol, gun_http2, State2},
+ {origin, <<"https">>, NewHost, NewPort}];
+ _ ->
+ [{state, State2#http_state{socket=TLSSocket, transport=gun_tls}},
+ {switch_transport, gun_tls, TLSSocket},
+ {origin, <<"https">>, NewHost, NewPort}]
+ end;
Error ->
Error
end;
- _ when DestProtocol =:= http2 ->
- [{switch_protocol, gun_http2, State2},
- {origin, <<"http">>, NewHost, NewPort}];
_ ->
- [{state, State2},
- {origin, <<"http">>, NewHost, NewPort}]
+ case maps:get(protocols, Destination, [http]) of
+ [http] ->
+ [{state, State2},
+ {origin, <<"http">>, NewHost, NewPort}];
+ [http2] ->
+ [{switch_protocol, gun_http2, State2},
+ {origin, <<"http">>, NewHost, NewPort}]
+ end
end;
{_, _} when Status >= 100, Status =< 199 ->
ReplyTo ! {gun_inform, self(), stream_ref(StreamRef), Status, Headers},
diff --git a/test/rfc7231_SUITE.erl b/test/rfc7231_SUITE.erl
index 90234d2..88bc114 100644
--- a/test/rfc7231_SUITE.erl
+++ b/test/rfc7231_SUITE.erl
@@ -88,13 +88,18 @@ do_proxy_loop(ClientSocket, OriginSocket) ->
end.
do_origin_start(Transport) ->
+ do_origin_start(Transport, http).
+
+do_origin_start(Transport, Protocol) ->
Self = self(),
Pid = spawn_link(fun() ->
case Transport of
tcp ->
do_origin_init_tcp(Self);
- tls ->
- do_origin_init_tls(Self)
+ tls when Protocol =:= http ->
+ do_origin_init_tls(Self);
+ tls when Protocol =:= http2 ->
+ do_origin_init_tls_h2(Self)
end
end),
Port = do_receive(Pid),
@@ -116,6 +121,17 @@ do_origin_init_tls(Parent) ->
ok = ssl:ssl_accept(ClientSocket, 1000),
do_origin_loop(Parent, ClientSocket, ssl).
+do_origin_init_tls_h2(Parent) ->
+ Opts = ct_helper:get_certs_from_ets(),
+ {ok, ListenSocket} = ssl:listen(0, [binary, {active, false},
+ {alpn_preferred_protocols, [<<"h2">>]}|Opts]),
+ {ok, {_, Port}} = ssl:sockname(ListenSocket),
+ Parent ! {self(), Port},
+ {ok, ClientSocket} = ssl:transport_accept(ListenSocket, 1000),
+ ok = ssl:ssl_accept(ClientSocket, 1000),
+ {ok, <<"h2">>} = ssl:negotiated_protocol(ClientSocket),
+ do_origin_loop(Parent, ClientSocket, ssl).
+
do_origin_loop(Parent, ClientSocket, ClientTransport) ->
case ClientTransport:recv(ClientSocket, 0, 1000) of
{ok, Data} ->
@@ -146,7 +162,7 @@ connect_https(_) ->
do_connect_http(tls).
do_connect_http(Transport) ->
- {ok, OriginPid, OriginPort} = do_origin_start(Transport),
+ {ok, OriginPid, OriginPort} = do_origin_start(Transport, http),
{ok, ProxyPid, ProxyPort} = do_proxy_start(),
Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]),
{ok, ConnPid} = gun:open("localhost", ProxyPort),
@@ -175,7 +191,7 @@ connect_h2(_) ->
do_connect_h2(tls).
do_connect_h2(Transport) ->
- {ok, OriginPid, OriginPort} = do_origin_start(Transport),
+ {ok, OriginPid, OriginPort} = do_origin_start(Transport, http2),
{ok, ProxyPid, ProxyPort} = do_proxy_start(),
Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]),
{ok, ConnPid} = gun:open("localhost", ProxyPort),
@@ -184,7 +200,7 @@ do_connect_h2(Transport) ->
host => "localhost",
port => OriginPort,
transport => Transport,
- protocol => http2
+ protocols => [http2]
}),
{request, <<"CONNECT">>, Authority, 'HTTP/1.1', _} = do_receive(ProxyPid),
{response, fin, 200, _} = gun:await(ConnPid, StreamRef),