Browse Source

Enable multiple steps handshake

Also fix some Protocol:start_link/4 into start_link/3
left over in the documentation.
juhlig 5 years ago
parent
commit
d44e7a16f7

+ 2 - 2
doc/src/guide/listeners.asciidoc

@@ -239,8 +239,8 @@ with the name of the listener as the only argument.
 [source,erlang]
 ranch:remove_connection(Ref).
 
-As seen in the chapter covering protocols, this pid is received as the
-first argument of the protocol's `start_link/4` callback.
+As seen in the chapter covering protocols, this reference is received
+as the first argument of the protocol's `start_link/3` callback.
 
 You can modify the `max_connections` value on a running listener by
 using the `ranch:set_max_connections/2` function, with the name of the

+ 1 - 1
doc/src/guide/protocols.asciidoc

@@ -6,7 +6,7 @@ protocol logic executed in this process.
 === Writing a protocol handler
 
 All protocol handlers must implement the `ranch_protocol` behavior
-which defines a single callback, `start_link/4`. This callback is
+which defines a single callback, `start_link/3`. This callback is
 responsible for spawning a new process for handling the connection.
 It receives four arguments: the name of the listener, the socket, the
 transport handler being used and the protocol options defined in

+ 2 - 0
doc/src/manual/ranch.asciidoc

@@ -26,6 +26,8 @@ Suspend/resume:
 Connections:
 
 * link:man:ranch:handshake(3)[ranch:handshake(3)] - Perform the transport handshake
+* link:man:ranch:handshake_continue(3)[ranch:handshake_continue(3)] - Resume the paused transport handshake
+* link:man:ranch:handshake_cancel(3)[ranch:handshake_cancel(3)] - Cancel the paused transport handshake
 * link:man:ranch:recv_proxy_header(3)[ranch:recv_proxy_header(3)] - Receive the PROXY protocol header
 * link:man:ranch:remove_connection(3)[ranch:remove_connection(3)] - Remove connection from the count
 

+ 18 - 4
doc/src/manual/ranch.handshake.asciidoc

@@ -8,12 +8,13 @@ ranch:handshake - Perform the transport handshake
 
 [source,erlang]
 ----
-handshake(Ref)       -> handshake(Ref, [])
-handshake(Ref, Opts) -> {ok, Socket}
+handshake(Ref)       -> {ok, Socket} | {continue, Info}
+handshake(Ref, Opts) -> {ok, Socket} | {continue, Info}
 
 Ref    :: ranch:ref()
 Opts   :: any()
 Socket :: any()
+Info   :: any()
 ----
 
 Perform the transport handshake.
@@ -38,12 +39,23 @@ Allowed options depend on the transport module.
 
 == Return value
 
-An `ok` tuple is returned containing the socket for the connection.
+An `ok` tuple is returned containing the socket for the connection
+by default.
+
+Depending on configuration, a `continue` tuple can otherwise
+be returned when the handshake operation is paused. It contains
+data provided by the transport that can be used to inform further
+decisions before resuming the handshake, for example to provide
+new transport options. The handshake can be resumed using
+link:man:ranch:handshake_continue(3)[ranch:handshake_continue(3)]
+or canceled using
+link:man:ranch:handshake_cancel(3)[ranch:handshake_cancel(3)].
 
 This function will trigger an exception when an error occurs.
 
 == Changelog
 
+* *2.0*: The `continue` tuple can now be returned.
 * *1.6*: Function introduced. Replaces `ranch:accept_ack/1`.
 
 == Examples
@@ -51,7 +63,7 @@ This function will trigger an exception when an error occurs.
 .Initialize the connection process
 [source,erlang]
 ----
-start_link(Ref, _, Transport, Opts) ->
+start_link(Ref, Transport, Opts) ->
     Pid = proc_lib:spawn_link(?MODULE, init,
         [Ref, Transport, Opts]),
     {ok, Pid}.
@@ -65,6 +77,8 @@ init(Ref, Transport, Opts) ->
 == See also
 
 link:man:ranch:start_listener(3)[ranch:start_listener(3)],
