From 66873af3193c4e14d94641eaeab03e464d98a9d9 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 5 Apr 2022 14:26:14 +0200 Subject: [PATCH 1/7] build: add erfmt plugin to root level rebar.config --- rebar.config | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rebar.config b/rebar.config index 6beb3a0b9..94f56ae9d 100644 --- a/rebar.config +++ b/rebar.config @@ -83,3 +83,5 @@ emqx_exhook_pb, % generated code for protobuf emqx_exproto_pb % generated code for protobuf ]}. + +{project_plugins, [erlfmt]}. From 0948417db82086e4471a047907cadb7cc564ff5e Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 5 Apr 2022 14:20:23 +0200 Subject: [PATCH 2/7] fix(emqx_plugin_libs_ssl): handle relative cert paths --- .../src/emqx_plugin_libs_ssl.erl | 120 ++++++++++++------ .../test/emqx_plugin_libs_ssl_tests.erl | 10 +- 2 files changed, 93 insertions(+), 37 deletions(-) diff --git a/apps/emqx_plugin_libs/src/emqx_plugin_libs_ssl.erl b/apps/emqx_plugin_libs/src/emqx_plugin_libs_ssl.erl index 770c05045..9d1c48ba9 100644 --- a/apps/emqx_plugin_libs/src/emqx_plugin_libs_ssl.erl +++ b/apps/emqx_plugin_libs/src/emqx_plugin_libs_ssl.erl @@ -16,12 +16,13 @@ -module(emqx_plugin_libs_ssl). --export([save_files_return_opts/2, - save_files_return_opts/3, - save_file/2 - ]). +-export([ + save_files_return_opts/2, + save_files_return_opts/3, + save_file/2 +]). --type file_input_key() :: atom() | binary(). %% <<"file">> | <<"filename">> +-type file_input_key() :: atom() | binary(). -type file_input() :: #{file_input_key() => binary()}. %% options are below paris @@ -39,6 +40,8 @@ -type opt_value() :: term(). -type opts() :: [{opt_key(), opt_value()}]. +-include_lib("emqx/include/logger.hrl"). + %% @doc Parse ssl options input. %% If the input contains file content, save the files in the given dir. %% Returns ssl options for Erlang's ssl application. @@ -48,8 +51,11 @@ %% In case it's a map, the file is saved in EMQX's `data_dir' %% (unless `SubDir' is an absolute path). %% NOTE: This function is now deprecated, use emqx_tls_lib:ensure_ssl_files/2 instead. --spec save_files_return_opts(opts_input(), atom() | string() | binary(), - string() | binary()) -> opts(). +-spec save_files_return_opts( + opts_input(), + atom() | string() | binary(), + string() | binary() +) -> opts(). save_files_return_opts(Options, SubDir, ResId) -> Dir = filename:join([emqx:data_dir(), SubDir, ResId]), save_files_return_opts(Options, Dir). @@ -70,51 +76,83 @@ save_files_return_opts(Options, Dir) -> KeyFile = Get(keyfile), CertFile = Get(certfile), CAFile = Get(cacertfile), - Key = do_save_file(KeyFile, Dir), - Cert = do_save_file(CertFile, Dir), - CA = do_save_file(CAFile, Dir), + Key = maybe_save_file(KeyFile, Dir), + Cert = maybe_save_file(CertFile, Dir), + CA = maybe_save_file(CAFile, Dir), Verify = GetD(verify, verify_none), - SNI = Get(server_name_indication), + SNI = + case Get(<<"server_name_indication">>) of + undefined -> undefined; + SNI0 -> ensure_str(SNI0) + end, Versions = emqx_tls_lib:integral_versions(Get(versions)), Ciphers = emqx_tls_lib:integral_ciphers(Versions, Get(ciphers)), - filter([{keyfile, Key}, {certfile, Cert}, {cacertfile, CA}, - {verify, Verify}, {server_name_indication, SNI}, - {versions, Versions}, {ciphers, Ciphers}]). + filter([ + {keyfile, Key}, + {certfile, Cert}, + {cacertfile, CA}, + {verify, Verify}, + {server_name_indication, SNI}, + {versions, Versions}, + {ciphers, Ciphers} + ]). %% @doc Save a key or certificate file in data dir, %% and return path of the saved file. %% empty string is returned if the input is empty. -spec save_file(file_input(), atom() | string() | binary()) -> string(). save_file(Param, SubDir) -> - Dir = filename:join([emqx:data_dir(), SubDir]), - do_save_file(Param, Dir). + Dir = filename:join([emqx:data_dir(), SubDir]), + maybe_save_file(Param, Dir). filter([]) -> []; filter([{_, undefined} | T]) -> filter(T); filter([{_, ""} | T]) -> filter(T); filter([H | T]) -> [H | filter(T)]. -do_save_file(#{filename := FileName, file := Content}, Dir) - when FileName =/= undefined andalso Content =/= undefined -> - do_save_file(ensure_str(FileName), iolist_to_binary(Content), Dir); -do_save_file(FilePath, _) when is_list(FilePath) -> +maybe_save_file(#{filename := FileName, file := Content}, Dir) when + FileName =/= undefined andalso Content =/= undefined +-> + maybe_save_file(ensure_str(FileName), iolist_to_binary(Content), Dir); +maybe_save_file(FilePath, _) when is_list(FilePath) -> FilePath; -do_save_file(FilePath, _) when is_binary(FilePath) -> +maybe_save_file(FilePath, _) when is_binary(FilePath) -> ensure_str(FilePath); -do_save_file(_, _) -> "". +maybe_save_file(_, _) -> + "". -do_save_file("", _, _Dir) -> ""; %% ignore -do_save_file(_, <<>>, _Dir) -> ""; %% ignore -do_save_file(FileName, Content, Dir) -> - FullFilename = filename:join([Dir, FileName]), - ok = filelib:ensure_dir(FullFilename), - case file:write_file(FullFilename, Content) of - ok -> - ensure_str(FullFilename); - {error, Reason} -> - logger:error("failed_to_save_ssl_file ~ts: ~0p", [FullFilename, Reason]), - error({"failed_to_save_ssl_file", FullFilename, Reason}) - end. +%% no filename, ignore +maybe_save_file("", _, _Dir) -> + ""; +%% no content, see if file exists +maybe_save_file(FileName, <<>>, Dir) -> + {ok, Cwd} = file:get_cwd(), + %% NOTE: when FileName is an absolute path, filename:join has no effect + CwdFile = ensure_str(filename:join([Cwd, FileName])), + DataDirFile = ensure_str(filename:join([Dir, FileName])), + Possibles = + case CwdFile =:= DataDirFile of + true -> [CwdFile]; + false -> [CwdFile, DataDirFile] + end, + case find_exist_file(FileName, Possibles) of + false -> erlang:throw({bad_cert_file, Possibles}); + Found -> Found + end; +maybe_save_file(FileName, Content, Dir) -> + FullFilename = filename:join([Dir, FileName]), + ok = filelib:ensure_dir(FullFilename), + case file:write_file(FullFilename, Content) of + ok -> + ensure_str(FullFilename); + {error, Reason} -> + ?SLOG(error, #{ + msg => "failed_to_save_ssl_file", + filename => FullFilename, + reason => Reason + }), + error({"failed_to_save_ssl_file", FullFilename, Reason}) + end. ensure_str(L) when is_list(L) -> L; ensure_str(B) when is_binary(B) -> unicode:characters_to_list(B, utf8). @@ -122,8 +160,18 @@ ensure_str(B) when is_binary(B) -> unicode:characters_to_list(B, utf8). -spec fuzzy_map_get(atom() | binary(), map(), any()) -> any(). fuzzy_map_get(Key, Options, Default) -> case maps:find(Key, Options) of - {ok, Val} -> Val; + {ok, Val} -> + Val; error when is_atom(Key) -> fuzzy_map_get(atom_to_binary(Key, utf8), Options, Default); - error -> Default + error -> + Default + end. + +find_exist_file(_Name, []) -> + false; +find_exist_file(Name, [F | Rest]) -> + case filelib:is_regular(F) of + true -> F; + false -> find_exist_file(Name, Rest) end. diff --git a/apps/emqx_plugin_libs/test/emqx_plugin_libs_ssl_tests.erl b/apps/emqx_plugin_libs/test/emqx_plugin_libs_ssl_tests.erl index 4f0d77a23..afac68166 100644 --- a/apps/emqx_plugin_libs/test/emqx_plugin_libs_ssl_tests.erl +++ b/apps/emqx_plugin_libs/test/emqx_plugin_libs_ssl_tests.erl @@ -42,7 +42,8 @@ prop_file_or_content() -> {prop_cert_file_name(), proper_types:binary()}]). prop_cert_file_name() -> - proper_types:oneof(["certname1", <<"certname2">>, "", <<>>, undefined]). + File = code:which(?MODULE), %% existing + proper_types:oneof(["", <<>>, undefined, File]). prop_tls_versions() -> proper_types:oneof(["tlsv1.3", @@ -76,3 +77,10 @@ file_or_content({Name, Content}) -> #{<<"file">> => Content, <<"filename">> => Name}; file_or_content(Name) -> Name. + +bad_cert_file_test() -> + Input = #{<<"keyfile">> => + #{<<"filename">> => "notafile", + <<"file">> => ""}}, + ?assertThrow({bad_cert_file, _}, + emqx_plugin_libs_ssl:save_files_return_opts(Input, "test-data")). From 822a4f9f73e12484b4b33b8ed8e0672d3bc161c8 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 5 Apr 2022 15:41:01 +0200 Subject: [PATCH 3/7] feat: parameterise EMQX_ETC_DIR both at build time and at runtime --- apps/emqx/src/emqx.erl | 28 +++++++++++++++++-- apps/emqx/src/emqx_tls_lib.erl | 2 +- apps/emqx_exhook/src/emqx_exhook_api.erl | 11 ++++---- apps/emqx_gateway/src/emqx_gateway_api.erl | 4 +-- .../src/emqx_gateway_api_listeners.erl | 18 ++++++------ apps/emqx_gateway/src/emqx_gateway_schema.erl | 2 +- .../emqx_plugin_libs/src/emqx_plugin_libs.erl | 1 - bin/emqx | 7 +++++ bin/emqx.cmd | 1 + build | 2 ++ 10 files changed, 55 insertions(+), 21 deletions(-) diff --git a/apps/emqx/src/emqx.erl b/apps/emqx/src/emqx.erl index c31ba3d6d..2c3f4d1c5 100644 --- a/apps/emqx/src/emqx.erl +++ b/apps/emqx/src/emqx.erl @@ -69,7 +69,9 @@ remove_config/2, reset_config/2, data_dir/0, - certs_dir/0 + etc_file/1, + cert_file/1, + mutable_certs_dir/0 ]). -define(APP, ?MODULE). @@ -253,8 +255,30 @@ reset_config([RootName | _] = KeyPath, Opts) -> Error end. +%% @doc Returns the data directory which is set at boot time. data_dir() -> application:get_env(emqx, data_dir, "data"). -certs_dir() -> +%% @doc Returns the directory for user uploaded certificates. +mutable_certs_dir() -> filename:join([data_dir(), certs]). + +%% @doc Returns the absolute path for a PEM certificate file +%% which is installed or provisioned by sysadmin in $EMQX_ETC_DIR/certs. +cert_file(SubPath) -> + filename:join([etc_dir(), "certs", SubPath]). + +%% @doc Returns the absolute path for a file in EMQX's etc dir. +%% i.e. for rpm and deb installation, it's /etc/emqx/ +%% for other installation, it's /etc/ +etc_file(SubPath) -> + filename:join([etc_dir(), SubPath]). + +etc_dir() -> + %% EMQX_ETC_DIR is inherited from RUNNER_ETC_DIR which is set at package build time. + %% when it's not set, it's most likely when running test cases. + Env = os:getenv("EMQX_ETC_DIR"), + case Env =:= "" orelse Env =:= false of + true -> "etc"; + false -> Env + end. diff --git a/apps/emqx/src/emqx_tls_lib.erl b/apps/emqx/src/emqx_tls_lib.erl index f6fba3bfc..9b049f74f 100644 --- a/apps/emqx/src/emqx_tls_lib.erl +++ b/apps/emqx/src/emqx_tls_lib.erl @@ -389,7 +389,7 @@ pem_file_name(Dir, Key, Pem) -> <> = crypto:hash(md5, Pem), Suffix = hex_str(CK), FileName = binary:replace(Key, <<"file">>, <<"-", Suffix/binary>>), - filename:join([emqx:certs_dir(), Dir, FileName]). + filename:join([emqx:mutable_certs_dir(), Dir, FileName]). hex_str(Bin) -> iolist_to_binary([io_lib:format("~2.16.0b", [X]) || <> <= Bin]). diff --git a/apps/emqx_exhook/src/emqx_exhook_api.erl b/apps/emqx_exhook/src/emqx_exhook_api.erl index bd4cb62e6..2a3ae8c84 100644 --- a/apps/emqx_exhook/src/emqx_exhook_api.erl +++ b/apps/emqx_exhook/src/emqx_exhook_api.erl @@ -171,6 +171,11 @@ params_server_name_in_path() -> ]. server_conf_schema() -> + SSL = #{ enable => false + , cacertfile => emqx:cert_file(<<"cacert.pem">>) + , certfile => emqx:cert_file(<<"cert.pem">>) + , keyfile => emqx:cert_file(<<"key.pem">>) + }, schema_with_example(ref(server_config), #{ name => "default" , enable => true @@ -179,11 +184,7 @@ server_conf_schema() -> , failed_action => deny , auto_reconnect => "60s" , pool_size => 8 - , ssl => #{ enable => false - , cacertfile => <<"{{ platform_etc_dir }}/certs/cacert.pem">> - , certfile => <<"{{ platform_etc_dir }}/certs/cert.pem">> - , keyfile => <<"{{ platform_etc_dir }}/certs/key.pem">> - } + , ssl => SSL }). %%-------------------------------------------------------------------- diff --git a/apps/emqx_gateway/src/emqx_gateway_api.erl b/apps/emqx_gateway/src/emqx_gateway_api.erl index c69b8e13a..771a2879c 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api.erl @@ -600,7 +600,7 @@ examples_gateway_confs() -> enable_stats => true, idle_timeout => <<"30s">>, mountpoint => <<"lwm2m/">>, - xml_dir => <<"etc/lwm2m_xml">>, + xml_dir => emqx:etc_file(<<"lwm2m_xml">>), lifetime_min => <<"1s">>, lifetime_max => <<"86400s">>, qmode_time_window => <<"22s">>, @@ -719,7 +719,7 @@ examples_update_gateway_confs() -> enable_stats => true, idle_timeout => <<"30s">>, mountpoint => <<"lwm2m2/">>, - xml_dir => <<"etc/lwm2m_xml">>, + xml_dir => emqx:etc_file(<<"lwm2m_xml">>), lifetime_min => <<"1s">>, lifetime_max => <<"86400s">>, qmode_time_window => <<"22s">>, diff --git a/apps/emqx_gateway/src/emqx_gateway_api_listeners.erl b/apps/emqx_gateway/src/emqx_gateway_api_listeners.erl index d5f2c9f20..44d938c60 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api_listeners.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api_listeners.erl @@ -833,9 +833,9 @@ examples_listener() -> <<"tlsv1.1">>, <<"tlsv1">> ], - cacertfile => <<"etc/certs/cacert.pem">>, - certfile => <<"etc/certs/cert.pem">>, - keyfile => <<"etc/certs/key.pem">>, + cacertfile => emqx:cert_file(<<"cacert.pem">>), + certfile => emqx:cert_file(<<"cert.pem">>), + keyfile => emqx:cert_file(<<"key.pem">>), verify => <<"verify_none">>, fail_if_no_peer_cert => false }, @@ -879,9 +879,9 @@ examples_listener() -> dtls => #{ versions => [<<"dtlsv1.2">>, <<"dtlsv1">>], - cacertfile => <<"etc/certs/cacert.pem">>, - certfile => <<"etc/certs/cert.pem">>, - keyfile => <<"etc/certs/key.pem">>, + cacertfile => emqx:cert_file(<<"cacert.pem">>), + certfile => emqx:cert_file(<<"cert.pem">>), + keyfile => emqx:cert_file(<<"key.pem">>), verify => <<"verify_none">>, fail_if_no_peer_cert => false }, @@ -906,9 +906,9 @@ examples_listener() -> dtls => #{ versions => [<<"dtlsv1.2">>, <<"dtlsv1">>], - cacertfile => <<"etc/certs/cacert.pem">>, - certfile => <<"etc/certs/cert.pem">>, - keyfile => <<"etc/certs/key.pem">>, + cacertfile => emqx:cert_file(<<"cacert.pem">>), + certfile => emqx:cert_file(<<"cert.pem">>), + keyfile => emqx:cert_file(<<"key.pem">>), verify => <<"verify_none">>, user_lookup_fun => <<"emqx_tls_psk:lookup">>, ciphers => diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index 3c28e5328..0f64fecfc 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -290,7 +290,7 @@ fields(lwm2m) -> sc( binary(), #{ - default => "etc/lwm2m_xml", + default => emqx:etc_file("lwm2m_xml"), required => true, desc => "The Directory for LwM2M Resource definition" } diff --git a/apps/emqx_plugin_libs/src/emqx_plugin_libs.erl b/apps/emqx_plugin_libs/src/emqx_plugin_libs.erl index d8cd1ab12..4edc24beb 100644 --- a/apps/emqx_plugin_libs/src/emqx_plugin_libs.erl +++ b/apps/emqx_plugin_libs/src/emqx_plugin_libs.erl @@ -15,4 +15,3 @@ %%-------------------------------------------------------------------- -module(emqx_plugin_libs). - diff --git a/bin/emqx b/bin/emqx index 1cfc36481..6708739c4 100755 --- a/bin/emqx +++ b/bin/emqx @@ -19,6 +19,13 @@ export RUNNER_ETC_DIR export REL_VSN export SCHEMA_MOD +# RUNNER_ETC_DIR is only used at boot time +# EMQX_ETC_DIR is by default RUNNER_ETC_DIR but the absolute path +# it is used at runtime by the emqx program +# so that it won't change even if file:set_cwd is evaluated by EMQX later +EMQX_ETC_DIR="${EMQX_ETC_DIR:-$(cd "$(readlink "$RUNNER_ETC_DIR")"; pwd -P)}" +export EMQX_ETC_DIR + RUNNER_SCRIPT="$RUNNER_BIN_DIR/$REL_NAME" CODE_LOADING_MODE="${CODE_LOADING_MODE:-embedded}" REL_DIR="$RUNNER_ROOT_DIR/releases/$REL_VSN" diff --git a/bin/emqx.cmd b/bin/emqx.cmd index 0777536e1..e64cae494 100644 --- a/bin/emqx.cmd +++ b/bin/emqx.cmd @@ -48,6 +48,7 @@ @set "RUNNER_ROOT_DIR=%rel_root_dir%" :: hard code etc dir @set "RUNNER_ETC_DIR=%rel_root_dir%\etc" +@set "EMQX_ETC_DIR=%rel_root_dir%\etc" @set "etc_dir=%rel_root_dir%\etc" @set "lib_dir=%rel_root_dir%\lib" @set "emqx_conf=%etc_dir%\emqx.conf" diff --git a/build b/build index e84609b23..421fdd6bd 100755 --- a/build +++ b/build @@ -281,6 +281,8 @@ case "$ARTIFACT" in make_tgz ;; pkg) + # this only affect build artifacts, such as schema doc + export EMQX_ETC_DIR='/etc/emqx/' if [ -z "${PKGERDIR:-}" ]; then log "Skipped making deb/rpm package for $SYSTEM" exit 0 From ba51f03cb72e7eb9c38197a61274b534e5cb0ed1 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 5 Apr 2022 17:38:33 +0200 Subject: [PATCH 4/7] refactor: remove the deprecated save_files_return_opts --- apps/emqx/src/emqx_tls_lib.erl | 49 +++++ .../src/emqx_connector_http.erl | 16 +- .../src/emqx_connector_ldap.erl | 5 +- .../src/emqx_connector_mongo.erl | 6 +- .../src/emqx_connector_mysql.erl | 9 +- .../src/emqx_connector_pgsql.erl | 3 +- .../src/emqx_connector_redis.erl | 4 +- .../src/emqx_plugin_libs_ssl.erl | 177 ------------------ .../test/emqx_plugin_libs_ssl_tests.erl | 86 --------- 9 files changed, 66 insertions(+), 289 deletions(-) delete mode 100644 apps/emqx_plugin_libs/src/emqx_plugin_libs_ssl.erl delete mode 100644 apps/emqx_plugin_libs/test/emqx_plugin_libs_ssl_tests.erl diff --git a/apps/emqx/src/emqx_tls_lib.erl b/apps/emqx/src/emqx_tls_lib.erl index 9b049f74f..d6ca233f7 100644 --- a/apps/emqx/src/emqx_tls_lib.erl +++ b/apps/emqx/src/emqx_tls_lib.erl @@ -34,6 +34,10 @@ file_content_as_options/1 ]). +-export([ + to_client_opts/1 +]). + -include("logger.hrl"). -define(IS_TRUE(Val), ((Val =:= true) or (Val =:= <<"true">>))). @@ -426,6 +430,51 @@ file_content_as_options([Key | Keys], SSL) -> end end. +%% @doc Convert hocon-checked ssl client options (map()) to +%% proplist accepted by ssl library. +to_client_opts(Opts) -> + GetD = fun(Key, Default) -> fuzzy_map_get(Key, Opts, Default) end, + Get = fun(Key) -> GetD(Key, undefined) end, + KeyFile = ensure_str(Get(keyfile)), + CertFile = ensure_str(Get(certfile)), + CAFile = ensure_str(Get(cacertfile)), + Verify = GetD(verify, verify_none), + SNI = + case GetD(server_name_indication, undefined) of + undefined -> undefined; + SNI0 -> ensure_str(SNI0) + end, + Versions = integral_versions(Get(versions)), + Ciphers = integral_ciphers(Versions, Get(ciphers)), + filter([ + {keyfile, KeyFile}, + {certfile, CertFile}, + {cacertfile, CAFile}, + {verify, Verify}, + {server_name_indication, SNI}, + {versions, Versions}, + {ciphers, Ciphers} + ]). + +filter([]) -> []; +filter([{_, undefined} | T]) -> filter(T); +filter([{_, ""} | T]) -> filter(T); +filter([H | T]) -> [H | filter(T)]. + +-spec fuzzy_map_get(atom() | binary(), map(), any()) -> any(). +fuzzy_map_get(Key, Options, Default) -> + case maps:find(Key, Options) of + {ok, Val} -> + Val; + error when is_atom(Key) -> + fuzzy_map_get(atom_to_binary(Key, utf8), Options, Default); + error -> + Default + end. + +ensure_str(L) when is_list(L) -> L; +ensure_str(B) when is_binary(B) -> unicode:characters_to_list(B, utf8). + -if(?OTP_RELEASE > 22). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). diff --git a/apps/emqx_connector/src/emqx_connector_http.erl b/apps/emqx_connector/src/emqx_connector_http.erl index a63ed78bb..8bf3e8ee5 100644 --- a/apps/emqx_connector/src/emqx_connector_http.erl +++ b/apps/emqx_connector/src/emqx_connector_http.erl @@ -145,14 +145,14 @@ on_start(InstId, #{base_url := #{scheme := Scheme, pool_size := PoolSize} = Config) -> ?SLOG(info, #{msg => "starting_http_connector", connector => InstId, config => Config}), - {Transport, TransportOpts} = case Scheme of - http -> - {tcp, []}; - https -> - SSLOpts = emqx_plugin_libs_ssl:save_files_return_opts( - maps:get(ssl, Config), "connectors", InstId), - {tls, SSLOpts} - end, + {Transport, TransportOpts} = + case Scheme of + http -> + {tcp, []}; + https -> + SSLOpts = emqx_tls_lib:to_client_opts(maps:get(ssl, Config)), + {tls, SSLOpts} + end, NTransportOpts = emqx_misc:ipv6_probe(TransportOpts), PoolOpts = [ {host, Host} , {port, Port} diff --git a/apps/emqx_connector/src/emqx_connector_ldap.erl b/apps/emqx_connector/src/emqx_connector_ldap.erl index 2416dab7b..3e3bc36ff 100644 --- a/apps/emqx_connector/src/emqx_connector_ldap.erl +++ b/apps/emqx_connector/src/emqx_connector_ldap.erl @@ -57,10 +57,7 @@ on_start(InstId, #{servers := Servers0, SslOpts = case maps:get(enable, SSL) of true -> [{ssl, true}, - {sslopts, emqx_plugin_libs_ssl:save_files_return_opts( - SSL, - "connectors", - InstId)} + {sslopts, emqx_tls_lib:to_client_opts(SSL)} ]; false -> [{ssl, false}] end, diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index 90945e5b8..35cbfd3c9 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -120,11 +120,7 @@ on_start(InstId, Config = #{mongo_type := Type, SslOpts = case maps:get(enable, SSL) of true -> [{ssl, true}, - {ssl_opts, - emqx_plugin_libs_ssl:save_files_return_opts( - SSL, - "connectors", - InstId)} + {ssl_opts, emqx_tls_lib:to_client_opts(SSL)} ]; false -> [{ssl, false}] end, diff --git a/apps/emqx_connector/src/emqx_connector_mysql.erl b/apps/emqx_connector/src/emqx_connector_mysql.erl index 8fbed74fb..2e939760c 100644 --- a/apps/emqx_connector/src/emqx_connector_mysql.erl +++ b/apps/emqx_connector/src/emqx_connector_mysql.erl @@ -67,10 +67,11 @@ on_start(InstId, #{server := {Host, Port}, ?SLOG(info, #{msg => "starting_mysql_connector", connector => InstId, config => Config}), SslOpts = case maps:get(enable, SSL) of - true -> - [{ssl, emqx_plugin_libs_ssl:save_files_return_opts(SSL, "connectors", InstId)}]; - false -> [] - end, + true -> + [{ssl, emqx_tls_lib:to_client_opts(SSL)}]; + false -> + [] + end, Options = [{host, Host}, {port, Port}, {user, User}, diff --git a/apps/emqx_connector/src/emqx_connector_pgsql.erl b/apps/emqx_connector/src/emqx_connector_pgsql.erl index 77509b276..7e556ad81 100644 --- a/apps/emqx_connector/src/emqx_connector_pgsql.erl +++ b/apps/emqx_connector/src/emqx_connector_pgsql.erl @@ -79,8 +79,7 @@ on_start(InstId, #{server := {Host, Port}, SslOpts = case maps:get(enable, SSL) of true -> [{ssl, true}, - {ssl_opts, - emqx_plugin_libs_ssl:save_files_return_opts(SSL, "connectors", InstId)}]; + {ssl_opts, emqx_tls_lib:to_client_opts(SSL)}]; false -> [{ssl, false}] end, diff --git a/apps/emqx_connector/src/emqx_connector_redis.erl b/apps/emqx_connector/src/emqx_connector_redis.erl index e8bcc86ac..b237ed6a4 100644 --- a/apps/emqx_connector/src/emqx_connector_redis.erl +++ b/apps/emqx_connector/src/emqx_connector_redis.erl @@ -109,9 +109,7 @@ on_start(InstId, #{redis_type := Type, Options = case maps:get(enable, SSL) of true -> [{ssl, true}, - {ssl_options, - emqx_plugin_libs_ssl:save_files_return_opts(SSL, "connectors", InstId)} - ]; + {ssl_options, emqx_tls_lib:to_client_opts(SSL)}]; false -> [{ssl, false}] end ++ [{sentinel, maps:get(sentinel, Config, undefined)}], PoolName = emqx_plugin_libs_pool:pool_name(InstId), diff --git a/apps/emqx_plugin_libs/src/emqx_plugin_libs_ssl.erl b/apps/emqx_plugin_libs/src/emqx_plugin_libs_ssl.erl deleted file mode 100644 index 9d1c48ba9..000000000 --- a/apps/emqx_plugin_libs/src/emqx_plugin_libs_ssl.erl +++ /dev/null @@ -1,177 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2021-2022 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% 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. -%%-------------------------------------------------------------------- - --module(emqx_plugin_libs_ssl). - --export([ - save_files_return_opts/2, - save_files_return_opts/3, - save_file/2 -]). - --type file_input_key() :: atom() | binary(). --type file_input() :: #{file_input_key() => binary()}. - -%% options are below paris -%% <<"keyfile">> => file_input() -%% <<"certfile">> => file_input() -%% <<"cafile">> => file_input() %% backward compatible -%% <<"cacertfile">> => file_input() -%% <<"verify">> => verify_none | verify_peer -%% <<"tls_versions">> => binary() -%% <<"ciphers">> => binary() --type opts_key() :: binary() | atom(). --type opts_input() :: #{opts_key() => term()}. - --type opt_key() :: keyfile | certfile | cacertfile | verify | versions | ciphers. --type opt_value() :: term(). --type opts() :: [{opt_key(), opt_value()}]. - --include_lib("emqx/include/logger.hrl"). - -%% @doc Parse ssl options input. -%% If the input contains file content, save the files in the given dir. -%% Returns ssl options for Erlang's ssl application. -%% -%% For SSL files in the input Option, it can either be a file path -%% or a map like `#{filename := FileName, file := Content}`. -%% In case it's a map, the file is saved in EMQX's `data_dir' -%% (unless `SubDir' is an absolute path). -%% NOTE: This function is now deprecated, use emqx_tls_lib:ensure_ssl_files/2 instead. --spec save_files_return_opts( - opts_input(), - atom() | string() | binary(), - string() | binary() -) -> opts(). -save_files_return_opts(Options, SubDir, ResId) -> - Dir = filename:join([emqx:data_dir(), SubDir, ResId]), - save_files_return_opts(Options, Dir). - -%% @doc Parse ssl options input. -%% If the input contains file content, save the files in the given dir. -%% Returns ssl options for Erlang's ssl application. -%% -%% For SSL files in the input Option, it can either be a file path -%% or a map like `#{filename := FileName, file := Content}`. -%% In case it's a map, the file is saved in EMQX's `data_dir' -%% (unless `SubDir' is an absolute path). -%% NOTE: This function is now deprecated, use emqx_tls_lib:ensure_ssl_files/2 instead. --spec save_files_return_opts(opts_input(), file:name_all()) -> opts(). -save_files_return_opts(Options, Dir) -> - GetD = fun(Key, Default) -> fuzzy_map_get(Key, Options, Default) end, - Get = fun(Key) -> GetD(Key, undefined) end, - KeyFile = Get(keyfile), - CertFile = Get(certfile), - CAFile = Get(cacertfile), - Key = maybe_save_file(KeyFile, Dir), - Cert = maybe_save_file(CertFile, Dir), - CA = maybe_save_file(CAFile, Dir), - Verify = GetD(verify, verify_none), - SNI = - case Get(<<"server_name_indication">>) of - undefined -> undefined; - SNI0 -> ensure_str(SNI0) - end, - Versions = emqx_tls_lib:integral_versions(Get(versions)), - Ciphers = emqx_tls_lib:integral_ciphers(Versions, Get(ciphers)), - filter([ - {keyfile, Key}, - {certfile, Cert}, - {cacertfile, CA}, - {verify, Verify}, - {server_name_indication, SNI}, - {versions, Versions}, - {ciphers, Ciphers} - ]). - -%% @doc Save a key or certificate file in data dir, -%% and return path of the saved file. -%% empty string is returned if the input is empty. --spec save_file(file_input(), atom() | string() | binary()) -> string(). -save_file(Param, SubDir) -> - Dir = filename:join([emqx:data_dir(), SubDir]), - maybe_save_file(Param, Dir). - -filter([]) -> []; -filter([{_, undefined} | T]) -> filter(T); -filter([{_, ""} | T]) -> filter(T); -filter([H | T]) -> [H | filter(T)]. - -maybe_save_file(#{filename := FileName, file := Content}, Dir) when - FileName =/= undefined andalso Content =/= undefined --> - maybe_save_file(ensure_str(FileName), iolist_to_binary(Content), Dir); -maybe_save_file(FilePath, _) when is_list(FilePath) -> - FilePath; -maybe_save_file(FilePath, _) when is_binary(FilePath) -> - ensure_str(FilePath); -maybe_save_file(_, _) -> - "". - -%% no filename, ignore -maybe_save_file("", _, _Dir) -> - ""; -%% no content, see if file exists -maybe_save_file(FileName, <<>>, Dir) -> - {ok, Cwd} = file:get_cwd(), - %% NOTE: when FileName is an absolute path, filename:join has no effect - CwdFile = ensure_str(filename:join([Cwd, FileName])), - DataDirFile = ensure_str(filename:join([Dir, FileName])), - Possibles = - case CwdFile =:= DataDirFile of - true -> [CwdFile]; - false -> [CwdFile, DataDirFile] - end, - case find_exist_file(FileName, Possibles) of - false -> erlang:throw({bad_cert_file, Possibles}); - Found -> Found - end; -maybe_save_file(FileName, Content, Dir) -> - FullFilename = filename:join([Dir, FileName]), - ok = filelib:ensure_dir(FullFilename), - case file:write_file(FullFilename, Content) of - ok -> - ensure_str(FullFilename); - {error, Reason} -> - ?SLOG(error, #{ - msg => "failed_to_save_ssl_file", - filename => FullFilename, - reason => Reason - }), - error({"failed_to_save_ssl_file", FullFilename, Reason}) - end. - -ensure_str(L) when is_list(L) -> L; -ensure_str(B) when is_binary(B) -> unicode:characters_to_list(B, utf8). - --spec fuzzy_map_get(atom() | binary(), map(), any()) -> any(). -fuzzy_map_get(Key, Options, Default) -> - case maps:find(Key, Options) of - {ok, Val} -> - Val; - error when is_atom(Key) -> - fuzzy_map_get(atom_to_binary(Key, utf8), Options, Default); - error -> - Default - end. - -find_exist_file(_Name, []) -> - false; -find_exist_file(Name, [F | Rest]) -> - case filelib:is_regular(F) of - true -> F; - false -> find_exist_file(Name, Rest) - end. diff --git a/apps/emqx_plugin_libs/test/emqx_plugin_libs_ssl_tests.erl b/apps/emqx_plugin_libs/test/emqx_plugin_libs_ssl_tests.erl deleted file mode 100644 index afac68166..000000000 --- a/apps/emqx_plugin_libs/test/emqx_plugin_libs_ssl_tests.erl +++ /dev/null @@ -1,86 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2021-2022 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% 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. -%%-------------------------------------------------------------------- - --module(emqx_plugin_libs_ssl_tests). - --include_lib("proper/include/proper.hrl"). --include_lib("eunit/include/eunit.hrl"). - -no_crash_test_() -> - Opts = [{numtests, 1000}, {to_file, user}], - {timeout, 60, - fun() -> ?assert(proper:quickcheck(prop_run(), Opts)) end}. - -prop_run() -> - ?FORALL(Generated, prop_opts_input(), test_opts_input(Generated)). - -%% proper type to generate input value. -prop_opts_input() -> - [{keyfile, prop_file_or_content()}, - {certfile, prop_file_or_content()}, - {cacertfile, prop_file_or_content()}, - {verify, proper_types:oneof([verify_none, verify_peer])}, - {versions, prop_tls_versions()}, - {ciphers, prop_tls_ciphers()}, - {other, proper_types:binary()}]. - -prop_file_or_content() -> - proper_types:oneof([prop_cert_file_name(), - {prop_cert_file_name(), proper_types:binary()}]). - -prop_cert_file_name() -> - File = code:which(?MODULE), %% existing - proper_types:oneof(["", <<>>, undefined, File]). - -prop_tls_versions() -> - proper_types:oneof(["tlsv1.3", - <<"tlsv1.3,tlsv1.2">>, - "tlsv1.2 , tlsv1.1", - "1.2", - "v1.3", - "", - <<>>, - undefined]). - -prop_tls_ciphers() -> - proper_types:oneof(["TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256", - <<>>, - "", - undefined]). - -test_opts_input(Inputs) -> - KF = fun(K) -> {_, V} = lists:keyfind(K, 1, Inputs), V end, - Generated = #{<<"keyfile">> => file_or_content(KF(keyfile)), - <<"certfile">> => file_or_content(KF(certfile)), - <<"cafile">> => file_or_content(KF(cacertfile)), - <<"verify">> => file_or_content(KF(verify)), - <<"tls_versions">> => KF(versions), - <<"ciphers">> => KF(ciphers), - <<"other">> => KF(other)}, - _ = emqx_plugin_libs_ssl:save_files_return_opts(Generated, "test-data"), - true. - -file_or_content({Name, Content}) -> - #{<<"file">> => Content, <<"filename">> => Name}; -file_or_content(Name) -> - Name. - -bad_cert_file_test() -> - Input = #{<<"keyfile">> => - #{<<"filename">> => "notafile", - <<"file">> => ""}}, - ?assertThrow({bad_cert_file, _}, - emqx_plugin_libs_ssl:save_files_return_opts(Input, "test-data")). From 1ac19b42e4f08acfa9f0f3a90c8d6e347166c8aa Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 5 Apr 2022 22:37:05 +0200 Subject: [PATCH 5/7] refactor(emqx_tls_lib): allow undefined for string fields --- apps/emqx/src/emqx_tls_lib.erl | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/emqx/src/emqx_tls_lib.erl b/apps/emqx/src/emqx_tls_lib.erl index d6ca233f7..c3748cbd4 100644 --- a/apps/emqx/src/emqx_tls_lib.erl +++ b/apps/emqx/src/emqx_tls_lib.erl @@ -439,11 +439,7 @@ to_client_opts(Opts) -> CertFile = ensure_str(Get(certfile)), CAFile = ensure_str(Get(cacertfile)), Verify = GetD(verify, verify_none), - SNI = - case GetD(server_name_indication, undefined) of - undefined -> undefined; - SNI0 -> ensure_str(SNI0) - end, + SNI = ensure_str(Get(server_name_indication)), Versions = integral_versions(Get(versions)), Ciphers = integral_ciphers(Versions, Get(ciphers)), filter([ @@ -472,6 +468,7 @@ fuzzy_map_get(Key, Options, Default) -> Default end. +ensure_str(undefined) -> undefined; ensure_str(L) when is_list(L) -> L; ensure_str(B) when is_binary(B) -> unicode:characters_to_list(B, utf8). From bc5692efd431a159d6ac36a40922d543b8d8ee7f Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 5 Apr 2022 23:02:14 +0200 Subject: [PATCH 6/7] fix(emqx_authn_api): return file path if does not exist --- apps/emqx_authn/src/emqx_authn_api.erl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index 927fe4072..97ec7823e 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -1196,8 +1196,12 @@ convert_certs(#{ssl := #{enable := true} = SSLOpts} = Config) -> undefined -> Acc; Filename -> - {ok, Bin} = file:read_file(Filename), - Acc#{K => Bin} + case file:read_file(Filename) of + {ok, Bin} -> + Acc#{K => Bin}; + {error, _} -> + Acc#{K => Filename} + end end end, SSLOpts, From 32694e601a785f02093f67203b6b82cd54e7eb59 Mon Sep 17 00:00:00 2001 From: "Zaiming (Stone) Shi" Date: Tue, 5 Apr 2022 23:07:10 +0200 Subject: [PATCH 7/7] refactor: rename RUNNER_ETC_DIR to EMQX_ETC_DIR --- apps/emqx/src/emqx.erl | 5 +++-- apps/emqx_conf/src/emqx_conf_schema.erl | 2 +- bin/emqx | 17 +++++------------ bin/emqx.cmd | 1 - mix.exs | 4 ++-- rebar.config.erl | 4 ++-- rel/emqx_vars | 2 +- 7 files changed, 14 insertions(+), 21 deletions(-) diff --git a/apps/emqx/src/emqx.erl b/apps/emqx/src/emqx.erl index 2c3f4d1c5..bd5bb083d 100644 --- a/apps/emqx/src/emqx.erl +++ b/apps/emqx/src/emqx.erl @@ -275,8 +275,9 @@ etc_file(SubPath) -> filename:join([etc_dir(), SubPath]). etc_dir() -> - %% EMQX_ETC_DIR is inherited from RUNNER_ETC_DIR which is set at package build time. - %% when it's not set, it's most likely when running test cases. + %% EMQX_ETC_DIR set by emqx boot script, + %% if it's not set, then it must be test environment + %% which should uses default path Env = os:getenv("EMQX_ETC_DIR"), case Env =:= "" orelse Env =:= false of true -> "etc"; diff --git a/apps/emqx_conf/src/emqx_conf_schema.erl b/apps/emqx_conf/src/emqx_conf_schema.erl index 0af6bb97c..ee9e3396b 100644 --- a/apps/emqx_conf/src/emqx_conf_schema.erl +++ b/apps/emqx_conf/src/emqx_conf_schema.erl @@ -830,7 +830,7 @@ tr_config_files(Conf) -> [_ | _] = Files -> Files; _ -> - case os:getenv("RUNNER_ETC_DIR") of + case os:getenv("EMQX_ETC_DIR") of false -> [filename:join([code:lib_dir(emqx), "etc", "emqx.conf"])]; Dir -> diff --git a/bin/emqx b/bin/emqx index 6708739c4..a7fad3589 100755 --- a/bin/emqx +++ b/bin/emqx @@ -15,17 +15,10 @@ ROOT_DIR="$(cd "$(dirname "$(readlink "$0" || echo "$0")")"/..; pwd -P)" # defined in emqx_vars export RUNNER_ROOT_DIR -export RUNNER_ETC_DIR +export EMQX_ETC_DIR export REL_VSN export SCHEMA_MOD -# RUNNER_ETC_DIR is only used at boot time -# EMQX_ETC_DIR is by default RUNNER_ETC_DIR but the absolute path -# it is used at runtime by the emqx program -# so that it won't change even if file:set_cwd is evaluated by EMQX later -EMQX_ETC_DIR="${EMQX_ETC_DIR:-$(cd "$(readlink "$RUNNER_ETC_DIR")"; pwd -P)}" -export EMQX_ETC_DIR - RUNNER_SCRIPT="$RUNNER_BIN_DIR/$REL_NAME" CODE_LOADING_MODE="${CODE_LOADING_MODE:-embedded}" REL_DIR="$RUNNER_ROOT_DIR/releases/$REL_VSN" @@ -362,7 +355,7 @@ call_hocon() { get_config_value() { path_to_value="$1" - call_hocon -s "$SCHEMA_MOD" -c "$RUNNER_ETC_DIR"/emqx.conf get "$path_to_value" | tr -d \" + call_hocon -s "$SCHEMA_MOD" -c "$EMQX_ETC_DIR"/emqx.conf get "$path_to_value" | tr -d \" } check_license() { @@ -425,7 +418,7 @@ generate_config() { ## NOTE: the generate command merges environment variables to the base config (emqx.conf), ## but does not include the cluster-override.conf and local-override.conf ## meaning, certain overrides will not be mapped to app.