Merge pull request #12016 from zmstone/1122-load-license-file
1122 load license file
This commit is contained in:
commit
d933d0b9e0
|
@ -5,12 +5,15 @@
|
||||||
-module(emqx_license_checker).
|
-module(emqx_license_checker).
|
||||||
|
|
||||||
-include("emqx_license.hrl").
|
-include("emqx_license.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
||||||
|
|
||||||
-behaviour(gen_server).
|
-behaviour(gen_server).
|
||||||
|
|
||||||
-define(CHECK_INTERVAL, 5000).
|
-define(CHECK_INTERVAL, timer:seconds(5)).
|
||||||
-define(EXPIRY_ALARM_CHECK_INTERVAL, 24 * 60 * 60).
|
-define(REFRESH_INTERVAL, timer:minutes(2)).
|
||||||
|
-define(EXPIRY_ALARM_CHECK_INTERVAL, timer:hours(24)).
|
||||||
|
|
||||||
-define(OK(EXPR),
|
-define(OK(EXPR),
|
||||||
try
|
try
|
||||||
_ = begin
|
_ = begin
|
||||||
|
@ -56,7 +59,7 @@ start_link(LicenseFetcher) ->
|
||||||
start_link(LicenseFetcher, CheckInterval) ->
|
start_link(LicenseFetcher, CheckInterval) ->
|
||||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [LicenseFetcher, CheckInterval], []).
|
gen_server:start_link({local, ?MODULE}, ?MODULE, [LicenseFetcher, CheckInterval], []).
|
||||||
|
|
||||||
-spec update(emqx_license_parser:license()) -> ok.
|
-spec update(emqx_license_parser:license()) -> map().
|
||||||
update(License) ->
|
update(License) ->
|
||||||
gen_server:call(?MODULE, {update, License}, infinity).
|
gen_server:call(?MODULE, {update, License}, infinity).
|
||||||
|
|
||||||
|
@ -94,15 +97,18 @@ init([LicenseFetcher, CheckInterval]) ->
|
||||||
check_license_interval => CheckInterval,
|
check_license_interval => CheckInterval,
|
||||||
license => License
|
license => License
|
||||||
}),
|
}),
|
||||||
State = ensure_check_expiry_timer(State0),
|
State1 = ensure_refresh_timer(State0),
|
||||||
|
State = ensure_check_expiry_timer(State1),
|
||||||
{ok, State};
|
{ok, State};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{stop, Reason}
|
{stop, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
handle_call({update, License}, _From, State) ->
|
handle_call({update, License}, _From, #{license := Old} = State) ->
|
||||||
ok = expiry_early_alarm(License),
|
ok = expiry_early_alarm(License),
|
||||||
{reply, check_license(License), State#{license => License}};
|
State1 = ensure_refresh_timer(State),
|
||||||
|
ok = log_new_license(Old, License),
|
||||||
|
{reply, check_license(License), State1#{license => License}};
|
||||||
handle_call(dump, _From, #{license := License} = State) ->
|
handle_call(dump, _From, #{license := License} = State) ->
|
||||||
{reply, emqx_license_parser:dump(License), State};
|
{reply, emqx_license_parser:dump(License), State};
|
||||||
handle_call(purge, _From, State) ->
|
handle_call(purge, _From, State) ->
|
||||||
|
@ -123,6 +129,10 @@ handle_info(check_expiry_alarm, #{license := License} = State) ->
|
||||||
ok = expiry_early_alarm(License),
|
ok = expiry_early_alarm(License),
|
||||||
NewState = ensure_check_expiry_timer(State),
|
NewState = ensure_check_expiry_timer(State),
|
||||||
{noreply, NewState};
|
{noreply, NewState};
|
||||||
|
handle_info(refresh, State0) ->
|
||||||
|
State1 = refresh(State0),
|
||||||
|
NewState = ensure_refresh_timer(State1),
|
||||||
|
{noreply, NewState};
|
||||||
handle_info(_Msg, State) ->
|
handle_info(_Msg, State) ->
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
|
@ -130,22 +140,59 @@ handle_info(_Msg, State) ->
|
||||||
%% Private functions
|
%% Private functions
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
refresh(#{license := #{source := <<"file://", _/binary>> = Source} = License} = State) ->
|
||||||
|
case emqx_license_parser:parse(Source) of
|
||||||
|
{ok, License} ->
|
||||||
|
?tp(emqx_license_refresh_no_change, #{}),
|
||||||
|
%% no change
|
||||||
|
State;
|
||||||
|
{ok, NewLicense} ->
|
||||||
|
ok = log_new_license(License, NewLicense),
|
||||||
|
%% ensure alarm is set or cleared
|
||||||
|
ok = expiry_early_alarm(NewLicense),
|
||||||
|
?tp(emqx_license_refresh_changed, #{new_license => NewLicense}),
|
||||||
|
State#{license => NewLicense};
|
||||||
|
{error, Reason} ->
|
||||||
|
?tp(
|
||||||
|
error,
|
||||||
|
emqx_license_refresh_failed,
|
||||||
|
Reason#{continue_with_license => emqx_license_parser:summary(License)}
|
||||||
|
),
|
||||||
|
State
|
||||||
|
end;
|
||||||
|
refresh(State) ->
|
||||||
|
State.
|
||||||
|
|
||||||
|
log_new_license(Old, New) ->
|
||||||
|
?SLOG(info, #{
|
||||||
|
msg => "new_license_loaded",
|
||||||
|
old_license => emqx_license_parser:summary(Old),
|
||||||
|
new_license => emqx_license_parser:summary(New)
|
||||||
|
}).
|
||||||
|
|
||||||
ensure_check_license_timer(#{check_license_interval := CheckInterval} = State) ->
|
ensure_check_license_timer(#{check_license_interval := CheckInterval} = State) ->
|
||||||
cancel_timer(State, timer),
|
ok = cancel_timer(State, check_timer),
|
||||||
State#{timer => erlang:send_after(CheckInterval, self(), check_license)}.
|
State#{check_timer => erlang:send_after(CheckInterval, self(), check_license)}.
|
||||||
|
|
||||||
ensure_check_expiry_timer(State) ->
|
ensure_check_expiry_timer(State) ->
|
||||||
cancel_timer(State, expiry_alarm_timer),
|
ok = cancel_timer(State, expiry_alarm_timer),
|
||||||
Ref = erlang:send_after(?EXPIRY_ALARM_CHECK_INTERVAL, self(), check_expiry_alarm),
|
Ref = erlang:send_after(?EXPIRY_ALARM_CHECK_INTERVAL, self(), check_expiry_alarm),
|
||||||
State#{expiry_alarm_timer => Ref}.
|
State#{expiry_alarm_timer => Ref}.
|
||||||
|
|
||||||
|
%% refresh is to work with file:// license keys.
|
||||||
|
ensure_refresh_timer(State) ->
|
||||||
|
ok = cancel_timer(State, refresh_timer),
|
||||||
|
Ref = erlang:send_after(?REFRESH_INTERVAL, self(), refresh),
|
||||||
|
State#{refresh_timer => Ref}.
|
||||||
|
|
||||||
cancel_timer(State, Key) ->
|
cancel_timer(State, Key) ->
|
||||||
_ =
|
case maps:find(Key, State) of
|
||||||
case maps:find(Key, State) of
|
{ok, Ref} when is_reference(Ref) ->
|
||||||
{ok, Ref} when is_reference(Ref) -> erlang:cancel_timer(Ref);
|
_ = erlang:cancel_timer(Ref),
|
||||||
_ -> ok
|
ok;
|
||||||
end,
|
_ ->
|
||||||
ok.
|
ok
|
||||||
|
end.
|
||||||
|
|
||||||
check_license(License) ->
|
check_license(License) ->
|
||||||
DaysLeft = days_left(License),
|
DaysLeft = days_left(License),
|
||||||
|
|
|
@ -54,7 +54,6 @@ schema("/license") ->
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
%% TODO(5.x): It's a update action, should use PUT instead
|
|
||||||
post => #{
|
post => #{
|
||||||
tags => ?LICENSE_TAGS,
|
tags => ?LICENSE_TAGS,
|
||||||
summary => <<"Update license key">>,
|
summary => <<"Update license key">>,
|
||||||
|
|
|
@ -32,7 +32,14 @@
|
||||||
|
|
||||||
-type license_type() :: ?OFFICIAL | ?TRIAL.
|
-type license_type() :: ?OFFICIAL | ?TRIAL.
|
||||||
|
|
||||||
-type license() :: #{module := module(), data := license_data()}.
|
-type license() :: #{
|
||||||
|
%% the parser module which parsed the license
|
||||||
|
module := module(),
|
||||||
|
%% the parse result
|
||||||
|
data := license_data(),
|
||||||
|
%% the source of the license, e.g. "file://path/to/license/file" or "******" for license key
|
||||||
|
source := binary()
|
||||||
|
}.
|
||||||
|
|
||||||
-export_type([
|
-export_type([
|
||||||
license_data/0,
|
license_data/0,
|
||||||
|
@ -45,12 +52,19 @@
|
||||||
parse/1,
|
parse/1,
|
||||||
parse/2,
|
parse/2,
|
||||||
dump/1,
|
dump/1,
|
||||||
|
summary/1,
|
||||||
customer_type/1,
|
customer_type/1,
|
||||||
license_type/1,
|
license_type/1,
|
||||||
expiry_date/1,
|
expiry_date/1,
|
||||||
max_connections/1
|
max_connections/1
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
%% for testing purpose
|
||||||
|
-export([
|
||||||
|
default/0,
|
||||||
|
pubkey/0
|
||||||
|
]).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Behaviour
|
%% Behaviour
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
@ -59,6 +73,9 @@
|
||||||
|
|
||||||
-callback dump(license_data()) -> list({atom(), term()}).
|
-callback dump(license_data()) -> list({atom(), term()}).
|
||||||
|
|
||||||
|
%% provide a summary map for logging purposes
|
||||||
|
-callback summary(license_data()) -> map().
|
||||||
|
|
||||||
-callback customer_type(license_data()) -> customer_type().
|
-callback customer_type(license_data()) -> customer_type().
|
||||||
|
|
||||||
-callback license_type(license_data()) -> license_type().
|
-callback license_type(license_data()) -> license_type().
|
||||||
|
@ -71,19 +88,37 @@
|
||||||
%% API
|
%% API
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
-ifdef(TEST).
|
pubkey() -> ?PUBKEY.
|
||||||
-spec parse(string() | binary()) -> {ok, license()} | {error, term()}.
|
default() -> emqx_license_schema:default_license().
|
||||||
parse(Content) ->
|
|
||||||
PubKey = persistent_term:get(emqx_license_test_pubkey, ?PUBKEY),
|
|
||||||
parse(Content, PubKey).
|
|
||||||
-else.
|
|
||||||
-spec parse(string() | binary()) -> {ok, license()} | {error, term()}.
|
|
||||||
parse(Content) ->
|
|
||||||
parse(Content, ?PUBKEY).
|
|
||||||
-endif.
|
|
||||||
|
|
||||||
parse(Content, Pem) ->
|
%% @doc Parse license key.
|
||||||
[PemEntry] = public_key:pem_decode(Pem),
|
%% If the license key is prefixed with "file://path/to/license/file",
|
||||||
|
%% then the license key is read from the file.
|
||||||
|
-spec parse(default | string() | binary()) -> {ok, license()} | {error, map()}.
|
||||||
|
parse(Content) ->
|
||||||
|
parse(to_bin(Content), ?MODULE:pubkey()).
|
||||||
|
|
||||||
|
parse(<<"default">>, PubKey) ->
|
||||||
|
parse(?MODULE:default(), PubKey);
|
||||||
|
parse(<<"file://", Path/binary>> = FileKey, PubKey) ->
|
||||||
|
case file:read_file(Path) of
|
||||||
|
{ok, Content} ->
|
||||||
|
case parse(Content, PubKey) of
|
||||||
|
{ok, License} ->
|
||||||
|
{ok, License#{source => FileKey}};
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason#{
|
||||||
|
license_file => Path
|
||||||
|
}}
|
||||||
|
end;
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, #{
|
||||||
|
license_file => Path,
|
||||||
|
read_error => Reason
|
||||||
|
}}
|
||||||
|
end;
|
||||||
|
parse(Content, PubKey) ->
|
||||||
|
[PemEntry] = public_key:pem_decode(PubKey),
|
||||||
Key = public_key:pem_entry_decode(PemEntry),
|
Key = public_key:pem_entry_decode(PemEntry),
|
||||||
do_parse(iolist_to_binary(Content), Key, ?LICENSE_PARSE_MODULES, []).
|
do_parse(iolist_to_binary(Content), Key, ?LICENSE_PARSE_MODULES, []).
|
||||||
|
|
||||||
|
@ -91,6 +126,10 @@ parse(Content, Pem) ->
|
||||||
dump(#{module := Module, data := LicenseData}) ->
|
dump(#{module := Module, data := LicenseData}) ->
|
||||||
Module:dump(LicenseData).
|
Module:dump(LicenseData).
|
||||||
|
|
||||||
|
-spec summary(license()) -> map().
|
||||||
|
summary(#{module := Module, data := Data}) ->
|
||||||
|
Module:summary(Data).
|
||||||
|
|
||||||
-spec customer_type(license()) -> customer_type().
|
-spec customer_type(license()) -> customer_type().
|
||||||
customer_type(#{module := Module, data := LicenseData}) ->
|
customer_type(#{module := Module, data := LicenseData}) ->
|
||||||
Module:customer_type(LicenseData).
|
Module:customer_type(LicenseData).
|
||||||
|
@ -112,14 +151,21 @@ max_connections(#{module := Module, data := LicenseData}) ->
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
do_parse(_Content, _Key, [], Errors) ->
|
do_parse(_Content, _Key, [], Errors) ->
|
||||||
{error, lists:reverse(Errors)};
|
{error, #{parse_results => lists:reverse(Errors)}};
|
||||||
do_parse(Content, Key, [Module | Modules], Errors) ->
|
do_parse(Content, Key, [Module | Modules], Errors) ->
|
||||||
try Module:parse(Content, Key) of
|
try Module:parse(Content, Key) of
|
||||||
{ok, LicenseData} ->
|
{ok, LicenseData} ->
|
||||||
{ok, #{module => Module, data => LicenseData}};
|
{ok, #{module => Module, data => LicenseData, source => <<"******">>}};
|
||||||
{error, Error} ->
|
{error, Error} ->
|
||||||
do_parse(Content, Key, Modules, [{Module, Error} | Errors])
|
do_parse(Content, Key, Modules, [#{module => Module, error => Error} | Errors])
|
||||||
catch
|
catch
|
||||||
_Class:Error:Stacktrace ->
|
_Class:Error:Stacktrace ->
|
||||||
do_parse(Content, Key, Modules, [{Module, {Error, Stacktrace}} | Errors])
|
do_parse(Content, Key, Modules, [
|
||||||
|
#{module => Module, error => Error, stacktrace => Stacktrace} | Errors
|
||||||
|
])
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
to_bin(A) when is_atom(A) ->
|
||||||
|
atom_to_binary(A);
|
||||||
|
to_bin(L) ->
|
||||||
|
iolist_to_binary(L).
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
-export([
|
-export([
|
||||||
parse/2,
|
parse/2,
|
||||||
dump/1,
|
dump/1,
|
||||||
|
summary/1,
|
||||||
customer_type/1,
|
customer_type/1,
|
||||||
license_type/1,
|
license_type/1,
|
||||||
expiry_date/1,
|
expiry_date/1,
|
||||||
|
@ -69,6 +70,21 @@ dump(
|
||||||
{expiry, Expiry}
|
{expiry, Expiry}
|
||||||
].
|
].
|
||||||
|
|
||||||
|
summary(
|
||||||
|
#{
|
||||||
|
deployment := Deployment,
|
||||||
|
date_start := DateStart,
|
||||||
|
max_connections := MaxConns
|
||||||
|
} = License
|
||||||
|
) ->
|
||||||
|
DateExpiry = expiry_date(License),
|
||||||
|
#{
|
||||||
|
deployment => Deployment,
|
||||||
|
max_connections => MaxConns,
|
||||||
|
start_at => format_date(DateStart),
|
||||||
|
expiry_at => format_date(DateExpiry)
|
||||||
|
}.
|
||||||
|
|
||||||
customer_type(#{customer_type := CType}) -> CType.
|
customer_type(#{customer_type := CType}) -> CType.
|
||||||
|
|
||||||
license_type(#{type := Type}) -> Type.
|
license_type(#{type := Type}) -> Type.
|
||||||
|
@ -85,17 +101,27 @@ max_connections(#{max_connections := MaxConns}) ->
|
||||||
%% Private functions
|
%% Private functions
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
do_parse(Content) ->
|
do_parse(Content0) ->
|
||||||
try
|
try
|
||||||
[EncodedPayload, EncodedSignature] = binary:split(Content, <<".">>),
|
Content = normalize(Content0),
|
||||||
Payload = base64:decode(EncodedPayload),
|
do_parse2(Content)
|
||||||
Signature = base64:decode(EncodedSignature),
|
|
||||||
{ok, {Payload, Signature}}
|
|
||||||
catch
|
catch
|
||||||
_:_ ->
|
_:_ ->
|
||||||
{error, bad_license_format}
|
{error, bad_license_format}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
do_parse2(<<>>) ->
|
||||||
|
{error, empty_string};
|
||||||
|
do_parse2(Content) ->
|
||||||
|
[EncodedPayload, EncodedSignature] = binary:split(Content, <<".">>),
|
||||||
|
Payload = base64:decode(EncodedPayload),
|
||||||
|
Signature = base64:decode(EncodedSignature),
|
||||||
|
{ok, {Payload, Signature}}.
|
||||||
|
|
||||||
|
%% drop whitespaces and newlines (CRLF)
|
||||||
|
normalize(Bin) ->
|
||||||
|
<<<<C>> || <<C>> <= Bin, C =/= $\s andalso C =/= $\n andalso C =/= $\r>>.
|
||||||
|
|
||||||
verify_signature(Payload, Signature, Key) ->
|
verify_signature(Payload, Signature, Key) ->
|
||||||
public_key:verify(Payload, ?DIGEST_TYPE, Signature, Key).
|
public_key:verify(Payload, ?DIGEST_TYPE, Signature, Key).
|
||||||
|
|
||||||
|
@ -182,7 +208,7 @@ collect_fields(Fields) ->
|
||||||
{FieldValues, []} ->
|
{FieldValues, []} ->
|
||||||
{ok, maps:from_list(FieldValues)};
|
{ok, maps:from_list(FieldValues)};
|
||||||
{_, Errors} ->
|
{_, Errors} ->
|
||||||
{error, lists:reverse(Errors)}
|
{error, maps:from_list(Errors)}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
format_date({Year, Month, Day}) ->
|
format_date({Year, Month, Day}) ->
|
||||||
|
|
|
@ -38,8 +38,8 @@ tags() ->
|
||||||
fields(key_license) ->
|
fields(key_license) ->
|
||||||
[
|
[
|
||||||
{key, #{
|
{key, #{
|
||||||
type => binary(),
|
type => hoconsc:union([default, binary()]),
|
||||||
default => default_license(),
|
default => <<"default">>,
|
||||||
%% so it's not logged
|
%% so it's not logged
|
||||||
sensitive => true,
|
sensitive => true,
|
||||||
required => true,
|
required => true,
|
||||||
|
|
|
@ -16,12 +16,14 @@ all() ->
|
||||||
emqx_common_test_helpers:all(?MODULE).
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
|
emqx_license_test_lib:mock_parser(),
|
||||||
_ = application:load(emqx_conf),
|
_ = application:load(emqx_conf),
|
||||||
emqx_config:save_schema_mod_and_names(emqx_license_schema),
|
emqx_config:save_schema_mod_and_names(emqx_license_schema),
|
||||||
emqx_common_test_helpers:start_apps([emqx_license], fun set_special_configs/1),
|
emqx_common_test_helpers:start_apps([emqx_license], fun set_special_configs/1),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
end_per_suite(_) ->
|
end_per_suite(_) ->
|
||||||
|
emqx_license_test_lib:unmock_parser(),
|
||||||
emqx_common_test_helpers:stop_apps([emqx_license]),
|
emqx_common_test_helpers:stop_apps([emqx_license]),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
@ -103,17 +105,7 @@ setup_test(TestCase, Config) when
|
||||||
),
|
),
|
||||||
ok;
|
ok;
|
||||||
(emqx_license) ->
|
(emqx_license) ->
|
||||||
LicensePath = filename:join(emqx_license:license_dir(), "emqx.lic"),
|
set_special_configs(emqx_license),
|
||||||
filelib:ensure_dir(LicensePath),
|
|
||||||
ok = file:write_file(LicensePath, LicenseKey),
|
|
||||||
LicConfig = #{type => file, file => LicensePath},
|
|
||||||
emqx_config:put([license], LicConfig),
|
|
||||||
RawConfig = #{<<"type">> => file, <<"file">> => LicensePath},
|
|
||||||
emqx_config:put_raw([<<"license">>], RawConfig),
|
|
||||||
ok = persistent_term:put(
|
|
||||||
emqx_license_test_pubkey,
|
|
||||||
emqx_license_test_lib:public_key_pem()
|
|
||||||
),
|
|
||||||
ok;
|
ok;
|
||||||
(_) ->
|
(_) ->
|
||||||
ok
|
ok
|
||||||
|
@ -129,9 +121,9 @@ teardown_test(_TestCase, _Config) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
set_special_configs(emqx_license) ->
|
set_special_configs(emqx_license) ->
|
||||||
Config = #{key => emqx_license_test_lib:default_license()},
|
Config = #{key => default},
|
||||||
emqx_config:put([license], Config),
|
emqx_config:put([license], Config),
|
||||||
RawConfig = #{<<"key">> => emqx_license_test_lib:default_license()},
|
RawConfig = #{<<"key">> => <<"default">>},
|
||||||
emqx_config:put_raw([<<"license">>], RawConfig);
|
emqx_config:put_raw([<<"license">>], RawConfig);
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
@ -146,11 +138,11 @@ assert_on_nodes(Nodes, RunFun, CheckFun) ->
|
||||||
|
|
||||||
t_update_value(_Config) ->
|
t_update_value(_Config) ->
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{error, [_ | _]},
|
{error, #{parse_results := [_ | _]}},
|
||||||
emqx_license:update_key("invalid.license")
|
emqx_license:update_key("invalid.license")
|
||||||
),
|
),
|
||||||
|
|
||||||
LicenseValue = emqx_license_test_lib:default_license(),
|
LicenseValue = emqx_license_test_lib:default_test_license(),
|
||||||
|
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{ok, #{}},
|
{ok, #{}},
|
||||||
|
|
|
@ -14,12 +14,14 @@
|
||||||
all() ->
|
all() ->
|
||||||
emqx_common_test_helpers:all(?MODULE).
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
init_per_suite(CtConfig) ->
|
||||||
_ = application:load(emqx_conf),
|
_ = application:load(emqx_conf),
|
||||||
|
emqx_license_test_lib:mock_parser(),
|
||||||
ok = emqx_common_test_helpers:start_apps([emqx_license], fun set_special_configs/1),
|
ok = emqx_common_test_helpers:start_apps([emqx_license], fun set_special_configs/1),
|
||||||
Config.
|
CtConfig.
|
||||||
|
|
||||||
end_per_suite(_) ->
|
end_per_suite(_) ->
|
||||||
|
emqx_license_test_lib:unmock_parser(),
|
||||||
ok = emqx_common_test_helpers:stop_apps([emqx_license]).
|
ok = emqx_common_test_helpers:stop_apps([emqx_license]).
|
||||||
|
|
||||||
init_per_testcase(t_default_limits, Config) ->
|
init_per_testcase(t_default_limits, Config) ->
|
||||||
|
@ -35,7 +37,7 @@ end_per_testcase(_Case, _Config) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
set_special_configs(emqx_license) ->
|
set_special_configs(emqx_license) ->
|
||||||
Config = #{key => emqx_license_test_lib:default_license()},
|
Config = #{key => emqx_license_test_lib:default_test_license()},
|
||||||
emqx_config:put([license], Config);
|
emqx_config:put([license], Config);
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
@ -100,7 +102,7 @@ t_update(_Config) ->
|
||||||
emqx_license_checker:limits()
|
emqx_license_checker:limits()
|
||||||
).
|
).
|
||||||
|
|
||||||
t_update_by_timer(_Config) ->
|
t_check_by_timer(_Config) ->
|
||||||
?check_trace(
|
?check_trace(
|
||||||
begin
|
begin
|
||||||
?wait_async_action(
|
?wait_async_action(
|
||||||
|
@ -228,10 +230,111 @@ t_unknown_calls(_Config) ->
|
||||||
some_msg = erlang:send(emqx_license_checker, some_msg),
|
some_msg = erlang:send(emqx_license_checker, some_msg),
|
||||||
?assertEqual(unknown, gen_server:call(emqx_license_checker, some_request)).
|
?assertEqual(unknown, gen_server:call(emqx_license_checker, some_request)).
|
||||||
|
|
||||||
|
t_refresh_no_change(Config) when is_list(Config) ->
|
||||||
|
{ok, License} = write_test_license(Config, ?FUNCTION_NAME, 1, 111),
|
||||||
|
#{} = emqx_license_checker:update(License),
|
||||||
|
?check_trace(
|
||||||
|
begin
|
||||||
|
?wait_async_action(
|
||||||
|
begin
|
||||||
|
erlang:send(
|
||||||
|
emqx_license_checker,
|
||||||
|
refresh
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
#{?snk_kind := emqx_license_refresh_no_change},
|
||||||
|
1000
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
fun(Trace) ->
|
||||||
|
?assertMatch([_ | _], ?of_kind(emqx_license_refresh_no_change, Trace))
|
||||||
|
end
|
||||||
|
).
|
||||||
|
|
||||||
|
t_refresh_change(Config) when is_list(Config) ->
|
||||||
|
{ok, License} = write_test_license(Config, ?FUNCTION_NAME, 1, 111),
|
||||||
|
#{} = emqx_license_checker:update(License),
|
||||||
|
{ok, License2} = write_test_license(Config, ?FUNCTION_NAME, 2, 222),
|
||||||
|
?check_trace(
|
||||||
|
begin
|
||||||
|
?wait_async_action(
|
||||||
|
begin
|
||||||
|
erlang:send(
|
||||||
|
emqx_license_checker,
|
||||||
|
refresh
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
#{?snk_kind := emqx_license_refresh_changed},
|
||||||
|
1000
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
fun(Trace) ->
|
||||||
|
?assertMatch(
|
||||||
|
[#{new_license := License2} | _], ?of_kind(emqx_license_refresh_changed, Trace)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
).
|
||||||
|
|
||||||
|
t_refresh_failure(Config) when is_list(Config) ->
|
||||||
|
Filename = test_license_file_name(Config, ?FUNCTION_NAME),
|
||||||
|
{ok, License} = write_test_license(Config, ?FUNCTION_NAME, 1, 111),
|
||||||
|
Summary = emqx_license_parser:summary(License),
|
||||||
|
#{} = emqx_license_checker:update(License),
|
||||||
|
ok = file:write_file(Filename, <<"invalid license">>),
|
||||||
|
?check_trace(
|
||||||
|
begin
|
||||||
|
?wait_async_action(
|
||||||
|
begin
|
||||||
|
erlang:send(
|
||||||
|
emqx_license_checker,
|
||||||
|
refresh
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
#{?snk_kind := emqx_license_refresh_failed},
|
||||||
|
1000
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
fun(Trace) ->
|
||||||
|
?assertMatch(
|
||||||
|
[#{continue_with_license := Summary} | _],
|
||||||
|
?of_kind(emqx_license_refresh_failed, Trace)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Tests
|
%% Tests
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
write_test_license(Config, Name, ExpireInDays, Connections) ->
|
||||||
|
{NowDate, _} = calendar:universal_time(),
|
||||||
|
DateTomorrow = calendar:gregorian_days_to_date(
|
||||||
|
calendar:date_to_gregorian_days(NowDate) + ExpireInDays
|
||||||
|
),
|
||||||
|
Fields = [
|
||||||
|
"220111",
|
||||||
|
"1",
|
||||||
|
"0",
|
||||||
|
"Foo",
|
||||||
|
"contact@foo.com",
|
||||||
|
"bar",
|
||||||
|
format_date(DateTomorrow),
|
||||||
|
"1",
|
||||||
|
integer_to_list(Connections)
|
||||||
|
],
|
||||||
|
FileName = test_license_file_name(Config, Name),
|
||||||
|
ok = write_license_file(FileName, Fields),
|
||||||
|
emqx_license_parser:parse(<<"file://", FileName/binary>>).
|
||||||
|
|
||||||
|
test_license_file_name(Config, Name) ->
|
||||||
|
Dir = ?config(data_dir, Config),
|
||||||
|
iolist_to_binary(filename:join(Dir, atom_to_list(Name) ++ ".lic")).
|
||||||
|
|
||||||
|
write_license_file(FileName, Fields) ->
|
||||||
|
EncodedLicense = emqx_license_test_lib:make_license(Fields),
|
||||||
|
ok = filelib:ensure_dir(FileName),
|
||||||
|
ok = file:write_file(FileName, EncodedLicense).
|
||||||
|
|
||||||
mk_license(Fields) ->
|
mk_license(Fields) ->
|
||||||
EncodedLicense = emqx_license_test_lib:make_license(Fields),
|
EncodedLicense = emqx_license_test_lib:make_license(Fields),
|
||||||
{ok, License} = emqx_license_parser:parse(
|
{ok, License} = emqx_license_parser:parse(
|
||||||
|
|
|
@ -24,15 +24,12 @@ end_per_suite(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
init_per_testcase(_Case, Config) ->
|
init_per_testcase(_Case, Config) ->
|
||||||
ok = persistent_term:put(
|
emqx_license_test_lib:mock_parser(),
|
||||||
emqx_license_test_pubkey,
|
|
||||||
emqx_license_test_lib:public_key_pem()
|
|
||||||
),
|
|
||||||
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
end_per_testcase(_Case, _Config) ->
|
end_per_testcase(_Case, _Config) ->
|
||||||
persistent_term:erase(emqx_license_test_pubkey),
|
emqx_license_test_lib:unmock_parser(),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
set_special_configs(emqx_license) ->
|
set_special_configs(emqx_license) ->
|
||||||
|
|
|
@ -19,6 +19,7 @@ all() ->
|
||||||
emqx_common_test_helpers:all(?MODULE).
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
|
emqx_license_test_lib:mock_parser(),
|
||||||
_ = application:load(emqx_conf),
|
_ = application:load(emqx_conf),
|
||||||
emqx_config:save_schema_mod_and_names(emqx_license_schema),
|
emqx_config:save_schema_mod_and_names(emqx_license_schema),
|
||||||
emqx_common_test_helpers:start_apps([emqx_license, emqx_dashboard], fun set_special_configs/1),
|
emqx_common_test_helpers:start_apps([emqx_license, emqx_dashboard], fun set_special_configs/1),
|
||||||
|
@ -31,7 +32,7 @@ end_per_suite(_) ->
|
||||||
emqx_config:put([license], Config),
|
emqx_config:put([license], Config),
|
||||||
RawConfig = #{<<"key">> => LicenseKey},
|
RawConfig = #{<<"key">> => LicenseKey},
|
||||||
emqx_config:put_raw([<<"license">>], RawConfig),
|
emqx_config:put_raw([<<"license">>], RawConfig),
|
||||||
persistent_term:erase(emqx_license_test_pubkey),
|
emqx_license_test_lib:unmock_parser(),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
set_special_configs(emqx_dashboard) ->
|
set_special_configs(emqx_dashboard) ->
|
||||||
|
@ -48,10 +49,6 @@ set_special_configs(emqx_license) ->
|
||||||
<<"connection_high_watermark">> => <<"80%">>
|
<<"connection_high_watermark">> => <<"80%">>
|
||||||
},
|
},
|
||||||
emqx_config:put_raw([<<"license">>], RawConfig),
|
emqx_config:put_raw([<<"license">>], RawConfig),
|
||||||
ok = persistent_term:put(
|
|
||||||
emqx_license_test_pubkey,
|
|
||||||
emqx_license_test_lib:public_key_pem()
|
|
||||||
),
|
|
||||||
ok;
|
ok;
|
||||||
set_special_configs(_) ->
|
set_special_configs(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
@ -113,6 +110,19 @@ t_license_info(_Config) ->
|
||||||
),
|
),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
t_set_default_license(_Config) ->
|
||||||
|
NewKey = <<"default">>,
|
||||||
|
Res = request(
|
||||||
|
post,
|
||||||
|
uri(["license"]),
|
||||||
|
#{key => NewKey}
|
||||||
|
),
|
||||||
|
?assertMatch({ok, 200, _}, Res),
|
||||||
|
{ok, 200, Payload} = Res,
|
||||||
|
%% assert that it's not the string "default" returned
|
||||||
|
?assertMatch(#{<<"customer">> := _}, emqx_utils_json:decode(Payload, [return_maps])),
|
||||||
|
ok.
|
||||||
|
|
||||||
t_license_upload_key_success(_Config) ->
|
t_license_upload_key_success(_Config) ->
|
||||||
NewKey = emqx_license_test_lib:make_license(#{max_connections => "999"}),
|
NewKey = emqx_license_test_lib:make_license(#{max_connections => "999"}),
|
||||||
Res = request(
|
Res = request(
|
||||||
|
|
|
@ -40,6 +40,7 @@ set_special_configs(_) ->
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
t_parse(_Config) ->
|
t_parse(_Config) ->
|
||||||
|
Parser = emqx_license_parser_v20220101,
|
||||||
?assertMatch({ok, _}, emqx_license_parser:parse(sample_license(), public_key_pem())),
|
?assertMatch({ok, _}, emqx_license_parser:parse(sample_license(), public_key_pem())),
|
||||||
|
|
||||||
%% invalid version
|
%% invalid version
|
||||||
|
@ -61,10 +62,7 @@ t_parse(_Config) ->
|
||||||
),
|
),
|
||||||
?assertMatch({error, _}, Res1),
|
?assertMatch({error, _}, Res1),
|
||||||
{error, Err1} = Res1,
|
{error, Err1} = Res1,
|
||||||
?assertEqual(
|
?assertMatch(#{error := invalid_version}, find_error(Parser, Err1)),
|
||||||
invalid_version,
|
|
||||||
proplists:get_value(emqx_license_parser_v20220101, Err1)
|
|
||||||
),
|
|
||||||
|
|
||||||
%% invalid field number
|
%% invalid field number
|
||||||
Res2 = emqx_license_parser:parse(
|
Res2 = emqx_license_parser:parse(
|
||||||
|
@ -87,9 +85,9 @@ t_parse(_Config) ->
|
||||||
),
|
),
|
||||||
?assertMatch({error, _}, Res2),
|
?assertMatch({error, _}, Res2),
|
||||||
{error, Err2} = Res2,
|
{error, Err2} = Res2,
|
||||||
?assertEqual(
|
?assertMatch(
|
||||||
unexpected_number_of_fields,
|
#{error := unexpected_number_of_fields},
|
||||||
proplists:get_value(emqx_license_parser_v20220101, Err2)
|
find_error(Parser, Err2)
|
||||||
),
|
),
|
||||||
|
|
||||||
Res3 = emqx_license_parser:parse(
|
Res3 = emqx_license_parser:parse(
|
||||||
|
@ -110,14 +108,17 @@ t_parse(_Config) ->
|
||||||
),
|
),
|
||||||
?assertMatch({error, _}, Res3),
|
?assertMatch({error, _}, Res3),
|
||||||
{error, Err3} = Res3,
|
{error, Err3} = Res3,
|
||||||
?assertEqual(
|
?assertMatch(
|
||||||
[
|
#{
|
||||||
{type, invalid_license_type},
|
error :=
|
||||||
{customer_type, invalid_customer_type},
|
#{
|
||||||
{date_start, invalid_date},
|
type := invalid_license_type,
|
||||||
{days, invalid_int_value}
|
customer_type := invalid_customer_type,
|
||||||
],
|
date_start := invalid_date,
|
||||||
proplists:get_value(emqx_license_parser_v20220101, Err3)
|
days := invalid_int_value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
find_error(Parser, Err3)
|
||||||
),
|
),
|
||||||
|
|
||||||
Res4 = emqx_license_parser:parse(
|
Res4 = emqx_license_parser:parse(
|
||||||
|
@ -139,14 +140,17 @@ t_parse(_Config) ->
|
||||||
?assertMatch({error, _}, Res4),
|
?assertMatch({error, _}, Res4),
|
||||||
{error, Err4} = Res4,
|
{error, Err4} = Res4,
|
||||||
|
|
||||||
?assertEqual(
|
?assertMatch(
|
||||||
[
|
#{
|
||||||
{type, invalid_license_type},
|
error :=
|
||||||
{customer_type, invalid_customer_type},
|
#{
|
||||||
{date_start, invalid_date},
|
type := invalid_license_type,
|
||||||
{days, invalid_int_value}
|
customer_type := invalid_customer_type,
|
||||||
],
|
date_start := invalid_date,
|
||||||
proplists:get_value(emqx_license_parser_v20220101, Err4)
|
days := invalid_int_value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
find_error(Parser, Err4)
|
||||||
),
|
),
|
||||||
|
|
||||||
%% invalid signature
|
%% invalid signature
|
||||||
|
@ -189,14 +193,14 @@ t_parse(_Config) ->
|
||||||
),
|
),
|
||||||
?assertMatch({error, _}, Res5),
|
?assertMatch({error, _}, Res5),
|
||||||
{error, Err5} = Res5,
|
{error, Err5} = Res5,
|
||||||
?assertEqual(
|
?assertMatch(
|
||||||
invalid_signature,
|
#{error := invalid_signature},
|
||||||
proplists:get_value(emqx_license_parser_v20220101, Err5)
|
find_error(Parser, Err5)
|
||||||
),
|
),
|
||||||
|
|
||||||
%% totally invalid strings as license
|
%% totally invalid strings as license
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{error, [_ | _]},
|
{error, #{parse_results := [#{error := bad_license_format}]}},
|
||||||
emqx_license_parser:parse(
|
emqx_license_parser:parse(
|
||||||
<<"badlicense">>,
|
<<"badlicense">>,
|
||||||
public_key_pem()
|
public_key_pem()
|
||||||
|
@ -204,7 +208,7 @@ t_parse(_Config) ->
|
||||||
),
|
),
|
||||||
|
|
||||||
?assertMatch(
|
?assertMatch(
|
||||||
{error, [_ | _]},
|
{error, #{parse_results := [#{error := bad_license_format}]}},
|
||||||
emqx_license_parser:parse(
|
emqx_license_parser:parse(
|
||||||
<<"bad.license">>,
|
<<"bad.license">>,
|
||||||
public_key_pem()
|
public_key_pem()
|
||||||
|
@ -249,6 +253,20 @@ t_expiry_date(_Config) ->
|
||||||
|
|
||||||
?assertEqual({2295, 10, 27}, emqx_license_parser:expiry_date(License)).
|
?assertEqual({2295, 10, 27}, emqx_license_parser:expiry_date(License)).
|
||||||
|
|
||||||
|
t_empty_string(_Config) ->
|
||||||
|
?assertMatch(
|
||||||
|
{error, #{
|
||||||
|
parse_results := [
|
||||||
|
#{
|
||||||
|
error := empty_string,
|
||||||
|
module := emqx_license_parser_v20220101
|
||||||
|
}
|
||||||
|
| _
|
||||||
|
]
|
||||||
|
}},
|
||||||
|
emqx_license_parser:parse(<<>>)
|
||||||
|
).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Helpers
|
%% Helpers
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
@ -270,3 +288,10 @@ sample_license() ->
|
||||||
"10"
|
"10"
|
||||||
]
|
]
|
||||||
).
|
).
|
||||||
|
|
||||||
|
find_error(Module, #{parse_results := Results}) ->
|
||||||
|
find_error(Module, Results);
|
||||||
|
find_error(Module, [#{module := Module} = Result | _Results]) ->
|
||||||
|
Result;
|
||||||
|
find_error(Module, [_Result | Results]) ->
|
||||||
|
find_error(Module, Results).
|
||||||
|
|
|
@ -7,17 +7,6 @@
|
||||||
-compile(nowarn_export_all).
|
-compile(nowarn_export_all).
|
||||||
-compile(export_all).
|
-compile(export_all).
|
||||||
|
|
||||||
-define(DEFAULT_LICENSE_VALUES, [
|
|
||||||
"220111",
|
|
||||||
"0",
|
|
||||||
"10",
|
|
||||||
"Foo",
|
|
||||||
"contact@foo.com",
|
|
||||||
"20220111",
|
|
||||||
"100000",
|
|
||||||
"10"
|
|
||||||
]).
|
|
||||||
|
|
||||||
private_key() ->
|
private_key() ->
|
||||||
test_key("pvt.key").
|
test_key("pvt.key").
|
||||||
|
|
||||||
|
@ -76,5 +65,18 @@ make_license(Values) ->
|
||||||
EncodedSignature = base64:encode(Signature),
|
EncodedSignature = base64:encode(Signature),
|
||||||
iolist_to_binary([EncodedText, ".", EncodedSignature]).
|
iolist_to_binary([EncodedText, ".", EncodedSignature]).
|
||||||
|
|
||||||
|
default_test_license() ->
|
||||||
|
make_license(#{}).
|
||||||
|
|
||||||
default_license() ->
|
default_license() ->
|
||||||
emqx_license_schema:default_license().
|
emqx_license_schema:default_license().
|
||||||
|
|
||||||
|
mock_parser() ->
|
||||||
|
meck:new(emqx_license_parser, [non_strict, passthrough, no_history, no_link]),
|
||||||
|
meck:expect(emqx_license_parser, pubkey, fun() -> public_key_pem() end),
|
||||||
|
meck:expect(emqx_license_parser, default, fun() -> default_test_license() end),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
unmock_parser() ->
|
||||||
|
meck:unload(emqx_license_parser),
|
||||||
|
ok.
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Enhanced license key management.
|
||||||
|
|
||||||
|
EMQX can now load the license key from a specified file. This is enabled by setting the `license.key` configuration to a file path, which should be prefixed with `"file://"`.
|
||||||
|
Also added the ability to revert to the default trial license by setting `license.key = default`. This option simplifies the process of returning to the trial license if needed.
|
|
@ -25,7 +25,16 @@ connection_low_watermark_field_deprecated.label:
|
||||||
"""deprecated use /license/setting instead"""
|
"""deprecated use /license/setting instead"""
|
||||||
|
|
||||||
key_field.desc:
|
key_field.desc:
|
||||||
"""License string"""
|
"""This configuration parameter is designated for the license key and supports below input formats:
|
||||||
|
|
||||||
|
- Direct Key: Enter the secret key directly as a string value.
|
||||||
|
- File Path: Specify the path to a file that contains the secret key. Ensure the path starts with <code>file://</code>.
|
||||||
|
- "default": Use string value <code>"default"</code> to apply the default trial license.
|
||||||
|
|
||||||
|
Note: An invalid license key or an incorrect file path may prevent EMQX from starting successfully.
|
||||||
|
If a file path is used, EMQX attempts to reload the license key from the file every 2 minutes.
|
||||||
|
Any failure in reloading the license file will be recorded as an error level log message,
|
||||||
|
and EMQX continues to apply the license loaded previously."""
|
||||||
|
|
||||||
key_field.label:
|
key_field.label:
|
||||||
"""License string"""
|
"""License string"""
|
||||||
|
|
Loading…
Reference in New Issue