+link:man:ranch:handshake_continue(3)[ranch:handshake_continue(3)],
+link:man:ranch:handshake_cancel(3)[ranch:handshake_cancel(3)],
 link:man:ranch:recv_proxy_header(3)[ranch:recv_proxy_header(3)],
 link:man:ranch:remove_connection(3)[ranch:remove_connection(3)],
 link:man:ranch(3)[ranch(3)]

+ 55 - 0
doc/src/manual/ranch.handshake_cancel.asciidoc

@@ -0,0 +1,55 @@
+= ranch:handshake_cancel(3)
+
+== Name
+
+ranch:handshake_cancel - Cancel the paused transport handshake
+
+== Description
+
+[source,erlang]
+----
+handshake_cancel(Ref :: ranch:ref()) -> ok
+----
+
+Cancel the paused transport handshake.
+
+This function may be called by the protocol process
+to cancel a paused handshake.
+
+== Arguments
+
+Ref::
+
+The listener name.
++
+Allowed options depend on the transport module.
+
+== Return value
+
+The return value depends on the transport module.
+
+== Changelog
+
+* *2.0*: Function introduced.
+
+== Examples
+
+.Cancel a paused transport handshake
+[source,erlang]
+----
+start_link(Ref, Transport, Opts) ->
+    Pid = proc_lib:spawn_link(?MODULE, init,
+        [Ref, Transport, Opts]),
+    {ok, Pid}.
+
+init(Ref, Transport, Opts) ->
+    {continue, _Info} = ranch:handshake(Ref),
+    ranch:handshake_cancel(Ref),
+    exit(handshake_cancelled).
+----
+
+== See also
+
+link:man:ranch:handshake(3)[ranch:handshake(3)],
+link:man:ranch:handshake_continue(3)[ranch:handshake_continue(3)],
+link:man:ranch(3)[ranch(3)]

+ 67 - 0
doc/src/manual/ranch.handshake_continue.asciidoc

@@ -0,0 +1,67 @@
+= ranch:handshake_continue(3)
+
+== Name
+
+ranch:handshake_continue - Resume the paused transport handshake
+
+== Description
+
+[source,erlang]
+----
+handshake_continue(Ref)       -> {ok, Socket}
+handshake_continue(Ref, Opts) -> {ok, Socket}
+
+Ref    :: ranch:ref()
+Opts   :: any()
+Socket :: any()
+----
+
+Resume the paused transport handshake.
+
+This function must be called by the protocol process in order
+to resume a paused handshake.
+
+== Arguments
+
+Ref::
+
+The listener name.
+
+Opts::
+
+Transport handshake options.
++
+Allowed options depend on the transport module.
+
+== Return value
+
+An `ok` tuple is returned containing the socket for the connection.
+
+This function will trigger an exception when an error occurs.
+
+== Changelog
+
+* *2.0*: Function introduced.
+
+== Examples
+
+.Continue a paused transport handshake
+[source,erlang]
+----
+start_link(Ref, Transport, Opts) ->
+    Pid = proc_lib:spawn_link(?MODULE, init,
+        [Ref, Transport, Opts]),
+    {ok, Pid}.
+
+init(Ref, Transport, Opts) ->
+    {continue, _Info} = ranch:handshake(Ref),
+    {ok, Socket} = ranch:handshake_continue(Ref),
+    loop(#state{ref=Ref, socket=Socket,
+        transport=Transport, opts=Opts}).
+----
+
+== See also
+
+link:man:ranch:handshake(3)[ranch:handshake(3)],
+link:man:ranch:handshake_cancel(3)[ranch:handshake_cancel(3)],
+link:man:ranch(3)[ranch(3)]

+ 1 - 1
doc/src/manual/ranch.recv_proxy_header.asciidoc

@@ -51,7 +51,7 @@ the error.
 .Receive the PROXY protocol header
 [source,erlang]
 ----
-start_link(Ref, _, Transport, Opts) ->
+start_link(Ref, Transport, Opts) ->
     Pid = proc_lib:spawn_link(?MODULE, init,
         [Ref, Transport, Opts]),
     {ok, Pid}.

+ 10 - 0
doc/src/manual/ranch_ssl.asciidoc

@@ -54,6 +54,7 @@ ssl_opt() = {alpn_preferred_protocols, [binary()]}
           | {dhfile, file:filename()}
           | {eccs, [atom()]}
           | {fail_if_no_peer_cert, boolean()}
