Browse Source

Introduce cowboy_req:sock/1 and cowboy_req:cert/1

To obtain the local socket ip/port and the client TLS
certificate, respectively.
Loïc Hoguin 7 years ago
parent
commit
ef58e15547

+ 9 - 2
doc/src/manual/cowboy_req.asciidoc

@@ -29,6 +29,12 @@ and to read the body once.
 
 == Exports
 
+Connection:
+
+* link:man:cowboy_req:peer(3)[cowboy_req:peer(3)] - Peer address and port
+* link:man:cowboy_req:sock(3)[cowboy_req:sock(3)] - Socket address and port
+* link:man:cowboy_req:cert(3)[cowboy_req:cert(3)] - Client TLS certificate
+
 Raw request:
 
 * link:man:cowboy_req:method(3)[cowboy_req:method(3)] - HTTP method
@@ -41,7 +47,6 @@ Raw request:
 * link:man:cowboy_req:uri(3)[cowboy_req:uri(3)] - Reconstructed URI
 * link:man:cowboy_req:header(3)[cowboy_req:header(3)] - HTTP header
 * link:man:cowboy_req:headers(3)[cowboy_req:headers(3)] - HTTP headers
-* link:man:cowboy_req:peer(3)[cowboy_req:peer(3)] - Peer address and port
 
 Processed request:
 
@@ -129,7 +134,9 @@ req() :: #{
     path    := binary(),               %% case sensitive
     qs      := binary(),               %% case sensitive
     headers := cowboy:http_headers(),
-    peer    := {inet:ip_address(), inet:port_number()}
+    peer    := {inet:ip_address(), inet:port_number()},
+    sock    := {inet:ip_address(), inet:port_number()},
+    cert    := binary() | undefined
 }
 ----
 

+ 71 - 0
doc/src/manual/cowboy_req.cert.asciidoc

@@ -0,0 +1,71 @@
+= cowboy_req:cert(3)
+
+== Name
+
+cowboy_req:cert - Client TLS certificate
+
+== Description
+
+[source,erlang]
+----
+cert(Req :: cowboy_req:req()) -> binary() | undefined
+----
+
+Return the peer's TLS certificate.
+
+Using the default configuration this function will always return
+`undefined`. You need to explicitly configure Cowboy to request
+the client certificate. To do this you need to set the `verify`
+transport option to `verify_peer`:
+
+[source,erlang]
+----
+{ok, _} = cowboy:start_tls(example, [
+    {port, 8443},
+    {cert, "path/to/cert.pem"},
+    {verify, verify_peer}
+], #{
+    env => #{dispatch => Dispatch}
+}).
+----
+
+You may also want to customize the `verify_fun` function. Please
+consult the `ssl` application's manual for more details.
+
+TCP connections do not allow a certificate and this function
+will therefore always return `undefined`.
+
+The certificate can also be obtained using pattern matching:
+
+[source,erlang]
+----
+#{cert := Cert} = Req.
+----
+
+== Arguments
+
+Req::
+
+The Req object.
+
+== Return value
+
+The client TLS certificate.
+
+== Changelog
+
+* *2.0*: Function introduced.
+
+== Examples
+
+.Get the client TLS certificate.
+[source,erlang]
+----
+Cert = cowboy_req:cert(Req).
+----
+
+== See also
+
+link:man:cowboy_req(3)[cowboy_req(3)],
+link:man:cowboy_req:peer(3)[cowboy_req:peer(3)],
+link:man:cowboy_req:sock(3)[cowboy_req:sock(3)]

+ 6 - 4
doc/src/manual/cowboy_req.peer.asciidoc

@@ -8,14 +8,14 @@ cowboy_req:peer - Peer address and port
 
 [source,erlang]
 ----
-peer(Req :: cowboy_req:req()) -> Peer
+peer(Req :: cowboy_req:req()) -> Info
 
-Peer :: {inet:ip_address(), inet:port_number()}
+Info :: {inet:ip_address(), inet:port_number()}
 ----
 
 Return the peer's IP address and port number.
 
-The peer can also be obtained using pattern matching:
+The peer information can also be obtained using pattern matching:
 
 [source,erlang]
 ----
