Skip to content

Commit a442927

Browse files
authored
Merge pull request #922 from benoitc/fix/pooled-tls-upgrade-timeout
Bound the pooled TLS upgrade handshake with connect_timeout
2 parents 81d47ff + 3d048b1 commit a442927

2 files changed

Lines changed: 41 additions & 2 deletions

File tree

src/hackney_conn.erl

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -898,7 +898,7 @@ connected({call, From}, is_ready, #conn_data{transport = Transport, socket = Soc
898898
connected({call, From}, {upgrade_to_ssl, _SslOpts, _UpgradeOpts}, #conn_data{transport = hackney_ssl} = _Data) ->
899899
%% Already SSL - no upgrade needed
900900
{keep_state_and_data, [{reply, From, ok}]};
901-
connected({call, From}, {upgrade_to_ssl, SslOpts, UpgradeOpts}, #conn_data{socket = Socket, host = Host, connect_options = ConnectOpts} = Data) ->
901+
connected({call, From}, {upgrade_to_ssl, SslOpts, UpgradeOpts}, #conn_data{socket = Socket, host = Host, connect_options = ConnectOpts, connect_timeout = HandshakeTimeout} = Data) ->
902902
%% Upgrade TCP socket to SSL (e.g., after CONNECT proxy tunnel)
903903
FinalSslOpts = case maps:get(final, UpgradeOpts, false) of
904904
true ->
@@ -926,7 +926,11 @@ connected({call, From}, {upgrade_to_ssl, SslOpts, UpgradeOpts}, #conn_data{socke
926926
Resumable = hackney_ssl:auto_tickets(FinalSslOpts),
927927
Cached = hackney_ssl:recall_alpn(Host, AlpnProtos),
928928
GatedSslOpts = gate_resumption(FinalSslOpts, Cached),
929-
case ssl:connect(Socket, GatedSslOpts) of
929+
%% Bound the pooled TLS upgrade handshake with the connection's
930+
%% connect_timeout. ssl:connect/2 has no handshake deadline, so a server
931+
%% that stalls after TCP accept would pin this process and its pool slot
932+
%% indefinitely, past connect_timeout/recv_timeout.
933+
case ssl:connect(Socket, GatedSslOpts, HandshakeTimeout) of
930934
{ok, SslSocket} ->
931935
%% Detect negotiated protocol, carrying ALPN across resumption
932936
Protocol = hackney_ssl:negotiated_protocol(SslSocket, Host, AlpnProtos, Cached, Resumable),
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
%%% Pooled TLS upgrade must not hang on a stalled handshake.
2+
%%%
3+
%%% ssl:connect/2 has no handshake deadline, so a peer that accepts TCP but
4+
%%% never completes the TLS handshake would pin the connection process (and its
5+
%%% pool slot) forever. The upgrade must be bounded by connect_timeout.
6+
-module(hackney_conn_upgrade_timeout_tests).
7+
8+
-include_lib("eunit/include/eunit.hrl").
9+
10+
pooled_tls_upgrade_times_out_test_() ->
11+
%% Without the bound the upgrade never returns and this test times out.
12+
{timeout, 10, fun pooled_tls_upgrade_times_out/0}.
13+
14+
pooled_tls_upgrade_times_out() ->
15+
{ok, _} = application:ensure_all_started(hackney),
16+
%% Listener that accepts the TCP connection but never speaks TLS.
17+
{ok, LSock} = gen_tcp:listen(0, [binary, {active, false}, {ip, {127, 0, 0, 1}}]),
18+
{ok, Port} = inet:port(LSock),
19+
%% Let the conn open (and thus own) its own TCP socket to the stalled peer.
20+
Opts = #{host => "127.0.0.1", port => Port, transport => hackney_tcp,
21+
connect_timeout => 500},
22+
{ok, Pid} = hackney_conn:start_link(Opts),
23+
ok = hackney_conn:connect(Pid, 1000),
24+
{ok, _ServerSock} = gen_tcp:accept(LSock, 1000),
25+
?assertEqual({ok, connected}, hackney_conn:get_state(Pid)),
26+
%% verify_none so the handshake proceeds and then stalls waiting for the
27+
%% ServerHello that never arrives; only the timeout can end it.
28+
T0 = erlang:monotonic_time(millisecond),
29+
Result = hackney_conn:upgrade_to_ssl(Pid, [{verify, verify_none}], #{final => true}),
30+
Elapsed = erlang:monotonic_time(millisecond) - T0,
31+
?assertMatch({error, _}, Result),
32+
%% 500ms bound with generous slack; a regression hangs instead.
33+
?assert(Elapsed < 4000),
34+
catch hackney_conn:stop(Pid),
35+
gen_tcp:close(LSock).

0 commit comments

Comments
 (0)