+          | {handshake, hello | full}
           | {hibernate_after, timeout()}
           | {honor_cipher_order, boolean()}
           | {honor_ecc_order, boolean()}
@@ -156,6 +157,15 @@ fail_if_no_peer_cert (false)::
 Whether to refuse the connection if the client sends an
 empty certificate.
 
+handshake (full)::
+
+If `hello` is specified for this option, the handshake is
+paused after receiving the client hello message. The handshake
+can then be resumed via `handshake_continue/3`, or cancelled
+via `handshake_cancel/1`.
++
+This option cannot be given to `ranch:handshake/1,2`.
+
 hibernate_after (undefined)::
 
 Time in ms after which SSL socket processes go into

+ 50 - 2
doc/src/manual/ranch_transport.asciidoc

@@ -80,9 +80,17 @@ Get one or more statistic options for the socket.
 [source,erlang]
 ----
 handshake(Socket0  :: socket(),
-          SockOpts :: any(),
           Timeout  :: timeout())
-    -> {ok, Socket}
+    -> {ok, Socket :: socket()}
+     | {ok, Socket :: socket(), Info :: any()}
+     | {error, any()}
+
+handshake(Socket0  :: socket(),
+          SockOpts :: opts(),
+          Timeout  :: timeout())
+    -> {ok, Socket :: socket()}
+     | {ok, Socket :: socket(), Info :: any()}
+     | {error, any()}
 ----
 
 Perform the transport-level handshake.
@@ -92,11 +100,51 @@ before performing any socket operation. It allows
 transports that require extra initialization to perform
 their task and return a socket that is ready to use.
 
+If the handshake is completed by this call, the function will
+return `{ok, Socket}`. However, some transports (notably,
+`ranch_ssl` if `{handshake, hello}` is specified in the socket
+options) may pause the handshake at a certain point and return
+`{ok, Socket, Info}` instead, in order to allow for
+additional decisions to be made before resuming the handshake
+with `handshake_continue/3` or cancelling it with
+`handshake_cancel/1`.
+
 This function may also be used to upgrade a connection
 from a transport to another depending on the capabilities
 of the transports. For example a `ranch_tcp` socket may
 be upgraded to a `ranch_ssl` one using this function.
 
+=== handshake_continue
+
+[source,erlang]
+----
+handshake_continue(Socket0  :: socket(),
+                   Timeout  :: timeout())
+    -> {ok, Socket :: socket()}
+     | {error, any()}
+
+handshake_continue(Socket0  :: socket(),
+                   SockOpts :: opts(),
+                   Timeout  :: timeout())
+    -> {ok, Socket :: socket()}
+     | {error, any()}
+----
+
+Resume the paused transport-level handshake and return a socket
+that is ready to use.
+
+This function will be called by connection processes
+to resume a paused handshake.
+
+=== handshake_cancel
+
+[source,erlang]
+----
+handshake_cancel(Socket :: socket()) -> ok
+----
+
+Cancel the paused transport-level handshake.
+
 === listen
 
 [source,erlang]

+ 55 - 19
src/ranch.erl

@@ -22,6 +22,9 @@
 -export([child_spec/5]).
 -export([handshake/1]).
 -export([handshake/2]).
+-export([handshake_continue/1]).
+-export([handshake_continue/2]).
+-export([handshake_cancel/1]).
 -export([recv_proxy_header/2]).
 -export([remove_connection/1]).
 -export([get_status/1]).
@@ -197,28 +200,61 @@ child_spec(Ref, Transport, TransOpts0, Protocol, ProtoOpts) ->
 		Ref, Transport, TransOpts, Protocol, ProtoOpts
 	]}, type => supervisor}.
 
--spec handshake(ref()) -> {ok, ranch_transport:socket()}.
+-spec handshake(ref()) -> {ok, ranch_transport:socket()} | {continue, any()}.
 handshake(Ref) ->
-	handshake(Ref, []).
+	handshake1(Ref, undefined).
 
--spec handshake(ref(), any()) -> {ok, ranch_transport:socket()}.
+-spec handshake(ref(), any()) -> {ok, ranch_transport:socket()} | {continue, any()}.
 handshake(Ref, Opts) ->
