0
Fork 0
mirror of https://github.com/ninenines/cowboy.git synced 2025-07-14 20:30:23 +00:00

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

To obtain the local socket ip/port and the client TLS
certificate, respectively.
This commit is contained in:
Loïc Hoguin 2017-10-25 20:17:21 +01:00
parent 4090adaecc
commit ef58e15547
No known key found for this signature in database
GPG key ID: 71366FF21851DF03
9 changed files with 268 additions and 35 deletions

View file

@ -29,6 +29,12 @@ and to read the body once.
== Exports == 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: Raw request:
* link:man:cowboy_req:method(3)[cowboy_req:method(3)] - HTTP method * 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:uri(3)[cowboy_req:uri(3)] - Reconstructed URI
* link:man:cowboy_req:header(3)[cowboy_req:header(3)] - HTTP header * 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:headers(3)[cowboy_req:headers(3)] - HTTP headers
* link:man:cowboy_req:peer(3)[cowboy_req:peer(3)] - Peer address and port
Processed request: Processed request:
@ -129,7 +134,9 @@ req() :: #{
path := binary(), %% case sensitive path := binary(), %% case sensitive
qs := binary(), %% case sensitive qs := binary(), %% case sensitive
headers := cowboy:http_headers(), 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
} }
---- ----

View file

@ -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)]

View file

@ -8,14 +8,14 @@ cowboy_req:peer - Peer address and port
[source,erlang] [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. 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] [source,erlang]
---- ----
@ -56,4 +56,6 @@ way of determining the source of an HTTP request.
== See also == 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)]

View file

@ -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)]

View file

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

View file

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

View file

@ -19,6 +19,8 @@
-export([method/1]). -export([method/1]).
-export([version/1]). -export([version/1]).
-export([peer/1]). -export([peer/1]).
-export([sock/1]).
-export([cert/1]).
-export([scheme/1]). -export([scheme/1]).
-export([host/1]). -export([host/1]).
-export([host_info/1]). -export([host_info/1]).
@ -151,6 +153,14 @@ version(#{version := Version}) ->
peer(#{peer := Peer}) -> peer(#{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(). -spec scheme(req()) -> binary().
scheme(#{scheme := Scheme}) -> scheme(#{scheme := Scheme}) ->
Scheme. Scheme.

View file

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

View file

@ -134,6 +134,30 @@ bindings(Config) ->
<<"#{key => <<\"bindings\">>}">> = do_get_body("/bindings", Config), <<"#{key => <<\"bindings\">>}">> = do_get_body("/bindings", Config),
ok. 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) -> header(Config) ->
doc("Request header with/without default."), doc("Request header with/without default."),
<<"value">> = do_get_body("/args/header/defined", [{<<"defined">>, "value"}], Config), <<"value">> = do_get_body("/args/header/defined", [{<<"defined">>, "value"}], Config),
@ -274,7 +298,7 @@ path_info(Config) ->
ok. ok.
peer(Config) -> 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("/peer", Config),
<<"{{127,0,0,1},", _/bits >> = do_get_body("/direct/peer", Config), <<"{{127,0,0,1},", _/bits >> = do_get_body("/direct/peer", Config),
ok. ok.
@ -309,6 +333,12 @@ do_scheme(Path, Config) ->
<<"https">> when Transport =:= ssl -> ok <<"https">> when Transport =:= ssl -> ok
end. 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) -> uri(Config) ->
doc("Request URI building/modification."), doc("Request URI building/modification."),
Scheme = case config(type, Config) of Scheme = case config(type, Config) of