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. --- src/cowboy_http.erl | 55 ++++++++++++++++++++++++++++++++++++---------- src/cowboy_http2.erl | 62 ++++++++++++++++++++++++++++++++++++++-------------- src/cowboy_req.erl | 10 +++++++++ 3 files changed, 99 insertions(+), 28 deletions(-) (limited to 'src') 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. -- cgit v1.2.3