-	receive {handshake, Ref, Transport, CSocket, HandshakeTimeout} ->
-		case Transport:handshake(CSocket, Opts, HandshakeTimeout) of
-			OK = {ok, _} ->
-				OK;
-			%% Garbage was most likely sent to the socket, don't error out.
-			{error, {tls_alert, _}} ->
-				ok = Transport:close(CSocket),
-				exit(normal);
-			%% Socket most likely stopped responding, don't error out.
-			{error, Reason} when Reason =:= timeout; Reason =:= closed ->
-				ok = Transport:close(CSocket),
-				exit(normal);
-			{error, Reason} ->
-				ok = Transport:close(CSocket),
-				error(Reason)
-		end
+	handshake1(Ref, {opts, Opts}).
+
+handshake1(Ref, Opts) ->
+	receive {handshake, Ref, Transport, CSocket, Timeout} ->
+		Handshake = handshake_transport(Transport, handshake, CSocket, Opts, Timeout),
+		handshake_result(Handshake, Ref, Transport, CSocket, Timeout)
+	end.
+
+-spec handshake_continue(ref()) -> {ok, ranch_transport:socket()}.
+handshake_continue(Ref) ->
+	handshake_continue1(Ref, undefined).
+
+-spec handshake_continue(ref(), any()) -> {ok, ranch_transport:socket()}.
+handshake_continue(Ref, Opts) ->
+	handshake_continue1(Ref, {opts, Opts}).
+
+handshake_continue1(Ref, Opts) ->
+	receive {handshake_continue, Ref, Transport, CSocket, Timeout} ->
+		Handshake = handshake_transport(Transport, handshake_continue, CSocket, Opts, Timeout),
+		handshake_result(Handshake, Ref, Transport, CSocket, Timeout)
+	end.
+
+handshake_transport(Transport, Fun, CSocket, undefined, Timeout) ->
+	Transport:Fun(CSocket, Timeout);
+handshake_transport(Transport, Fun, CSocket, {opts, Opts}, Timeout) ->
+	Transport:Fun(CSocket, Opts, Timeout).
+
+handshake_result(Result, Ref, Transport, CSocket, Timeout) ->
+	case Result of
+		OK = {ok, _} ->
+			OK;
+		{ok, CSocket2, Info} ->
+			self() ! {handshake_continue, Ref, Transport, CSocket2, Timeout},
+			{continue, Info};
+		{error, {tls_alert, _}} ->
+			ok = Transport:close(CSocket),
+			exit(normal);
+		{error, Reason} when Reason =:= timeout; Reason =:= closed ->
+			ok = Transport:close(CSocket),
+			exit(normal);
+		{error, Reason} ->
+			ok = Transport:close(CSocket),
+			error(Reason)
+	end.
+
+-spec handshake_cancel(ref()) -> ok.
+handshake_cancel(Ref) ->
+	receive {handshake_continue, Ref, Transport, CSocket, _} ->
+		Transport:handshake_cancel(CSocket)
 	end.
 
 %% Unlike handshake/2 this function always return errors because

+ 34 - 3
src/ranch_ssl.erl

@@ -21,7 +21,11 @@
 -export([listen/1]).
 -export([disallowed_listen_options/0]).
 -export([accept/2]).
+-export([handshake/2]).
 -export([handshake/3]).
+-export([handshake_continue/2]).
+-export([handshake_continue/3]).
+-export([handshake_cancel/1]).
 -export([connect/3]).
 -export([connect/4]).
 -export([recv/3]).
@@ -56,6 +60,7 @@
 	%% @todo Update when ssl exports named_curve().
 	| {eccs, [atom()]}
 	| {fail_if_no_peer_cert, boolean()}
+	| {handshake, hello | full}
 	| {hibernate_after, timeout()}
 	| {honor_cipher_order, boolean()}
 	| {honor_ecc_order, boolean()}
@@ -138,16 +143,42 @@ disallowed_listen_options() ->
 accept(LSocket, Timeout) ->
 	ssl:transport_accept(LSocket, Timeout).
 
+-spec handshake(inet:socket() | ssl:sslsocket(), timeout())
+	-> {ok, ssl:sslsocket()} | {ok, ssl:sslsocket(), ssl:protocol_extensions()} | {error, any()}.
+handshake(CSocket, Timeout) ->
+	handshake(CSocket, [], Timeout).
+
 -spec handshake(inet:socket() | ssl:sslsocket(), opts(), timeout())
