diff --git a/src/emqx_crl_cache.erl b/src/emqx_crl_cache.erl index 79906701c..957890f46 100644 --- a/src/emqx_crl_cache.erl +++ b/src/emqx_crl_cache.erl @@ -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 + }. diff --git a/test/emqx_crl_cache_SUITE.erl b/test/emqx_crl_cache_SUITE.erl index 4c282a376..da5b0a17f 100644 --- a/test/emqx_crl_cache_SUITE.erl +++ b/test/emqx_crl_cache_SUITE.erl @@ -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) ->