@@ -56,4 +56,6 @@ way of determining the source of an HTTP request.
 
 == See also
 
-link:man:cowboy_req(3)[cowboy_req(3)]
+link:man:cowboy_req(3)[cowboy_req(3)],
+link:man:cowboy_req:sock(3)[cowboy_req:sock(3)],
+link:man:cowboy_req:cert(3)[cowboy_req:cert(3)]

+ 51 - 0
doc/src/manual/cowboy_req.sock.asciidoc

@@ -0,0 +1,51 @@
+= cowboy_req:sock(3)
+
+== Name
+
+cowboy_req:sock - Socket address and port
+
+== Description
+
+[source,erlang]
+----
+sock(Req :: cowboy_req:req()) -> Info
+
+Info :: {inet:ip_address(), inet:port_number()}
+----
+
+Return the socket's IP address and port number.
+
+The socket information can also be obtained using pattern matching:
+
+[source,erlang]
+----
+#{sock := {IP, Port}} = Req.
+----
+
+== Arguments
+
+Req::
+
+The Req object.
+
+== Return value
+
+The socket's local IP address and port number.
+
+== Changelog
+
+* *2.0*: Function introduced.
+
+== Examples
+
+.Get the socket's IP address and port number.
+[source,erlang]
+----
+{IP, Port} = cowboy_req:sock(Req).
+----
+
+== See also
+
+link:man:cowboy_req(3)[cowboy_req(3)],
+link:man:cowboy_req:peer(3)[cowboy_req:peer(3)],
+link:man:cowboy_req:cert(3)[cowboy_req:cert(3)]

+ 43 - 12
src/cowboy_http.erl

@@ -85,6 +85,12 @@
 	%% Remote address and port for the connection.
 	peer = undefined :: {inet:ip_address(), inet:port_number()},
 
+	%% Local address and port for the connection.
+	sock = undefined :: {inet:ip_address(), inet:port_number()},
+
+	%% Client certificate (TLS only).
+	cert :: undefined | binary(),
+
 	timer = undefined :: undefined | reference(),
 
 	%% Identifier for the stream currently being read (or waiting to be received).
@@ -115,16 +121,36 @@
 
 -spec init(pid(), ranch:ref(), inet:socket(), module(), cowboy:opts()) -> ok.
 init(Parent, Ref, Socket, Transport, Opts) ->
-	case Transport:peername(Socket) of
-		{ok, Peer} ->
+	Peer0 = Transport:peername(Socket),
+	Sock0 = Transport:sockname(Socket),
+	Cert1 = case Transport:name() of
+		ssl ->
+			case ssl:peercert(Socket) of
+				{error, no_peercert} ->
+					{ok, undefined};
+				Cert0 ->
+					Cert0
+			end;
+		_ ->
+			{ok, undefined}
+	end,
+	case {Peer0, Sock0, Cert1} of
+		{{ok, Peer}, {ok, Sock}, {ok, Cert}} ->
 			LastStreamID = maps:get(max_keepalive, Opts, 100),
 			before_loop(set_timeout(#state{
 				parent=Parent, ref=Ref, socket=Socket,
 				transport=Transport, opts=Opts,
-				peer=Peer, last_streamid=LastStreamID}), <<>>);
-		{error, Reason} ->
-			%% Couldn't read the peer address; connection is gone.
-			terminate(undefined, {socket_error, Reason, 'An error has occurred on the socket.'})
+				peer=Peer, sock=Sock, cert=Cert,
+				last_streamid=LastStreamID}), <<>>);
+		{{error, Reason}, _, _} ->
+			terminate(undefined, {socket_error, Reason,
+				'A socket error occurred when retrieving the peer name.'});
+		{_, {error, Reason}, _} ->
+			terminate(undefined, {socket_error, Reason,
+				'A socket error occurred when retrieving the sock name.'});
+		{_, _, {error, Reason}} ->
+			terminate(undefined, {socket_error, Reason,
+				'A socket error occurred when retrieving the client TLS certificate.'})
 	end.
 
 before_loop(State=#state{socket=Socket, transport=Transport}, Buffer) ->
@@ -559,8 +585,9 @@ default_port(_) -> 80.
 
 %% End of request parsing.
 
-request(Buffer, State0=#state{ref=Ref, transport=Transport, peer=Peer, in_streamid=StreamID,
-		in_state=PS=#ps_header{method=Method, path=Path, qs=Qs, version=Version}},
+request(Buffer, State0=#state{ref=Ref, transport=Transport, peer=Peer, sock=Sock, cert=Cert,
+		in_streamid=StreamID, in_state=
+			PS=#ps_header{method=Method, path=Path, qs=Qs, version=Version}},
 		Headers, Host, Port) ->
 	Scheme = case Transport:secure() of
 		true -> <<"https">>;
@@ -589,6 +616,8 @@ request(Buffer, State0=#state{ref=Ref, transport=Transport, peer=Peer, in_stream
 		pid => self(),
 		streamid => StreamID,
 		peer => Peer,
+		sock => Sock,
+		cert => Cert,
 		method => Method,
 		scheme => Scheme,
 		host => Host,
@@ -644,11 +673,12 @@ is_http2_upgrade(_, _) ->
 
 %% Prior knowledge upgrade, without an HTTP/1.1 request.
 http2_upgrade(State=#state{parent=Parent, ref=Ref, socket=Socket, transport=Transport,
-		opts=Opts, peer=Peer}, Buffer) ->
+		opts=Opts, peer=Peer, sock=Sock, cert=Cert}, Buffer) ->
 	case Transport:secure() of
 		false ->
 			_ = cancel_timeout(State),
