Merge pull request #6946 from zmstone/refactor-license-simplify-parser-error

refactor: treat throw exception as `{error, Reason}` return
This commit is contained in:
Zaiming (Stone) Shi 2022-02-11 19:09:40 +01:00 committed by GitHub
commit b9343891e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 128 additions and 68 deletions

View File

@ -90,6 +90,7 @@ get_raw_cluster_override_conf() ->
-spec init(term()) -> {ok, state()}. -spec init(term()) -> {ok, state()}.
init(_) -> init(_) ->
process_flag(trap_exit, true),
{ok, #{handlers => #{?MOD => ?MODULE}}}. {ok, #{handlers => #{?MOD => ?MODULE}}}.
handle_call({add_handler, ConfKeyPath, HandlerName}, _From, State = #{handlers := Handlers}) -> handle_call({add_handler, ConfKeyPath, HandlerName}, _From, State = #{handlers := Handlers}) ->
@ -102,24 +103,8 @@ handle_call({add_handler, ConfKeyPath, HandlerName}, _From, State = #{handlers :
handle_call({change_config, SchemaModule, ConfKeyPath, UpdateArgs}, _From, handle_call({change_config, SchemaModule, ConfKeyPath, UpdateArgs}, _From,
#{handlers := Handlers} = State) -> #{handlers := Handlers} = State) ->
Reply = try Result = handle_update_request(SchemaModule, ConfKeyPath, Handlers, UpdateArgs),
case process_update_request(ConfKeyPath, Handlers, UpdateArgs) of {reply, Result, State};
{ok, NewRawConf, OverrideConf, Opts} ->
check_and_save_configs(SchemaModule, ConfKeyPath, Handlers, NewRawConf,
OverrideConf, UpdateArgs, Opts);
{error, Result} ->
{error, Result}
end
catch Error:Reason:ST ->
?SLOG(error, #{
msg => "change_config_failed",
exception => Error,
reason => Reason,
stacktrace => ST
}),
{error, Reason}
end,
{reply, Reply, State};
handle_call(get_raw_cluster_override_conf, _From, State) -> handle_call(get_raw_cluster_override_conf, _From, State) ->
Reply = emqx_config:read_override_conf(#{override_to => cluster}), Reply = emqx_config:read_override_conf(#{override_to => cluster}),
{reply, Reply, State}; {reply, Reply, State};
@ -165,6 +150,30 @@ deep_put_handler2(Key, KeyPath, Handlers, Mod) ->
Error Error
end. end.
handle_update_request(SchemaModule, ConfKeyPath, Handlers, UpdateArgs) ->
try
do_handle_update_request(SchemaModule, ConfKeyPath, Handlers, UpdateArgs)
catch
throw : Reason ->
{error, Reason};
Error : Reason : ST ->
?SLOG(error, #{msg => "change_config_failed",
exception => Error,
reason => Reason,
stacktrace => ST
}),
{error, config_update_crashed}
end.
do_handle_update_request(SchemaModule, ConfKeyPath, Handlers, UpdateArgs) ->
case process_update_request(ConfKeyPath, Handlers, UpdateArgs) of
{ok, NewRawConf, OverrideConf, Opts} ->
check_and_save_configs(SchemaModule, ConfKeyPath, Handlers, NewRawConf,
OverrideConf, UpdateArgs, Opts);
{error, Result} ->
{error, Result}
end.
process_update_request(ConfKeyPath, _Handlers, {remove, Opts}) -> process_update_request(ConfKeyPath, _Handlers, {remove, Opts}) ->
OldRawConf = emqx_config:get_root_raw(ConfKeyPath), OldRawConf = emqx_config:get_root_raw(ConfKeyPath),
BinKeyPath = bin_path(ConfKeyPath), BinKeyPath = bin_path(ConfKeyPath),

View File

@ -48,6 +48,7 @@
, ipv6_probe/1 , ipv6_probe/1
, gen_id/0 , gen_id/0
, gen_id/1 , gen_id/1
, explain_posix/1
]). ]).
-export([ bin2hexstr_A_F/1 -export([ bin2hexstr_A_F/1
@ -314,6 +315,41 @@ clamp(Val, Min, Max) ->
true -> Val true -> Val
end. end.
%% @doc https://www.erlang.org/doc/man/file.html#posix-error-codes
explain_posix(eacces) -> "Permission denied";
explain_posix(eagain) -> "Resource temporarily unavailable";
explain_posix(ebadf) -> "Bad file number";
explain_posix(ebusy) -> "File busy";
explain_posix(edquot) -> "Disk quota exceeded";
explain_posix(eexist) -> "File already exists";
explain_posix(efault) -> "Bad address in system call argument";
explain_posix(efbig) -> "File too large";
explain_posix(eintr) -> "Interrupted system call";
explain_posix(einval) -> "Invalid argument argument file/socket";
explain_posix(eio) -> "I/O error";
explain_posix(eisdir) -> "Illegal operation on a directory";
explain_posix(eloop) -> "Too many levels of symbolic links";
explain_posix(emfile) -> "Too many open files";
explain_posix(emlink) -> "Too many links";
explain_posix(enametoolong) -> "Filename too long";
explain_posix(enfile) -> "File table overflow";
explain_posix(enodev) -> "No such device";
explain_posix(enoent) -> "No such file or directory";
explain_posix(enomem) -> "Not enough memory";
explain_posix(enospc) -> "No space left on device";
explain_posix(enotblk) -> "Block device required";
explain_posix(enotdir) -> "Not a directory";
explain_posix(enotsup) -> "Operation not supported";
explain_posix(enxio) -> "No such device or address";
explain_posix(eperm) -> "Not owner";
explain_posix(epipe) -> "Broken pipe";
explain_posix(erofs) -> "Read-only file system";
explain_posix(espipe) -> "Invalid seek";
explain_posix(esrch) -> "No such process";
explain_posix(estale) -> "Stale remote file handle";
explain_posix(exdev) -> "Cross-domain link";
explain_posix(NotPosix) -> NotPosix.
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Internal Functions %% Internal Functions
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------

View File

@ -41,15 +41,14 @@ init(#{path := Path} = Source) ->
Rules = case file:consult(Path) of Rules = case file:consult(Path) of
{ok, Terms} -> {ok, Terms} ->
[emqx_authz_rule:compile(Term) || Term <- Terms]; [emqx_authz_rule:compile(Term) || Term <- Terms];
{error, eacces} -> {error, Reason} when is_atom(Reason) ->
?SLOG(alert, #{msg => "insufficient_permissions_to_read_file", path => Path}), ?SLOG(alert, #{msg => failed_to_read_acl_file,
error(eaccess); path => Path,
{error, enoent} -> explain => emqx_misc:explain_posix(Reason)}),
?SLOG(alert, #{msg => "file_does_not_exist", path => Path}), throw(failed_to_read_acl_file);
error(enoent);
{error, Reason} -> {error, Reason} ->
?SLOG(alert, #{msg => "failed_to_read_file", path => Path, reason => Reason}), ?SLOG(alert, #{msg => bad_acl_file_content, path => Path, reason => Reason}),
error(Reason) throw(bad_acl_file_content)
end, end,
Source#{annotations => #{rules => Rules}}. Source#{annotations => #{rules => Rules}}.

View File

@ -75,12 +75,12 @@ t_invalid_file(_Config) ->
ok = file:write_file(<<"acl.conf">>, <<"{{invalid term">>), ok = file:write_file(<<"acl.conf">>, <<"{{invalid term">>),
?assertMatch( ?assertMatch(
{error, {1, erl_parse, _}}, {error, bad_acl_file_content},
emqx_authz:update(?CMD_REPLACE, [raw_file_authz_config()])). emqx_authz:update(?CMD_REPLACE, [raw_file_authz_config()])).
t_nonexistent_file(_Config) -> t_nonexistent_file(_Config) ->
?assertEqual( ?assertEqual(
{error, enoent}, {error, failed_to_read_acl_file},
emqx_authz:update(?CMD_REPLACE, emqx_authz:update(?CMD_REPLACE,
[maps:merge(raw_file_authz_config(), [maps:merge(raw_file_authz_config(),
#{<<"path">> => <<"nonexistent.conf">>}) #{<<"path">> => <<"nonexistent.conf">>})

View File

@ -9,7 +9,7 @@
-record(cluster_rpc_mfa, { -record(cluster_rpc_mfa, {
tnx_id :: pos_integer(), tnx_id :: pos_integer(),
mfa :: mfa(), mfa :: {module(), atom(), [any()]},
created_at :: calendar:datetime(), created_at :: calendar:datetime(),
initiator :: node() initiator :: node()
}). }).

View File

@ -228,7 +228,7 @@ catch_up(#{node := Node, retry_interval := RetryMs} = State, SkipResult) ->
{atomic, ok} -> catch_up(State, false); {atomic, ok} -> catch_up(State, false);
Error -> Error ->
?SLOG(error, #{ ?SLOG(error, #{
msg => "failed to commit applied call", msg => "failed_to_commit_applied_call",
applied_id => NextId, applied_id => NextId,
error => Error}), error => Error}),
RetryMs RetryMs
@ -359,28 +359,34 @@ apply_mfa(TnxId, {M, F, A}) ->
Res = Res =
try erlang:apply(M, F, A) try erlang:apply(M, F, A)
catch catch
Class:Reason:Stacktrace -> throw : Reason ->
{error, #{reason => Reason}};
Class : Reason : Stacktrace ->
{error, #{exception => Class, reason => Reason, stacktrace => Stacktrace}} {error, #{exception => Class, reason => Reason, stacktrace => Stacktrace}}
end, end,
Meta = #{tnx_id => TnxId, module => M, function => F, args => ?TO_BIN(A)}, %% Do not log args as it might be sensitive information
Meta = #{tnx_id => TnxId, entrypoint => format_mfa(M, F, length(A))},
IsSuccess = is_success(Res), IsSuccess = is_success(Res),
log_and_alarm(IsSuccess, Res, Meta, TnxId), log_and_alarm(IsSuccess, Res, Meta),
{IsSuccess, Res}. {IsSuccess, Res}.
format_mfa(M, F, A) ->
iolist_to_binary([atom_to_list(M), ":", atom_to_list(F), "/", integer_to_list(A)]).
is_success(ok) -> true; is_success(ok) -> true;
is_success({ok, _}) -> true; is_success({ok, _}) -> true;
is_success(_) -> false. is_success(_) -> false.
log_and_alarm(true, Res, Meta, TnxId) -> log_and_alarm(true, Res, Meta) ->
OkMeta = Meta#{msg => <<"succeeded to apply MFA">>, result => Res}, ?SLOG(debug, Meta#{msg => "cluster_rpc_apply_ok", result => Res}),
?SLOG(debug, OkMeta), do_alarm(deactivate, Res, Meta);
Message = ["cluster_rpc_apply_failed:", integer_to_binary(TnxId)], log_and_alarm(false, Res, Meta) ->
emqx_alarm:deactivate(cluster_rpc_apply_failed, OkMeta#{result => ?TO_BIN(Res)}, Message); ?SLOG(error, Meta#{msg => "cluster_rpc_apply_failed", result => Res}),
log_and_alarm(false, Res, Meta, TnxId) -> do_alarm(activate, Res, Meta).
NotOkMeta = Meta#{msg => <<"failed to apply MFA">>, result => Res},
?SLOG(error, NotOkMeta), do_alarm(Fun, Res, #{tnx_id := Id} = Meta) ->
Message = ["cluster_rpc_apply_failed:", integer_to_binary(TnxId)], AlarmMsg = ["cluster_rpc_apply_failed=", integer_to_list(Id)],
emqx_alarm:activate(cluster_rpc_apply_failed, NotOkMeta#{result => ?TO_BIN(Res)}, Message). emqx_alarm:Fun(cluster_rpc_apply_failed, Meta#{result => ?TO_BIN(Res)}, AlarmMsg).
wait_for_all_nodes_commit(TnxId, Delay, Remain) -> wait_for_all_nodes_commit(TnxId, Delay, Remain) ->
case lagging_node(TnxId) of case lagging_node(TnxId) of

View File

@ -73,7 +73,7 @@ admins(_) ->
{"cluster_call status", "status"}, {"cluster_call status", "status"},
{"cluster_call skip [node]", "increase one commit on specific node"}, {"cluster_call skip [node]", "increase one commit on specific node"},
{"cluster_call tnxid <TnxId>", "get detailed about TnxId"}, {"cluster_call tnxid <TnxId>", "get detailed about TnxId"},
{"cluster_call fast_forward [node] [tnx_id]", "fast forwards to tnx_id" } {"cluster_call fast_forward [node] [tnx_id]", "fast forwards to tnx_id" }
]). ]).
status() -> status() ->

View File

@ -118,10 +118,10 @@ do_update({file, Filename}, _Conf) ->
{ok, _License} -> {ok, _License} ->
#{<<"file">> => Filename}; #{<<"file">> => Filename};
{error, Reason} -> {error, Reason} ->
error(Reason) erlang:throw(Reason)
end; end;
{error, Reason} -> {error, Reason} ->
error({invalid_license_file, Reason}) erlang:throw({invalid_license_file, Reason})
end; end;
do_update({key, Content}, _Conf) when is_binary(Content); is_list(Content) -> do_update({key, Content}, _Conf) when is_binary(Content); is_list(Content) ->
@ -129,7 +129,7 @@ do_update({key, Content}, _Conf) when is_binary(Content); is_list(Content) ->
{ok, _License} -> {ok, _License} ->
#{<<"key">> => Content}; #{<<"key">> => Content};
{error, Reason} -> {error, Reason} ->
error(Reason) erlang:throw(Reason)
end. end.
check_max_clients_exceeded(MaxClients) -> check_max_clients_exceeded(MaxClients) ->

View File

@ -98,7 +98,7 @@ max_connections(#{module := Module, data := LicenseData}) ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
do_parse(_Content, _Key, [], Errors) -> do_parse(_Content, _Key, [], Errors) ->
{error, {unknown_format, lists:reverse(Errors)}}; {error, 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
@ -107,7 +107,7 @@ do_parse(Content, Key, [Module | Modules], Errors) ->
{error, Error} -> {error, Error} ->
do_parse(Content, Key, Modules, [{Module, Error} | Errors]) do_parse(Content, Key, Modules, [{Module, Error} | Errors])
catch catch
_Class:Error:_Stk -> _Class : Error ->
do_parse(Content, Key, Modules, [{Module, Error} | Errors]) do_parse(Content, Key, Modules, [{Module, Error} | Errors])
end. end.

View File

@ -24,12 +24,14 @@
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
parse(Content, Key) -> parse(Content, Key) ->
[EncodedPayload, EncodedSignature] = binary:split(Content, <<".">>), case do_parse(Content) of
Payload = base64:decode(EncodedPayload), {ok, {Payload, Signature}} ->
Signature = base64:decode(EncodedSignature), case verify_signature(Payload, Signature, Key) of
case verify_signature(Payload, Signature, Key) of true -> parse_payload(Payload);
true -> parse_payload(Payload); false -> {error, invalid_signature}
false -> {error, invalid_signature} end;
{error, Reason} ->
{error, Reason}
end. end.
dump(#{type := Type, dump(#{type := Type,
@ -67,6 +69,17 @@ max_connections(#{max_connections := MaxConns}) ->
%% Private functions %% 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) -> verify_signature(Payload, Signature, Key) ->
RSAPublicKey = public_key:der_decode('RSAPublicKey', Key), RSAPublicKey = public_key:der_decode('RSAPublicKey', Key),
public_key:verify(Payload, ?DIGEST_TYPE, Signature, RSAPublicKey). public_key:verify(Payload, ?DIGEST_TYPE, Signature, RSAPublicKey).

View File

@ -68,7 +68,7 @@ t_update_file(_Config) ->
ok = file:write_file("license_with_invalid_content.lic", <<"bad license">>), ok = file:write_file("license_with_invalid_content.lic", <<"bad license">>),
?assertMatch( ?assertMatch(
{error, {unknown_format, _}}, {error, [_ | _]},
emqx_license:update_file("license_with_invalid_content.lic")), emqx_license:update_file("license_with_invalid_content.lic")),
?assertMatch( ?assertMatch(
@ -77,7 +77,7 @@ t_update_file(_Config) ->
t_update_value(_Config) -> t_update_value(_Config) ->
?assertMatch( ?assertMatch(
{error, {unknown_format, _}}, {error, [_ | _]},
emqx_license:update_key("invalid.license")), emqx_license:update_key("invalid.license")),
{ok, LicenseValue} = file:read_file(emqx_license_test_lib:default_license()), {ok, LicenseValue} = file:read_file(emqx_license_test_lib:default_license()),

View File

@ -45,8 +45,7 @@ t_parse(_Config) ->
%% invalid version %% invalid version
?assertMatch( ?assertMatch(
{error, {error,
{unknown_format, [{emqx_license_parser_v20220101,invalid_version}]},
[{emqx_license_parser_v20220101,invalid_version}]}},
emqx_license_parser:parse( emqx_license_parser:parse(
emqx_license_test_lib:make_license( emqx_license_test_lib:make_license(
["220101", ["220101",
@ -63,8 +62,7 @@ t_parse(_Config) ->
%% invalid field number %% invalid field number
?assertMatch( ?assertMatch(
{error, {error,
{unknown_format, [{emqx_license_parser_v20220101,invalid_field_number}]},
[{emqx_license_parser_v20220101,invalid_field_number}]}},
emqx_license_parser:parse( emqx_license_parser:parse(
emqx_license_test_lib:make_license( emqx_license_test_lib:make_license(
["220111", ["220111",
@ -80,12 +78,11 @@ t_parse(_Config) ->
?assertMatch( ?assertMatch(
{error, {error,
{unknown_format,
[{emqx_license_parser_v20220101, [{emqx_license_parser_v20220101,
[{type,invalid_license_type}, [{type,invalid_license_type},
{customer_type,invalid_customer_type}, {customer_type,invalid_customer_type},
{date_start,invalid_date}, {date_start,invalid_date},
{days,invalid_int_value}]}]}}, {days,invalid_int_value}]}]},
emqx_license_parser:parse( emqx_license_parser:parse(
emqx_license_test_lib:make_license( emqx_license_test_lib:make_license(
["220111", ["220111",
@ -125,21 +122,20 @@ t_parse(_Config) ->
?assertMatch( ?assertMatch(
{error, {error,
{unknown_format, [{emqx_license_parser_v20220101,invalid_signature}]},
[{emqx_license_parser_v20220101,invalid_signature}]}},
emqx_license_parser:parse( emqx_license_parser:parse(
iolist_to_binary([LicensePart, <<".">>, SignaturePart]), iolist_to_binary([LicensePart, <<".">>, SignaturePart]),
public_key_encoded())), public_key_encoded())),
%% totally invalid strings as license %% totally invalid strings as license
?assertMatch( ?assertMatch(
{error, {unknown_format, _}}, {error, [_ | _]},
emqx_license_parser:parse( emqx_license_parser:parse(
<<"badlicense">>, <<"badlicense">>,
public_key_encoded())), public_key_encoded())),
?assertMatch( ?assertMatch(
{error, {unknown_format, _}}, {error, [_ | _]},
emqx_license_parser:parse( emqx_license_parser:parse(
<<"bad.license">>, <<"bad.license">>,
public_key_encoded())). public_key_encoded())).

View File

@ -11,7 +11,6 @@
## ./scripts/buildx.sh --profile emqx --pkgtype tgz --arch arm64 --builder ghcr.io/emqx/emqx-builder/4.4-4:24.1.5-3-debian10 ## ./scripts/buildx.sh --profile emqx --pkgtype tgz --arch arm64 --builder ghcr.io/emqx/emqx-builder/4.4-4:24.1.5-3-debian10
set -euo pipefail set -euo pipefail
set -x
help() { help() {
echo echo
@ -91,6 +90,8 @@ if [ -z "${PROFILE:-}" ] ||
exit 1 exit 1
fi fi
set -x
if [ -z "${WITH_ELIXIR:-}" ]; then if [ -z "${WITH_ELIXIR:-}" ]; then
WITH_ELIXIR=no WITH_ELIXIR=no
fi fi