emqx/lib-ee/emqx_license/src/emqx_license_parser_v202201...

198 lines
5.7 KiB
Erlang

%%--------------------------------------------------------------------
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_license_parser_v20220101).
-behaviour(emqx_license_parser).
-include_lib("emqx/include/logger.hrl").
-include("emqx_license.hrl").
-define(DIGEST_TYPE, sha256).
-define(LICENSE_VERSION, <<"220111">>).
%% This is the earliest license start date for version 220111
%% in theory it should be the same as ?LICENSE_VERSION (20220111),
%% however, in order to make expiration test easier (before Mar.2022),
%% allow it to start from Nov.2021
-define(MIN_START_DATE, 20211101).
-export([
parse/2,
dump/1,
customer_type/1,
license_type/1,
expiry_date/1,
max_connections/1
]).
%%------------------------------------------------------------------------------
%% API
%%------------------------------------------------------------------------------
parse(Content, Key) ->
case do_parse(Content) of
{ok, {Payload, Signature}} ->
case verify_signature(Payload, Signature, Key) of
true -> parse_payload(Payload);
false -> {error, invalid_signature}
end;
{error, Reason} ->
{error, Reason}
end.
dump(
#{
type := Type,
customer_type := CType,
customer := Customer,
email := Email,
deployment := Deployment,
date_start := DateStart,
max_connections := MaxConns
} = License
) ->
DateExpiry = expiry_date(License),
{DateNow, _} = calendar:universal_time(),
Expiry = DateNow > DateExpiry,
[
{customer, Customer},
{email, Email},
{deployment, Deployment},
{max_connections, MaxConns},
{start_at, format_date(DateStart)},
{expiry_at, format_date(DateExpiry)},
{type, format_type(Type)},
{customer_type, CType},
{expiry, Expiry}
].
customer_type(#{customer_type := CType}) -> CType.
license_type(#{type := Type}) -> Type.
expiry_date(#{date_start := DateStart, days := Days}) ->
calendar:gregorian_days_to_date(
calendar:date_to_gregorian_days(DateStart) + Days
).
max_connections(#{max_connections := MaxConns}) ->
MaxConns.
%%------------------------------------------------------------------------------
%% Private functions
%%------------------------------------------------------------------------------
do_parse(Content) ->
try
[EncodedPayload, EncodedSignature] = binary:split(Content, <<".">>),
Payload = base64:decode(EncodedPayload),
Signature = base64:decode(EncodedSignature),
{ok, {Payload, Signature}}
catch
_:_ ->
{error, bad_license_format}
end.
verify_signature(Payload, Signature, Key) ->
public_key:verify(Payload, ?DIGEST_TYPE, Signature, Key).
parse_payload(Payload) ->
Lines = lists:map(
fun string:trim/1,
string:split(string:trim(Payload), <<"\n">>, all)
),
case Lines of
[?LICENSE_VERSION, Type, CType, Customer, Email, Deployment, DateStart, Days, MaxConns] ->
collect_fields([
{type, parse_type(Type)},
{customer_type, parse_customer_type(CType)},
{customer, {ok, Customer}},
{email, {ok, Email}},
{deployment, {ok, Deployment}},
{date_start, parse_date_start(DateStart)},
{days, parse_days(Days)},
{max_connections, parse_max_connections(MaxConns)}
]);
[_Version, _Type, _CType, _Customer, _Email, _Deployment, _DateStart, _Days, _MaxConns] ->
{error, invalid_version};
_ ->
{error, unexpected_number_of_fields}
end.
parse_type(TypeStr) ->
case parse_int(TypeStr) of
{ok, Type} -> {ok, Type};
_ -> {error, invalid_license_type}
end.
parse_customer_type(CTypeStr) ->
case parse_int(CTypeStr) of
{ok, CType} -> {ok, CType};
_ -> {error, invalid_customer_type}
end.
parse_date_start(DateStr) ->
case parse_int(DateStr) of
{ok, Num} when Num >= ?MIN_START_DATE ->
Y = Num div 1_00_00,
M = (Num rem 1_00_00) div 1_00,
D = Num rem 1_00,
case calendar:valid_date({Y, M, D}) of
true -> {ok, {Y, M, D}};
false -> {error, invalid_date}
end;
_ ->
{error, invalid_date}
end.
parse_days(DaysStr) ->
case parse_int(DaysStr) of
{ok, Days} when Days > 0 -> {ok, Days};
_ -> {error, invalid_int_value}
end.
parse_max_connections(MaxConnStr) ->
case parse_int(MaxConnStr) of
{ok, MaxConns} when MaxConns > 0 -> {ok, MaxConns};
_ -> {error, invalid_connection_limit}
end.
parse_int(Str0) ->
Str = iolist_to_binary(string:replace(Str0, ",", "")),
case string:to_integer(Str) of
{Num, <<"">>} -> {ok, Num};
_ -> error
end.
collect_fields(Fields) ->
Collected = lists:foldl(
fun
({Name, {ok, Value}}, {FieldValues, Errors}) ->
{[{Name, Value} | FieldValues], Errors};
({Name, {error, Reason}}, {FieldValues, Errors}) ->
{FieldValues, [{Name, Reason} | Errors]}
end,
{[], []},
Fields
),
case Collected of
{FieldValues, []} ->
{ok, maps:from_list(FieldValues)};
{_, Errors} ->
{error, lists:reverse(Errors)}
end.
format_date({Year, Month, Day}) ->
iolist_to_binary(
io_lib:format(
"~4..0w-~2..0w-~2..0w",
[Year, Month, Day]
)
).
format_type(?OFFICIAL) -> <<"official">>;
format_type(?TRIAL) -> <<"trial">>.