From baf0e420917ca1cb2806f8594a6cdb4710d2793d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Thu, 20 Sep 2018 15:06:42 +0200 Subject: 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. --- doc/src/manual/gun.asciidoc | 2 +- doc/src/manual/gun.connect.asciidoc | 4 ++-- src/gun.erl | 44 ++++++++++++++++++++++++++++--------- src/gun_http.erl | 31 +++++++++++++++----------- test/rfc7231_SUITE.erl | 26 +++++++++++++++++----- 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), -- cgit v1.2.3