-			cowboy_http2:init(Parent, Ref, Socket, Transport, Opts, Peer, Buffer);
+			cowboy_http2:init(Parent, Ref, Socket, Transport, Opts,
+				Peer, Sock, Cert, Buffer);
 		true ->
 			error_terminate(400, State, {connection_error, protocol_error,
 				'Clients that support HTTP/2 over TLS MUST use ALPN. (RFC7540 3.4)'})
@@ -656,7 +686,7 @@ http2_upgrade(State=#state{parent=Parent, ref=Ref, socket=Socket, transport=Tran
 
 %% Upgrade via an HTTP/1.1 request.
 http2_upgrade(State=#state{parent=Parent, ref=Ref, socket=Socket, transport=Transport,
-		opts=Opts, peer=Peer}, Buffer, HTTP2Settings, Req) ->
+		opts=Opts, peer=Peer, sock=Sock, cert=Cert}, Buffer, HTTP2Settings, Req) ->
 	%% @todo
 	%% However if the client sent a body, we need to read the body in full
 	%% and if we can't do that, return a 413 response. Some options are in order.
@@ -664,7 +694,8 @@ http2_upgrade(State=#state{parent=Parent, ref=Ref, socket=Socket, transport=Tran
 	try cow_http_hd:parse_http2_settings(HTTP2Settings) of
 		Settings ->
 			_ = cancel_timeout(State),
-			cowboy_http2:init(Parent, Ref, Socket, Transport, Opts, Peer, Buffer, Settings, Req)
+			cowboy_http2:init(Parent, Ref, Socket, Transport, Opts,
+				Peer, Sock, Cert, Buffer, Settings, Req)
 	catch _:_ ->
 		error_terminate(400, State, {connection_error, protocol_error,
 			'The HTTP2-Settings header must contain a base64 SETTINGS payload. (RFC7540 3.2, RFC7540 3.2.1)'})

+ 46 - 16
src/cowboy_http2.erl

@@ -15,8 +15,8 @@
 -module(cowboy_http2).
 
 -export([init/5]).
--export([init/7]).
 -export([init/9]).
+-export([init/11]).
 
 -export([system_continue/3]).
 -export([system_terminate/4]).
@@ -64,6 +64,12 @@
 	%% Remote address and port for the connection.
 	peer = undefined :: {inet:ip_address(), inet:port_number()},
 
+	%% Local address and port for the connection.
+	sock = undefined :: {inet:ip_address(), inet:port_number()},
+
+	%% Client certificate (TLS only).
+	cert :: undefined | binary(),
+
 	%% Settings are separate for each endpoint. In addition, settings
 	%% must be acknowledged before they can be expected to be applied.
 	%%
@@ -123,19 +129,39 @@
 
 -spec init(pid(), ranch:ref(), inet:socket(), module(), cowboy:opts()) -> ok.
 init(Parent, Ref, Socket, Transport, Opts) ->
-	case Transport:peername(Socket) of
-		{ok, Peer} ->
-			init(Parent, Ref, Socket, Transport, Opts, Peer, <<>>);
-		{error, Reason} ->
-			%% Couldn't read the peer address; connection is gone.
-			terminate(undefined, {socket_error, Reason, 'An error has occurred on the socket.'})
+	Peer0 = Transport:peername(Socket),
+	Sock0 = Transport:sockname(Socket),
+	Cert1 = case Transport:name() of
+		ssl ->
+			case ssl:peercert(Socket) of
+				{error, no_peercert} ->
+					{ok, undefined};
+				Cert0 ->
+					Cert0
+			end;
+		_ ->
+			{ok, undefined}
+	end,
+	case {Peer0, Sock0, Cert1} of
+		{{ok, Peer}, {ok, Sock}, {ok, Cert}} ->
+			init(Parent, Ref, Socket, Transport, Opts, Peer, Sock, Cert, <<>>);
+		{{error, Reason}, _, _} ->
+			terminate(undefined, {socket_error, Reason,
+				'A socket error occurred when retrieving the peer name.'});
+		{_, {error, Reason}, _} ->
+			terminate(undefined, {socket_error, Reason,
+				'A socket error occurred when retrieving the sock name.'});
+		{_, _, {error, Reason}} ->
+			terminate(undefined, {socket_error, Reason,
+				'A socket error occurred when retrieving the client TLS certificate.'})
 	end.
 
 -spec init(pid(), ranch:ref(), inet:socket(), module(), cowboy:opts(),
-	{inet:ip_address(), inet:port_number()}, binary()) -> ok.
-init(Parent, Ref, Socket, Transport, Opts, Peer, Buffer) ->
+	{inet:ip_address(), inet:port_number()}, {inet:ip_address(), inet:port_number()},
+	binary() | undefined, binary()) -> ok.
+init(Parent, Ref, Socket, Transport, Opts, Peer, Sock, Cert, Buffer) ->
 	State = #state{parent=Parent, ref=Ref, socket=Socket,
