emqx/apps/emqx_license/src/emqx_license_parser.erl

183 lines
5.4 KiB
Erlang

%%--------------------------------------------------------------------
%% Copyright (c) 2022-2024 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% @doc EMQX License Management.
%%--------------------------------------------------------------------
-module(emqx_license_parser).
-include_lib("emqx/include/logger.hrl").
-include("emqx_license.hrl").
-define(PUBKEY, <<
""
"\n"
"-----BEGIN PUBLIC KEY-----\n"
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbtkdos3TZmSv+D7+X5pc0yfcjum2\n"
"Q1DK6PCWkiQihjvjJjKFzdYzcWOgC6f4Ou3mgGAUSjdQYYnFKZ/9f5ax4g==\n"
"-----END PUBLIC KEY-----\n"
""
>>).
-define(LICENSE_PARSE_MODULES, [
emqx_license_parser_v20220101
]).
-type license_data() :: term().
-type customer_type() ::
?SMALL_CUSTOMER
| ?MEDIUM_CUSTOMER
| ?LARGE_CUSTOMER
| ?BUSINESS_CRITICAL_CUSTOMER
| ?EVALUATION_CUSTOMER.
-type license_type() :: ?OFFICIAL | ?TRIAL.
-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()
}.
-type raw_license() :: string() | binary() | default.
-export_type([
license_data/0,
customer_type/0,
license_type/0,
license/0
]).
-export([
parse/1,
parse/2,
dump/1,
summary/1,
customer_type/1,
license_type/1,
expiry_date/1,
max_connections/1,
is_business_critical/1
]).
%% for testing purpose
-export([
default/0,
pubkey/0
]).
%%--------------------------------------------------------------------
%% Behaviour
%%--------------------------------------------------------------------
-callback parse(string() | binary(), binary()) -> {ok, license_data()} | {error, 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 license_type(license_data()) -> license_type().
-callback expiry_date(license_data()) -> calendar:date().
-callback max_connections(license_data()) -> non_neg_integer().
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
pubkey() -> ?PUBKEY.
default() -> emqx_license_schema:default_license().
%% @doc Parse license key.
%% If the license key is prefixed with "file://path/to/license/file",
%% then the license key is read from the file.
-spec parse(raw_license()) -> {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),
do_parse(iolist_to_binary(Content), Key, ?LICENSE_PARSE_MODULES, []).
-spec dump(license()) -> list({atom(), term()}).
dump(#{module := Module, data := LicenseData}) ->
Module:dump(LicenseData).
-spec summary(license()) -> map().
summary(#{module := Module, data := Data}) ->
Module:summary(Data).
-spec customer_type(license()) -> customer_type().
customer_type(#{module := Module, data := LicenseData}) ->
Module:customer_type(LicenseData).
-spec license_type(license()) -> license_type().
license_type(#{module := Module, data := LicenseData}) ->
Module:license_type(LicenseData).
-spec expiry_date(license()) -> calendar:date().
expiry_date(#{module := Module, data := LicenseData}) ->
Module:expiry_date(LicenseData).
-spec max_connections(license()) -> non_neg_integer().
max_connections(#{module := Module, data := LicenseData}) ->
Module:max_connections(LicenseData).
-spec is_business_critical(license() | raw_license()) -> boolean().
is_business_critical(#{module := Module, data := LicenseData}) ->
Module:customer_type(LicenseData) =:= ?BUSINESS_CRITICAL_CUSTOMER;
is_business_critical(Key) when is_binary(Key) ->
{ok, License} = parse(Key),
is_business_critical(License).
%%--------------------------------------------------------------------
%% Private functions
%%--------------------------------------------------------------------
do_parse(_Content, _Key, [], Errors) ->
{error, #{parse_results => lists:reverse(Errors)}};
do_parse(Content, Key, [Module | Modules], Errors) ->
try Module:parse(Content, Key) of
{ok, LicenseData} ->
{ok, #{module => Module, data => LicenseData, source => <<"******">>}};
{error, Error} ->
do_parse(Content, Key, Modules, [#{module => Module, error => Error} | Errors])
catch
_Class:Error:Stacktrace ->
do_parse(Content, Key, Modules, [
#{module => Module, error => Error, stacktrace => Stacktrace} | Errors
])
end.
to_bin(A) when is_atom(A) ->
atom_to_binary(A);
to_bin(L) ->
iolist_to_binary(L).