From ef58e15547ee171a716eaa768374e2e7e2f7d397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Wed, 25 Oct 2017 20:17:21 +0100 Subject: Introduce cowboy_req:sock/1 and cowboy_req:cert/1 To obtain the local socket ip/port and the client TLS certificate, respectively. --- doc/src/manual/cowboy_req.asciidoc | 11 ++++- doc/src/manual/cowboy_req.cert.asciidoc | 71 +++++++++++++++++++++++++++++++++ doc/src/manual/cowboy_req.peer.asciidoc | 10 +++-- doc/src/manual/cowboy_req.sock.asciidoc | 51 +++++++++++++++++++++++ src/cowboy_http.erl | 55 +++++++++++++++++++------ src/cowboy_http2.erl | 62 ++++++++++++++++++++-------- src/cowboy_req.erl | 10 +++++ test/cowboy_test.erl | 1 + test/req_SUITE.erl | 32 ++++++++++++++- 9 files changed, 268 insertions(+), 35 deletions(-) create mode 100644 doc/src/manual/cowboy_req.cert.asciidoc create mode 100644 doc/src/manual/cowboy_req.sock.asciidoc diff --git a/doc/src/manual/cowboy_req.asciidoc b/doc/src/manual/cowboy_req.asciidoc index 7f026c3..b038764 100644 --- a/doc/src/manual/cowboy_req.asciidoc +++ b/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 } ---- diff --git a/doc/src/manual/cowboy_req.cert.asciidoc b/doc/src/manual/cowboy_req.cert.asciidoc new file mode 100644 index 0000000..c398f60 --- /dev/null +++ b/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)] diff --git a/doc/src/manual/cowboy_req.peer.asciidoc b/doc/src/manual/cowboy_req.peer.asciidoc index a091aa2..0f134b3 100644 --- a/doc/src/manual/cowboy_req.peer.asciidoc +++ b/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)] diff --git a/doc/src/manual/cowboy_req.sock.asciidoc b/doc/src/manual/cowboy_req.sock.asciidoc new file mode 100644 index 0000000..c5e7fa7 --- /dev/null +++ b/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)] diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl index 2ed0840..f6d064e 100644 --- a/src/cowboy_http.erl +++ b/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)'}) diff --git a/src/cowboy_http2.erl b/src/cowboy_http2.erl index e57f02c..d863d1a 100644 --- a/src/cowboy_http2.erl +++ b/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, diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl index 43f7a44..1615c07 100644 --- a/src/cowboy_req.erl +++ b/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. diff --git a/test/cowboy_test.erl b/test/cowboy_test.erl index bdae7e4..31cba63 100644 --- a/test/cowboy_test.erl +++ b/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. diff --git a/test/req_SUITE.erl b/test/req_SUITE.erl index 3b32b3a..26ced62 100644 --- a/test/req_SUITE.erl +++ b/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 -- cgit v1.2.3