Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix SSL accept blocking the main acceptor #38

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/tcp_echo/src/echo_protocol.erl
Expand Up @@ -8,8 +8,8 @@ start_link(ListenerPid, Socket, Transport, Opts) ->
{ok, Pid}.

init(ListenerPid, Socket, Transport, _Opts = []) ->
ok = ranch:accept_ack(ListenerPid),
loop(Socket, Transport).
{ok, Socket1} = ranch:accept_ack(ListenerPid, Socket, Transport, infinity),
loop(Socket1, Transport).

loop(Socket, Transport) ->
case Transport:recv(Socket, 0, 5000) of
Expand Down
25 changes: 13 additions & 12 deletions guide/protocols.md
Expand Up @@ -19,17 +19,18 @@ the call to `ranch:start_listener/6`. This callback must
return `{ok, Pid}`, with `Pid` the pid of the new process.

The newly started process can then freely initialize itself. However,
it must call `ranch:accept_ack/1` before doing any socket operation.
it must call `ranch:accept_ack/4` before doing any socket operation.
This will ensure the connection process is the owner of the socket.
It expects the listener's pid as argument.
It expects the listener's pid as argument, as well as the socket and
the transport.

``` erlang
ok = ranch:accept_ack(ListenerPid).
ok = ranch:accept_ack(ListenerPid, Socket, Transport, Timeout).
```

If your protocol code requires specific socket options, you should
set them while initializing your connection process and before
starting `ranch:accept_ack/1`. You can use `Transport:setopts/2`
starting `ranch:accept_ack/4`. You can use `Transport:setopts/2`
for that purpose.

Following is the complete protocol code for the example found
Expand All @@ -47,8 +48,8 @@ start_link(ListenerPid, Socket, Transport, Opts) ->
{ok, Pid}.

init(ListenerPid, Socket, Transport, _Opts = []) ->
ok = ranch:accept_ack(ListenerPid),
loop(Socket, Transport).
{ok, Socket1} = ranch:accept_ack(ListenerPid, Socket, Transport, infinity),
loop(Socket1, Transport).

loop(Socket, Transport) ->
case Transport:recv(Socket, 0, 5000) of
Expand All @@ -66,7 +67,7 @@ Using gen_server
Special processes like the ones that use the `gen_server` or `gen_fsm`
behaviours have the particularity of having their `start_link` call not
return until the `init` function returns. This is problematic, because
you won't be able to call `ranch:accept_ack/1` from the `init` callback
you won't be able to call `ranch:accept_ack/4` from the `init` callback
as this would cause a deadlock to happen.

There are two ways of solving this problem.
Expand All @@ -92,9 +93,9 @@ start_link(ListenerPid, Socket, Transport, Opts) ->
init(ListenerPid, Socket, Transport, _Opts = []) ->
ok = proc_lib:init_ack({ok, self()}),
%% Perform any required state initialization here.
ok = ranch:accept_ack(ListenerPid),
{ok, Socket1} = ranch:accept_ack(ListenerPid, Socket, Transport, infinity),
ok = Transport:setopts(Socket, [{active, once}]),
gen_server:enter_loop(?MODULE, [], {state, Socket, Transport}).
gen_server:enter_loop(?MODULE, [], {state, Socket1, Transport}).

%% Other gen_server callbacks here.
```
Expand All @@ -114,8 +115,8 @@ init([ListenerPid, Socket, Transport]) ->
{ok, {state, ListenerPid, Socket, Transport}, 0}.

handle_info(timeout, State={state, ListenerPid, Socket, Transport}) ->
ok = ranch:accept_ack(ListenerPid),
ok = Transport:setopts(Socket, [{active, once}]),
{noreply, State};
{ok, Socket1} = ranch:accept_ack(ListenerPid, Socket, Transport, infinity),
ok = Transport:setopts(Socket1, [{active, once}]),
{noreply, {state, ListenerPid, Socket1, Transport}};
%% ...
```
9 changes: 5 additions & 4 deletions src/ranch.erl
Expand Up @@ -18,7 +18,7 @@
-export([start_listener/6]).
-export([stop_listener/1]).
-export([child_spec/6]).
-export([accept_ack/1]).
-export([accept_ack/4]).
-export([get_port/1]).
-export([get_max_connections/1]).
-export([set_max_connections/2]).
Expand Down Expand Up @@ -115,9 +115,10 @@ child_spec(Ref, NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts)
%%
%% Effectively used to make sure the socket control has been given to
%% the protocol process before starting to use it.
-spec accept_ack(pid()) -> ok.
accept_ack(ListenerPid) ->
receive {shoot, ListenerPid} -> ok end.
-spec accept_ack(pid(), any(), module(), timeout()) -> {ok, inet:socket()} | {error, closed | timeout | atom()}.
accept_ack(ListenerPid, Socket, Transport, Timeout) ->
receive {shoot, ListenerPid} -> ok end,
Transport:handshake(Socket, Timeout).

%% @doc Return the listener's port.
-spec get_port(any()) -> inet:port_number().
Expand Down
37 changes: 17 additions & 20 deletions src/ranch_ssl.erl
Expand Up @@ -29,6 +29,7 @@
-export([messages/0]).
-export([listen/1]).
-export([accept/2]).
-export([handshake/2]).
-export([connect/3]).
-export([recv/3]).
-export([send/2]).
Expand Down Expand Up @@ -110,21 +111,29 @@ listen(Opts) ->
[binary, {active, false}, {packet, raw},
{reuseaddr, true}, {nodelay, true}])).