-	-> {ok, ssl:sslsocket()} | {error, any()}.
+	-> {ok, ssl:sslsocket()} | {ok, ssl:sslsocket(), ssl:protocol_extensions()} | {error, any()}.
 handshake(CSocket, Opts, Timeout) ->
 	case ssl:handshake(CSocket, Opts, Timeout) of
-		{ok, NewSocket} ->
-			{ok, NewSocket};
+		OK = {ok, _} ->
+			OK;
+		OK = {ok, _, _} ->
+			OK;
+		Error = {error, _} ->
+			Error
+	end.
+
+-spec handshake_continue(ssl:sslsocket(), timeout())
+	-> {ok, ssl:sslsocket()} | {error, any()}.
+handshake_continue(CSocket, Timeout) ->
+	handshake_continue(CSocket, [], Timeout).
+
+-spec handshake_continue(ssl:sslsocket(), [ssl:tls_server_option()], timeout())
+	-> {ok, ssl:sslsocket()} | {error, any()}.
+handshake_continue(CSocket, Opts, Timeout) ->
+	case ssl:handshake_continue(CSocket, Opts, Timeout) of
+		OK = {ok, _} ->
+			OK;
 		Error = {error, _} ->
 			Error
 	end.
 
+-spec handshake_cancel(ssl:sslsocket()) -> ok.
+handshake_cancel(CSocket) ->
+	ok = ssl:handshake_cancel(CSocket).
+
 %% @todo Probably filter Opts?
 -spec connect(inet:ip_address() | inet:hostname(),
 	inet:port_number(), any())

+ 20 - 0
src/ranch_tcp.erl

@@ -21,7 +21,11 @@
 -export([listen/1]).
 -export([disallowed_listen_options/0]).
 -export([accept/2]).
+-export([handshake/2]).
 -export([handshake/3]).
+-export([handshake_continue/2]).
+-export([handshake_continue/3]).
+-export([handshake_cancel/1]).
 -export([connect/3]).
 -export([connect/4]).
 -export([recv/3]).
@@ -105,10 +109,26 @@ disallowed_listen_options() ->
 accept(LSocket, Timeout) ->
 	gen_tcp:accept(LSocket, Timeout).
 
+-spec handshake(inet:socket(), timeout()) -> {ok, inet:socket()}.
+handshake(CSocket, Timeout) ->
+	handshake(CSocket, [], Timeout).
+
 -spec handshake(inet:socket(), opts(), timeout()) -> {ok, inet:socket()}.
 handshake(CSocket, _, _) ->
 	{ok, CSocket}.
 
+-spec handshake_continue(inet:socket(), timeout()) -> no_return().
+handshake_continue(CSocket, Timeout) ->
+	handshake_continue(CSocket, [], Timeout).
+
+-spec handshake_continue(inet:socket(), opts(), timeout()) -> no_return().
+handshake_continue(_, _, _) ->
+	error(not_supported).
+
+-spec handshake_cancel(inet:socket()) -> no_return().
+handshake_cancel(_) ->
+	error(not_supported).
+
 %% @todo Probably filter Opts?
 -spec connect(inet:ip_address() | inet:hostname(),
 	inet:port_number(), any())

+ 5 - 1
src/ranch_transport.erl

@@ -30,7 +30,11 @@
 -callback listen(ranch:transport_opts(any())) -> {ok, socket()} | {error, atom()}.
 -callback accept(socket(), timeout())
 	-> {ok, socket()} | {error, closed | timeout | atom()}.
--callback handshake(socket(), opts(), timeout()) -> {ok, socket()} | {error, any()}.
+-callback handshake(socket(), timeout()) -> {ok, socket()} | {ok, socket(), any()} | {error, any()}.
+-callback handshake(socket(), opts(), timeout()) -> {ok, socket()} | {ok, socket(), any()} | {error, any()}.
+-callback handshake_continue(socket(), timeout()) -> {ok, socket()} | {error, any()}.
+-callback handshake_continue(socket(), opts(), timeout()) -> {ok, socket()} | {error, any()}.
+-callback handshake_cancel(socket()) -> ok.
 -callback connect(string(), inet:port_number(), opts())
 	-> {ok, socket()} | {error, atom()}.
 -callback connect(string(), inet:port_number(), opts(), timeout())