-		transport=Transport, opts=Opts, peer=Peer,
+		transport=Transport, opts=Opts, peer=Peer, sock=Sock, cert=Cert,
 		parse_state={preface, sequence, preface_timeout(Opts)}},
 	preface(State),
 	case Buffer of
@@ -145,10 +171,11 @@ init(Parent, Ref, Socket, Transport, Opts, Peer, Buffer) ->
 
 %% @todo Add an argument for the request body.
 -spec init(pid(), ranch:ref(), inet:socket(), module(), cowboy:opts(),
-	{inet:ip_address(), inet:port_number()}, binary(), map() | undefined, cowboy_req:req()) -> ok.
-init(Parent, Ref, Socket, Transport, Opts, Peer, Buffer, _Settings, Req) ->
+	{inet:ip_address(), inet:port_number()}, {inet:ip_address(), inet:port_number()},
+	binary() | undefined, binary(), map() | undefined, cowboy_req:req()) -> ok.
+init(Parent, Ref, Socket, Transport, Opts, Peer, Sock, Cert, Buffer, _Settings, Req) ->
 	State0 = #state{parent=Parent, ref=Ref, socket=Socket,
-		transport=Transport, opts=Opts, peer=Peer,
+		transport=Transport, opts=Opts, peer=Peer, sock=Sock, cert=Cert,
 		parse_state={preface, sequence, preface_timeout(Opts)}},
 	%% @todo Apply settings.
 	%% StreamID from HTTP/1.1 Upgrade requests is always 1.
@@ -720,9 +747,10 @@ stream_decode_init(State=#state{socket=Socket, transport=Transport,
 			'Error while trying to decode HPACK-encoded header block. (RFC7540 4.3)'})
 	end.
 
