feat(crl): add refresh config API

This commit is contained in:
Thales Macedo Garitezi 2022-11-11 12:21:03 -03:00
parent b08d1651ad
commit 0ca7492515
2 changed files with 130 additions and 13 deletions

View File

@ -23,6 +23,7 @@
, start_link/1
, refresh/1
, evict/1
, refresh_config/0
]).
%% gen_server callbacks
@ -48,6 +49,7 @@
{ refresh_timers = #{} :: #{binary() => timer:tref()}
, refresh_interval = timer:minutes(15) :: timer:time()
, http_timeout = ?HTTP_TIMEOUT :: timer:time()
, extra = #{} :: map() %% for future use
}).
%%--------------------------------------------------------------------
@ -55,20 +57,11 @@
%%--------------------------------------------------------------------
start_link() ->
Listeners = emqx:get_env(listeners, []),
URLs = collect_urls(Listeners),
RefreshIntervalMS0 = emqx:get_env(crl_cache_refresh_interval,
timer:minutes(15)),
MinimumRefreshInverval = timer:minutes(1),
RefreshIntervalMS = max(RefreshIntervalMS0, MinimumRefreshInverval),
HTTPTimeoutMS = emqx:get_env(crl_cache_http_timeout, ?HTTP_TIMEOUT),
start_link(#{ urls => URLs
, refresh_interval => RefreshIntervalMS
, http_timeout => HTTPTimeoutMS
}).
Config = gather_config(),
start_link(Config).
start_link(Opts = #{urls := _, refresh_interval := _, http_timeout := _}) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, Opts, []).
start_link(Config = #{urls := _, refresh_interval := _, http_timeout := _}) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, Config, []).
refresh(URL) ->
gen_server:cast(?MODULE, {refresh, URL}).
@ -76,6 +69,11 @@ refresh(URL) ->
evict(URL) ->
gen_server:cast(?MODULE, {evict, URL}).
%% to pick up changes from the config
-spec refresh_config() -> ok.
refresh_config() ->
gen_server:cast(?MODULE, refresh_config).
%%--------------------------------------------------------------------
%% gen_server behaviour
%%--------------------------------------------------------------------
@ -116,6 +114,21 @@ handle_cast({refresh, URL}, State0) ->
?LOG(debug, "fetched crl response for ~p", [URL]),
{noreply, ensure_timer(URL, State0)}
end;
handle_cast(refresh_config, State0) ->
#{ urls := URLs
, http_timeout := HTTPTimeoutMS
, refresh_interval := RefreshIntervalMS
} = gather_config(),
State = lists:foldl(fun(URL, Acc) -> ensure_timer(URL, Acc, 0) end,
State0#state{ refresh_interval = RefreshIntervalMS
, http_timeout = HTTPTimeoutMS
},
URLs),
?tp(crl_cache_refresh_config, #{ refresh_interval => RefreshIntervalMS
, http_timeout => HTTPTimeoutMS
, urls => URLs
}),
State;
handle_cast(_Cast, State) ->
{noreply, State}.
@ -186,6 +199,7 @@ ensure_timer(URL, State = #state{refresh_interval = Timeout}) ->
ensure_timer(URL, State, Timeout).
ensure_timer(URL, State = #state{refresh_timers = RefreshTimers0}, Timeout) ->
?tp(crl_cache_ensure_timer, #{url => URL, timeout => Timeout}),
MTimer = maps:get(URL, RefreshTimers0, undefined),
emqx_misc:cancel_timer(MTimer),
RefreshTimers = RefreshTimers0#{URL => emqx_misc:start_timer(
@ -209,3 +223,20 @@ collect_urls(Listeners) ->
end,
CRLOpts1),
lists:usort(CRLURLs).
-spec gather_config() -> #{ urls := [string()]
, refresh_interval := timer:time()
, http_timeout := timer:time()
}.
gather_config() ->
Listeners = emqx:get_env(listeners, []),
URLs = collect_urls(Listeners),
RefreshIntervalMS0 = emqx:get_env(crl_cache_refresh_interval,
timer:minutes(15)),
MinimumRefreshInverval = timer:minutes(1),
RefreshIntervalMS = max(RefreshIntervalMS0, MinimumRefreshInverval),
HTTPTimeoutMS = emqx:get_env(crl_cache_http_timeout, ?HTTP_TIMEOUT),
#{ urls => URLs
, refresh_interval => RefreshIntervalMS
, http_timeout => HTTPTimeoutMS
}.

View File

