198 lines
5.7 KiB
Erlang
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">>.
|