diff --git a/Makefile b/Makefile index 55dfd711e..2d3989929 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ dep_goldrush = git https://github.com/basho/goldrush 0.1.9 dep_gproc = git https://github.com/uwiger/gproc dep_getopt = git https://github.com/jcomellas/getopt v0.8.2 dep_lager = git https://github.com/basho/lager master -dep_esockd = git https://github.com/emqtt/esockd v5.1 +dep_esockd = git https://github.com/emqtt/esockd v5.2 dep_ekka = git https://github.com/emqtt/ekka master dep_mochiweb = git https://github.com/emqtt/mochiweb v4.2.0 dep_pbkdf2 = git https://github.com/emqtt/pbkdf2 2.0.1 diff --git a/etc/emq.conf b/etc/emq.conf index d6dacc8fb..db62e1d49 100644 --- a/etc/emq.conf +++ b/etc/emq.conf @@ -340,6 +340,9 @@ listener.tcp.external.access.2 = allow all ## listener.tcp.external.proxy_protocol = on ## listener.tcp.external.proxy_protocol_timeout = 3s +### Use the PP2_SUBTYPE_SSL_CN field from Proxy Protocol V2 as a username. +## listener.tcp.external.peer_cert_as_username = cn + ## TCP Socket Options listener.tcp.external.backlog = 1024 @@ -508,6 +511,10 @@ listener.ws.external.max_clients = 64 listener.ws.external.access.1 = allow all +## Proxy Protocol V1/2 +## listener.ws.external.proxy_protocol = on +## listener.ws.external.proxy_protocol_timeout = 3s + ## TCP Options listener.ws.external.backlog = 1024 @@ -534,6 +541,10 @@ listener.wss.external.max_clients = 64 listener.wss.external.access.1 = allow all +## Proxy Protocol V1/2 +## listener.wss.external.proxy_protocol = on +## listener.wss.external.proxy_protocol_timeout = 3s + ## SSL Options listener.wss.external.handshake_timeout = 15s diff --git a/priv/emq.schema b/priv/emq.schema index e8746582b..aaefce4c2 100644 --- a/priv/emq.schema +++ b/priv/emq.schema @@ -795,15 +795,17 @@ end}. ]}. {mapping, "listener.tcp.$name.proxy_protocol", "emqttd.listeners", [ - %%{default, off}, {datatype, flag} ]}. {mapping, "listener.tcp.$name.proxy_protocol_timeout", "emqttd.listeners", [ - %%{default, "5s"}, {datatype, {duration, ms}} ]}. +{mapping, "listener.tcp.$name.peer_cert_as_username", "emqttd.listeners", [ + {datatype, {enum, [cn, dn]}} +]}. + {mapping, "listener.tcp.$name.backlog", "emqttd.listeners", [ {datatype, integer}, {default, 1024} @@ -879,12 +881,10 @@ end}. ]}. {mapping, "listener.ssl.$name.proxy_protocol", "emqttd.listeners", [ - %%{default, off}, {datatype, flag} ]}. {mapping, "listener.ssl.$name.proxy_protocol_timeout", "emqttd.listeners", [ - %%{default, "5s"}, {datatype, {duration, ms}} ]}. @@ -1011,6 +1011,14 @@ end}. {datatype, string} ]}. +{mapping, "listener.ws.$name.proxy_protocol", "emqttd.listeners", [ + {datatype, flag} +]}. + +{mapping, "listener.ws.$name.proxy_protocol_timeout", "emqttd.listeners", [ + {datatype, {duration, ms}} +]}. + {mapping, "listener.ws.$name.backlog", "emqttd.listeners", [ {default, 1024}, {datatype, integer} @@ -1084,6 +1092,14 @@ end}. {datatype, string} ]}. +{mapping, "listener.wss.$name.proxy_protocol", "emqttd.listeners", [ + {datatype, flag} +]}. + +{mapping, "listener.wss.$name.proxy_protocol_timeout", "emqttd.listeners", [ + {datatype, {duration, ms}} +]}. + {mapping, "listener.wss.$name.backlog", "emqttd.listeners", [ {default, 1024}, {datatype, integer} diff --git a/src/emqttd_protocol.erl b/src/emqttd_protocol.erl index 4505b73c6..d021c9e1f 100644 --- a/src/emqttd_protocol.erl +++ b/src/emqttd_protocol.erl @@ -69,7 +69,7 @@ init(Peername, SendFun, Opts) -> max_clientid_len = MaxLen, is_superuser = false, client_pid = self(), - peercert_username = false, + peercert_username = undefined, ws_initial_headers = WsInitialHeaders, keepalive_backoff = Backoff, stats_data = #proto_stats{enable_stats = EnableStats}}. @@ -82,23 +82,16 @@ enrich_opt([], _Conn, State) -> enrich_opt([{mountpoint, MountPoint} | ConnOpts], Conn, State) -> enrich_opt(ConnOpts, Conn, State#proto_state{mountpoint = MountPoint}); enrich_opt([{peer_cert_as_username, N} | ConnOpts], Conn, State) -> - case Conn:type() of - ssl -> enrich_opt(ConnOpts, Conn, State#proto_state{ - peercert_username = peercert_username(N, Conn:peercert())}); - _ -> enrich_opt(ConnOpts, Conn, State) - end; + enrich_opt(ConnOpts, Conn, State#proto_state{peercert_username = peercert_username(N, Conn)}); enrich_opt([_ | ConnOpts], Conn, State) -> enrich_opt(ConnOpts, Conn, State). -peercert_username(cn, Cert) -> - case emqttd_ssl:peer_cert_common_name(Cert) of - not_found -> undefined; - CN -> iolist_to_binary(CN) - end; -peercert_username(dn, Cert) -> - iolist_to_binary(emqttd_ssl:peer_cert_subject(Cert)). +peercert_username(cn, Conn) -> + Conn:peer_cert_common_name(); +peercert_username(dn, Conn) -> + Conn:peer_cert_subject(). -repl_username_with_peercert(State = #proto_state{peercert_username = false}) -> +repl_username_with_peercert(State = #proto_state{peercert_username = undefined}) -> State; repl_username_with_peercert(State = #proto_state{peercert_username = PeerCert}) -> State#proto_state{username = PeerCert}. diff --git a/src/emqttd_ssl.erl b/src/emqttd_ssl.erl deleted file mode 100644 index c4126e820..000000000 --- a/src/emqttd_ssl.erl +++ /dev/null @@ -1,259 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2013-2017 EMQ Enterprise, Inc. (http://emqtt.io) -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- -%% -%% @doc SSL Utility Functions. This module is copied from rabbit_ssl.erl -%% - --module(emqttd_ssl). - --include_lib("public_key/include/public_key.hrl"). - --type(certificate() :: binary()). - --export([peer_cert_issuer/1, peer_cert_subject/1, peer_cert_common_name/1, - peer_cert_subject_items/2, peer_cert_validity/1]). - -%% Return a string describing the certificate's issuer. --spec(peer_cert_issuer(certificate()) -> string()). -peer_cert_issuer(Cert) -> - cert_info(fun(#'OTPCertificate' { - tbsCertificate = #'OTPTBSCertificate' { - issuer = Issuer }}) -> - format_rdn_sequence(Issuer) - end, Cert). - -%% Return a string describing the certificate's subject, as per RFC4514. --spec(peer_cert_subject(certificate()) -> string()). -peer_cert_subject(Cert) -> - cert_info(fun(#'OTPCertificate' { - tbsCertificate = #'OTPTBSCertificate' { - subject = Subject }}) -> - format_rdn_sequence(Subject) - end, Cert). - --spec(peer_cert_common_name(certificate()) -> string() | 'not_found'). -peer_cert_common_name(Cert) -> - case peer_cert_subject_items(Cert, ?'id-at-commonName') of - not_found -> not_found; - CNs -> string:join(CNs, ",") - end. - -%% Return the parts of the certificate's subject. --spec(peer_cert_subject_items(certificate(), tuple()) -> [string()] | 'undefined'). -peer_cert_subject_items(Cert, Type) -> - cert_info(fun(#'OTPCertificate' { - tbsCertificate = #'OTPTBSCertificate' { - subject = Subject }}) -> - find_by_type(Type, Subject) - end, Cert). - -%% Return a string describing the certificate's validity. --spec(peer_cert_validity(certificate()) -> string()). -peer_cert_validity(Cert) -> - cert_info(fun(#'OTPCertificate' { - tbsCertificate = #'OTPTBSCertificate' { - validity = {'Validity', Start, End} }}) -> - format("~s - ~s", [format_asn1_value(Start), - format_asn1_value(End)]) - end, Cert). - -cert_info(F, {ok, Cert}) -> - F(case public_key:pkix_decode_cert(Cert, otp) of - {ok, DecCert} -> DecCert; %%pre R14B - DecCert -> DecCert %%R14B onwards - end). - -find_by_type(Type, {rdnSequence, RDNs}) -> - case [V || #'AttributeTypeAndValue'{type = T, value = V} - <- lists:flatten(RDNs), - T == Type] of - [] -> not_found; - L -> [format_asn1_value(V) || V <- L] - end. - -%%-------------------------------------------------------------------------- -%% Formatting functions. -%%-------------------------------------------------------------------------- - -%% Format and rdnSequence as a RFC4514 subject string. -format_rdn_sequence({rdnSequence, Seq}) -> - string:join(lists:reverse([format_complex_rdn(RDN) || RDN <- Seq]), ","). - -%% Format an RDN set. -format_complex_rdn(RDNs) -> - string:join([format_rdn(RDN) || RDN <- RDNs], "+"). - -%% Format an RDN. If the type name is unknown, use the dotted decimal -%% representation. See RFC4514, section 2.3. -format_rdn(#'AttributeTypeAndValue'{type = T, value = V}) -> - FV = escape_rdn_value(format_asn1_value(V)), - Fmts = [{?'id-at-surname' , "SN"}, - {?'id-at-givenName' , "GIVENNAME"}, - {?'id-at-initials' , "INITIALS"}, - {?'id-at-generationQualifier' , "GENERATIONQUALIFIER"}, - {?'id-at-commonName' , "CN"}, - {?'id-at-localityName' , "L"}, - {?'id-at-stateOrProvinceName' , "ST"}, - {?'id-at-organizationName' , "O"}, - {?'id-at-organizationalUnitName' , "OU"}, - {?'id-at-title' , "TITLE"}, - {?'id-at-countryName' , "C"}, - {?'id-at-serialNumber' , "SERIALNUMBER"}, - {?'id-at-pseudonym' , "PSEUDONYM"}, - {?'id-domainComponent' , "DC"}, - {?'id-emailAddress' , "EMAILADDRESS"}, - {?'street-address' , "STREET"}, - {{0,9,2342,19200300,100,1,1} , "UID"}], %% Not in public_key.hrl - case proplists:lookup(T, Fmts) of - {_, Fmt} -> - format(Fmt ++ "=~s", [FV]); - none when is_tuple(T) -> - TypeL = [format("~w", [X]) || X <- tuple_to_list(T)], - format("~s=~s", [string:join(TypeL, "."), FV]); - none -> - format("~p=~s", [T, FV]) - end. - -%% Escape a string as per RFC4514. -escape_rdn_value(V) -> - escape_rdn_value(V, start). - -escape_rdn_value([], _) -> - []; -escape_rdn_value([C | S], start) when C =:= $ ; C =:= $# -> - [$\\, C | escape_rdn_value(S, middle)]; -escape_rdn_value(S, start) -> - escape_rdn_value(S, middle); -escape_rdn_value([$ ], middle) -> - [$\\, $ ]; -escape_rdn_value([C | S], middle) when C =:= $"; C =:= $+; C =:= $,; C =:= $;; - C =:= $<; C =:= $>; C =:= $\\ -> - [$\\, C | escape_rdn_value(S, middle)]; -escape_rdn_value([C | S], middle) when C < 32 ; C >= 126 -> - %% Of ASCII characters only U+0000 needs escaping, but for display - %% purposes it's handy to escape all non-printable chars. All non-ASCII - %% characters get converted to UTF-8 sequences and then escaped. We've - %% already got a UTF-8 sequence here, so just escape it. - rabbit_misc:format("\\~2.16.0B", [C]) ++ escape_rdn_value(S, middle); -escape_rdn_value([C | S], middle) -> - [C | escape_rdn_value(S, middle)]. - -%% Get the string representation of an OTPCertificate field. -format_asn1_value({ST, S}) when ST =:= teletexString; ST =:= printableString; - ST =:= universalString; ST =:= utf8String; - ST =:= bmpString -> - format_directory_string(ST, S); -format_asn1_value({utcTime, [Y1, Y2, M1, M2, D1, D2, H1, H2, - Min1, Min2, S1, S2, $Z]}) -> - format("20~c~c-~c~c-~c~cT~c~c:~c~c:~c~cZ", - [Y1, Y2, M1, M2, D1, D2, H1, H2, Min1, Min2, S1, S2]); -%% We appear to get an untagged value back for an ia5string -%% (e.g. domainComponent). -format_asn1_value(V) when is_list(V) -> - V; -format_asn1_value(V) when is_binary(V) -> - %% OTP does not decode some values when combined with an unknown - %% type. That's probably wrong, so as a last ditch effort let's - %% try manually decoding. 'DirectoryString' is semi-arbitrary - - %% but it is the type which covers the various string types we - %% handle below. - try - {ST, S} = public_key:der_decode('DirectoryString', V), - format_directory_string(ST, S) - catch _:_ -> - format("~p", [V]) - end; -format_asn1_value(V) -> - format("~p", [V]). - -%% DirectoryString { INTEGER : maxSize } ::= CHOICE { -%% teletexString TeletexString (SIZE (1..maxSize)), -%% printableString PrintableString (SIZE (1..maxSize)), -%% bmpString BMPString (SIZE (1..maxSize)), -%% universalString UniversalString (SIZE (1..maxSize)), -%% uTF8String UTF8String (SIZE (1..maxSize)) } -%% -%% Precise definitions of printable / teletexString are hard to come -%% by. This is what I reconstructed: -%% -%% printableString: -%% "intended to represent the limited character sets available to -%% mainframe input terminals" -%% A-Z a-z 0-9 ' ( ) + , - . / : = ? [space] -%% http://msdn.microsoft.com/en-us/library/bb540814(v=vs.85).aspx -%% -%% teletexString: -%% "a sizable volume of software in the world treats TeletexString -%% (T61String) as a simple 8-bit string with mostly Windows Latin 1 -%% (superset of iso-8859-1) encoding" -%% http://www.mail-archive.com/asn1@asn1.org/msg00460.html -%% -%% (However according to that link X.680 actually defines -%% TeletexString in some much more involved and crazy way. I suggest -%% we treat it as ISO-8859-1 since Erlang does not support Windows -%% Latin 1). -%% -%% bmpString: -%% UCS-2 according to RFC 3641. Hence cannot represent Unicode -%% characters above 65535 (outside the "Basic Multilingual Plane"). -%% -%% universalString: -%% UCS-4 according to RFC 3641. -%% -%% utf8String: -%% UTF-8 according to RFC 3641. -%% -%% Within Rabbit we assume UTF-8 encoding. Since printableString is a -%% subset of ASCII it is also a subset of UTF-8. The others need -%% converting. Fortunately since the Erlang SSL library does the -%% decoding for us (albeit into a weird format, see below), we just -%% need to handle encoding into UTF-8. Note also that utf8Strings come -%% back as binary. -%% -%% Note for testing: the default Ubuntu configuration for openssl will -%% only create printableString or teletexString types no matter what -%% you do. Edit string_mask in the [req] section of -%% /etc/ssl/openssl.cnf to change this (see comments there). You -%% probably also need to set utf8 = yes to get it to accept UTF-8 on -%% the command line. Also note I could not get openssl to generate a -%% universalString. - -format_directory_string(printableString, S) -> S; -format_directory_string(teletexString, S) -> utf8_list_from(S); -format_directory_string(bmpString, S) -> utf8_list_from(S); -format_directory_string(universalString, S) -> utf8_list_from(S); -format_directory_string(utf8String, S) -> binary_to_list(S). - -utf8_list_from(S) -> - binary_to_list( - unicode:characters_to_binary(flatten_ssl_list(S), utf32, utf8)). - -%% The Erlang SSL implementation invents its own representation for -%% non-ascii strings - looking like [97,{0,0,3,187}] (that's LATIN -%% SMALL LETTER A followed by GREEK SMALL LETTER LAMDA). We convert -%% this into a list of unicode characters, which we can tell -%% unicode:characters_to_binary is utf32. - -flatten_ssl_list(L) -> [flatten_ssl_list_item(I) || I <- L]. - -flatten_ssl_list_item({A, B, C, D}) -> - A * (1 bsl 24) + B * (1 bsl 16) + C * (1 bsl 8) + D; -flatten_ssl_list_item(N) when is_number (N) -> - N. - -format(Fmt, Args) -> - lists:flatten(io_lib:format(Fmt, Args)). -