aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/gun.erl87
-rw-r--r--src/gun_http2.erl25
-rw-r--r--src/gun_http3.erl1
-rw-r--r--src/gun_tls.erl2
-rw-r--r--src/gun_tls_proxy.erl33
5 files changed, 133 insertions, 15 deletions
diff --git a/src/gun.erl b/src/gun.erl
index 085e47f..e23da3a 100644
--- a/src/gun.erl
+++ b/src/gun.erl
@@ -332,7 +332,7 @@
keepalive_ref :: undefined | reference(),
socket :: undefined | inet:socket() | ssl:sslsocket() | pid(),
transport :: module(),
- active = true :: boolean(),
+ active = false :: boolean(),
messages :: {atom(), atom(), atom()},
protocol :: module(),
protocol_state :: any(),
@@ -1331,10 +1331,18 @@ normal_tls_handshake(Socket, State=#state{
EvHandlerState1 = EvHandler:tls_handshake_start(HandshakeEvent, EvHandlerState0),
case gun_tls:connect(Socket, TLSOpts, TLSTimeout) of
{ok, TLSSocket} ->
- %% This call may return {error,closed} when the socket has
- %% been closed by the peer. This should be very rare (due to
- %% timing) but can happen for example when client certificates
- %% were required but not sent or invalid with some servers.
+ %% When initially connecting we are in passive mode and
+ %% in that state we expect this call to always succeed.
+ %% In rare scenarios (suspended Gun process) it may
+ %% return {error,closed}, but this indicates that the
+ %% socket process is gone and we cannot retrieve a potential
+ %% TLS alert.
+ %%
+ %% When using HTTP/1.1 CONNECT we are also in passive mode
+ %% because CONNECT involves a response that is received via
+ %% active mode, which automatically goes into passive mode
+ %% ({active,once}), and we only reenable active mode after
+ %% processing commands.
case ssl:negotiated_protocol(TLSSocket) of
{error, Reason = closed} ->
EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{
@@ -1368,7 +1376,8 @@ connected_protocol_init(internal, {connected, Retries, Socket, NewProtocol},
{ok, StateName, ProtoState} ->
%% @todo Don't send gun_up and gun_down if active/1 fails here.
reply(Owner, {gun_up, self(), Protocol:name()}),
- State1 = State0#state{socket=Socket, protocol=Protocol, protocol_state=ProtoState},
+ State1 = State0#state{socket=Socket, protocol=Protocol,
+ protocol_state=ProtoState, active=true},
case active(State1) of
{ok, State2} ->
State = case Protocol:has_keepalive() of
@@ -1899,7 +1908,8 @@ commands([TLSHandshake={tls_handshake, _, _, _}], State) ->
disconnect(State0=#state{owner=Owner, status=Status, opts=Opts,
intermediaries=Intermediaries, socket=Socket, transport=Transport0,
protocol=Protocol, protocol_state=ProtoState,
- event_handler=EvHandler, event_handler_state=EvHandlerState0}, Reason) ->
+ event_handler=EvHandler, event_handler_state=EvHandlerState0}, Reason0) ->
+ Reason = maybe_tls_alert(State0, Reason0),
EvHandlerState1 = Protocol:close(Reason, ProtoState, EvHandler, EvHandlerState0),
_ = Transport0:close(Socket),
EvHandlerState = EvHandler:disconnect(#{reason => Reason}, EvHandlerState1),
@@ -1913,6 +1923,9 @@ disconnect(State0=#state{owner=Owner, status=Status, opts=Opts,
%% We closed the socket, discard any remaining socket events.
disconnect_flush(State1),
KilledStreams = Protocol:down(ProtoState),
+ %% @todo Reason here may be {error, Reason1} which leads to
+ %% different behavior compared to down messages received
+ %% from failing to connect where Reason1 is what gets sent.
reply(Owner, {gun_down, self(), Protocol:name(), Reason, KilledStreams}),
Retry = maps:get(retry, Opts, 5),
State2 = keepalive_cancel(State1#state{
@@ -1935,6 +1948,66 @@ disconnect(State0=#state{owner=Owner, status=Status, opts=Opts,
{next_event, internal, {retries, Retry, Reason}}}
end.
+%% With TLS 1.3 the handshake may not have validated the certificate
+%% by the time it completes. The validation may therefore fail at any
+%% time afterwards. TLS 1.3 also introduced post-handshake authentication
+%% which would produce the same results. Erlang/OTP's ssl has a number
+%% of asynchronous functions which won't return the alert as an error
+%% and instead return a plain {error,closed}, including ssl:send.
+%% Gun must therefore check whether a close is resulting from a TLS alert
+%% and use that alert as a more descriptive disconnect reason.
+%%
+%% Sometimes, ssl:send will return {error,einval}, because while the
+%% TLS pseudo-socket still exists, the underlying TCP socket is already
+%% gone. In that case we can still query the TLS pseudo-socket to get
+%% the detailed TLS alert.
+%%
+%% @todo We currently do not support retrieving the alert from a gun_tls_proxy
+%% socket. We need a test case to best understand what should be done there.
+%% But since the socket belongs to that process we likely need additional
+%% changes there to make it work.
+maybe_tls_alert(#state{socket=Socket, transport=gun_tls,
+ active=true, messages={_, _, Error}}, Reason0)
+ %% The unwrapped tuple we get half the time makes this clause more complex.
+ when Reason0 =:= {error, closed}; Reason0 =:= {error, einval}; Reason0 =:= closed ->
+ %% When active mode is enabled we should have the alert in our
+ %% mailbox so we can just retrieve it. In case it is late we
+ %% use a short timeout to increase the chances of catching it.
+ receive
+ {Error, Socket, Reason} ->
+ Reason
+ after 200 ->
+ Reason0
+ end;
+maybe_tls_alert(#state{socket=Socket, transport=Transport=gun_tls,
+ active=false}, Reason0)
+ when Reason0 =:= {error, closed}; Reason0 =:= {error, einval}; Reason0 =:= closed ->
+ %% When active mode is disabled we can do a number of operations to
+ %% receive the alert. Enabling active mode is one of them.
+ case Transport:setopts(Socket, [{active, once}]) of
+ {error, Reason={tls_alert, _}} ->
+ Reason;
+ _ ->
+ Reason0
+ end;
+%% We unwrap the TLS alert error for consistency.
+%% @todo Consistenly wrap/unwrap all errors instead of just this one.
+maybe_tls_alert(_, {error, Reason={tls_alert, _}}) ->
+ Reason;
+%% We may also need to receive the alert when proxying TLS.
+maybe_tls_alert(#state{socket=Socket, transport=gun_tls_proxy,
+ active=true, messages={_, _, Error}}, Reason0)
+ %% The unwrapped tuple we get half the time makes this clause more complex.
+ when Reason0 =:= {error, closed}; Reason0 =:= {error, einval}; Reason0 =:= closed ->
+ receive
+ {Error, Socket, Reason} ->
+ Reason
+ after 200 ->
+ Reason0
+ end;
+maybe_tls_alert(_, Reason) ->
+ Reason.
+
disconnect_flush(State=#state{socket=Socket, messages={OK, Closed, Error}}) ->
receive
{OK, Socket, _} -> disconnect_flush(State);
diff --git a/src/gun_http2.erl b/src/gun_http2.erl
index b39be6e..0feb40e 100644
--- a/src/gun_http2.erl
+++ b/src/gun_http2.erl
@@ -206,6 +206,16 @@ init(ReplyTo, Socket, Transport, Opts0) ->
{ok, connected, #http2_state{reply_to=ReplyTo, socket=Socket, transport=Transport,
opts=Opts, base_stream_ref=BaseStreamRef, tunnel_transport=TunnelTransport,
content_handlers=Handlers, http2_machine=HTTP2Machine}};
+ Error0={error, R} when R =:= closed; R =:= einval ->
+ %% Check whether we have a TLS alert and in that case,
+ %% return it. We must do this here because Protocol:init
+ %% failure doesn't go through disconnect.
+ case Transport:setopts(Socket, [{active, once}]) of
+ Error={error, {tls_alert, _}} ->
+ Error;
+ _ ->
+ Error0
+ end;
Error={error, _Reason} ->
Error
end.
@@ -488,8 +498,20 @@ tunnel_commands([{state, ProtoState}|Tail], Stream=#stream{tunnel=Tunnel},
State, EvHandler, EvHandlerState) ->
tunnel_commands(Tail, Stream#stream{tunnel=Tunnel#tunnel{protocol_state=ProtoState}},
State, EvHandler, EvHandlerState);
-tunnel_commands([{error, Reason}|_], #stream{id=StreamID, ref=StreamRef, reply_to=ReplyTo},
+tunnel_commands([{error, Reason0}|_], #stream{id=StreamID, ref=StreamRef, reply_to=ReplyTo},
State, _EvHandler, EvHandlerState) ->
+ %% See gun:maybe_tls_alert for details.
+ Reason = case Reason0 of
+ closed ->
+ receive
+ {handle_continue, StreamRef, {tls_proxy_error, _Socket, Reason1}} ->
+ Reason1
+ after 200 ->
+ Reason0
+ end;
+ _ ->
+ Reason0
+ end,
gun:reply(ReplyTo, {gun_error, self(), stream_ref(State, StreamRef),
{stream_error, Reason, 'Tunnel closed unexpectedly.'}}),
{{state, delete_stream(State, StreamID)}, EvHandlerState};
@@ -948,6 +970,7 @@ close(Reason0, State=#http2_state{streams=Streams}, _, EvHandlerState) ->
end, [], Streams),
EvHandlerState.
+%% @todo This can get {error,closed} leading to {closed,{error,closed}}.
close_reason(closed) -> closed;
close_reason(Reason) -> {closed, Reason}.
diff --git a/src/gun_http3.erl b/src/gun_http3.erl
index 38d8970..26aa072 100644
--- a/src/gun_http3.erl
+++ b/src/gun_http3.erl
@@ -104,6 +104,7 @@ default_keepalive() -> infinity.
init(ReplyTo, Conn, Transport, Opts) ->
Handlers = maps:get(content_handlers, Opts, [gun_data_h]),
{ok, SettingsBin, HTTP3Machine0} = cow_http3_machine:init(client, Opts),
+ %% @todo We may get a TLS 1.3 error/alert here in mTLS scenarios.
{ok, ControlID} = Transport:start_unidi_stream(Conn, [<<0>>, SettingsBin]),
{ok, EncoderID} = Transport:start_unidi_stream(Conn, [<<2>>]),
{ok, DecoderID} = Transport:start_unidi_stream(Conn, [<<3>>]),
diff --git a/src/gun_tls.erl b/src/gun_tls.erl
index cf1bcfd..ef2d800 100644
--- a/src/gun_tls.erl
+++ b/src/gun_tls.erl
@@ -35,7 +35,7 @@ connect(Socket, Opts, Timeout) ->
send(Socket, Packet) ->
ssl:send(Socket, Packet).
--spec setopts(ssl:sslsocket(), list()) -> ok | {error, atom()}.
+-spec setopts(ssl:sslsocket(), list()) -> ok | {error, any()}.
setopts(Socket, Opts) ->
ssl:setopts(Socket, Opts).
diff --git a/src/gun_tls_proxy.erl b/src/gun_tls_proxy.erl
index 024c7a7..f3719b6 100644
--- a/src/gun_tls_proxy.erl
+++ b/src/gun_tls_proxy.erl
@@ -131,6 +131,8 @@ cb_send(Pid, Data) ->
try
gen_statem:call(Pid, {?FUNCTION_NAME, Data})
catch
+ exit:{{shutdown, close}, _} ->
+ {error, closed};
exit:{noproc, _} ->
{error, closed}
end.
@@ -140,6 +142,8 @@ cb_setopts(Pid, Opts) ->
try
gen_statem:call(Pid, {?FUNCTION_NAME, Opts})
catch
+ exit:{{shutdown, close}, _} ->
+ {error, closed};
exit:{noproc, _} ->
{error, einval}
end.
@@ -178,6 +182,9 @@ sockname(Pid) ->
close(Pid) ->
?DEBUG_LOG("pid ~0p", [Pid]),
try
+ %% We must unlink before closing otherwise the closing
+ %% will take down the Gun process with it.
+ unlink(Pid),
gen_statem:call(Pid, ?FUNCTION_NAME)
catch
%% May happen for example when the handshake fails.
@@ -236,11 +243,13 @@ not_connected(cast, Msg={setopts, _}, State) ->
{keep_state_and_data, postpone};
not_connected(cast, Msg={connect_proc, {ok, Socket}}, State=#state{owner_pid=OwnerPid, extra=Extra}) ->
?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]),
- OwnerPid ! {?MODULE, self(), {ok, ssl:negotiated_protocol(Socket)}, Extra},
- %% We need to spawn this call before OTP-21.2 because it triggers
- %% a cb_setopts call that blocks us. Might be OK to just leave it
- %% like this once we support 21.2+ only.
- spawn(fun() -> ok = ssl:setopts(Socket, [{active, true}]) end),
+ Negotiated = ssl:negotiated_protocol(Socket),
+ _ = case ssl:setopts(Socket, [{active, true}]) of
+ ok ->
+ OwnerPid ! {?MODULE, self(), {ok, Negotiated}, Extra};
+ Error ->
+ OwnerPid ! {?MODULE, self(), Error, Extra}
+ end,
{next_state, connected, State#state{proxy_socket=Socket}};
not_connected(cast, Msg={connect_proc, Error}, State=#state{owner_pid=OwnerPid, extra=Extra}) ->
?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]),
@@ -332,8 +341,20 @@ handle_common(cast, Msg={cb_controlling_process, ProxyPid}, State) ->
handle_common(cast, Msg={setopts, Opts}, State) ->
?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]),
{keep_state, owner_setopts(Opts, State)};
-handle_common(cast, Msg={send_result, From, Result}, State) ->
+handle_common(cast, Msg={send_result, From, Result0}, State=#state{proxy_socket=Socket}) ->
?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]),
+ %% See gun:maybe_tls_alert for details.
+ Result = case Result0 of
+ {error, closed} ->
+ receive
+ {ssl_error, Socket, Reason} ->
+ {error, Reason}
+ after 200 ->
+ Result0
+ end;
+ _ ->
+ Result0
+ end,
gen_statem:reply(From, Result),
keep_state_and_data;
%% Messages from the real socket.