%% @doc Accept connections with the given listening socket.
%% @doc Accept transport connections with the given listening socket.
%%
%% Note that this function does both the transport accept and
%% the SSL handshake. The returned socket is thus fully connected.
%% Note that this function only does the transport accept and
%% the returned socket is only connected at the transport layer.
%%
%% @see handshake/2
%% @see ssl:transport_accept/2
%% @see ssl:ssl_accept/2
-spec accept(ssl:sslsocket(), timeout())
-> {ok, ssl:sslsocket()} | {error, closed | timeout | atom() | tuple()}.
accept(LSocket, Timeout) ->
case ssl:transport_accept(LSocket, Timeout) of
{ok, CSocket} ->
ssl_accept(CSocket, Timeout);
ssl:transport_accept(LSocket, Timeout).

%% @doc Initiate the ssl handshake for the connected socket
%%
%% @see ssl:ssl_accept/2
-spec handshake(ssl:sslsocket(), timeout())
-> {ok, ssl:sslsocket()} | {error, closed | timeout | atom() | tuple()}.
handshake(Socket, Timeout) ->
case ssl:ssl_accept(Socket, Timeout) of
ok ->
{ok, Socket};
{error, Reason} ->
{error, Reason}
{error, {ssl_accept, Reason}}
end.

%% @private Experimental. Open a connection to the given host and port number.
Expand Down Expand Up @@ -215,15 +224,3 @@ sockname(Socket) ->
-spec close(ssl:sslsocket()) -> ok.
close(Socket) ->
ssl:close(Socket).

%% Internal.

-spec ssl_accept(ssl:sslsocket(), timeout())
-> {ok, ssl:sslsocket()} | {error, {ssl_accept, atom()}}.
ssl_accept(Socket, Timeout) ->
case ssl:ssl_accept(Socket, Timeout) of
ok ->
{ok, Socket};
{error, Reason} ->
{error, {ssl_accept, Reason}}
end.
7 changes: 7 additions & 0 deletions src/ranch_tcp.erl
Expand Up @@ -24,6 +24,7 @@
-export([messages/0]).
-export([listen/1]).
-export([accept/2]).
-export([handshake/2]).
-export([connect/3]).
-export([recv/3]).
-export([send/2]).
Expand Down Expand Up @@ -81,6 +82,12 @@ listen(Opts) ->
accept(LSocket, Timeout) ->
gen_tcp:accept(LSocket, Timeout).

%% @doc Post-accept handshake for the connected socket.
-spec handshake(inet:socket(), timeout())
-> {ok, inet:socket()} | {error, closed | timeout | atom()}.
handshake(Socket, _Timeout) ->
{ok, Socket}.

%% @private Experimental. Open a connection to the given host and port number.
%% @see gen_tcp:connect/3
%% @todo Probably filter Opts?
Expand Down
4 changes: 4 additions & 0 deletions src/ranch_transport.erl
Expand Up @@ -44,6 +44,10 @@
-callback accept(socket(), timeout())
-> {ok, socket()} | {error, closed | timeout | atom() | tuple()}.

%% Initiate any post-accept handshake operations
-callback handshake(socket(), timeout())
-> {ok, socket()} | {error, closed | timeout | atom() | tuple()}.

%% Experimental. Open a connection to the given host and port number.
-callback connect(string(), inet:port_number(), opts())
-> {ok, socket()} | {error, atom()}.
Expand Down
4 changes: 2 additions & 2 deletions test/active_echo_protocol.erl
Expand Up @@ -9,8 +9,8 @@ start_link(ListenerPid, Socket, Transport, Opts) ->
{ok, Pid}.

init(ListenerPid, Socket, Transport, _Opts = []) ->
ok = ranch:accept_ack(ListenerPid),
loop(Socket, Transport).
{ok, Socket1} = ranch:accept_ack(ListenerPid, Socket, Transport, infinity),
loop(Socket1, Transport).

loop(Socket, Transport) ->
{OK, Closed, Error} = Transport:messages(),
Expand Down
4 changes: 2 additions & 2 deletions test/echo_protocol.erl
Expand Up @@ -9,8 +9,8 @@ start_link(ListenerPid, Socket, Transport, Opts) ->
{ok, Pid}.

init(ListenerPid, Socket, Transport, _Opts = []) ->
ok = ranch:accept_ack(ListenerPid),
loop(Socket, Transport).
{ok, Socket1} = ranch:accept_ack(ListenerPid, Socket, Transport, infinity),
loop(Socket1, Transport).

loop(Socket, Transport) ->
case Transport:recv(Socket, 0, 5000) of
Expand Down
10 changes: 5 additions & 5 deletions test/remove_conn_and_wait_protocol.erl
Expand Up @@ -2,14 +2,14 @@
-behaviour(ranch_protocol).

-export([start_link/4]).
-export([init/2]).
-export([init/4]).

start_link(ListenerPid, _, _, [{remove, MaybeRemove}]) ->
Pid = spawn_link(?MODULE, init, [ListenerPid, MaybeRemove]),
start_link(ListenerPid, Socket, Transport, [{remove, MaybeRemove}]) ->
Pid = spawn_link(?MODULE, init, [ListenerPid, Socket, Transport, MaybeRemove]),
{ok, Pid}.

init(ListenerPid, MaybeRemove) ->
ranch:accept_ack(ListenerPid),
init(ListenerPid, Socket, Transport, MaybeRemove) ->
_ = ranch:accept_ack(ListenerPid, Socket, Transport, infinity),
case MaybeRemove of
true ->
ranch_listener:remove_connection(ListenerPid);
Expand Down