@ -56,6 +56,29 @@ init_per_testcase(t_not_cached_and_unreachable, Config) ->
[ {crl_pem, CRLPem}
, {crl_der, CRLDer}
| Config];
init_per_testcase(t_refresh_config, Config) ->
DataDir = ?config(data_dir, Config),
CRLFile = filename:join([DataDir, "crl.pem"]),
{ok, CRLPem} = file:read_file(CRLFile),
[{'CertificateList', CRLDer, not_encrypted}] = public_key:pem_decode(CRLPem),
TestPid = self(),
ok = meck:new(emqx_crl_cache, [non_strict, passthrough, no_history, no_link]),
meck:expect(emqx_crl_cache, http_get,
fun(URL, _HTTPTimeout) ->
TestPid ! {http_get, URL},
{ok, {{"HTTP/1.0", 200, 'OK'}, [], CRLPem}}
end),
OldListeners = emqx:get_env(listeners),
OldRefreshInterval = emqx:get_env(crl_cache_refresh_interval),
OldHTTPTimeout = emqx:get_env(crl_cache_http_timeout),
ok = setup_crl_options(Config, #{is_cached => false}),
[ {crl_pem, CRLPem}
, {crl_der, CRLDer}
, {old_configs, [ {listeners, OldListeners}
, {crl_cache_refresh_interval, OldRefreshInterval}
, {crl_cache_http_timeout, OldHTTPTimeout}
]}
| Config];
init_per_testcase(_TestCase, Config) ->
DataDir = ?config(data_dir, Config),
CRLFile = filename:join([DataDir, "crl.pem"]),
@ -86,6 +109,7 @@ end_per_testcase(TestCase, Config)
]),
application:stop(cowboy),
clear_crl_cache(),
ok = snabbkaffe:stop(),
ok;
end_per_testcase(t_not_cached_and_unreachable, _Config) ->
emqx_ct_helpers:stop_apps([]),
@ -95,10 +119,34 @@ end_per_testcase(t_not_cached_and_unreachable, _Config) ->
]}
]),
clear_crl_cache(),
ok = snabbkaffe:stop(),
ok;
end_per_testcase(t_refresh_config, Config) ->
OldConfigs = ?config(old_configs, Config),
meck:unload([emqx_crl_cache]),
emqx_ct_helpers:stop_apps([]),
emqx_ct_helpers:change_emqx_opts(
ssl_twoway, [ {crl_options, [ {crl_check_enabled, false}
, {crl_cache_urls, []}
]}
]),
clear_crl_cache(),
lists:foreach(
fun({Key, MValue}) ->
case MValue of
undefined -> ok;
Value -> application:set_env(emqx, Key, Value)
end
end,
OldConfigs),
application:stop(cowboy),
clear_crl_cache(),
ok = snabbkaffe:stop(),
ok;
end_per_testcase(_TestCase, _Config) ->
meck:unload([emqx_crl_cache]),
clear_crl_cache(),
ok = snabbkaffe:stop(),
ok.
%%--------------------------------------------------------------------
@ -422,6 +470,44 @@ t_filled_cache(Config) ->
emqtt:disconnect(C),
ok.
t_refresh_config(_Config) ->
URLs = [ "http://localhost:9878/some.crl.pem"
, "http://localhost:9878/another.crl.pem"
],
SortedURLs = lists:sort(URLs),
emqx_ct_helpers:change_emqx_opts(
ssl_twoway, [ {crl_options, [ {crl_check_enabled, true}
, {crl_cache_urls, URLs}
]}
]),
%% has to be more than 1 minute
NewRefreshInterval = timer:seconds(64),
NewHTTPTimeout = timer:seconds(7),
application:set_env(emqx, crl_cache_refresh_interval, NewRefreshInterval),
application:set_env(emqx, crl_cache_http_timeout, NewHTTPTimeout),
?check_trace(
?wait_async_action(
emqx_crl_cache:refresh_config(),
#{?snk_kind := crl_cache_refresh_config},
_Timeout = 10_000),
fun(Res, Trace) ->
?assertMatch({ok, {ok, _}}, Res),
?assertMatch(
[#{ urls := SortedURLs
, refresh_interval := NewRefreshInterval
, http_timeout := NewHTTPTimeout
}],
?of_kind(crl_cache_refresh_config, Trace),
#{ expected => #{ urls => SortedURLs
, refresh_interval => NewRefreshInterval
, http_timeout => NewHTTPTimeout
}
}),
?assertEqual(SortedURLs, ?projection(url, ?of_kind(crl_cache_ensure_timer, Trace))),
ok
end),
ok.
%% If the CRL is not cached when the client tries to connect and the
%% CRL server is unreachable, the client will be denied connection.
t_not_cached_and_unreachable(Config) ->