Merge pull request #10971 from zmstone/0607-merge-master-to-release-51
0607 merge master to release 51
This commit is contained in:
commit
641166a67a
|
@ -59,12 +59,12 @@
|
||||||
{statistics, true}
|
{statistics, true}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
{project_plugins, [
|
{project_plugins, [erlfmt]}.
|
||||||
{erlfmt, [
|
|
||||||
|
{erlfmt, [
|
||||||
{files, [
|
{files, [
|
||||||
"{src,include,test}/*.{hrl,erl,app.src}",
|
"{src,include,test}/*.{hrl,erl,app.src}",
|
||||||
"rebar.config",
|
"rebar.config",
|
||||||
"rebar.config.script"
|
"rebar.config.script"
|
||||||
]}
|
]}
|
||||||
]}
|
|
||||||
]}.
|
]}.
|
||||||
|
|
|
@ -32,9 +32,7 @@
|
||||||
%% Used in emqx_gateway
|
%% Used in emqx_gateway
|
||||||
-export([
|
-export([
|
||||||
certs_dir/2,
|
certs_dir/2,
|
||||||
convert_certs/2,
|
convert_certs/2
|
||||||
convert_certs/3,
|
|
||||||
clear_certs/2
|
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-export_type([config/0]).
|
-export_type([config/0]).
|
||||||
|
@ -97,7 +95,7 @@ do_pre_config_update(_, {update_authenticator, ChainName, AuthenticatorID, Confi
|
||||||
NewConfig = lists:map(
|
NewConfig = lists:map(
|
||||||
fun(OldConfig0) ->
|
fun(OldConfig0) ->
|
||||||
case AuthenticatorID =:= authenticator_id(OldConfig0) of
|
case AuthenticatorID =:= authenticator_id(OldConfig0) of
|
||||||
true -> convert_certs(CertsDir, Config, OldConfig0);
|
true -> convert_certs(CertsDir, Config);
|
||||||
false -> OldConfig0
|
false -> OldConfig0
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
@ -162,17 +160,10 @@ do_post_config_update(
|
||||||
_,
|
_,
|
||||||
{delete_authenticator, ChainName, AuthenticatorID},
|
{delete_authenticator, ChainName, AuthenticatorID},
|
||||||
_NewConfig,
|
_NewConfig,
|
||||||
OldConfig,
|
_OldConfig,
|
||||||
_AppEnvs
|
_AppEnvs
|
||||||
) ->
|
) ->
|
||||||
case emqx_authentication:delete_authenticator(ChainName, AuthenticatorID) of
|
emqx_authentication:delete_authenticator(ChainName, AuthenticatorID);
|
||||||
ok ->
|
|
||||||
Config = get_authenticator_config(AuthenticatorID, to_list(OldConfig)),
|
|
||||||
CertsDir = certs_dir(ChainName, AuthenticatorID),
|
|
||||||
ok = clear_certs(CertsDir, Config);
|
|
||||||
{error, Reason} ->
|
|
||||||
{error, Reason}
|
|
||||||
end;
|
|
||||||
do_post_config_update(
|
do_post_config_update(
|
||||||
_,
|
_,
|
||||||
{update_authenticator, ChainName, AuthenticatorID, Config},
|
{update_authenticator, ChainName, AuthenticatorID, Config},
|
||||||
|
@ -231,9 +222,7 @@ delete_authenticators(NewIds, ChainName, OldConfig) ->
|
||||||
true ->
|
true ->
|
||||||
ok;
|
ok;
|
||||||
false ->
|
false ->
|
||||||
_ = emqx_authentication:delete_authenticator(ChainName, Id),
|
emqx_authentication:delete_authenticator(ChainName, Id)
|
||||||
CertsDir = certs_dir(ChainName, Conf),
|
|
||||||
ok = clear_certs(CertsDir, Conf)
|
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
OldConfig
|
OldConfig
|
||||||
|
@ -244,21 +233,10 @@ to_list(M) when M =:= #{} -> [];
|
||||||
to_list(M) when is_map(M) -> [M];
|
to_list(M) when is_map(M) -> [M];
|
||||||
to_list(L) when is_list(L) -> L.
|
to_list(L) when is_list(L) -> L.
|
||||||
|
|
||||||
convert_certs(CertsDir, Config) ->
|
convert_certs(CertsDir, NewConfig) ->
|
||||||
case emqx_tls_lib:ensure_ssl_files(CertsDir, maps:get(<<"ssl">>, Config, undefined)) of
|
|
||||||
{ok, SSL} ->
|
|
||||||
new_ssl_config(Config, SSL);
|
|
||||||
{error, Reason} ->
|
|
||||||
?SLOG(error, Reason#{msg => "bad_ssl_config"}),
|
|
||||||
throw({bad_ssl_config, Reason})
|
|
||||||
end.
|
|
||||||
|
|
||||||
convert_certs(CertsDir, NewConfig, OldConfig) ->
|
|
||||||
OldSSL = maps:get(<<"ssl">>, OldConfig, undefined),
|
|
||||||
NewSSL = maps:get(<<"ssl">>, NewConfig, undefined),
|
NewSSL = maps:get(<<"ssl">>, NewConfig, undefined),
|
||||||
case emqx_tls_lib:ensure_ssl_files(CertsDir, NewSSL) of
|
case emqx_tls_lib:ensure_ssl_files(CertsDir, NewSSL) of
|
||||||
{ok, NewSSL1} ->
|
{ok, NewSSL1} ->
|
||||||
ok = emqx_tls_lib:delete_ssl_files(CertsDir, NewSSL1, OldSSL),
|
|
||||||
new_ssl_config(NewConfig, NewSSL1);
|
new_ssl_config(NewConfig, NewSSL1);
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, Reason#{msg => "bad_ssl_config"}),
|
?SLOG(error, Reason#{msg => "bad_ssl_config"}),
|
||||||
|
@ -268,10 +246,6 @@ convert_certs(CertsDir, NewConfig, OldConfig) ->
|
||||||
new_ssl_config(Config, undefined) -> Config;
|
new_ssl_config(Config, undefined) -> Config;
|
||||||
new_ssl_config(Config, SSL) -> Config#{<<"ssl">> => SSL}.
|
new_ssl_config(Config, SSL) -> Config#{<<"ssl">> => SSL}.
|
||||||
|
|
||||||
clear_certs(CertsDir, Config) ->
|
|
||||||
OldSSL = maps:get(<<"ssl">>, Config, undefined),
|
|
||||||
ok = emqx_tls_lib:delete_ssl_files(CertsDir, undefined, OldSSL).
|
|
||||||
|
|
||||||
get_authenticator_config(AuthenticatorID, AuthenticatorsConfig) ->
|
get_authenticator_config(AuthenticatorID, AuthenticatorsConfig) ->
|
||||||
case filter_authenticator(AuthenticatorID, AuthenticatorsConfig) of
|
case filter_authenticator(AuthenticatorID, AuthenticatorsConfig) of
|
||||||
[C] -> C;
|
[C] -> C;
|
||||||
|
|
|
@ -37,7 +37,8 @@ init([]) ->
|
||||||
child_spec(emqx_metrics, worker),
|
child_spec(emqx_metrics, worker),
|
||||||
child_spec(emqx_authn_authz_metrics_sup, supervisor),
|
child_spec(emqx_authn_authz_metrics_sup, supervisor),
|
||||||
child_spec(emqx_ocsp_cache, worker),
|
child_spec(emqx_ocsp_cache, worker),
|
||||||
child_spec(emqx_crl_cache, worker)
|
child_spec(emqx_crl_cache, worker),
|
||||||
|
child_spec(emqx_tls_lib_sup, supervisor)
|
||||||
]
|
]
|
||||||
}}.
|
}}.
|
||||||
|
|
||||||
|
|
|
@ -552,13 +552,12 @@ remove_listener(Type, Name, OldConf) ->
|
||||||
case stop_listener(Type, Name, OldConf) of
|
case stop_listener(Type, Name, OldConf) of
|
||||||
ok ->
|
ok ->
|
||||||
_ = emqx_authentication:delete_chain(listener_id(Type, Name)),
|
_ = emqx_authentication:delete_chain(listener_id(Type, Name)),
|
||||||
clear_certs(certs_dir(Type, Name), OldConf);
|
ok;
|
||||||
Err ->
|
Err ->
|
||||||
Err
|
Err
|
||||||
end.
|
end.
|
||||||
|
|
||||||
update_listener(Type, Name, {OldConf, NewConf}) ->
|
update_listener(Type, Name, {OldConf, NewConf}) ->
|
||||||
try_clear_ssl_files(certs_dir(Type, Name), NewConf, OldConf),
|
|
||||||
ok = maybe_unregister_ocsp_stapling_refresh(Type, Name, NewConf),
|
ok = maybe_unregister_ocsp_stapling_refresh(Type, Name, NewConf),
|
||||||
Res = restart_listener(Type, Name, {OldConf, NewConf}),
|
Res = restart_listener(Type, Name, {OldConf, NewConf}),
|
||||||
recreate_authenticators(Res, Type, Name, NewConf).
|
recreate_authenticators(Res, Type, Name, NewConf).
|
||||||
|
@ -867,10 +866,6 @@ convert_certs(Type, Name, Conf) ->
|
||||||
throw({bad_ssl_config, Reason})
|
throw({bad_ssl_config, Reason})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
clear_certs(CertsDir, Conf) ->
|
|
||||||
OldSSL = get_ssl_options(Conf),
|
|
||||||
emqx_tls_lib:delete_ssl_files(CertsDir, undefined, OldSSL).
|
|
||||||
|
|
||||||
filter_stacktrace({Reason, _Stacktrace}) -> Reason;
|
filter_stacktrace({Reason, _Stacktrace}) -> Reason;
|
||||||
filter_stacktrace(Reason) -> Reason.
|
filter_stacktrace(Reason) -> Reason.
|
||||||
|
|
||||||
|
@ -880,11 +875,6 @@ ensure_override_limiter_conf(Conf, #{<<"limiter">> := Limiter}) ->
|
||||||
ensure_override_limiter_conf(Conf, _) ->
|
ensure_override_limiter_conf(Conf, _) ->
|
||||||
Conf.
|
Conf.
|
||||||
|
|
||||||
try_clear_ssl_files(CertsDir, NewConf, OldConf) ->
|
|
||||||
NewSSL = get_ssl_options(NewConf),
|
|
||||||
OldSSL = get_ssl_options(OldConf),
|
|
||||||
emqx_tls_lib:delete_ssl_files(CertsDir, NewSSL, OldSSL).
|
|
||||||
|
|
||||||
get_ssl_options(Conf = #{}) ->
|
get_ssl_options(Conf = #{}) ->
|
||||||
case maps:find(ssl_options, Conf) of
|
case maps:find(ssl_options, Conf) of
|
||||||
{ok, SSL} ->
|
{ok, SSL} ->
|
||||||
|
|
|
@ -84,7 +84,7 @@ check_pub(Zone, Flags) when is_map(Flags) ->
|
||||||
error ->
|
error ->
|
||||||
Flags
|
Flags
|
||||||
end,
|
end,
|
||||||
get_caps(?PUBCAP_KEYS, Zone)
|
emqx_config:get_zone_conf(Zone, [mqtt])
|
||||||
).
|
).
|
||||||
|
|
||||||
do_check_pub(#{topic_levels := Levels}, #{max_topic_levels := Limit}) when
|
do_check_pub(#{topic_levels := Levels}, #{max_topic_levels := Limit}) when
|
||||||
|
@ -107,24 +107,13 @@ do_check_pub(_Flags, _Caps) ->
|
||||||
) ->
|
) ->
|
||||||
ok_or_error(emqx_types:reason_code()).
|
ok_or_error(emqx_types:reason_code()).
|
||||||
check_sub(ClientInfo = #{zone := Zone}, Topic, SubOpts) ->
|
check_sub(ClientInfo = #{zone := Zone}, Topic, SubOpts) ->
|
||||||
Caps = get_caps(?SUBCAP_KEYS, Zone),
|
Caps = emqx_config:get_zone_conf(Zone, [mqtt]),
|
||||||
Flags = lists:foldl(
|
Flags = #{
|
||||||
fun
|
topic_levels => emqx_topic:levels(Topic),
|
||||||
(max_topic_levels, Map) ->
|
is_wildcard => emqx_topic:wildcard(Topic),
|
||||||
Map#{topic_levels => emqx_topic:levels(Topic)};
|
is_shared => maps:is_key(share, SubOpts),
|
||||||
(wildcard_subscription, Map) ->
|
is_exclusive => maps:get(is_exclusive, SubOpts, false)
|
||||||
Map#{is_wildcard => emqx_topic:wildcard(Topic)};
|
},
|
||||||
(shared_subscription, Map) ->
|
|
||||||
Map#{is_shared => maps:is_key(share, SubOpts)};
|
|
||||||
(exclusive_subscription, Map) ->
|
|
||||||
Map#{is_exclusive => maps:get(is_exclusive, SubOpts, false)};
|
|
||||||
%% Ignore
|
|
||||||
(_Key, Map) ->
|
|
||||||
Map
|
|
||||||
end,
|
|
||||||
#{},
|
|
||||||
maps:keys(Caps)
|
|
||||||
),
|
|
||||||
do_check_sub(Flags, Caps, ClientInfo, Topic).
|
do_check_sub(Flags, Caps, ClientInfo, Topic).
|
||||||
|
|
||||||
do_check_sub(#{topic_levels := Levels}, #{max_topic_levels := Limit}, _, _) when
|
do_check_sub(#{topic_levels := Levels}, #{max_topic_levels := Limit}, _, _) when
|
||||||
|
|
|
@ -0,0 +1,370 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2023 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% @doc Orphaned TLS certificates / keyfiles garbage collector
|
||||||
|
%%
|
||||||
|
%% This module is a worker process that periodically scans the mutable
|
||||||
|
%% certificates directory and removes any files that are not referenced
|
||||||
|
%% by _any_ TLS configuration in _any_ of the config roots. Such files
|
||||||
|
%% are called "orphans".
|
||||||
|
%%
|
||||||
|
%% In order to ensure safety, GC considers a file to be a candidate for
|
||||||
|
%% deletion (a "convict") only if it was considered an orphan twice in
|
||||||
|
%% a row. This should help avoid deleting files that are not yet in the
|
||||||
|
%% config but was already materialized on disk (e.g. during
|
||||||
|
%% `pre_config_update/3`).
|
||||||
|
|
||||||
|
-module(emqx_tls_certfile_gc).
|
||||||
|
|
||||||
|
-include_lib("kernel/include/file.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
-include_lib("snabbkaffe/include/trace.hrl").
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([
|
||||||
|
start_link/0,
|
||||||
|
start_link/1,
|
||||||
|
run/0,
|
||||||
|
force/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
-behaviour(gen_server).
|
||||||
|
-export([
|
||||||
|
init/1,
|
||||||
|
handle_call/3,
|
||||||
|
handle_cast/2,
|
||||||
|
handle_info/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% Testing & maintenance
|
||||||
|
-export([
|
||||||
|
orphans/0,
|
||||||
|
orphans/1,
|
||||||
|
convicts/2,
|
||||||
|
collect_files/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-define(GC_INVERVAL, 5 * 60 * 1000).
|
||||||
|
|
||||||
|
-define(HAS_OWNER_READ(Mode), ((Mode band 8#00400) > 0)).
|
||||||
|
-define(HAS_OWNER_WRITE(Mode), ((Mode band 8#00200) > 0)).
|
||||||
|
|
||||||
|
-type filename() :: string().
|
||||||
|
-type basename() :: string().
|
||||||
|
-type fileinfo() :: #file_info{}.
|
||||||
|
-type fileref() :: {basename(), fileinfo()}.
|
||||||
|
-type orphans() :: #{fileref() => [filename()]}.
|
||||||
|
|
||||||
|
-type st() :: #{
|
||||||
|
orphans => orphans(),
|
||||||
|
gc_interval => pos_integer(),
|
||||||
|
next_gc_timer => reference()
|
||||||
|
}.
|
||||||
|
|
||||||
|
-type event() ::
|
||||||
|
{collect, filename(), ok | {error, file:posix()}}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% API
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-spec start_link() ->
|
||||||
|
{ok, pid()}.
|
||||||
|
start_link() ->
|
||||||
|
start_link(?GC_INVERVAL).
|
||||||
|
|
||||||
|
-spec start_link(_Interval :: pos_integer()) ->
|
||||||
|
{ok, pid()}.
|
||||||
|
start_link(Interval) ->
|
||||||
|
gen_server:start_link({local, ?MODULE}, ?MODULE, Interval, []).
|
||||||
|
|
||||||
|
-spec run() ->
|
||||||
|
{ok, _Events :: [event()]}.
|
||||||
|
run() ->
|
||||||
|
gen_server:call(?MODULE, collect).
|
||||||
|
|
||||||
|
-spec force() ->
|
||||||
|
{ok, _Events :: [event()]}.
|
||||||
|
force() ->
|
||||||
|
% NOTE
|
||||||
|
% Simulate a complete GC cycle by running it twice. Mostly useful in tests.
|
||||||
|
{ok, Events1} = run(),
|
||||||
|
{ok, Events2} = run(),
|
||||||
|
{ok, Events1 ++ Events2}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Supervisor callbacks
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-spec init(_) ->
|
||||||
|
{ok, st()}.
|
||||||
|
init(Interval) ->
|
||||||
|
{ok, start_timer(#{gc_interval => Interval})}.
|
||||||
|
|
||||||
|
-spec handle_call(collect | _Call, gen_server:from(), st()) ->
|
||||||
|
{reply, {ok, [event()]}, st()} | {noreply, st()}.
|
||||||
|
handle_call(collect, From, St) ->
|
||||||
|
{ok, Events, StNext} = ?tp_span(
|
||||||
|
tls_certfile_gc_manual,
|
||||||
|
#{caller => From},
|
||||||
|
collect(St, #{evhandler => {fun emqx_utils:cons/2, []}})
|
||||||
|
),
|
||||||
|
{reply, {ok, Events}, restart_timer(StNext)};
|
||||||
|
handle_call(Call, From, St) ->
|
||||||
|
?SLOG(error, #{msg => "unexpected_call", call => Call, from => From}),
|
||||||
|
{noreply, St}.
|
||||||
|
|
||||||
|
-spec handle_cast(_Cast, st()) ->
|
||||||
|
{noreply, st()}.
|
||||||
|
handle_cast(Cast, State) ->
|
||||||
|
?SLOG(error, #{msg => "unexpected_cast", cast => Cast}),
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
-spec handle_info({timeout, reference(), collect}, st()) ->
|
||||||
|
{noreply, st()}.
|
||||||
|
handle_info({timeout, TRef, collect}, St = #{next_gc_timer := TRef}) ->
|
||||||
|
{ok, _, StNext} = ?tp_span(
|
||||||
|
tls_certfile_gc_periodic,
|
||||||
|
#{},
|
||||||
|
collect(St, #{evhandler => {fun log_event/2, []}})
|
||||||
|
),
|
||||||
|
{noreply, restart_timer(StNext)}.
|
||||||
|
|
||||||
|
start_timer(St = #{gc_interval := Interval}) ->
|
||||||
|
TRef = erlang:start_timer(Interval, self(), collect),
|
||||||
|
St#{next_gc_timer => TRef}.
|
||||||
|
|
||||||
|
restart_timer(St = #{next_gc_timer := TRef}) ->
|
||||||
|
ok = emqx_utils:cancel_timer(TRef),
|
||||||
|
start_timer(St).
|
||||||
|
|
||||||
|
log_event({collect, Filename, ok}, _) ->
|
||||||
|
?tp(info, "tls_certfile_gc_collected", #{filename => Filename});
|
||||||
|
log_event({collect, Filename, {error, Reason}}, _) ->
|
||||||
|
?tp(warning, "tls_certfile_gc_collect_error", #{filename => Filename, reason => Reason}).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internal functions
|
||||||
|
%% -------------------------------------------------------------------
|
||||||
|
|
||||||
|
collect(St, Opts) ->
|
||||||
|
RootDir = emqx_utils_fs:canonicalize(emqx:mutable_certs_dir()),
|
||||||
|
Orphans = orphans(RootDir),
|
||||||
|
OrphansLast = maps:get(orphans, St, #{}),
|
||||||
|
Convicts = convicts(Orphans, OrphansLast),
|
||||||
|
Result = collect_files(Convicts, RootDir, Opts),
|
||||||
|
{ok, Result, St#{orphans => Orphans}}.
|
||||||
|
|
||||||
|
-spec orphans() ->
|
||||||
|
orphans().
|
||||||
|
orphans() ->
|
||||||
|
Dir = emqx_utils_fs:canonicalize(emqx:mutable_certs_dir()),
|
||||||
|
orphans(Dir).
|
||||||
|
|
||||||
|
-spec orphans(_Dir :: filename()) ->
|
||||||
|
orphans().
|
||||||
|
orphans(Dir) ->
|
||||||
|
% NOTE
|
||||||
|
% Orphans are files located under directory `Dir` that are not referenced by any
|
||||||
|
% configuration fragment defining TLS-related options.
|
||||||
|
Certfiles = find_managed_files(fun is_managed_file/2, Dir),
|
||||||
|
lists:foldl(
|
||||||
|
fun(Root, Acc) ->
|
||||||
|
% NOTE
|
||||||
|
% In situations where there's an ambiguity in `Certfiles` (e.g. a fileref
|
||||||
|
% pointing to more than one file), we'll spare _all of them_ from marking
|
||||||
|
% as orphans.
|
||||||
|
References = find_config_references(Root),
|
||||||
|
maps:without(References, Acc)
|
||||||
|
end,
|
||||||
|
Certfiles,
|
||||||
|
emqx_config:get_root_names()
|
||||||
|
).
|
||||||
|
|
||||||
|
-spec convicts(orphans(), orphans()) ->
|
||||||
|
[filename()].
|
||||||
|
convicts(Orphans, OrphansLast) ->
|
||||||
|
% NOTE
|
||||||
|
% Convicts are files that are orphans both in `Orphans` and `OrphansLast`.
|
||||||
|
maps:fold(
|
||||||
|
fun(FileRef, Filenames, Acc) ->
|
||||||
|
case maps:get(FileRef, Orphans, []) of
|
||||||
|
Filenames ->
|
||||||
|
% Certfile was not changed / recreated in the meantime
|
||||||
|
Filenames ++ Acc;
|
||||||
|
_ ->
|
||||||
|
% Certfile was changed / created / recreated in the meantime
|
||||||
|
Acc
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
[],
|
||||||
|
OrphansLast
|
||||||
|
).
|
||||||
|
|
||||||
|
-spec find_managed_files(Filter, _Dir :: filename()) -> orphans() when
|
||||||
|
Filter :: fun((_Abs :: filename(), fileinfo()) -> boolean()).
|
||||||
|
find_managed_files(Filter, Dir) ->
|
||||||
|
emqx_utils_fs:traverse_dir(
|
||||||
|
fun
|
||||||
|
(AbsPath, Info = #file_info{}, Acc) ->
|
||||||
|
case Filter(AbsPath, Info) of
|
||||||
|
true ->
|
||||||
|
FileRef = mk_fileref(AbsPath, Info),
|
||||||
|
Acc#{FileRef => [AbsPath | maps:get(FileRef, Acc, [])]};
|
||||||
|
false ->
|
||||||
|
Acc
|
||||||
|
end;
|
||||||
|
(AbsPath, {error, Reason}, Acc) ->
|
||||||
|
?SLOG(notice, "filesystem_object_inaccessible", #{
|
||||||
|
abspath => AbsPath,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
|
Acc
|
||||||
|
end,
|
||||||
|
#{},
|
||||||
|
Dir
|
||||||
|
).
|
||||||
|
|
||||||
|
is_managed_file(AbsPath, #file_info{type = Type, mode = Mode}) ->
|
||||||
|
% NOTE
|
||||||
|
% We consider a certfile is managed if: this is a regular file, owner has RW permission
|
||||||
|
% and the filename looks like a managed filename.
|
||||||
|
Type == regular andalso
|
||||||
|
?HAS_OWNER_READ(Mode) andalso
|
||||||
|
?HAS_OWNER_WRITE(Mode) andalso
|
||||||
|
emqx_tls_lib:is_managed_ssl_file(AbsPath).
|
||||||
|
|
||||||
|
-spec find_config_references(Root :: binary()) ->
|
||||||
|
[fileref() | {basename(), {error, _}}].
|
||||||
|
find_config_references(Root) ->
|
||||||
|
Config = emqx_config:get_raw([Root]),
|
||||||
|
fold_config(
|
||||||
|
fun(Stack, Value, Acc) ->
|
||||||
|
case is_file_reference(Stack) andalso is_binary(Value) of
|
||||||
|
true ->
|
||||||
|
Filename = emqx_schema:naive_env_interpolation(Value),
|
||||||
|
{stop, [mk_fileref(Filename) | Acc]};
|
||||||
|
false ->
|
||||||
|
{cont, Acc}
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
[],
|
||||||
|
Config
|
||||||
|
).
|
||||||
|
|
||||||
|
is_file_reference(Stack) ->
|
||||||
|
lists:any(
|
||||||
|
fun(KP) -> lists:prefix(lists:reverse(KP), Stack) end,
|
||||||
|
emqx_tls_lib:ssl_file_conf_keypaths()
|
||||||
|
).
|
||||||
|
|
||||||
|
mk_fileref(AbsPath) ->
|
||||||
|
case emqx_utils_fs:read_info(AbsPath) of
|
||||||
|
{ok, Info} ->
|
||||||
|
mk_fileref(AbsPath, Info);
|
||||||
|
{error, _} = Error ->
|
||||||
|
% NOTE
|
||||||
|
% Such broken fileref will not be regarded as live reference. However, there
|
||||||
|
% are some edge cases where this _might be wrong_, e.g. one of the `AbsPath`
|
||||||
|
% components is a symlink w/o read permission.
|
||||||
|
{filename:basename(AbsPath), Error}
|
||||||
|
end.
|
||||||
|
|
||||||
|
mk_fileref(AbsPath, Info = #file_info{}) ->
|
||||||
|
% NOTE
|
||||||
|
% This structure helps to tell if two file paths refer to the same file, without the
|
||||||
|
% need to reimplement `realpath` or something similar. It is not perfect because false
|
||||||
|
% positives are always possible, due to:
|
||||||
|
% * On Windows, inode is always 0.
|
||||||
|
% * On Unix, files can be hardlinked and have the same basename.
|
||||||
|
{filename:basename(AbsPath), Info#file_info{atime = undefined}}.
|
||||||
|
|
||||||
|
%%
|
||||||
|
|
||||||
|
fold_config(FoldFun, AccIn, Config) ->
|
||||||
|
fold_config(FoldFun, AccIn, [], Config).
|
||||||
|
|
||||||
|
fold_config(FoldFun, AccIn, Stack, Config) when is_map(Config) ->
|
||||||
|
maps:fold(
|
||||||
|
fun(K, SubConfig, Acc) ->
|
||||||
|
fold_subconf(FoldFun, Acc, [K | Stack], SubConfig)
|
||||||
|
end,
|
||||||
|
AccIn,
|
||||||
|
Config
|
||||||
|
);
|
||||||
|
fold_config(FoldFun, Acc, Stack, []) ->
|
||||||
|
fold_confval(FoldFun, Acc, Stack, []);
|
||||||
|
fold_config(FoldFun, Acc, Stack, Config) when is_list(Config) ->
|
||||||
|
fold_confarray(FoldFun, Acc, Stack, 1, Config);
|
||||||
|
fold_config(FoldFun, Acc, Stack, Config) ->
|
||||||
|
fold_confval(FoldFun, Acc, Stack, Config).
|
||||||
|
|
||||||
|
fold_confarray(FoldFun, AccIn, StackIn, I, [H | T]) ->
|
||||||
|
Acc = fold_subconf(FoldFun, AccIn, [I | StackIn], H),
|
||||||
|
fold_confarray(FoldFun, Acc, StackIn, I + 1, T);
|
||||||
|
fold_confarray(_FoldFun, Acc, _Stack, _, []) ->
|
||||||
|
Acc.
|
||||||
|
|
||||||
|
fold_subconf(FoldFun, AccIn, Stack, SubConfig) ->
|
||||||
|
case FoldFun(Stack, SubConfig, AccIn) of
|
||||||
|
{cont, Acc} ->
|
||||||
|
fold_config(FoldFun, Acc, Stack, SubConfig);
|
||||||
|
{stop, Acc} ->
|
||||||
|
Acc
|
||||||
|
end.
|
||||||
|
|
||||||
|
fold_confval(FoldFun, AccIn, Stack, ConfVal) ->
|
||||||
|
case FoldFun(Stack, ConfVal, AccIn) of
|
||||||
|
{_, Acc} ->
|
||||||
|
Acc
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%
|
||||||
|
|
||||||
|
-spec collect_files([filename()], filename()) ->
|
||||||
|
[event()].
|
||||||
|
collect_files(Filenames, RootDir) ->
|
||||||
|
collect_files(Filenames, RootDir, #{evhandler => {fun emqx_utils:cons/2, []}}).
|
||||||
|
|
||||||
|
collect_files(Filenames, RootDir, Opts) ->
|
||||||
|
{Handler, AccIn} = maps:get(evhandler, Opts),
|
||||||
|
lists:foldl(
|
||||||
|
fun(Filename, Acc) -> collect_file(Filename, RootDir, Handler, Acc) end,
|
||||||
|
AccIn,
|
||||||
|
Filenames
|
||||||
|
).
|
||||||
|
|
||||||
|
collect_file(Filename, RootDir, Handler, AccIn) ->
|
||||||
|
case file:delete(Filename) of
|
||||||
|
ok ->
|
||||||
|
Acc = Handler({collect, Filename, ok}, AccIn),
|
||||||
|
collect_parents(filename:dirname(Filename), RootDir, Handler, Acc);
|
||||||
|
{error, _} = Error ->
|
||||||
|
Handler({collect, Filename, Error}, AccIn)
|
||||||
|
end.
|
||||||
|
|
||||||
|
collect_parents(RootDir, RootDir, _Handler, Acc) ->
|
||||||
|
Acc;
|
||||||
|
collect_parents(ParentDir, RootDir, Handler, AccIn) ->
|
||||||
|
case file:del_dir(ParentDir) of
|
||||||
|
ok ->
|
||||||
|
Acc = Handler({collect, ParentDir, ok}, AccIn),
|
||||||
|
collect_parents(filename:dirname(ParentDir), RootDir, Handler, Acc);
|
||||||
|
{error, eexist} ->
|
||||||
|
AccIn;
|
||||||
|
{error, _} = Error ->
|
||||||
|
Handler({collect, ParentDir, Error}, AccIn)
|
||||||
|
end.
|
|
@ -30,8 +30,10 @@
|
||||||
-export([
|
-export([
|
||||||
ensure_ssl_files/2,
|
ensure_ssl_files/2,
|
||||||
ensure_ssl_files/3,
|
ensure_ssl_files/3,
|
||||||
delete_ssl_files/3,
|
|
||||||
drop_invalid_certs/1,
|
drop_invalid_certs/1,
|
||||||
|
ssl_file_conf_keypaths/0,
|
||||||
|
pem_dir/1,
|
||||||
|
is_managed_ssl_file/1,
|
||||||
is_valid_pem_file/1,
|
is_valid_pem_file/1,
|
||||||
is_pem/1
|
is_pem/1
|
||||||
]).
|
]).
|
||||||
|
@ -326,38 +328,6 @@ ensure_ssl_files_per_key(Dir, SSL, [KeyPath | KeyPaths], Opts) ->
|
||||||
{error, Reason#{which_options => [KeyPath]}}
|
{error, Reason#{which_options => [KeyPath]}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @doc Compare old and new config, delete the ones in old but not in new.
|
|
||||||
-spec delete_ssl_files(file:name_all(), undefined | map(), undefined | map()) -> ok.
|
|
||||||
delete_ssl_files(Dir, NewOpts0, OldOpts0) ->
|
|
||||||
DryRun = true,
|
|
||||||
{ok, NewOpts} = ensure_ssl_files(Dir, NewOpts0, #{dry_run => DryRun}),
|
|
||||||
{ok, OldOpts} = ensure_ssl_files(Dir, OldOpts0, #{dry_run => DryRun}),
|
|
||||||
Get = fun
|
|
||||||
(_KP, undefined) -> undefined;
|
|
||||||
(KP, Opts) -> emqx_utils_maps:deep_get(KP, Opts, undefined)
|
|
||||||
end,
|
|
||||||
lists:foreach(
|
|
||||||
fun(KeyPath) -> delete_old_file(Get(KeyPath, NewOpts), Get(KeyPath, OldOpts)) end,
|
|
||||||
?SSL_FILE_OPT_PATHS ++ ?SSL_FILE_OPT_PATHS_A
|
|
||||||
),
|
|
||||||
%% try to delete the dir if it is empty
|
|
||||||
_ = file:del_dir(pem_dir(Dir)),
|
|
||||||
ok.
|
|
||||||
|
|
||||||
delete_old_file(New, Old) when New =:= Old -> ok;
|
|
||||||
delete_old_file(_New, _Old = undefined) ->
|
|
||||||
ok;
|
|
||||||
delete_old_file(_New, Old) ->
|
|
||||||
case is_generated_file(Old) andalso filelib:is_regular(Old) andalso file:delete(Old) of
|
|
||||||
ok ->
|
|
||||||
ok;
|
|
||||||
%% the file is not generated by us, or it is already deleted
|
|
||||||
false ->
|
|
||||||
ok;
|
|
||||||
{error, Reason} ->
|
|
||||||
?SLOG(error, #{msg => "failed_to_delete_ssl_file", file_path => Old, reason => Reason})
|
|
||||||
end.
|
|
||||||
|
|
||||||
ensure_ssl_file(_Dir, _KeyPath, SSL, undefined, _Opts) ->
|
ensure_ssl_file(_Dir, _KeyPath, SSL, undefined, _Opts) ->
|
||||||
{ok, SSL};
|
{ok, SSL};
|
||||||
ensure_ssl_file(Dir, KeyPath, SSL, MaybePem, Opts) ->
|
ensure_ssl_file(Dir, KeyPath, SSL, MaybePem, Opts) ->
|
||||||
|
@ -402,6 +372,10 @@ is_valid_string(Binary) when is_binary(Binary) ->
|
||||||
_Otherwise -> false
|
_Otherwise -> false
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
-spec ssl_file_conf_keypaths() -> [_ConfKeypath :: [binary()]].
|
||||||
|
ssl_file_conf_keypaths() ->
|
||||||
|
?SSL_FILE_OPT_PATHS.
|
||||||
|
|
||||||
%% Check if it is a valid PEM formatted key.
|
%% Check if it is a valid PEM formatted key.
|
||||||
is_pem(MaybePem) ->
|
is_pem(MaybePem) ->
|
||||||
try
|
try
|
||||||
|
@ -432,45 +406,34 @@ save_pem_file(Dir, KeyPath, Pem, DryRun) ->
|
||||||
%% the filename is prefixed by the option name without the 'file' part
|
%% the filename is prefixed by the option name without the 'file' part
|
||||||
%% and suffixed with the first 8 byets the PEM content's md5 checksum.
|
%% and suffixed with the first 8 byets the PEM content's md5 checksum.
|
||||||
%% e.g. key-1234567890abcdef, cert-1234567890abcdef, and cacert-1234567890abcdef
|
%% e.g. key-1234567890abcdef, cert-1234567890abcdef, and cacert-1234567890abcdef
|
||||||
is_generated_file(Filename) ->
|
is_managed_ssl_file(Filename) ->
|
||||||
case string:split(filename:basename(Filename), "-") of
|
case string:split(filename:basename(Filename), "-") of
|
||||||
[_Name, Suffix] -> is_hex_str(Suffix);
|
[_Name, Suffix] -> is_hex_str(Suffix);
|
||||||
_ -> false
|
_ -> false
|
||||||
end.
|
end.
|
||||||
|
|
||||||
pem_file_name(Dir, KeyPath, Pem) ->
|
pem_file_name(Dir, KeyPath, Pem) ->
|
||||||
<<CK:8/binary, _/binary>> = crypto:hash(md5, Pem),
|
% NOTE
|
||||||
Suffix = hex_str(CK),
|
% Wee need to have the same filename on every cluster node.
|
||||||
Segments = lists:map(fun ensure_bin/1, KeyPath),
|
Segments = lists:map(fun ensure_bin/1, KeyPath),
|
||||||
Filename0 = iolist_to_binary(lists:join(<<"_">>, Segments)),
|
Filename0 = iolist_to_binary(lists:join(<<"_">>, Segments)),
|
||||||
Filename1 = binary:replace(Filename0, <<"file">>, <<>>),
|
Filename1 = binary:replace(Filename0, <<"file">>, <<>>),
|
||||||
|
Fingerprint = crypto:hash(md5, [Dir, Filename1, Pem]),
|
||||||
|
Suffix = binary:encode_hex(binary:part(Fingerprint, 0, 8)),
|
||||||
Filename = <<Filename1/binary, "-", Suffix/binary>>,
|
Filename = <<Filename1/binary, "-", Suffix/binary>>,
|
||||||
filename:join([pem_dir(Dir), Filename]).
|
filename:join([pem_dir(Dir), Filename]).
|
||||||
|
|
||||||
pem_dir(Dir) ->
|
pem_dir(Dir) ->
|
||||||
filename:join([emqx:mutable_certs_dir(), Dir]).
|
filename:join([emqx:mutable_certs_dir(), Dir]).
|
||||||
|
|
||||||
is_hex_str(HexStr) ->
|
is_hex_str(Str) ->
|
||||||
try
|
try
|
||||||
is_hex_str2(ensure_str(HexStr))
|
_ = binary:decode_hex(iolist_to_binary(Str)),
|
||||||
|
true
|
||||||
catch
|
catch
|
||||||
throw:not_hex -> false
|
error:badarg -> false
|
||||||
end.
|
end.
|
||||||
|
|
||||||
is_hex_str2(HexStr) ->
|
|
||||||
_ = [
|
|
||||||
case S of
|
|
||||||
S when S >= $0, S =< $9 -> S;
|
|
||||||
S when S >= $a, S =< $f -> S;
|
|
||||||
_ -> throw(not_hex)
|
|
||||||
end
|
|
||||||
|| S <- HexStr
|
|
||||||
],
|
|
||||||
true.
|
|
||||||
|
|
||||||
hex_str(Bin) ->
|
|
||||||
iolist_to_binary([io_lib:format("~2.16.0b", [X]) || <<X:8>> <= Bin]).
|
|
||||||
|
|
||||||
%% @doc Returns 'true' when the file is a valid pem, otherwise {error, Reason}.
|
%% @doc Returns 'true' when the file is a valid pem, otherwise {error, Reason}.
|
||||||
is_valid_pem_file(Path0) ->
|
is_valid_pem_file(Path0) ->
|
||||||
Path = resolve_cert_path_for_read(Path0),
|
Path = resolve_cert_path_for_read(Path0),
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2023 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_tls_lib_sup).
|
||||||
|
|
||||||
|
-behaviour(supervisor).
|
||||||
|
|
||||||
|
-export([start_link/0]).
|
||||||
|
|
||||||
|
-export([init/1]).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% API
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
start_link() ->
|
||||||
|
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Supervisor callbacks
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
init([]) ->
|
||||||
|
SupFlags = #{
|
||||||
|
strategy => one_for_one,
|
||||||
|
intensity => 100,
|
||||||
|
period => 10
|
||||||
|
},
|
||||||
|
GC = #{
|
||||||
|
id => emqx_tls_certfile_gc,
|
||||||
|
start => {emqx_tls_certfile_gc, start_link, []},
|
||||||
|
restart => permanent,
|
||||||
|
shutdown => brutal_kill,
|
||||||
|
type => worker
|
||||||
|
},
|
||||||
|
{ok, {SupFlags, [GC]}}.
|
|
@ -26,7 +26,6 @@
|
||||||
|
|
||||||
-include_lib("common_test/include/ct.hrl").
|
-include_lib("common_test/include/ct.hrl").
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
-include_lib("typerefl/include/types.hrl").
|
|
||||||
-include("emqx_authentication.hrl").
|
-include("emqx_authentication.hrl").
|
||||||
|
|
||||||
-define(AUTHN, emqx_authentication).
|
-define(AUTHN, emqx_authentication).
|
||||||
|
@ -474,52 +473,6 @@ t_restart({'end', _Config}) ->
|
||||||
?AUTHN:deregister_providers([{password_based, built_in_database}]),
|
?AUTHN:deregister_providers([{password_based, built_in_database}]),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
t_convert_certs({_, Config}) ->
|
|
||||||
Config;
|
|
||||||
t_convert_certs(Config) when is_list(Config) ->
|
|
||||||
Global = <<"mqtt:global">>,
|
|
||||||
Certs = certs([
|
|
||||||
{<<"keyfile">>, "key.pem"},
|
|
||||||
{<<"certfile">>, "cert.pem"},
|
|
||||||
{<<"cacertfile">>, "cacert.pem"}
|
|
||||||
]),
|
|
||||||
|
|
||||||
CertsDir = certs_dir(Config, [Global, <<"password_based:built_in_database">>]),
|
|
||||||
#{<<"ssl">> := NCerts} = convert_certs(CertsDir, #{<<"ssl">> => Certs}),
|
|
||||||
|
|
||||||
Certs2 = certs([
|
|
||||||
{<<"keyfile">>, "key.pem"},
|
|
||||||
{<<"certfile">>, "cert.pem"}
|
|
||||||
]),
|
|
||||||
|
|
||||||
#{<<"ssl">> := NCerts2} = convert_certs(
|
|
||||||
CertsDir,
|
|
||||||
#{<<"ssl">> => Certs2},
|
|
||||||
#{<<"ssl">> => NCerts}
|
|
||||||
),
|
|
||||||
|
|
||||||
?assertEqual(maps:get(<<"keyfile">>, NCerts), maps:get(<<"keyfile">>, NCerts2)),
|
|
||||||
?assertEqual(maps:get(<<"certfile">>, NCerts), maps:get(<<"certfile">>, NCerts2)),
|
|
||||||
|
|
||||||
Certs3 = certs([
|
|
||||||
{<<"keyfile">>, "client-key.pem"},
|
|
||||||
{<<"certfile">>, "client-cert.pem"},
|
|
||||||
{<<"cacertfile">>, "cacert.pem"}
|
|
||||||
]),
|
|
||||||
|
|
||||||
#{<<"ssl">> := NCerts3} = convert_certs(
|
|
||||||
CertsDir,
|
|
||||||
#{<<"ssl">> => Certs3},
|
|
||||||
#{<<"ssl">> => NCerts2}
|
|
||||||
),
|
|
||||||
|
|
||||||
?assertNotEqual(maps:get(<<"keyfile">>, NCerts2), maps:get(<<"keyfile">>, NCerts3)),
|
|
||||||
?assertNotEqual(maps:get(<<"certfile">>, NCerts2), maps:get(<<"certfile">>, NCerts3)),
|
|
||||||
|
|
||||||
?assertEqual(true, filelib:is_regular(maps:get(<<"keyfile">>, NCerts3))),
|
|
||||||
clear_certs(CertsDir, #{<<"ssl">> => NCerts3}),
|
|
||||||
?assertEqual(false, filelib:is_regular(maps:get(<<"keyfile">>, NCerts3))).
|
|
||||||
|
|
||||||
t_combine_authn_and_callback({init, Config}) ->
|
t_combine_authn_and_callback({init, Config}) ->
|
||||||
[
|
[
|
||||||
{listener_id, 'tcp:default'},
|
{listener_id, 'tcp:default'},
|
||||||
|
@ -627,18 +580,3 @@ certs(Certs) ->
|
||||||
|
|
||||||
register_provider(Type, Module) ->
|
register_provider(Type, Module) ->
|
||||||
ok = ?AUTHN:register_providers([{Type, Module}]).
|
ok = ?AUTHN:register_providers([{Type, Module}]).
|
||||||
|
|
||||||
certs_dir(CtConfig, Path) ->
|
|
||||||
DataDir = proplists:get_value(data_dir, CtConfig),
|
|
||||||
Dir = filename:join([DataDir | Path]),
|
|
||||||
filelib:ensure_dir(Dir),
|
|
||||||
Dir.
|
|
||||||
|
|
||||||
convert_certs(CertsDir, SslConfig) ->
|
|
||||||
emqx_authentication_config:convert_certs(CertsDir, SslConfig).
|
|
||||||
|
|
||||||
convert_certs(CertsDir, New, Old) ->
|
|
||||||
emqx_authentication_config:convert_certs(CertsDir, New, Old).
|
|
||||||
|
|
||||||
clear_certs(CertsDir, SslConfig) ->
|
|
||||||
emqx_authentication_config:clear_certs(CertsDir, SslConfig).
|
|
||||||
|
|
|
@ -347,6 +347,7 @@ stop_apps(Apps, Opts) ->
|
||||||
ok = mria_mnesia:delete_schema(),
|
ok = mria_mnesia:delete_schema(),
|
||||||
%% to avoid inter-suite flakiness
|
%% to avoid inter-suite flakiness
|
||||||
application:unset_env(emqx, init_config_load_done),
|
application:unset_env(emqx, init_config_load_done),
|
||||||
|
application:unset_env(emqx, boot_modules),
|
||||||
persistent_term:erase(?EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY),
|
persistent_term:erase(?EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY),
|
||||||
case Opts of
|
case Opts of
|
||||||
#{erase_all_configs := false} ->
|
#{erase_all_configs := false} ->
|
||||||
|
|
|
@ -0,0 +1,483 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2021-2023 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_tls_certfile_gc_SUITE).
|
||||||
|
|
||||||
|
-compile([export_all, nowarn_export_all]).
|
||||||
|
|
||||||
|
-include_lib("typerefl/include/types.hrl").
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
|
||||||
|
-include_lib("snabbkaffe/include/test_macros.hrl").
|
||||||
|
|
||||||
|
-define(of_pid(PID, EVENTS), [E || #{?snk_meta := #{pid := __Pid}} = E <- EVENTS, __Pid == PID]).
|
||||||
|
|
||||||
|
-import(hoconsc, [mk/1, mk/2, ref/2]).
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
|
init_per_suite(Config) ->
|
||||||
|
_ = application:load(emqx),
|
||||||
|
ok = application:set_env(emqx, data_dir, ?config(priv_dir, Config)),
|
||||||
|
ok = emqx_config:save_schema_mod_and_names(?MODULE),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_suite(_Config) ->
|
||||||
|
emqx_config:erase_all().
|
||||||
|
|
||||||
|
init_per_testcase(TC, Config) ->
|
||||||
|
TCAbsDir = filename:join(?config(priv_dir, Config), TC),
|
||||||
|
ok = application:set_env(emqx, data_dir, TCAbsDir),
|
||||||
|
ok = snabbkaffe:start_trace(),
|
||||||
|
[{tc_name, atom_to_list(TC)}, {tc_absdir, TCAbsDir} | Config].
|
||||||
|
|
||||||
|
end_per_testcase(_TC, Config) ->
|
||||||
|
ok = snabbkaffe:stop(),
|
||||||
|
ok = application:set_env(emqx, data_dir, ?config(priv_dir, Config)),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
t_no_orphans(Config) ->
|
||||||
|
SSL0 = #{
|
||||||
|
<<"keyfile">> => key(),
|
||||||
|
<<"certfile">> => cert(),
|
||||||
|
<<"cacertfile">> => cert()
|
||||||
|
},
|
||||||
|
{ok, SSL} = emqx_tls_lib:ensure_ssl_files("ssl", SSL0),
|
||||||
|
{ok, SSLUnused} = emqx_tls_lib:ensure_ssl_files("unused", SSL0),
|
||||||
|
SSLKeyfile = maps:get(<<"keyfile">>, SSL),
|
||||||
|
ok = load_config(#{
|
||||||
|
<<"clients">> => [
|
||||||
|
#{<<"transport">> => #{<<"ssl">> => SSL}}
|
||||||
|
],
|
||||||
|
<<"servers">> => #{
|
||||||
|
<<"noname">> => #{<<"ssl">> => SSL}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
% Should not be considered an orphan: it's a symlink.
|
||||||
|
ok = file:make_symlink(
|
||||||
|
SSLKeyfile,
|
||||||
|
filename:join(?config(tc_absdir, Config), filename:basename(SSLKeyfile))
|
||||||
|
),
|
||||||
|
% Should not be considered orphans: files are now read-only.
|
||||||
|
ok = file:change_mode(maps:get(<<"keyfile">>, SSLUnused), 8#400),
|
||||||
|
ok = file:change_mode(maps:get(<<"certfile">>, SSLUnused), 8#400),
|
||||||
|
ok = file:change_mode(maps:get(<<"cacertfile">>, SSLUnused), 8#400),
|
||||||
|
% Verify there are no orphans
|
||||||
|
?assertEqual(
|
||||||
|
#{},
|
||||||
|
orphans()
|
||||||
|
),
|
||||||
|
% Verify there are no orphans, since SSL config is still in use
|
||||||
|
ok = put_config([<<"servers">>, <<"noname">>, <<"ssl">>], #{<<"enable">> => false}),
|
||||||
|
?assertEqual(
|
||||||
|
#{},
|
||||||
|
orphans()
|
||||||
|
).
|
||||||
|
|
||||||
|
t_collect_orphans(_Config) ->
|
||||||
|
% 0. Set up a client and two servers (each with the same set of certfiles).
|
||||||
|
SSL0 = #{
|
||||||
|
<<"keyfile">> => key(),
|
||||||
|
<<"certfile">> => cert(),
|
||||||
|
<<"cacertfile">> => cert()
|
||||||
|
},
|
||||||
|
SSL1 = SSL0#{
|
||||||
|
<<"ocsp">> => #{<<"issuer_pem">> => cert()}
|
||||||
|
},
|
||||||
|
{ok, SSL2} = emqx_tls_lib:ensure_ssl_files("client", SSL0),
|
||||||
|
{ok, SSL3} = emqx_tls_lib:ensure_ssl_files("server", SSL1),
|
||||||
|
ok = load_config(#{
|
||||||
|
<<"clients">> => [
|
||||||
|
#{<<"transport">> => #{<<"ssl">> => SSL2}}
|
||||||
|
],
|
||||||
|
<<"servers">> => #{
|
||||||
|
<<"name">> => #{<<"ssl">> => SSL3},
|
||||||
|
<<"noname">> => #{<<"ssl">> => SSL3}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Orphans1 = orphans(),
|
||||||
|
?assertEqual(
|
||||||
|
#{},
|
||||||
|
Orphans1
|
||||||
|
),
|
||||||
|
% 1. Remove clients from the config
|
||||||
|
ok = put_config([<<"clients">>], []),
|
||||||
|
Orphans2 = orphans(),
|
||||||
|
?assertMatch(
|
||||||
|
M = #{} when map_size(M) == 3,
|
||||||
|
Orphans2
|
||||||
|
),
|
||||||
|
% All orphans are newly observed, nothing to collect
|
||||||
|
?assertEqual(
|
||||||
|
[],
|
||||||
|
collect(convicts(Orphans2, Orphans1))
|
||||||
|
),
|
||||||
|
% Same orphans are "observed", should be collected
|
||||||
|
?assertMatch(
|
||||||
|
[
|
||||||
|
{collect, _DirClient, ok},
|
||||||
|
{collect, _CACert, ok},
|
||||||
|
{collect, _Cert, ok},
|
||||||
|
{collect, _Key, ok}
|
||||||
|
],
|
||||||
|
collect(convicts(Orphans2, Orphans2))
|
||||||
|
),
|
||||||
|
% 2. Remove server from the config
|
||||||
|
ok = put_config([<<"servers">>, <<"name">>, <<"ssl">>], #{}),
|
||||||
|
Orphans3 = orphans(),
|
||||||
|
% Files are still referenced by the "noname" server
|
||||||
|
?assertEqual(
|
||||||
|
#{},
|
||||||
|
Orphans3
|
||||||
|
),
|
||||||
|
% 3. Remove another server from the config
|
||||||
|
ok = put_config([<<"servers">>, <<"noname">>, <<"ssl">>], #{}),
|
||||||
|
Orphans4 = orphans(),
|
||||||
|
?assertMatch(
|
||||||
|
M = #{} when map_size(M) == 4,
|
||||||
|
Orphans4
|
||||||
|
),
|
||||||
|
?assertMatch(
|
||||||
|
[
|
||||||
|
{collect, _DirServer, ok},
|
||||||
|
{collect, _IssuerPEM, ok},
|
||||||
|
{collect, _CACert, ok},
|
||||||
|
{collect, _Cert, ok},
|
||||||
|
{collect, _Key, ok}
|
||||||
|
],
|
||||||
|
collect(convicts(Orphans4, Orphans4))
|
||||||
|
),
|
||||||
|
% No more orphans left
|
||||||
|
?assertEqual(
|
||||||
|
#{},
|
||||||
|
orphans()
|
||||||
|
).
|
||||||
|
|
||||||
|
t_gc_runs_periodically(_Config) ->
|
||||||
|
{ok, Pid} = emqx_tls_certfile_gc:start_link(500),
|
||||||
|
|
||||||
|
% Set up two servers in the config, each with its own set of certfiles
|
||||||
|
SSL = #{
|
||||||
|
<<"keyfile">> => key(),
|
||||||
|
<<"certfile">> => cert()
|
||||||
|
},
|
||||||
|
{ok, SSL1} = emqx_tls_lib:ensure_ssl_files("s1", SSL),
|
||||||
|
SSL1Keyfile = emqx_utils_fs:canonicalize(maps:get(<<"keyfile">>, SSL1)),
|
||||||
|
SSL1Certfile = emqx_utils_fs:canonicalize(maps:get(<<"certfile">>, SSL1)),
|
||||||
|
{ok, SSL2} = emqx_tls_lib:ensure_ssl_files("s2", SSL#{
|
||||||
|
<<"ocsp">> => #{<<"issuer_pem">> => cert()}
|
||||||
|
}),
|
||||||
|
SSL2Keyfile = emqx_utils_fs:canonicalize(maps:get(<<"keyfile">>, SSL2)),
|
||||||
|
SSL2Certfile = emqx_utils_fs:canonicalize(maps:get(<<"certfile">>, SSL2)),
|
||||||
|
SSL2IssPEM = emqx_utils_fs:canonicalize(
|
||||||
|
emqx_utils_maps:deep_get([<<"ocsp">>, <<"issuer_pem">>], SSL2)
|
||||||
|
),
|
||||||
|
ok = load_config(#{
|
||||||
|
<<"servers">> => #{
|
||||||
|
<<"name">> => #{<<"ssl">> => SSL1},
|
||||||
|
<<"noname">> => #{<<"ssl">> => SSL2}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
% Wait for a periodic collection event
|
||||||
|
?check_trace(
|
||||||
|
?block_until(#{?snk_kind := tls_certfile_gc_periodic, ?snk_span := {complete, _}}),
|
||||||
|
fun(Events) ->
|
||||||
|
% Nothing should have been collected yet
|
||||||
|
?assertMatch(
|
||||||
|
[
|
||||||
|
#{?snk_kind := tls_certfile_gc_periodic, ?snk_span := start},
|
||||||
|
#{?snk_kind := tls_certfile_gc_periodic, ?snk_span := {complete, _}}
|
||||||
|
],
|
||||||
|
?of_pid(Pid, Events)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
),
|
||||||
|
|
||||||
|
% Delete the server from the config
|
||||||
|
ok = put_config([<<"servers">>, <<"noname">>, <<"ssl">>], #{}),
|
||||||
|
|
||||||
|
% Wait for a periodic collection event
|
||||||
|
?check_trace(
|
||||||
|
?block_until(#{?snk_kind := tls_certfile_gc_periodic, ?snk_span := {complete, _}}),
|
||||||
|
fun(Events) ->
|
||||||
|
% Nothing should have been collected yet, certfiles considered orphans for the first time
|
||||||
|
?assertMatch(
|
||||||
|
[
|
||||||
|
#{?snk_kind := tls_certfile_gc_periodic, ?snk_span := start},
|
||||||
|
#{?snk_kind := tls_certfile_gc_periodic, ?snk_span := {complete, _}}
|
||||||
|
],
|
||||||
|
?of_pid(Pid, Events)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
),
|
||||||
|
|
||||||
|
% Delete another server from the config
|
||||||
|
ok = put_config([<<"servers">>, <<"name">>, <<"ssl">>], #{}),
|
||||||
|
|
||||||
|
% Wait for next periodic collection event
|
||||||
|
?check_trace(
|
||||||
|
?block_until(#{?snk_kind := tls_certfile_gc_periodic, ?snk_span := {complete, _}}),
|
||||||
|
fun(Events) ->
|
||||||
|
% SSL2 certfiles should have been collected now, but not SSL1
|
||||||
|
?assertMatch(
|
||||||
|
[
|
||||||
|
#{?snk_kind := tls_certfile_gc_periodic, ?snk_span := start},
|
||||||
|
#{?snk_kind := "tls_certfile_gc_collected", filename := SSL2IssPEM},
|
||||||
|
#{?snk_kind := "tls_certfile_gc_collected", filename := SSL2Keyfile},
|
||||||
|
#{?snk_kind := "tls_certfile_gc_collected", filename := SSL2Certfile},
|
||||||
|
#{?snk_kind := "tls_certfile_gc_collected", filename := _SSL2Dir},
|
||||||
|
#{?snk_kind := tls_certfile_gc_periodic, ?snk_span := {complete, _}}
|
||||||
|
],
|
||||||
|
?of_pid(Pid, Events)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
),
|
||||||
|
|
||||||
|
% Wait for next periodic collection event
|
||||||
|
?check_trace(
|
||||||
|
?block_until(#{?snk_kind := tls_certfile_gc_periodic, ?snk_span := {complete, _}}),
|
||||||
|
fun(Events) ->
|
||||||
|
% SSL1 certfiles should have been collected finally, they were considered orphans before
|
||||||
|
?assertMatch(
|
||||||
|
[
|
||||||
|
#{?snk_kind := tls_certfile_gc_periodic, ?snk_span := start},
|
||||||
|
#{?snk_kind := "tls_certfile_gc_collected", filename := SSL1Keyfile},
|
||||||
|
#{?snk_kind := "tls_certfile_gc_collected", filename := SSL1Certfile},
|
||||||
|
#{?snk_kind := "tls_certfile_gc_collected", filename := _SSL1Dir},
|
||||||
|
#{?snk_kind := tls_certfile_gc_periodic, ?snk_span := {complete, _}}
|
||||||
|
],
|
||||||
|
?of_pid(Pid, Events)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
),
|
||||||
|
|
||||||
|
ok = proc_lib:stop(Pid).
|
||||||
|
|
||||||
|
t_gc_spares_recreated_certfiles(_Config) ->
|
||||||
|
{ok, Pid} = emqx_tls_certfile_gc:start_link(),
|
||||||
|
|
||||||
|
% Create two sets of certfiles, with no references to them
|
||||||
|
SSL = #{
|
||||||
|
<<"keyfile">> => key(),
|
||||||
|
<<"certfile">> => cert()
|
||||||
|
},
|
||||||
|
{ok, SSL1} = emqx_tls_lib:ensure_ssl_files("s1", SSL),
|
||||||
|
SSL1Keyfile = emqx_utils_fs:canonicalize(maps:get(<<"keyfile">>, SSL1)),
|
||||||
|
SSL1Certfile = emqx_utils_fs:canonicalize(maps:get(<<"certfile">>, SSL1)),
|
||||||
|
{ok, SSL2} = emqx_tls_lib:ensure_ssl_files("s2", SSL),
|
||||||
|
SSL2Keyfile = emqx_utils_fs:canonicalize(maps:get(<<"keyfile">>, SSL2)),
|
||||||
|
SSL2Certfile = emqx_utils_fs:canonicalize(maps:get(<<"certfile">>, SSL2)),
|
||||||
|
ok = load_config(#{}),
|
||||||
|
|
||||||
|
% Nothing should have been collected yet
|
||||||
|
?assertMatch(
|
||||||
|
{ok, []},
|
||||||
|
emqx_tls_certfile_gc:run()
|
||||||
|
),
|
||||||
|
|
||||||
|
% At least one second should pass, since mtime has second resolution
|
||||||
|
ok = timer:sleep(1000),
|
||||||
|
ok = file:change_time(SSL2Keyfile, erlang:localtime()),
|
||||||
|
ok = file:change_time(SSL2Certfile, erlang:localtime()),
|
||||||
|
% Only SSL1 certfiles should have been collected
|
||||||
|
?assertMatch(
|
||||||
|
{ok, [
|
||||||
|
{collect, _SSL1Dir, ok},
|
||||||
|
{collect, SSL1Certfile, ok},
|
||||||
|
{collect, SSL1Keyfile, ok}
|
||||||
|
]},
|
||||||
|
emqx_tls_certfile_gc:run()
|
||||||
|
),
|
||||||
|
|
||||||
|
% Recreate the SSL2 certfiles
|
||||||
|
ok = file:delete(SSL2Keyfile),
|
||||||
|
ok = file:delete(SSL2Certfile),
|
||||||
|
{ok, _} = emqx_tls_lib:ensure_ssl_files("s2", SSL),
|
||||||
|
% Nothing should have been collected
|
||||||
|
?assertMatch(
|
||||||
|
{ok, []},
|
||||||
|
emqx_tls_certfile_gc:run()
|
||||||
|
),
|
||||||
|
|
||||||
|
ok = proc_lib:stop(Pid).
|
||||||
|
|
||||||
|
t_gc_spares_symlinked_datadir(Config) ->
|
||||||
|
{ok, Pid} = emqx_tls_certfile_gc:start_link(),
|
||||||
|
|
||||||
|
% Create a certfiles set and a server that references it
|
||||||
|
SSL = #{
|
||||||
|
<<"keyfile">> => key(),
|
||||||
|
<<"certfile">> => cert(),
|
||||||
|
<<"ocsp">> => #{<<"issuer_pem">> => cert()}
|
||||||
|
},
|
||||||
|
{ok, SSL1} = emqx_tls_lib:ensure_ssl_files("srv", SSL),
|
||||||
|
SSL1Keyfile = emqx_utils_fs:canonicalize(maps:get(<<"keyfile">>, SSL1)),
|
||||||
|
|
||||||
|
ok = load_config(#{
|
||||||
|
<<"servers">> => #{<<"srv">> => #{<<"ssl">> => SSL1}}
|
||||||
|
}),
|
||||||
|
|
||||||
|
% Change the emqx data_dir to a symlink
|
||||||
|
TCAbsDir = ?config(tc_absdir, Config),
|
||||||
|
TCAbsLink = filename:join(?config(priv_dir, Config), ?config(tc_name, Config) ++ ".symlink"),
|
||||||
|
ok = file:make_symlink(TCAbsDir, TCAbsLink),
|
||||||
|
ok = application:set_env(emqx, data_dir, TCAbsLink),
|
||||||
|
|
||||||
|
% Make a hardlink to the SSL1 keyfile, that looks like a managed SSL file
|
||||||
|
SSL1KeyfileHardlink = filename:join([
|
||||||
|
filename:dirname(SSL1Keyfile),
|
||||||
|
"hardlink",
|
||||||
|
filename:basename(SSL1Keyfile)
|
||||||
|
]),
|
||||||
|
ok = filelib:ensure_dir(SSL1KeyfileHardlink),
|
||||||
|
ok = file:make_link(SSL1Keyfile, SSL1KeyfileHardlink),
|
||||||
|
|
||||||
|
% Nothing should have been collected
|
||||||
|
?assertMatch(
|
||||||
|
{ok, []},
|
||||||
|
emqx_tls_certfile_gc:force()
|
||||||
|
),
|
||||||
|
|
||||||
|
ok = put_config([<<"servers">>, <<"srv">>, <<"ssl">>], #{}),
|
||||||
|
|
||||||
|
% Everything should have been collected, including the hardlink
|
||||||
|
?assertMatch(
|
||||||
|
{ok, [
|
||||||
|
{collect, _SSL1Dir, ok},
|
||||||
|
{collect, _SSL1Certfile, ok},
|
||||||
|
{collect, _SSL1KeyfileHardlinkDir, ok},
|
||||||
|
{collect, _SSL1KeyfileHardlink, ok},
|
||||||
|
{collect, _SSL1Keyfile, ok},
|
||||||
|
{collect, _SSL1IssuerPEM, ok}
|
||||||
|
]},
|
||||||
|
emqx_tls_certfile_gc:force()
|
||||||
|
),
|
||||||
|
|
||||||
|
ok = proc_lib:stop(Pid).
|
||||||
|
|
||||||
|
t_gc_active(_Config) ->
|
||||||
|
ok = emqx_common_test_helpers:boot_modules([]),
|
||||||
|
ok = emqx_common_test_helpers:start_apps([]),
|
||||||
|
try
|
||||||
|
?assertEqual(
|
||||||
|
{ok, []},
|
||||||
|
emqx_tls_certfile_gc:run()
|
||||||
|
)
|
||||||
|
after
|
||||||
|
emqx_common_test_helpers:stop_apps([])
|
||||||
|
end.
|
||||||
|
|
||||||
|
orphans() ->
|
||||||
|
emqx_tls_certfile_gc:orphans(emqx:mutable_certs_dir()).
|
||||||
|
|
||||||
|
convicts(Orphans, OrphansLast) ->
|
||||||
|
emqx_tls_certfile_gc:convicts(Orphans, OrphansLast).
|
||||||
|
|
||||||
|
collect(Convicts) ->
|
||||||
|
emqx_tls_certfile_gc:collect_files(Convicts, emqx:mutable_certs_dir()).
|
||||||
|
|
||||||
|
load_config(Config) ->
|
||||||
|
emqx_config:init_load(
|
||||||
|
?MODULE,
|
||||||
|
emqx_utils_json:encode(#{<<?MODULE_STRING>> => Config})
|
||||||
|
).
|
||||||
|
|
||||||
|
put_config(Path, SubConfig) ->
|
||||||
|
emqx_config:put_raw([<<?MODULE_STRING>> | Path], SubConfig).
|
||||||
|
|
||||||
|
cert() ->
|
||||||
|
<<
|
||||||
|
"-----BEGIN CERTIFICATE-----\n"
|
||||||
|
"MIIFljCCA36gAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCU0Ux\n"
|
||||||
|
"EjAQBgNVBAgMCVN0b2NraG9sbTESMBAGA1UECgwJTXlPcmdOYW1lMRkwFwYDVQQL\n"
|
||||||
|
"DBBNeUludGVybWVkaWF0ZUNBMRkwFwYDVQQDDBBNeUludGVybWVkaWF0ZUNBMB4X\n"
|
||||||
|
"DTIzMDExMjEzMDgxNloXDTMzMDQxOTEzMDgxNlowdzELMAkGA1UEBhMCU0UxEjAQ\n"
|
||||||
|
"BgNVBAgMCVN0b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMRIwEAYDVQQKDAlN\n"
|
||||||
|
"eU9yZ05hbWUxGTAXBgNVBAsMEE15SW50ZXJtZWRpYXRlQ0ExETAPBgNVBAMMCE15\n"
|
||||||
|
"Q2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvGuAShewEo8V\n"
|
||||||
|
"/+aWVO/MuUt92m8K0Ut4nC2gOvpjMjf8mhSSf6KfnxPklsFwP4fdyPOjOiXwCsf3\n"
|
||||||
|
"1QO5fjVr8to3iGTHhEyZpzRcRqmw1eYJC7iDh3BqtYLAT30R+Kq6Mk+f4tXB5Lp/\n"
|
||||||
|
"2jXgdi0wshWagCPgJO3CtiwGyE8XSa+Q6EBYwzgh3NFbgYdJma4x+S86Y/5WfmXP\n"
|
||||||
|
"zF//UipsFp4gFUqwGuj6kJrN9NnA1xCiuOxCyN4JuFNMfM/tkeh26jAp0OHhJGsT\n"
|
||||||
|
"s3YiUm9Dpt7Rs7o0so9ov9K+hgDFuQw9HZW3WIJI99M5a9QZ4ZEQqKpABtYBl/Nb\n"
|
||||||
|
"VPXcr+T3fQIDAQABo4IBNjCCATIwCQYDVR0TBAIwADARBglghkgBhvhCAQEEBAMC\n"
|
||||||
|
"BaAwMwYJYIZIAYb4QgENBCYWJE9wZW5TU0wgR2VuZXJhdGVkIENsaWVudCBDZXJ0\n"
|
||||||
|
"aWZpY2F0ZTAdBgNVHQ4EFgQUOIChBA5aZB0dPWEtALfMIfSopIIwHwYDVR0jBBgw\n"
|
||||||
|
"FoAUTHCGOxVSibq1D5KqrrExQRO+c/MwDgYDVR0PAQH/BAQDAgXgMB0GA1UdJQQW\n"
|
||||||
|
"MBQGCCsGAQUFBwMCBggrBgEFBQcDBDA7BgNVHR8ENDAyMDCgLqAshipodHRwOi8v\n"
|
||||||
|
"bG9jYWxob3N0Ojk4NzgvaW50ZXJtZWRpYXRlLmNybC5wZW0wMQYIKwYBBQUHAQEE\n"
|
||||||
|
"JTAjMCEGCCsGAQUFBzABhhVodHRwOi8vbG9jYWxob3N0Ojk4NzcwDQYJKoZIhvcN\n"
|
||||||
|
"AQELBQADggIBAE0qTL5WIWcxRPU9oTrzJ+oxMTp1JZ7oQdS+ZekLkQ8mP7T6C/Ew\n"
|
||||||
|
"6YftjvkopnHUvn842+PTRXSoEtlFiTccmA60eMAai2tn5asxWBsLIRC9FH3LzOgV\n"
|
||||||
|
"/jgyY7HXuh8XyDBCDD+Sj9QityO+accTHijYAbHPAVBwmZU8nO5D/HsxLjRrCfQf\n"
|
||||||
|
"qf4OQpX3l1ryOi19lqoRXRGwcoZ95dqq3YgTMlLiEqmerQZSR6iSPELw3bcwnAV1\n"
|
||||||
|
"hoYYzeKps3xhwszCTz2+WaSsUO2sQlcFEsZ9oHex/02UiM4a8W6hGFJl5eojErxH\n"
|
||||||
|
"7MqaSyhwwyX6yt8c75RlNcUThv+4+TLkUTbTnWgC9sFjYfd5KSfAdIMp3jYzw3zw\n"
|
||||||
|
"XEMTX5FaLaOCAfUDttPzn+oNezWZ2UyFTQXQE2CazpRdJoDd04qVg9WLpQxLYRP7\n"
|
||||||
|
"xSFEHulOPccdAYF2C45yNtJAZyWKfGaAZIxrgEXbMkcdDMlYphpRwpjS8SIBNZ31\n"
|
||||||
|
"KFE8BczKrg2qO0ywIjanPaRgrFVmeSvBKeU/YLQVx6fZMgOk6vtidLGZLyDXy0Ff\n"
|
||||||
|
"yaZSoj+on++RDz1IXb96Y8scuNlfcYI8QeoNjwiLtf80BV8SRJiG4e/jTvMf/z9L\n"
|
||||||
|
"kWrnDWvx4xkUmxFg4TK42dkNp7sEYBTlVVq9fjKE92ha7FGZRqsxOLNQ\n"
|
||||||
|
"-----END CERTIFICATE-----\n"
|
||||||
|
>>.
|
||||||
|
|
||||||
|
key() ->
|
||||||
|
<<
|
||||||
|
"-----BEGIN EC PRIVATE KEY-----\n"
|
||||||
|
"MHQCAQEEICKTbbathzvD8zvgjL7qRHhW4alS0+j0Loo7WeYX9AxaoAcGBSuBBAAK\n"
|
||||||
|
"oUQDQgAEJBdF7MIdam5T4YF3JkEyaPKdG64TVWCHwr/plC0QzNVJ67efXwxlVGTo\n"
|
||||||
|
"ju0VBj6tOX1y6C0U+85VOM0UU5xqvw==\n"
|
||||||
|
"-----END EC PRIVATE KEY-----\n"
|
||||||
|
>>.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Schema
|
||||||
|
%% -------------------------------------------------------------------
|
||||||
|
|
||||||
|
roots() ->
|
||||||
|
[?MODULE].
|
||||||
|
|
||||||
|
namespace() ->
|
||||||
|
"ct".
|
||||||
|
|
||||||
|
fields(?MODULE) ->
|
||||||
|
[
|
||||||
|
{servers, mk(hoconsc:map(string(), ref(?MODULE, server)))},
|
||||||
|
{clients, mk(hoconsc:array(ref(?MODULE, client)))}
|
||||||
|
];
|
||||||
|
fields(server) ->
|
||||||
|
[
|
||||||
|
{ssl, mk(ref(emqx_schema, "listener_ssl_opts"))}
|
||||||
|
];
|
||||||
|
fields(client) ->
|
||||||
|
[
|
||||||
|
{transport,
|
||||||
|
mk(
|
||||||
|
ref(?MODULE, transport),
|
||||||
|
#{required => false}
|
||||||
|
)}
|
||||||
|
];
|
||||||
|
fields(transport) ->
|
||||||
|
[
|
||||||
|
{ssl,
|
||||||
|
mk(
|
||||||
|
ref(emqx_schema, "ssl_client_opts"),
|
||||||
|
#{default => #{<<"enable">> => false}}
|
||||||
|
)}
|
||||||
|
].
|
|
@ -108,8 +108,8 @@ ssl_files_failure_test_() ->
|
||||||
{error, #{file_read := enoent, pem_check := invalid_pem}},
|
{error, #{file_read := enoent, pem_check := invalid_pem}},
|
||||||
emqx_tls_lib:ensure_ssl_files("/tmp", #{
|
emqx_tls_lib:ensure_ssl_files("/tmp", #{
|
||||||
<<"keyfile">> => NonExistingFile,
|
<<"keyfile">> => NonExistingFile,
|
||||||
<<"certfile">> => bin(test_key()),
|
<<"certfile">> => test_key(),
|
||||||
<<"cacertfile">> => bin(test_key())
|
<<"cacertfile">> => test_key()
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
end},
|
end},
|
||||||
|
@ -121,8 +121,8 @@ ssl_files_failure_test_() ->
|
||||||
}},
|
}},
|
||||||
emqx_tls_lib:ensure_ssl_files("/tmp", #{
|
emqx_tls_lib:ensure_ssl_files("/tmp", #{
|
||||||
<<"keyfile">> => <<>>,
|
<<"keyfile">> => <<>>,
|
||||||
<<"certfile">> => bin(test_key()),
|
<<"certfile">> => test_key(),
|
||||||
<<"cacertfile">> => bin(test_key())
|
<<"cacertfile">> => test_key()
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
%% not valid unicode
|
%% not valid unicode
|
||||||
|
@ -132,8 +132,8 @@ ssl_files_failure_test_() ->
|
||||||
}},
|
}},
|
||||||
emqx_tls_lib:ensure_ssl_files("/tmp", #{
|
emqx_tls_lib:ensure_ssl_files("/tmp", #{
|
||||||
<<"keyfile">> => <<255, 255>>,
|
<<"keyfile">> => <<255, 255>>,
|
||||||
<<"certfile">> => bin(test_key()),
|
<<"certfile">> => test_key(),
|
||||||
<<"cacertfile">> => bin(test_key())
|
<<"cacertfile">> => test_key()
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
|
@ -142,9 +142,9 @@ ssl_files_failure_test_() ->
|
||||||
which_options := [[<<"ocsp">>, <<"issuer_pem">>]]
|
which_options := [[<<"ocsp">>, <<"issuer_pem">>]]
|
||||||
}},
|
}},
|
||||||
emqx_tls_lib:ensure_ssl_files("/tmp", #{
|
emqx_tls_lib:ensure_ssl_files("/tmp", #{
|
||||||
<<"keyfile">> => bin(test_key()),
|
<<"keyfile">> => test_key(),
|
||||||
<<"certfile">> => bin(test_key()),
|
<<"certfile">> => test_key(),
|
||||||
<<"cacertfile">> => bin(test_key()),
|
<<"cacertfile">> => test_key(),
|
||||||
<<"ocsp">> => #{<<"issuer_pem">> => <<255, 255>>}
|
<<"ocsp">> => #{<<"issuer_pem">> => <<255, 255>>}
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
@ -153,8 +153,8 @@ ssl_files_failure_test_() ->
|
||||||
{error, #{reason := invalid_file_path_or_pem_string}},
|
{error, #{reason := invalid_file_path_or_pem_string}},
|
||||||
emqx_tls_lib:ensure_ssl_files("/tmp", #{
|
emqx_tls_lib:ensure_ssl_files("/tmp", #{
|
||||||
<<"keyfile">> => <<33, 22>>,
|
<<"keyfile">> => <<33, 22>>,
|
||||||
<<"certfile">> => bin(test_key()),
|
<<"certfile">> => test_key(),
|
||||||
<<"cacertfile">> => bin(test_key())
|
<<"cacertfile">> => test_key()
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
TmpFile = filename:join("/tmp", integer_to_list(erlang:system_time(microsecond))),
|
TmpFile = filename:join("/tmp", integer_to_list(erlang:system_time(microsecond))),
|
||||||
|
@ -178,63 +178,9 @@ ssl_files_failure_test_() ->
|
||||||
end}
|
end}
|
||||||
].
|
].
|
||||||
|
|
||||||
ssl_files_save_delete_test() ->
|
|
||||||
Key = bin(test_key()),
|
|
||||||
SSL0 = #{
|
|
||||||
<<"keyfile">> => Key,
|
|
||||||
<<"certfile">> => Key,
|
|
||||||
<<"cacertfile">> => Key,
|
|
||||||
<<"ocsp">> => #{<<"issuer_pem">> => Key}
|
|
||||||
},
|
|
||||||
Dir = filename:join(["/tmp", "ssl-test-dir"]),
|
|
||||||
{ok, SSL} = emqx_tls_lib:ensure_ssl_files(Dir, SSL0),
|
|
||||||
FileKey = maps:get(<<"keyfile">>, SSL),
|
|
||||||
?assertMatch(<<"/tmp/ssl-test-dir/key-", _:16/binary>>, FileKey),
|
|
||||||
?assertEqual({ok, bin(test_key())}, file:read_file(FileKey)),
|
|
||||||
FileIssuerPem = emqx_utils_maps:deep_get([<<"ocsp">>, <<"issuer_pem">>], SSL),
|
|
||||||
?assertMatch(<<"/tmp/ssl-test-dir/ocsp_issuer_pem-", _:16/binary>>, FileIssuerPem),
|
|
||||||
?assertEqual({ok, bin(test_key())}, file:read_file(FileIssuerPem)),
|
|
||||||
%% no old file to delete
|
|
||||||
ok = emqx_tls_lib:delete_ssl_files(Dir, SSL, undefined),
|
|
||||||
?assertEqual({ok, bin(test_key())}, file:read_file(FileKey)),
|
|
||||||
?assertEqual({ok, bin(test_key())}, file:read_file(FileIssuerPem)),
|
|
||||||
%% old and new identical, no delete
|
|
||||||
ok = emqx_tls_lib:delete_ssl_files(Dir, SSL, SSL),
|
|
||||||
?assertEqual({ok, bin(test_key())}, file:read_file(FileKey)),
|
|
||||||
?assertEqual({ok, bin(test_key())}, file:read_file(FileIssuerPem)),
|
|
||||||
%% new is gone, delete old
|
|
||||||
ok = emqx_tls_lib:delete_ssl_files(Dir, undefined, SSL),
|
|
||||||
?assertEqual({error, enoent}, file:read_file(FileKey)),
|
|
||||||
?assertEqual({error, enoent}, file:read_file(FileIssuerPem)),
|
|
||||||
%% test idempotence
|
|
||||||
ok = emqx_tls_lib:delete_ssl_files(Dir, undefined, SSL),
|
|
||||||
ok.
|
|
||||||
|
|
||||||
ssl_files_handle_non_generated_file_test() ->
|
|
||||||
TmpKeyFile = <<"my-key-file.pem">>,
|
|
||||||
KeyFileContent = bin(test_key()),
|
|
||||||
ok = file:write_file(TmpKeyFile, KeyFileContent),
|
|
||||||
?assert(filelib:is_regular(TmpKeyFile)),
|
|
||||||
SSL0 = #{
|
|
||||||
<<"keyfile">> => TmpKeyFile,
|
|
||||||
<<"certfile">> => TmpKeyFile,
|
|
||||||
<<"cacertfile">> => TmpKeyFile,
|
|
||||||
<<"ocsp">> => #{<<"issuer_pem">> => TmpKeyFile}
|
|
||||||
},
|
|
||||||
Dir = filename:join(["/tmp", "ssl-test-dir-00"]),
|
|
||||||
{ok, SSL2} = emqx_tls_lib:ensure_ssl_files(Dir, SSL0),
|
|
||||||
File1 = maps:get(<<"keyfile">>, SSL2),
|
|
||||||
%% verify the filename and path is not changed by the emqx_tls_lib
|
|
||||||
?assertEqual(TmpKeyFile, File1),
|
|
||||||
ok = emqx_tls_lib:delete_ssl_files(Dir, undefined, SSL2),
|
|
||||||
%% verify the file is not delete and not changed, because it is not generated by
|
|
||||||
%% emqx_tls_lib
|
|
||||||
?assertEqual({ok, KeyFileContent}, file:read_file(TmpKeyFile)),
|
|
||||||
ok = file:delete(TmpKeyFile).
|
|
||||||
|
|
||||||
ssl_file_replace_test() ->
|
ssl_file_replace_test() ->
|
||||||
Key1 = bin(test_key()),
|
Key1 = test_key(),
|
||||||
Key2 = bin(test_key2()),
|
Key2 = test_key2(),
|
||||||
SSL0 = #{
|
SSL0 = #{
|
||||||
<<"keyfile">> => Key1,
|
<<"keyfile">> => Key1,
|
||||||
<<"certfile">> => Key1,
|
<<"certfile">> => Key1,
|
||||||
|
@ -258,32 +204,44 @@ ssl_file_replace_test() ->
|
||||||
?assert(filelib:is_regular(File2)),
|
?assert(filelib:is_regular(File2)),
|
||||||
?assert(filelib:is_regular(IssuerPem1)),
|
?assert(filelib:is_regular(IssuerPem1)),
|
||||||
?assert(filelib:is_regular(IssuerPem2)),
|
?assert(filelib:is_regular(IssuerPem2)),
|
||||||
%% delete old file (File1, in SSL2)
|
|
||||||
ok = emqx_tls_lib:delete_ssl_files(Dir, SSL3, SSL2),
|
|
||||||
?assertNot(filelib:is_regular(File1)),
|
|
||||||
?assert(filelib:is_regular(File2)),
|
|
||||||
?assertNot(filelib:is_regular(IssuerPem1)),
|
|
||||||
?assert(filelib:is_regular(IssuerPem2)),
|
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
ssl_file_deterministic_names_test() ->
|
||||||
|
SSL0 = #{
|
||||||
|
<<"keyfile">> => test_key(),
|
||||||
|
<<"certfile">> => test_key()
|
||||||
|
},
|
||||||
|
Dir0 = filename:join(["/tmp", ?FUNCTION_NAME, "ssl0"]),
|
||||||
|
Dir1 = filename:join(["/tmp", ?FUNCTION_NAME, "ssl1"]),
|
||||||
|
{ok, SSLFiles0} = emqx_tls_lib:ensure_ssl_files(Dir0, SSL0),
|
||||||
|
?assertEqual(
|
||||||
|
{ok, SSLFiles0},
|
||||||
|
emqx_tls_lib:ensure_ssl_files(Dir0, SSL0)
|
||||||
|
),
|
||||||
|
?assertNotEqual(
|
||||||
|
{ok, SSLFiles0},
|
||||||
|
emqx_tls_lib:ensure_ssl_files(Dir1, SSL0)
|
||||||
|
),
|
||||||
|
_ = file:del_dir_r(filename:join(["/tmp", ?FUNCTION_NAME])).
|
||||||
|
|
||||||
bin(X) -> iolist_to_binary(X).
|
bin(X) -> iolist_to_binary(X).
|
||||||
|
|
||||||
test_key() ->
|
test_key() ->
|
||||||
""
|
<<
|
||||||
"\n"
|
"\n"
|
||||||
"-----BEGIN EC PRIVATE KEY-----\n"
|
"-----BEGIN EC PRIVATE KEY-----\n"
|
||||||
"MHQCAQEEICKTbbathzvD8zvgjL7qRHhW4alS0+j0Loo7WeYX9AxaoAcGBSuBBAAK\n"
|
"MHQCAQEEICKTbbathzvD8zvgjL7qRHhW4alS0+j0Loo7WeYX9AxaoAcGBSuBBAAK\n"
|
||||||
"oUQDQgAEJBdF7MIdam5T4YF3JkEyaPKdG64TVWCHwr/plC0QzNVJ67efXwxlVGTo\n"
|
"oUQDQgAEJBdF7MIdam5T4YF3JkEyaPKdG64TVWCHwr/plC0QzNVJ67efXwxlVGTo\n"
|
||||||
"ju0VBj6tOX1y6C0U+85VOM0UU5xqvw==\n"
|
"ju0VBj6tOX1y6C0U+85VOM0UU5xqvw==\n"
|
||||||
"-----END EC PRIVATE KEY-----\n"
|
"-----END EC PRIVATE KEY-----\n"
|
||||||
"".
|
>>.
|
||||||
|
|
||||||
test_key2() ->
|
test_key2() ->
|
||||||
""
|
<<
|
||||||
"\n"
|
"\n"
|
||||||
"-----BEGIN EC PRIVATE KEY-----\n"
|
"-----BEGIN EC PRIVATE KEY-----\n"
|
||||||
"MHQCAQEEID9UlIyAlLFw0irkRHX29N+ZGivGtDjlVJvATY3B0TTmoAcGBSuBBAAK\n"
|
"MHQCAQEEID9UlIyAlLFw0irkRHX29N+ZGivGtDjlVJvATY3B0TTmoAcGBSuBBAAK\n"
|
||||||
"oUQDQgAEUwiarudRNAT25X11js8gE9G+q0GdsT53QJQjRtBO+rTwuCW1vhLzN0Ve\n"
|
"oUQDQgAEUwiarudRNAT25X11js8gE9G+q0GdsT53QJQjRtBO+rTwuCW1vhLzN0Ve\n"
|
||||||
"AbToUD4JmV9m/XwcSVH06ZaWqNuC5w==\n"
|
"AbToUD4JmV9m/XwcSVH06ZaWqNuC5w==\n"
|
||||||
"-----END EC PRIVATE KEY-----\n"
|
"-----END EC PRIVATE KEY-----\n"
|
||||||
"".
|
>>.
|
||||||
|
|
|
@ -33,21 +33,12 @@ all() ->
|
||||||
emqx_common_test_helpers:all(?MODULE).
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
%% ensure dependent apps stopped
|
ok = emqx_common_test_helpers:start_apps([]),
|
||||||
emqx_common_test_helpers:stop_apps([]),
|
Listeners = emqx_listeners:list(),
|
||||||
?check_trace(
|
ct:pal("emqx_listeners:list() = ~p~n", [Listeners]),
|
||||||
?wait_async_action(
|
?assertMatch(
|
||||||
emqx_common_test_helpers:start_apps([]),
|
[_ | _],
|
||||||
#{?snk_kind := listener_started, bind := 1883},
|
[ID || {ID, #{running := true}} <- Listeners]
|
||||||
timer:seconds(100)
|
|
||||||
),
|
|
||||||
fun(Trace) ->
|
|
||||||
ct:pal("listener start statuses: ~p", [
|
|
||||||
?of_kind([listener_started, listener_not_started], Trace)
|
|
||||||
]),
|
|
||||||
%% more than one listener
|
|
||||||
?assertMatch([_ | _], ?of_kind(listener_started, Trace))
|
|
||||||
end
|
|
||||||
),
|
),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
|
|
|
@ -62,8 +62,6 @@
|
||||||
|
|
||||||
-define(METRICS, [?METRIC_SUPERUSER, ?METRIC_ALLOW, ?METRIC_DENY, ?METRIC_NOMATCH]).
|
-define(METRICS, [?METRIC_SUPERUSER, ?METRIC_ALLOW, ?METRIC_DENY, ?METRIC_NOMATCH]).
|
||||||
|
|
||||||
-define(IS_ENABLED(Enable), ((Enable =:= true) or (Enable =:= <<"true">>))).
|
|
||||||
|
|
||||||
%% Initialize authz backend.
|
%% Initialize authz backend.
|
||||||
%% Populate the passed configuration map with necessary data,
|
%% Populate the passed configuration map with necessary data,
|
||||||
%% like `ResourceID`s
|
%% like `ResourceID`s
|
||||||
|
@ -266,7 +264,6 @@ ensure_deleted(#{enable := false}, _) ->
|
||||||
ensure_deleted(Source, #{clear_metric := ClearMetric}) ->
|
ensure_deleted(Source, #{clear_metric := ClearMetric}) ->
|
||||||
TypeName = type(Source),
|
TypeName = type(Source),
|
||||||
ensure_resource_deleted(Source),
|
ensure_resource_deleted(Source),
|
||||||
clear_certs(Source),
|
|
||||||
ClearMetric andalso emqx_metrics_worker:clear_metrics(authz_metrics, TypeName).
|
ClearMetric andalso emqx_metrics_worker:clear_metrics(authz_metrics, TypeName).
|
||||||
|
|
||||||
ensure_resource_deleted(#{type := Type} = Source) ->
|
ensure_resource_deleted(#{type := Type} = Source) ->
|
||||||
|
@ -530,22 +527,16 @@ write_acl_file(Source) ->
|
||||||
acl_conf_file() ->
|
acl_conf_file() ->
|
||||||
filename:join([emqx:data_dir(), "authz", "acl.conf"]).
|
filename:join([emqx:data_dir(), "authz", "acl.conf"]).
|
||||||
|
|
||||||
maybe_write_certs(#{<<"type">> := Type} = Source) ->
|
maybe_write_certs(#{<<"type">> := Type, <<"ssl">> := SSL = #{}} = Source) ->
|
||||||
case
|
case emqx_tls_lib:ensure_ssl_files(ssl_file_path(Type), SSL) of
|
||||||
emqx_tls_lib:ensure_ssl_files(
|
{ok, NSSL} ->
|
||||||
ssl_file_path(Type), maps:get(<<"ssl">>, Source, undefined)
|
Source#{<<"ssl">> => NSSL};
|
||||||
)
|
|
||||||
of
|
|
||||||
{ok, SSL} ->
|
|
||||||
new_ssl_source(Source, SSL);
|
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, Reason#{msg => "bad_ssl_config"}),
|
?SLOG(error, Reason#{msg => "bad_ssl_config"}),
|
||||||
throw({bad_ssl_config, Reason})
|
throw({bad_ssl_config, Reason})
|
||||||
end.
|
end;
|
||||||
|
maybe_write_certs(#{} = Source) ->
|
||||||
clear_certs(OldSource) ->
|
Source.
|
||||||
OldSSL = maps:get(ssl, OldSource, undefined),
|
|
||||||
ok = emqx_tls_lib:delete_ssl_files(ssl_file_path(type(OldSource)), undefined, OldSSL).
|
|
||||||
|
|
||||||
write_file(Filename, Bytes) ->
|
write_file(Filename, Bytes) ->
|
||||||
ok = filelib:ensure_dir(Filename),
|
ok = filelib:ensure_dir(Filename),
|
||||||
|
@ -560,11 +551,6 @@ write_file(Filename, Bytes) ->
|
||||||
ssl_file_path(Type) ->
|
ssl_file_path(Type) ->
|
||||||
filename:join(["authz", Type]).
|
filename:join(["authz", Type]).
|
||||||
|
|
||||||
new_ssl_source(Source, undefined) ->
|
|
||||||
Source;
|
|
||||||
new_ssl_source(Source, SSL) ->
|
|
||||||
Source#{<<"ssl">> => SSL}.
|
|
||||||
|
|
||||||
get_source_by_type(Type, Sources) ->
|
get_source_by_type(Type, Sources) ->
|
||||||
{Source, _Front, _Rear} = take(Type, Sources),
|
{Source, _Front, _Rear} = take(Type, Sources),
|
||||||
Source.
|
Source.
|
||||||
|
|
|
@ -69,15 +69,13 @@ pre_config_update(Path, Conf, _OldConfig) when is_map(Conf) ->
|
||||||
{ok, ConfNew}
|
{ok, ConfNew}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
post_config_update([bridges, BridgeType, BridgeName] = Path, '$remove', _, OldConf, _AppEnvs) ->
|
post_config_update([bridges, BridgeType, BridgeName], '$remove', _, _OldConf, _AppEnvs) ->
|
||||||
_ = emqx_connector_ssl:clear_certs(filename:join(Path), OldConf),
|
|
||||||
ok = emqx_bridge_resource:remove(BridgeType, BridgeName),
|
ok = emqx_bridge_resource:remove(BridgeType, BridgeName),
|
||||||
Bridges = emqx_utils_maps:deep_remove([BridgeType, BridgeName], emqx:get_config([bridges])),
|
Bridges = emqx_utils_maps:deep_remove([BridgeType, BridgeName], emqx:get_config([bridges])),
|
||||||
emqx_bridge:reload_hook(Bridges),
|
emqx_bridge:reload_hook(Bridges),
|
||||||
?tp(bridge_post_config_update_done, #{}),
|
?tp(bridge_post_config_update_done, #{}),
|
||||||
ok;
|
ok;
|
||||||
post_config_update([bridges, BridgeType, BridgeName] = Path, _Req, NewConf, undefined, _AppEnvs) ->
|
post_config_update([bridges, BridgeType, BridgeName], _Req, NewConf, undefined, _AppEnvs) ->
|
||||||
_ = emqx_connector_ssl:try_clear_certs(filename:join(Path), NewConf, undefined),
|
|
||||||
ResOpts = emqx_resource:fetch_creation_opts(NewConf),
|
ResOpts = emqx_resource:fetch_creation_opts(NewConf),
|
||||||
ok = emqx_bridge_resource:create(BridgeType, BridgeName, NewConf, ResOpts),
|
ok = emqx_bridge_resource:create(BridgeType, BridgeName, NewConf, ResOpts),
|
||||||
Bridges = emqx_utils_maps:deep_put(
|
Bridges = emqx_utils_maps:deep_put(
|
||||||
|
@ -86,8 +84,7 @@ post_config_update([bridges, BridgeType, BridgeName] = Path, _Req, NewConf, unde
|
||||||
emqx_bridge:reload_hook(Bridges),
|
emqx_bridge:reload_hook(Bridges),
|
||||||
?tp(bridge_post_config_update_done, #{}),
|
?tp(bridge_post_config_update_done, #{}),
|
||||||
ok;
|
ok;
|
||||||
post_config_update([bridges, BridgeType, BridgeName] = Path, _Req, NewConf, OldConf, _AppEnvs) ->
|
post_config_update([bridges, BridgeType, BridgeName], _Req, NewConf, OldConf, _AppEnvs) ->
|
||||||
_ = emqx_connector_ssl:try_clear_certs(filename:join(Path), NewConf, OldConf),
|
|
||||||
ResOpts = emqx_resource:fetch_creation_opts(NewConf),
|
ResOpts = emqx_resource:fetch_creation_opts(NewConf),
|
||||||
ok = emqx_bridge_resource:update(BridgeType, BridgeName, {OldConf, NewConf}, ResOpts),
|
ok = emqx_bridge_resource:update(BridgeType, BridgeName, {OldConf, NewConf}, ResOpts),
|
||||||
Bridges = emqx_utils_maps:deep_put(
|
Bridges = emqx_utils_maps:deep_put(
|
||||||
|
|
|
@ -247,25 +247,22 @@ recreate(Type, Name, Conf, Opts) ->
|
||||||
).
|
).
|
||||||
|
|
||||||
create_dry_run(Type, Conf0) ->
|
create_dry_run(Type, Conf0) ->
|
||||||
TmpPath0 = iolist_to_binary([?TEST_ID_PREFIX, emqx_utils:gen_id(8)]),
|
TmpName = iolist_to_binary([?TEST_ID_PREFIX, emqx_utils:gen_id(8)]),
|
||||||
TmpPath = emqx_utils:safe_filename(TmpPath0),
|
TmpPath = emqx_utils:safe_filename(TmpName),
|
||||||
Conf = emqx_utils_maps:safe_atom_key_map(Conf0),
|
Conf = emqx_utils_maps:safe_atom_key_map(Conf0),
|
||||||
case emqx_connector_ssl:convert_certs(TmpPath, Conf) of
|
case emqx_connector_ssl:convert_certs(TmpPath, Conf) of
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, Reason};
|
{error, Reason};
|
||||||
{ok, ConfNew} ->
|
{ok, ConfNew} ->
|
||||||
try
|
try
|
||||||
ParseConf = parse_confs(bin(Type), TmpPath, ConfNew),
|
ParseConf = parse_confs(bin(Type), TmpName, ConfNew),
|
||||||
Res = emqx_resource:create_dry_run_local(
|
emqx_resource:create_dry_run_local(bridge_to_resource_type(Type), ParseConf)
|
||||||
bridge_to_resource_type(Type), ParseConf
|
|
||||||
),
|
|
||||||
Res
|
|
||||||
catch
|
catch
|
||||||
%% validation errors
|
%% validation errors
|
||||||
throw:Reason ->
|
throw:Reason ->
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
after
|
after
|
||||||
_ = maybe_clear_certs(TmpPath, ConfNew)
|
_ = file:del_dir_r(emqx_tls_lib:pem_dir(TmpPath))
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
@ -285,27 +282,6 @@ remove(Type, Name, _Conf, _Opts) ->
|
||||||
{error, Reason} -> {error, Reason}
|
{error, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
maybe_clear_certs(TmpPath, #{ssl := SslConf} = Conf) ->
|
|
||||||
%% don't remove the cert files if they are in use
|
|
||||||
case is_tmp_path_conf(TmpPath, SslConf) of
|
|
||||||
true -> emqx_connector_ssl:clear_certs(TmpPath, Conf);
|
|
||||||
false -> ok
|
|
||||||
end;
|
|
||||||
maybe_clear_certs(_TmpPath, _ConfWithoutSsl) ->
|
|
||||||
ok.
|
|
||||||
|
|
||||||
is_tmp_path_conf(TmpPath, #{certfile := Certfile}) ->
|
|
||||||
is_tmp_path(TmpPath, Certfile);
|
|
||||||
is_tmp_path_conf(TmpPath, #{keyfile := Keyfile}) ->
|
|
||||||
is_tmp_path(TmpPath, Keyfile);
|
|
||||||
is_tmp_path_conf(TmpPath, #{cacertfile := CaCertfile}) ->
|
|
||||||
is_tmp_path(TmpPath, CaCertfile);
|
|
||||||
is_tmp_path_conf(_TmpPath, _Conf) ->
|
|
||||||
false.
|
|
||||||
|
|
||||||
is_tmp_path(TmpPath, File) ->
|
|
||||||
string:str(str(File), str(TmpPath)) > 0.
|
|
||||||
|
|
||||||
%% convert bridge configs to what the connector modules want
|
%% convert bridge configs to what the connector modules want
|
||||||
parse_confs(
|
parse_confs(
|
||||||
<<"webhook">>,
|
<<"webhook">>,
|
||||||
|
@ -412,9 +388,6 @@ parse_url(Url) ->
|
||||||
invalid_data(<<"Missing scheme in URL: ", Url/binary>>)
|
invalid_data(<<"Missing scheme in URL: ", Url/binary>>)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
str(Bin) when is_binary(Bin) -> binary_to_list(Bin);
|
|
||||||
str(Str) when is_list(Str) -> Str.
|
|
||||||
|
|
||||||
bin(Bin) when is_binary(Bin) -> Bin;
|
bin(Bin) when is_binary(Bin) -> Bin;
|
||||||
bin(Str) when is_list(Str) -> list_to_binary(Str);
|
bin(Str) when is_list(Str) -> list_to_binary(Str);
|
||||||
bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8).
|
bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8).
|
||||||
|
|
|
@ -156,6 +156,7 @@ setup_fake_telemetry_data() ->
|
||||||
|
|
||||||
t_update_ssl_conf(Config) ->
|
t_update_ssl_conf(Config) ->
|
||||||
Path = proplists:get_value(config_path, Config),
|
Path = proplists:get_value(config_path, Config),
|
||||||
|
CertDir = filename:join([emqx:mutable_certs_dir() | Path]),
|
||||||
EnableSSLConf = #{
|
EnableSSLConf = #{
|
||||||
<<"bridge_mode">> => false,
|
<<"bridge_mode">> => false,
|
||||||
<<"clean_start">> => true,
|
<<"clean_start">> => true,
|
||||||
|
@ -172,22 +173,13 @@ t_update_ssl_conf(Config) ->
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ok, _} = emqx:update_config(Path, EnableSSLConf),
|
{ok, _} = emqx:update_config(Path, EnableSSLConf),
|
||||||
{ok, Certs} = list_pem_dir(Path),
|
?assertMatch({ok, [_, _, _]}, file:list_dir(CertDir)),
|
||||||
?assertMatch([_, _, _], Certs),
|
|
||||||
NoSSLConf = EnableSSLConf#{<<"ssl">> := #{<<"enable">> => false}},
|
NoSSLConf = EnableSSLConf#{<<"ssl">> := #{<<"enable">> => false}},
|
||||||
{ok, _} = emqx:update_config(Path, NoSSLConf),
|
{ok, _} = emqx:update_config(Path, NoSSLConf),
|
||||||
?assertMatch({error, not_dir}, list_pem_dir(Path)),
|
{ok, _} = emqx_tls_certfile_gc:force(),
|
||||||
|
?assertMatch({error, enoent}, file:list_dir(CertDir)),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
list_pem_dir(Path) ->
|
|
||||||
Dir = filename:join([emqx:mutable_certs_dir() | Path]),
|
|
||||||
case filelib:is_dir(Dir) of
|
|
||||||
true ->
|
|
||||||
file:list_dir(Dir);
|
|
||||||
_ ->
|
|
||||||
{error, not_dir}
|
|
||||||
end.
|
|
||||||
|
|
||||||
data_file(Name) ->
|
data_file(Name) ->
|
||||||
Dir = code:lib_dir(emqx_bridge, test),
|
Dir = code:lib_dir(emqx_bridge, test),
|
||||||
{ok, Bin} = file:read_file(filename:join([Dir, "data", Name])),
|
{ok, Bin} = file:read_file(filename:join([Dir, "data", Name])),
|
||||||
|
|
|
@ -19,9 +19,7 @@
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
-export([
|
-export([
|
||||||
convert_certs/2,
|
convert_certs/2
|
||||||
clear_certs/2,
|
|
||||||
try_clear_certs/3
|
|
||||||
]).
|
]).
|
||||||
|
|
||||||
convert_certs(RltvDir, #{<<"ssl">> := SSL} = Config) ->
|
convert_certs(RltvDir, #{<<"ssl">> := SSL} = Config) ->
|
||||||
|
@ -32,26 +30,6 @@ convert_certs(RltvDir, #{ssl := SSL} = Config) ->
|
||||||
convert_certs(_RltvDir, Config) ->
|
convert_certs(_RltvDir, Config) ->
|
||||||
{ok, Config}.
|
{ok, Config}.
|
||||||
|
|
||||||
clear_certs(RltvDir, Config) ->
|
|
||||||
clear_certs2(RltvDir, normalize_key_to_bin(Config)).
|
|
||||||
|
|
||||||
clear_certs2(RltvDir, #{<<"ssl">> := OldSSL} = _Config) ->
|
|
||||||
ok = emqx_tls_lib:delete_ssl_files(RltvDir, undefined, OldSSL);
|
|
||||||
clear_certs2(_RltvDir, _) ->
|
|
||||||
ok.
|
|
||||||
|
|
||||||
try_clear_certs(RltvDir, NewConf, OldConf) ->
|
|
||||||
try_clear_certs2(
|
|
||||||
RltvDir,
|
|
||||||
normalize_key_to_bin(NewConf),
|
|
||||||
normalize_key_to_bin(OldConf)
|
|
||||||
).
|
|
||||||
|
|
||||||
try_clear_certs2(RltvDir, NewConf, OldConf) ->
|
|
||||||
NewSSL = try_map_get(<<"ssl">>, NewConf, undefined),
|
|
||||||
OldSSL = try_map_get(<<"ssl">>, OldConf, undefined),
|
|
||||||
ok = emqx_tls_lib:delete_ssl_files(RltvDir, NewSSL, OldSSL).
|
|
||||||
|
|
||||||
new_ssl_config(RltvDir, Config, SSL) ->
|
new_ssl_config(RltvDir, Config, SSL) ->
|
||||||
case emqx_tls_lib:ensure_ssl_files(RltvDir, SSL) of
|
case emqx_tls_lib:ensure_ssl_files(RltvDir, SSL) of
|
||||||
{ok, NewSSL} ->
|
{ok, NewSSL} ->
|
||||||
|
@ -70,13 +48,3 @@ new_ssl_config(#{<<"ssl">> := _} = Config, NewSSL) ->
|
||||||
Config#{<<"ssl">> => NewSSL};
|
Config#{<<"ssl">> => NewSSL};
|
||||||
new_ssl_config(Config, _NewSSL) ->
|
new_ssl_config(Config, _NewSSL) ->
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
normalize_key_to_bin(undefined) ->
|
|
||||||
undefined;
|
|
||||||
normalize_key_to_bin(Map) when is_map(Map) ->
|
|
||||||
emqx_utils_maps:binary_key_map(Map).
|
|
||||||
|
|
||||||
try_map_get(Key, Map, Default) when is_map(Map) ->
|
|
||||||
maps:get(Key, Map, Default);
|
|
||||||
try_map_get(_Key, undefined, Default) ->
|
|
||||||
Default.
|
|
||||||
|
|
|
@ -177,7 +177,6 @@ pre_config_update(?EXHOOK, NewConf = #{<<"servers">> := Servers}, _OldConf) ->
|
||||||
|
|
||||||
post_config_update(_KeyPath, UpdateReq, NewConf, OldConf, _AppEnvs) ->
|
post_config_update(_KeyPath, UpdateReq, NewConf, OldConf, _AppEnvs) ->
|
||||||
Result = call({update_config, UpdateReq, NewConf, OldConf}),
|
Result = call({update_config, UpdateReq, NewConf, OldConf}),
|
||||||
try_clear_ssl_files(UpdateReq, NewConf, OldConf),
|
|
||||||
{ok, Result}.
|
{ok, Result}.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
@ -646,44 +645,3 @@ new_ssl_source(Source, undefined) ->
|
||||||
Source;
|
Source;
|
||||||
new_ssl_source(Source, SSL) ->
|
new_ssl_source(Source, SSL) ->
|
||||||
Source#{<<"ssl">> => SSL}.
|
Source#{<<"ssl">> => SSL}.
|
||||||
|
|
||||||
try_clear_ssl_files({delete, Name}, _NewConf, OldConfs) ->
|
|
||||||
OldSSL = find_server_ssl_cfg(Name, OldConfs),
|
|
||||||
emqx_tls_lib:delete_ssl_files(ssl_file_path(Name), undefined, OldSSL);
|
|
||||||
try_clear_ssl_files({Op, Name, _}, NewConfs, OldConfs) when
|
|
||||||
Op =:= update; Op =:= enable
|
|
||||||
->
|
|
||||||
NewSSL = find_server_ssl_cfg(Name, NewConfs),
|
|
||||||
OldSSL = find_server_ssl_cfg(Name, OldConfs),
|
|
||||||
emqx_tls_lib:delete_ssl_files(ssl_file_path(Name), NewSSL, OldSSL);
|
|
||||||
%% replace the whole config from the cli
|
|
||||||
try_clear_ssl_files(_Req, #{servers := NewServers}, #{servers := OldServers}) ->
|
|
||||||
lists:foreach(
|
|
||||||
fun(#{name := Name} = Conf) ->
|
|
||||||
NewSSL = find_server_ssl_cfg(Name, NewServers),
|
|
||||||
OldSSL = maps:get(ssl, Conf, undefined),
|
|
||||||
emqx_tls_lib:delete_ssl_files(ssl_file_path(Name), NewSSL, OldSSL)
|
|
||||||
end,
|
|
||||||
OldServers
|
|
||||||
);
|
|
||||||
try_clear_ssl_files(_Req, _NewConf, _OldConf) ->
|
|
||||||
ok.
|
|
||||||
|
|
||||||
search_server_cfg(Name, Confs) ->
|
|
||||||
lists:search(
|
|
||||||
fun
|
|
||||||
(#{name := SvrName}) when SvrName =:= Name ->
|
|
||||||
true;
|
|
||||||
(_) ->
|
|
||||||
false
|
|
||||||
end,
|
|
||||||
Confs
|
|
||||||
).
|
|
||||||
|
|
||||||
find_server_ssl_cfg(Name, Confs) ->
|
|
||||||
case search_server_cfg(Name, Confs) of
|
|
||||||
{value, Value} ->
|
|
||||||
maps:get(ssl, Value, undefined);
|
|
||||||
false ->
|
|
||||||
undefined
|
|
||||||
end.
|
|
||||||
|
|
|
@ -385,13 +385,10 @@ t_stop_timeout(_) ->
|
||||||
t_ssl_clear(_) ->
|
t_ssl_clear(_) ->
|
||||||
SvrName = <<"ssl_test">>,
|
SvrName = <<"ssl_test">>,
|
||||||
SSLConf = #{
|
SSLConf = #{
|
||||||
<<"cacertfile">> => cert_file("cafile"),
|
|
||||||
|
|
||||||
<<"certfile">> => cert_file("certfile"),
|
|
||||||
|
|
||||||
<<"enable">> => true,
|
<<"enable">> => true,
|
||||||
|
<<"cacertfile">> => cert_file("cafile"),
|
||||||
|
<<"certfile">> => cert_file("certfile"),
|
||||||
<<"keyfile">> => cert_file("keyfile"),
|
<<"keyfile">> => cert_file("keyfile"),
|
||||||
|
|
||||||
<<"verify">> => <<"verify_peer">>
|
<<"verify">> => <<"verify_peer">>
|
||||||
},
|
},
|
||||||
AddConf = #{
|
AddConf = #{
|
||||||
|
@ -402,7 +399,6 @@ t_ssl_clear(_) ->
|
||||||
<<"pool_size">> => 16,
|
<<"pool_size">> => 16,
|
||||||
<<"request_timeout">> => <<"5s">>,
|
<<"request_timeout">> => <<"5s">>,
|
||||||
<<"ssl">> => SSLConf,
|
<<"ssl">> => SSLConf,
|
||||||
|
|
||||||
<<"url">> => <<"http://127.0.0.1:9000">>
|
<<"url">> => <<"http://127.0.0.1:9000">>
|
||||||
},
|
},
|
||||||
emqx_exhook_mgr:update_config([exhook, servers], {add, AddConf}),
|
emqx_exhook_mgr:update_config([exhook, servers], {add, AddConf}),
|
||||||
|
@ -412,6 +408,7 @@ t_ssl_clear(_) ->
|
||||||
|
|
||||||
UpdateConf = AddConf#{<<"ssl">> => SSLConf#{<<"keyfile">> => cert_file("keyfile2")}},
|
UpdateConf = AddConf#{<<"ssl">> => SSLConf#{<<"keyfile">> => cert_file("keyfile2")}},
|
||||||
emqx_exhook_mgr:update_config([exhook, servers], {update, SvrName, UpdateConf}),
|
emqx_exhook_mgr:update_config([exhook, servers], {update, SvrName, UpdateConf}),
|
||||||
|
{ok, _} = emqx_tls_certfile_gc:force(),
|
||||||
ListResult2 = list_pem_dir(SvrName),
|
ListResult2 = list_pem_dir(SvrName),
|
||||||
?assertMatch({ok, [_, _, _]}, ListResult2),
|
?assertMatch({ok, [_, _, _]}, ListResult2),
|
||||||
{ok, ResultList2} = ListResult2,
|
{ok, ResultList2} = ListResult2,
|
||||||
|
@ -428,7 +425,8 @@ t_ssl_clear(_) ->
|
||||||
?assertNotEqual(FindKeyFile(ResultList1), FindKeyFile(ResultList2)),
|
?assertNotEqual(FindKeyFile(ResultList1), FindKeyFile(ResultList2)),
|
||||||
|
|
||||||
emqx_exhook_mgr:update_config([exhook, servers], {delete, SvrName}),
|
emqx_exhook_mgr:update_config([exhook, servers], {delete, SvrName}),
|
||||||
?assertMatch({error, not_dir}, list_pem_dir(SvrName)),
|
{ok, _} = emqx_tls_certfile_gc:force(),
|
||||||
|
?assertMatch({error, enoent}, list_pem_dir(SvrName)),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
@ -500,12 +498,7 @@ is_exhook_callback(Cb) ->
|
||||||
|
|
||||||
list_pem_dir(Name) ->
|
list_pem_dir(Name) ->
|
||||||
Dir = filename:join([emqx:mutable_certs_dir(), "exhook", Name]),
|
Dir = filename:join([emqx:mutable_certs_dir(), "exhook", Name]),
|
||||||
case filelib:is_dir(Dir) of
|
file:list_dir(Dir).
|
||||||
true ->
|
|
||||||
file:list_dir(Dir);
|
|
||||||
_ ->
|
|
||||||
{error, not_dir}
|
|
||||||
end.
|
|
||||||
|
|
||||||
data_file(Name) ->
|
data_file(Name) ->
|
||||||
Dir = code:lib_dir(emqx_exhook, test),
|
Dir = code:lib_dir(emqx_exhook, test),
|
||||||
|
|
|
@ -394,11 +394,6 @@ pre_config_update(?GATEWAY, {update_gateway, GwName, Conf}, RawConf) ->
|
||||||
{ok, emqx_utils_maps:deep_put([GwName], RawConf, NConf1)}
|
{ok, emqx_utils_maps:deep_put([GwName], RawConf, NConf1)}
|
||||||
end;
|
end;
|
||||||
pre_config_update(?GATEWAY, {unload_gateway, GwName}, RawConf) ->
|
pre_config_update(?GATEWAY, {unload_gateway, GwName}, RawConf) ->
|
||||||
_ = tune_gw_certs(
|
|
||||||
fun clear_certs/2,
|
|
||||||
GwName,
|
|
||||||
maps:get(GwName, RawConf, #{})
|
|
||||||
),
|
|
||||||
{ok, maps:remove(GwName, RawConf)};
|
{ok, maps:remove(GwName, RawConf)};
|
||||||
pre_config_update(?GATEWAY, {add_listener, GwName, {LType, LName}, Conf}, RawConf) ->
|
pre_config_update(?GATEWAY, {add_listener, GwName, {LType, LName}, Conf}, RawConf) ->
|
||||||
case get_listener(GwName, LType, LName, RawConf) of
|
case get_listener(GwName, LType, LName, RawConf) of
|
||||||
|
@ -417,8 +412,8 @@ pre_config_update(?GATEWAY, {update_listener, GwName, {LType, LName}, Conf}, Raw
|
||||||
case get_listener(GwName, LType, LName, RawConf) of
|
case get_listener(GwName, LType, LName, RawConf) of
|
||||||
undefined ->
|
undefined ->
|
||||||
badres_listener(not_found, GwName, LType, LName);
|
badres_listener(not_found, GwName, LType, LName);
|
||||||
OldConf ->
|
_OldConf ->
|
||||||
NConf = convert_certs(certs_dir(GwName), Conf, OldConf),
|
NConf = convert_certs(certs_dir(GwName), Conf),
|
||||||
NRawConf = emqx_utils_maps:deep_put(
|
NRawConf = emqx_utils_maps:deep_put(
|
||||||
[GwName, <<"listeners">>, LType, LName],
|
[GwName, <<"listeners">>, LType, LName],
|
||||||
RawConf,
|
RawConf,
|
||||||
|
@ -430,8 +425,7 @@ pre_config_update(?GATEWAY, {remove_listener, GwName, {LType, LName}}, RawConf)
|
||||||
case get_listener(GwName, LType, LName, RawConf) of
|
case get_listener(GwName, LType, LName, RawConf) of
|
||||||
undefined ->
|
undefined ->
|
||||||
{ok, RawConf};
|
{ok, RawConf};
|
||||||
OldConf ->
|
_OldConf ->
|
||||||
clear_certs(certs_dir(GwName), OldConf),
|
|
||||||
Path = [GwName, <<"listeners">>, LType, LName],
|
Path = [GwName, <<"listeners">>, LType, LName],
|
||||||
{ok, emqx_utils_maps:deep_remove(Path, RawConf)}
|
{ok, emqx_utils_maps:deep_remove(Path, RawConf)}
|
||||||
end;
|
end;
|
||||||
|
@ -471,15 +465,17 @@ pre_config_update(?GATEWAY, {add_authn, GwName, {LType, LName}, Conf}, RawConf)
|
||||||
end
|
end
|
||||||
end;
|
end;
|
||||||
pre_config_update(?GATEWAY, {update_authn, GwName, Conf}, RawConf) ->
|
pre_config_update(?GATEWAY, {update_authn, GwName, Conf}, RawConf) ->
|
||||||
|
Path = [GwName, ?AUTHN_BIN],
|
||||||
case get_authn(GwName, RawConf) of
|
case get_authn(GwName, RawConf) of
|
||||||
undefined ->
|
undefined ->
|
||||||
badres_authn(not_found, GwName);
|
badres_authn(not_found, GwName);
|
||||||
OldAuthnConf ->
|
_OldConf ->
|
||||||
CertsDir = authn_certs_dir(GwName, Conf),
|
CertsDir = authn_certs_dir(GwName, Conf),
|
||||||
Conf1 = emqx_authentication_config:convert_certs(CertsDir, Conf, OldAuthnConf),
|
Conf1 = emqx_authentication_config:convert_certs(CertsDir, Conf),
|
||||||
{ok, emqx_utils_maps:deep_put([GwName, ?AUTHN_BIN], RawConf, Conf1)}
|
{ok, emqx_utils_maps:deep_put(Path, RawConf, Conf1)}
|
||||||
end;
|
end;
|
||||||
pre_config_update(?GATEWAY, {update_authn, GwName, {LType, LName}, Conf}, RawConf) ->
|
pre_config_update(?GATEWAY, {update_authn, GwName, {LType, LName}, Conf}, RawConf) ->
|
||||||
|
Path = [GwName, <<"listeners">>, LType, LName],
|
||||||
case get_listener(GwName, LType, LName, RawConf) of
|
case get_listener(GwName, LType, LName, RawConf) of
|
||||||
undefined ->
|
undefined ->
|
||||||
badres_listener(not_found, GwName, LType, LName);
|
badres_listener(not_found, GwName, LType, LName);
|
||||||
|
@ -489,51 +485,20 @@ pre_config_update(?GATEWAY, {update_authn, GwName, {LType, LName}, Conf}, RawCon
|
||||||
badres_listener_authn(not_found, GwName, LType, LName);
|
badres_listener_authn(not_found, GwName, LType, LName);
|
||||||
OldAuthnConf ->
|
OldAuthnConf ->
|
||||||
CertsDir = authn_certs_dir(GwName, LType, LName, OldAuthnConf),
|
CertsDir = authn_certs_dir(GwName, LType, LName, OldAuthnConf),
|
||||||
Conf1 = emqx_authentication_config:convert_certs(
|
Conf1 = emqx_authentication_config:convert_certs(CertsDir, Conf),
|
||||||
CertsDir,
|
|
||||||
Conf,
|
|
||||||
OldAuthnConf
|
|
||||||
),
|
|
||||||
NListener = maps:put(
|
NListener = maps:put(
|
||||||
?AUTHN_BIN,
|
?AUTHN_BIN,
|
||||||
Conf1,
|
Conf1,
|
||||||
Listener
|
Listener
|
||||||
),
|
),
|
||||||
{ok,
|
{ok, emqx_utils_maps:deep_put(Path, RawConf, NListener)}
|
||||||
emqx_utils_maps:deep_put(
|
|
||||||
[GwName, <<"listeners">>, LType, LName],
|
|
||||||
RawConf,
|
|
||||||
NListener
|
|
||||||
)}
|
|
||||||
end
|
end
|
||||||
end;
|
end;
|
||||||
pre_config_update(?GATEWAY, {remove_authn, GwName}, RawConf) ->
|
pre_config_update(?GATEWAY, {remove_authn, GwName}, RawConf) ->
|
||||||
case get_authn(GwName, RawConf) of
|
Path = [GwName, ?AUTHN_BIN],
|
||||||
undefined ->
|
{ok, emqx_utils_maps:deep_remove(Path, RawConf)};
|
||||||
ok;
|
|
||||||
OldAuthnConf ->
|
|
||||||
CertsDir = authn_certs_dir(GwName, OldAuthnConf),
|
|
||||||
emqx_authentication_config:clear_certs(CertsDir, OldAuthnConf)
|
|
||||||
end,
|
|
||||||
{ok,
|
|
||||||
emqx_utils_maps:deep_remove(
|
|
||||||
[GwName, ?AUTHN_BIN], RawConf
|
|
||||||
)};
|
|
||||||
pre_config_update(?GATEWAY, {remove_authn, GwName, {LType, LName}}, RawConf) ->
|
pre_config_update(?GATEWAY, {remove_authn, GwName, {LType, LName}}, RawConf) ->
|
||||||
Path = [GwName, <<"listeners">>, LType, LName, ?AUTHN_BIN],
|
Path = [GwName, <<"listeners">>, LType, LName, ?AUTHN_BIN],
|
||||||
case
|
|
||||||
emqx_utils_maps:deep_get(
|
|
||||||
Path,
|
|
||||||
RawConf,
|
|
||||||
undefined
|
|
||||||
)
|
|
||||||
of
|
|
||||||
undefined ->
|
|
||||||
ok;
|
|
||||||
OldAuthnConf ->
|
|
||||||
CertsDir = authn_certs_dir(GwName, LType, LName, OldAuthnConf),
|
|
||||||
emqx_authentication_config:clear_certs(CertsDir, OldAuthnConf)
|
|
||||||
end,
|
|
||||||
{ok, emqx_utils_maps:deep_remove(Path, RawConf)};
|
{ok, emqx_utils_maps:deep_remove(Path, RawConf)};
|
||||||
pre_config_update(?GATEWAY, NewRawConf0 = #{}, OldRawConf = #{}) ->
|
pre_config_update(?GATEWAY, NewRawConf0 = #{}, OldRawConf = #{}) ->
|
||||||
%% FIXME don't support gateway's listener's authn update.
|
%% FIXME don't support gateway's listener's authn update.
|
||||||
|
@ -897,43 +862,14 @@ authn_certs_dir(GwName, AuthnConf) ->
|
||||||
convert_certs(SubDir, Conf) ->
|
convert_certs(SubDir, Conf) ->
|
||||||
convert_certs(<<"dtls_options">>, SubDir, convert_certs(<<"ssl_options">>, SubDir, Conf)).
|
convert_certs(<<"dtls_options">>, SubDir, convert_certs(<<"ssl_options">>, SubDir, Conf)).
|
||||||
|
|
||||||
convert_certs(Type, SubDir, Conf) when ?IS_SSL(Type) ->
|
convert_certs(Type, SubDir, Conf) ->
|
||||||
case
|
SSL = maps:get(Type, Conf, undefined),
|
||||||
emqx_tls_lib:ensure_ssl_files(
|
case is_map(SSL) andalso emqx_tls_lib:ensure_ssl_files(SubDir, SSL) of
|
||||||
SubDir,
|
false ->
|
||||||
maps:get(Type, Conf, undefined)
|
Conf;
|
||||||
)
|
{ok, NSSL = #{}} ->
|
||||||
of
|
Conf#{Type => NSSL};
|
||||||
{ok, SSL} ->
|
|
||||||
new_ssl_config(Type, Conf, SSL);
|
|
||||||
{error, Reason} ->
|
|
||||||
?SLOG(error, Reason#{msg => bad_ssl_config}),
|
|
||||||
throw({bad_ssl_config, Reason})
|
|
||||||
end;
|
|
||||||
convert_certs(SubDir, NConf, OConf) when is_map(NConf); is_map(OConf) ->
|
|
||||||
convert_certs(
|
|
||||||
<<"dtls_options">>, SubDir, convert_certs(<<"ssl_options">>, SubDir, NConf, OConf), OConf
|
|
||||||
).
|
|
||||||
|
|
||||||
convert_certs(Type, SubDir, NConf, OConf) when ?IS_SSL(Type) ->
|
|
||||||
OSSL = maps:get(Type, OConf, undefined),
|
|
||||||
NSSL = maps:get(Type, NConf, undefined),
|
|
||||||
case emqx_tls_lib:ensure_ssl_files(SubDir, NSSL) of
|
|
||||||
{ok, NSSL1} ->
|
|
||||||
ok = emqx_tls_lib:delete_ssl_files(SubDir, NSSL1, OSSL),
|
|
||||||
new_ssl_config(Type, NConf, NSSL1);
|
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?SLOG(error, Reason#{msg => bad_ssl_config}),
|
?SLOG(error, Reason#{msg => bad_ssl_config}),
|
||||||
throw({bad_ssl_config, Reason})
|
throw({bad_ssl_config, Reason})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
new_ssl_config(_Type, Conf, undefined) -> Conf;
|
|
||||||
new_ssl_config(Type, Conf, SSL) when ?IS_SSL(Type) -> Conf#{Type => SSL}.
|
|
||||||
|
|
||||||
clear_certs(SubDir, Conf) ->
|
|
||||||
clear_certs(<<"ssl_options">>, SubDir, Conf),
|
|
||||||
clear_certs(<<"dtls_options">>, SubDir, Conf).
|
|
||||||
|
|
||||||
clear_certs(Type, SubDir, Conf) when ?IS_SSL(Type) ->
|
|
||||||
SSL = maps:get(Type, Conf, undefined),
|
|
||||||
ok = emqx_tls_lib:delete_ssl_files(SubDir, undefined, SSL).
|
|
||||||
|
|
|
@ -674,6 +674,7 @@ t_add_listener_with_certs_content(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
assert_ssl_confs_files_deleted(SslConf) when is_map(SslConf) ->
|
assert_ssl_confs_files_deleted(SslConf) when is_map(SslConf) ->
|
||||||
|
{ok, _} = emqx_tls_certfile_gc:force(),
|
||||||
Ks = [<<"cacertfile">>, <<"certfile">>, <<"keyfile">>],
|
Ks = [<<"cacertfile">>, <<"certfile">>, <<"keyfile">>],
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(K) ->
|
fun(K) ->
|
||||||
|
|
|
@ -237,6 +237,7 @@ t_clear_certs(Config) when is_list(Config) ->
|
||||||
[<<"ssl_options">>, <<"keyfile">>], NewConf2, cert_file("keyfile2")
|
[<<"ssl_options">>, <<"keyfile">>], NewConf2, cert_file("keyfile2")
|
||||||
),
|
),
|
||||||
_ = request(put, NewPath, [], UpdateConf),
|
_ = request(put, NewPath, [], UpdateConf),
|
||||||
|
_ = emqx_tls_certfile_gc:force(),
|
||||||
ListResult2 = list_pem_dir("ssl", "clear"),
|
ListResult2 = list_pem_dir("ssl", "clear"),
|
||||||
|
|
||||||
%% make sure the old cret file is deleted
|
%% make sure the old cret file is deleted
|
||||||
|
@ -259,7 +260,8 @@ t_clear_certs(Config) when is_list(Config) ->
|
||||||
|
|
||||||
%% remove, check all cert files are deleted
|
%% remove, check all cert files are deleted
|
||||||
_ = delete(NewPath),
|
_ = delete(NewPath),
|
||||||
?assertMatch({error, not_dir}, list_pem_dir("ssl", "clear")),
|
_ = emqx_tls_certfile_gc:force(),
|
||||||
|
?assertMatch({error, enoent}, list_pem_dir("ssl", "clear")),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
get_tcp_listeners(Node) ->
|
get_tcp_listeners(Node) ->
|
||||||
|
@ -431,12 +433,7 @@ is_running(Id) ->
|
||||||
list_pem_dir(Type, Name) ->
|
list_pem_dir(Type, Name) ->
|
||||||
ListenerDir = emqx_listeners:certs_dir(Type, Name),
|
ListenerDir = emqx_listeners:certs_dir(Type, Name),
|
||||||
Dir = filename:join([emqx:mutable_certs_dir(), ListenerDir]),
|
Dir = filename:join([emqx:mutable_certs_dir(), ListenerDir]),
|
||||||
case filelib:is_dir(Dir) of
|
file:list_dir(Dir).
|
||||||
true ->
|
|
||||||
file:list_dir(Dir);
|
|
||||||
_ ->
|
|
||||||
{error, not_dir}
|
|
||||||
end.
|
|
||||||
|
|
||||||
data_file(Name) ->
|
data_file(Name) ->
|
||||||
Dir = code:lib_dir(emqx, test),
|
Dir = code:lib_dir(emqx, test),
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
maybe_apply/2,
|
maybe_apply/2,
|
||||||
compose/1,
|
compose/1,
|
||||||
compose/2,
|
compose/2,
|
||||||
|
cons/2,
|
||||||
run_fold/3,
|
run_fold/3,
|
||||||
pipeline/3,
|
pipeline/3,
|
||||||
start_timer/2,
|
start_timer/2,
|
||||||
|
@ -136,6 +137,10 @@ compose(F, G) when is_function(G) -> fun(X) -> G(F(X)) end;
|
||||||
compose(F, [G]) -> compose(F, G);
|
compose(F, [G]) -> compose(F, G);
|
||||||
compose(F, [G | More]) -> compose(compose(F, G), More).
|
compose(F, [G | More]) -> compose(compose(F, G), More).
|
||||||
|
|
||||||
|
-spec cons(X, [X]) -> [X, ...].
|
||||||
|
cons(Head, Tail) ->
|
||||||
|
[Head | Tail].
|
||||||
|
|
||||||
%% @doc RunFold
|
%% @doc RunFold
|
||||||
run_fold([], Acc, _State) ->
|
run_fold([], Acc, _State) ->
|
||||||
Acc;
|
Acc;
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2023 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_utils_fs).
|
||||||
|
|
||||||
|
-include_lib("kernel/include/file.hrl").
|
||||||
|
|
||||||
|
-export([traverse_dir/3]).
|
||||||
|
-export([read_info/1]).
|
||||||
|
-export([canonicalize/1]).
|
||||||
|
|
||||||
|
-type fileinfo() :: #file_info{}.
|
||||||
|
-type foldfun(Acc) ::
|
||||||
|
fun((_Filepath :: file:name(), fileinfo() | {error, file:posix()}, Acc) -> Acc).
|
||||||
|
|
||||||
|
%% @doc Traverse a directory recursively and apply a fold function to each file.
|
||||||
|
%%
|
||||||
|
%% This is a safer version of `filelib:fold_files/5` which does not follow symlinks
|
||||||
|
%% and reports errors to the fold function, giving the user more control over the
|
||||||
|
%% traversal.
|
||||||
|
%% It's not an error if `Dirpath` is not a directory, in which case the fold function
|
||||||
|
%% will be called once with the file info of `Dirpath`.
|
||||||
|
-spec traverse_dir(foldfun(Acc), Acc, _Dirpath :: file:name()) ->
|
||||||
|
Acc.
|
||||||
|
traverse_dir(FoldFun, Acc, Dirpath) ->
|
||||||
|
traverse_dir(FoldFun, Acc, Dirpath, read_info(Dirpath)).
|
||||||
|
|
||||||
|
traverse_dir(FoldFun, AccIn, DirPath, {ok, #file_info{type = directory}}) ->
|
||||||
|
case file:list_dir(DirPath) of
|
||||||
|
{ok, Filenames} ->
|
||||||
|
lists:foldl(
|
||||||
|
fun(Filename, Acc) ->
|
||||||
|
AbsPath = filename:join(DirPath, Filename),
|
||||||
|
traverse_dir(FoldFun, Acc, AbsPath)
|
||||||
|
end,
|
||||||
|
AccIn,
|
||||||
|
Filenames
|
||||||
|
);
|
||||||
|
{error, Reason} ->
|
||||||
|
FoldFun(DirPath, {error, Reason}, AccIn)
|
||||||
|
end;
|
||||||
|
traverse_dir(FoldFun, Acc, AbsPath, {ok, Info}) ->
|
||||||
|
FoldFun(AbsPath, Info, Acc);
|
||||||
|
traverse_dir(FoldFun, Acc, AbsPath, {error, Reason}) ->
|
||||||
|
FoldFun(AbsPath, {error, Reason}, Acc).
|
||||||
|
|
||||||
|
-spec read_info(file:name()) ->
|
||||||
|
{ok, fileinfo()} | {error, file:posix() | badarg}.
|
||||||
|
read_info(AbsPath) ->
|
||||||
|
file:read_link_info(AbsPath, [{time, posix}, raw]).
|
||||||
|
|
||||||
|
%% @doc Canonicalize a file path.
|
||||||
|
%% Removes stray slashes and converts to a string.
|
||||||
|
-spec canonicalize(file:name()) ->
|
||||||
|
string().
|
||||||
|
canonicalize(Filename) ->
|
||||||
|
case filename:split(str(Filename)) of
|
||||||
|
Components = [_ | _] ->
|
||||||
|
filename:join(Components);
|
||||||
|
[] ->
|
||||||
|
""
|
||||||
|
end.
|
||||||
|
|
||||||
|
str(Value) ->
|
||||||
|
case unicode:characters_to_list(Value, unicode) of
|
||||||
|
Str when is_list(Str) ->
|
||||||
|
Str;
|
||||||
|
{error, _, _} ->
|
||||||
|
erlang:error(badarg, [Value])
|
||||||
|
end.
|
|
@ -31,7 +31,6 @@
|
||||||
binary_string/1,
|
binary_string/1,
|
||||||
deep_convert/3,
|
deep_convert/3,
|
||||||
diff_maps/2,
|
diff_maps/2,
|
||||||
merge_with/3,
|
|
||||||
best_effort_recursive_sum/3,
|
best_effort_recursive_sum/3,
|
||||||
if_only_to_toggle_enable/2
|
if_only_to_toggle_enable/2
|
||||||
]).
|
]).
|
||||||
|
@ -231,55 +230,6 @@ convert_keys_to_atom(BinKeyMap, Conv) ->
|
||||||
[]
|
[]
|
||||||
).
|
).
|
||||||
|
|
||||||
%% copy from maps.erl OTP24.0
|
|
||||||
merge_with(Combiner, Map1, Map2) when
|
|
||||||
is_map(Map1),
|
|
||||||
is_map(Map2),
|
|
||||||
is_function(Combiner, 3)
|
|
||||||
->
|
|
||||||
case map_size(Map1) > map_size(Map2) of
|
|
||||||
true ->
|
|
||||||
Iterator = maps:iterator(Map2),
|
|
||||||
merge_with_t(
|
|
||||||
maps:next(Iterator),
|
|
||||||
Map1,
|
|
||||||
Map2,
|
|
||||||
Combiner
|
|
||||||
);
|
|
||||||
false ->
|
|
||||||
Iterator = maps:iterator(Map1),
|
|
||||||
merge_with_t(
|
|
||||||
maps:next(Iterator),
|
|
||||||
Map2,
|
|
||||||
Map1,
|
|
||||||
fun(K, V1, V2) -> Combiner(K, V2, V1) end
|
|
||||||
)
|
|
||||||
end;
|
|
||||||
merge_with(Combiner, Map1, Map2) ->
|
|
||||||
ErrorType = error_type_merge_intersect(Map1, Map2, Combiner),
|
|
||||||
throw(#{maps_merge_error => ErrorType, args => [Map1, Map2]}).
|
|
||||||
|
|
||||||
merge_with_t({K, V2, Iterator}, Map1, Map2, Combiner) ->
|
|
||||||
case Map1 of
|
|
||||||
#{K := V1} ->
|
|
||||||
NewMap1 = Map1#{K := Combiner(K, V1, V2)},
|
|
||||||
merge_with_t(maps:next(Iterator), NewMap1, Map2, Combiner);
|
|
||||||
#{} ->
|
|
||||||
merge_with_t(maps:next(Iterator), maps:put(K, V2, Map1), Map2, Combiner)
|
|
||||||
end;
|
|
||||||
merge_with_t(none, Result, _, _) ->
|
|
||||||
Result.
|
|
||||||
|
|
||||||
error_type_merge_intersect(M1, M2, Combiner) when is_function(Combiner, 3) ->
|
|
||||||
error_type_two_maps(M1, M2);
|
|
||||||
error_type_merge_intersect(_M1, _M2, _Combiner) ->
|
|
||||||
badarg_combiner_function.
|
|
||||||
|
|
||||||
error_type_two_maps(M1, M2) when is_map(M1) ->
|
|
||||||
{badmap, M2};
|
|
||||||
error_type_two_maps(M1, _M2) ->
|
|
||||||
{badmap, M1}.
|
|
||||||
|
|
||||||
%% @doc Sum-merge map values.
|
%% @doc Sum-merge map values.
|
||||||
%% For bad merges, ErrorLogger is called to log the key, and value in M2 is ignored.
|
%% For bad merges, ErrorLogger is called to log the key, and value in M2 is ignored.
|
||||||
best_effort_recursive_sum(M10, M20, ErrorLogger) ->
|
best_effort_recursive_sum(M10, M20, ErrorLogger) ->
|
||||||
|
@ -314,7 +264,7 @@ do_best_effort_recursive_sum(M1, M2, ErrorLogger) ->
|
||||||
V1 + V2
|
V1 + V2
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
merge_with(F, M1, M2).
|
maps:merge_with(F, M1, M2).
|
||||||
|
|
||||||
deep_filter(M, F) when is_map(M) ->
|
deep_filter(M, F) when is_map(M) ->
|
||||||
%% maps:filtermap is not available before OTP 24
|
%% maps:filtermap is not available before OTP 24
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2023 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_utils_fs_SUITE).
|
||||||
|
|
||||||
|
-compile(export_all).
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-include_lib("kernel/include/file.hrl").
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
|
%%
|
||||||
|
|
||||||
|
t_traverse_dir(Config) ->
|
||||||
|
Dir = ?config(data_dir, Config),
|
||||||
|
Traversal = lists:sort(emqx_utils_fs:traverse_dir(fun cons_fileinfo/3, [], Dir)),
|
||||||
|
?assertMatch(
|
||||||
|
[
|
||||||
|
{"nonempty/d1/1", #file_info{type = regular}},
|
||||||
|
{"nonempty/d1/2", #file_info{type = regular}},
|
||||||
|
{"nonempty/d1/mutrec", #file_info{type = symlink, mode = ARWX}},
|
||||||
|
{"nonempty/d2/deep/down/here", #file_info{type = regular, mode = ORW}},
|
||||||
|
{"nonempty/d2/deep/mutrec", #file_info{type = symlink, mode = ARWX}}
|
||||||
|
] when
|
||||||
|
((ORW band 8#00600 =:= 8#00600) and
|
||||||
|
(ARWX band 8#00777 =:= 8#00777)),
|
||||||
|
|
||||||
|
[{string:prefix(Filename, Dir), Info} || {Filename, Info} <- Traversal]
|
||||||
|
).
|
||||||
|
|
||||||
|
t_traverse_symlink(Config) ->
|
||||||
|
Dir = filename:join([?config(data_dir, Config), "nonempty", "d1", "mutrec"]),
|
||||||
|
?assertMatch(
|
||||||
|
[{Dir, #file_info{type = symlink}}],
|
||||||
|
emqx_utils_fs:traverse_dir(fun cons_fileinfo/3, [], Dir)
|
||||||
|
).
|
||||||
|
|
||||||
|
t_traverse_symlink_subdir(Config) ->
|
||||||
|
Dir = filename:join([?config(data_dir, Config), "nonempty", "d2", "deep", "mutrec", "."]),
|
||||||
|
Traversal = lists:sort(emqx_utils_fs:traverse_dir(fun cons_fileinfo/3, [], Dir)),
|
||||||
|
?assertMatch(
|
||||||
|
[
|
||||||
|
{"nonempty/d2/deep/mutrec/1", #file_info{type = regular}},
|
||||||
|
{"nonempty/d2/deep/mutrec/2", #file_info{type = regular}},
|
||||||
|
{"nonempty/d2/deep/mutrec/mutrec", #file_info{type = symlink}}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{string:prefix(Filename, ?config(data_dir, Config)), Info}
|
||||||
|
|| {Filename, Info} <- Traversal
|
||||||
|
]
|
||||||
|
).
|
||||||
|
|
||||||
|
t_traverse_empty(Config) ->
|
||||||
|
Dir = filename:join(?config(data_dir, Config), "empty"),
|
||||||
|
_ = file:make_dir(Dir),
|
||||||
|
?assertEqual(
|
||||||
|
[],
|
||||||
|
emqx_utils_fs:traverse_dir(fun cons_fileinfo/3, [], Dir)
|
||||||
|
).
|
||||||
|
|
||||||
|
t_traverse_nonexisting(_) ->
|
||||||
|
?assertEqual(
|
||||||
|
[{"this/should/not/exist", {error, enoent}}],
|
||||||
|
emqx_utils_fs:traverse_dir(fun cons_fileinfo/3, [], "this/should/not/exist")
|
||||||
|
).
|
||||||
|
|
||||||
|
cons_fileinfo(Filename, Info, Acc) ->
|
||||||
|
[{Filename, Info} | Acc].
|
||||||
|
|
||||||
|
%%
|
||||||
|
|
||||||
|
t_canonicalize_empty(_) ->
|
||||||
|
?assertEqual(
|
||||||
|
"",
|
||||||
|
emqx_utils_fs:canonicalize(<<>>)
|
||||||
|
).
|
||||||
|
|
||||||
|
t_canonicalize_relative(_) ->
|
||||||
|
?assertEqual(
|
||||||
|
"rel",
|
||||||
|
emqx_utils_fs:canonicalize(<<"rel/">>)
|
||||||
|
).
|
||||||
|
|
||||||
|
t_canonicalize_trailing_slash(_) ->
|
||||||
|
?assertEqual(
|
||||||
|
"/usr/local",
|
||||||
|
emqx_utils_fs:canonicalize("/usr/local/")
|
||||||
|
).
|
||||||
|
|
||||||
|
t_canonicalize_double_slashes(_) ->
|
||||||
|
?assertEqual(
|
||||||
|
"/usr/local/.",
|
||||||
|
emqx_utils_fs:canonicalize("//usr//local//.//")
|
||||||
|
).
|
||||||
|
|
||||||
|
t_canonicalize_non_utf8(_) ->
|
||||||
|
?assertError(
|
||||||
|
badarg,
|
||||||
|
emqx_utils_fs:canonicalize(<<128, 128, 128>>)
|
||||||
|
).
|
|
@ -0,0 +1 @@
|
||||||
|
../d2/deep/down
|
|
@ -0,0 +1 @@
|
||||||
|
../../d1
|
Loading…
Reference in New Issue