+ 30 - 0
test/acceptor_SUITE.erl

@@ -57,6 +57,7 @@ groups() ->
 		ssl_echo,
 		ssl_local_echo,
 		ssl_graceful,
+		ssl_handshake,
 		ssl_sni_echo,
 		ssl_sni_fail,
 		ssl_upgrade_from_tcp,
@@ -533,6 +534,35 @@ ssl_echo(_) ->
 	{'EXIT', _} = begin catch ranch:get_port(Name) end,
 	ok.
 
+ssl_handshake(_) ->
+	doc("Ensure that multiple steps handshake works with SSL transport."),
+	Name = name(),
+	{CaCert1, Cert1, Key1} = ct_helper:make_certs(),
+	{CaCert2, Cert2, Key2} = ct_helper:make_certs(),
+	Opts1 = [{cert, Cert1}, {key, Key1}, {cacerts, [CaCert1]}, {verify, verify_peer}],
+	Opts2 = [{cert, Cert2}, {key, Key2}, {cacerts, [CaCert2]}, {verify, verify_peer}],
+	DefaultOpts = ct_helper:get_certs_from_ets(),
+	{ok, _} = ranch:start_listener(Name,
+		ranch_ssl, [{handshake, hello}|DefaultOpts],
+		handshake_protocol, #{"ranch1" => Opts1, "ranch2" => Opts2}),
+	Port = ranch:get_port(Name),
+	{ok, Socket1} = ssl:connect("localhost", Port, [binary, {active, false}, {packet, raw},
+		{server_name_indication, "ranch1"}], 5000),
+	{ok, Cert1} = ssl:peercert(Socket1),
+	ok = ssl:send(Socket1, <<"SSL Ranch is working!">>),
+	{ok, <<"SSL Ranch is working!">>} = ssl:recv(Socket1, 21, 1000),
+	{ok, Socket2} = ssl:connect("localhost", Port, [binary, {active, false}, {packet, raw},
+		{server_name_indication, "ranch2"}], 5000),
+	{ok, Cert2} = ssl:peercert(Socket2),
+	ok = ssl:send(Socket2, <<"SSL Ranch is working!">>),
+	{ok, <<"SSL Ranch is working!">>} = ssl:recv(Socket2, 21, 1000),
+	ok = ranch:stop_listener(Name),
+	{error, closed} = ssl:recv(Socket1, 0, 1000),
+	{error, closed} = ssl:recv(Socket2, 0, 1000),
+	%% Make sure the listener stopped.
+	{'EXIT', _} = begin catch ranch:get_port(Name) end,
+	ok.
+
 ssl_local_echo(_) ->
 	case do_os_supports_local_sockets() of
 		true ->

+ 32 - 0
test/handshake_protocol.erl

@@ -0,0 +1,32 @@
+-module(handshake_protocol).
+-behaviour(ranch_protocol).
+
+-export([start_link/3]).
+-export([init/3]).
+
+start_link(Ref, Transport, Opts) ->
+	Pid = spawn_link(?MODULE, init, [Ref, Transport, Opts]),
+	{ok, Pid}.
+
+init(Ref, Transport, Opts) ->
+	SniHost = case ranch:handshake(Ref) of
+		%% Due to a bug in ssl (https://bugs.erlang.org/browse/ERL-951,
+		%% fixed in OTP 22.0.3) the value for sni may be {sni, Hostname}
+		%% instead of Hostname.
+		{continue, #{sni := {sni, Hostname}}} ->
+			Hostname;
+		{continue, #{sni := Hostname}} ->
+			Hostname
+	end,
+	SniHostOpts = maps:get(SniHost, Opts),
+	{ok, Socket} = ranch:handshake_continue(Ref, SniHostOpts),
+	loop(Socket, Transport).
+
+loop(Socket, Transport) ->
+	case Transport:recv(Socket, 0, 5000) of
+		{ok, Data} ->
+			Transport:send(Socket, Data),
+			loop(Socket, Transport);
+		_ ->
+			ok = Transport:close(Socket)
+	end.