-stream_req_init(State=#state{ref=Ref, peer=Peer}, StreamID, IsFin, Headers0=#{
-		<<":method">> := Method, <<":scheme">> := Scheme,
-		<<":authority">> := Authority, <<":path">> := PathWithQs}) ->
+stream_req_init(State=#state{ref=Ref, peer=Peer, sock=Sock, cert=Cert},
+		StreamID, IsFin, Headers0=#{
+			<<":method">> := Method, <<":scheme">> := Scheme,
+			<<":authority">> := Authority, <<":path">> := PathWithQs}) ->
 	Headers = maps:without([<<":method">>, <<":scheme">>, <<":authority">>, <<":path">>], Headers0),
 	BodyLength = case Headers of
 		_ when IsFin =:= fin ->
@@ -746,6 +774,8 @@ stream_req_init(State=#state{ref=Ref, peer=Peer}, StreamID, IsFin, Headers0=#{
 		pid => self(),
 		streamid => StreamID,
 		peer => Peer,
+		sock => Sock,
+		cert => Cert,
 		method => Method,
 		scheme => Scheme,
 		host => Host,

+ 10 - 0
src/cowboy_req.erl

@@ -19,6 +19,8 @@
 -export([method/1]).
 -export([version/1]).
 -export([peer/1]).
+-export([sock/1]).
+-export([cert/1]).
 -export([scheme/1]).
 -export([host/1]).
 -export([host_info/1]).
@@ -151,6 +153,14 @@ version(#{version := Version}) ->
 peer(#{peer := Peer}) ->
 	Peer.
 
+-spec sock(req()) -> {inet:ip_address(), inet:port_number()}.
+sock(#{sock := Sock}) ->
+	Sock.
+
+-spec cert(req()) -> binary() | undefined.
+cert(#{cert := Cert}) ->
+	Cert.
+
 -spec scheme(req()) -> binary().
 scheme(#{scheme := Scheme}) ->
 	Scheme.

+ 1 - 0
test/cowboy_test.erl

@@ -110,6 +110,7 @@ gun_open(Config, Opts) ->
 	{ok, ConnPid} = gun:open("localhost", config(port, Config), Opts#{
 		retry => 0,
 		transport => config(type, Config),
+		transport_opts => proplists:get_value(transport_opts, Config, []),
 		protocols => [config(protocol, Config)]
 	}),
 	ConnPid.

+ 31 - 1
test/req_SUITE.erl

@@ -134,6 +134,30 @@ bindings(Config) ->
 	<<"#{key => <<\"bindings\">>}">> = do_get_body("/bindings", Config),
 	ok.
 
+cert(Config) ->
+	case config(type, Config) of
+		tcp -> doc("TLS certificates can only be provided over TLS.");
+		ssl -> do_cert(Config)
+	end.
+
+do_cert(Config0) ->
+	doc("A client TLS certificate was provided."),
+	{CaCert, Cert, Key} = ct_helper:make_certs(),
+	Config = [{transport_opts, [
+		{cert, Cert},
+		{key, Key},
+		{cacerts, [CaCert]}
+	]}|Config0],
+	Cert = do_get_body("/cert", Config),
+	Cert = do_get_body("/direct/cert", Config),
+	ok.
+
+cert_undefined(Config) ->
+	doc("No client TLS certificate was provided."),
+	<<"undefined">> = do_get_body("/cert", Config),
+	<<"undefined">> = do_get_body("/direct/cert", Config),
+	ok.
+
 header(Config) ->
 	doc("Request header with/without default."),
 	<<"value">> = do_get_body("/args/header/defined", [{<<"defined">>, "value"}], Config),
@@ -274,7 +298,7 @@ path_info(Config) ->
 	ok.
 
 peer(Config) ->
-	doc("Request peer."),
+	doc("Remote socket address."),
 	<<"{{127,0,0,1},", _/bits >> = do_get_body("/peer", Config),
 	<<"{{127,0,0,1},", _/bits >> = do_get_body("/direct/peer", Config),
 	ok.
@@ -309,6 +333,12 @@ do_scheme(Path, Config) ->
 		<<"https">> when Transport =:= ssl -> ok
 	end.
 
+sock(Config) ->
+	doc("Local socket address."),
+	<<"{{127,0,0,1},", _/bits >> = do_get_body("/sock", Config),
+	<<"{{127,0,0,1},", _/bits >> = do_get_body("/direct/sock", Config),
+	ok.
+
 uri(Config) ->
 	doc("Request URI building/modification."),
 	Scheme = case config(type, Config) of