From caef8cb38146f10d07e60dbcfb43ff03e866e960 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Tue, 31 Aug 2021 10:24:17 +0800 Subject: [PATCH 001/109] fix: retainer message format time by rfc3339 (#5607) * fix: retainer message format time by rfc3339 --- apps/emqx_retainer/src/emqx_retainer_api.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/emqx_retainer/src/emqx_retainer_api.erl b/apps/emqx_retainer/src/emqx_retainer_api.erl index 313ae9b02..503827d4b 100644 --- a/apps/emqx_retainer/src/emqx_retainer_api.erl +++ b/apps/emqx_retainer/src/emqx_retainer_api.erl @@ -46,9 +46,9 @@ message_props() -> {topic, string, <<"MQTT Topic">>}, {qos, string, <<"MQTT QoS">>}, {payload, string, <<"MQTT Payload">>}, - {publish_at, string, <<"publish datetime">>}, - {from_clientid, string, <<"publisher ClientId">>}, - {from_username, string, <<"publisher Username">>} + {publish_at, string, <<"Publish datetime, in RFC 3339 format">>}, + {from_clientid, string, <<"Publisher ClientId">>}, + {from_username, string, <<"Publisher Username">>} ]). parameters() -> @@ -170,7 +170,7 @@ format_message(#message{id = ID, qos = Qos, topic = Topic, from = From, timestam #{msgid => emqx_guid:to_hexstr(ID), qos => Qos, topic => Topic, - publish_at => erlang:list_to_binary(emqx_mgmt_util:strftime(Timestamp div 1000)), + publish_at => list_to_binary(calendar:system_time_to_rfc3339(Timestamp, [{unit, millisecond}])), from_clientid => to_bin_string(From), from_username => maps:get(username, Headers, <<>>) }. From 6b313a60d4d9920494ceeed908905725e9945b56 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Mon, 30 Aug 2021 17:42:56 +0800 Subject: [PATCH 002/109] refactor: refactor emqx_authz Signed-off-by: zhanghongtong --- apps/emqx_authz/src/emqx_authz.erl | 206 ++++++++++-------- apps/emqx_authz/src/emqx_authz_api.erl | 195 ++++++++--------- apps/emqx_authz/src/emqx_authz_api_schema.erl | 10 +- apps/emqx_authz/test/emqx_authz_SUITE.erl | 192 ++++++++-------- apps/emqx_authz/test/emqx_authz_api_SUITE.erl | 192 ++++++++-------- 5 files changed, 415 insertions(+), 380 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 3a39a2984..4a6d7033e 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -51,33 +51,41 @@ init() -> lookup() -> {_M, _F, [A]}= find_action_in_hooks(), A. -lookup(Id) -> - try find_source_by_id(Id, lookup()) of +lookup(Type) -> + try find_source_by_type(atom(Type), lookup()) of {_, Source} -> Source catch error:Reason -> {error, Reason} end. -move(Id, Position) -> - emqx:update_config(?CONF_KEY_PATH, {move, Id, Position}). +move(Type, #{<<"before">> := Before}) -> + emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), #{<<"before">> => atom(Before)}}); +move(Type, #{<<"after">> := After}) -> + emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), #{<<"after">> => atom(After)}}); +move(Type, Position) -> + emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), Position}). +update({replace_once, Type}, Sources) -> + emqx:update_config(?CONF_KEY_PATH, {{replace_once, atom(Type)}, Sources}); +update({delete_once, Type}, Sources) -> + emqx:update_config(?CONF_KEY_PATH, {{delete_once, atom(Type)}, Sources}); update(Cmd, Sources) -> emqx:update_config(?CONF_KEY_PATH, {Cmd, Sources}). -pre_config_update({move, Id, <<"top">>}, Conf) when is_list(Conf) -> - {Index, _} = find_source_by_id(Id), +pre_config_update({move, Type, <<"top">>}, Conf) when is_list(Conf) -> + {Index, _} = find_source_by_type(Type), {List1, List2} = lists:split(Index, Conf), {ok, [lists:nth(Index, Conf)] ++ lists:droplast(List1) ++ List2}; -pre_config_update({move, Id, <<"bottom">>}, Conf) when is_list(Conf) -> - {Index, _} = find_source_by_id(Id), +pre_config_update({move, Type, <<"bottom">>}, Conf) when is_list(Conf) -> + {Index, _} = find_source_by_type(Type), {List1, List2} = lists:split(Index, Conf), {ok, lists:droplast(List1) ++ List2 ++ [lists:nth(Index, Conf)]}; -pre_config_update({move, Id, #{<<"before">> := BeforeId}}, Conf) when is_list(Conf) -> - {Index1, _} = find_source_by_id(Id), +pre_config_update({move, Type, #{<<"before">> := Before}}, Conf) when is_list(Conf) -> + {Index1, _} = find_source_by_type(Type), Conf1 = lists:nth(Index1, Conf), - {Index2, _} = find_source_by_id(BeforeId), + {Index2, _} = find_source_by_type(Before), Conf2 = lists:nth(Index2, Conf), {List1, List2} = lists:split(Index2, Conf), @@ -85,10 +93,10 @@ pre_config_update({move, Id, #{<<"before">> := BeforeId}}, Conf) when is_list(Co ++ [Conf1] ++ [Conf2] ++ lists:delete(Conf1, List2)}; -pre_config_update({move, Id, #{<<"after">> := AfterId}}, Conf) when is_list(Conf) -> - {Index1, _} = find_source_by_id(Id), +pre_config_update({move, Type, #{<<"after">> := After}}, Conf) when is_list(Conf) -> + {Index1, _} = find_source_by_type(Type), Conf1 = lists:nth(Index1, Conf), - {Index2, _} = find_source_by_id(AfterId), + {Index2, _} = find_source_by_type(After), {List1, List2} = lists:split(Index2, Conf), {ok, lists:delete(Conf1, List1) @@ -99,34 +107,37 @@ pre_config_update({head, Sources}, Conf) when is_list(Sources), is_list(Conf) -> {ok, Sources ++ Conf}; pre_config_update({tail, Sources}, Conf) when is_list(Sources), is_list(Conf) -> {ok, Conf ++ Sources}; -pre_config_update({{replace_once, Id}, Source}, Conf) when is_map(Source), is_list(Conf) -> - {Index, _} = find_source_by_id(Id), +pre_config_update({{replace_once, Type}, Source}, Conf) when is_map(Source), is_list(Conf) -> + {Index, _} = find_source_by_type(Type), {List1, List2} = lists:split(Index, Conf), {ok, lists:droplast(List1) ++ [Source] ++ List2}; +pre_config_update({{delete_once, Type}, _Source}, Conf) when is_list(Conf) -> + {_, Source} = find_source_by_type(Type), + {ok, lists:delete(Source, Conf)}; pre_config_update({_, Sources}, _Conf) when is_list(Sources)-> %% overwrite the entire config! {ok, Sources}. post_config_update(_, undefined, _Conf, _AppEnvs) -> ok; -post_config_update({move, Id, <<"top">>}, _NewSources, _OldSources, _AppEnvs) -> +post_config_update({move, Type, <<"top">>}, _NewSources, _OldSources, _AppEnvs) -> InitedSources = lookup(), - {Index, Source} = find_source_by_id(Id, InitedSources), + {Index, Source} = find_source_by_type(Type, InitedSources), {Sources1, Sources2 } = lists:split(Index, InitedSources), Sources3 = [Source] ++ lists:droplast(Sources1) ++ Sources2, ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Sources3]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({move, Id, <<"bottom">>}, _NewSources, _OldSources, _AppEnvs) -> +post_config_update({move, Type, <<"bottom">>}, _NewSources, _OldSources, _AppEnvs) -> InitedSources = lookup(), - {Index, Source} = find_source_by_id(Id, InitedSources), + {Index, Source} = find_source_by_type(Type, InitedSources), {Sources1, Sources2 } = lists:split(Index, InitedSources), Sources3 = lists:droplast(Sources1) ++ Sources2 ++ [Source], ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Sources3]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({move, Id, #{<<"before">> := BeforeId}}, _NewSources, _OldSources, _AppEnvs) -> +post_config_update({move, Type, #{<<"before">> := Before}}, _NewSources, _OldSources, _AppEnvs) -> InitedSources = lookup(), - {_, Source0} = find_source_by_id(Id, InitedSources), - {Index, Source1} = find_source_by_id(BeforeId, InitedSources), + {_, Source0} = find_source_by_type(Type, InitedSources), + {Index, Source1} = find_source_by_type(Before, InitedSources), {Sources1, Sources2} = lists:split(Index, InitedSources), Sources3 = lists:delete(Source0, lists:droplast(Sources1)) ++ [Source0] ++ [Source1] @@ -134,10 +145,10 @@ post_config_update({move, Id, #{<<"before">> := BeforeId}}, _NewSources, _OldSou ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Sources3]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({move, Id, #{<<"after">> := AfterId}}, _NewSources, _OldSources, _AppEnvs) -> +post_config_update({move, Type, #{<<"after">> := After}}, _NewSources, _OldSources, _AppEnvs) -> InitedSources = lookup(), - {_, Source} = find_source_by_id(Id, InitedSources), - {Index, _} = find_source_by_id(AfterId, InitedSources), + {_, Source} = find_source_by_type(Type, InitedSources), + {Index, _} = find_source_by_type(After, InitedSources), {Sources1, Sources2} = lists:split(Index, InitedSources), Sources3 = lists:delete(Source, Sources1) ++ [Source] @@ -155,9 +166,9 @@ post_config_update({tail, Sources}, _NewSources, _OldConf, _AppEnvs) -> emqx_hooks:put('client.authorize', {?MODULE, authorize, [lookup() ++ InitedSources]}, -1), ok = emqx_authz_cache:drain_cache(); -post_config_update({{replace_once, Id}, Source}, _NewSources, _OldConf, _AppEnvs) when is_map(Source) -> +post_config_update({{replace_once, Type}, #{type := Type} = Source}, _NewSources, _OldConf, _AppEnvs) when is_map(Source) -> OldInitedSources = lookup(), - {Index, OldSource} = find_source_by_id(Id, OldInitedSources), + {Index, OldSource} = find_source_by_type(Type, OldInitedSources), case maps:get(type, OldSource, undefined) of undefined -> ok; _ -> @@ -165,10 +176,19 @@ post_config_update({{replace_once, Id}, Source}, _NewSources, _OldConf, _AppEnvs ok = emqx_resource:remove(Id) end, {OldSources1, OldSources2 } = lists:split(Index, OldInitedSources), - InitedSources = [init_source(R#{annotations => #{id => Id}}) || R <- check_sources([Source])], + InitedSources = [init_source(R) || R <- check_sources([Source])], ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [lists:droplast(OldSources1) ++ InitedSources ++ OldSources2]}, -1), ok = emqx_authz_cache:drain_cache(); - +post_config_update({{delete_once, Type}, _Source}, _NewSources, _OldConf, _AppEnvs) -> + OldInitedSources = lookup(), + {_, OldSource} = find_source_by_type(Type, OldInitedSources), + case OldSource of + #{annotations := #{id := Id}} -> + ok = emqx_resource:remove(Id); + _ -> ok + end, + ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [lists:delete(OldSource, OldInitedSources)]}, -1), + ok = emqx_authz_cache:drain_cache(); post_config_update(_, NewSources, _OldConf, _AppEnvs) -> %% overwrite the entire config! OldInitedSources = lookup(), @@ -181,52 +201,13 @@ post_config_update(_, NewSources, _OldConf, _AppEnvs) -> ok = emqx_authz_cache:drain_cache(). %%-------------------------------------------------------------------- -%% Internal functions +%% Initialize source %%-------------------------------------------------------------------- -check_sources(RawSources) -> - {ok, Conf} = hocon:binary(jsx:encode(#{<<"authorization">> => #{<<"sources">> => RawSources}}), #{format => richmap}), - CheckConf = hocon_schema:check(emqx_authz_schema, Conf, #{atom_key => true}), - #{authorization:= #{sources := Sources}} = hocon_schema:richmap_to_map(CheckConf), - Sources. - -find_source_by_id(Id) -> find_source_by_id(Id, lookup()). -find_source_by_id(Id, Sources) -> find_source_by_id(Id, Sources, 1). -find_source_by_id(_SourceId, [], _N) -> error(not_found_rule); -find_source_by_id(SourceId, [ Source = #{annotations := #{id := Id}} | Tail], N) -> - case SourceId =:= Id of - true -> {N, Source}; - false -> find_source_by_id(SourceId, Tail, N + 1) - end. - -find_action_in_hooks() -> - Callbacks = emqx_hooks:lookup('client.authorize'), - [Action] = [Action || {callback,{?MODULE, authorize, _} = Action, _, _} <- Callbacks ], - Action. - -gen_id(Type) -> - iolist_to_binary([io_lib:format("~s_~s",[?APP, Type]), "_", integer_to_list(erlang:system_time())]). - -create_resource(#{type := DB, - config := Config, - annotations := #{id := ResourceID}}) -> - case emqx_resource:update(ResourceID, connector_module(DB), Config, []) of - {ok, _} -> ResourceID; - {error, Reason} -> {error, Reason} - end; -create_resource(#{type := DB, - config := Config}) -> - ResourceID = gen_id(DB), - case emqx_resource:create(ResourceID, connector_module(DB), Config) of - {ok, already_created} -> ResourceID; - {ok, _} -> ResourceID; - {error, Reason} -> {error, Reason} - end. - init_source(#{enable := true, - type := file, - path := Path - } = Source) -> + type := file, + path := Path + } = Source) -> Rules = case file:consult(Path) of {ok, Terms} -> [emqx_authz_rule:compile(Term) || Term <- Terms]; @@ -240,35 +221,28 @@ init_source(#{enable := true, ?LOG(alert, "Failed to read ~s: ~p", [Path, Reason]), error(Reason) end, - Source#{annotations => - #{id => gen_id(file), - rules => Rules - }}; + Source#{annotations => #{rules => Rules}}; init_source(#{enable := true, - type := http, - config := #{url := Url} = Config - } = Source) -> + type := http, + config := #{url := Url} = Config + } = Source) -> NConfig = maps:merge(Config, #{base_url => maps:remove(query, Url)}), case create_resource(Source#{config := NConfig}) of {error, Reason} -> error({load_config_error, Reason}); - Id -> Source#{annotations => - #{id => Id} - } + Id -> Source#{annotations => #{id => Id}} end; init_source(#{enable := true, - type := DB - } = Source) when DB =:= redis; + type := DB + } = Source) when DB =:= redis; DB =:= mongo -> case create_resource(Source) of {error, Reason} -> error({load_config_error, Reason}); - Id -> Source#{annotations => - #{id => Id} - } + Id -> Source#{annotations => #{id => Id}} end; init_source(#{enable := true, - type := DB, - sql := SQL - } = Source) when DB =:= mysql; + type := DB, + sql := SQL + } = Source) when DB =:= mysql; DB =:= pgsql -> Mod = authz_module(DB), case create_resource(Source) of @@ -323,8 +297,58 @@ do_authorize(Client, PubSub, Topic, Matched -> Matched end. +%%-------------------------------------------------------------------- +%% Internal function +%%-------------------------------------------------------------------- + +check_sources(RawSources) -> + {ok, Conf} = hocon:binary(jsx:encode(#{<<"authorization">> => #{<<"sources">> => RawSources}}), #{format => richmap}), + CheckConf = hocon_schema:check(emqx_authz_schema, Conf, #{atom_key => true}), + #{authorization:= #{sources := Sources}} = hocon_schema:richmap_to_map(CheckConf), + Sources. + +find_source_by_type(Type) -> find_source_by_type(Type, lookup()). +find_source_by_type(Type, Sources) -> find_source_by_type(Type, Sources, 1). +find_source_by_type(_, [], _N) -> error(not_found_rule); +find_source_by_type(Type, [ Source = #{type := T} | Tail], N) -> + case Type =:= T of + true -> {N, Source}; + false -> find_source_by_type(Type, Tail, N + 1) + end. + +find_action_in_hooks() -> + Callbacks = emqx_hooks:lookup('client.authorize'), + [Action] = [Action || {callback,{?MODULE, authorize, _} = Action, _, _} <- Callbacks ], + Action. + +gen_id(Type) -> + iolist_to_binary([io_lib:format("~s_~s",[?APP, Type])]). + +create_resource(#{type := DB, + config := Config, + annotations := #{id := ResourceID}}) -> + case emqx_resource:update(ResourceID, connector_module(DB), Config, []) of + {ok, _} -> ResourceID; + {error, Reason} -> {error, Reason} + end; +create_resource(#{type := DB, + config := Config}) -> + ResourceID = gen_id(DB), + case emqx_resource:create(ResourceID, connector_module(DB), Config) of + {ok, already_created} -> ResourceID; + {ok, _} -> ResourceID; + {error, Reason} -> {error, Reason} + end. + authz_module(Type) -> list_to_existing_atom("emqx_authz_" ++ atom_to_list(Type)). connector_module(Type) -> list_to_existing_atom("emqx_connector_" ++ atom_to_list(Type)). + +atom(B) when is_binary(B) -> + try binary_to_existing_atom(B, utf8) + catch + _ -> binary_to_atom(B) + end; +atom(A) when is_atom(A) -> A. diff --git a/apps/emqx_authz/src/emqx_authz_api.erl b/apps/emqx_authz/src/emqx_authz_api.erl index 1646a9af2..ff5217426 100644 --- a/apps/emqx_authz/src/emqx_authz_api.erl +++ b/apps/emqx_authz/src/emqx_authz_api.erl @@ -30,7 +30,7 @@ -define(EXAMPLE_RETURNED_RULES, - #{rules => [?EXAMPLE_RETURNED_RULE1 + #{sources => [?EXAMPLE_RETURNED_RULE1 ] }). @@ -40,23 +40,23 @@ topics => [<<"#">>]}). -export([ api_spec/0 - , rules/2 - , rule/2 - , move_rule/2 + , sources/2 + , source/2 + , move_source/2 ]). api_spec() -> - {[ rules_api() - , rule_api() - , move_rule_api() + {[ sources_api() + , source_api() + , move_source_api() ], definitions()}. definitions() -> emqx_authz_api_schema:definitions(). -rules_api() -> +sources_api() -> Metadata = #{ get => #{ - description => "List authorization rules", + description => "List authorization sources", parameters => [ #{ name => page, @@ -82,16 +82,16 @@ rules_api() -> 'application/json' => #{ schema => #{ type => object, - required => [rules], - properties => #{rules => #{ + required => [sources], + properties => #{sources => #{ type => array, - items => minirest:ref(<<"returned_rules">>) + items => minirest:ref(<<"returned_sources">>) } } }, examples => #{ - rules => #{ - summary => <<"Rules">>, + sources => #{ + summary => <<"Sources">>, value => jsx:encode(?EXAMPLE_RETURNED_RULES) } } @@ -101,14 +101,14 @@ rules_api() -> } }, post => #{ - description => "Add new rule", + description => "Add new source", requestBody => #{ content => #{ 'application/json' => #{ - schema => minirest:ref(<<"rules">>), + schema => minirest:ref(<<"sources">>), examples => #{ - simple_rule => #{ - summary => <<"Rules">>, + simple_source => #{ + summary => <<"Sources">>, value => jsx:encode(?EXAMPLE_RULE1) } } @@ -138,17 +138,17 @@ rules_api() -> }, put => #{ - description => "Update all rules", + description => "Update all sources", requestBody => #{ content => #{ 'application/json' => #{ schema => #{ type => array, - items => minirest:ref(<<"returned_rules">>) + items => minirest:ref(<<"returned_sources">>) }, examples => #{ - rules => #{ - summary => <<"Rules">>, + sources => #{ + summary => <<"Sources">>, value => jsx:encode([?EXAMPLE_RULE1]) } } @@ -177,15 +177,15 @@ rules_api() -> } } }, - {"/authorization", Metadata, rules}. + {"/authorization", Metadata, sources}. -rule_api() -> +source_api() -> Metadata = #{ get => #{ - description => "List authorization rules", + description => "List authorization sources", parameters => [ #{ - name => id, + name => type, in => path, schema => #{ type => string @@ -198,10 +198,10 @@ rule_api() -> description => <<"OK">>, content => #{ 'application/json' => #{ - schema => minirest:ref(<<"returned_rules">>), + schema => minirest:ref(<<"returned_sources">>), examples => #{ - rules => #{ - summary => <<"Rules">>, + sources => #{ + summary => <<"Sources">>, value => jsx:encode(?EXAMPLE_RETURNED_RULE1) } } @@ -218,7 +218,7 @@ rule_api() -> summary => <<"Not Found">>, value => #{ code => <<"NOT_FOUND">>, - message => <<"rule xxx not found">> + message => <<"source xxx not found">> } } } @@ -228,10 +228,10 @@ rule_api() -> } }, put => #{ - description => "Update rule", + description => "Update source", parameters => [ #{ - name => id, + name => type, in => path, schema => #{ type => string @@ -242,10 +242,10 @@ rule_api() -> requestBody => #{ content => #{ 'application/json' => #{ - schema => minirest:ref(<<"rules">>), + schema => minirest:ref(<<"sources">>), examples => #{ - simple_rule => #{ - summary => <<"Rules">>, + simple_source => #{ + summary => <<"Sources">>, value => jsx:encode(?EXAMPLE_RULE1) } } @@ -264,7 +264,7 @@ rule_api() -> summary => <<"Not Found">>, value => #{ code => <<"NOT_FOUND">>, - message => <<"rule xxx not found">> + message => <<"source xxx not found">> } } } @@ -291,7 +291,7 @@ rule_api() -> } }, delete => #{ - description => "Delete rule", + description => "Delete source", parameters => [ #{ name => id, @@ -324,15 +324,15 @@ rule_api() -> } } }, - {"/authorization/:id", Metadata, rule}. + {"/authorization/:type", Metadata, source}. -move_rule_api() -> +move_source_api() -> Metadata = #{ post => #{ - description => "Change the order of rules", + description => "Change the order of sources", parameters => [ #{ - name => id, + name => type, in => path, schema => #{ type => string @@ -381,15 +381,13 @@ move_rule_api() -> }, <<"404">> => #{ description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), + content => #{ 'application/json' => #{ schema => minirest:ref(<<"error">>), examples => #{ example1 => #{ summary => <<"Not Found">>, value => #{ code => <<"NOT_FOUND">>, - message => <<"rule xxx not found">> + message => <<"source xxx not found">> } } } @@ -416,56 +414,54 @@ move_rule_api() -> } } }, - {"/authorization/:id/move", Metadata, move_rule}. + {"/authorization/:type/move", Metadata, move_source}. -rules(get, #{query_string := Query}) -> - Rules = lists:foldl(fun (#{type := _Type, enable := true, config := #{server := Server} = Config, annotations := #{id := Id}} = Rule, AccIn) -> - NRule = case emqx_resource:health_check(Id) of +sources(get, #{query_string := Query}) -> + Sources = lists:foldl(fun (#{type := _Type, enable := true, config := #{server := Server} = Config, annotations := #{id := Id}} = Source, AccIn) -> + NSource = case emqx_resource:health_check(Id) of ok -> - Rule#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, - annotations => #{id => Id, - status => healthy}}; + Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, + annotations => #{id => Id, + status => healthy}}; _ -> - Rule#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, - annotations => #{id => Id, - status => unhealthy}} + Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, + annotations => #{id => Id, + status => unhealthy}} end, - lists:append(AccIn, [NRule]); - (#{type := _Type, enable := true, annotations := #{id := Id}} = Rule, AccIn) -> - NRule = case emqx_resource:health_check(Id) of + lists:append(AccIn, [NSource]); + (#{type := _Type, enable := true, annotations := #{id := Id}} = Source, AccIn) -> + NSource = case emqx_resource:health_check(Id) of ok -> - Rule#{annotations => #{id => Id, - status => healthy}}; + Source#{annotations => #{status => healthy}}; _ -> - Rule#{annotations => #{id => Id, - status => unhealthy}} + Source#{annotations => #{status => unhealthy}} end, - lists:append(AccIn, [NRule]); - (Rule, AccIn) -> - lists:append(AccIn, [Rule]) + lists:append(AccIn, [NSource]); + (Source, AccIn) -> + lists:append(AccIn, [Source]) end, [], emqx_authz:lookup()), case maps:is_key(<<"page">>, Query) andalso maps:is_key(<<"limit">>, Query) of true -> Page = maps:get(<<"page">>, Query), Limit = maps:get(<<"limit">>, Query), Index = (binary_to_integer(Page) - 1) * binary_to_integer(Limit), - {_, Rules1} = lists:split(Index, Rules), - case binary_to_integer(Limit) < length(Rules1) of + {_, Sources1} = lists:split(Index, Sources), + case binary_to_integer(Limit) < length(Sources1) of true -> - {Rules2, _} = lists:split(binary_to_integer(Limit), Rules1), - {200, #{rules => Rules2}}; - false -> {200, #{rules => Rules1}} + {Sources2, _} = lists:split(binary_to_integer(Limit), Sources1), + {200, #{sources => Sources2}}; + false -> {200, #{sources => Sources1}} end; - false -> {200, #{rules => Rules}} + false -> {200, #{sources => Sources}} end; -rules(post, #{body := RawConfig}) -> +sources(post, #{body := RawConfig}) -> case emqx_authz:update(head, [RawConfig]) of {ok, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} end; -rules(put, #{body := RawConfig}) -> +sources(put, #{body := RawConfig}) -> case emqx_authz:update(replace, RawConfig) of {ok, _} -> {204}; {error, Reason} -> @@ -473,56 +469,57 @@ rules(put, #{body := RawConfig}) -> messgae => atom_to_binary(Reason)}} end. -rule(get, #{bindings := #{id := Id}}) -> - case emqx_authz:lookup(Id) of +source(get, #{bindings := #{type := Type}}) -> + case emqx_authz:lookup(Type) of {error, Reason} -> {404, #{messgae => atom_to_binary(Reason)}}; - #{type := file} = Rule -> {200, Rule}; - #{config := #{server := Server} = Config} = Rule -> + #{enable := false} = Source -> {200, Source}; + #{type := file} = Source -> {200, Source}; + #{config := #{server := Server, + annotations := #{id := Id} + } = Config} = Source -> case emqx_resource:health_check(Id) of ok -> - {200, Rule#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, - annotations => #{id => Id, - status => healthy}}}; + {200, Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, + annotations => #{status => healthy}}}; _ -> - {200, Rule#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, - annotations => #{id => Id, - status => unhealthy}}} + {200, Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, + annotations => #{status => unhealthy}}} end; - Rule -> + #{config := #{annotations := #{id := Id}}} = Source -> case emqx_resource:health_check(Id) of ok -> - {200, Rule#{annotations => #{id => Id, - status => healthy}}}; + {200, Source#{annotations => #{status => healthy}}}; _ -> - {200, Rule#{annotations => #{id => Id, - status => unhealthy}}} + {200, Source#{annotations => #{status => unhealthy}}} end end; -rule(put, #{bindings := #{id := RuleId}, body := RawConfig}) -> - case emqx_authz:update({replace_once, RuleId}, RawConfig) of +source(put, #{bindings := #{type := Type}, body := RawConfig}) -> + case emqx_authz:update({replace_once, Type}, RawConfig) of {ok, _} -> {204}; - {error, not_found_rule} -> + {error, not_found_source} -> {404, #{code => <<"NOT_FOUND">>, - messgae => <<"rule ", RuleId/binary, " not found">>}}; + messgae => <<"source ", Type/binary, " not found">>}}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} end; -rule(delete, #{bindings := #{id := RuleId}}) -> - case emqx_authz:update({replace_once, RuleId}, #{}) of +source(delete, #{bindings := #{type := Type}}) -> + case emqx_authz:update({delete_once, Type}, #{}) of {ok, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} end. -move_rule(post, #{bindings := #{id := RuleId}, body := Body}) -> - #{<<"position">> := Position} = Body, - case emqx_authz:move(RuleId, Position) of +move_source(post, #{bindings := #{type := Type}, body := #{<<"position">> := Position}}) -> + case emqx_authz:move(Type, Position) of {ok, _} -> {204}; - {error, not_found_rule} -> + {error, not_found_source} -> {404, #{code => <<"NOT_FOUND">>, - messgae => <<"rule ", RuleId/binary, " not found">>}}; + messgae => <<"source ", Type/binary, " not found">>}}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} end. + + + diff --git a/apps/emqx_authz/src/emqx_authz_api_schema.erl b/apps/emqx_authz/src/emqx_authz_api_schema.erl index 64ecc58eb..1bc316986 100644 --- a/apps/emqx_authz/src/emqx_authz_api_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_api_schema.erl @@ -34,11 +34,11 @@ definitions() -> } } } - , minirest:ref(<<"rules">>) + , minirest:ref(<<"sources">>) ] }, Rules = #{ - oneOf => [ minirest:ref(<<"simple_rule">>) + oneOf => [ minirest:ref(<<"simple_source">>) % , minirest:ref(<<"connector_redis">>) ] }, @@ -144,9 +144,9 @@ definitions() -> } } }, - [ #{<<"returned_rules">> => RetruenedRules} - , #{<<"rules">> => Rules} - , #{<<"simple_rule">> => SimpleRule} + [ #{<<"returned_sources">> => RetruenedRules} + , #{<<"sources">> => Rules} + , #{<<"simple_source">> => SimpleRule} , #{<<"principal">> => Principal} , #{<<"principal_username">> => PrincipalUsername} , #{<<"principal_clientid">> => PrincipalClientid} diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index ef7644a65..cee83cd30 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -61,120 +61,132 @@ init_per_testcase(_, Config) -> Config. -define(SOURCE1, #{<<"type">> => <<"http">>, - <<"config">> => #{ - <<"url">> => <<"https://fake.com:443/">>, - <<"headers">> => #{}, - <<"method">> => <<"get">>, - <<"request_timeout">> => 5000} - }). + <<"enable">> => true, + <<"config">> => #{ + <<"url">> => <<"https://fake.com:443/">>, + <<"headers">> => #{}, + <<"method">> => <<"get">>, + <<"request_timeout">> => 5000} + }). -define(SOURCE2, #{<<"type">> => <<"mongo">>, - <<"config">> => #{ - <<"mongo_type">> => <<"single">>, - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"ssl">> => #{<<"enable">> => false}}, - <<"collection">> => <<"fake">>, - <<"find">> => #{<<"a">> => <<"b">>} - }). + <<"enable">> => true, + <<"config">> => #{ + <<"mongo_type">> => <<"single">>, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"ssl">> => #{<<"enable">> => false}}, + <<"collection">> => <<"fake">>, + <<"find">> => #{<<"a">> => <<"b">>} + }). -define(SOURCE3, #{<<"type">> => <<"mysql">>, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"username">> => <<"xx">>, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, - <<"sql">> => <<"abcb">> - }). + <<"enable">> => true, + <<"config">> => #{ + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}}, + <<"sql">> => <<"abcb">> + }). -define(SOURCE4, #{<<"type">> => <<"pgsql">>, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"username">> => <<"xx">>, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, - <<"sql">> => <<"abcb">> - }). + <<"enable">> => true, + <<"config">> => #{ + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}}, + <<"sql">> => <<"abcb">> + }). -define(SOURCE5, #{<<"type">> => <<"redis">>, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => 0, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, - <<"cmd">> => <<"HGETALL mqtt_authz:%u">> - }). + <<"enable">> => true, + <<"config">> => #{ + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => 0, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}}, + <<"cmd">> => <<"HGETALL mqtt_authz:%u">> + }). %%------------------------------------------------------------------------------ %% Testcases %%------------------------------------------------------------------------------ t_update_source(_) -> - {ok, _} = emqx_authz:update(replace, [?SOURCE2]), + {ok, _} = emqx_authz:update(replace, [?SOURCE3]), + {ok, _} = emqx_authz:update(head, [?SOURCE2]), {ok, _} = emqx_authz:update(head, [?SOURCE1]), - {ok, _} = emqx_authz:update(tail, [?SOURCE3]), + {ok, _} = emqx_authz:update(tail, [?SOURCE4]), + {ok, _} = emqx_authz:update(tail, [?SOURCE5]), - ?assertMatch([#{type := http}, #{type := mongo}, #{type := mysql}], emqx:get_config([authorization, sources], [])), + ?assertMatch([ #{type := http, enable := true} + , #{type := mongo, enable := true} + , #{type := mysql, enable := true} + , #{type := pgsql, enable := true} + , #{type := redis, enable := true} + ], emqx:get_config([authorization, sources], [])), - [#{annotations := #{id := Id1}, type := http}, - #{annotations := #{id := Id2}, type := mongo}, - #{annotations := #{id := Id3}, type := mysql} - ] = emqx_authz:lookup(), + {ok, _} = emqx_authz:update({replace_once, http}, ?SOURCE1#{<<"enable">> := false}), + {ok, _} = emqx_authz:update({replace_once, mongo}, ?SOURCE2#{<<"enable">> := false}), + {ok, _} = emqx_authz:update({replace_once, mysql}, ?SOURCE3#{<<"enable">> := false}), + {ok, _} = emqx_authz:update({replace_once, pgsql}, ?SOURCE4#{<<"enable">> := false}), + {ok, _} = emqx_authz:update({replace_once, redis}, ?SOURCE5#{<<"enable">> := false}), - {ok, _} = emqx_authz:update({replace_once, Id1}, ?SOURCE5), - {ok, _} = emqx_authz:update({replace_once, Id3}, ?SOURCE4), - ?assertMatch([#{type := redis}, #{type := mongo}, #{type := pgsql}], emqx:get_config([authorization, sources], [])), - - [#{annotations := #{id := Id1}, type := redis}, - #{annotations := #{id := Id2}, type := mongo}, - #{annotations := #{id := Id3}, type := pgsql} - ] = emqx_authz:lookup(), + ?assertMatch([ #{type := http, enable := false} + , #{type := mongo, enable := false} + , #{type := mysql, enable := false} + , #{type := pgsql, enable := false} + , #{type := redis, enable := false} + ], emqx:get_config([authorization, sources], [])), {ok, _} = emqx_authz:update(replace, []). t_move_source(_) -> {ok, _} = emqx_authz:update(replace, [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5]), - [#{annotations := #{id := Id1}}, - #{annotations := #{id := Id2}}, - #{annotations := #{id := Id3}}, - #{annotations := #{id := Id4}}, - #{annotations := #{id := Id5}} - ] = emqx_authz:lookup(), - - {ok, _} = emqx_authz:move(Id4, <<"top">>), - ?assertMatch([#{annotations := #{id := Id4}}, - #{annotations := #{id := Id1}}, - #{annotations := #{id := Id2}}, - #{annotations := #{id := Id3}}, - #{annotations := #{id := Id5}} + ?assertMatch([ #{type := http} + , #{type := mongo} + , #{type := mysql} + , #{type := pgsql} + , #{type := redis} ], emqx_authz:lookup()), - {ok, _} = emqx_authz:move(Id1, <<"bottom">>), - ?assertMatch([#{annotations := #{id := Id4}}, - #{annotations := #{id := Id2}}, - #{annotations := #{id := Id3}}, - #{annotations := #{id := Id5}}, - #{annotations := #{id := Id1}} + {ok, _} = emqx_authz:move(pgsql, <<"top">>), + ?assertMatch([ #{type := pgsql} + , #{type := http} + , #{type := mongo} + , #{type := mysql} + , #{type := redis} ], emqx_authz:lookup()), - {ok, _} = emqx_authz:move(Id3, #{<<"before">> => Id4}), - ?assertMatch([#{annotations := #{id := Id3}}, - #{annotations := #{id := Id4}}, - #{annotations := #{id := Id2}}, - #{annotations := #{id := Id5}}, - #{annotations := #{id := Id1}} + {ok, _} = emqx_authz:move(http, <<"bottom">>), + ?assertMatch([ #{type := pgsql} + , #{type := mongo} + , #{type := mysql} + , #{type := redis} + , #{type := http} ], emqx_authz:lookup()), - {ok, _} = emqx_authz:move(Id2, #{<<"after">> => Id1}), - ?assertMatch([#{annotations := #{id := Id3}}, - #{annotations := #{id := Id4}}, - #{annotations := #{id := Id5}}, - #{annotations := #{id := Id1}}, - #{annotations := #{id := Id2}} + {ok, _} = emqx_authz:move(mysql, #{<<"before">> => pgsql}), + ?assertMatch([ #{type := mysql} + , #{type := pgsql} + , #{type := mongo} + , #{type := redis} + , #{type := http} ], emqx_authz:lookup()), + + {ok, _} = emqx_authz:move(mongo, #{<<"after">> => http}), + ?assertMatch([ #{type := mysql} + , #{type := pgsql} + , #{type := redis} + , #{type := http} + , #{type := mongo} + ], emqx_authz:lookup()), + ok. diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl index 8d92413b3..c8901af77 100644 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl @@ -38,54 +38,59 @@ -define(BASE_PATH, "api"). -define(SOURCE1, #{<<"type">> => <<"http">>, - <<"config">> => #{ - <<"url">> => <<"https://fake.com:443/">>, - <<"headers">> => #{}, - <<"method">> => <<"get">>, - <<"request_timeout">> => 5000} - }). + <<"enable">> => true, + <<"config">> => #{ + <<"url">> => <<"https://fake.com:443/">>, + <<"headers">> => #{}, + <<"method">> => <<"get">>, + <<"request_timeout">> => 5000} + }). -define(SOURCE2, #{<<"type">> => <<"mongo">>, - <<"config">> => #{ - <<"mongo_type">> => <<"single">>, - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"ssl">> => #{<<"enable">> => false}}, - <<"collection">> => <<"fake">>, - <<"find">> => #{<<"a">> => <<"b">>} - }). + <<"enable">> => true, + <<"config">> => #{ + <<"mongo_type">> => <<"single">>, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"ssl">> => #{<<"enable">> => false}}, + <<"collection">> => <<"fake">>, + <<"find">> => #{<<"a">> => <<"b">>} + }). -define(SOURCE3, #{<<"type">> => <<"mysql">>, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"username">> => <<"xx">>, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, - <<"sql">> => <<"abcb">> - }). + <<"enable">> => true, + <<"config">> => #{ + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}}, + <<"sql">> => <<"abcb">> + }). -define(SOURCE4, #{<<"type">> => <<"pgsql">>, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"username">> => <<"xx">>, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, - <<"sql">> => <<"abcb">> - }). + <<"enable">> => true, + <<"config">> => #{ + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}}, + <<"sql">> => <<"abcb">> + }). -define(SOURCE5, #{<<"type">> => <<"redis">>, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => 0, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, - <<"cmd">> => <<"HGETALL mqtt_authz:%u">> - }). + <<"enable">> => true, + <<"config">> => #{ + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => 0, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}}, + <<"cmd">> => <<"HGETALL mqtt_authz:%u">> + }). all() -> emqx_ct:all(?MODULE). @@ -134,7 +139,7 @@ set_special_configs(emqx_dashboard) -> emqx_config:put([emqx_dashboard], Config), ok; set_special_configs(emqx_authz) -> - emqx_config:put([authorization], #{rules => []}), + emqx_config:put([authorization], #{sources => []}), ok; set_special_configs(_App) -> ok. @@ -145,89 +150,86 @@ set_special_configs(_App) -> t_api(_) -> {ok, 200, Result1} = request(get, uri(["authorization"]), []), - ?assertEqual([], get_rules(Result1)), + ?assertEqual([], get_sources(Result1)), lists:foreach(fun(_) -> {ok, 204, _} = request(post, uri(["authorization"]), ?SOURCE1) end, lists:seq(1, 20)), {ok, 200, Result2} = request(get, uri(["authorization"]), []), - ?assertEqual(20, length(get_rules(Result2))), + ?assertEqual(20, length(get_sources(Result2))), lists:foreach(fun(Page) -> Query = "?page=" ++ integer_to_list(Page) ++ "&&limit=10", Url = uri(["authorization" ++ Query]), {ok, 200, Result} = request(get, Url, []), - ?assertEqual(10, length(get_rules(Result))) + ?assertEqual(10, length(get_sources(Result))) end, lists:seq(1, 2)), {ok, 204, _} = request(put, uri(["authorization"]), [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4]), {ok, 200, Result3} = request(get, uri(["authorization"]), []), - Rules = get_rules(Result3), - ?assertEqual(4, length(Rules)), + Sources = get_sources(Result3), ?assertMatch([ #{<<"type">> := <<"http">>} , #{<<"type">> := <<"mongo">>} , #{<<"type">> := <<"mysql">>} , #{<<"type">> := <<"pgsql">>} - ], Rules), + ], Sources), - #{<<"annotations">> := #{<<"id">> := Id}} = lists:nth(2, Rules), + {ok, 204, _} = request(put, uri(["authorization", "http"]), ?SOURCE1#{<<"enable">> := false}), - {ok, 204, _} = request(put, uri(["authorization", binary_to_list(Id)]), ?SOURCE5), + {ok, 200, Result4} = request(get, uri(["authorization", "http"]), []), + ?assertMatch(#{<<"type">> := <<"http">>, <<"enable">> := false}, jsx:decode(Result4)), - {ok, 200, Result4} = request(get, uri(["authorization", binary_to_list(Id)]), []), - ?assertMatch(#{<<"type">> := <<"redis">>}, jsx:decode(Result4)), - - lists:foreach(fun(#{<<"annotations">> := #{<<"id">> := Id0}}) -> - {ok, 204, _} = request(delete, uri(["authorization", binary_to_list(Id0)]), []) - end, Rules), + lists:foreach(fun(#{<<"type">> := Type}) -> + {ok, 204, _} = request(delete, uri(["authorization", binary_to_list(Type)]), []) + end, Sources), {ok, 200, Result5} = request(get, uri(["authorization"]), []), - ?assertEqual([], get_rules(Result5)), + ?assertEqual([], get_sources(Result5)), ok. -t_move_rule(_) -> +t_move_source(_) -> {ok, _} = emqx_authz:update(replace, [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5]), - [#{annotations := #{id := Id1}}, - #{annotations := #{id := Id2}}, - #{annotations := #{id := Id3}}, - #{annotations := #{id := Id4}}, - #{annotations := #{id := Id5}} - ] = emqx_authz:lookup(), + ?assertMatch([ #{type := http} + , #{type := mongo} + , #{type := mysql} + , #{type := pgsql} + , #{type := redis} + ], emqx_authz:lookup()), - {ok, 204, _} = request(post, uri(["authorization", Id4, "move"]), + {ok, 204, _} = request(post, uri(["authorization", "pgsql", "move"]), #{<<"position">> => <<"top">>}), - ?assertMatch([#{annotations := #{id := Id4}}, - #{annotations := #{id := Id1}}, - #{annotations := #{id := Id2}}, - #{annotations := #{id := Id3}}, - #{annotations := #{id := Id5}} + ?assertMatch([ #{type := pgsql} + , #{type := http} + , #{type := mongo} + , #{type := mysql} + , #{type := redis} ], emqx_authz:lookup()), - {ok, 204, _} = request(post, uri(["authorization", Id1, "move"]), + {ok, 204, _} = request(post, uri(["authorization", "http", "move"]), #{<<"position">> => <<"bottom">>}), - ?assertMatch([#{annotations := #{id := Id4}}, - #{annotations := #{id := Id2}}, - #{annotations := #{id := Id3}}, - #{annotations := #{id := Id5}}, - #{annotations := #{id := Id1}} + ?assertMatch([ #{type := pgsql} + , #{type := mongo} + , #{type := mysql} + , #{type := redis} + , #{type := http} ], emqx_authz:lookup()), - {ok, 204, _} = request(post, uri(["authorization", Id3, "move"]), - #{<<"position">> => #{<<"before">> => Id4}}), - ?assertMatch([#{annotations := #{id := Id3}}, - #{annotations := #{id := Id4}}, - #{annotations := #{id := Id2}}, - #{annotations := #{id := Id5}}, - #{annotations := #{id := Id1}} + {ok, 204, _} = request(post, uri(["authorization", "mysql", "move"]), + #{<<"position">> => #{<<"before">> => <<"pgsql">>}}), + ?assertMatch([ #{type := mysql} + , #{type := pgsql} + , #{type := mongo} + , #{type := redis} + , #{type := http} ], emqx_authz:lookup()), - {ok, 204, _} = request(post, uri(["authorization", Id2, "move"]), - #{<<"position">> => #{<<"after">> => Id1}}), - ?assertMatch([#{annotations := #{id := Id3}}, - #{annotations := #{id := Id4}}, - #{annotations := #{id := Id5}}, - #{annotations := #{id := Id1}}, - #{annotations := #{id := Id2}} + {ok, 204, _} = request(post, uri(["authorization", "mongo", "move"]), + #{<<"position">> => #{<<"after">> => <<"http">>}}), + ?assertMatch([ #{type := mysql} + , #{type := pgsql} + , #{type := redis} + , #{type := http} + , #{type := mongo} ], emqx_authz:lookup()), ok. @@ -256,8 +258,8 @@ uri(Parts) when is_list(Parts) -> NParts = [E || E <- Parts], ?HOST ++ filename:join([?BASE_PATH, ?API_VERSION | NParts]). -get_rules(Result) -> - maps:get(<<"rules">>, jsx:decode(Result), []). +get_sources(Result) -> + maps:get(<<"sources">>, jsx:decode(Result), []). auth_header_() -> Username = <<"admin">>, From c0eaa30064502880557970f4c0c57be182c49f96 Mon Sep 17 00:00:00 2001 From: Rory Z Date: Tue, 31 Aug 2021 10:29:28 +0800 Subject: [PATCH 003/109] chore(emqx_authz): change api path --- apps/emqx_authz/src/emqx_authz_api.erl | 7 ++--- apps/emqx_authz/test/emqx_authz_api_SUITE.erl | 28 +++++++++---------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz_api.erl b/apps/emqx_authz/src/emqx_authz_api.erl index ff5217426..dc8694c3f 100644 --- a/apps/emqx_authz/src/emqx_authz_api.erl +++ b/apps/emqx_authz/src/emqx_authz_api.erl @@ -137,7 +137,6 @@ sources_api() -> } }, put => #{ - description => "Update all sources", requestBody => #{ content => #{ @@ -177,7 +176,7 @@ sources_api() -> } } }, - {"/authorization", Metadata, sources}. + {"/authorization/sources", Metadata, sources}. source_api() -> Metadata = #{ @@ -324,7 +323,7 @@ source_api() -> } } }, - {"/authorization/:type", Metadata, source}. + {"/authorization/sources/:type", Metadata, source}. move_source_api() -> Metadata = #{ @@ -414,7 +413,7 @@ move_source_api() -> } } }, - {"/authorization/:type/move", Metadata, move_source}. + {"/authorization/sources/:type/move", Metadata, move_source}. sources(get, #{query_string := Query}) -> Sources = lists:foldl(fun (#{type := _Type, enable := true, config := #{server := Server} = Config, annotations := #{id := Id}} = Source, AccIn) -> diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl index c8901af77..946b1a30b 100644 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_SUITE.erl @@ -149,25 +149,25 @@ set_special_configs(_App) -> %%------------------------------------------------------------------------------ t_api(_) -> - {ok, 200, Result1} = request(get, uri(["authorization"]), []), + {ok, 200, Result1} = request(get, uri(["authorization", "sources"]), []), ?assertEqual([], get_sources(Result1)), lists:foreach(fun(_) -> - {ok, 204, _} = request(post, uri(["authorization"]), ?SOURCE1) + {ok, 204, _} = request(post, uri(["authorization", "sources"]), ?SOURCE1) end, lists:seq(1, 20)), - {ok, 200, Result2} = request(get, uri(["authorization"]), []), + {ok, 200, Result2} = request(get, uri(["authorization", "sources"]), []), ?assertEqual(20, length(get_sources(Result2))), lists:foreach(fun(Page) -> Query = "?page=" ++ integer_to_list(Page) ++ "&&limit=10", - Url = uri(["authorization" ++ Query]), + Url = uri(["authorization/sources" ++ Query]), {ok, 200, Result} = request(get, Url, []), ?assertEqual(10, length(get_sources(Result))) end, lists:seq(1, 2)), - {ok, 204, _} = request(put, uri(["authorization"]), [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4]), + {ok, 204, _} = request(put, uri(["authorization", "sources"]), [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4]), - {ok, 200, Result3} = request(get, uri(["authorization"]), []), + {ok, 200, Result3} = request(get, uri(["authorization", "sources"]), []), Sources = get_sources(Result3), ?assertMatch([ #{<<"type">> := <<"http">>} , #{<<"type">> := <<"mongo">>} @@ -175,15 +175,15 @@ t_api(_) -> , #{<<"type">> := <<"pgsql">>} ], Sources), - {ok, 204, _} = request(put, uri(["authorization", "http"]), ?SOURCE1#{<<"enable">> := false}), + {ok, 204, _} = request(put, uri(["authorization", "sources", "http"]), ?SOURCE1#{<<"enable">> := false}), - {ok, 200, Result4} = request(get, uri(["authorization", "http"]), []), + {ok, 200, Result4} = request(get, uri(["authorization", "sources", "http"]), []), ?assertMatch(#{<<"type">> := <<"http">>, <<"enable">> := false}, jsx:decode(Result4)), lists:foreach(fun(#{<<"type">> := Type}) -> - {ok, 204, _} = request(delete, uri(["authorization", binary_to_list(Type)]), []) + {ok, 204, _} = request(delete, uri(["authorization", "sources", binary_to_list(Type)]), []) end, Sources), - {ok, 200, Result5} = request(get, uri(["authorization"]), []), + {ok, 200, Result5} = request(get, uri(["authorization", "sources"]), []), ?assertEqual([], get_sources(Result5)), ok. @@ -196,7 +196,7 @@ t_move_source(_) -> , #{type := redis} ], emqx_authz:lookup()), - {ok, 204, _} = request(post, uri(["authorization", "pgsql", "move"]), + {ok, 204, _} = request(post, uri(["authorization", "sources", "pgsql", "move"]), #{<<"position">> => <<"top">>}), ?assertMatch([ #{type := pgsql} , #{type := http} @@ -205,7 +205,7 @@ t_move_source(_) -> , #{type := redis} ], emqx_authz:lookup()), - {ok, 204, _} = request(post, uri(["authorization", "http", "move"]), + {ok, 204, _} = request(post, uri(["authorization", "sources", "http", "move"]), #{<<"position">> => <<"bottom">>}), ?assertMatch([ #{type := pgsql} , #{type := mongo} @@ -214,7 +214,7 @@ t_move_source(_) -> , #{type := http} ], emqx_authz:lookup()), - {ok, 204, _} = request(post, uri(["authorization", "mysql", "move"]), + {ok, 204, _} = request(post, uri(["authorization", "sources", "mysql", "move"]), #{<<"position">> => #{<<"before">> => <<"pgsql">>}}), ?assertMatch([ #{type := mysql} , #{type := pgsql} @@ -223,7 +223,7 @@ t_move_source(_) -> , #{type := http} ], emqx_authz:lookup()), - {ok, 204, _} = request(post, uri(["authorization", "mongo", "move"]), + {ok, 204, _} = request(post, uri(["authorization", "sources", "mongo", "move"]), #{<<"position">> => #{<<"after">> => <<"http">>}}), ?assertMatch([ #{type := mysql} , #{type := pgsql} From ca327b7c5570a45265336c8edf7cff63c7906353 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Sun, 29 Aug 2021 15:23:18 +0800 Subject: [PATCH 004/109] refactor(listener): GET /listeners API returns full config of listeners --- apps/emqx/src/emqx_map_lib.erl | 46 +++++++---- .../src/emqx_mgmt_api_configs.erl | 2 +- .../src/emqx_mgmt_api_listeners.erl | 82 ++++++++++--------- 3 files changed, 74 insertions(+), 56 deletions(-) diff --git a/apps/emqx/src/emqx_map_lib.erl b/apps/emqx/src/emqx_map_lib.erl index 0486c10da..529c22816 100644 --- a/apps/emqx/src/emqx_map_lib.erl +++ b/apps/emqx/src/emqx_map_lib.erl @@ -24,13 +24,16 @@ , safe_atom_key_map/1 , unsafe_atom_key_map/1 , jsonable_map/1 - , jsonable_value/1 - , deep_convert/2 + , jsonable_map/2 + , binary_string/1 + , deep_convert/3 ]). -export_type([config_key/0, config_key_path/0]). -type config_key() :: atom() | binary(). -type config_key_path() :: [config_key()]. +-type convert_fun() :: fun((K::any(), V::any(), Args::list()) -> + {K1::any(), V1::any()} | drop). %%----------------------------------------------------------------- -spec deep_get(config_key_path(), map()) -> term(). @@ -100,15 +103,17 @@ deep_merge(BaseMap, NewMap) -> end, #{}, BaseMap), maps:merge(MergedBase, maps:with(NewKeys, NewMap)). --spec deep_convert(map(), fun((K::any(), V::any()) -> {K1::any(), V1::any()})) -> map(). -deep_convert(Map, ConvFun) when is_map(Map) -> +-spec deep_convert(map(), convert_fun(), Args::list()) -> map(). +deep_convert(Map, ConvFun, Args) when is_map(Map) -> maps:fold(fun(K, V, Acc) -> - {K1, V1} = ConvFun(K, deep_convert(V, ConvFun)), - Acc#{K1 => V1} + case apply(ConvFun, [K, deep_convert(V, ConvFun, Args) | Args]) of + drop -> Acc; + {K1, V1} -> Acc#{K1 => V1} + end end, #{}, Map); -deep_convert(ListV, ConvFun) when is_list(ListV) -> - [deep_convert(V, ConvFun) || V <- ListV]; -deep_convert(Val, _) -> Val. +deep_convert(ListV, ConvFun, Args) when is_list(ListV) -> + [deep_convert(V, ConvFun, Args) || V <- ListV]; +deep_convert(Val, _, _Args) -> Val. -spec unsafe_atom_key_map(#{binary() | atom() => any()}) -> #{atom() => any()}. unsafe_atom_key_map(Map) -> @@ -120,17 +125,24 @@ safe_atom_key_map(Map) -> -spec jsonable_map(map() | list()) -> map() | list(). jsonable_map(Map) -> - deep_convert(Map, fun(K, V) -> - {jsonable_value(K), jsonable_value(V)} - end). + jsonable_map(Map, fun(K, V) -> {K, V} end). -jsonable_value([]) -> []; -jsonable_value(Val) when is_list(Val) -> +jsonable_map(Map, JsonableFun) -> + deep_convert(Map, fun binary_string_kv/3, [JsonableFun]). + +binary_string_kv(K, V, JsonableFun) -> + case JsonableFun(K, V) of + drop -> drop; + {K1, V1} -> {binary_string(K1), binary_string(V1)} + end. + +binary_string([]) -> []; +binary_string(Val) when is_list(Val) -> case io_lib:printable_unicode_list(Val) of true -> unicode:characters_to_binary(Val); - false -> Val + false -> [binary_string(V) || V <- Val] end; -jsonable_value(Val) -> +binary_string(Val) -> Val. %%--------------------------------------------------------------------------- @@ -138,4 +150,4 @@ covert_keys_to_atom(BinKeyMap, Conv) -> deep_convert(BinKeyMap, fun (K, V) when is_atom(K) -> {K, V}; (K, V) when is_binary(K) -> {Conv(K), V} - end). + end, []). diff --git a/apps/emqx_management/src/emqx_mgmt_api_configs.erl b/apps/emqx_management/src/emqx_mgmt_api_configs.erl index ba864fa89..a859f2002 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_configs.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_configs.erl @@ -188,7 +188,7 @@ gen_schema(_Conf) -> #{type => string}. with_default_value(Type, Value) -> - Type#{example => emqx_map_lib:jsonable_value(Value)}. + Type#{example => emqx_map_lib:binary_string(Value)}. path_join(Path) -> path_join(Path, "/"). diff --git a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl index e13549a86..4bad6a26b 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl @@ -24,14 +24,9 @@ , list_listeners_by_id/2 , list_listeners_on_node/2 , get_listener_by_id_on_node/2 - , manage_listeners/2]). - --import(emqx_mgmt_util, [ schema/1 - , object_schema/2 - , object_array_schema/2 - , error_schema/2 - , properties/1 - ]). + , manage_listeners/2 + , jsonable_resp/2 + ]). -export([format/1]). @@ -53,17 +48,23 @@ api_spec() -> [] }. -properties() -> - properties([ - {node, string, <<"Node">>}, - {id, string, <<"Identifier">>}, - {acceptors, integer, <<"Number of Acceptor process">>}, - {max_conn, integer, <<"Maximum number of allowed connection">>}, - {type, string, <<"Listener type">>}, - {listen_on, string, <<"Listener port">>}, - {running, boolean, <<"Open or close">>}, - {auth, boolean, <<"Has auth">>} - ]). +-define(TYPES, [tcp, ssl, ws, wss, quic]). +req_schema() -> + Schema = [emqx_mgmt_api_configs:gen_schema( + emqx:get_raw_config([listeners, T, default], #{})) + || T <- ?TYPES], + #{oneOf => Schema}. + +resp_schema() -> + #{oneOf := Schema} = req_schema(), + AddMetadata = fun(Prop) -> + Prop#{running => #{type => boolean}, + id => #{type => string}, + node => #{type => string}} + end, + Schema1 = [S#{properties => AddMetadata(Prop)} + || S = #{properties := Prop} <- Schema], + #{oneOf => Schema1}. api_list_listeners() -> Metadata = #{ @@ -71,7 +72,7 @@ api_list_listeners() -> description => <<"List listeners from all nodes in the cluster">>, responses => #{ <<"200">> => - object_array_schema(properties(), <<"List listeners successfully">>)}}}, + emqx_mgmt_util:array_schema(resp_schema(), <<"List listeners successfully">>)}}}, {"/listeners", Metadata, list_listeners}. api_list_listeners_by_id() -> @@ -81,9 +82,9 @@ api_list_listeners_by_id() -> parameters => [param_path_id()], responses => #{ <<"404">> => - error_schema(?LISTENER_NOT_FOUND, ['BAD_LISTENER_ID']), + emqx_mgmt_util:error_schema(?LISTENER_NOT_FOUND, ['BAD_LISTENER_ID']), <<"200">> => - object_array_schema(properties(), <<"List listeners successfully">>)}}}, + emqx_mgmt_util:array_schema(resp_schema(), <<"List listeners successfully">>)}}}, {"/listeners/:id", Metadata, list_listeners_by_id}. api_list_listeners_on_node() -> @@ -92,7 +93,7 @@ api_list_listeners_on_node() -> description => <<"List listeners in one node">>, parameters => [param_path_node()], responses => #{ - <<"200">> => object_schema(properties(), <<"List listeners successfully">>)}}}, + <<"200">> => emqx_mgmt_util:object_schema(resp_schema(), <<"List listeners successfully">>)}}}, {"/nodes/:node/listeners", Metadata, list_listeners_on_node}. api_get_listener_by_id_on_node() -> @@ -102,10 +103,10 @@ api_get_listener_by_id_on_node() -> parameters => [param_path_node(), param_path_id()], responses => #{ <<"404">> => - error_schema(?NODE_LISTENER_NOT_FOUND, + emqx_mgmt_util:error_schema(?NODE_LISTENER_NOT_FOUND, ['BAD_NODE_NAME', 'BAD_LISTENER_ID']), <<"200">> => - object_schema(properties(), <<"Get listener successfully">>)}}}, + emqx_mgmt_util:object_schema(resp_schema(), <<"Get listener successfully">>)}}}, {"/nodes/:node/listeners/:id", Metadata, get_listener_by_id_on_node}. api_manage_listeners() -> @@ -116,9 +117,9 @@ api_manage_listeners() -> param_path_id(), param_path_operation()], responses => #{ - <<"500">> => error_schema(<<"Operation Failed">>, ['INTERNAL_ERROR']), - <<"200">> => schema(<<"Operation success">>)}}}, - {"/listeners/:id/:operation", Metadata, manage_listeners}. + <<"500">> => emqx_mgmt_util:error_schema(<<"Operation Failed">>, ['INTERNAL_ERROR']), + <<"200">> => emqx_mgmt_util:schema(<<"Operation success">>)}}}, + {"/listeners/:id/operation/:operation", Metadata, manage_listeners}. api_manage_listeners_on_node() -> Metadata = #{ @@ -129,9 +130,9 @@ api_manage_listeners_on_node() -> param_path_id(), param_path_operation()], responses => #{ - <<"500">> => error_schema(<<"Operation Failed">>, ['INTERNAL_ERROR']), - <<"200">> => schema(<<"Operation success">>)}}}, - {"/nodes/:node/listeners/:id/:operation", Metadata, manage_listeners}. + <<"500">> => emqx_mgmt_util:error_schema(<<"Operation Failed">>, ['INTERNAL_ERROR']), + <<"200">> => emqx_mgmt_util:schema(<<"Operation success">>)}}}, + {"/nodes/:node/listeners/:id/operation/:operation", Metadata, manage_listeners}. %%%============================================================================================== %% parameters @@ -247,16 +248,12 @@ format({error, Reason}) -> {error, Reason}; format({ID, Conf}) -> - {Type, _Name} = emqx_listeners:parse_listener_id(ID), - #{ + emqx_map_lib:jsonable_map(Conf#{ id => ID, node => maps:get(node, Conf), - acceptors => maps:get(acceptors, Conf), - max_conn => maps:get(max_connections, Conf), - type => Type, - listen_on => list_to_binary(esockd:to_string(maps:get(bind, Conf))), running => trans_running(Conf) - }. + }, fun ?MODULE:jsonable_resp/2). + trans_running(Conf) -> case maps:get(running, Conf) of {error, _} -> @@ -265,6 +262,15 @@ trans_running(Conf) -> Running end. +jsonable_resp(bind, Port) when is_integer(Port) -> + {bind, Port}; +jsonable_resp(bind, {Addr, Port}) when is_tuple(Addr); is_integer(Port)-> + {bind, inet:ntoa(Addr) ++ ":" ++ integer_to_list(Port)}; +jsonable_resp(user_lookup_fun, _) -> + drop; +jsonable_resp(K, V) -> + {K, V}. + atom(B) when is_binary(B) -> binary_to_atom(B, utf8); atom(S) when is_list(S) -> list_to_atom(S); atom(A) when is_atom(A) -> A. From 05fc6d9e45f8b30d37072d291d73ab9f7fd749dd Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 30 Aug 2021 09:48:20 +0800 Subject: [PATCH 005/109] fix(dialyzer): bad function spec for emqx_map_lib:deep_convert/3 --- apps/emqx/src/emqx_map_lib.erl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/emqx/src/emqx_map_lib.erl b/apps/emqx/src/emqx_map_lib.erl index 529c22816..de2b41b32 100644 --- a/apps/emqx/src/emqx_map_lib.erl +++ b/apps/emqx/src/emqx_map_lib.erl @@ -32,8 +32,7 @@ -export_type([config_key/0, config_key_path/0]). -type config_key() :: atom() | binary(). -type config_key_path() :: [config_key()]. --type convert_fun() :: fun((K::any(), V::any(), Args::list()) -> - {K1::any(), V1::any()} | drop). +-type convert_fun() :: fun((...) -> {K1::any(), V1::any()} | drop). %%----------------------------------------------------------------- -spec deep_get(config_key_path(), map()) -> term(). From 8c36b7879f08594d3683928ff906014b11b25210 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Sun, 29 Aug 2021 16:03:17 +0800 Subject: [PATCH 006/109] feat(listeners): APIs for updating the listener --- apps/emqx/src/emqx_listeners.erl | 17 +++-- apps/emqx_management/src/emqx_mgmt.erl | 14 +++- .../src/emqx_mgmt_api_listeners.erl | 70 +++++++++++++------ 3 files changed, 75 insertions(+), 26 deletions(-) diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 2d3357f37..d9670b858 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -265,12 +265,12 @@ format_addr({Addr, Port}) when is_tuple(Addr) -> io_lib:format("~s:~w", [inet:ntoa(Addr), Port]). listener_id(Type, ListenerName) -> - list_to_atom(lists:append([atom_to_list(Type), ":", atom_to_list(ListenerName)])). + list_to_atom(lists:append([str(Type), ":", str(ListenerName)])). parse_listener_id(Id) -> try - [Zone, Listen] = string:split(atom_to_list(Id), ":", leading), - {list_to_existing_atom(Zone), list_to_existing_atom(Listen)} + [Type, Name] = string:split(str(Id), ":", leading), + {list_to_existing_atom(Type), list_to_atom(Name)} catch _ : _ -> error({invalid_listener_id, Id}) end. @@ -291,8 +291,8 @@ tcp_opts(Opts) -> foreach_listeners(Do) -> lists:foreach( - fun({ZoneName, LName, LConf}) -> - Do(ZoneName, LName, LConf) + fun({Type, LName, LConf}) -> + Do(Type, LName, LConf) end, do_list()). has_enabled_listener_conf_by_type(Type) -> @@ -307,3 +307,10 @@ apply_on_listener(ListenerId, Do) -> {not_found, _, _} -> error({listener_config_not_found, Type, ListenerName}); {ok, Conf} -> Do(Type, ListenerName, Conf) end. + +str(A) when is_atom(A) -> + atom_to_list(A); +str(B) when is_binary(B) -> + binary_to_list(B); +str(S) when is_list(S) -> + S. diff --git a/apps/emqx_management/src/emqx_mgmt.erl b/apps/emqx_management/src/emqx_mgmt.erl index 02bb8662c..4a628d994 100644 --- a/apps/emqx_management/src/emqx_mgmt.erl +++ b/apps/emqx_management/src/emqx_mgmt.erl @@ -90,6 +90,8 @@ , list_listeners_by_id/1 , get_listener/2 , manage_listener/2 + , update_listener/2 + , update_listener/3 ]). %% Alarms @@ -473,7 +475,7 @@ list_listeners() -> lists:append([list_listeners(Node) || Node <- ekka_mnesia:running_nodes()]). list_listeners(Node) when Node =:= node() -> - [{Id, maps:put(node, Node, Conf)} || {Id, Conf} <- emqx_listeners:list()]; + [Conf#{node => Node, id => Id} || {Id, Conf} <- emqx_listeners:list()]; list_listeners(Node) -> rpc_call(Node, list_listeners, [Node]). @@ -501,6 +503,16 @@ manage_listener(Operation, #{id := ID, node := Node}) when Node =:= node()-> manage_listener(Operation, Param = #{node := Node}) -> rpc_call(Node, manage_listener, [Operation, Param]). +update_listener(Id, Config) -> + [update_listener(Node, Id, Config) || Node <- ekka_mnesia:running_nodes()]. + +update_listener(Node, Id, Config) when Node =:= node() -> + {Type, Name} = emqx_listeners:parse_listener_id(Id), + {ok, #{raw_config := RawConf}} = emqx:update_config([listeners, Type, Name], Config, #{}), + RawConf#{node => Node, id => Id, running => true}; +update_listener(Node, Id, Config) -> + rpc_call(Node, update_listener, [Node, Id, Config]). + %%-------------------------------------------------------------------- %% Get Alarms %%-------------------------------------------------------------------- diff --git a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl index 4bad6a26b..f1749b1d9 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl @@ -21,9 +21,9 @@ -export([api_spec/0]). -export([ list_listeners/2 - , list_listeners_by_id/2 + , list_update_listeners_by_id/2 , list_listeners_on_node/2 - , get_listener_by_id_on_node/2 + , get_update_listener_by_id_on_node/2 , manage_listeners/2 , jsonable_resp/2 ]). @@ -39,10 +39,10 @@ api_spec() -> { [ api_list_listeners(), - api_list_listeners_by_id(), + api_list_update_listeners_by_id(), api_manage_listeners(), api_list_listeners_on_node(), - api_get_listener_by_id_on_node(), + api_get_update_listener_by_id_on_node(), api_manage_listeners_on_node() ], [] @@ -75,7 +75,7 @@ api_list_listeners() -> emqx_mgmt_util:array_schema(resp_schema(), <<"List listeners successfully">>)}}}, {"/listeners", Metadata, list_listeners}. -api_list_listeners_by_id() -> +api_list_update_listeners_by_id() -> Metadata = #{ get => #{ description => <<"List listeners by a given Id from all nodes in the cluster">>, @@ -84,8 +84,18 @@ api_list_listeners_by_id() -> <<"404">> => emqx_mgmt_util:error_schema(?LISTENER_NOT_FOUND, ['BAD_LISTENER_ID']), <<"200">> => - emqx_mgmt_util:array_schema(resp_schema(), <<"List listeners successfully">>)}}}, - {"/listeners/:id", Metadata, list_listeners_by_id}. + emqx_mgmt_util:array_schema(resp_schema(), <<"List listeners successfully">>)}}, + put => #{ + description => <<"Create or update listeners by a given Id to all nodes in the cluster">>, + parameters => [param_path_id()], + requestBody => emqx_mgmt_util:schema(req_schema(), <<"Listener Config">>), + responses => #{ + <<"404">> => + emqx_mgmt_util:error_schema(?LISTENER_NOT_FOUND, ['BAD_LISTENER_ID']), + <<"200">> => + emqx_mgmt_util:array_schema(resp_schema(), <<"List listeners successfully">>)}} + }, + {"/listeners/:id", Metadata, list_update_listeners_by_id}. api_list_listeners_on_node() -> Metadata = #{ @@ -96,7 +106,7 @@ api_list_listeners_on_node() -> <<"200">> => emqx_mgmt_util:object_schema(resp_schema(), <<"List listeners successfully">>)}}}, {"/nodes/:node/listeners", Metadata, list_listeners_on_node}. -api_get_listener_by_id_on_node() -> +api_get_update_listener_by_id_on_node() -> Metadata = #{ get => #{ description => <<"Get a listener by a given Id on a specific node">>, @@ -106,8 +116,19 @@ api_get_listener_by_id_on_node() -> emqx_mgmt_util:error_schema(?NODE_LISTENER_NOT_FOUND, ['BAD_NODE_NAME', 'BAD_LISTENER_ID']), <<"200">> => - emqx_mgmt_util:object_schema(resp_schema(), <<"Get listener successfully">>)}}}, - {"/nodes/:node/listeners/:id", Metadata, get_listener_by_id_on_node}. + emqx_mgmt_util:object_schema(resp_schema(), <<"Get listener successfully">>)}}, + put => #{ + description => <<"Create or update a listener by a given Id on a specific node">>, + parameters => [param_path_node(), param_path_id()], + requestBody => emqx_mgmt_util:schema(req_schema(), <<"Listener Config">>), + responses => #{ + <<"404">> => + emqx_mgmt_util:error_schema(?NODE_LISTENER_NOT_FOUND, + ['BAD_NODE_NAME', 'BAD_LISTENER_ID']), + <<"200">> => + emqx_mgmt_util:object_schema(resp_schema(), <<"Get listener successfully">>)}} + }, + {"/nodes/:node/listeners/:id", Metadata, get_update_listener_by_id_on_node}. api_manage_listeners() -> Metadata = #{ @@ -169,14 +190,16 @@ param_path_operation()-> list_listeners(get, _Request) -> {200, format(emqx_mgmt:list_listeners())}. -list_listeners_by_id(get, #{bindings := #{id := Id}}) -> - case [L || L = {Id0, _Conf} <- emqx_mgmt:list_listeners(), +list_update_listeners_by_id(get, #{bindings := #{id := Id}}) -> + case [L || L = #{id := Id0} <- emqx_mgmt:list_listeners(), atom_to_binary(Id0, latin1) =:= Id] of [] -> {400, #{code => 'RESOURCE_NOT_FOUND', message => ?LISTENER_NOT_FOUND}}; Listeners -> {200, format(Listeners)} - end. + end; +list_update_listeners_by_id(put, #{bindings := #{id := Id}, body := Conf}) -> + return_listeners(emqx_mgmt:update_listener(Id, Conf)). list_listeners_on_node(get, #{bindings := #{node := Node}}) -> case emqx_mgmt:list_listeners(atom(Node)) of @@ -186,7 +209,7 @@ list_listeners_on_node(get, #{bindings := #{node := Node}}) -> {200, format(Listener)} end. -get_listener_by_id_on_node(get, #{bindings := #{id := Id, node := Node}}) -> +get_update_listener_by_id_on_node(get, #{bindings := #{id := Id, node := Node}}) -> case emqx_mgmt:get_listener(atom(Node), atom(Id)) of {error, not_found} -> {404, #{code => 'RESOURCE_NOT_FOUND', message => ?NODE_LISTENER_NOT_FOUND}}; @@ -194,7 +217,9 @@ get_listener_by_id_on_node(get, #{bindings := #{id := Id, node := Node}}) -> {500, #{code => 'UNKNOW_ERROR', message => err_msg(Reason)}}; Listener -> {200, format(Listener)} - end. + end; +get_update_listener_by_id_on_node(put, #{bindings := #{id := Id, node := Node, body := Conf}}) -> + return_listeners(emqx_mgmt:update_listener(atom(Node), Id, Conf)). manage_listeners(_, #{bindings := #{id := Id, operation := Oper, node := Node}}) -> {_, Result} = do_manage_listeners(Node, Id, Oper), @@ -236,6 +261,13 @@ do_manage_listeners2(<<"restart">>, Param) -> {500, #{code => 'UNKNOW_ERROR', message => err_msg(Reason)}} end. +return_listeners(Listeners) -> + Results = format(Listeners), + case lists:filter(fun({error, _}) -> true; (_) -> false end, Results) of + [] -> {200, Results}; + Errors -> {500, #{code => 'UNKNOW_ERROR', message => manage_listeners_err(Errors)}} + end. + manage_listeners_err(Errors) -> list_to_binary(lists:foldl(fun({Node, Err}, Str) -> err_msg_str(#{node => Node, error => Err}) ++ "; " ++ Str @@ -247,12 +279,10 @@ format(Listeners) when is_list(Listeners) -> format({error, Reason}) -> {error, Reason}; -format({ID, Conf}) -> +format(#{node := _Node, id := _Id} = Conf) when is_map(Conf) -> emqx_map_lib:jsonable_map(Conf#{ - id => ID, - node => maps:get(node, Conf), - running => trans_running(Conf) - }, fun ?MODULE:jsonable_resp/2). + running => trans_running(Conf) + }, fun ?MODULE:jsonable_resp/2). trans_running(Conf) -> case maps:get(running, Conf) of From 7390d2bb366cf3a29053b801079c7232efbcae29 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 30 Aug 2021 17:54:01 +0800 Subject: [PATCH 007/109] fix(listeners): test case emqx_mgmt_listeners_api_SUITE failed --- apps/emqx_management/src/emqx_mgmt.erl | 13 ++++++------- .../src/emqx_mgmt_api_listeners.erl | 2 +- .../test/emqx_mgmt_api_test_util.erl | 14 ++++++++++++-- .../test/emqx_mgmt_listeners_api_SUITE.erl | 10 ++++------ 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt.erl b/apps/emqx_management/src/emqx_mgmt.erl index 4a628d994..eb4166675 100644 --- a/apps/emqx_management/src/emqx_mgmt.erl +++ b/apps/emqx_management/src/emqx_mgmt.erl @@ -480,20 +480,19 @@ list_listeners(Node) when Node =:= node() -> list_listeners(Node) -> rpc_call(Node, list_listeners, [Node]). -list_listeners_by_id(Identifier) -> - listener_id_filter(Identifier, list_listeners()). +list_listeners_by_id(Id) -> + listener_id_filter(Id, list_listeners()). -get_listener(Node, Identifier) -> - case listener_id_filter(Identifier, list_listeners(Node)) of +get_listener(Node, Id) -> + case listener_id_filter(Id, list_listeners(Node)) of [] -> {error, not_found}; [Listener] -> Listener end. -listener_id_filter(Identifier, Listeners) -> - Filter = - fun({Id, _}) -> Id =:= Identifier end, +listener_id_filter(Id, Listeners) -> + Filter = fun(#{id := Id0}) -> Id0 =:= Id end, lists:filter(Filter, Listeners). -spec manage_listener(Operation :: start_listener|stop_listener|restart_listener, Param :: map()) -> diff --git a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl index f1749b1d9..4b8e132e7 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl @@ -132,7 +132,7 @@ api_get_update_listener_by_id_on_node() -> api_manage_listeners() -> Metadata = #{ - get => #{ + post => #{ description => <<"Restart listeners on all nodes in the cluster">>, parameters => [ param_path_id(), diff --git a/apps/emqx_management/test/emqx_mgmt_api_test_util.erl b/apps/emqx_management/test/emqx_mgmt_api_test_util.erl index 0babef05a..fa924a71c 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_test_util.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_test_util.erl @@ -52,13 +52,23 @@ request_api(Method, Url, Auth) -> request_api(Method, Url, QueryParams, Auth) -> request_api(Method, Url, QueryParams, Auth, []). -request_api(Method, Url, QueryParams, Auth, []) -> +request_api(Method, Url, QueryParams, Auth, []) + when (Method =:= options) orelse + (Method =:= get) orelse + (Method =:= put) orelse + (Method =:= head) orelse + (Method =:= delete) orelse + (Method =:= trace) -> NewUrl = case QueryParams of "" -> Url; _ -> Url ++ "?" ++ QueryParams end, do_request_api(Method, {NewUrl, [Auth]}); -request_api(Method, Url, QueryParams, Auth, Body) -> +request_api(Method, Url, QueryParams, Auth, Body) + when (Method =:= post) orelse + (Method =:= patch) orelse + (Method =:= put) orelse + (Method =:= delete) -> NewUrl = case QueryParams of "" -> Url; _ -> Url ++ "?" ++ QueryParams diff --git a/apps/emqx_management/test/emqx_mgmt_listeners_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_listeners_api_SUITE.erl index 10e1def26..0dd00b38e 100644 --- a/apps/emqx_management/test/emqx_mgmt_listeners_api_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_listeners_api_SUITE.erl @@ -58,8 +58,8 @@ t_manage_listener(_) -> manage_listener(ID, "restart", true). manage_listener(ID, Operation, Running) -> - Path = emqx_mgmt_api_test_util:api_path(["listeners", ID, Operation]), - {ok, _} = emqx_mgmt_api_test_util:request_api(get, Path), + Path = emqx_mgmt_api_test_util:api_path(["listeners", ID, "operation", Operation]), + {ok, _} = emqx_mgmt_api_test_util:request_api(post, Path), timer:sleep(500), GetPath = emqx_mgmt_api_test_util:api_path(["listeners", ID]), {ok, ListenersResponse} = emqx_mgmt_api_test_util:request_api(get, GetPath), @@ -106,10 +106,8 @@ comparison_listener(Local, Response) -> ?assertEqual(maps:get(id, Local), binary_to_atom(maps:get(<<"id">>, Response))), ?assertEqual(maps:get(node, Local), binary_to_atom(maps:get(<<"node">>, Response))), ?assertEqual(maps:get(acceptors, Local), maps:get(<<"acceptors">>, Response)), - ?assertEqual(maps:get(max_conn, Local), maps:get(<<"max_conn">>, Response)), - ?assertEqual(maps:get(listen_on, Local), maps:get(<<"listen_on">>, Response)), ?assertEqual(maps:get(running, Local), maps:get(<<"running">>, Response)). -listener_stats(Listener, Stats) -> - ?assertEqual(maps:get(<<"running">>, Listener), Stats). +listener_stats(Listener, ExpectedStats) -> + ?assertEqual(ExpectedStats, maps:get(<<"running">>, Listener)). From 50ccaec4b0a8e055ec0096494a211f580c5c5361 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 30 Aug 2021 20:49:58 +0800 Subject: [PATCH 008/109] fix(emqx_schema): define bind as a mandatory config of listener --- apps/emqx/src/emqx_schema.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 7d1e39510..812e52f9b 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -342,7 +342,7 @@ mqtt_listener() -> ]. base_listener() -> - [ {"bind", t(union(ip_port(), integer()))} + [ {"bind", hoconsc:t(union(ip_port(), integer()), #{nullable => false})} , {"acceptors", t(integer(), undefined, 16)} , {"max_connections", maybe_infinity(integer(), infinity)} , {"mountpoint", t(binary(), undefined, <<>>)} From 4da413c4530ed27d2314f51ffad857057f5599f0 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 30 Aug 2021 20:51:30 +0800 Subject: [PATCH 009/109] fix(APIs): clarify the error message when update listener failed --- apps/emqx_management/src/emqx_mgmt.erl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt.erl b/apps/emqx_management/src/emqx_mgmt.erl index eb4166675..8216e2c3f 100644 --- a/apps/emqx_management/src/emqx_mgmt.erl +++ b/apps/emqx_management/src/emqx_mgmt.erl @@ -507,8 +507,12 @@ update_listener(Id, Config) -> update_listener(Node, Id, Config) when Node =:= node() -> {Type, Name} = emqx_listeners:parse_listener_id(Id), - {ok, #{raw_config := RawConf}} = emqx:update_config([listeners, Type, Name], Config, #{}), - RawConf#{node => Node, id => Id, running => true}; + case emqx:update_config([listeners, Type, Name], Config, #{}) of + {ok, #{raw_config := RawConf}} -> + RawConf#{node => Node, id => Id, running => true}; + {error, Reason} -> + error(Reason) + end; update_listener(Node, Id, Config) -> rpc_call(Node, update_listener, [Node, Id, Config]). From e6306bccd8cc92ca879be5c5ae960db757989837 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 30 Aug 2021 20:53:02 +0800 Subject: [PATCH 010/109] feat(map_lib): add emqx_map_lib:diff_maps/2 --- apps/emqx/src/emqx_map_lib.erl | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/apps/emqx/src/emqx_map_lib.erl b/apps/emqx/src/emqx_map_lib.erl index de2b41b32..2ed25d22d 100644 --- a/apps/emqx/src/emqx_map_lib.erl +++ b/apps/emqx/src/emqx_map_lib.erl @@ -27,6 +27,7 @@ , jsonable_map/2 , binary_string/1 , deep_convert/3 + , diff_maps/2 ]). -export_type([config_key/0, config_key_path/0]). @@ -129,6 +130,27 @@ jsonable_map(Map) -> jsonable_map(Map, JsonableFun) -> deep_convert(Map, fun binary_string_kv/3, [JsonableFun]). +-spec diff_maps(map(), map()) -> + #{added := [map()], identical := [map()], removed := [map()], + changed := [#{any() => {OldValue::any(), NewValue::any()}}]}. +diff_maps(NewMap, OldMap) -> + InitR = #{identical => [], changed => [], removed => []}, + {Result, RemInNew} = + lists:foldl(fun({OldK, OldV}, {Result0 = #{identical := I, changed := U, removed := D}, + RemNewMap}) -> + Result1 = case maps:find(OldK, NewMap) of + error -> + Result0#{removed => [#{OldK => OldV} | D]}; + {ok, NewV} when NewV == OldV -> + Result0#{identical => [#{OldK => OldV} | I]}; + {ok, NewV} -> + Result0#{changed => [#{OldK => {OldV, NewV}} | U]} + end, + {Result1, maps:remove(OldK, RemNewMap)} + end, {InitR, NewMap}, maps:to_list(OldMap)), + Result#{added => RemInNew}. + + binary_string_kv(K, V, JsonableFun) -> case JsonableFun(K, V) of drop -> drop; From 0d1bc6d6895d7e72a6798551be7b9b0c78248f7e Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 30 Aug 2021 20:53:36 +0800 Subject: [PATCH 011/109] feat(listeners): add config handler for listeners --- apps/emqx/src/emqx_listeners.erl | 67 ++++++++++++++++++++++++++++---- apps/emqx/src/emqx_map_lib.erl | 12 +++--- 2 files changed, 66 insertions(+), 13 deletions(-) diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index d9670b858..399bd3d08 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -41,6 +41,10 @@ , parse_listener_id/1 ]). +-export([post_config_update/4]). + +-define(CONF_KEY_PATH, [listeners]). + %% @doc List configured listeners. -spec(list() -> [{ListenerId :: atom(), ListenerConf :: map()}]). list() -> @@ -88,6 +92,9 @@ is_running(quic, _ListenerId, _Conf)-> %% @doc Start all listeners. -spec(start() -> ok). start() -> + %% The ?MODULE:start/0 will be called by emqx_app when emqx get started, + %% so we install the config handler here. + ok = emqx_config_handler:add_handler(?CONF_KEY_PATH, ?MODULE), foreach_listeners(fun start_listener/3). -spec start_listener(atom()) -> ok | {error, term()}. @@ -102,7 +109,7 @@ start_listener(Type, ListenerName, #{bind := Bind} = Conf) -> console_print("- Skip - starting listener ~s on ~s ~n due to ~p", [listener_id(Type, ListenerName), format_addr(Bind), Reason]); {ok, _} -> - console_print("Start listener ~s on ~s successfully.~n", + console_print("Listener ~s on ~s started.~n", [listener_id(Type, ListenerName), format_addr(Bind)]); {error, {already_started, Pid}} -> {error, {already_started, Pid}}; @@ -122,27 +129,47 @@ restart_listener(ListenerId) -> apply_on_listener(ListenerId, fun restart_listener/3). -spec(restart_listener(atom(), atom(), map()) -> ok | {error, term()}). +restart_listener(Type, ListenerName, {OldConf, NewConf}) -> + restart_listener(Type, ListenerName, OldConf, NewConf); restart_listener(Type, ListenerName, Conf) -> - case stop_listener(Type, ListenerName, Conf) of - ok -> start_listener(Type, ListenerName, Conf); + restart_listener(Type, ListenerName, Conf, Conf). + +restart_listener(Type, ListenerName, OldConf, NewConf) -> + case stop_listener(Type, ListenerName, OldConf) of + ok -> start_listener(Type, ListenerName, NewConf); Error -> Error end. %% @doc Stop all listeners. -spec(stop() -> ok). stop() -> + %% The ?MODULE:stop/0 will be called by emqx_app when emqx is going to shutdown, + %% so we uninstall the config handler here. + _ = emqx_config_handler:remove_handler(?CONF_KEY_PATH), foreach_listeners(fun stop_listener/3). -spec(stop_listener(atom()) -> ok | {error, term()}). stop_listener(ListenerId) -> apply_on_listener(ListenerId, fun stop_listener/3). --spec(stop_listener(atom(), atom(), map()) -> ok | {error, term()}). -stop_listener(Type, ListenerName, #{bind := ListenOn}) when Type == tcp; Type == ssl -> +stop_listener(Type, ListenerName, #{bind := Bind} = Conf) -> + case do_stop_listener(Type, ListenerName, Conf) of + ok -> + console_print("Listener ~s on ~s stopped.~n", + [listener_id(Type, ListenerName), format_addr(Bind)]), + ok; + {error, Reason} -> + ?ELOG("Failed to stop listener ~s on ~s: ~0p~n", + [listener_id(Type, ListenerName), format_addr(Bind), Reason]), + {error, Reason} + end. + +-spec(do_stop_listener(atom(), atom(), map()) -> ok | {error, term()}). +do_stop_listener(Type, ListenerName, #{bind := ListenOn}) when Type == tcp; Type == ssl -> esockd:close(listener_id(Type, ListenerName), ListenOn); -stop_listener(Type, ListenerName, _Conf) when Type == ws; Type == wss -> +do_stop_listener(Type, ListenerName, _Conf) when Type == ws; Type == wss -> cowboy:stop_listener(listener_id(Type, ListenerName)); -stop_listener(quic, ListenerName, _Conf) -> +do_stop_listener(quic, ListenerName, _Conf) -> quicer:stop_listener(listener_id(quic, ListenerName)). -ifndef(TEST). @@ -201,6 +228,32 @@ do_start_listener(quic, ListenerName, #{bind := ListenOn} = Opts) -> {ok, {skipped, quic_app_missing}} end. +%% Update the listeners at runtime +post_config_update(_Req, NewListeners, OldListeners, _AppEnvs) -> + #{added := Added, removed := Removed, changed := Updated} + = diff_listeners(NewListeners, OldListeners), + perform_listener_changes(fun stop_listener/3, Removed), + perform_listener_changes(fun start_listener/3, Added), + perform_listener_changes(fun restart_listener/3, Updated). + +perform_listener_changes(Action, MapConfs) -> + lists:foreach(fun + ({Id, Conf}) -> + {Type, Name} = parse_listener_id(Id), + Action(Type, Name, Conf) + end, maps:to_list(MapConfs)). + +diff_listeners(NewListeners, OldListeners) -> + emqx_map_lib:diff_maps(flatten_listeners(NewListeners), flatten_listeners(OldListeners)). + +flatten_listeners(Conf0) -> + maps:from_list( + lists:append([do_flatten_listeners(Type, Conf) + || {Type, Conf} <- maps:to_list(Conf0)])). + +do_flatten_listeners(Type, Conf0) -> + [{listener_id(Type, Name), Conf} || {Name, Conf} <- maps:to_list(Conf0)]. + esockd_opts(Type, Opts0) -> Opts1 = maps:with([acceptors, max_connections, proxy_protocol, proxy_protocol_timeout], Opts0), Opts2 = case emqx_config:get_zone_conf(zone(Opts0), [rate_limit, max_conn_rate]) of diff --git a/apps/emqx/src/emqx_map_lib.erl b/apps/emqx/src/emqx_map_lib.erl index 2ed25d22d..d5e851971 100644 --- a/apps/emqx/src/emqx_map_lib.erl +++ b/apps/emqx/src/emqx_map_lib.erl @@ -131,20 +131,20 @@ jsonable_map(Map, JsonableFun) -> deep_convert(Map, fun binary_string_kv/3, [JsonableFun]). -spec diff_maps(map(), map()) -> - #{added := [map()], identical := [map()], removed := [map()], - changed := [#{any() => {OldValue::any(), NewValue::any()}}]}. + #{added := map(), identical := map(), removed := map(), + changed := #{any() => {OldValue::any(), NewValue::any()}}}. diff_maps(NewMap, OldMap) -> - InitR = #{identical => [], changed => [], removed => []}, + InitR = #{identical => #{}, changed => #{}, removed => #{}}, {Result, RemInNew} = lists:foldl(fun({OldK, OldV}, {Result0 = #{identical := I, changed := U, removed := D}, RemNewMap}) -> Result1 = case maps:find(OldK, NewMap) of error -> - Result0#{removed => [#{OldK => OldV} | D]}; + Result0#{removed => D#{OldK => OldV}}; {ok, NewV} when NewV == OldV -> - Result0#{identical => [#{OldK => OldV} | I]}; + Result0#{identical => I#{OldK => OldV}}; {ok, NewV} -> - Result0#{changed => [#{OldK => {OldV, NewV}} | U]} + Result0#{changed => U#{OldK => {OldV, NewV}}} end, {Result1, maps:remove(OldK, RemNewMap)} end, {InitR, NewMap}, maps:to_list(OldMap)), From 0af39e88a4b33671c5fb289e70ce93d49f68959c Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 31 Aug 2021 14:08:49 +0800 Subject: [PATCH 012/109] feat(listeners): add DELETE APIs for removing the listeners --- apps/emqx/src/emqx_config.erl | 4 +- apps/emqx/src/emqx_config_handler.erl | 11 ++-- apps/emqx_management/src/emqx_mgmt.erl | 15 ++++++ .../src/emqx_mgmt_api_listeners.erl | 53 ++++++++++++++----- 4 files changed, 65 insertions(+), 18 deletions(-) diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 2f5bc9551..ddedef024 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -94,8 +94,8 @@ -type update_stage() :: pre_config_update | post_config_update. -type update_error() :: {update_stage(), module(), term()} | {save_configs, term()} | term(). -type update_result() :: #{ - config := emqx_config:config(), - raw_config := emqx_config:raw_config(), + config => emqx_config:config(), + raw_config => emqx_config:raw_config(), post_config_update => #{module() => any()} }. diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl index b45f89538..a86efb2bc 100644 --- a/apps/emqx/src/emqx_config_handler.erl +++ b/apps/emqx/src/emqx_config_handler.erl @@ -217,10 +217,9 @@ call_post_config_update(Handlers, OldConf, NewConf, AppEnvs, UpdateReq, Result) false -> {ok, Result} end. -save_configs(ConfKeyPath, AppEnvs, CheckedConf, NewRawConf, OverrideConf, {_Cmd, Opts}) -> +save_configs(ConfKeyPath, AppEnvs, CheckedConf, NewRawConf, OverrideConf, UpdateArgs) -> case emqx_config:save_configs(AppEnvs, CheckedConf, NewRawConf, OverrideConf) of - ok -> {ok, #{config => emqx_config:get(ConfKeyPath), - raw_config => return_rawconf(ConfKeyPath, Opts)}}; + ok -> {ok, return_change_result(ConfKeyPath, UpdateArgs)}; {error, Reason} -> {error, {save_configs, Reason}} end. @@ -241,6 +240,12 @@ update_override_config(RawConf) -> up_req({remove, _Opts}) -> '$remove'; up_req({{update, Req}, _Opts}) -> Req. +return_change_result(ConfKeyPath, {{update, _Req}, Opts}) -> + #{config => emqx_config:get(ConfKeyPath), + raw_config => return_rawconf(ConfKeyPath, Opts)}; +return_change_result(_ConfKeyPath, {remove, _Opts}) -> + #{}. + return_rawconf(ConfKeyPath, #{rawconf_with_defaults := true}) -> FullRawConf = emqx_config:fill_defaults(emqx_config:get_raw([])), emqx_map_lib:deep_get(bin_path(ConfKeyPath), FullRawConf); diff --git a/apps/emqx_management/src/emqx_mgmt.erl b/apps/emqx_management/src/emqx_mgmt.erl index 8216e2c3f..3cc31b47f 100644 --- a/apps/emqx_management/src/emqx_mgmt.erl +++ b/apps/emqx_management/src/emqx_mgmt.erl @@ -92,6 +92,8 @@ , manage_listener/2 , update_listener/2 , update_listener/3 + , remove_listener/1 + , remove_listener/2 ]). %% Alarms @@ -516,6 +518,19 @@ update_listener(Node, Id, Config) when Node =:= node() -> update_listener(Node, Id, Config) -> rpc_call(Node, update_listener, [Node, Id, Config]). +remove_listener(Id) -> + [remove_listener(Node, Id) || Node <- ekka_mnesia:running_nodes()]. + +remove_listener(Node, Id) when Node =:= node() -> + {Type, Name} = emqx_listeners:parse_listener_id(Id), + case emqx:remove_config([listeners, Type, Name], #{}) of + {ok, _} -> ok; + {error, Reason} -> + error(Reason) + end; +remove_listener(Node, Id) -> + rpc_call(Node, remove_listener, [Node, Id]). + %%-------------------------------------------------------------------- %% Get Alarms %%-------------------------------------------------------------------- diff --git a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl index 4b8e132e7..51487fb2a 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl @@ -21,9 +21,9 @@ -export([api_spec/0]). -export([ list_listeners/2 - , list_update_listeners_by_id/2 + , crud_listeners_by_id/2 , list_listeners_on_node/2 - , get_update_listener_by_id_on_node/2 + , crud_listener_by_id_on_node/2 , manage_listeners/2 , jsonable_resp/2 ]). @@ -86,16 +86,24 @@ api_list_update_listeners_by_id() -> <<"200">> => emqx_mgmt_util:array_schema(resp_schema(), <<"List listeners successfully">>)}}, put => #{ - description => <<"Create or update listeners by a given Id to all nodes in the cluster">>, + description => <<"Create or update a listener by a given Id to all nodes in the cluster">>, parameters => [param_path_id()], requestBody => emqx_mgmt_util:schema(req_schema(), <<"Listener Config">>), responses => #{ <<"404">> => emqx_mgmt_util:error_schema(?LISTENER_NOT_FOUND, ['BAD_LISTENER_ID']), <<"200">> => - emqx_mgmt_util:array_schema(resp_schema(), <<"List listeners successfully">>)}} + emqx_mgmt_util:array_schema(resp_schema(), <<"Create or update listener successfully">>)}}, + delete => #{ + description => <<"Delete a listener by a given Id to all nodes in the cluster">>, + parameters => [param_path_id()], + responses => #{ + <<"404">> => + emqx_mgmt_util:error_schema(?LISTENER_NOT_FOUND, ['BAD_LISTENER_ID']), + <<"200">> => + emqx_mgmt_util:schema(<<"Delete listener successfully">>)}} }, - {"/listeners/:id", Metadata, list_update_listeners_by_id}. + {"/listeners/:id", Metadata, crud_listeners_by_id}. api_list_listeners_on_node() -> Metadata = #{ @@ -126,9 +134,17 @@ api_get_update_listener_by_id_on_node() -> emqx_mgmt_util:error_schema(?NODE_LISTENER_NOT_FOUND, ['BAD_NODE_NAME', 'BAD_LISTENER_ID']), <<"200">> => - emqx_mgmt_util:object_schema(resp_schema(), <<"Get listener successfully">>)}} + emqx_mgmt_util:object_schema(resp_schema(), <<"Get listener successfully">>)}}, + delete => #{ + description => <<"Delete a listener by a given Id to all nodes in the cluster">>, + parameters => [param_path_node(), param_path_id()], + responses => #{ + <<"404">> => + emqx_mgmt_util:error_schema(?LISTENER_NOT_FOUND, ['BAD_LISTENER_ID']), + <<"200">> => + emqx_mgmt_util:schema(<<"Delete listener successfully">>)}} }, - {"/nodes/:node/listeners/:id", Metadata, get_update_listener_by_id_on_node}. + {"/nodes/:node/listeners/:id", Metadata, crud_listener_by_id_on_node}. api_manage_listeners() -> Metadata = #{ @@ -190,7 +206,7 @@ param_path_operation()-> list_listeners(get, _Request) -> {200, format(emqx_mgmt:list_listeners())}. -list_update_listeners_by_id(get, #{bindings := #{id := Id}}) -> +crud_listeners_by_id(get, #{bindings := #{id := Id}}) -> case [L || L = #{id := Id0} <- emqx_mgmt:list_listeners(), atom_to_binary(Id0, latin1) =:= Id] of [] -> @@ -198,8 +214,14 @@ list_update_listeners_by_id(get, #{bindings := #{id := Id}}) -> Listeners -> {200, format(Listeners)} end; -list_update_listeners_by_id(put, #{bindings := #{id := Id}, body := Conf}) -> - return_listeners(emqx_mgmt:update_listener(Id, Conf)). +crud_listeners_by_id(put, #{bindings := #{id := Id}, body := Conf}) -> + return_listeners(emqx_mgmt:update_listener(Id, Conf)); +crud_listeners_by_id(delete, #{bindings := #{id := Id}}) -> + Results = emqx_mgmt:remove_listener(Id), + case lists:filter(fun({error, _}) -> true; (_) -> false end, Results) of + [] -> {200}; + Errors -> {500, #{code => 'UNKNOW_ERROR', message => err_msg(Errors)}} + end. list_listeners_on_node(get, #{bindings := #{node := Node}}) -> case emqx_mgmt:list_listeners(atom(Node)) of @@ -209,7 +231,7 @@ list_listeners_on_node(get, #{bindings := #{node := Node}}) -> {200, format(Listener)} end. -get_update_listener_by_id_on_node(get, #{bindings := #{id := Id, node := Node}}) -> +crud_listener_by_id_on_node(get, #{bindings := #{id := Id, node := Node}}) -> case emqx_mgmt:get_listener(atom(Node), atom(Id)) of {error, not_found} -> {404, #{code => 'RESOURCE_NOT_FOUND', message => ?NODE_LISTENER_NOT_FOUND}}; @@ -218,8 +240,13 @@ get_update_listener_by_id_on_node(get, #{bindings := #{id := Id, node := Node}}) Listener -> {200, format(Listener)} end; -get_update_listener_by_id_on_node(put, #{bindings := #{id := Id, node := Node, body := Conf}}) -> - return_listeners(emqx_mgmt:update_listener(atom(Node), Id, Conf)). +crud_listener_by_id_on_node(put, #{bindings := #{id := Id, node := Node, body := Conf}}) -> + return_listeners(emqx_mgmt:update_listener(atom(Node), Id, Conf)); +crud_listener_by_id_on_node(delete, #{bindings := #{id := Id, node := Node}}) -> + case emqx_mgmt:remove_listener(atom(Node), Id) of + ok -> {200}; + {error, Reason} -> {500, #{code => 'UNKNOW_ERROR', message => err_msg(Reason)}} + end. manage_listeners(_, #{bindings := #{id := Id, operation := Oper, node := Node}}) -> {_, Result} = do_manage_listeners(Node, Id, Oper), From 7e53469bb8c531b61e3c4d6a0aeae3e244c00fa5 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Tue, 31 Aug 2021 14:21:53 +0800 Subject: [PATCH 013/109] fix(listeners): update the testcases for listeners --- apps/emqx/src/emqx_config_handler.erl | 4 ++++ apps/emqx/test/emqx_listeners_SUITE.erl | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl index a86efb2bc..f16f8a97a 100644 --- a/apps/emqx/src/emqx_config_handler.erl +++ b/apps/emqx/src/emqx_config_handler.erl @@ -23,6 +23,7 @@ %% API functions -export([ start_link/0 + , stop/0 , add_handler/2 , remove_handler/1 , update_config/3 @@ -68,6 +69,9 @@ start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, {}, []). +stop() -> + gen_server:stop(?MODULE). + -spec update_config(module(), emqx_config:config_key_path(), emqx_config:update_args()) -> {ok, emqx_config:update_result()} | {error, emqx_config:update_error()}. update_config(SchemaModule, ConfKeyPath, UpdateArgs) -> diff --git a/apps/emqx/test/emqx_listeners_SUITE.erl b/apps/emqx/test/emqx_listeners_SUITE.erl index a8760c7e8..a3bfb2d47 100644 --- a/apps/emqx/test/emqx_listeners_SUITE.erl +++ b/apps/emqx/test/emqx_listeners_SUITE.erl @@ -37,6 +37,14 @@ end_per_suite(_Config) -> application:stop(esockd), application:stop(cowboy). +init_per_testcase(_, Config) -> + {ok, _} = emqx_config_handler:start_link(), + Config. + +end_per_testcase(_, _Config) -> + _ = emqx_config_handler:stop(), + ok. + t_start_stop_listeners(_) -> ok = emqx_listeners:start(), ?assertException(error, _, emqx_listeners:start_listener({ws,{"127.0.0.1", 8083}, []})), From 4c468b383acd501963c39ee6ce818f177734853f Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Tue, 31 Aug 2021 19:04:38 +0800 Subject: [PATCH 014/109] fix: delayed math string (#5609) * fix: delayed ms --- apps/emqx_modules/src/emqx_delayed.erl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/emqx_modules/src/emqx_delayed.erl b/apps/emqx_modules/src/emqx_delayed.erl index b773f04ac..0b1f00e14 100644 --- a/apps/emqx_modules/src/emqx_delayed.erl +++ b/apps/emqx_modules/src/emqx_delayed.erl @@ -52,6 +52,10 @@ -record(delayed_message, {key, delayed, msg}). +%% sync ms with record change +-define(QUERY_MS(Id), [{{delayed_message, {'_', Id}, '_', '_'}, [], ['$_']}]). +-define(DELETE_MS(Id), [{{delayed_message, {'$1', Id}, '_', '_'}, [], ['$1']}]). + -define(TAB, ?MODULE). -define(SERVER, ?MODULE). -define(MAX_INTERVAL, 4294967). @@ -161,8 +165,7 @@ to_rfc3339(Timestamp) -> get_delayed_message(Id0) -> Id = emqx_guid:from_hexstr(Id0), - Ms = [{{delayed_message,{'_',Id},'_'},[],['$_']}], - case ets:select(?TAB, Ms) of + case ets:select(?TAB, ?QUERY_MS(Id)) of [] -> {error, not_found}; Rows -> @@ -172,8 +175,7 @@ get_delayed_message(Id0) -> delete_delayed_message(Id0) -> Id = emqx_guid:from_hexstr(Id0), - Ms = [{{delayed_message, {'$1', Id}, '_'}, [], ['$1']}], - case ets:select(?TAB, Ms) of + case ets:select(?TAB, ?DELETE_MS(Id)) of [] -> {error, not_found}; Rows -> From 00d469976f16094ac7f716083f965b50ff17ecf8 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Tue, 31 Aug 2021 19:04:52 +0800 Subject: [PATCH 015/109] fix: subscriptions api share param name (#5610) --- apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl index 62514e314..058d824ac 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl @@ -37,6 +37,7 @@ [ {<<"clientid">>, binary} , {<<"topic">>, binary} , {<<"share">>, binary} + , {<<"share_group">>, binary} , {<<"qos">>, integer} , {<<"match_topic">>, binary}]}). @@ -87,10 +88,10 @@ parameters() -> schema => #{type => integer, enum => [0, 1, 2]} }, #{ - name => share, + name => share_group, in => query, - description => <<"Shared subscription">>, - schema => #{type => boolean} + description => <<"Shared subscription group name">>, + schema => #{type => string} }, #{ name => topic, @@ -183,7 +184,7 @@ update_ms(clientid, X, {{Pid, Topic}, Opts}) -> {{Pid, Topic}, Opts#{subid => X}}; update_ms(topic, X, {{Pid, _Topic}, Opts}) -> {{Pid, X}, Opts}; -update_ms(share, X, {{Pid, Topic}, Opts}) -> +update_ms(share_group, X, {{Pid, Topic}, Opts}) -> {{Pid, Topic}, Opts#{share => X}}; update_ms(qos, X, {{Pid, Topic}, Opts}) -> {{Pid, Topic}, Opts#{qos => X}}. From 8d2b72c278416002d642371c264a2e6dd01a83d5 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Tue, 31 Aug 2021 19:12:27 +0800 Subject: [PATCH 016/109] fix: alarms api return time (#5612) --- apps/emqx/src/emqx_alarm.erl | 6 ++++++ apps/emqx_management/src/emqx_mgmt_api_alarms.erl | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/emqx/src/emqx_alarm.erl b/apps/emqx/src/emqx_alarm.erl index 11a2805f3..b43f5c52e 100644 --- a/apps/emqx/src/emqx_alarm.erl +++ b/apps/emqx/src/emqx_alarm.erl @@ -159,6 +159,7 @@ format(#activated_alarm{name = Name, message = Message, activate_at = At, detail name => Name, message => Message, duration => (Now - At) div 1000, %% to millisecond + activate_at => to_rfc3339(At), details => Details }; format(#deactivated_alarm{name = Name, message = Message, activate_at = At, details = Details, @@ -168,11 +169,16 @@ format(#deactivated_alarm{name = Name, message = Message, activate_at = At, deta name => Name, message => Message, duration => DAt - At, + activate_at => to_rfc3339(At), + deactivate_at => to_rfc3339(DAt), details => Details }; format(_) -> {error, unknow_alarm}. +to_rfc3339(Timestamp) -> + list_to_binary(calendar:system_time_to_rfc3339(Timestamp div 1000, [{unit, millisecond}])). + %%-------------------------------------------------------------------- %% gen_server callbacks %%-------------------------------------------------------------------- diff --git a/apps/emqx_management/src/emqx_mgmt_api_alarms.erl b/apps/emqx_management/src/emqx_mgmt_api_alarms.erl index 40956fd11..1adb5fce3 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_alarms.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_alarms.erl @@ -42,7 +42,9 @@ properties() -> {name, string, <<"Alarm name">>}, {message, string, <<"Alarm readable information">>}, {details, object}, - {duration, integer, <<"Alarms duration time; UNIX time stamp">>} + {duration, integer, <<"Alarms duration time; UNIX time stamp, millisecond">>}, + {activate_at, string, <<"Alarms activate time, RFC 3339">>}, + {deactivate_at, string, <<"Nullable, alarms deactivate time, RFC 3339">>} ]). alarms_api() -> From 560f415964c28fe9cf7ef35f9b980b794c49153c Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Tue, 31 Aug 2021 19:27:08 +0800 Subject: [PATCH 017/109] fix: auto sub api doc & null body check (#5613) * fix: auto sub api doc & null body check --- apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl index d55444dba..7eeef52ff 100644 --- a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl @@ -23,6 +23,7 @@ -export([auto_subscribe/2]). -define(EXCEED_LIMIT, 'EXCEED_LIMIT'). +-define(BAD_REQUEST, 'BAD_REQUEST'). api_spec() -> {[auto_subscribe_api()], []}. @@ -43,6 +44,8 @@ auto_subscribe_api() -> 'requestBody' => schema(), responses => #{ <<"200">> => schema(), + <<"400">> => emqx_mgmt_util:error_schema( + <<"Request body required">>, [?BAD_REQUEST]), <<"409">> => emqx_mgmt_util:error_schema( <<"Auto Subscribe topics max limit">>, [?EXCEED_LIMIT])}} }, @@ -53,6 +56,8 @@ auto_subscribe_api() -> auto_subscribe(get, _) -> {200, emqx_auto_subscribe:list()}; +auto_subscribe(put, #{body := #{}}) -> + {400, #{code => ?BAD_REQUEST, message => <<"Request body required">>}}; auto_subscribe(put, #{body := Params}) -> case emqx_auto_subscribe:update(Params) of {error, quota_exceeded} -> From 7075f3260e4cc86a774d3afea6cc649797104372 Mon Sep 17 00:00:00 2001 From: lafirest Date: Tue, 24 Aug 2021 17:12:09 +0800 Subject: [PATCH 018/109] refactor(emqx_lwm2m): port lwm2m into emqx_gateway framework --- apps/emqx_gateway/etc/emqx_gateway.conf | 32 +- apps/emqx_gateway/src/coap/README.md | 31 + apps/emqx_gateway/src/coap/doc/flow.png | Bin 111145 -> 76789 bytes .../src/coap/emqx_coap_channel.erl | 316 ++--- .../emqx_gateway/src/coap/emqx_coap_frame.erl | 48 +- .../src/coap/emqx_coap_medium.erl | 107 ++ .../src/coap/emqx_coap_message.erl | 93 +- .../src/coap/emqx_coap_resource.erl | 37 - .../src/coap/emqx_coap_session.erl | 155 +- apps/emqx_gateway/src/coap/emqx_coap_tm.erl | 341 +++-- .../src/coap/emqx_coap_transport.erl | 178 ++- .../coap/handler/emqx_coap_mqtt_handler.erl | 21 +- .../coap/handler/emqx_coap_pubsub_handler.erl | 80 +- .../src/coap/include/emqx_coap.hrl | 17 +- apps/emqx_gateway/src/emqx_gateway_schema.erl | 35 +- .../emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl | 105 +- .../src/lwm2m/emqx_lwm2m_channel.erl | 459 ++++++ apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cm.erl | 153 -- .../emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl | 410 ++++++ .../src/lwm2m/emqx_lwm2m_cmd_handler.erl | 310 ---- .../src/lwm2m/emqx_lwm2m_coap_resource.erl | 386 ----- .../src/lwm2m/emqx_lwm2m_impl.erl | 55 +- .../src/lwm2m/emqx_lwm2m_json.erl | 351 ----- .../src/lwm2m/emqx_lwm2m_protocol.erl | 560 -------- .../src/lwm2m/emqx_lwm2m_session.erl | 773 ++++++++++ .../src/lwm2m/emqx_lwm2m_timer.erl | 47 - .../src/lwm2m/emqx_lwm2m_xml_object.erl | 6 +- .../src/lwm2m/emqx_lwm2m_xml_object_db.erl | 34 +- .../src/lwm2m/include/emqx_lwm2m.hrl | 11 +- apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl | 1263 +++++++++-------- 30 files changed, 3243 insertions(+), 3171 deletions(-) create mode 100644 apps/emqx_gateway/src/coap/emqx_coap_medium.erl delete mode 100644 apps/emqx_gateway/src/coap/emqx_coap_resource.erl create mode 100644 apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl delete mode 100644 apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cm.erl create mode 100644 apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl delete mode 100644 apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd_handler.erl delete mode 100644 apps/emqx_gateway/src/lwm2m/emqx_lwm2m_coap_resource.erl delete mode 100644 apps/emqx_gateway/src/lwm2m/emqx_lwm2m_json.erl delete mode 100644 apps/emqx_gateway/src/lwm2m/emqx_lwm2m_protocol.erl create mode 100644 apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl delete mode 100644 apps/emqx_gateway/src/lwm2m/emqx_lwm2m_timer.erl diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index 206c54b93..5134246cd 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -134,7 +134,7 @@ gateway.lwm2m { enable_stats = true ## When publishing or subscribing, prefix all topics with a mountpoint string. - mountpoint = "lwm2m/%e/" + mountpoint = "lwm2m" xml_dir = "{{ platform_etc_dir }}/lwm2m_xml" @@ -146,12 +146,32 @@ gateway.lwm2m { ## always | contains_object_list update_msg_publish_condition = contains_object_list + translators { - command = "dn/#" - response = "up/resp" - notify = "up/notify" - register = "up/resp" - update = "up/resp" + command { + topic = "dn/#" + qos = 0 + } + + response { + topic = "up/resp" + qos = 0 + } + + notify { + topic = "up/notify" + qos = 0 + } + + register { + topic = "up/resp" + qos = 0 + } + + update { + topic = "up/resp" + qos = 0 + } } listeners.udp.default { diff --git a/apps/emqx_gateway/src/coap/README.md b/apps/emqx_gateway/src/coap/README.md index 12b5ac5b7..88f657537 100644 --- a/apps/emqx_gateway/src/coap/README.md +++ b/apps/emqx_gateway/src/coap/README.md @@ -9,6 +9,7 @@ 4. [Query String](#org9a6b996) 2. [Implementation](#org9985dfe) 1. [Request/Response flow](#orge94210c) + 3. [Example](#ref_example) @@ -401,3 +402,33 @@ CoAP gateway uses some options in query string to conversion between MQTT CoAP. + + + +## Example +1. Create Connection +``` +coap-client -m post -e "" "coap://127.0.0.1/mqtt/connection?clientid=123&username=admin&password=public" +``` +Server will return token **X** in payload + +2. Update Connection +``` +coap-client -m put -e "" "coap://127.0.0.1/mqtt/connection?clientid=123&username=admin&password=public&token=X" +``` + +3. Publish +``` +coap-client -m post -e "Hellow" "coap://127.0.0.1/ps/coap/test?clientid=123&username=admin&password=public" +``` +if you want to publish with auth, you must first establish a connection, and then post publish request on the same socket, so libcoap client can't simulation publish with a token + +4. Subscribe +``` +coap-client -m get -s 60 -O 6,0x00 -o - -T "obstoken" "coap://127.0.0.1/ps/coap/test?clientid=123&username=admin&password=public" +``` + +5. Close Connection +``` +coap-client -m delete -e "" "coap://127.0.0.1/mqtt/connection?clientid=123&username=admin&password=public&token=X" +``` \ No newline at end of file diff --git a/apps/emqx_gateway/src/coap/doc/flow.png b/apps/emqx_gateway/src/coap/doc/flow.png index 5c72883487b21f467864698eaeca248bfbefd839..bb9b775a5f14ce2319605f3888685ece2b62fa8b 100644 GIT binary patch literal 76789 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 literal 111145 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 z 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

    ()tPcaXYm%S-P)Pz0toF&0B;|4K0pXVlYp{fsTT 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 `~M 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"]}, - {links, [{"Homepage", "https://emqx.io/"}, - {"Github", "https://github.com/emqx/emqx-bridge-mqtt"} - ]} - ]}. diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_app.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_app.erl deleted file mode 100644 index a145009c9..000000000 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_app.erl +++ /dev/null @@ -1,31 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_bridge_mqtt_app). - --behaviour(application). - --export([start/2, stop/1]). - -start(_StartType, _StartArgs) -> - emqx_ctl:register_command(bridges, {emqx_bridge_mqtt_cli, cli}, []), - emqx_bridge_worker:register_metrics(), - emqx_bridge_mqtt_sup:start_link(). - -stop(_State) -> - emqx_ctl:unregister_command(bridges), - ok. - diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_cli.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_cli.erl deleted file mode 100644 index a76ea3a8c..000000000 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_cli.erl +++ /dev/null @@ -1,92 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_bridge_mqtt_cli). - --include("emqx_bridge_mqtt.hrl"). - --import(lists, [foreach/2]). - --export([cli/1]). - -cli(["list"]) -> - foreach(fun({Name, State0}) -> - State = case State0 of - connected -> <<"Running">>; - _ -> <<"Stopped">> - end, - emqx_ctl:print("name: ~s status: ~s~n", [Name, State]) - end, emqx_bridge_mqtt_sup:bridges()); - -cli(["start", Name]) -> - emqx_ctl:print("~s.~n", [try emqx_bridge_worker:ensure_started(Name) of - ok -> <<"Start bridge successfully">>; - connected -> <<"Bridge already started">>; - _ -> <<"Start bridge failed">> - catch - _Error:_Reason -> - <<"Start bridge failed">> - end]); - -cli(["stop", Name]) -> - emqx_ctl:print("~s.~n", [try emqx_bridge_worker:ensure_stopped(Name) of - ok -> <<"Stop bridge successfully">>; - _ -> <<"Stop bridge failed">> - catch - _Error:_Reason -> - <<"Stop bridge failed">> - end]); - -cli(["forwards", Name]) -> - foreach(fun(Topic) -> - emqx_ctl:print("topic: ~s~n", [Topic]) - end, emqx_bridge_worker:get_forwards(Name)); - -cli(["add-forward", Name, Topic]) -> - ok = emqx_bridge_worker:ensure_forward_present(Name, iolist_to_binary(Topic)), - emqx_ctl:print("Add-forward topic successfully.~n"); - -cli(["del-forward", Name, Topic]) -> - ok = emqx_bridge_worker:ensure_forward_absent(Name, iolist_to_binary(Topic)), - emqx_ctl:print("Del-forward topic successfully.~n"); - -cli(["subscriptions", Name]) -> - foreach(fun({Topic, Qos}) -> - emqx_ctl:print("topic: ~s, qos: ~p~n", [Topic, Qos]) - end, emqx_bridge_worker:get_subscriptions(Name)); - -cli(["add-subscription", Name, Topic, Qos]) -> - case emqx_bridge_worker:ensure_subscription_present(Name, Topic, list_to_integer(Qos)) of - ok -> emqx_ctl:print("Add-subscription topic successfully.~n"); - {error, Reason} -> emqx_ctl:print("Add-subscription failed reason: ~p.~n", [Reason]) - end; - -cli(["del-subscription", Name, Topic]) -> - ok = emqx_bridge_worker:ensure_subscription_absent(Name, Topic), - emqx_ctl:print("Del-subscription topic successfully.~n"); - -cli(_) -> - emqx_ctl:usage([{"bridges list", "List bridges"}, - {"bridges start ", "Start a bridge"}, - {"bridges stop ", "Stop a bridge"}, - {"bridges forwards ", "Show a bridge forward topic"}, - {"bridges add-forward ", "Add bridge forward topic"}, - {"bridges del-forward ", "Delete bridge forward topic"}, - {"bridges subscriptions ", "Show a bridge subscriptions topic"}, - {"bridges add-subscription ", "Add bridge subscriptions topic"}, - {"bridges del-subscription ", "Delete bridge subscriptions topic"}]). - - diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl deleted file mode 100644 index f370af277..000000000 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl +++ /dev/null @@ -1,94 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_bridge_mqtt_schema). - --include_lib("typerefl/include/types.hrl"). - --behaviour(hocon_schema). - --export([ namespace/0 - , roots/0 - , fields/1]). - -namespace() -> "bridge_mqtt". - -roots() -> [array("bridge_mqtt")]. - -array(Name) -> {Name, hoconsc:array(hoconsc:ref(?MODULE, Name))}. - -fields("bridge_mqtt") -> - [ {name, sc(string(), #{default => true})} - , {start_type, fun start_type/1} - , {forwards, fun forwards/1} - , {forward_mountpoint, sc(string(), #{})} - , {reconnect_interval, sc(emqx_schema:duration_ms(), #{default => "30s"})} - , {batch_size, sc(integer(), #{default => 100})} - , {queue, sc(hoconsc:ref(?MODULE, "queue"), #{})} - , {config, sc(hoconsc:union([hoconsc:ref(?MODULE, "mqtt"), - hoconsc:ref(?MODULE, "rpc")]), - #{})} - ]; - -fields("mqtt") -> - [ {conn_type, fun conn_type/1} - , {address, sc(string(), #{default => "127.0.0.1:1883"})} - , {proto_ver, fun proto_ver/1} - , {bridge_mode, sc(boolean(), #{default => true})} - , {clientid, sc(string(), #{})} - , {username, sc(string(), #{})} - , {password, sc(string(), #{})} - , {clean_start, sc(boolean(), #{default => true})} - , {keepalive, sc(integer(), #{default => 300})} - , {subscriptions, sc(hoconsc:array(hoconsc:ref(?MODULE, "subscriptions")), #{})} - , {receive_mountpoint, sc(string(), #{})} - , {retry_interval, sc(emqx_schema:duration_ms(), #{default => "30s"})} - , {max_inflight, sc(integer(), #{default => 32})} - ]; - -fields("rpc") -> - [ {conn_type, fun conn_type/1} - , {node, sc(atom(), #{default => 'emqx@127.0.0.1'})} - ]; - -fields("subscriptions") -> - [ {topic, #{type => binary(), nullable => false}} - , {qos, sc(integer(), #{default => 1})} - ]; - -fields("queue") -> - [ {replayq_dir, hoconsc:union([boolean(), string()])} - , {replayq_seg_bytes, sc(emqx_schema:bytesize(), #{default => "100MB"})} - , {replayq_offload_mode, sc(boolean(), #{default => false})} - , {replayq_max_total_bytes, sc(emqx_schema:bytesize(), #{default => "1024MB"})} - ]. - -conn_type(type) -> hoconsc:enum([mqtt, rpc]); -conn_type(_) -> undefined. - -proto_ver(type) -> hoconsc:enum([v3, v4, v5]); -proto_ver(default) -> v4; -proto_ver(_) -> undefined. - -start_type(type) -> hoconsc:enum([auto, manual]); -start_type(default) -> auto; -start_type(_) -> undefined. - -forwards(type) -> hoconsc:array(binary()); -forwards(default) -> []; -forwards(_) -> undefined. - -sc(Type, Meta) -> hoconsc:mk(Type, Meta). diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_sup.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_sup.erl deleted file mode 100644 index c75592edb..000000000 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_sup.erl +++ /dev/null @@ -1,69 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_bridge_mqtt_sup). --behaviour(supervisor). - --include("emqx_bridge_mqtt.hrl"). --include_lib("emqx/include/logger.hrl"). - - -%% APIs --export([ start_link/0 - ]). - --export([ create_bridge/1 - , drop_bridge/1 - , bridges/0 - ]). - -%% supervisor callbacks --export([init/1]). - --define(WORKER_SUP, emqx_bridge_worker_sup). - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -init([]) -> - SupFlag = #{strategy => one_for_one, - intensity => 100, - period => 10}, - {ok, {SupFlag, []}}. - -bridge_spec(Config) -> - #{id => maps:get(name, Config), - start => {emqx_bridge_worker, start_link, [Config]}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_bridge_worker]}. - --spec(bridges() -> [{node(), map()}]). -bridges() -> - [{Name, emqx_bridge_worker:status(Name)} - || {Name, _Pid, _, _} <- supervisor:which_children(?MODULE)]. - -create_bridge(Config) -> - supervisor:start_child(?MODULE, bridge_spec(Config)). - -drop_bridge(Name) -> - case supervisor:terminate_child(?MODULE, Name) of - ok -> - supervisor:delete_child(?MODULE, Name); - {error, Error} -> - {error, Error} - end. diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_rpc.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_rpc.erl deleted file mode 100644 index 33511cc03..000000000 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_rpc.erl +++ /dev/null @@ -1,95 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - -%% @doc This module implements EMQX Bridge transport layer based on gen_rpc. - --module(emqx_bridge_rpc). - --export([ start/1 - , send/2 - , stop/1 - ]). - -%% Internal exports --export([ handle_send/1 - , heartbeat/2 - ]). - --type ack_ref() :: emqx_bridge_worker:ack_ref(). --type batch() :: emqx_bridge_worker:batch(). --define(HEARTBEAT_INTERVAL, timer:seconds(1)). - --define(RPC, emqx_rpc). - -start(#{node := RemoteNode}) -> - case poke(RemoteNode) of - ok -> - Pid = proc_lib:spawn_link(?MODULE, heartbeat, [self(), RemoteNode]), - {ok, #{client_pid => Pid, remote_node => RemoteNode}}; - Error -> - Error - end. - -stop(#{client_pid := Pid}) when is_pid(Pid) -> - Ref = erlang:monitor(process, Pid), - unlink(Pid), - Pid ! stop, - receive - {'DOWN', Ref, process, Pid, _Reason} -> - ok - after - 1000 -> - exit(Pid, kill) - end, - ok. - -%% @doc Callback for `emqx_bridge_connect' behaviour --spec send(#{remote_node := atom(), _ => _}, batch()) -> {ok, ack_ref()} | {error, any()}. -send(#{remote_node := RemoteNode}, Batch) -> - case ?RPC:call(RemoteNode, ?MODULE, handle_send, [Batch]) of - ok -> - Ref = make_ref(), - self() ! {batch_ack, Ref}, - {ok, Ref}; - {badrpc, Reason} -> {error, Reason} - end. - -%% @doc Handle send on receiver side. --spec handle_send(batch()) -> ok. -handle_send(Batch) -> - lists:foreach(fun(Msg) -> emqx_broker:publish(Msg) end, Batch). - -%% @hidden Heartbeat loop -heartbeat(Parent, RemoteNode) -> - Interval = ?HEARTBEAT_INTERVAL, - receive - stop -> exit(normal) - after - Interval -> - case poke(RemoteNode) of - ok -> - ?MODULE:heartbeat(Parent, RemoteNode); - {error, Reason} -> - Parent ! {disconnected, self(), Reason}, - exit(normal) - end - end. - -poke(RemoteNode) -> - case ?RPC:call(RemoteNode, erlang, node, []) of - RemoteNode -> ok; - {badrpc, Reason} -> {error, Reason} - end. diff --git a/apps/emqx_bridge_mqtt/test/emqx_bridge_rpc_tests.erl b/apps/emqx_bridge_mqtt/test/emqx_bridge_rpc_tests.erl deleted file mode 100644 index cbd80ba3d..000000000 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_rpc_tests.erl +++ /dev/null @@ -1,42 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_bridge_rpc_tests). --include_lib("eunit/include/eunit.hrl"). - -send_and_ack_test() -> - %% delegate from emqx_rpc to rpc for unit test - meck:new(emqx_rpc, [passthrough, no_history]), - meck:expect(emqx_rpc, call, 4, - fun(Node, Module, Fun, Args) -> - rpc:call(Node, Module, Fun, Args) - end), - meck:expect(emqx_rpc, cast, 4, - fun(Node, Module, Fun, Args) -> - rpc:cast(Node, Module, Fun, Args) - end), - meck:new(emqx_bridge_worker, [passthrough, no_history]), - try - {ok, #{client_pid := Pid, remote_node := Node}} = emqx_bridge_rpc:start(#{node => node()}), - {ok, Ref} = emqx_bridge_rpc:send(#{remote_node => Node}, []), - receive - {batch_ack, Ref} -> - ok - end, - ok = emqx_bridge_rpc:stop( #{client_pid => Pid}) - after - meck:unload(emqx_rpc) - end. diff --git a/apps/emqx_bridge_mqtt/test/emqx_bridge_stub_conn.erl b/apps/emqx_bridge_mqtt/test/emqx_bridge_stub_conn.erl deleted file mode 100644 index 4c2fde6dd..000000000 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_stub_conn.erl +++ /dev/null @@ -1,38 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_bridge_stub_conn). - --export([ start/1 - , send/2 - , stop/1 - ]). - --type ack_ref() :: emqx_bridge_worker:ack_ref(). --type batch() :: emqx_bridge_worker:batch(). - -start(#{client_pid := Pid} = Cfg) -> - Pid ! {self(), ?MODULE, ready}, - {ok, Cfg}. - -stop(_) -> ok. - -%% @doc Callback for `emqx_bridge_connect' behaviour --spec send(_, batch()) -> {ok, ack_ref()} | {error, any()}. -send(#{client_pid := Pid}, Batch) -> - Ref = make_ref(), - Pid ! {stub_message, self(), Ref, Batch}, - {ok, Ref}. diff --git a/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_SUITE.erl b/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_SUITE.erl deleted file mode 100644 index f3f5d5ceb..000000000 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_SUITE.erl +++ /dev/null @@ -1,372 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_bridge_worker_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("eunit/include/eunit.hrl"). --include_lib("common_test/include/ct.hrl"). --include_lib("emqx/include/emqx_mqtt.hrl"). --include_lib("emqx/include/emqx.hrl"). --include_lib("snabbkaffe/include/snabbkaffe.hrl"). - --define(wait(For, Timeout), emqx_ct_helpers:wait_for(?FUNCTION_NAME, ?LINE, fun() -> For end, Timeout)). - --define(SNK_WAIT(WHAT), ?assertMatch({ok, _}, ?block_until(#{?snk_kind := WHAT}, 2000, 1000))). - -receive_messages(Count) -> - receive_messages(Count, []). - -receive_messages(0, Msgs) -> - Msgs; -receive_messages(Count, Msgs) -> - receive - {publish, Msg} -> - receive_messages(Count-1, [Msg|Msgs]); - _Other -> - receive_messages(Count, Msgs) - after 1000 -> - Msgs - end. - -all() -> - lists:filtermap( - fun({FunName, _Arity}) -> - case atom_to_list(FunName) of - "t_" ++ _ -> {true, FunName}; - _ -> false - end - end, - ?MODULE:module_info(exports)). - -init_per_suite(Config) -> - case node() of - nonode@nohost -> net_kernel:start(['emqx@127.0.0.1', longnames]); - _ -> ok - end, - emqx_ct_helpers:start_apps([emqx_bridge_mqtt]), - emqx_logger:set_log_level(error), - [{log_level, error} | Config]. - -end_per_suite(_Config) -> - emqx_ct_helpers:stop_apps([emqx_bridge_mqtt]). - -init_per_testcase(_TestCase, Config) -> - ok = snabbkaffe:start_trace(), - Config. - -end_per_testcase(_TestCase, _Config) -> - ok = snabbkaffe:stop(). - -t_rpc_mngr(_Config) -> - Name = "rpc_name", - Cfg = #{ - name => Name, - forwards => [<<"mngr">>], - forward_mountpoint => <<"forwarded">>, - start_type => auto, - config => #{ - conn_type => rpc, - node => node() - } - }, - {ok, Pid} = emqx_bridge_mqtt_sup:create_bridge(Cfg), - ?assertEqual([<<"mngr">>], emqx_bridge_worker:get_forwards(Name)), - ?assertEqual(ok, emqx_bridge_worker:ensure_forward_present(Name, "mngr")), - ?assertEqual(ok, emqx_bridge_worker:ensure_forward_present(Name, "mngr2")), - ?assertEqual([<<"mngr2">>, <<"mngr">>], emqx_bridge_worker:get_forwards(Name)), - ?assertEqual(ok, emqx_bridge_worker:ensure_forward_absent(Name, "mngr2")), - ?assertEqual(ok, emqx_bridge_worker:ensure_forward_absent(Name, "mngr3")), - ?assertEqual([<<"mngr">>], emqx_bridge_worker:get_forwards(Name)), - ?assertEqual({error, no_remote_subscription_support}, - emqx_bridge_worker:ensure_subscription_present(Name, <<"t">>, 0)), - ?assertEqual({error, no_remote_subscription_support}, - emqx_bridge_worker:ensure_subscription_absent(Name, <<"t">>)), - ok = emqx_bridge_worker:stop(Pid). - -t_mqtt_mngr(_Config) -> - Name = "mqtt_name", - Cfg = #{ - name => Name, - forwards => [<<"mngr">>], - forward_mountpoint => <<"forwarded">>, - start_type => auto, - config => #{ - address => "127.0.0.1:1883", - conn_type => mqtt, - clientid => <<"client1">>, - keepalive => 300, - subscriptions => [#{topic => <<"t/#">>, qos => 1}] - } - }, - {ok, Pid} = emqx_bridge_mqtt_sup:create_bridge(Cfg), - ?assertEqual([<<"mngr">>], emqx_bridge_worker:get_forwards(Name)), - ?assertEqual(ok, emqx_bridge_worker:ensure_forward_present(Name, "mngr")), - ?assertEqual(ok, emqx_bridge_worker:ensure_forward_present(Name, "mngr2")), - ?assertEqual([<<"mngr2">>, <<"mngr">>], emqx_bridge_worker:get_forwards(Name)), - ?assertEqual(ok, emqx_bridge_worker:ensure_forward_absent(Name, "mngr2")), - ?assertEqual(ok, emqx_bridge_worker:ensure_forward_absent(Name, "mngr3")), - ?assertEqual([<<"mngr">>], emqx_bridge_worker:get_forwards(Name)), - ?assertEqual(ok, emqx_bridge_worker:ensure_subscription_present(Name, <<"t">>, 0)), - ?assertEqual(ok, emqx_bridge_worker:ensure_subscription_absent(Name, <<"t">>)), - ?assertEqual([{<<"t/#">>,1}], emqx_bridge_worker:get_subscriptions(Name)), - ok = emqx_bridge_worker:stop(Pid). - -%% A loopback RPC to local node -t_rpc(_Config) -> - Name = "rpc", - Cfg = #{ - name => Name, - forwards => [<<"t_rpc/#">>], - forward_mountpoint => <<"forwarded">>, - start_type => auto, - config => #{ - conn_type => rpc, - node => node() - } - }, - {ok, Pid} = emqx_bridge_mqtt_sup:create_bridge(Cfg), - {ok, ConnPid} = emqtt:start_link([{clientid, <<"ClientId">>}]), - {ok, _Props} = emqtt:connect(ConnPid), - {ok, _Props, [1]} = emqtt:subscribe(ConnPid, {<<"forwarded/t_rpc/one">>, ?QOS_1}), - timer:sleep(100), - {ok, _PacketId} = emqtt:publish(ConnPid, <<"t_rpc/one">>, <<"hello">>, ?QOS_1), - timer:sleep(100), - ?assertEqual(1, length(receive_messages(1))), - emqtt:disconnect(ConnPid), - emqx_bridge_worker:stop(Pid). - -%% Full data loopback flow explained: -%% mqtt-client ----> local-broker ---(local-subscription)---> -%% bridge(export) --- (mqtt-connection)--> local-broker ---(remote-subscription) --> -%% bridge(import) --> mqtt-client -t_mqtt(_Config) -> - SendToTopic = <<"t_mqtt/one">>, - SendToTopic2 = <<"t_mqtt/two">>, - SendToTopic3 = <<"t_mqtt/three">>, - Mountpoint = <<"forwarded/${node}/">>, - Name = "mqtt", - Cfg = #{ - name => Name, - forwards => [SendToTopic], - forward_mountpoint => Mountpoint, - start_type => auto, - config => #{ - address => "127.0.0.1:1883", - conn_type => mqtt, - clientid => <<"client1">>, - keepalive => 300, - subscriptions => [#{topic => SendToTopic2, qos => 1}], - receive_mountpoint => <<"receive/aws/">> - }, - queue => #{ - replayq_dir => "data/t_mqtt/", - replayq_seg_bytes => 10000, - batch_bytes_limit => 1000, - batch_count_limit => 10 - } - }, - {ok, Pid} = emqx_bridge_mqtt_sup:create_bridge(Cfg), - ?assertEqual([{SendToTopic2, 1}], emqx_bridge_worker:get_subscriptions(Name)), - ok = emqx_bridge_worker:ensure_subscription_present(Name, SendToTopic3, _QoS = 1), - ?assertEqual([{SendToTopic3, 1},{SendToTopic2, 1}], - emqx_bridge_worker:get_subscriptions(Name)), - {ok, ConnPid} = emqtt:start_link([{clientid, <<"client-1">>}]), - {ok, _Props} = emqtt:connect(ConnPid), - emqtt:subscribe(ConnPid, <<"forwarded/+/t_mqtt/one">>, 1), - %% message from a different client, to avoid getting terminated by no-local - Max = 10, - Msgs = lists:seq(1, Max), - lists:foreach(fun(I) -> - {ok, _PacketId} = emqtt:publish(ConnPid, SendToTopic, integer_to_binary(I), ?QOS_1) - end, Msgs), - ?assertEqual(10, length(receive_messages(200))), - - emqtt:subscribe(ConnPid, <<"receive/aws/t_mqtt/two">>, 1), - %% message from a different client, to avoid getting terminated by no-local - Max = 10, - Msgs = lists:seq(1, Max), - lists:foreach(fun(I) -> - {ok, _PacketId} = emqtt:publish(ConnPid, SendToTopic2, integer_to_binary(I), ?QOS_1) - end, Msgs), - ?assertEqual(10, length(receive_messages(200))), - - emqtt:disconnect(ConnPid), - ok = emqx_bridge_worker:stop(Pid). - -t_stub_normal(Config) when is_list(Config) -> - Name = "stub_normal", - Cfg = #{ - name => Name, - forwards => [<<"t_stub_normal/#">>], - forward_mountpoint => <<"forwarded">>, - start_type => auto, - config => #{ - conn_type => emqx_bridge_stub_conn, - client_pid => self() - } - }, - {ok, Pid} = emqx_bridge_mqtt_sup:create_bridge(Cfg), - receive - {Pid, emqx_bridge_stub_conn, ready} -> ok - after - 5000 -> - error(timeout) - end, - {ok, ConnPid} = emqtt:start_link([{clientid, <<"ClientId">>}]), - {ok, _} = emqtt:connect(ConnPid), - {ok, _PacketId} = emqtt:publish(ConnPid, <<"t_stub_normal/one">>, <<"hello">>, ?QOS_1), - receive - {stub_message, WorkerPid, BatchRef, _Batch} -> - WorkerPid ! {batch_ack, BatchRef}, - ok - after - 5000 -> - error(timeout) - end, - ?SNK_WAIT(inflight_drained), - ?SNK_WAIT(replayq_drained), - emqtt:disconnect(ConnPid), - ok = emqx_bridge_worker:stop(Pid). - -t_stub_overflow(_Config) -> - Topic = <<"t_stub_overflow/one">>, - MaxInflight = 20, - Name = "stub_overflow", - Cfg = #{ - name => Name, - forwards => [<<"t_stub_overflow/one">>], - forward_mountpoint => <<"forwarded">>, - start_type => auto, - max_inflight => MaxInflight, - config => #{ - conn_type => emqx_bridge_stub_conn, - client_pid => self() - } - }, - {ok, Worker} = emqx_bridge_mqtt_sup:create_bridge(Cfg), - {ok, ConnPid} = emqtt:start_link([{clientid, <<"ClientId">>}]), - {ok, _} = emqtt:connect(ConnPid), - lists:foreach( - fun(I) -> - Data = integer_to_binary(I), - _ = emqtt:publish(ConnPid, Topic, Data, ?QOS_1) - end, lists:seq(1, MaxInflight * 2)), - ?SNK_WAIT(inflight_full), - Acks = stub_receive(MaxInflight), - lists:foreach(fun({Pid, Ref}) -> Pid ! {batch_ack, Ref} end, Acks), - Acks2 = stub_receive(MaxInflight), - lists:foreach(fun({Pid, Ref}) -> Pid ! {batch_ack, Ref} end, Acks2), - ?SNK_WAIT(inflight_drained), - ?SNK_WAIT(replayq_drained), - emqtt:disconnect(ConnPid), - ok = emqx_bridge_worker:stop(Worker). - -t_stub_random_order(_Config) -> - Topic = <<"t_stub_random_order/a">>, - MaxInflight = 10, - Name = "stub_random_order", - Cfg = #{ - name => Name, - forwards => [Topic], - forward_mountpoint => <<"forwarded">>, - start_type => auto, - max_inflight => MaxInflight, - config => #{ - conn_type => emqx_bridge_stub_conn, - client_pid => self() - } - }, - {ok, Worker} = emqx_bridge_mqtt_sup:create_bridge(Cfg), - ClientId = <<"ClientId">>, - {ok, ConnPid} = emqtt:start_link([{clientid, ClientId}]), - {ok, _} = emqtt:connect(ConnPid), - lists:foreach( - fun(I) -> - Data = integer_to_binary(I), - _ = emqtt:publish(ConnPid, Topic, Data, ?QOS_1) - end, lists:seq(1, MaxInflight)), - Acks = stub_receive(MaxInflight), - lists:foreach(fun({Pid, Ref}) -> Pid ! {batch_ack, Ref} end, - lists:reverse(Acks)), - ?SNK_WAIT(inflight_drained), - ?SNK_WAIT(replayq_drained), - emqtt:disconnect(ConnPid), - ok = emqx_bridge_worker:stop(Worker). - -t_stub_retry_inflight(_Config) -> - Topic = <<"to_stub_retry_inflight/a">>, - MaxInflight = 10, - Name = "stub_retry_inflight", - Cfg = #{ - name => Name, - forwards => [Topic], - forward_mountpoint => <<"forwarded">>, - reconnect_interval => 10, - start_type => auto, - max_inflight => MaxInflight, - config => #{ - conn_type => emqx_bridge_stub_conn, - client_pid => self() - } - }, - {ok, Worker} = emqx_bridge_mqtt_sup:create_bridge(Cfg), - ClientId = <<"ClientId2">>, - case ?block_until(#{?snk_kind := connected, inflight := 0}, 2000, 1000) of - {ok, #{inflight := 0}} -> ok; - Other -> ct:fail("~p", [Other]) - end, - {ok, ConnPid} = emqtt:start_link([{clientid, ClientId}]), - {ok, _} = emqtt:connect(ConnPid), - lists:foreach( - fun(I) -> - Data = integer_to_binary(I), - _ = emqtt:publish(ConnPid, Topic, Data, ?QOS_1) - end, lists:seq(1, MaxInflight)), - %% receive acks but do not ack - Acks1 = stub_receive(MaxInflight), - ?assertEqual(MaxInflight, length(Acks1)), - %% simulate a disconnect - Worker ! {disconnected, self(), test}, - ?SNK_WAIT(disconnected), - case ?block_until(#{?snk_kind := connected, inflight := MaxInflight}, 2000, 20) of - {ok, _} -> ok; - Error -> ct:fail("~p", [Error]) - end, - %% expect worker to retry inflight, so to receive acks again - Acks2 = stub_receive(MaxInflight), - ?assertEqual(MaxInflight, length(Acks2)), - lists:foreach(fun({Pid, Ref}) -> Pid ! {batch_ack, Ref} end, - lists:reverse(Acks2)), - ?SNK_WAIT(inflight_drained), - ?SNK_WAIT(replayq_drained), - emqtt:disconnect(ConnPid), - ok = emqx_bridge_worker:stop(Worker). - -stub_receive(N) -> - stub_receive(N, []). - -stub_receive(0, Acc) -> lists:reverse(Acc); -stub_receive(N, Acc) -> - receive - {stub_message, WorkerPid, BatchRef, _Batch} -> - stub_receive(N - 1, [{WorkerPid, BatchRef} | Acc]) - after - 5000 -> - lists:reverse(Acc) - end. diff --git a/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_tests.erl b/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_tests.erl deleted file mode 100644 index ffa2e9ee5..000000000 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_worker_tests.erl +++ /dev/null @@ -1,135 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_bridge_worker_tests). - --include_lib("eunit/include/eunit.hrl"). --include_lib("emqx/include/emqx.hrl"). --include_lib("emqx/include/emqx_mqtt.hrl"). - --define(BRIDGE_NAME, test). --define(BRIDGE_REG_NAME, emqx_bridge_worker_test). --define(WAIT(PATTERN, TIMEOUT), - receive - PATTERN -> - ok - after - TIMEOUT -> - error(timeout) - end). - -%% stub callbacks --export([start/1, send/2, stop/1]). - -start(#{connect_result := Result, test_pid := Pid, test_ref := Ref}) -> - case is_pid(Pid) of - true -> Pid ! {connection_start_attempt, Ref}; - false -> ok - end, - Result. - -send(SendFun, Batch) when is_function(SendFun, 2) -> - SendFun(Batch). - -stop(_Pid) -> ok. - -%% bridge worker should retry connecting remote node indefinitely -% reconnect_test() -> -% emqx_metrics:start_link(), -% emqx_bridge_worker:register_metrics(), -% Ref = make_ref(), -% Config = make_config(Ref, self(), {error, test}), -% {ok, Pid} = emqx_bridge_worker:start_link(?BRIDGE_NAME, Config), -% %% assert name registered -% ?assertEqual(Pid, whereis(?BRIDGE_REG_NAME)), -% ?WAIT({connection_start_attempt, Ref}, 1000), -% %% expect same message again -% ?WAIT({connection_start_attempt, Ref}, 1000), -% ok = emqx_bridge_worker:stop(?BRIDGE_REG_NAME), -% emqx_metrics:stop(), -% ok. - -%% connect first, disconnect, then connect again -disturbance_test() -> - emqx_metrics:start_link(), - emqx_bridge_worker:register_metrics(), - Ref = make_ref(), - TestPid = self(), - Config = make_config(Ref, TestPid, {ok, #{client_pid => TestPid}}), - {ok, Pid} = emqx_bridge_worker:start_link(Config#{name => disturbance}), - ?assertEqual(Pid, whereis(emqx_bridge_worker_disturbance)), - ?WAIT({connection_start_attempt, Ref}, 1000), - Pid ! {disconnected, TestPid, test}, - ?WAIT({connection_start_attempt, Ref}, 1000), - emqx_metrics:stop(), - ok = emqx_bridge_worker:stop(Pid). - -% % %% buffer should continue taking in messages when disconnected -% buffer_when_disconnected_test_() -> -% {timeout, 10000, fun test_buffer_when_disconnected/0}. - -% test_buffer_when_disconnected() -> -% Ref = make_ref(), -% Nums = lists:seq(1, 100), -% Sender = spawn_link(fun() -> receive {bridge, Pid} -> sender_loop(Pid, Nums, _Interval = 5) end end), -% SenderMref = monitor(process, Sender), -% Receiver = spawn_link(fun() -> receive {bridge, Pid} -> receiver_loop(Pid, Nums, _Interval = 1) end end), -% ReceiverMref = monitor(process, Receiver), -% SendFun = fun(Batch) -> -% BatchRef = make_ref(), -% Receiver ! {batch, BatchRef, Batch}, -% {ok, BatchRef} -% end, -% Config0 = make_config(Ref, false, {ok, #{client_pid => undefined}}), -% Config = Config0#{reconnect_delay_ms => 100}, -% emqx_metrics:start_link(), -% emqx_bridge_worker:register_metrics(), -% {ok, Pid} = emqx_bridge_worker:start_link(?BRIDGE_NAME, Config), -% Sender ! {bridge, Pid}, -% Receiver ! {bridge, Pid}, -% ?assertEqual(Pid, whereis(?BRIDGE_REG_NAME)), -% Pid ! {disconnected, Ref, test}, -% ?WAIT({'DOWN', SenderMref, process, Sender, normal}, 5000), -% ?WAIT({'DOWN', ReceiverMref, process, Receiver, normal}, 1000), -% ok = emqx_bridge_worker:stop(?BRIDGE_REG_NAME), -% emqx_metrics:stop(). - -manual_start_stop_test() -> - emqx_metrics:start_link(), - emqx_bridge_worker:register_metrics(), - Ref = make_ref(), - TestPid = self(), - BridgeName = manual_start_stop, - Config0 = make_config(Ref, TestPid, {ok, #{client_pid => TestPid}}), - Config = Config0#{start_type := manual}, - {ok, Pid} = emqx_bridge_worker:start_link(Config#{name => BridgeName}), - %% call ensure_started again should yeld the same result - ok = emqx_bridge_worker:ensure_started(BridgeName), - emqx_bridge_worker:ensure_stopped(BridgeName), - emqx_metrics:stop(), - ok = emqx_bridge_worker:stop(Pid). - -make_config(Ref, TestPid, Result) -> - #{ - start_type => auto, - reconnect_interval => 50, - config => #{ - test_pid => TestPid, - test_ref => Ref, - conn_type => ?MODULE, - connect_result => Result - } - }. diff --git a/apps/emqx_connector/rebar.config b/apps/emqx_connector/rebar.config index cbeff37eb..fd2329cbd 100644 --- a/apps/emqx_connector/rebar.config +++ b/apps/emqx_connector/rebar.config @@ -17,7 +17,8 @@ %% By accident, We have always been using the upstream fork due to %% eredis_cluster's dependency getting resolved earlier. %% Here we pin 1.5.2 to avoid surprises in the future. - {poolboy, {git, "https://github.com/emqx/poolboy.git", {tag, "1.5.2"}}} + {poolboy, {git, "https://github.com/emqx/poolboy.git", {tag, "1.5.2"}}}, + {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.4.3"}}} ]}. {shell, [ diff --git a/apps/emqx_connector/src/emqx_connector.app.src b/apps/emqx_connector/src/emqx_connector.app.src index 5e1ca2ca8..f4481dc2c 100644 --- a/apps/emqx_connector/src/emqx_connector.app.src +++ b/apps/emqx_connector/src/emqx_connector.app.src @@ -13,7 +13,8 @@ epgsql, mysql, mongodb, - emqx + emqx, + emqtt ]}, {env,[]}, {modules, []}, diff --git a/apps/emqx_connector/src/emqx_connector_app.erl b/apps/emqx_connector/src/emqx_connector_app.erl index 64e6b8109..4de078076 100644 --- a/apps/emqx_connector/src/emqx_connector_app.erl +++ b/apps/emqx_connector/src/emqx_connector_app.erl @@ -21,6 +21,7 @@ -export([start/2, stop/1]). start(_StartType, _StartArgs) -> + emqx_connector_mqtt_worker:register_metrics(), emqx_connector_sup:start_link(). stop(_State) -> diff --git a/apps/emqx_connector/src/emqx_connector_mqtt.erl b/apps/emqx_connector/src/emqx_connector_mqtt.erl index 1f2ee0b12..bbd347ae9 100644 --- a/apps/emqx_connector/src/emqx_connector_mqtt.erl +++ b/apps/emqx_connector/src/emqx_connector_mqtt.erl @@ -18,6 +18,16 @@ -include_lib("typerefl/include/types.hrl"). -include_lib("emqx_resource/include/emqx_resource_behaviour.hrl"). +-behaviour(supervisor). + +%% API and callbacks for supervisor +-export([ start_link/0 + , init/1 + , create_bridge/1 + , drop_bridge/1 + , bridges/0 + ]). + %% callbacks of behaviour emqx_resource -export([ on_start/2 , on_stop/2 @@ -36,55 +46,42 @@ roots() -> [{config, #{type => hoconsc:ref(?MODULE, "config")}}]. fields("config") -> - [ {server, hoconsc:mk(emqx_schema:ip_port(), #{default => "127.0.0.1:1883"})} - , {reconnect_interval, hoconsc:mk(emqx_schema:duration_ms(), #{default => "30s"})} - , {proto_ver, fun proto_ver/1} - , {bridge_mode, hoconsc:mk(boolean(), #{default => true})} - , {clientid_prefix, hoconsc:mk(string(), #{default => ""})} - , {username, hoconsc:mk(string())} - , {password, hoconsc:mk(string())} - , {clean_start, hoconsc:mk(boolean(), #{default => true})} - , {keepalive, hoconsc:mk(integer(), #{default => 300})} - , {retry_interval, hoconsc:mk(emqx_schema:duration_ms(), #{default => "30s"})} - , {max_inflight, hoconsc:mk(integer(), #{default => 32})} - , {replayq, hoconsc:mk(hoconsc:ref(?MODULE, "replayq"))} - , {in, hoconsc:mk(hoconsc:array(hoconsc:ref(?MODULE, "in")), #{default => []})} - , {out, hoconsc:mk(hoconsc:array(hoconsc:ref(?MODULE, "out")), #{default => []})} - ] ++ emqx_connector_schema_lib:ssl_fields(); + emqx_connector_mqtt_schema:fields("config"). -fields("in") -> - [ {subscribe_remote_topic, #{type => binary(), nullable => false}} - , {local_topic, hoconsc:mk(binary(), #{default => <<"${topic}">>})} - , {subscribe_qos, hoconsc:mk(qos(), #{default => 1})} - ] ++ common_inout_confs(); +%% =================================================================== +%% supervisor APIs +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). -fields("out") -> - [ {subscribe_local_topic, #{type => binary(), nullable => false}} - , {remote_topic, hoconsc:mk(binary(), #{default => <<"${topic}">>})} - ] ++ common_inout_confs(); +init([]) -> + SupFlag = #{strategy => one_for_one, + intensity => 100, + period => 10}, + {ok, {SupFlag, []}}. -fields("replayq") -> - [ {dir, hoconsc:union([boolean(), string()])} - , {seg_bytes, hoconsc:mk(emqx_schema:bytesize(), #{default => "100MB"})} - , {offload, hoconsc:mk(boolean(), #{default => false})} - , {max_total_bytes, hoconsc:mk(emqx_schema:bytesize(), #{default => "1024MB"})} - ]. +bridge_spec(Config) -> + #{id => maps:get(name, Config), + start => {emqx_connector_mqtt_worker, start_link, [Config]}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [emqx_connector_mqtt_worker]}. -common_inout_confs() -> - [{id, #{type => binary(), nullable => false}}] ++ publish_confs(). +-spec(bridges() -> [{node(), map()}]). +bridges() -> + [{Name, emqx_connector_mqtt_worker:status(Name)} + || {Name, _Pid, _, _} <- supervisor:which_children(?MODULE)]. -publish_confs() -> - [ {qos, hoconsc:mk(qos(), #{default => <<"${qos}">>})} - , {retain, hoconsc:mk(hoconsc:union([boolean(), binary()]), #{default => <<"${retain}">>})} - , {payload, hoconsc:mk(binary(), #{default => <<"${payload}">>})} - ]. +create_bridge(Config) -> + supervisor:start_child(?MODULE, bridge_spec(Config)). -qos() -> - hoconsc:union([typerefl:integer(0), typerefl:integer(1), typerefl:integer(2), binary()]). - -proto_ver(type) -> hoconsc:enum([v3, v4, v5]); -proto_ver(default) -> v4; -proto_ver(_) -> undefined. +drop_bridge(Name) -> + case supervisor:terminate_child(?MODULE, Name) of + ok -> + supervisor:delete_child(?MODULE, Name); + {error, Error} -> + {error, Error} + end. %% =================================================================== on_start(InstId, Conf) -> @@ -105,7 +102,7 @@ on_start(InstId, Conf) -> on_stop(InstId, #{}) -> logger:info("stopping mqtt connector: ~p", [InstId]), - case emqx_bridge_mqtt_sup:drop_bridge(InstId) of + case ?MODULE:drop_bridge(InstId) of ok -> ok; {error, not_found} -> ok; {error, Reason} -> @@ -124,7 +121,7 @@ on_query(InstId, {publish_to_remote, Msg}, _AfterQuery, _State) -> logger:debug("publish to remote node, connector: ~p, msg: ~p", [InstId, Msg]). on_health_check(_InstId, #{sub_bridges := NameList} = State) -> - Results = [{Name, emqx_bridge_worker:ping(Name)} || Name <- NameList], + Results = [{Name, emqx_connector_mqtt_worker:ping(Name)} || Name <- NameList], case lists:all(fun({_, pong}) -> true; ({_, _}) -> false end, Results) of true -> {ok, State}; false -> {error, {some_sub_bridge_down, Results}, State} @@ -155,7 +152,7 @@ create_channel(#{subscribe_local_topic := _, id := BridgeId} = OutConf, NamePref subscriptions => undefined, forwards => OutConf}). create_sub_bridge(#{name := Name} = Conf) -> - case emqx_bridge_mqtt_sup:create_bridge(Conf) of + case ?MODULE:create_bridge(Conf) of {ok, _Pid} -> start_sub_bridge(Name); {error, {already_started, _Pid}} -> @@ -165,7 +162,7 @@ create_sub_bridge(#{name := Name} = Conf) -> end. start_sub_bridge(Name) -> - case emqx_bridge_worker:ensure_started(Name) of + case emqx_connector_mqtt_worker:ensure_started(Name) of ok -> {ok, Name}; {error, Reason} -> {error, Reason} end. diff --git a/apps/emqx_connector/src/emqx_connector_sup.erl b/apps/emqx_connector/src/emqx_connector_sup.erl index 603b9a8ad..a24a97b8f 100644 --- a/apps/emqx_connector/src/emqx_connector_sup.erl +++ b/apps/emqx_connector/src/emqx_connector_sup.erl @@ -28,9 +28,19 @@ start_link() -> init([]) -> SupFlags = #{strategy => one_for_all, - intensity => 0, - period => 1}, - ChildSpecs = [], + intensity => 5, + period => 20}, + ChildSpecs = [ + child_spec(emqx_connector_mqtt) + ], {ok, {SupFlags, ChildSpecs}}. +child_spec(Mod) -> + #{id => Mod, + start => {Mod, start_link, []}, + restart => permanent, + shutdown => 3000, + type => supervisor, + modules => [Mod]}. + %% internal functions diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl similarity index 97% rename from apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.erl rename to apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl index 2609e8bea..c8b7ff77b 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl @@ -16,7 +16,7 @@ %% @doc This module implements EMQX Bridge transport layer on top of MQTT protocol --module(emqx_bridge_mqtt). +-module(emqx_connector_mqtt_mod). -export([ start/1 , send/2 @@ -51,7 +51,7 @@ start(Config) -> {Host, Port} = maps:get(server, Config), Mountpoint = maps:get(receive_mountpoint, Config, undefined), Subscriptions = maps:get(subscriptions, Config), - Vars = emqx_bridge_msg:make_pub_vars(Mountpoint, Subscriptions), + Vars = emqx_connector_mqtt_msg:make_pub_vars(Mountpoint, Subscriptions), Handlers = make_hdlr(Parent, Vars), Config1 = Config#{ msg_handler => Handlers, @@ -161,7 +161,7 @@ handle_publish(Msg, undefined) -> ?LOG(error, "cannot publish to local broker as 'bridge.mqtt..in' not configured, msg: ~p", [Msg]); handle_publish(Msg, Vars) -> ?LOG(debug, "publish to local broker, msg: ~p, vars: ~p", [Msg, Vars]), - emqx_broker:publish(emqx_bridge_msg:to_broker_msg(Msg, Vars)). + emqx_broker:publish(emqx_connector_mqtt_msg:to_broker_msg(Msg, Vars)). handle_disconnected(Reason, Parent) -> Parent ! {disconnected, self(), Reason}. diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_msg.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl similarity index 92% rename from apps/emqx_bridge_mqtt/src/emqx_bridge_msg.erl rename to apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl index e78844ed4..425fa06f1 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_msg.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_bridge_msg). +-module(emqx_connector_mqtt_msg). -export([ to_binary/1 , from_binary/1 @@ -28,7 +28,6 @@ -include_lib("emqx/include/emqx.hrl"). --include_lib("emqx_bridge_mqtt/include/emqx_bridge_mqtt.hrl"). -include_lib("emqtt/include/emqtt.hrl"). @@ -56,13 +55,13 @@ make_pub_vars(Mountpoint, #{payload := _, qos := _, retain := _, local_topic := %% Shame that we have to know the callback module here %% would be great if we can get rid of #mqtt_msg{} record %% and use #message{} in all places. --spec to_remote_msg(emqx_bridge_rpc | emqx_bridge_worker, msg(), variables()) +-spec to_remote_msg(emqx_bridge_rpc | emqx_connector_mqtt_mod, msg(), variables()) -> exp_msg(). -to_remote_msg(emqx_bridge_mqtt, #message{flags = Flags0} = Msg, Vars) -> +to_remote_msg(emqx_connector_mqtt_mod, #message{flags = Flags0} = Msg, Vars) -> Retain0 = maps:get(retain, Flags0, false), MapMsg = maps:put(retain, Retain0, emqx_message:to_map(Msg)), - to_remote_msg(emqx_bridge_mqtt, MapMsg, Vars); -to_remote_msg(emqx_bridge_mqtt, MapMsg, #{topic := TopicToken, payload := PayloadToken, + to_remote_msg(emqx_connector_mqtt_mod, MapMsg, Vars); +to_remote_msg(emqx_connector_mqtt_mod, MapMsg, #{topic := TopicToken, payload := PayloadToken, qos := QoSToken, retain := RetainToken, mountpoint := Mountpoint}) when is_map(MapMsg) -> Topic = replace_vars_in_str(TopicToken, MapMsg), Payload = replace_vars_in_str(PayloadToken, MapMsg), diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl new file mode 100644 index 000000000..ed7fd4408 --- /dev/null +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_schema.erl @@ -0,0 +1,78 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_connector_mqtt_schema). + +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ roots/0 + , fields/1]). + +roots() -> + [{config, #{type => hoconsc:ref(?MODULE, "config")}}]. + +fields("config") -> + [ {server, hoconsc:mk(emqx_schema:ip_port(), #{default => "127.0.0.1:1883"})} + , {reconnect_interval, hoconsc:mk(emqx_schema:duration_ms(), #{default => "30s"})} + , {proto_ver, fun proto_ver/1} + , {bridge_mode, hoconsc:mk(boolean(), #{default => true})} + , {clientid_prefix, hoconsc:mk(string(), #{default => ""})} + , {username, hoconsc:mk(string())} + , {password, hoconsc:mk(string())} + , {clean_start, hoconsc:mk(boolean(), #{default => true})} + , {keepalive, hoconsc:mk(integer(), #{default => 300})} + , {retry_interval, hoconsc:mk(emqx_schema:duration_ms(), #{default => "30s"})} + , {max_inflight, hoconsc:mk(integer(), #{default => 32})} + , {replayq, hoconsc:mk(hoconsc:ref(?MODULE, "replayq"))} + , {in, hoconsc:mk(hoconsc:array(hoconsc:ref(?MODULE, "in")), #{default => []})} + , {out, hoconsc:mk(hoconsc:array(hoconsc:ref(?MODULE, "out")), #{default => []})} + ] ++ emqx_connector_schema_lib:ssl_fields(); + +fields("in") -> + [ {subscribe_remote_topic, #{type => binary(), nullable => false}} + , {local_topic, hoconsc:mk(binary(), #{default => <<"${topic}">>})} + , {subscribe_qos, hoconsc:mk(qos(), #{default => 1})} + ] ++ common_inout_confs(); + +fields("out") -> + [ {subscribe_local_topic, #{type => binary(), nullable => false}} + , {remote_topic, hoconsc:mk(binary(), #{default => <<"${topic}">>})} + ] ++ common_inout_confs(); + +fields("replayq") -> + [ {dir, hoconsc:union([boolean(), string()])} + , {seg_bytes, hoconsc:mk(emqx_schema:bytesize(), #{default => "100MB"})} + , {offload, hoconsc:mk(boolean(), #{default => false})} + , {max_total_bytes, hoconsc:mk(emqx_schema:bytesize(), #{default => "1024MB"})} + ]. + +common_inout_confs() -> + [{id, #{type => binary(), nullable => false}}] ++ publish_confs(). + +publish_confs() -> + [ {qos, hoconsc:mk(qos(), #{default => <<"${qos}">>})} + , {retain, hoconsc:mk(hoconsc:union([boolean(), binary()]), #{default => <<"${retain}">>})} + , {payload, hoconsc:mk(binary(), #{default => <<"${payload}">>})} + ]. + +qos() -> + hoconsc:union([typerefl:integer(0), typerefl:integer(1), typerefl:integer(2), binary()]). + +proto_ver(type) -> hoconsc:enum([v3, v4, v5]); +proto_ver(default) -> v4; +proto_ver(_) -> undefined. diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_worker.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl similarity index 97% rename from apps/emqx_bridge_mqtt/src/emqx_bridge_worker.erl rename to apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl index ac861d08f..83f7ce746 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_worker.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl @@ -19,7 +19,7 @@ %% to remote MQTT node/cluster via `connection' transport layer. %% In case `REMOTE' is also an EMQX node, `connection' is recommended to be %% the `gen_rpc' based implementation `emqx_bridge_rpc'. Otherwise `connection' -%% has to be `emqx_bridge_mqtt'. +%% has to be `emqx_connector_mqtt_mod'. %% %% ``` %% +------+ +--------+ @@ -59,7 +59,7 @@ %% NOTES: %% * Local messages are all normalised to QoS-1 when exporting to remote --module(emqx_bridge_worker). +-module(emqx_connector_mqtt_worker). -behaviour(gen_statem). -include_lib("snabbkaffe/include/snabbkaffe.hrl"). @@ -106,7 +106,7 @@ -type id() :: atom() | string() | pid(). -type qos() :: emqx_mqtt_types:qos(). -type config() :: map(). --type batch() :: [emqx_bridge_msg:exp_msg()]. +-type batch() :: [emqx_connector_mqtt_msg:exp_msg()]. -type ack_ref() :: term(). -type topic() :: emqx_topic:topic(). @@ -222,7 +222,7 @@ open_replayq(Name, QCfg) -> false -> #{dir => filename:join([Dir, node(), Name]), seg_bytes => SegBytes, max_total_size => MaxTotalSize} end, - replayq:open(QueueConfig#{sizer => fun emqx_bridge_msg:estimate_size/1, + replayq:open(QueueConfig#{sizer => fun emqx_connector_mqtt_msg:estimate_size/1, marshaller => fun ?MODULE:msg_marshaller/1}). pre_process_opts(#{subscriptions := InConf, forwards := OutConf} = ConnectOpts) -> @@ -412,10 +412,10 @@ do_send(#{inflight := Inflight, mountpoint := Mountpoint, connect_opts := #{forwards := Forwards}, if_record_metrics := IfRecordMetrics} = State, QAckRef, [_ | _] = Batch) -> - Vars = emqx_bridge_msg:make_pub_vars(Mountpoint, Forwards), + Vars = emqx_connector_mqtt_msg:make_pub_vars(Mountpoint, Forwards), ExportMsg = fun(Message) -> bridges_metrics_inc(IfRecordMetrics, 'bridge.mqtt.message_sent'), - emqx_bridge_msg:to_remote_msg(Module, Message, Vars) + emqx_connector_mqtt_msg:to_remote_msg(Module, Message, Vars) end, ?LOG(debug, "publish to remote broker, msg: ~p, vars: ~p", [Batch, Vars]), case Module:send(Connection, [ExportMsg(M) || M <- Batch]) of @@ -501,8 +501,8 @@ disconnect(State) -> State. %% Called only when replayq needs to dump it to disk. -msg_marshaller(Bin) when is_binary(Bin) -> emqx_bridge_msg:from_binary(Bin); -msg_marshaller(Msg) -> emqx_bridge_msg:to_binary(Msg). +msg_marshaller(Bin) when is_binary(Bin) -> emqx_connector_mqtt_msg:from_binary(Bin); +msg_marshaller(Msg) -> emqx_connector_mqtt_msg:to_binary(Msg). format_mountpoint(undefined) -> undefined; @@ -541,7 +541,7 @@ is_sensitive(_) -> false. conn_type(rpc) -> emqx_bridge_rpc; conn_type(mqtt) -> - emqx_bridge_mqtt; + emqx_connector_mqtt_mod; conn_type(Mod) when is_atom(Mod) -> Mod. diff --git a/apps/emqx_bridge_mqtt/test/emqx_bridge_mqtt_tests.erl b/apps/emqx_connector/test/emqx_connetor_mqtt_tests.erl similarity index 89% rename from apps/emqx_bridge_mqtt/test/emqx_bridge_mqtt_tests.erl rename to apps/emqx_connector/test/emqx_connetor_mqtt_tests.erl index 5babe0ed9..7943f5a77 100644 --- a/apps/emqx_bridge_mqtt/test/emqx_bridge_mqtt_tests.erl +++ b/apps/emqx_connector/test/emqx_connetor_mqtt_tests.erl @@ -37,11 +37,11 @@ send_and_ack_test() -> try Max = 1, Batch = lists:seq(1, Max), - {ok, Conn} = emqx_bridge_mqtt:start(#{address => "127.0.0.1:1883"}), + {ok, Conn} = emqx_connector_mqtt_mod:start(#{address => "127.0.0.1:1883"}), % %% return last packet id as batch reference - {ok, _AckRef} = emqx_bridge_mqtt:send(Conn, Batch), + {ok, _AckRef} = emqx_connector_mqtt_mod:send(Conn, Batch), - ok = emqx_bridge_mqtt:stop(Conn) + ok = emqx_connector_mqtt_mod:stop(Conn) after meck:unload(emqtt) end. diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index 1f5d639cd..614609875 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -48,7 +48,6 @@ , emqx_statsd_schema , emqx_authz_schema , emqx_auto_subscribe_schema - , emqx_bridge_mqtt_schema , emqx_modules_schema , emqx_dashboard_schema , emqx_gateway_schema diff --git a/apps/emqx_rule_actions/src/emqx_bridge_mqtt_actions.erl b/apps/emqx_rule_actions/src/emqx_bridge_mqtt_actions.erl index fc5f89091..ce1192579 100644 --- a/apps/emqx_rule_actions/src/emqx_bridge_mqtt_actions.erl +++ b/apps/emqx_rule_actions/src/emqx_bridge_mqtt_actions.erl @@ -411,7 +411,7 @@ test_resource_status(PoolName) -> IsConnected = fun(Worker) -> case ecpool_worker:client(Worker) of {ok, Bridge} -> - try emqx_bridge_worker:status(Bridge) of + try emqx_connector_mqtt_worker:status(Bridge) of connected -> true; _ -> false catch _Error:_Reason -> @@ -524,7 +524,7 @@ connect(Options = #{disk_cache := DiskCache, ecpool_worker_id := Id, pool_name : end end, Options2 = maps:without([ecpool_worker_id, pool_name, append], Options1), - emqx_bridge_worker:start_link(Options2#{name => name(Pool, Id)}). + emqx_connector_mqtt_worker:start_link(Options2#{name => name(Pool, Id)}). name(Pool, Id) -> list_to_atom(atom_to_list(Pool) ++ ":" ++ integer_to_list(Id)). pool_name(ResId) -> diff --git a/rebar.config.erl b/rebar.config.erl index 65b469c1c..3f4d86f37 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -272,7 +272,6 @@ relx_apps(ReleaseType) -> , emqx_bridge , emqx_rule_engine , emqx_rule_actions - , emqx_bridge_mqtt , emqx_modules , emqx_management , emqx_dashboard From 1ecec5ef3a32b3c25182ae4e2a2fb6208c28379b Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 9 Sep 2021 19:23:06 +0800 Subject: [PATCH 107/109] refactor(bridges): move some test cases from old emqx_bridge_mqtt app --- .../src/emqx_connector_mqtt.erl | 1 - .../src/mqtt/emqx_connector_mqtt_mod.erl | 2 +- .../src/mqtt/emqx_connector_mqtt_msg.erl | 12 +- .../src/mqtt/emqx_connector_mqtt_worker.erl | 46 ++---- ...ests.erl => emqx_connector_mqtt_tests.erl} | 4 +- .../test/emqx_connector_mqtt_worker_tests.erl | 149 ++++++++++++++++++ ...TE.erl => emqx_plugin_libs_rule_SUITE.erl} | 2 +- 7 files changed, 173 insertions(+), 43 deletions(-) rename apps/emqx_connector/test/{emqx_connetor_mqtt_tests.erl => emqx_connector_mqtt_tests.erl} (93%) create mode 100644 apps/emqx_connector/test/emqx_connector_mqtt_worker_tests.erl rename apps/emqx_plugin_libs/test/{emqx_rule_libs_rule_SUITE.erl => emqx_plugin_libs_rule_SUITE.erl} (99%) diff --git a/apps/emqx_connector/src/emqx_connector_mqtt.erl b/apps/emqx_connector/src/emqx_connector_mqtt.erl index bbd347ae9..6631fd23a 100644 --- a/apps/emqx_connector/src/emqx_connector_mqtt.erl +++ b/apps/emqx_connector/src/emqx_connector_mqtt.erl @@ -182,7 +182,6 @@ basic_config(#{ replayq := ReplayQ, ssl := #{enable := EnableSsl} = Ssl}) -> #{ - conn_type => mqtt, replayq => ReplayQ, %% connection opts server => Server, diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl index c8b7ff77b..3de7feac4 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_mod.erl @@ -50,7 +50,7 @@ start(Config) -> Parent = self(), {Host, Port} = maps:get(server, Config), Mountpoint = maps:get(receive_mountpoint, Config, undefined), - Subscriptions = maps:get(subscriptions, Config), + Subscriptions = maps:get(subscriptions, Config, undefined), Vars = emqx_connector_mqtt_msg:make_pub_vars(Mountpoint, Subscriptions), Handlers = make_hdlr(Parent, Vars), Config1 = Config#{ diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl index 425fa06f1..7f8435fd1 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl @@ -19,7 +19,7 @@ -export([ to_binary/1 , from_binary/1 , make_pub_vars/2 - , to_remote_msg/3 + , to_remote_msg/2 , to_broker_msg/2 , estimate_size/1 ]). @@ -55,13 +55,13 @@ make_pub_vars(Mountpoint, #{payload := _, qos := _, retain := _, local_topic := %% Shame that we have to know the callback module here %% would be great if we can get rid of #mqtt_msg{} record %% and use #message{} in all places. --spec to_remote_msg(emqx_bridge_rpc | emqx_connector_mqtt_mod, msg(), variables()) +-spec to_remote_msg(msg(), variables()) -> exp_msg(). -to_remote_msg(emqx_connector_mqtt_mod, #message{flags = Flags0} = Msg, Vars) -> +to_remote_msg(#message{flags = Flags0} = Msg, Vars) -> Retain0 = maps:get(retain, Flags0, false), MapMsg = maps:put(retain, Retain0, emqx_message:to_map(Msg)), - to_remote_msg(emqx_connector_mqtt_mod, MapMsg, Vars); -to_remote_msg(emqx_connector_mqtt_mod, MapMsg, #{topic := TopicToken, payload := PayloadToken, + to_remote_msg(MapMsg, Vars); +to_remote_msg(MapMsg, #{topic := TopicToken, payload := PayloadToken, qos := QoSToken, retain := RetainToken, mountpoint := Mountpoint}) when is_map(MapMsg) -> Topic = replace_vars_in_str(TopicToken, MapMsg), Payload = replace_vars_in_str(PayloadToken, MapMsg), @@ -72,7 +72,7 @@ to_remote_msg(emqx_connector_mqtt_mod, MapMsg, #{topic := TopicToken, payload := topic = topic(Mountpoint, Topic), props = #{}, payload = Payload}; -to_remote_msg(_Module, #message{topic = Topic} = Msg, #{mountpoint := Mountpoint}) -> +to_remote_msg(#message{topic = Topic} = Msg, #{mountpoint := Mountpoint}) -> Msg#message{topic = topic(Mountpoint, Topic)}. %% published from remote node over a MQTT connection diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl index 83f7ce746..6ced719df 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_worker.erl @@ -185,12 +185,10 @@ callback_mode() -> [state_functions]. init(#{name := Name} = ConnectOpts) -> ?LOG(info, "starting bridge worker for ~p", [Name]), erlang:process_flag(trap_exit, true), - ConnectModule = conn_type(maps:get(conn_type, ConnectOpts)), Queue = open_replayq(Name, maps:get(replayq, ConnectOpts, #{})), State = init_state(ConnectOpts), self() ! idle, {ok, idle, State#{ - connect_module => ConnectModule, connect_opts => pre_process_opts(ConnectOpts), replayq => Queue }}. @@ -311,15 +309,13 @@ connected(Type, Content, State) -> %% Common handlers common(StateName, {call, From}, status, _State) -> {keep_state_and_data, [{reply, From, StateName}]}; -common(_StateName, {call, From}, ping, #{connection := Conn, - connect_module := ConnectModule} =_State) -> - Reply = ConnectModule:ping(Conn), +common(_StateName, {call, From}, ping, #{connection := Conn} =_State) -> + Reply = emqx_connector_mqtt_mod:ping(Conn), {keep_state_and_data, [{reply, From, Reply}]}; common(_StateName, {call, From}, ensure_stopped, #{connection := undefined} = _State) -> {keep_state_and_data, [{reply, From, ok}]}; -common(_StateName, {call, From}, ensure_stopped, #{connection := Conn, - connect_module := ConnectModule} = State) -> - Reply = ConnectModule:stop(Conn), +common(_StateName, {call, From}, ensure_stopped, #{connection := Conn} = State) -> + Reply = emqx_connector_mqtt_mod:stop(Conn), {next_state, idle, State#{connection => undefined}, [{reply, From, Reply}]}; common(_StateName, {call, From}, get_forwards, #{connect_opts := #{forwards := Forwards}}) -> {keep_state_and_data, [{reply, From, Forwards}]}; @@ -341,22 +337,21 @@ common(StateName, Type, Content, #{name := Name} = State) -> [Name, Type, StateName, Content]), {keep_state, State}. -do_connect(#{connect_module := ConnectModule, - connect_opts := ConnectOpts = #{forwards := Forwards}, +do_connect(#{connect_opts := ConnectOpts = #{forwards := Forwards}, inflight := Inflight, name := Name} = State) -> case Forwards of undefined -> ok; #{subscribe_local_topic := Topic} -> subscribe_local_topic(Topic, Name) end, - case ConnectModule:start(ConnectOpts) of + case emqx_connector_mqtt_mod:start(ConnectOpts) of {ok, Conn} -> ?tp(info, connected, #{name => Name, inflight => length(Inflight)}), {ok, State#{connection => Conn}}; {error, Reason} -> ConnectOpts1 = obfuscate(ConnectOpts), - ?LOG(error, "Failed to connect with module=~p\n" - "config=~p\nreason:~p", [ConnectModule, ConnectOpts1, Reason]), + ?LOG(error, "Failed to connect \n" + "config=~p\nreason:~p", [ConnectOpts1, Reason]), {error, Reason, State} end. @@ -385,16 +380,13 @@ pop_and_send(#{inflight := Inflight, max_inflight := Max} = State) -> pop_and_send_loop(State, 0) -> ?tp(debug, inflight_full, #{}), {ok, State}; -pop_and_send_loop(#{replayq := Q, connect_module := Module} = State, N) -> +pop_and_send_loop(#{replayq := Q} = State, N) -> case replayq:is_empty(Q) of true -> ?tp(debug, replayq_drained, #{}), {ok, State}; false -> - BatchSize = case Module of - emqx_bridge_rpc -> maps:get(batch_size, State); - _ -> 1 - end, + BatchSize = 1, Opts = #{count_limit => BatchSize, bytes_limit => 999999999}, {Q1, QAckRef, Batch} = replayq:pop(Q, Opts), case do_send(State#{replayq := Q1}, QAckRef, Batch) of @@ -407,7 +399,6 @@ pop_and_send_loop(#{replayq := Q, connect_module := Module} = State, N) -> do_send(#{connect_opts := #{forwards := undefined}}, _QAckRef, Batch) -> ?LOG(error, "cannot forward messages to remote broker as 'bridge.mqtt..in' not configured, msg: ~p", [Batch]); do_send(#{inflight := Inflight, - connect_module := Module, connection := Connection, mountpoint := Mountpoint, connect_opts := #{forwards := Forwards}, @@ -415,10 +406,10 @@ do_send(#{inflight := Inflight, Vars = emqx_connector_mqtt_msg:make_pub_vars(Mountpoint, Forwards), ExportMsg = fun(Message) -> bridges_metrics_inc(IfRecordMetrics, 'bridge.mqtt.message_sent'), - emqx_connector_mqtt_msg:to_remote_msg(Module, Message, Vars) + emqx_connector_mqtt_msg:to_remote_msg(Message, Vars) end, ?LOG(debug, "publish to remote broker, msg: ~p, vars: ~p", [Batch, Vars]), - case Module:send(Connection, [ExportMsg(M) || M <- Batch]) of + case emqx_connector_mqtt_mod:send(Connection, [ExportMsg(M) || M <- Batch]) of {ok, Refs} -> {ok, State#{inflight := Inflight ++ [#{q_ack_ref => QAckRef, send_ack_ref => map_set(Refs), @@ -492,10 +483,8 @@ do_subscribe(RawTopic, Name) -> {Topic, SubOpts} = emqx_topic:parse(TopicFilter, #{qos => ?QOS_2}), emqx_broker:subscribe(Topic, Name, SubOpts). -disconnect(#{connection := Conn, - connect_module := Module - } = State) when Conn =/= undefined -> - Module:stop(Conn), +disconnect(#{connection := Conn} = State) when Conn =/= undefined -> + emqx_connector_mqtt_mod:stop(Conn), State#{connection => undefined}; disconnect(State) -> State. @@ -538,13 +527,6 @@ obfuscate(Map) -> is_sensitive(password) -> true; is_sensitive(_) -> false. -conn_type(rpc) -> - emqx_bridge_rpc; -conn_type(mqtt) -> - emqx_connector_mqtt_mod; -conn_type(Mod) when is_atom(Mod) -> - Mod. - str(A) when is_atom(A) -> atom_to_list(A); str(B) when is_binary(B) -> diff --git a/apps/emqx_connector/test/emqx_connetor_mqtt_tests.erl b/apps/emqx_connector/test/emqx_connector_mqtt_tests.erl similarity index 93% rename from apps/emqx_connector/test/emqx_connetor_mqtt_tests.erl rename to apps/emqx_connector/test/emqx_connector_mqtt_tests.erl index 7943f5a77..0f4d651c9 100644 --- a/apps/emqx_connector/test/emqx_connetor_mqtt_tests.erl +++ b/apps/emqx_connector/test/emqx_connector_mqtt_tests.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_bridge_mqtt_tests). +-module(emqx_connector_mqtt_tests). -include_lib("eunit/include/eunit.hrl"). -include_lib("emqx/include/emqx_mqtt.hrl"). @@ -37,7 +37,7 @@ send_and_ack_test() -> try Max = 1, Batch = lists:seq(1, Max), - {ok, Conn} = emqx_connector_mqtt_mod:start(#{address => "127.0.0.1:1883"}), + {ok, Conn} = emqx_connector_mqtt_mod:start(#{server => {{127,0,0,1}, 1883}}), % %% return last packet id as batch reference {ok, _AckRef} = emqx_connector_mqtt_mod:send(Conn, Batch), diff --git a/apps/emqx_connector/test/emqx_connector_mqtt_worker_tests.erl b/apps/emqx_connector/test/emqx_connector_mqtt_worker_tests.erl new file mode 100644 index 000000000..090106cef --- /dev/null +++ b/apps/emqx_connector/test/emqx_connector_mqtt_worker_tests.erl @@ -0,0 +1,149 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_connector_mqtt_worker_tests). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("emqx/include/emqx.hrl"). +-include_lib("emqx/include/emqx_mqtt.hrl"). + +-define(BRIDGE_NAME, test). +-define(BRIDGE_REG_NAME, emqx_connector_mqtt_worker_test). +-define(WAIT(PATTERN, TIMEOUT), + receive + PATTERN -> + ok + after + TIMEOUT -> + error(timeout) + end). + +-export([start/1, send/2, stop/1]). + +start(#{connect_result := Result, test_pid := Pid, test_ref := Ref}) -> + case is_pid(Pid) of + true -> Pid ! {connection_start_attempt, Ref}; + false -> ok + end, + Result. + +send(SendFun, Batch) when is_function(SendFun, 2) -> + SendFun(Batch). + +stop(_Pid) -> ok. + +%% bridge worker should retry connecting remote node indefinitely +% reconnect_test() -> +% emqx_metrics:start_link(), +% emqx_connector_mqtt_worker:register_metrics(), +% Ref = make_ref(), +% Config = make_config(Ref, self(), {error, test}), +% {ok, Pid} = emqx_connector_mqtt_worker:start_link(?BRIDGE_NAME, Config), +% %% assert name registered +% ?assertEqual(Pid, whereis(?BRIDGE_REG_NAME)), +% ?WAIT({connection_start_attempt, Ref}, 1000), +% %% expect same message again +% ?WAIT({connection_start_attempt, Ref}, 1000), +% ok = emqx_connector_mqtt_worker:stop(?BRIDGE_REG_NAME), +% emqx_metrics:stop(), +% ok. + +%% connect first, disconnect, then connect again +disturbance_test() -> + meck:new(emqx_connector_mqtt_mod, [passthrough, no_history]), + meck:expect(emqx_connector_mqtt_mod, start, 1, fun(Conf) -> start(Conf) end), + meck:expect(emqx_connector_mqtt_mod, send, 2, fun(SendFun, Batch) -> send(SendFun, Batch) end), + meck:expect(emqx_connector_mqtt_mod, stop, 1, fun(Pid) -> stop(Pid) end), + try + emqx_metrics:start_link(), + emqx_connector_mqtt_worker:register_metrics(), + Ref = make_ref(), + TestPid = self(), + Config = make_config(Ref, TestPid, {ok, #{client_pid => TestPid}}), + {ok, Pid} = emqx_connector_mqtt_worker:start_link(Config#{name => bridge_disturbance}), + ?assertEqual(Pid, whereis(bridge_disturbance)), + ?WAIT({connection_start_attempt, Ref}, 1000), + Pid ! {disconnected, TestPid, test}, + ?WAIT({connection_start_attempt, Ref}, 1000), + emqx_metrics:stop(), + ok = emqx_connector_mqtt_worker:stop(Pid) + after + meck:unload(emqx_connector_mqtt_mod) + end. + +% % %% buffer should continue taking in messages when disconnected +% buffer_when_disconnected_test_() -> +% {timeout, 10000, fun test_buffer_when_disconnected/0}. + +% test_buffer_when_disconnected() -> +% Ref = make_ref(), +% Nums = lists:seq(1, 100), +% Sender = spawn_link(fun() -> receive {bridge, Pid} -> sender_loop(Pid, Nums, _Interval = 5) end end), +% SenderMref = monitor(process, Sender), +% Receiver = spawn_link(fun() -> receive {bridge, Pid} -> receiver_loop(Pid, Nums, _Interval = 1) end end), +% ReceiverMref = monitor(process, Receiver), +% SendFun = fun(Batch) -> +% BatchRef = make_ref(), +% Receiver ! {batch, BatchRef, Batch}, +% {ok, BatchRef} +% end, +% Config0 = make_config(Ref, false, {ok, #{client_pid => undefined}}), +% Config = Config0#{reconnect_delay_ms => 100}, +% emqx_metrics:start_link(), +% emqx_connector_mqtt_worker:register_metrics(), +% {ok, Pid} = emqx_connector_mqtt_worker:start_link(?BRIDGE_NAME, Config), +% Sender ! {bridge, Pid}, +% Receiver ! {bridge, Pid}, +% ?assertEqual(Pid, whereis(?BRIDGE_REG_NAME)), +% Pid ! {disconnected, Ref, test}, +% ?WAIT({'DOWN', SenderMref, process, Sender, normal}, 5000), +% ?WAIT({'DOWN', ReceiverMref, process, Receiver, normal}, 1000), +% ok = emqx_connector_mqtt_worker:stop(?BRIDGE_REG_NAME), +% emqx_metrics:stop(). + +manual_start_stop_test() -> + meck:new(emqx_connector_mqtt_mod, [passthrough, no_history]), + meck:expect(emqx_connector_mqtt_mod, start, 1, fun(Conf) -> start(Conf) end), + meck:expect(emqx_connector_mqtt_mod, send, 2, fun(SendFun, Batch) -> send(SendFun, Batch) end), + meck:expect(emqx_connector_mqtt_mod, stop, 1, fun(Pid) -> stop(Pid) end), + try + emqx_metrics:start_link(), + emqx_connector_mqtt_worker:register_metrics(), + Ref = make_ref(), + TestPid = self(), + BridgeName = manual_start_stop, + Config0 = make_config(Ref, TestPid, {ok, #{client_pid => TestPid}}), + Config = Config0#{start_type := manual}, + {ok, Pid} = emqx_connector_mqtt_worker:start_link(Config#{name => BridgeName}), + %% call ensure_started again should yeld the same result + ok = emqx_connector_mqtt_worker:ensure_started(BridgeName), + emqx_connector_mqtt_worker:ensure_stopped(BridgeName), + emqx_metrics:stop(), + ok = emqx_connector_mqtt_worker:stop(Pid) + after + meck:unload(emqx_connector_mqtt_mod) + end. + +make_config(Ref, TestPid, Result) -> + #{ + start_type => auto, + subscriptions => undefined, + forwards => undefined, + reconnect_interval => 50, + test_pid => TestPid, + test_ref => Ref, + connect_result => Result + }. diff --git a/apps/emqx_plugin_libs/test/emqx_rule_libs_rule_SUITE.erl b/apps/emqx_plugin_libs/test/emqx_plugin_libs_rule_SUITE.erl similarity index 99% rename from apps/emqx_plugin_libs/test/emqx_rule_libs_rule_SUITE.erl rename to apps/emqx_plugin_libs/test/emqx_plugin_libs_rule_SUITE.erl index e4c358695..56733147f 100644 --- a/apps/emqx_plugin_libs/test/emqx_rule_libs_rule_SUITE.erl +++ b/apps/emqx_plugin_libs/test/emqx_plugin_libs_rule_SUITE.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_rule_utils_SUITE). +-module(emqx_plugin_libs_rule_SUITE). -compile(export_all). -compile(nowarn_export_all). From 135c005467d7fca6675e316c7aa18394256ff067 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 10 Sep 2021 09:21:06 +0800 Subject: [PATCH 108/109] fix(bridges): do not start any bridge by default --- apps/emqx_bridge/etc/emqx_bridge.conf | 207 ++++-------------- .../src/mqtt/emqx_connector_mqtt_msg.erl | 2 +- .../src/emqx_rule_runtime.erl | 1 - 3 files changed, 44 insertions(+), 166 deletions(-) diff --git a/apps/emqx_bridge/etc/emqx_bridge.conf b/apps/emqx_bridge/etc/emqx_bridge.conf index 2844af3bf..08873228d 100644 --- a/apps/emqx_bridge/etc/emqx_bridge.conf +++ b/apps/emqx_bridge/etc/emqx_bridge.conf @@ -2,169 +2,48 @@ ## EMQ X Bridge ##-------------------------------------------------------------------- -bridges.mqtt.my_mqtt_bridge { - server = "127.0.0.1:1883" - proto_ver = "v4" - ## the clientid will be the concatenation of `clientid_prefix` and ids in `in` and `out`. - clientid_prefix = "bridge_client:" - username = "username1" - password = "" - clean_start = true - keepalive = 300 - retry_interval = "30s" - max_inflight = 32 - reconnect_interval = "30s" - bridge_mode = true - replayq { - dir = "{{ platform_data_dir }}/replayq/bridge_mqtt/" - seg_bytes = "100MB" - offload = false - max_total_bytes = "1GB" - } - ssl { - enable = false - keyfile = "{{ platform_etc_dir }}/certs/client-key.pem" - certfile = "{{ platform_etc_dir }}/certs/client-cert.pem" - cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" - } - ## we will create one MQTT connection for each element of the `in` - in: [{ - id = "pull_msgs_from_aws" - subscribe_remote_topic = "aws/#" - subscribe_qos = 1 - local_topic = "from_aws/${topic}" - payload = "${payload}" - qos = "${qos}" - retain = "${retain}" - }] - ## we will create one MQTT connection for each element of the `out` - out: [{ - id = "push_msgs_to_aws" - subscribe_local_topic = "emqx/#" - remote_topic = "from_emqx/${topic}" - payload = "${payload}" - qos = 1 - retain = false - }] -} - -# {name: "mysql_bridge_1" -# type: mysql -# config: { -# server: "192.168.0.172:3306" -# database: mqtt -# pool_size: 1 -# username: root -# password: public -# auto_reconnect: true -# ssl: false -# } +#bridges.mqtt.my_mqtt_bridge { +# server = "127.0.0.1:1883" +# proto_ver = "v4" +# ## the clientid will be the concatenation of `clientid_prefix` and ids in `in` and `out`. +# clientid_prefix = "bridge_client:" +# username = "username1" +# password = "" +# clean_start = true +# keepalive = 300 +# retry_interval = "30s" +# max_inflight = 32 +# reconnect_interval = "30s" +# bridge_mode = true +# replayq { +# dir = "{{ platform_data_dir }}/replayq/bridge_mqtt/" +# seg_bytes = "100MB" +# offload = false +# max_total_bytes = "1GB" # } -# , {name: "pgsql_bridge_1" -# type: pgsql -# config: { -# server: "192.168.0.172:5432" -# database: mqtt -# pool_size: 1 -# username: root -# password: public -# auto_reconnect: true -# ssl: false -# } +# ssl { +# enable = false +# keyfile = "{{ platform_etc_dir }}/certs/client-key.pem" +# certfile = "{{ platform_etc_dir }}/certs/client-cert.pem" +# cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" # } -# , {name: "mongodb_bridge_single" -# type: mongo -# config: { -# servers: "192.168.0.172:27017" -# mongo_type: single -# pool_size: 1 -# login: root -# password: public -# auth_source: mqtt -# database: mqtt -# ssl: false -# } -# } -# ,{name: "mongodb_bridge_rs" -# type: mongo -# config: { -# servers: "127.0.0.1:27017" -# mongo_type: rs -# rs_set_name: rs_name -# pool_size: 1 -# login: root -# password: public -# auth_source: mqtt -# database: mqtt -# ssl: false -# } -# } -# ,{name: "mongodb_bridge_shared" -# type: mongo -# config: { -# servers: "127.0.0.1:27017" -# mongo_type: shared -# pool_size: 1 -# login: root -# password: public -# auth_source: mqtt -# database: mqtt -# ssl: false -# max_overflow: 1 -# overflow_ttl: -# overflow_check_period: 10s -# local_threshold_ms: 10s -# connect_timeout_ms: 10s -# socket_timeout_ms: 10s -# server_selection_timeout_ms: 10s -# wait_queue_timeout_ms: 10s -# heartbeat_frequency_ms: 10s -# min_heartbeat_frequency_ms: 10s -# } -# } -# , {name: "redis_bridge_single" -# type: redis -# config: { -# servers: "192.168.0.172:6379" -# redis_type: single -# pool_size: 1 -# database: 0 -# password: public -# auto_reconnect: true -# ssl: false -# } -# } -# ,{name: "redis_bridge_sentinel" -# type: redis -# config: { -# servers: "127.0.0.1:6379, 127.0.0.2:6379, 127.0.0.3:6379" -# redis_type: sentinel -# sentinel_name: mymaster -# pool_size: 1 -# database: 0 -# ssl: false -# } -# } -# ,{name: "redis_bridge_cluster" -# type: redis -# config: { -# servers: "127.0.0.1:6379, 127.0.0.2:6379, 127.0.0.3:6379" -# redis_type: cluster -# pool_size: 1 -# database: 0 -# password: "public" -# ssl: false -# } -# } -# , {name: "ldap_bridge_1" -# type: ldap -# config: { -# servers: "192.168.0.172" -# port: 389 -# bind_dn: "cn=root,dc=emqx,dc=io" -# bind_password: "public" -# timeout: 30s -# pool_size: 1 -# ssl: false -# } -# } +# ## we will create one MQTT connection for each element of the `in` +# in: [{ +# id = "pull_msgs_from_aws" +# subscribe_remote_topic = "aws/#" +# subscribe_qos = 1 +# local_topic = "from_aws/${topic}" +# payload = "${payload}" +# qos = "${qos}" +# retain = "${retain}" +# }] +# ## we will create one MQTT connection for each element of the `out` +# out: [{ +# id = "push_msgs_to_aws" +# subscribe_local_topic = "emqx/#" +# remote_topic = "from_emqx/${topic}" +# payload = "${payload}" +# qos = 1 +# retain = false +# }] +#} diff --git a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl index 7f8435fd1..5f076ed9e 100644 --- a/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl +++ b/apps/emqx_connector/src/mqtt/emqx_connector_mqtt_msg.erl @@ -55,7 +55,7 @@ make_pub_vars(Mountpoint, #{payload := _, qos := _, retain := _, local_topic := %% Shame that we have to know the callback module here %% would be great if we can get rid of #mqtt_msg{} record %% and use #message{} in all places. --spec to_remote_msg(msg(), variables()) +-spec to_remote_msg(msg() | map(), variables()) -> exp_msg(). to_remote_msg(#message{flags = Flags0} = Msg, Vars) -> Retain0 = maps:get(retain, Flags0, false), diff --git a/apps/emqx_rule_engine/src/emqx_rule_runtime.erl b/apps/emqx_rule_engine/src/emqx_rule_runtime.erl index e8dcc8a58..f9e210ab3 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_runtime.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_runtime.erl @@ -222,7 +222,6 @@ do_compare('<=', L, R) -> L =< R; do_compare('>=', L, R) -> L >= R; do_compare('<>', L, R) -> L /= R; do_compare('!=', L, R) -> L /= R; -do_compare('~=', T, F) -> emqx_topic:match(T, F); do_compare('=~', T, F) -> emqx_topic:match(T, F). number(Bin) -> From 07069898b12a0364228c4b77bf1e08b94232680e Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 10 Sep 2021 09:22:00 +0800 Subject: [PATCH 109/109] fix(README): update docs for running ct on single app --- README-CN.md | 2 +- README-JP.md | 2 +- README-RU.md | 2 +- README.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README-CN.md b/README-CN.md index fa068fc29..80e926199 100644 --- a/README-CN.md +++ b/README-CN.md @@ -90,7 +90,7 @@ make eunit ct ### 执行部分应用的 common tests ```bash -make apps/emqx_bridge-ct +make apps/emqx_retainer-ct ``` ### 静态分析(Dialyzer) diff --git a/README-JP.md b/README-JP.md index 7e276e9eb..57c9a1809 100644 --- a/README-JP.md +++ b/README-JP.md @@ -84,7 +84,7 @@ make eunit ct ### common test の一部を実行する ```bash -make apps/emqx_bridge-ct +make apps/emqx_retainer-ct ``` ### Dialyzer diff --git a/README-RU.md b/README-RU.md index 093b49782..e02f47aa4 100644 --- a/README-RU.md +++ b/README-RU.md @@ -93,7 +93,7 @@ make eunit ct Пример: ```bash -make apps/emqx_bridge-ct +make apps/emqx_retainer-ct ``` ### Dialyzer diff --git a/README.md b/README.md index 7f445b226..f60ed3cd9 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ make eunit ct Examples ```bash -make apps/emqx_bridge-ct +make apps/emqx_retainer-ct ``` ### Dialyzer

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 diff --git a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl index 510432441..24f06549b 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl @@ -22,17 +22,10 @@ -include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). %% API --export([]). - -export([ info/1 , info/2 , stats/1 - , validator/3 - , get_clientinfo/1 - , get_config/2 - , get_config/3 - , result_keys/0 - , transfer_result/3]). + , validator/4]). -export([ init/2 , handle_in/2 @@ -61,20 +54,17 @@ keepalive :: emqx_keepalive:keepalive() | undefined, %% Timer timers :: #{atom() => disable | undefined | reference()}, - token :: binary() | undefined, - config :: hocon:config() + + conn_state :: idle | connected, + + token :: binary() | undefined }). -%% the execuate context for session call --record(exec_ctx, { config :: hocon:config(), - ctx :: emqx_gateway_ctx:context(), - clientinfo :: emqx_types:clientinfo() - }). - -type channel() :: #channel{}. --define(DISCONNECT_WAIT_TIME, timer:seconds(10)). +-define(TOKEN_MAXIMUM, 4294967295). -define(INFO_KEYS, [conninfo, conn_state, clientinfo, session]). +-import(emqx_coap_medium, [reply/2, reply/3, reply/4, iter/3, iter/4]). %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- @@ -87,8 +77,8 @@ info(Keys, Channel) when is_list(Keys) -> info(conninfo, #channel{conninfo = ConnInfo}) -> ConnInfo; -info(conn_state, _) -> - connected; +info(conn_state, #channel{conn_state = CState}) -> + CState; info(clientinfo, #channel{clientinfo = ClientInfo}) -> ClientInfo; info(session, #channel{session = Session}) -> @@ -106,18 +96,13 @@ init(ConnInfo = #{peername := {PeerHost, _}, #{ctx := Ctx} = Config) -> Peercert = maps:get(peercert, ConnInfo, undefined), Mountpoint = maps:get(mountpoint, Config, undefined), - EnableAuth = is_authentication_enabled(Config), ClientInfo = set_peercert_infos( Peercert, #{ zone => default , protocol => 'coap' , peerhost => PeerHost , sockport => SockPort - , clientid => if EnableAuth -> - undefined; - true -> - emqx_guid:to_base62(emqx_guid:gen()) - end + , clientid => emqx_guid:to_base62(emqx_guid:gen()) , username => undefined , is_bridge => false , is_superuser => false @@ -125,56 +110,29 @@ init(ConnInfo = #{peername := {PeerHost, _}, } ), + Heartbeat = emqx:get_config([gateway, coap, idle_timeout]), #channel{ ctx = Ctx , conninfo = ConnInfo , clientinfo = ClientInfo , timers = #{} - , config = Config , session = emqx_coap_session:new() - , keepalive = emqx_keepalive:init(maps:get(heartbeat, Config)) + , keepalive = emqx_keepalive:init(Heartbeat) + , conn_state = idle }. -is_authentication_enabled(Cfg) -> - case maps:get(authentication, Cfg, #{enable => false}) of - AuthCfg when is_map(AuthCfg) -> - maps:get(enable, AuthCfg, true); - _ -> false - end. - -validator(Type, Topic, #exec_ctx{ctx = Ctx, - clientinfo = ClientInfo}) -> +validator(Type, Topic, Ctx, ClientInfo) -> emqx_gateway_ctx:authorize(Ctx, ClientInfo, Type, Topic). -get_clientinfo(#exec_ctx{clientinfo = ClientInfo}) -> - ClientInfo. - -get_config(Key, Ctx) -> - get_config(Key, Ctx, undefined). - -get_config(Key, #exec_ctx{config = Cfg}, Def) -> - maps:get(Key, Cfg, Def). - -result_keys() -> - [out, connection]. - -transfer_result(From, Value, Result) -> - ?TRANSFER_RESULT(From, Value, Result). - %%-------------------------------------------------------------------- %% Handle incoming packet %%-------------------------------------------------------------------- handle_in(Msg, ChannleT) -> Channel = ensure_keepalive_timer(ChannleT), - case convert_queries(Msg) of - {ok, Msg2} -> - case emqx_coap_message:is_request(Msg2) of - true -> - check_auth_state(Msg2, Channel); - _ -> - call_session(handle_response, Msg2, Channel) - end; + case emqx_coap_message:is_request(Msg) of + true -> + check_auth_state(Msg, Channel); _ -> - response({error, bad_request}, <<"bad uri_query">>, Msg, Channel) + call_session(handle_response, Msg, Channel) end. %%-------------------------------------------------------------------- @@ -258,94 +216,57 @@ make_timer(Name, Time, Msg, Channel = #channel{timers = Timers}) -> ensure_keepalive_timer(Channel) -> ensure_keepalive_timer(fun ensure_timer/4, Channel). -ensure_keepalive_timer(Fun, #channel{config = Cfg} = Channel) -> - Interval = maps:get(heartbeat, Cfg), - Fun(keepalive, Interval, keepalive, Channel). +ensure_keepalive_timer(Fun, Channel) -> + Heartbeat = emqx:get_config([gateway, coap, idle_timeout]), + Fun(keepalive, Heartbeat, keepalive, Channel). -call_session(Fun, - Msg, - #channel{session = Session} = Channel) -> - Ctx = new_exec_ctx(Channel), - Result = erlang:apply(emqx_coap_session, Fun, [Msg, Ctx, Session]), - process_result([session, connection, out], Result, Msg, Channel). - -process_result([Key | T], Result, Msg, Channel) -> - case handle_result(Key, Result, Msg, Channel) of - {ok, Channel2} -> - process_result(T, Result, Msg, Channel2); - Other -> - Other - end; - -process_result(_, _, _, Channel) -> - {ok, Channel}. - -handle_result(session, #{session := Session}, _, Channel) -> - {ok, Channel#channel{session = Session}}; - -handle_result(connection, #{connection := open}, Msg, Channel) -> - do_connect(Msg, Channel); - -handle_result(connection, #{connection := close}, Msg, Channel) -> - Reply = emqx_coap_message:piggyback({ok, deleted}, Msg), - {shutdown, close, {outgoing, Reply}, Channel}; - -handle_result(out, #{out := Out}, _, Channel) -> - {ok, {outgoing, Out}, Channel}; - -handle_result(_, _, _, Channel) -> - {ok, Channel}. - -check_auth_state(Msg, #channel{config = Cfg} = Channel) -> - Enable = is_authentication_enabled(Cfg), +check_auth_state(Msg, Channel) -> + Enable = emqx:get_config([gateway, coap, enable_stats]), check_token(Enable, Msg, Channel). check_token(true, - #coap_message{options = Options} = Msg, + Msg, #channel{token = Token, - clientinfo = ClientInfo} = Channel) -> + clientinfo = ClientInfo, + conn_state = CState} = Channel) -> #{clientid := ClientId} = ClientInfo, - case maps:get(uri_query, Options, undefined) of + case emqx_coap_message:get_option(uri_query, Msg) of #{<<"clientid">> := ClientId, <<"token">> := Token} -> call_session(handle_request, Msg, Channel); #{<<"clientid">> := DesireId} -> - try_takeover(ClientId, DesireId, Msg, Channel); + try_takeover(CState, DesireId, Msg, Channel); _ -> - response({error, unauthorized}, Msg, Channel) + Reply = emqx_coap_message:piggyback({error, unauthorized}, Msg), + {ok, {outgoing, Reply}, Msg} end; -check_token(false, - #coap_message{options = Options} = Msg, - Channel) -> - case maps:get(uri_query, Options, undefined) of +check_token(false, Msg, Channel) -> + case emqx_coap_message:get_option(uri_query, Msg) of #{<<"clientid">> := _} -> - response({error, unauthorized}, Msg, Channel); + Reply = emqx_coap_message:piggyback({error, unauthorized}, Msg), + {ok, {outgoing, Reply}, Msg}; #{<<"token">> := _} -> - response({error, unauthorized}, Msg, Channel); + Reply = emqx_coap_message:piggyback({error, unauthorized}, Msg), + {ok, {outgoing, Reply}, Msg}; _ -> call_session(handle_request, Msg, Channel) end. -response(Method, Req, Channel) -> - response(Method, <<>>, Req, Channel). - -response(Method, Payload, Req, Channel) -> - Reply = emqx_coap_message:piggyback(Method, Payload, Req), - call_session(handle_out, Reply, Channel). - -try_takeover(undefined, - DesireId, - #coap_message{options = Opts} = Msg, - Channel) -> - case maps:get(uri_path, Opts, []) of - [<<"mqtt">>, <<"connection">> | _] -> +try_takeover(idle, DesireId, Msg, Channel) -> + case emqx_coap_message:get_option(uri_path, Msg, []) of + [<<"mqtt">>, <<"connection">> | _] -> %% may be is a connect request %% TODO need check repeat connect, unless we implement the %% udp connection baseon the clientid call_session(handle_request, Msg, Channel); _ -> - do_takeover(DesireId, Msg, Channel) + case emqx:get_config([gateway, coap, authentication], undefined) of + undefined -> + call_session(handle_request, Msg, Channel); + _ -> + do_takeover(DesireId, Msg, Channel) + end end; try_takeover(_, DesireId, Msg, Channel) -> @@ -354,31 +275,7 @@ try_takeover(_, DesireId, Msg, Channel) -> do_takeover(_DesireId, Msg, Channel) -> %% TODO completed the takeover, now only reset the message Reset = emqx_coap_message:reset(Msg), - call_session(handle_out, Reset, Channel). - -new_exec_ctx(#channel{config = Cfg, - ctx = Ctx, - clientinfo = ClientInfo}) -> - #exec_ctx{config = Cfg, - ctx = Ctx, - clientinfo = ClientInfo}. - -do_connect(#coap_message{options = Opts} = Req, Channel) -> - Queries = maps:get(uri_query, Opts), - case emqx_misc:pipeline( - [ fun run_conn_hooks/2 - , fun enrich_clientinfo/2 - , fun set_log_meta/2 - , fun auth_connect/2 - ], - {Queries, Req}, - Channel) of - {ok, _Input, NChannel} -> - process_connect(ensure_connected(NChannel), Req); - {error, ReasonCode, NChannel} -> - ErrMsg = io_lib:format("Login Failed: ~s", [ReasonCode]), - response({error, bad_request}, ErrMsg, Req, NChannel) - end. + {ok, {outgoing, Reset}, Channel}. run_conn_hooks(Input, Channel = #channel{ctx = Ctx, conninfo = ConnInfo}) -> @@ -439,11 +336,11 @@ ensure_connected(Channel = #channel{ctx = Ctx, ok = run_hooks(Ctx, 'client.connected', [ClientInfo, NConnInfo]), Channel#channel{conninfo = NConnInfo}. -process_connect(Channel = #channel{ctx = Ctx, - session = Session, - conninfo = ConnInfo, - clientinfo = ClientInfo}, - Msg) -> +process_connect(#channel{ctx = Ctx, + session = Session, + conninfo = ConnInfo, + clientinfo = ClientInfo} = Channel, + Msg, Result, Iter) -> %% inherit the old session SessFun = fun(_,_) -> Session end, case emqx_gateway_ctx:open_session( @@ -455,10 +352,14 @@ process_connect(Channel = #channel{ctx = Ctx, emqx_coap_session ) of {ok, _Sess} -> - response({ok, created}, <<"connected">>, Msg, Channel); + RandVal = rand:uniform(?TOKEN_MAXIMUM), + Token = erlang:list_to_binary(erlang:integer_to_list(RandVal)), + iter(Iter, + reply({ok, created}, Token, Msg, Result), + Channel#channel{token = Token}); {error, Reason} -> ?LOG(error, "Failed to open session du to ~p", [Reason]), - response({error, bad_request}, Msg, Channel) + iter(Iter, reply({error, bad_request}, Msg, Result), Channel) end. run_hooks(Ctx, Name, Args) -> @@ -469,20 +370,93 @@ run_hooks(Ctx, Name, Args, Acc) -> emqx_gateway_ctx:metrics_inc(Ctx, Name), emqx_hooks:run_fold(Name, Args, Acc). -convert_queries(#coap_message{options = Opts} = Msg) -> - case maps:get(uri_query, Opts, undefined) of - undefined -> - {ok, Msg#coap_message{options = Opts#{uri_query => #{}}}}; - Queries -> - convert_queries(Queries, #{}, Msg) - end. +%%-------------------------------------------------------------------- +%% Call Chain +%%-------------------------------------------------------------------- +call_session(Fun, + Msg, + #channel{session = Session} = Channel) -> + iter([ session, fun process_session/4 + , proto, fun process_protocol/4 + , reply, fun process_reply/4 + , out, fun process_out/4 + , fun process_nothing/3 + ], + emqx_coap_session:Fun(Msg, Session), + Channel). -convert_queries([H | T], Queries, Msg) -> - case re:split(H, "=") of - [Key, Val] -> - convert_queries(T, Queries#{Key => Val}, Msg); - _ -> - error +call_handler(request, Msg, Result, + #channel{ctx = Ctx, + clientinfo = ClientInfo} = Channel, Iter) -> + HandlerResult = + case emqx_coap_message:get_option(uri_path, Msg) of + [<<"ps">> | RestPath] -> + emqx_coap_pubsub_handler:handle_request(RestPath, Msg, Ctx, ClientInfo); + [<<"mqtt">> | RestPath] -> + emqx_coap_mqtt_handler:handle_request(RestPath, Msg, Ctx, ClientInfo); + _ -> + reply({error, bad_request}, Msg) + end, + iter([ connection, fun process_connection/4 + , subscribe, fun process_subscribe/4 | Iter], + maps:merge(Result, HandlerResult), + Channel); + +call_handler(_, _, Result, Channel, Iter) -> + iter(Iter, Result, Channel). + +process_session(Session, Result, Channel, Iter) -> + iter(Iter, Result, Channel#channel{session = Session}). + +process_protocol({Type, Msg}, Result, Channel, Iter) -> + call_handler(Type, Msg, Result, Channel, Iter). + +%% leaf node +process_out(Outs, Result, Channel, _) -> + Outs2 = lists:reverse(Outs), + Outs3 = case maps:get(reply, Result, undefined) of + undefined -> + Outs2; + Reply -> + [Reply | Outs2] + end, + {ok, {outgoing, Outs3}, Channel}. + +%% leaf node +process_nothing(_, _, Channel) -> + {ok, Channel}. + +process_connection({open, Req}, Result, Channel, Iter) -> + Queries = emqx_coap_message:get_option(uri_query, Req), + case emqx_misc:pipeline( + [ fun run_conn_hooks/2 + , fun enrich_clientinfo/2 + , fun set_log_meta/2 + , fun auth_connect/2 + ], + {Queries, Req}, + Channel) of + {ok, _Input, NChannel} -> + process_connect(ensure_connected(NChannel), Req, Result, Iter); + {error, ReasonCode, NChannel} -> + ErrMsg = io_lib:format("Login Failed: ~s", [ReasonCode]), + Payload = erlang:list_to_binary(lists:flatten(ErrMsg)), + iter(Iter, + reply({error, bad_request}, Payload, Req, Result), + NChannel) end; -convert_queries([], Queries, #coap_message{options = Opts} = Msg) -> - {ok, Msg#coap_message{options = Opts#{uri_query => Queries}}}. + +process_connection({close, Msg}, _, Channel, _) -> + Reply = emqx_coap_message:piggyback({ok, deleted}, Msg), + {shutdown, close, Reply, Channel}. + +process_subscribe({Sub, Msg}, Result, #channel{session = Session} = Channel, Iter) -> + Result2 = emqx_coap_session:process_subscribe(Sub, Msg, Result, Session), + iter([session, fun process_session/4 | Iter], Result2, Channel). + +%% leaf node +process_reply(Reply, Result, #channel{session = Session} = Channel, _) -> + Session2 = emqx_coap_session:set_reply(Reply, Session), + Outs = maps:get(out, Result, []), + Outs2 = lists:reverse(Outs), + {ok, {outgoing, [Reply | Outs2]}, Channel#channel{session = Session2}}. diff --git a/apps/emqx_gateway/src/coap/emqx_coap_frame.erl b/apps/emqx_gateway/src/coap/emqx_coap_frame.erl index c1bc08928..4d12997a7 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_frame.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_frame.erl @@ -103,11 +103,7 @@ flatten_options([{OptId, OptVal} | T], Acc) -> false -> [encode_option(OptId, OptVal) | Acc]; _ -> - lists:foldl(fun(undefined, InnerAcc) -> - InnerAcc; - (E, InnerAcc) -> - [encode_option(OptId, E) | InnerAcc] - end, Acc, OptVal) + try_encode_repeatable(OptId, OptVal) ++ Acc end); flatten_options([], Acc) -> @@ -141,6 +137,19 @@ encode_option_list([], _LastNum, Acc, <<>>) -> encode_option_list([], _, Acc, Payload) -> <>. +try_encode_repeatable(uri_query, Val) when is_map(Val) -> + maps:fold(fun(K, V, Acc) -> + [encode_option(uri_query, <>) | Acc] + end, + [], Val); + +try_encode_repeatable(K, Val) -> + lists:foldr(fun(undefined, Acc) -> + Acc; + (E, Acc) -> + [encode_option(K, E) | Acc] + end, [], Val). + %% RFC 7252 encode_option(if_match, OptVal) -> {?OPTION_IF_MATCH, OptVal}; encode_option(uri_host, OptVal) -> {?OPTION_URI_HOST, OptVal}; @@ -188,6 +197,8 @@ content_format_to_code(<<"application/octet-stream">>) -> 42; content_format_to_code(<<"application/exi">>) -> 47; content_format_to_code(<<"application/json">>) -> 50; content_format_to_code(<<"application/cbor">>) -> 60; +content_format_to_code(<<"application/vnd.oma.lwm2m+tlv">>) -> 11542; +content_format_to_code(<<"application/vnd.oma.lwm2m+json">>) -> 11543; content_format_to_code(_) -> 42. %% use octet-stream as default method_to_class_code(get) -> {0, 01}; @@ -235,12 +246,7 @@ parse(< {Options, Payload} = decode_option_list(Tail), Options2 = maps:fold(fun(K, V, Acc) -> - case is_repeatable_option(K) of - true -> - Acc#{K => lists:reverse(V)}; - _ -> - Acc#{K => V} - end + Acc#{K => get_option_val(K, V)} end, #{}, Options), @@ -255,6 +261,24 @@ parse(<>, ParseState}. +get_option_val(uri_query, V) -> + KVList = lists:foldl(fun(E, Acc) -> + [Key, Val] = re:split(E, "="), + [{Key, Val} | Acc] + + end, + [], + V), + maps:from_list(KVList); + +get_option_val(K, V) -> + case is_repeatable_option(K) of + true -> + lists:reverse(V); + _ -> + V + end. + -spec decode_type(X) -> message_type() when X :: 0 .. 3. decode_type(0) -> con; @@ -359,6 +383,8 @@ content_code_to_format(42) -> <<"application/octet-stream">>; content_code_to_format(47) -> <<"application/exi">>; content_code_to_format(50) -> <<"application/json">>; content_code_to_format(60) -> <<"application/cbor">>; +content_code_to_format(11542) -> <<"application/vnd.oma.lwm2m+tlv">>; +content_code_to_format(11543) -> <<"application/vnd.oma.lwm2m+json">>; content_code_to_format(_) -> <<"application/octet-stream">>. %% use octet as default %% RFC 7252 diff --git a/apps/emqx_gateway/src/coap/emqx_coap_medium.erl b/apps/emqx_gateway/src/coap/emqx_coap_medium.erl new file mode 100644 index 000000000..ae5763179 --- /dev/null +++ b/apps/emqx_gateway/src/coap/emqx_coap_medium.erl @@ -0,0 +1,107 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +%% Simplified semi-automatic CPS mode tree for coap +%% The tree must have a terminal leaf node, and it's return is the result of the entire tree. +%% This module currently only supports simple linear operation + +-module(emqx_coap_medium). + +-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). + +%% API +-export([ empty/0, reset/1, reset/2 + , out/1, out/2, proto_out/1 + , proto_out/2, iter/3, iter/4 + , reply/2, reply/3, reply/4]). + +%%-type result() :: map() | empty. +-define(DEFINE_DEF(Name), Name(Msg) -> Name(Msg, #{})). +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- +empty() -> #{}. + +?DEFINE_DEF(reset). + +reset(Msg, Result) -> + out(emqx_coap_message:reset(Msg), Result). + +out(Msg) -> + #{out => [Msg]}. + +out(Msg, #{out := Outs} = Result) -> + Result#{out := [Msg | Outs]}; + +out(Msg, Result) -> + Result#{out => [Msg]}. + +?DEFINE_DEF(proto_out). + +proto_out(Proto, Resut) -> + Resut#{proto => Proto}. + +reply(Method, Req) when not is_record(Method, coap_message) -> + reply(Method, <<>>, Req); + +reply(Reply, Result) -> + Result#{reply => Reply}. + +reply(Method, Req, Result) when is_record(Req, coap_message) -> + reply(Method, <<>>, Req, Result); + +reply(Method, Payload, Req) -> + reply(Method, Payload, Req, #{}). + +reply(Method, Payload, Req, Result) -> + Result#{reply => emqx_coap_message:piggyback(Method, Payload, Req)}. + +%% run a tree +iter([Key, Fun | T], Input, State) -> + case maps:get(Key, Input, undefined) of + undefined -> + iter(T, Input, State); + Val -> + Fun(Val, maps:remove(Key, Input), State, T) + %% reserved + %% if is_function(Fun) -> + %% Fun(Val, maps:remove(Key, Input), State, T); + %% true -> + %% %% switch to sub branch + %% [FunH | FunT] = Fun, + %% FunH(Val, maps:remove(Key, Input), State, FunT) + %% end + end; + +%% terminal node +iter([Fun], Input, State) -> + Fun(undefined, Input, State). + +%% run a tree with argument +iter([Key, Fun | T], Input, Arg, State) -> + case maps:get(Key, Input, undefined) of + undefined -> + iter(T, Input, Arg, State); + Val -> + Fun(Val, maps:remove(Key, Input), Arg, State, T) + end; + +iter([Fun], Input, Arg, State) -> + Fun(undefined, Input, Arg, State). + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- diff --git a/apps/emqx_gateway/src/coap/emqx_coap_message.erl b/apps/emqx_gateway/src/coap/emqx_coap_message.erl index 2e9fb144e..3851b3428 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_message.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_message.erl @@ -31,7 +31,8 @@ -export([is_request/1]). --export([set/3, set_payload/2, get_content/1, set_content/2, set_content/3, get_option/2]). +-export([ set/3, set_payload/2, get_option/2 + , get_option/3, set_payload_block/3, set_payload_block/4]). -include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). @@ -42,11 +43,10 @@ request(Type, Method, Payload) -> request(Type, Method, Payload, []). request(Type, Method, Payload, Options) when is_binary(Payload) -> - #coap_message{type = Type, method = Method, payload = Payload, options = Options}; - -request(Type, Method, Content=#coap_content{}, Options) -> - set_content(Content, - #coap_message{type = Type, method = Method, options = Options}). + #coap_message{type = Type, + method = Method, + payload = Payload, + options = to_options(Options)}. ack(#coap_message{id = Id}) -> #coap_message{type = ack, id = Id}. @@ -55,20 +55,20 @@ reset(#coap_message{id = Id}) -> #coap_message{type = reset, id = Id}. %% just make a response -response(#coap_message{type = Type, - id = Id, - token = Token}) -> - #coap_message{type = Type, - id = Id, - token = Token}. +response(Request) -> + response(undefined, Request). response(Method, Request) -> - set_method(Method, response(Request)). + response(Method, <<>>, Request). -response(Method, Payload, Request) -> - set_method(Method, - set_payload(Payload, - response(Request))). +response(Method, Payload, #coap_message{type = Type, + id = Id, + token = Token}) -> + #coap_message{type = Type, + id = Id, + token = Token, + method = Method, + payload = Payload}. %% make a response which maybe is a piggyback ack piggyback(Method, Request) -> @@ -90,14 +90,11 @@ set(max_age, ?DEFAULT_MAX_AGE, Msg) -> Msg; set(Option, Value, Msg = #coap_message{options = Options}) -> Msg#coap_message{options = Options#{Option => Value}}. -get_option(Option, #coap_message{options = Options}) -> - maps:get(Option, Options, undefined). +get_option(Option, Msg) -> + get_option(Option, Msg, undefined). -set_method(Method, Msg) -> - Msg#coap_message{method = Method}. - -set_payload(Payload = #coap_content{}, Msg) -> - set_content(Payload, undefined, Msg); +get_option(Option, #coap_message{options = Options}, Def) -> + maps:get(Option, Options, Def). set_payload(Payload, Msg) when is_binary(Payload) -> Msg#coap_message{payload = Payload}; @@ -105,49 +102,6 @@ set_payload(Payload, Msg) when is_binary(Payload) -> set_payload(Payload, Msg) when is_list(Payload) -> Msg#coap_message{payload = list_to_binary(Payload)}. -get_content(#coap_message{options = Options, payload = Payload}) -> - #coap_content{etag = maps:get(etag, Options, undefined), - max_age = maps:get(max_age, Options, ?DEFAULT_MAX_AGE), - format = maps:get(content_format, Options, undefined), - location_path = maps:get(location_path, Options, []), - payload = Payload}. - -set_content(Content, Msg) -> - set_content(Content, undefined, Msg). - -%% segmentation not requested and not required -set_content(#coap_content{etag = ETag, - max_age = MaxAge, - format = Format, - location_path = LocPath, - payload = Payload}, - undefined, - Msg) - when byte_size(Payload) =< ?MAX_BLOCK_SIZE -> - #coap_message{options = Options} = Msg2 = set_payload(Payload, Msg), - Options2 = Options#{etag => [ETag], - max_age => MaxAge, - content_format => Format, - location_path => LocPath}, - Msg2#coap_message{options = Options2}; - -%% segmentation not requested, but required (late negotiation) -set_content(Content, undefined, Msg) -> - set_content(Content, {0, true, ?MAX_BLOCK_SIZE}, Msg); - -%% segmentation requested (early negotiation) -set_content(#coap_content{etag = ETag, - max_age = MaxAge, - format = Format, - payload = Payload}, - Block, - Msg) -> - #coap_message{options = Options} = Msg2 = set_payload_block(Payload, Block, Msg), - Options2 = Options#{etag => [ETag], - max => MaxAge, - content_format => Format}, - Msg2#coap_message{options = Options2}. - set_payload_block(Content, Block, Msg = #coap_message{method = Method}) when is_atom(Method) -> set_payload_block(Content, block1, Block, Msg); @@ -172,3 +126,8 @@ is_request(#coap_message{method = Method}) when is_atom(Method) -> is_request(_) -> false. + +to_options(Opts) when is_map(Opts) -> + Opts; +to_options(Opts) -> + maps:from_list(Opts). diff --git a/apps/emqx_gateway/src/coap/emqx_coap_resource.erl b/apps/emqx_gateway/src/coap/emqx_coap_resource.erl deleted file mode 100644 index 93fe82aba..000000000 --- a/apps/emqx_gateway/src/coap/emqx_coap_resource.erl +++ /dev/null @@ -1,37 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2017-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_coap_resource). - --include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). - --type context() :: any(). --type topic() :: binary(). --type token() :: token(). - --type register() :: {topic(), token()} - | topic() - | undefined. - --type result() :: emqx_coap_message() - | {has_sub, emqx_coap_message(), register()}. - --callback init(hocon:confg()) -> context(). --callback stop(context()) -> ok. --callback get(emqx_coap_message(), hocon:config()) -> result(). --callback put(emqx_coap_message(), hocon:config()) -> result(). --callback post(emqx_coap_message(), hocon:config()) -> result(). --callback delete(emqx_coap_message(), hocon:config()) -> result(). diff --git a/apps/emqx_gateway/src/coap/emqx_coap_session.erl b/apps/emqx_gateway/src/coap/emqx_coap_session.erl index 50e91797b..b7e6c53f4 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_session.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_session.erl @@ -21,24 +21,25 @@ -include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). %% API --export([new/0, transfer_result/3]). +-export([ new/0 + , process_subscribe/4]). -export([ info/1 , info/2 , stats/1 ]). --export([ handle_request/3 - , handle_response/3 - , handle_out/3 - , deliver/3 - , timeout/3]). +-export([ handle_request/2 + , handle_response/2 + , handle_out/2 + , set_reply/2 + , deliver/2 + , timeout/2]). -export_type([session/0]). -record(session, { transport_manager :: emqx_coap_tm:manager() , observe_manager :: emqx_coap_observe_res:manager() - , next_msg_id :: coap_message_id() , created_at :: pos_integer() }). @@ -64,6 +65,8 @@ awaiting_rel_max ]). +-import(emqx_coap_medium, [iter/3]). + %%%------------------------------------------------------------------- %%% API %%%------------------------------------------------------------------- @@ -72,7 +75,6 @@ new() -> _ = emqx_misc:rand_seed(), #session{ transport_manager = emqx_coap_tm:new() , observe_manager = emqx_coap_observe_res:new_manager() - , next_msg_id = rand:uniform(?MAX_MESSAGE_ID) , created_at = erlang:system_time(millisecond)}. %%-------------------------------------------------------------------- @@ -110,8 +112,8 @@ info(mqueue_max, _) -> 0; info(mqueue_dropped, _) -> 0; -info(next_pkt_id, #session{next_msg_id = PacketId}) -> - PacketId; +info(next_pkt_id, _) -> + 0; info(awaiting_rel, _) -> #{}; info(awaiting_rel_cnt, _) -> @@ -130,105 +132,87 @@ stats(Session) -> info(?STATS_KEYS, Session). %%%------------------------------------------------------------------- %%% Process Message %%%------------------------------------------------------------------- -handle_request(Msg, Ctx, Session) -> +handle_request(Msg, Session) -> call_transport_manager(?FUNCTION_NAME, Msg, - Ctx, - [fun process_tm/3, fun process_subscribe/3], Session). -handle_response(Msg, Ctx, Session) -> - call_transport_manager(?FUNCTION_NAME, Msg, Ctx, [fun process_tm/3], Session). +handle_response(Msg, Session) -> + call_transport_manager(?FUNCTION_NAME, Msg, Session). -handle_out(Msg, Ctx, Session) -> - call_transport_manager(?FUNCTION_NAME, Msg, Ctx, [fun process_tm/3], Session). +handle_out(Msg, Session) -> + call_transport_manager(?FUNCTION_NAME, Msg, Session). -deliver(Delivers, Ctx, Session) -> - Fun = fun({_, Topic, Message}, - #{out := OutAcc, - session := #session{observe_manager = OM, - next_msg_id = MsgId, - transport_manager = TM} = SAcc} = Acc) -> - case emqx_coap_observe_res:res_changed(Topic, OM) of +set_reply(Msg, #session{transport_manager = TM} = Session) -> + TM2 = emqx_coap_tm:set_reply(Msg, TM), + Session#session{transport_manager = TM2}. + +deliver(Delivers, #session{observe_manager = OM, + transport_manager = TM} = Session) -> + Fun = fun({_, Topic, Message}, {OutAcc, OMAcc, TMAcc} = Acc) -> + case emqx_coap_observe_res:res_changed(Topic, OMAcc) of undefined -> Acc; {Token, SeqId, OM2} -> - Msg = mqtt_to_coap(Message, MsgId, Token, SeqId, Ctx), - SAcc2 = SAcc#session{next_msg_id = next_msg_id(MsgId, TM), - observe_manager = OM2}, - #{out := Out} = Result = handle_out(Msg, Ctx, SAcc2), - Result#{out := [Out | OutAcc]} + Msg = mqtt_to_coap(Message, Token, SeqId), + #{out := Out, tm := TM2} = emqx_coap_tm:handle_out(Msg, TMAcc), + {Out ++ OutAcc, OM2, TM2} end end, - lists:foldl(Fun, - #{out => [], session => Session}, - lists:reverse(Delivers)). + {Outs, OM2, TM2} = lists:foldl(Fun, {[], OM, TM}, lists:reverse(Delivers)), -timeout(Timer, Ctx, Session) -> - call_transport_manager(?FUNCTION_NAME, Timer, Ctx, [fun process_tm/3], Session). + #{out => lists:reverse(Outs), + session => Session#session{observe_manager = OM2, + transport_manager = TM2}}. -result_keys() -> - [tm, subscribe] ++ emqx_coap_channel:result_keys(). - -transfer_result(From, Value, Result) -> - ?TRANSFER_RESULT(From, Value, Result). +timeout(Timer, Session) -> + call_transport_manager(?FUNCTION_NAME, Timer, Session). %%%------------------------------------------------------------------- %%% Internal functions %%%------------------------------------------------------------------- call_transport_manager(Fun, Msg, - Ctx, - Processor, #session{transport_manager = TM} = Session) -> - try - Result = emqx_coap_tm:Fun(Msg, Ctx, TM), - {ok, Result2, Session2} = pipeline(Processor, - Result, - Msg, - Session), - emqx_coap_channel:transfer_result(session, Session2, Result2) - catch Type:Reason:Stack -> - ?ERROR("process transmission with, message:~p failed~nType:~p,Reason:~p~n,StackTrace:~p~n", - [Msg, Type, Reason, Stack]), - ?REPLY({error, internal_server_error}, Msg) - end. + Result = emqx_coap_tm:Fun(Msg, TM), + iter([tm, fun process_tm/4, fun process_session/3], + Result, + Session). -process_tm(#{tm := TM}, _, Session) -> - {ok, Session#session{transport_manager = TM}}; -process_tm(_, _, Session) -> - {ok, Session}. +process_tm(TM, Result, Session, Cursor) -> + iter(Cursor, Result, Session#session{transport_manager = TM}). -process_subscribe(#{subscribe := Sub} = Result, - Msg, - #session{observe_manager = OM} = Session) -> +process_session(_, Result, Session) -> + Result#{session => Session}. + +process_subscribe(Sub, Msg, Result, + #session{observe_manager = OM} = Session) -> case Sub of undefined -> - {ok, Result, Session}; + Result; {Topic, Token} -> {SeqId, OM2} = emqx_coap_observe_res:insert(Topic, Token, OM), Replay = emqx_coap_message:piggyback({ok, content}, Msg), Replay2 = Replay#coap_message{options = #{observe => SeqId}}, - {ok, Result#{reply => Replay2}, Session#session{observe_manager = OM2}}; + Result#{reply => Replay2, + session => Session#session{observe_manager = OM2}}; Topic -> OM2 = emqx_coap_observe_res:remove(Topic, OM), Replay = emqx_coap_message:piggyback({ok, nocontent}, Msg), - {ok, Result#{reply => Replay}, Session#session{observe_manager = OM2}} - end; -process_subscribe(Result, _, Session) -> - {ok, Result, Session}. + Result#{reply => Replay, + session => Session#session{observe_manager = OM2}} + end. -mqtt_to_coap(MQTT, MsgId, Token, SeqId, Ctx) -> +mqtt_to_coap(MQTT, Token, SeqId) -> #message{payload = Payload} = MQTT, - #coap_message{type = get_notify_type(MQTT, Ctx), + #coap_message{type = get_notify_type(MQTT), method = {ok, content}, - id = MsgId, token = Token, payload = Payload, options = #{observe => SeqId}}. -get_notify_type(#message{qos = Qos}, Ctx) -> - case emqx_coap_channel:get_config(notify_type, Ctx) of +get_notify_type(#message{qos = Qos}) -> + case emqx:get_config([gateway, coap, notify_qos], non) of qos -> case Qos of ?QOS_0 -> @@ -239,32 +223,3 @@ get_notify_type(#message{qos = Qos}, Ctx) -> Other -> Other end. - -next_msg_id(MsgId, TM) -> - next_msg_id(MsgId + 1, MsgId, TM). - -next_msg_id(MsgId, MsgId, _) -> - erlang:throw("too many message in delivering"); -next_msg_id(MsgId, BeginId, TM) when MsgId >= ?MAX_MESSAGE_ID -> - check_is_inused(1, BeginId, TM); -next_msg_id(MsgId, BeginId, TM) -> - check_is_inused(MsgId, BeginId, TM). - -check_is_inused(NewMsgId, BeginId, TM) -> - case emqx_coap_tm:is_inused(out, NewMsgId, TM) of - false -> - NewMsgId; - _ -> - next_msg_id(NewMsgId + 1, BeginId, TM) - end. - -pipeline([Fun | T], Result, Msg, Session) -> - case Fun(Result, Msg, Session) of - {ok, Session2} -> - pipeline(T, Result, Msg, Session2); - {ok, Result2, Session2} -> - pipeline(T, Result2, Msg, Session2) - end; - -pipeline([], Result, _, Session) -> - {ok, Result, Session}. diff --git a/apps/emqx_gateway/src/coap/emqx_coap_tm.erl b/apps/emqx_gateway/src/coap/emqx_coap_tm.erl index 5a664b0f2..bdc061b1d 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_tm.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_tm.erl @@ -18,11 +18,12 @@ -module(emqx_coap_tm). -export([ new/0 - , handle_request/3 - , handle_response/3 + , handle_request/2 + , handle_response/2 + , handle_out/2 , handle_out/3 - , timeout/3 - , is_inused/3]). + , set_reply/2 + , timeout/2]). -export_type([manager/0, event_result/1]). @@ -30,17 +31,28 @@ -include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). -type direction() :: in | out. --type state_machine_id() :: {direction(), non_neg_integer()}. --record(state_machine, { id :: state_machine_id() +-record(state_machine, { seq_id :: seq_id() + , id :: state_machine_key() + , token :: token() | undefined + , observe :: 0 | 1 | undefined | observed , state :: atom() , timers :: maps:map() , transport :: emqx_coap_transport:transport()}). -type state_machine() :: #state_machine{}. -type message_id() :: 0 .. ?MAX_MESSAGE_ID. +-type token_key() :: {token, token()}. +-type state_machine_key() :: {direction(), message_id()}. +-type seq_id() :: pos_integer(). +-type manager_key() :: token_key() | state_machine_key() | seq_id(). --type manager() :: #{message_id() => state_machine()}. +-type manager() :: #{ seq_id => seq_id() + , next_msg_id => coap_message_id() + , token_key() => seq_id() + , state_machine_key() => seq_id() + , seq_id() => state_machine() + }. -type ttimeout() :: {state_timeout, pos_integer(), any()} | {stop_timeout, pos_integer()}. @@ -48,6 +60,7 @@ -type topic() :: binary(). -type token() :: binary(). -type sub_register() :: {topic(), token()} | topic(). + -type event_result(State) :: #{next => State, outgoing => emqx_coap_message(), @@ -55,108 +68,161 @@ has_sub => undefined | sub_register(), transport => emqx_coap_transport:transprot()}. +-define(TOKEN_ID(T), {token, T}). + +-import(emqx_coap_medium, [empty/0, iter/4, reset/1, proto_out/2]). + %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- new() -> - #{}. + #{ seq_id => 1 + , next_msg_id => rand:uniform(?MAX_MESSAGE_ID) + }. -handle_request(#coap_message{id = MsgId} = Msg, Ctx, TM) -> +%% client request +handle_request(#coap_message{id = MsgId} = Msg, TM) -> Id = {in, MsgId}, - case maps:get(Id, TM, undefined) of + case find_machine(Id, TM) of undefined -> - Transport = emqx_coap_transport:new(), - Machine = new_state_machine(Id, Transport), - process_event(in, Msg, TM, Ctx, Machine); + {Machine, TM2} = new_in_machine(Id, TM), + process_event(in, Msg, TM2, Machine); Machine -> - process_event(in, Msg, TM, Ctx, Machine) + process_event(in, Msg, TM, Machine) end. -handle_response(#coap_message{type = Type, id = MsgId} = Msg, Ctx, TM) -> +%% client response +handle_response(#coap_message{type = Type, id = MsgId, token = Token} = Msg, TM) -> Id = {out, MsgId}, - case maps:get(Id, TM, undefined) of + TokenId = ?TOKEN_ID(Token), + case find_machine_by_keys([Id, TokenId], TM) of undefined -> case Type of reset -> - ?EMPTY_RESULT; + empty(); _ -> - ?RESET(Msg) + reset(Msg) end; Machine -> - process_event(in, Msg, TM, Ctx, Machine) + process_event(in, Msg, TM, Machine) end. -handle_out(#coap_message{id = MsgId} = Msg, Ctx, TM) -> +%% send to a client, msg can be request/piggyback/separate/notify +handle_out(Msg, TM) -> + handle_out(Msg, undefined, TM). + +handle_out(#coap_message{token = Token} = MsgT, Ctx, TM) -> + {MsgId, TM2} = alloc_message_id(TM), + Msg = MsgT#coap_message{id = MsgId}, Id = {out, MsgId}, - case maps:get(Id, TM, undefined) of + TokenId = ?TOKEN_ID(Token), + %% TODO why find by token ? + case find_machine_by_keys([Id, TokenId], TM2) of undefined -> - Transport = emqx_coap_transport:new(), - Machine = new_state_machine(Id, Transport), - process_event(out, Msg, TM, Ctx, Machine); + {Machine, TM3} = new_out_machine(Id, Msg, TM), + process_event(out, {Ctx, Msg}, TM3, Machine); _ -> %% ignore repeat send - ?EMPTY_RESULT + empty() end. -timeout({Id, Type, Msg}, Ctx, TM) -> - case maps:get(Id, TM, undefined) of +set_reply(#coap_message{id = MsgId} = Msg, TM) -> + Id = {in, MsgId}, + case find_machine(Id, TM) of undefined -> - ?EMPTY_RESULT; + TM; + #state_machine{transport = Transport, + seq_id = SeqId} = Machine -> + Transport2 = emqx_coap_transport:set_cache(Msg, Transport), + Machine2 = Machine#state_machine{transport = Transport2}, + TM#{SeqId => Machine2} + end. + +timeout({SeqId, Type, Msg}, TM) -> + case maps:get(SeqId, TM, undefined) of + undefined -> + empty(); #state_machine{timers = Timers} = Machine -> %% maybe timer has been canceled case maps:is_key(Type, Timers) of true -> - process_event(Type, Msg, TM, Ctx, Machine); + process_event(Type, Msg, TM, Machine); _ -> - ?EMPTY_RESULT + empty() end end. --spec is_inused(direction(), message_id(), manager()) -> boolean(). -is_inused(Dir, Msg, Manager) -> - maps:is_key({Dir, Msg}, Manager). - %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- -new_state_machine(Id, Transport) -> - #state_machine{id = Id, - state = idle, - timers = #{}, - transport = Transport}. +process_event(stop_timeout, _, TM, Machine) -> + process_manager(stop, #{}, Machine, TM); -process_event(stop_timeout, - _, - TM, - _, - #state_machine{id = Id, - timers = Timers}) -> - lists:foreach(fun({_, Ref}) -> - emqx_misc:cancel_timer(Ref) - end, - maps:to_list(Timers)), - #{tm => maps:remove(Id, TM)}; +process_event(Event, Msg, TM, #state_machine{state = State, + transport = Transport} = Machine) -> + Result = emqx_coap_transport:State(Event, Msg, Transport), + iter([ proto, fun process_observe_response/5 + , next, fun process_state_change/5 + , transport, fun process_transport_change/5 + , timeouts, fun process_timeouts/5 + , fun process_manager/4], + Result, + Machine, + TM). -process_event(Event, - Msg, - TM, - Ctx, - #state_machine{id = Id, - state = State, - transport = Transport} = Machine) -> - Result = emqx_coap_transport:State(Event, Msg, Ctx, Transport), - {ok, _, Machine2} = emqx_misc:pipeline([fun process_state_change/2, - fun process_transport_change/2, - fun process_timeouts/2], - Result, - Machine), - TM2 = TM#{Id => Machine2}, - emqx_coap_session:transfer_result(tm, TM2, Result). +process_observe_response({response, {_, Msg}} = Response, + Result, + #state_machine{observe = 0} = Machine, + TM, + Iter) -> + Result2 = proto_out(Response, Result), + case Msg#coap_message.method of + {ok, _} -> + iter(Iter, + Result2#{next => observe}, + Machine#state_machine{observe = observed}, + TM); + _ -> + iter(Iter, Result2, Machine, TM) + end; -process_state_change(#{next := Next}, Machine) -> - {ok, cancel_state_timer(Machine#state_machine{state = Next})}; -process_state_change(_, Machine) -> - {ok, Machine}. +process_observe_response(Proto, Result, Machine, TM, Iter) -> + iter(Iter, proto_out(Proto, Result), Machine, TM). + +process_state_change(Next, Result, Machine, TM, Iter) -> + case Next of + stop -> + process_manager(stop, Result, Machine, TM); + _ -> + iter(Iter, + Result, + cancel_state_timer(Machine#state_machine{state = Next}), + TM) + end. + +process_transport_change(Transport, Result, Machine, TM, Iter) -> + iter(Iter, Result, Machine#state_machine{transport = Transport}, TM). + +process_timeouts([], Result, Machine, TM, Iter) -> + iter(Iter, Result, Machine, TM); + +process_timeouts(Timeouts, Result, + #state_machine{seq_id = SeqId, + timers = Timers} = Machine, TM, Iter) -> + NewTimers = lists:foldl(fun({state_timeout, _, _} = Timer, Acc) -> + process_timer(SeqId, Timer, Acc); + ({stop_timeout, I}, Acc) -> + process_timer(SeqId, {stop_timeout, I, stop}, Acc) + end, + Timers, + Timeouts), + iter(Iter, Result, Machine#state_machine{timers = NewTimers}, TM). + +process_manager(stop, Result, #state_machine{seq_id = SeqId}, TM) -> + Result#{tm => delete_machine(SeqId, TM)}; + +process_manager(_, Result, #state_machine{seq_id = SeqId} = Machine2, TM) -> + Result#{tm => TM#{SeqId => Machine2}}. cancel_state_timer(#state_machine{timers = Timers} = Machine) -> case maps:get(state_timer, Timers, undefined) of @@ -167,27 +233,118 @@ cancel_state_timer(#state_machine{timers = Timers} = Machine) -> Machine#state_machine{timers = maps:remove(state_timer, Timers)} end. -process_transport_change(#{transport := Transport}, Machine) -> - {ok, Machine#state_machine{transport = Transport}}; -process_transport_change(_, Machine) -> - {ok, Machine}. - -process_timeouts(#{timeouts := []}, Machine) -> - {ok, Machine}; -process_timeouts(#{timeouts := Timeouts}, - #state_machine{id = Id, timers = Timers} = Machine) -> - NewTimers = lists:foldl(fun({state_timeout, _, _} = Timer, Acc) -> - process_timer(Id, Timer, Acc); - ({stop_timeout, I}, Acc) -> - process_timer(Id, {stop_timeout, I, stop}, Acc) - end, - Timers, - Timeouts), - {ok, Machine#state_machine{timers = NewTimers}}; - -process_timeouts(_, Machine) -> - {ok, Machine}. - -process_timer(Id, {Type, Interval, Msg}, Timers) -> - Ref = emqx_misc:start_timer(Interval, {state_machine, {Id, Type, Msg}}), +process_timer(SeqId, {Type, Interval, Msg}, Timers) -> + Ref = emqx_misc:start_timer(Interval, {state_machine, {SeqId, Type, Msg}}), Timers#{Type => Ref}. + +-spec delete_machine(manager_key(), manager()) -> manager(). +delete_machine(Id, Manager) -> + case find_machine(Id, Manager) of + undefined -> + Manager; + #state_machine{seq_id = SeqId, + id = MachineId, + token = Token, + timers = Timers} -> + lists:foreach(fun({_, Ref}) -> + emqx_misc:cancel_timer(Ref) + end, + maps:to_list(Timers)), + maps:without([SeqId, MachineId, ?TOKEN_ID(Token)], Manager) + end. + +-spec find_machine(manager_key(), manager()) -> state_machine() | undefined. +find_machine({_, _} = Id, Manager) -> + find_machine_by_seqid(maps:get(Id, Manager, undefined), Manager); +find_machine(SeqId, Manager) -> + find_machine_by_seqid(SeqId, Manager). + +-spec find_machine_by_seqid(seq_id() | undefined, manager()) -> + state_machine() | undefined. +find_machine_by_seqid(SeqId, Manager) -> + maps:get(SeqId, Manager, undefined). + +-spec find_machine_by_keys(list(manager_key()), + manager()) -> state_machine() | undefined. +find_machine_by_keys([H | T], Manager) -> + case H of + ?TOKEN_ID(<<>>) -> + find_machine_by_keys(T, Manager); + _ -> + case find_machine(H, Manager) of + undefined -> + find_machine_by_keys(T, Manager); + Machine -> + Machine + end + end; +find_machine_by_keys(_, _) -> + undefined. + +-spec new_in_machine(state_machine_key(), manager()) -> + {state_machine(), manager()}. +new_in_machine(MachineId, #{seq_id := SeqId} = Manager) -> + Machine = #state_machine{ seq_id = SeqId + , id = MachineId + , state = idle + , timers = #{} + , transport = emqx_coap_transport:new()}, + {Machine, Manager#{seq_id := SeqId + 1, + SeqId => Machine, + MachineId => SeqId}}. + +-spec new_out_machine(state_machine_key(), emqx_coap_message(), manager()) -> + {state_machine(), manager()}. +new_out_machine(MachineId, + #coap_message{type = Type, token = Token, options = Opts}, + #{seq_id := SeqId} = Manager) -> + Observe = maps:get(observe, Opts, undefined), + Machine = #state_machine{ seq_id = SeqId + , id = MachineId + , token = Token + , observe = Observe + , state = idle + , timers = #{} + , transport = emqx_coap_transport:new()}, + + Manager2 = Manager#{seq_id := SeqId + 1, + SeqId => Machine, + MachineId => SeqId}, + {Machine, + if Token =:= <<>> -> + Manager2; + Observe =:= 1 -> + TokenId = ?TOKEN_ID(Token), + delete_machine(TokenId, Manager2); + Type =:= con orelse Observe =:= 0 -> + TokenId = ?TOKEN_ID(Token), + case maps:get(TokenId, Manager, undefined) of + undefined -> + Manager2#{TokenId => SeqId}; + _ -> + throw("token conflict") + end; + true -> + Manager2 + end + }. + +alloc_message_id(#{next_msg_id := MsgId} = TM) -> + alloc_message_id(MsgId, TM). + +alloc_message_id(MsgId, TM) -> + Id = {out, MsgId}, + case maps:get(Id, TM, undefined) of + undefined -> + {MsgId, TM#{next_msg_id => next_message_id(MsgId)}}; + _ -> + alloc_message_id(next_message_id(MsgId), TM) + end. + +next_message_id(MsgId) -> + Next = MsgId + 1, + if Next >= ?MAX_MESSAGE_ID -> + 1; + true -> + Next + end. diff --git a/apps/emqx_gateway/src/coap/emqx_coap_transport.erl b/apps/emqx_gateway/src/coap/emqx_coap_transport.erl index 2c2aaab2e..eb7ce9bd4 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_transport.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_transport.erl @@ -9,19 +9,27 @@ -define(EXCHANGE_LIFETIME, 247000). -define(NON_LIFETIME, 145000). +-type request_context() :: any(). + -record(transport, { cache :: undefined | emqx_coap_message() + , req_context :: request_context() , retry_interval :: non_neg_integer() , retry_count :: non_neg_integer() + , observe :: non_neg_integer() | undefined }). -type transport() :: #transport{}. --export([ new/0, idle/4, maybe_reset/4 - , maybe_resend/4, wait_ack/4, until_stop/4]). +-export([ new/0, idle/3, maybe_reset/3, set_cache/2 + , maybe_resend_4request/3, wait_ack/3, until_stop/3 + , observe/3, maybe_resend_4response/3]). -export_type([transport/0]). -import(emqx_coap_message, [reset/1]). +-import(emqx_coap_medium, [ empty/0, reset/2, proto_out/2 + , out/1, out/2, proto_out/1 + , reply/2]). -spec new() -> transport(). new() -> @@ -31,96 +39,152 @@ new() -> idle(in, #coap_message{type = non, method = Method} = Msg, - Ctx, _) -> - Ret = #{next => until_stop, - timeouts => [{stop_timeout, ?NON_LIFETIME}]}, case Method of undefined -> - ?RESET(Msg); + reset(Msg, #{next => stop}); _ -> - Result = call_handler(Msg, Ctx), - maps:merge(Ret, Result) + proto_out({request, Msg}, + #{next => until_stop, + timeouts => + [{stop_timeout, ?NON_LIFETIME}]}) end; idle(in, #coap_message{type = con, method = Method} = Msg, - Ctx, - Transport) -> - Ret = #{next => maybe_resend, - timeouts =>[{stop_timeout, ?EXCHANGE_LIFETIME}]}, + _) -> case Method of undefined -> - ResetMsg = reset(Msg), - Ret#{transport => Transport#transport{cache = ResetMsg}, - out => ResetMsg}; + reset(Msg, #{next => stop}); _ -> - Result = call_handler(Msg, Ctx), - maps:merge(Ret, Result) + proto_out({request, Msg}, + #{next => maybe_resend_4request, + timeouts =>[{stop_timeout, ?EXCHANGE_LIFETIME}]}) end; -idle(out, #coap_message{type = non} = Msg, _, _) -> - #{next => maybe_reset, - out => Msg, - timeouts => [{stop_timeout, ?NON_LIFETIME}]}; +idle(out, {Ctx, Msg}, Transport) -> + idle(out, Msg, Transport#transport{req_context = Ctx}); -idle(out, Msg, _, Transport) -> +idle(out, #coap_message{type = non} = Msg, _) -> + out(Msg, #{next => maybe_reset, + timeouts => [{stop_timeout, ?NON_LIFETIME}]}); + +idle(out, Msg, Transport) -> _ = emqx_misc:rand_seed(), Timeout = ?ACK_TIMEOUT + rand:uniform(?ACK_RANDOM_FACTOR), - #{next => wait_ack, - transport => Transport#transport{cache = Msg}, - out => Msg, - timeouts => [ {state_timeout, Timeout, ack_timeout} - , {stop_timeout, ?EXCHANGE_LIFETIME}]}. + out(Msg, #{next => wait_ack, + transport => Transport#transport{cache = Msg}, + timeouts => [ {state_timeout, Timeout, ack_timeout} + , {stop_timeout, ?EXCHANGE_LIFETIME}]}). -maybe_reset(in, Message, _, _) -> - case Message of - #coap_message{type = reset} -> - ?INFO("Reset Message:~p~n", [Message]); +maybe_resend_4request(in, Msg, Transport) -> + maybe_resend(Msg, true, Transport). + +maybe_resend_4response(in, Msg, Transport) -> + maybe_resend(Msg, false, Transport). + +maybe_resend(Msg, IsExpecteReq, #transport{cache = Cache}) -> + IsExpected = emqx_coap_message:is_request(Msg) =:= IsExpecteReq, + case IsExpected of + true -> + case Cache of + undefined -> + %% handler in processing, ignore + empty(); + _ -> + out(Cache) + end; _ -> - ok - end, - ?EMPTY_RESULT. + reset(Msg, #{next => stop}) + end. -maybe_resend(in, _, _, #transport{cache = Cache}) -> - #{out => Cache}. +maybe_reset(in, #coap_message{type = Type, method = Method} = Message, + #transport{req_context = Ctx} = Transport) -> + Ret = #{next => stop}, + CtxMsg = {Ctx, Message}, + if Type =:= reset -> + proto_out({reset, CtxMsg}, Ret); + is_tuple(Method) -> + on_response(Message, + Transport, + if Type =:= non -> until_stop; + true -> maybe_resend_4response + end); + true -> + reset(Message, Ret) + end. -wait_ack(in, #coap_message{type = Type}, _, _) -> +wait_ack(in, #coap_message{type = Type, method = Method} = Msg, #transport{req_context = Ctx}) -> + CtxMsg = {Ctx, Msg}, case Type of - ack -> - #{next => until_stop}; reset -> - #{next => until_stop}; + proto_out({reset, CtxMsg}, #{next => stop}); _ -> - ?EMPTY_RESULT + case Method of + undefined -> + %% empty ack, keep transport to recv response + proto_out({ack, CtxMsg}); + {_, _} -> + %% ack with payload + proto_out({response, CtxMsg}, #{next => stop}); + _ -> + reset(Msg, #{next => stop}) + end end; wait_ack(state_timeout, ack_timeout, - _, #transport{cache = Msg, retry_interval = Timeout, retry_count = Count} =Transport) -> case Count < ?MAX_RETRANSMIT of true -> Timeout2 = Timeout * 2, - #{transport => Transport#transport{retry_interval = Timeout2, - retry_count = Count + 1}, - out => Msg, - timeouts => [{state_timeout, Timeout2, ack_timeout}]}; + out(Msg, + #{transport => Transport#transport{retry_interval = Timeout2, + retry_count = Count + 1}, + timeouts => [{state_timeout, Timeout2, ack_timeout}]}); _ -> - #{next_state => until_stop} + proto_out({ack_failure, Msg}, #{next_state => stop}) end. -until_stop(_, _, _, _) -> - ?EMPTY_RESULT. - -call_handler(#coap_message{options = Opts} = Msg, Ctx) -> - case maps:get(uri_path, Opts, undefined) of - [<<"ps">> | RestPath] -> - emqx_coap_pubsub_handler:handle_request(RestPath, Msg, Ctx); - [<<"mqtt">> | RestPath] -> - emqx_coap_mqtt_handler:handle_request(RestPath, Msg, Ctx); +observe(in, + #coap_message{method = Method} = Message, + #transport{observe = Observe} = Transport) -> + case Method of + {ok, _} -> + case emqx_coap_message:get_option(observe, Message, Observe) of + Observe -> + %% repeatd notify, ignore + empty(); + NewObserve -> + on_response(Message, + Transport#transport{observe = NewObserve}, + ?FUNCTION_NAME) + end; + {error, _} -> + #{next => stop}; _ -> - ?REPLY({error, bad_request}, Msg) + reset(Message) + end. + +until_stop(_, _, _) -> + empty(). + +set_cache(Cache, Transport) -> + Transport#transport{cache = Cache}. + +on_response(#coap_message{type = Type} = Message, + #transport{req_context = Ctx} = Transport, + NextState) -> + CtxMsg = {Ctx, Message}, + if Type =:= non -> + proto_out({response, CtxMsg}, #{next => NextState}); + Type =:= con -> + Ack = emqx_coap_message:ack(Message), + proto_out({response, CtxMsg}, + out(Ack, #{next => NextState, + transport => Transport#transport{cache = Ack}})); + true -> + reset(Message) end. diff --git a/apps/emqx_gateway/src/coap/handler/emqx_coap_mqtt_handler.erl b/apps/emqx_gateway/src/coap/handler/emqx_coap_mqtt_handler.erl index 88a4a2310..47bf14d9b 100644 --- a/apps/emqx_gateway/src/coap/handler/emqx_coap_mqtt_handler.erl +++ b/apps/emqx_gateway/src/coap/handler/emqx_coap_mqtt_handler.erl @@ -18,23 +18,24 @@ -include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). --export([handle_request/3]). +-export([handle_request/4]). -import(emqx_coap_message, [response/2, response/3]). +-import(emqx_coap_medium, [reply/2]). -handle_request([<<"connection">>], #coap_message{method = Method} = Msg, _) -> +handle_request([<<"connection">>], #coap_message{method = Method} = Msg, _Ctx, _CInfo) -> handle_method(Method, Msg); -handle_request(_, Msg, _) -> - ?REPLY({error, bad_request}, Msg). +handle_request(_, Msg, _, _) -> + reply({error, bad_request}, Msg). handle_method(put, Msg) -> - ?REPLY({ok, changed}, Msg); + reply({ok, changed}, Msg); -handle_method(post, _) -> - #{connection => open}; +handle_method(post, Msg) -> + #{connection => {open, Msg}}; -handle_method(delete, _) -> - #{connection => close}; +handle_method(delete, Msg) -> + #{connection => {close, Msg}}; handle_method(_, Msg) -> - ?REPLY({error, method_not_allowed}, Msg). + reply({error, method_not_allowed}, Msg). diff --git a/apps/emqx_gateway/src/coap/handler/emqx_coap_pubsub_handler.erl b/apps/emqx_gateway/src/coap/handler/emqx_coap_pubsub_handler.erl index e6886a559..ca734993a 100644 --- a/apps/emqx_gateway/src/coap/handler/emqx_coap_pubsub_handler.erl +++ b/apps/emqx_gateway/src/coap/handler/emqx_coap_pubsub_handler.erl @@ -20,48 +20,48 @@ -include_lib("emqx/include/emqx_mqtt.hrl"). -include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). --export([handle_request/3]). +-export([handle_request/4]). -import(emqx_coap_message, [response/2, response/3]). +-import(emqx_coap_medium, [reply/2, reply/3]). --define(UNSUB(Topic), #{subscribe => Topic}). --define(SUB(Topic, Token), #{subscribe => {Topic, Token}}). +-define(UNSUB(Topic, Msg), #{subscribe => {Topic, Msg}}). +-define(SUB(Topic, Token, Msg), #{subscribe => {{Topic, Token}, Msg}}). -define(SUBOPTS, #{qos => 0, rh => 0, rap => 0, nl => 0, is_new => false}). -handle_request(Path, #coap_message{method = Method} = Msg, Ctx) -> +handle_request(Path, #coap_message{method = Method} = Msg, Ctx, CInfo) -> case check_topic(Path) of {ok, Topic} -> - handle_method(Method, Topic, Msg, Ctx); + handle_method(Method, Topic, Msg, Ctx, CInfo); _ -> - ?REPLY({error, bad_request}, <<"invalid topic">>, Msg) + reply({error, bad_request}, <<"invalid topic">>, Msg) end. -handle_method(get, Topic, #coap_message{options = Opts} = Msg, Ctx) -> - case maps:get(observe, Opts, undefined) of +handle_method(get, Topic, Msg, Ctx, CInfo) -> + case emqx_coap_message:get_option(observe, Msg) of 0 -> - subscribe(Msg, Topic, Ctx); + subscribe(Msg, Topic, Ctx, CInfo); 1 -> - unsubscribe(Topic, Ctx); + unsubscribe(Msg, Topic, CInfo); _ -> - ?REPLY({error, bad_request}, <<"invalid observe value">>, Msg) + reply({error, bad_request}, <<"invalid observe value">>, Msg) end; -handle_method(post, Topic, #coap_message{payload = Payload} = Msg, Ctx) -> - case emqx_coap_channel:validator(publish, Topic, Ctx) of +handle_method(post, Topic, #coap_message{payload = Payload} = Msg, Ctx, CInfo) -> + case emqx_coap_channel:validator(publish, Topic, Ctx, CInfo) of allow -> - ClientInfo = emqx_coap_channel:get_clientinfo(Ctx), - #{clientid := ClientId} = ClientInfo, - QOS = get_publish_qos(Msg, Ctx), + #{clientid := ClientId} = CInfo, + QOS = get_publish_qos(Msg), MQTTMsg = emqx_message:make(ClientId, QOS, Topic, Payload), MQTTMsg2 = apply_publish_opts(Msg, MQTTMsg), _ = emqx_broker:publish(MQTTMsg2), - ?REPLY({ok, changed}, Msg); + reply({ok, changed}, Msg); _ -> - ?REPLY({error, unauthorized}, Msg) + reply({error, unauthorized}, Msg) end; -handle_method(_, _, Msg, _) -> - ?REPLY({error, method_not_allowed}, Msg). +handle_method(_, _, Msg, _, _) -> + reply({error, method_not_allowed}, Msg). check_topic([]) -> error; @@ -76,13 +76,13 @@ check_topic(Path) -> <<>>, Path))}. -get_sub_opts(#coap_message{options = Opts} = Msg, Ctx) -> +get_sub_opts(#coap_message{options = Opts} = Msg) -> SubOpts = maps:fold(fun parse_sub_opts/3, #{}, Opts), case SubOpts of #{qos := _} -> maps:merge(SubOpts, ?SUBOPTS); _ -> - CfgType = emqx_coap_channel:get_config(subscribe_qos, Ctx), + CfgType = emqx:get_config([gateway, coap, subscribe_qos], ?QOS_0), maps:merge(SubOpts, ?SUBOPTS#{qos => type_to_qos(CfgType, Msg)}) end. @@ -106,16 +106,16 @@ type_to_qos(coap, #coap_message{type = Type}) -> ?QOS_1 end. -get_publish_qos(#coap_message{options = Opts} = Msg, Ctx) -> - case maps:get(uri_query, Opts) of +get_publish_qos(Msg) -> + case emqx_coap_message:get_option(uri_query, Msg) of #{<<"qos">> := QOS} -> erlang:binary_to_integer(QOS); _ -> - CfgType = emqx_coap_channel:get_config(publish_qos, Ctx), + CfgType = emqx:get_config([gateway, coap, publish_qos], ?QOS_0), type_to_qos(CfgType, Msg) end. -apply_publish_opts(#coap_message{options = Opts}, MQTTMsg) -> +apply_publish_opts(Msg, MQTTMsg) -> maps:fold(fun(<<"retain">>, V, Acc) -> Val = erlang:binary_to_atom(V), emqx_message:set_flag(retain, Val, Acc); @@ -129,27 +129,25 @@ apply_publish_opts(#coap_message{options = Opts}, MQTTMsg) -> Acc end, MQTTMsg, - maps:get(uri_query, Opts)). + emqx_coap_message:get_option(uri_query, Msg)). -subscribe(#coap_message{token = <<>>} = Msg, _, _) -> - ?REPLY({error, bad_request}, <<"observe without token">>, Msg); +subscribe(#coap_message{token = <<>>} = Msg, _, _, _) -> + reply({error, bad_request}, <<"observe without token">>, Msg); -subscribe(#coap_message{token = Token} = Msg, Topic, Ctx) -> - case emqx_coap_channel:validator(subscribe, Topic, Ctx) of +subscribe(#coap_message{token = Token} = Msg, Topic, Ctx, CInfo) -> + case emqx_coap_channel:validator(subscribe, Topic, Ctx, CInfo) of allow -> - ClientInfo = emqx_coap_channel:get_clientinfo(Ctx), - #{clientid := ClientId} = ClientInfo, - SubOpts = get_sub_opts(Msg, Ctx), + #{clientid := ClientId} = CInfo, + SubOpts = get_sub_opts(Msg), emqx_broker:subscribe(Topic, ClientId, SubOpts), emqx_hooks:run('session.subscribed', - [ClientInfo, Topic, SubOpts]), - ?SUB(Topic, Token); + [CInfo, Topic, SubOpts]), + ?SUB(Topic, Token, Msg); _ -> - ?REPLY({error, unauthorized}, Msg) + reply({error, unauthorized}, Msg) end. -unsubscribe(Topic, Ctx) -> - ClientInfo = emqx_coap_channel:get_clientinfo(Ctx), +unsubscribe(Msg, Topic, CInfo) -> emqx_broker:unsubscribe(Topic), - emqx_hooks:run('session.unsubscribed', [ClientInfo, Topic, ?SUBOPTS]), - ?UNSUB(Topic). + emqx_hooks:run('session.unsubscribed', [CInfo, Topic, ?SUBOPTS]), + ?UNSUB(Topic, Msg). diff --git a/apps/emqx_gateway/src/coap/include/emqx_coap.hrl b/apps/emqx_gateway/src/coap/include/emqx_coap.hrl index 3b0268abb..d47dd17fd 100644 --- a/apps/emqx_gateway/src/coap/include/emqx_coap.hrl +++ b/apps/emqx_gateway/src/coap/include/emqx_coap.hrl @@ -22,18 +22,6 @@ -define(DEFAULT_MAX_AGE, 60). -define(MAXIMUM_MAX_AGE, 4294967295). --define(EMPTY_RESULT, #{}). --define(TRANSFER_RESULT(From, Value, R1), - begin - Keys = result_keys(), - R2 = maps:with(Keys, R1), - R2#{From => Value} - end). - --define(RESET(Msg), #{out => emqx_coap_message:reset(Msg)}). --define(REPLY(Resp, Payload, Msg), #{out => emqx_coap_message:piggyback(Resp, Payload, Msg)}). --define(REPLY(Resp, Msg), ?REPLY(Resp, <<>>, Msg)). - -type coap_message_id() :: 1 .. ?MAX_MESSAGE_ID. -type message_type() :: con | non | ack | reset. -type max_age() :: 1 .. ?MAXIMUM_MAX_AGE. @@ -66,7 +54,7 @@ , uri_path => list(binary()) , content_format => 0 .. 65535 , max_age => non_neg_integer() - , uri_query => list(binary()) + , uri_query => list(binary()) | map() , 'accept' => 0 .. 65535 , location_query => list(binary()) , proxy_uri => binary() @@ -85,7 +73,4 @@ , options = #{} , payload = <<>>}). --record(coap_content, {etag, max_age = ?DEFAULT_MAX_AGE, format, location_path = [], payload = <<>>}). - -type emqx_coap_message() :: #coap_message{}. --type coap_content() :: #coap_content{}. diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index 9371f8c6b..9ab26e480 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -94,6 +94,7 @@ fields(lwm2m_structs) -> , {lifetime_max, t(duration())} , {qmode_time_windonw, t(integer())} , {auto_observe, t(boolean())} + , {mountpoint, t(string())} , {update_msg_publish_condition, t(union([always, contains_object_list]))} , {translators, t(ref(translators))} , {listeners, t(ref(udp_listener_group))} @@ -122,7 +123,17 @@ fields(clientinfo_override) -> ]; fields(translators) -> - [{"$name", t(binary())}]; + [ {command, t(ref(translator))} + , {response, t(ref(translator))} + , {notify, t(ref(translator))} + , {register, t(ref(translator))} + , {update, t(ref(translator))} + ]; + +fields(translator) -> + [ {topic, t(binary())} + , {qos, t(range(0, 2))} + ]; fields(udp_listener_group) -> [ {udp, t(ref(udp_listener))} @@ -160,7 +171,7 @@ fields(listener_settings) -> , {max_connections, t(integer(), undefined, 1024)} , {max_conn_rate, t(integer())} , {active_n, t(integer(), undefined, 100)} - %, {rate_limit, t(comma_separated_list())} + %, {rate_limit, t(comma_separated_list())} , {access, t(ref(access))} , {proxy_protocol, t(boolean())} , {proxy_protocol_timeout, t(duration())} @@ -183,24 +194,24 @@ fields(tcp_listener_settings) -> fields(ssl_listener_settings) -> [ - %% some special confs for ssl listener + %% some special confs for ssl listener ] ++ - ssl(undefined, #{handshake_timeout => <<"15s">> - , depth => 10 - , reuse_sessions => true}) ++ fields(listener_settings); + ssl(undefined, #{handshake_timeout => <<"15s">> + , depth => 10 + , reuse_sessions => true}) ++ fields(listener_settings); fields(udp_listener_settings) -> [ - %% some special confs for udp listener + %% some special confs for udp listener ] ++ fields(listener_settings); fields(dtls_listener_settings) -> [ - %% some special confs for dtls listener + %% some special confs for dtls listener ] ++ - ssl(undefined, #{handshake_timeout => <<"15s">> - , depth => 10 - , reuse_sessions => true}) ++ fields(listener_settings); + ssl(undefined, #{handshake_timeout => <<"15s">> + , depth => 10 + , reuse_sessions => true}) ++ fields(listener_settings); fields(access) -> [ {"$id", #{type => binary(), @@ -270,7 +281,7 @@ ref(Field) -> %% ... ssl(Mapping, Defaults) -> M = fun (Field) -> - case (Mapping) of + case (Mapping) of undefined -> undefined; _ -> Mapping ++ "." ++ Field end end, diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl index 80449238c..03c3a6bc2 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl @@ -55,7 +55,8 @@ list(#{node := Node }, Params) -> end; list(#{}, _Params) -> - Channels = emqx_lwm2m_cm:all_channels(), + %% Channels = emqx_lwm2m_cm:all_channels(), + Channels = [], return({ok, format(Channels)}). lookup_cmd(#{ep := Ep, node := Node}, Params) -> @@ -64,26 +65,27 @@ lookup_cmd(#{ep := Ep, node := Node}, Params) -> _ -> rpc_call(Node, lookup_cmd, [#{ep => Ep}, Params]) end; -lookup_cmd(#{ep := Ep}, Params) -> - MsgType = proplists:get_value(<<"msgType">>, Params), - Path0 = proplists:get_value(<<"path">>, Params), - case emqx_lwm2m_cm:lookup_cmd(Ep, Path0, MsgType) of - [] -> return({ok, []}); - [{_, undefined} | _] -> return({ok, []}); - [{{IMEI, Path, MsgType}, undefined}] -> - return({ok, [{imei, IMEI}, - {'msgType', IMEI}, - {'code', <<"6.01">>}, - {'codeMsg', <<"reply_not_received">>}, - {'path', Path}]}); - [{{IMEI, Path, MsgType}, {Code, CodeMsg, Content}}] -> - Payload1 = format_cmd_content(Content, MsgType), - return({ok, [{imei, IMEI}, - {'msgType', IMEI}, - {'code', Code}, - {'codeMsg', CodeMsg}, - {'path', Path}] ++ Payload1}) - end. +lookup_cmd(#{ep := _Ep}, Params) -> + _MsgType = proplists:get_value(<<"msgType">>, Params), + _Path0 = proplists:get_value(<<"path">>, Params), + %% case emqx_lwm2m_cm:lookup_cmd(Ep, Path0, MsgType) of + %% [] -> return({ok, []}); + %% [{_, undefined} | _] -> return({ok, []}); + %% [{{IMEI, Path, MsgType}, undefined}] -> + %% return({ok, [{imei, IMEI}, + %% {'msgType', IMEI}, + %% {'code', <<"6.01">>}, + %% {'codeMsg', <<"reply_not_received">>}, + %% {'path', Path}]}); + %% [{{IMEI, Path, MsgType}, {Code, CodeMsg, Content}}] -> + %% Payload1 = format_cmd_content(Content, MsgType), + %% return({ok, [{imei, IMEI}, + %% {'msgType', IMEI}, + %% {'code', Code}, + %% {'codeMsg', CodeMsg}, + %% {'path', Path}] ++ Payload1}) + %% end. + return({ok, []}). rpc_call(Node, Fun, Args) -> case rpc:call(Node, ?MODULE, Fun, Args) of @@ -115,36 +117,37 @@ format(Channels) -> {'objectList', ObjectList}] end, Channels). -format_cmd_content(undefined, _MsgType) -> []; -format_cmd_content(Content, <<"discover">>) -> - [H | Content1] = Content, - {_, [HObjId]} = emqx_lwm2m_coap_resource:parse_object_list(H), - [ObjId | _]= path_list(HObjId), - ObjectList = case Content1 of - [Content2 | _] -> - {_, ObjL} = emqx_lwm2m_coap_resource:parse_object_list(Content2), - ObjL; - [] -> [] - end, - R = case emqx_lwm2m_xml_object:get_obj_def(binary_to_integer(ObjId), true) of - {error, _} -> - lists:map(fun(Object) -> {Object, Object} end, ObjectList); - ObjDefinition -> - lists:map(fun(Object) -> - [_, _, ResId| _] = path_list(Object), - Operations = case emqx_lwm2m_xml_object:get_resource_operations(binary_to_integer(ResId), ObjDefinition) of - "E" -> [{operations, list_to_binary("E")}]; - Oper -> [{'dataType', list_to_binary(emqx_lwm2m_xml_object:get_resource_type(binary_to_integer(ResId), ObjDefinition))}, - {operations, list_to_binary(Oper)}] - end, - [{path, Object}, - {name, list_to_binary(emqx_lwm2m_xml_object:get_resource_name(binary_to_integer(ResId), ObjDefinition))} - ] ++ Operations - end, ObjectList) - end, - [{content, R}]; -format_cmd_content(Content, _) -> - [{content, Content}]. +%% format_cmd_content(undefined, _MsgType) -> []; +%% format_cmd_content(_Content, <<"discover">>) -> +%% %% [H | Content1] = Content, +%% %% {_, [HObjId]} = emqx_lwm2m_coap_resource:parse_object_list(H), +%% %% [ObjId | _]= path_list(HObjId), +%% %% ObjectList = case Content1 of +%% %% [Content2 | _] -> +%% %% {_, ObjL} = emqx_lwm2m_coap_resource:parse_object_list(Content2), +%% %% ObjL; +%% %% [] -> [] +%% %% end, +%% %% R = case emqx_lwm2m_xml_object:get_obj_def(binary_to_integer(ObjId), true) of +%% %% {error, _} -> +%% %% lists:map(fun(Object) -> {Object, Object} end, ObjectList); +%% %% ObjDefinition -> +%% %% lists:map(fun(Object) -> +%% %% [_, _, ResId| _] = path_list(Object), +%% %% Operations = case emqx_lwm2m_xml_object:get_resource_operations(binary_to_integer(ResId), ObjDefinition) of +%% %% "E" -> [{operations, list_to_binary("E")}]; +%% %% Oper -> [{'dataType', list_to_binary(emqx_lwm2m_xml_object:get_resource_type(binary_to_integer(ResId), ObjDefinition))}, +%% %% {operations, list_to_binary(Oper)}] +%% %% end, +%% %% [{path, Object}, +%% %% {name, list_to_binary(emqx_lwm2m_xml_object:get_resource_name(binary_to_integer(ResId), ObjDefinition))} +%% %% ] ++ Operations +%% %% end, ObjectList) +%% %% end, +%% %% [{content, R}]; +%% []; +%% format_cmd_content(Content, _) -> +%% [{content, Content}]. ntoa({0,0,0,0,0,16#ffff,AB,CD}) -> inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256}); diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl new file mode 100644 index 000000000..80078407b --- /dev/null +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl @@ -0,0 +1,459 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2017-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_lwm2m_channel). + +-include_lib("emqx/include/logger.hrl"). +-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). +-include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl"). + +%% API +-export([ info/1 + , info/2 + , stats/1 + , validator/2 + , validator/4 + , do_takeover/3]). + +-export([ init/2 + , handle_in/2 + , handle_deliver/2 + , handle_timeout/3 + , terminate/2 + ]). + +-export([ handle_call/2 + , handle_cast/2 + , handle_info/2 + ]). + +-record(channel, { + %% Context + ctx :: emqx_gateway_ctx:context(), + %% Connection Info + conninfo :: emqx_types:conninfo(), + %% Client Info + clientinfo :: emqx_types:clientinfo(), + %% Session + session :: emqx_lwm2m_session:session() | undefined, + + %% Timer + timers :: #{atom() => disable | undefined | reference()}, + + validator :: function() + }). + +-define(INFO_KEYS, [conninfo, conn_state, clientinfo, session]). + +-import(emqx_coap_medium, [reply/2, reply/3, reply/4, iter/3, iter/4]). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- +info(Channel) -> + maps:from_list(info(?INFO_KEYS, Channel)). + +info(Keys, Channel) when is_list(Keys) -> + [{Key, info(Key, Channel)} || Key <- Keys]; + +info(conninfo, #channel{conninfo = ConnInfo}) -> + ConnInfo; +info(conn_state, _) -> + connected; +info(clientinfo, #channel{clientinfo = ClientInfo}) -> + ClientInfo; +info(session, #channel{session = Session}) -> + emqx_misc:maybe_apply(fun emqx_session:info/1, Session); +info(clientid, #channel{clientinfo = #{clientid := ClientId}}) -> + ClientId; +info(ctx, #channel{ctx = Ctx}) -> + Ctx. + +stats(_) -> + []. + +init(ConnInfo = #{peername := {PeerHost, _}, + sockname := {_, SockPort}}, + #{ctx := Ctx} = Config) -> + Peercert = maps:get(peercert, ConnInfo, undefined), + Mountpoint = maps:get(mountpoint, Config, undefined), + ClientInfo = set_peercert_infos( + Peercert, + #{ zone => default + , protocol => lwm2m + , peerhost => PeerHost + , sockport => SockPort + , username => undefined + , clientid => undefined + , is_bridge => false + , is_superuser => false + , mountpoint => Mountpoint + } + ), + + #channel{ ctx = Ctx + , conninfo = ConnInfo + , clientinfo = ClientInfo + , timers = #{} + , session = emqx_lwm2m_session:new() + , validator = validator(Ctx, ClientInfo) + }. + +validator(_Type, _Topic, _Ctx, _ClientInfo) -> + allow. + %emqx_gateway_ctx:authorize(Ctx, ClientInfo, Type, Topic). + +validator(Ctx, ClientInfo) -> + fun(Type, Topic) -> + validator(Type, Topic, Ctx, ClientInfo) + end. + +%%-------------------------------------------------------------------- +%% Handle incoming packet +%%-------------------------------------------------------------------- +handle_in(Msg, ChannleT) -> + Channel = update_life_timer(ChannleT), + call_session(handle_coap_in, Msg, Channel). + +%%-------------------------------------------------------------------- +%% Handle Delivers from broker to client +%%-------------------------------------------------------------------- +handle_deliver(Delivers, Channel) -> + call_session(handle_deliver, Delivers, Channel). + +%%-------------------------------------------------------------------- +%% Handle timeout +%%-------------------------------------------------------------------- +handle_timeout(_, lifetime, Channel) -> + {shutdown, timeout, Channel}; + +handle_timeout(_, {transport, _} = Msg, Channel) -> + call_session(timeout, Msg, Channel); + +handle_timeout(_, disconnect, Channel) -> + {shutdown, normal, Channel}; + +handle_timeout(_, _, Channel) -> + {ok, Channel}. + +%%-------------------------------------------------------------------- +%% Handle call +%%-------------------------------------------------------------------- +handle_call(Req, Channel) -> + ?LOG(error, "Unexpected call: ~p", [Req]), + {reply, ignored, Channel}. + +%%-------------------------------------------------------------------- +%% Handle Cast +%%-------------------------------------------------------------------- +handle_cast(Req, Channel) -> + ?LOG(error, "Unexpected cast: ~p", [Req]), + {ok, Channel}. + +%%-------------------------------------------------------------------- +%% Handle Info +%%-------------------------------------------------------------------- +handle_info(Info, Channel) -> + ?LOG(error, "Unexpected info: ~p", [Info]), + {ok, Channel}. + +%%-------------------------------------------------------------------- +%% Terminate +%%-------------------------------------------------------------------- +terminate(_Reason, #channel{session = Session}) -> + emqx_lwm2m_session:on_close(Session). + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- +set_peercert_infos(NoSSL, ClientInfo) + when NoSSL =:= nossl; + NoSSL =:= undefined -> + ClientInfo; +set_peercert_infos(Peercert, ClientInfo) -> + {DN, CN} = {esockd_peercert:subject(Peercert), + esockd_peercert:common_name(Peercert)}, + ClientInfo#{dn => DN, cn => CN}. + +make_timer(Name, Time, Msg, Channel = #channel{timers = Timers}) -> + TRef = emqx_misc:start_timer(Time, Msg), + Channel#channel{timers = Timers#{Name => TRef}}. + +update_life_timer(#channel{session = Session, timers = Timers} = Channel) -> + LifeTime = emqx_lwm2m_session:info(lifetime, Session), + _ = case maps:get(lifetime, Timers, undefined) of + undefined -> ok; + Ref -> erlang:cancel_timer(Ref) + end, + make_timer(lifetime, LifeTime, lifetime, Channel). + +check_location(Location, #channel{session = Session}) -> + SLocation = emqx_lwm2m_session:info(location_path, Session), + Location =:= SLocation. + +do_takeover(_DesireId, Msg, Channel) -> + %% TODO completed the takeover, now only reset the message + Reset = emqx_coap_message:reset(Msg), + call_session(handle_out, Reset, Channel). + +do_connect(Req, Result, Channel, Iter) -> + case emqx_misc:pipeline( + [ fun check_lwm2m_version/2 + , fun run_conn_hooks/2 + , fun enrich_clientinfo/2 + , fun set_log_meta/2 + , fun auth_connect/2 + ], + Req, + Channel) of + {ok, _Input, #channel{session = Session, + validator = Validator} = NChannel} -> + case emqx_lwm2m_session:info(reg_info, Session) of + undefined -> + process_connect(ensure_connected(NChannel), Req, Result, Iter); + _ -> + NewResult = emqx_lwm2m_session:reregister(Req, Validator, Session), + iter(Iter, maps:merge(Result, NewResult), NChannel) + end; + {error, ReasonCode, NChannel} -> + ErrMsg = io_lib:format("Login Failed: ~s", [ReasonCode]), + Payload = erlang:list_to_binary(lists:flatten(ErrMsg)), + iter(Iter, + reply({error, bad_request}, Payload, Req, Result), + NChannel) + end. + +check_lwm2m_version(#coap_message{options = Opts}, + #channel{conninfo = ConnInfo} = Channel) -> + Ver = gets([uri_query, <<"lwm2m">>], Opts), + IsValid = case Ver of + <<"1.0">> -> + true; + <<"1">> -> + true; + <<"1.1">> -> + true; + _ -> + false + end, + if IsValid -> + NConnInfo = ConnInfo#{ connected_at => erlang:system_time(millisecond) + , proto_name => <<"lwm2m">> + , proto_ver => Ver + }, + {ok, Channel#channel{conninfo = NConnInfo}}; + true -> + ?LOG(error, "Reject REGISTER due to unsupported version: ~0p", [Ver]), + {error, "invalid lwm2m version", Channel} + end. + +run_conn_hooks(Input, Channel = #channel{ctx = Ctx, + conninfo = ConnInfo}) -> + ConnProps = #{}, + case run_hooks(Ctx, 'client.connect', [ConnInfo], ConnProps) of + Error = {error, _Reason} -> Error; + _NConnProps -> + {ok, Input, Channel} + end. + +enrich_clientinfo(#coap_message{options = Options} = Msg, + Channel = #channel{clientinfo = ClientInfo0}) -> + Query = maps:get(uri_query, Options, #{}), + case Query of + #{<<"ep">> := Epn} -> + UserName = maps:get(<<"imei">>, Query, undefined), + Password = maps:get(<<"password">>, Query, undefined), + ClientId = maps:get(<<"device_id">>, Query, Epn), + ClientInfo = + ClientInfo0#{username => UserName, + password => Password, + clientid => ClientId}, + {ok, NClientInfo} = fix_mountpoint(Msg, ClientInfo), + {ok, Channel#channel{clientinfo = NClientInfo}}; + _ -> + ?LOG(error, "Reject REGISTER due to wrong parameters, Query=~p", [Query]), + {error, "invalid queries", Channel} + end. + +set_log_meta(_Input, #channel{clientinfo = #{clientid := ClientId}}) -> + emqx_logger:set_metadata_clientid(ClientId), + ok. + +auth_connect(_Input, Channel = #channel{ctx = Ctx, + clientinfo = ClientInfo}) -> + #{clientid := ClientId, username := Username} = ClientInfo, + case emqx_gateway_ctx:authenticate(Ctx, ClientInfo) of + {ok, NClientInfo} -> + {ok, Channel#channel{clientinfo = NClientInfo, + validator = validator(Ctx, ClientInfo)}}; + {error, Reason} -> + ?LOG(warning, "Client ~s (Username: '~s') login failed for ~0p", + [ClientId, Username, Reason]), + {error, Reason} + end. + +fix_mountpoint(_Packet, #{mountpoint := undefined} = ClientInfo) -> + {ok, ClientInfo}; +fix_mountpoint(_Packet, ClientInfo = #{mountpoint := Mountpoint}) -> + %% TODO: Enrich the varibale replacement???? + %% i.e: ${ClientInfo.auth_result.productKey} + Mountpoint1 = emqx_mountpoint:replvar(Mountpoint, ClientInfo), + {ok, ClientInfo#{mountpoint := Mountpoint1}}. + +ensure_connected(Channel = #channel{ctx = Ctx, + conninfo = ConnInfo, + clientinfo = ClientInfo}) -> + ok = run_hooks(Ctx, 'client.connected', [ClientInfo, ConnInfo]), + Channel. + +process_connect(Channel = #channel{ctx = Ctx, + session = Session, + conninfo = ConnInfo, + clientinfo = ClientInfo, + validator = Validator}, + Msg, Result, Iter) -> + %% inherit the old session + SessFun = fun(_,_) -> #{} end, + case emqx_gateway_ctx:open_session( + Ctx, + true, + ClientInfo, + ConnInfo, + SessFun, + emqx_lwm2m_session + ) of + {ok, _} -> + NewResult = emqx_lwm2m_session:init(Msg, Validator, Session), + iter(Iter, maps:merge(Result, NewResult), Channel); + {error, Reason} -> + ?LOG(error, "Failed to open session du to ~p", [Reason]), + iter(Iter, reply({error, bad_request}, Msg, Result), Channel) + end. + +run_hooks(Ctx, Name, Args) -> + emqx_gateway_ctx:metrics_inc(Ctx, Name), + emqx_hooks:run(Name, Args). + +run_hooks(Ctx, Name, Args, Acc) -> + emqx_gateway_ctx:metrics_inc(Ctx, Name), + emqx_hooks:run_fold(Name, Args, Acc). + +gets(_, undefined) -> + undefined; +gets([H | T], Map) -> + gets(T, maps:get(H, Map, undefined)); +gets([], Val) -> + Val. + +%%-------------------------------------------------------------------- +%% Call Chain +%%-------------------------------------------------------------------- +call_session(Fun, + Msg, + #channel{session = Session, + validator = Validator} = Channel) -> + iter([ session, fun process_session/4 + , proto, fun process_protocol/4 + , return, fun process_return/4 + , lifetime, fun process_lifetime/4 + , reply, fun process_reply/4 + , out, fun process_out/4 + , fun process_nothing/3 + ], + emqx_lwm2m_session:Fun(Msg, Validator, Session), + Channel). + +process_session(Session, Result, Channel, Iter) -> + iter(Iter, Result, Channel#channel{session = Session}). + +process_protocol({request, Msg}, Result, Channel, Iter) -> + #coap_message{method = Method} = Msg, + handle_request_protocol(Method, Msg, Result, Channel, Iter); + +process_protocol(Msg, Result, + #channel{validator = Validator, session = Session} = Channel, Iter) -> + ProtoResult = emqx_lwm2m_session:handle_protocol_in(Msg, Validator, Session), + iter(Iter, maps:merge(Result, ProtoResult), Channel). + +handle_request_protocol(post, #coap_message{options = Opts} = Msg, + Result, Channel, Iter) -> + case Opts of + #{uri_path := [?REG_PREFIX]} -> + do_connect(Msg, Result, Channel, Iter); + #{uri_path := Location} -> + do_update(Location, Msg, Result, Channel, Iter); + _ -> + iter(Iter, reply({error, not_found}, Msg, Result), Channel) + end; + +handle_request_protocol(delete, #coap_message{options = Opts} = Msg, + Result, Channel, Iter) -> + case Opts of + #{uri_path := Location} -> + case check_location(Location, Channel) of + true -> + Reply = emqx_coap_message:piggyback({ok, deleted}, Msg), + {shutdown, close, Reply, Channel}; + _ -> + iter(Iter, reply({error, not_found}, Msg, Result), Channel) + end; + _ -> + iter(Iter, reply({error, bad_request}, Msg, Result), Channel) + end. + +do_update(Location, Msg, Result, + #channel{session = Session, validator = Validator} = Channel, Iter) -> + case check_location(Location, Channel) of + true -> + NewResult = emqx_lwm2m_session:update(Msg, Validator, Session), + iter(Iter, maps:merge(Result, NewResult), Channel); + _ -> + iter(Iter, reply({error, not_found}, Msg, Result), Channel) + end. + +process_return({Outs, Session}, Result, Channel, Iter) -> + OldOuts = maps:get(out, Result, []), + iter(Iter, + Result#{out => Outs ++ OldOuts}, + Channel#channel{session = Session}). + +process_out(Outs, Result, Channel, _) -> + Outs2 = lists:reverse(Outs), + Outs3 = case maps:get(reply, Result, undefined) of + undefined -> + Outs2; + Reply -> + [Reply | Outs2] + end, + %% emqx_gateway_conn bug, work around + case Outs3 of + [] -> + {ok, Channel}; + _ -> + {ok, {outgoing, Outs3}, Channel} + end. + +process_reply(Reply, Result, #channel{session = Session} = Channel, _) -> + Session2 = emqx_lwm2m_session:set_reply(Reply, Session), + Outs = maps:get(out, Result, []), + Outs2 = lists:reverse(Outs), + {ok, {outgoing, [Reply | Outs2]}, Channel#channel{session = Session2}}. + +process_lifetime(_, Result, Channel, Iter) -> + iter(Iter, Result, update_life_timer(Channel)). + +process_nothing(_, _, Channel) -> + {ok, Channel}. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cm.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cm.erl deleted file mode 100644 index 16e938b84..000000000 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cm.erl +++ /dev/null @@ -1,153 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_lwm2m_cm). - --export([start_link/0]). - --export([ register_channel/5 - , update_reg_info/2 - , unregister_channel/1 - ]). - --export([ lookup_channel/1 - , all_channels/0 - ]). - --export([ register_cmd/3 - , register_cmd/4 - , lookup_cmd/3 - , lookup_cmd_by_imei/1 - ]). - -%% gen_server callbacks --export([ init/1 - , handle_call/3 - , handle_cast/2 - , handle_info/2 - , terminate/2 - , code_change/3 - ]). - --define(LOG(Level, Format, Args), logger:Level("LWM2M-CM: " ++ Format, Args)). - -%% Server name --define(CM, ?MODULE). - --define(LWM2M_CHANNEL_TAB, emqx_lwm2m_channel). --define(LWM2M_CMD_TAB, emqx_lwm2m_cmd). - -%% Batch drain --define(BATCH_SIZE, 100000). - -%% @doc Start the channel manager. -start_link() -> - gen_server:start_link({local, ?CM}, ?MODULE, [], []). - -%%-------------------------------------------------------------------- -%% API -%%-------------------------------------------------------------------- - -register_channel(IMEI, RegInfo, LifeTime, Ver, Peername) -> - Info = #{ - reg_info => RegInfo, - lifetime => LifeTime, - version => Ver, - peername => Peername - }, - true = ets:insert(?LWM2M_CHANNEL_TAB, {IMEI, Info}), - cast({registered, {IMEI, self()}}). - -update_reg_info(IMEI, RegInfo) -> - case lookup_channel(IMEI) of - [{_, RegInfo0}] -> - true = ets:insert(?LWM2M_CHANNEL_TAB, {IMEI, RegInfo0#{reg_info => RegInfo}}), - ok; - [] -> - ok - end. - -unregister_channel(IMEI) when is_binary(IMEI) -> - true = ets:delete(?LWM2M_CHANNEL_TAB, IMEI), - ok. - -lookup_channel(IMEI) -> - ets:lookup(?LWM2M_CHANNEL_TAB, IMEI). - -all_channels() -> - ets:tab2list(?LWM2M_CHANNEL_TAB). - -register_cmd(IMEI, Path, Type) -> - true = ets:insert(?LWM2M_CMD_TAB, {{IMEI, Path, Type}, undefined}). - -register_cmd(_IMEI, undefined, _Type, _Result) -> - ok; -register_cmd(IMEI, Path, Type, Result) -> - true = ets:insert(?LWM2M_CMD_TAB, {{IMEI, Path, Type}, Result}). - -lookup_cmd(IMEI, Path, Type) -> - ets:lookup(?LWM2M_CMD_TAB, {IMEI, Path, Type}). - -lookup_cmd_by_imei(IMEI) -> - ets:select(?LWM2M_CHANNEL_TAB, [{{{IMEI, '_', '_'}, '$1'}, [], ['$_']}]). - -%% @private -cast(Msg) -> gen_server:cast(?CM, Msg). - -%%-------------------------------------------------------------------- -%% gen_server callbacks -%%-------------------------------------------------------------------- - -init([]) -> - TabOpts = [public, {write_concurrency, true}, {read_concurrency, true}], - ok = emqx_tables:new(?LWM2M_CHANNEL_TAB, [set, compressed | TabOpts]), - ok = emqx_tables:new(?LWM2M_CMD_TAB, [set, compressed | TabOpts]), - {ok, #{chan_pmon => emqx_pmon:new()}}. - -handle_call(Req, _From, State) -> - ?LOG(error, "Unexpected call: ~p", [Req]), - {reply, ignored, State}. - -handle_cast({registered, {IMEI, ChanPid}}, State = #{chan_pmon := PMon}) -> - PMon1 = emqx_pmon:monitor(ChanPid, IMEI, PMon), - {noreply, State#{chan_pmon := PMon1}}; - -handle_cast(Msg, State) -> - ?LOG(error, "Unexpected cast: ~p", [Msg]), - {noreply, State}. - -handle_info({'DOWN', _MRef, process, Pid, _Reason}, State = #{chan_pmon := PMon}) -> - ChanPids = [Pid | emqx_misc:drain_down(?BATCH_SIZE)], - {Items, PMon1} = emqx_pmon:erase_all(ChanPids, PMon), - ok = emqx_pool:async_submit(fun lists:foreach/2, [fun clean_down/1, Items]), - {noreply, State#{chan_pmon := PMon1}}; - -handle_info(Info, State) -> - ?LOG(error, "Unexpected info: ~p", [Info]), - {noreply, State}. - -terminate(_Reason, _State) -> - emqx_stats:cancel_update(chan_stats). - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%-------------------------------------------------------------------- -%% Internal functions -%%-------------------------------------------------------------------- - -clean_down({_ChanPid, IMEI}) -> - unregister_channel(IMEI). diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl new file mode 100644 index 000000000..925ca1d94 --- /dev/null +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl @@ -0,0 +1,410 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2016-2017 EMQ Enterprise, Inc. (http://emqtt.io) +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_lwm2m_cmd). + +-include_lib("emqx/include/logger.hrl"). +-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). +-include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl"). + +-export([ mqtt_to_coap/2 + , coap_to_mqtt/4 + , empty_ack_to_mqtt/1 + , coap_failure_to_mqtt/2 + ]). + +-export([path_list/1, extract_path/1]). + +-define(STANDARD, 1). + +%-type msg_type() :: <<"create">> +% | <<"delete">> +% | <<"read">> +% | <<"write">> +% | <<"execute">> +% | <<"discover">> +% | <<"write-attr">> +% | <<"observe">> +% | <<"cancel-observe">>. +% + %-type cmd() :: #{ <<"msgType">> := msg_type() + % , <<"data">> := maps() + % %% more keys? + % }. + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"create">>, <<"data">> := Data}) -> + {PathList, QueryList} = path_list(maps:get(<<"basePath">>, Data, <<"/">>)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + TlvData = emqx_lwm2m_message:json_to_tlv(PathList, maps:get(<<"content">>, Data)), + Payload = emqx_lwm2m_tlv:encode(TlvData), + CoapRequest = emqx_coap_message:request(con, post, Payload, + [{uri_path, FullPathList}, + {uri_query, QueryList}, + {content_format, <<"application/vnd.oma.lwm2m+tlv">>}]), + {CoapRequest, InputCmd}; + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"delete">>, <<"data">> := Data}) -> + {PathList, QueryList} = path_list(maps:get(<<"path">>, Data)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + {emqx_coap_message:request(con, delete, <<>>, + [{uri_path, FullPathList}, + {uri_query, QueryList}]), InputCmd}; + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"read">>, <<"data">> := Data}) -> + {PathList, QueryList} = path_list(maps:get(<<"path">>, Data)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + {emqx_coap_message:request(con, get, <<>>, + [{uri_path, FullPathList}, + {uri_query, QueryList}]), InputCmd}; + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"write">>, <<"data">> := Data}) -> + CoapRequest = + case maps:get(<<"basePath">>, Data, <<"/">>) of + <<"/">> -> + single_write_request(AlternatePath, Data); + BasePath -> + batch_write_request(AlternatePath, BasePath, maps:get(<<"content">>, Data)) + end, + {CoapRequest, InputCmd}; + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"execute">>, <<"data">> := Data}) -> + {PathList, QueryList} = path_list(maps:get(<<"path">>, Data)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + Args = + case maps:get(<<"args">>, Data, <<>>) of + <<"undefined">> -> <<>>; + undefined -> <<>>; + Arg1 -> Arg1 + end, + {emqx_coap_message:request(con, post, Args, + [{uri_path, FullPathList}, + {uri_query, QueryList}, + {content_format, <<"text/plain">>}]), InputCmd}; + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"discover">>, <<"data">> := Data}) -> + {PathList, QueryList} = path_list(maps:get(<<"path">>, Data)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + {emqx_coap_message:request(con, get, <<>>, + [{uri_path, FullPathList}, + {uri_query, QueryList}, + {'accept', ?LWM2M_FORMAT_LINK}]), InputCmd}; + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"write-attr">>, <<"data">> := Data}) -> + {PathList, QueryList} = path_list(maps:get(<<"path">>, Data)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + Query = attr_query_list(Data), + {emqx_coap_message:request(con, put, <<>>, + [{uri_path, FullPathList}, + {uri_query, QueryList}, + {uri_query, Query}]), InputCmd}; + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"observe">>, <<"data">> := Data}) -> + {PathList, QueryList} = path_list(maps:get(<<"path">>, Data)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + {emqx_coap_message:request(con, get, <<>>, + [{uri_path, FullPathList}, + {uri_query, QueryList}, + {observe, 0}]), InputCmd}; + +mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"cancel-observe">>, <<"data">> := Data}) -> + {PathList, QueryList} = path_list(maps:get(<<"path">>, Data)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + {emqx_coap_message:request(con, get, <<>>, + [{uri_path, FullPathList}, + {uri_query, QueryList}, + {observe, 1}]), InputCmd}. + +coap_to_mqtt(_Method = {_, Code}, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"create">>}) -> + make_response(Code, Ref); + +coap_to_mqtt(_Method = {_, Code}, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"delete">>}) -> + make_response(Code, Ref); + +coap_to_mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"read">>}) -> + read_resp_to_mqtt(Method, CoapPayload, data_format(Options), Ref); + +coap_to_mqtt(Method, CoapPayload, _Options, Ref=#{<<"msgType">> := <<"write">>}) -> + write_resp_to_mqtt(Method, CoapPayload, Ref); + +coap_to_mqtt(Method, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"execute">>}) -> + execute_resp_to_mqtt(Method, Ref); + +coap_to_mqtt(Method, CoapPayload, _Options, Ref=#{<<"msgType">> := <<"discover">>}) -> + discover_resp_to_mqtt(Method, CoapPayload, Ref); + +coap_to_mqtt(Method, CoapPayload, _Options, Ref=#{<<"msgType">> := <<"write-attr">>}) -> + writeattr_resp_to_mqtt(Method, CoapPayload, Ref); + +coap_to_mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"observe">>}) -> + observe_resp_to_mqtt(Method, CoapPayload, data_format(Options), observe_seq(Options), Ref); + +coap_to_mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"cancel-observe">>}) -> + cancel_observe_resp_to_mqtt(Method, CoapPayload, data_format(Options), Ref). + +read_resp_to_mqtt({error, ErrorCode}, _CoapPayload, _Format, Ref) -> + make_response(ErrorCode, Ref); + +read_resp_to_mqtt({ok, SuccessCode}, CoapPayload, Format, Ref) -> + try + Result = content_to_mqtt(CoapPayload, Format, Ref), + make_response(SuccessCode, Ref, Format, Result) + catch + error:not_implemented -> make_response(not_implemented, Ref); + _:Ex:_ST -> + ?LOG(error, "~0p, bad payload format: ~0p", [Ex, CoapPayload]), + make_response(bad_request, Ref) + end. + +empty_ack_to_mqtt(Ref) -> + make_base_response(maps:put(<<"msgType">>, <<"ack">>, Ref)). + +coap_failure_to_mqtt(Ref, MsgType) -> + make_base_response(maps:put(<<"msgType">>, MsgType, Ref)). + +content_to_mqtt(CoapPayload, <<"text/plain">>, Ref) -> + emqx_lwm2m_message:text_to_json(extract_path(Ref), CoapPayload); + +content_to_mqtt(CoapPayload, <<"application/octet-stream">>, Ref) -> + emqx_lwm2m_message:opaque_to_json(extract_path(Ref), CoapPayload); + +content_to_mqtt(CoapPayload, <<"application/vnd.oma.lwm2m+tlv">>, Ref) -> + emqx_lwm2m_message:tlv_to_json(extract_path(Ref), CoapPayload); + +content_to_mqtt(CoapPayload, <<"application/vnd.oma.lwm2m+json">>, _Ref) -> + emqx_lwm2m_message:translate_json(CoapPayload). + +write_resp_to_mqtt({ok, changed}, _CoapPayload, Ref) -> + make_response(changed, Ref); + +write_resp_to_mqtt({ok, content}, CoapPayload, Ref) when CoapPayload =:= <<>> -> + make_response(method_not_allowed, Ref); + +write_resp_to_mqtt({ok, content}, _CoapPayload, Ref) -> + make_response(changed, Ref); + +write_resp_to_mqtt({error, Error}, _CoapPayload, Ref) -> + make_response(Error, Ref). + +execute_resp_to_mqtt({ok, changed}, Ref) -> + make_response(changed, Ref); + +execute_resp_to_mqtt({error, Error}, Ref) -> + make_response(Error, Ref). + +discover_resp_to_mqtt({ok, content}, CoapPayload, Ref) -> + Links = binary:split(CoapPayload, <<",">>, [global]), + make_response(content, Ref, <<"application/link-format">>, Links); + +discover_resp_to_mqtt({error, Error}, _CoapPayload, Ref) -> + make_response(Error, Ref). + +writeattr_resp_to_mqtt({ok, changed}, _CoapPayload, Ref) -> + make_response(changed, Ref); + +writeattr_resp_to_mqtt({error, Error}, _CoapPayload, Ref) -> + make_response(Error, Ref). + +observe_resp_to_mqtt({error, Error}, _CoapPayload, _Format, _ObserveSeqNum, Ref) -> + make_response(Error, Ref); + +observe_resp_to_mqtt({ok, content}, CoapPayload, Format, 0, Ref) -> + read_resp_to_mqtt({ok, content}, CoapPayload, Format, Ref); + +observe_resp_to_mqtt({ok, content}, CoapPayload, Format, ObserveSeqNum, Ref) -> + read_resp_to_mqtt({ok, content}, CoapPayload, Format, Ref#{<<"seqNum">> => ObserveSeqNum}). + +cancel_observe_resp_to_mqtt({ok, content}, CoapPayload, Format, Ref) -> + read_resp_to_mqtt({ok, content}, CoapPayload, Format, Ref); + +cancel_observe_resp_to_mqtt({error, Error}, _CoapPayload, _Format, Ref) -> + make_response(Error, Ref). + +make_response(Code, Ref=#{}) -> + BaseRsp = make_base_response(Ref), + make_data_response(BaseRsp, Code). + +make_response(Code, Ref=#{}, _Format, Result) -> + BaseRsp = make_base_response(Ref), + make_data_response(BaseRsp, Code, _Format, Result). + +%% The base response format is what included in the request: +%% +%% #{ +%% <<"seqNum">> => SeqNum, +%% <<"imsi">> => maps:get(<<"imsi">>, Ref, null), +%% <<"imei">> => maps:get(<<"imei">>, Ref, null), +%% <<"requestID">> => maps:get(<<"requestID">>, Ref, null), +%% <<"cacheID">> => maps:get(<<"cacheID">>, Ref, null), +%% <<"msgType">> => maps:get(<<"msgType">>, Ref, null) +%% } + +make_base_response(Ref=#{}) -> + remove_tmp_fields(Ref). + +make_data_response(BaseRsp, Code) -> + BaseRsp#{ + <<"data">> => #{ + <<"reqPath">> => extract_path(BaseRsp), + <<"code">> => code(Code), + <<"codeMsg">> => Code + } + }. + +make_data_response(BaseRsp, Code, _Format, Result) -> + BaseRsp#{ + <<"data">> => + #{ + <<"reqPath">> => extract_path(BaseRsp), + <<"code">> => code(Code), + <<"codeMsg">> => Code, + <<"content">> => Result + } + }. + +remove_tmp_fields(Ref) -> + maps:remove(observe_type, Ref). + +-spec path_list(Path::binary()) -> {[PathWord::binary()], [Query::binary()]}. +path_list(Path) -> + case binary:split(binary_util:trim(Path, $/), [<<$/>>], [global]) of + [ObjId, ObjInsId, ResId, LastPart] -> + {ResInstId, QueryList} = query_list(LastPart), + {[ObjId, ObjInsId, ResId, ResInstId], QueryList}; + [ObjId, ObjInsId, LastPart] -> + {ResId, QueryList} = query_list(LastPart), + {[ObjId, ObjInsId, ResId], QueryList}; + [ObjId, LastPart] -> + {ObjInsId, QueryList} = query_list(LastPart), + {[ObjId, ObjInsId], QueryList}; + [LastPart] -> + {ObjId, QueryList} = query_list(LastPart), + {[ObjId], QueryList} + end. + +query_list(PathWithQuery) -> + case binary:split(PathWithQuery, [<<$?>>], []) of + [Path] -> {Path, []}; + [Path, Querys] -> + {Path, binary:split(Querys, [<<$&>>], [global])} + end. + +attr_query_list(Data) -> + attr_query_list(Data, valid_attr_keys(), []). + +attr_query_list(QueryJson = #{}, ValidAttrKeys, QueryList) -> + maps:fold( + fun + (_K, null, Acc) -> Acc; + (K, V, Acc) -> + case lists:member(K, ValidAttrKeys) of + true -> + KV = <>, <<"pmax">>, <<"gt">>, <<"lt">>, <<"st">>]. + +data_format(Options) -> + maps:get(content_format, Options, <<"text/plain">>). + +observe_seq(Options) -> + maps:get(observe, Options, rand:uniform(1000000) + 1 ). + +add_alternate_path_prefix(<<"/">>, PathList) -> + PathList; + +add_alternate_path_prefix(AlternatePath, PathList) -> + [binary_util:trim(AlternatePath, $/) | PathList]. + +extract_path(Ref = #{}) -> + drop_query( + case Ref of + #{<<"data">> := Data} -> + case maps:get(<<"path">>, Data, nil) of + nil -> maps:get(<<"basePath">>, Data, undefined); + Path -> Path + end; + #{<<"path">> := Path} -> + Path + end). + + +batch_write_request(AlternatePath, BasePath, Content) -> + {PathList, QueryList} = path_list(BasePath), + Method = case length(PathList) of + 2 -> post; + 3 -> put + end, + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + TlvData = emqx_lwm2m_message:json_to_tlv(PathList, Content), + Payload = emqx_lwm2m_tlv:encode(TlvData), + emqx_coap_message:request(con, Method, Payload, + [{uri_path, FullPathList}, + {uri_query, QueryList}, + {content_format, <<"application/vnd.oma.lwm2m+tlv">>}]). + +single_write_request(AlternatePath, Data) -> + {PathList, QueryList} = path_list(maps:get(<<"path">>, Data)), + FullPathList = add_alternate_path_prefix(AlternatePath, PathList), + %% TO DO: handle write to resource instance, e.g. /4/0/1/0 + TlvData = emqx_lwm2m_message:json_to_tlv(PathList, [Data]), + Payload = emqx_lwm2m_tlv:encode(TlvData), + emqx_coap_message:request(con, put, Payload, + [{uri_path, FullPathList}, + {uri_query, QueryList}, + {content_format, <<"application/vnd.oma.lwm2m+tlv">>}]). + +drop_query(Path) -> + case binary:split(Path, [<<$?>>]) of + [Path] -> Path; + [PathOnly, _Query] -> PathOnly + end. + +code(get) -> <<"0.01">>; +code(post) -> <<"0.02">>; +code(put) -> <<"0.03">>; +code(delete) -> <<"0.04">>; +code(created) -> <<"2.01">>; +code(deleted) -> <<"2.02">>; +code(valid) -> <<"2.03">>; +code(changed) -> <<"2.04">>; +code(content) -> <<"2.05">>; +code(continue) -> <<"2.31">>; +code(bad_request) -> <<"4.00">>; +code(unauthorized) -> <<"4.01">>; +code(bad_option) -> <<"4.02">>; +code(forbidden) -> <<"4.03">>; +code(not_found) -> <<"4.04">>; +code(method_not_allowed) -> <<"4.05">>; +code(not_acceptable) -> <<"4.06">>; +code(request_entity_incomplete) -> <<"4.08">>; +code(precondition_failed) -> <<"4.12">>; +code(request_entity_too_large) -> <<"4.13">>; +code(unsupported_content_format) -> <<"4.15">>; +code(internal_server_error) -> <<"5.00">>; +code(not_implemented) -> <<"5.01">>; +code(bad_gateway) -> <<"5.02">>; +code(service_unavailable) -> <<"5.03">>; +code(gateway_timeout) -> <<"5.04">>; +code(proxying_not_supported) -> <<"5.05">>. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd_handler.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd_handler.erl deleted file mode 100644 index 318328e3c..000000000 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd_handler.erl +++ /dev/null @@ -1,310 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_lwm2m_cmd_handler). - --include("src/lwm2m/include/emqx_lwm2m.hrl"). - --include_lib("lwm2m_coap/include/coap.hrl"). - --export([ mqtt2coap/2 - , coap2mqtt/4 - , ack2mqtt/1 - , extract_path/1 - ]). - --export([path_list/1]). - --define(LOG(Level, Format, Args), logger:Level("LWM2M-CMD: " ++ Format, Args)). - -mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"create">>, <<"data">> := Data}) -> - PathList = path_list(maps:get(<<"basePath">>, Data, <<"/">>)), - FullPathList = add_alternate_path_prefix(AlternatePath, PathList), - TlvData = emqx_lwm2m_message:json_to_tlv(PathList, maps:get(<<"content">>, Data)), - Payload = emqx_lwm2m_tlv:encode(TlvData), - CoapRequest = lwm2m_coap_message:request(con, post, Payload, [{uri_path, FullPathList}, - {content_format, <<"application/vnd.oma.lwm2m+tlv">>}]), - {CoapRequest, InputCmd}; -mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"delete">>, <<"data">> := Data}) -> - FullPathList = add_alternate_path_prefix(AlternatePath, path_list(maps:get(<<"path">>, Data))), - {lwm2m_coap_message:request(con, delete, <<>>, [{uri_path, FullPathList}]), InputCmd}; -mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"read">>, <<"data">> := Data}) -> - FullPathList = add_alternate_path_prefix(AlternatePath, path_list(maps:get(<<"path">>, Data))), - {lwm2m_coap_message:request(con, get, <<>>, [{uri_path, FullPathList}]), InputCmd}; -mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"write">>, <<"data">> := Data}) -> - Encoding = maps:get(<<"encoding">>, InputCmd, <<"plain">>), - CoapRequest = - case maps:get(<<"basePath">>, Data, <<"/">>) of - <<"/">> -> - single_write_request(AlternatePath, Data, Encoding); - BasePath -> - batch_write_request(AlternatePath, BasePath, maps:get(<<"content">>, Data), Encoding) - end, - {CoapRequest, InputCmd}; - -mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"execute">>, <<"data">> := Data}) -> - FullPathList = add_alternate_path_prefix(AlternatePath, path_list(maps:get(<<"path">>, Data))), - Args = - case maps:get(<<"args">>, Data, <<>>) of - <<"undefined">> -> <<>>; - undefined -> <<>>; - Arg1 -> Arg1 - end, - {lwm2m_coap_message:request(con, post, Args, [{uri_path, FullPathList}, {content_format, <<"text/plain">>}]), InputCmd}; -mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"discover">>, <<"data">> := Data}) -> - FullPathList = add_alternate_path_prefix(AlternatePath, path_list(maps:get(<<"path">>, Data))), - {lwm2m_coap_message:request(con, get, <<>>, [{uri_path, FullPathList}, {'accept', ?LWM2M_FORMAT_LINK}]), InputCmd}; -mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"write-attr">>, <<"data">> := Data}) -> - FullPathList = add_alternate_path_prefix(AlternatePath, path_list(maps:get(<<"path">>, Data))), - Query = attr_query_list(Data), - {lwm2m_coap_message:request(con, put, <<>>, [{uri_path, FullPathList}, {uri_query, Query}]), InputCmd}; -mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"observe">>, <<"data">> := Data}) -> - PathList = path_list(maps:get(<<"path">>, Data)), - FullPathList = add_alternate_path_prefix(AlternatePath, PathList), - {lwm2m_coap_message:request(con, get, <<>>, [{uri_path, FullPathList}, {observe, 0}]), InputCmd}; -mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"cancel-observe">>, <<"data">> := Data}) -> - PathList = path_list(maps:get(<<"path">>, Data)), - FullPathList = add_alternate_path_prefix(AlternatePath, PathList), - {lwm2m_coap_message:request(con, get, <<>>, [{uri_path, FullPathList}, {observe, 1}]), InputCmd}. - -coap2mqtt(_Method = {_, Code}, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"create">>}) -> - make_response(Code, Ref); -coap2mqtt(_Method = {_, Code}, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"delete">>}) -> - make_response(Code, Ref); -coap2mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"read">>}) -> - coap_read_to_mqtt(Method, CoapPayload, data_format(Options), Ref); -coap2mqtt(Method, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"write">>}) -> - coap_write_to_mqtt(Method, Ref); -coap2mqtt(Method, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"execute">>}) -> - coap_execute_to_mqtt(Method, Ref); -coap2mqtt(Method, CoapPayload, _Options, Ref=#{<<"msgType">> := <<"discover">>}) -> - coap_discover_to_mqtt(Method, CoapPayload, Ref); -coap2mqtt(Method, CoapPayload, _Options, Ref=#{<<"msgType">> := <<"write-attr">>}) -> - coap_writeattr_to_mqtt(Method, CoapPayload, Ref); -coap2mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"observe">>}) -> - coap_observe_to_mqtt(Method, CoapPayload, data_format(Options), observe_seq(Options), Ref); -coap2mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"cancel-observe">>}) -> - coap_cancel_observe_to_mqtt(Method, CoapPayload, data_format(Options), Ref). - -coap_read_to_mqtt({error, ErrorCode}, _CoapPayload, _Format, Ref) -> - make_response(ErrorCode, Ref); -coap_read_to_mqtt({ok, SuccessCode}, CoapPayload, Format, Ref) -> - try - Result = coap_content_to_mqtt_payload(CoapPayload, Format, Ref), - make_response(SuccessCode, Ref, Format, Result) - catch - error:not_implemented -> make_response(not_implemented, Ref); - C:R:Stack -> - ?LOG(error, "~p, bad payload format: ~p, stacktrace: ~p", [{C, R}, CoapPayload, Stack]), - make_response(bad_request, Ref) - end. - -ack2mqtt(Ref) -> - make_base_response(Ref). - -coap_content_to_mqtt_payload(CoapPayload, <<"text/plain">>, Ref) -> - emqx_lwm2m_message:text_to_json(extract_path(Ref), CoapPayload); -coap_content_to_mqtt_payload(CoapPayload, <<"application/octet-stream">>, Ref) -> - emqx_lwm2m_message:opaque_to_json(extract_path(Ref), CoapPayload); -coap_content_to_mqtt_payload(CoapPayload, <<"application/vnd.oma.lwm2m+tlv">>, Ref) -> - emqx_lwm2m_message:tlv_to_json(extract_path(Ref), CoapPayload); -coap_content_to_mqtt_payload(CoapPayload, <<"application/vnd.oma.lwm2m+json">>, _Ref) -> - emqx_lwm2m_message:translate_json(CoapPayload). - -coap_write_to_mqtt({ok, changed}, Ref) -> - make_response(changed, Ref); -coap_write_to_mqtt({error, Error}, Ref) -> - make_response(Error, Ref). - -coap_execute_to_mqtt({ok, changed}, Ref) -> - make_response(changed, Ref); -coap_execute_to_mqtt({error, Error}, Ref) -> - make_response(Error, Ref). - -coap_discover_to_mqtt({ok, content}, CoapPayload, Ref) -> - Links = binary:split(CoapPayload, <<",">>), - make_response(content, Ref, <<"application/link-format">>, Links); -coap_discover_to_mqtt({error, Error}, _CoapPayload, Ref) -> - make_response(Error, Ref). - -coap_writeattr_to_mqtt({ok, changed}, _CoapPayload, Ref) -> - make_response(changed, Ref); -coap_writeattr_to_mqtt({error, Error}, _CoapPayload, Ref) -> - make_response(Error, Ref). - -coap_observe_to_mqtt({error, Error}, _CoapPayload, _Format, _ObserveSeqNum, Ref) -> - make_response(Error, Ref); -coap_observe_to_mqtt({ok, content}, CoapPayload, Format, 0, Ref) -> - coap_read_to_mqtt({ok, content}, CoapPayload, Format, Ref); -coap_observe_to_mqtt({ok, content}, CoapPayload, Format, ObserveSeqNum, Ref) -> - RefWithObserve = maps:put(<<"seqNum">>, ObserveSeqNum, Ref), - RefNotify = maps:put(<<"msgType">>, <<"notify">>, RefWithObserve), - coap_read_to_mqtt({ok, content}, CoapPayload, Format, RefNotify). - -coap_cancel_observe_to_mqtt({ok, content}, CoapPayload, Format, Ref) -> - coap_read_to_mqtt({ok, content}, CoapPayload, Format, Ref); -coap_cancel_observe_to_mqtt({error, Error}, _CoapPayload, _Format, Ref) -> - make_response(Error, Ref). - -make_response(Code, Ref=#{}) -> - BaseRsp = make_base_response(Ref), - make_data_response(BaseRsp, Code). -make_response(Code, Ref=#{}, _Format, Result) -> - BaseRsp = make_base_response(Ref), - make_data_response(BaseRsp, Code, _Format, Result). - -%% The base response format is what included in the request: -%% -%% #{ -%% <<"seqNum">> => SeqNum, -%% <<"requestID">> => maps:get(<<"requestID">>, Ref, null), -%% <<"cacheID">> => maps:get(<<"cacheID">>, Ref, null), -%% <<"msgType">> => maps:get(<<"msgType">>, Ref, null) -%% } - -make_base_response(Ref=#{}) -> - remove_tmp_fields(Ref). - -make_data_response(BaseRsp, Code) -> - BaseRsp#{ - <<"data">> => #{ - <<"reqPath">> => extract_path(BaseRsp), - <<"code">> => code(Code), - <<"codeMsg">> => Code - } - }. -make_data_response(BaseRsp, Code, _Format, Result) -> - BaseRsp#{ - <<"data">> => #{ - <<"reqPath">> => extract_path(BaseRsp), - <<"code">> => code(Code), - <<"codeMsg">> => Code, - <<"content">> => Result - } - }. - -remove_tmp_fields(Ref) -> - maps:remove(observe_type, Ref). - -path_list(Path) -> - case binary:split(binary_util:trim(Path, $/), [<<$/>>], [global]) of - [ObjId, ObjInsId, ResId, ResInstId] -> [ObjId, ObjInsId, ResId, ResInstId]; - [ObjId, ObjInsId, ResId] -> [ObjId, ObjInsId, ResId]; - [ObjId, ObjInsId] -> [ObjId, ObjInsId]; - [ObjId] -> [ObjId] - end. - -attr_query_list(Data) -> - attr_query_list(Data, valid_attr_keys(), []). -attr_query_list(QueryJson = #{}, ValidAttrKeys, QueryList) -> - maps:fold( - fun - (_K, null, Acc) -> Acc; - (K, V, Acc) -> - case lists:member(K, ValidAttrKeys) of - true -> - Val = bin(V), - KV = <>, <<"pmax">>, <<"gt">>, <<"lt">>, <<"st">>]. - -data_format(Options) -> - proplists:get_value(content_format, Options, <<"text/plain">>). -observe_seq(Options) -> - proplists:get_value(observe, Options, rand:uniform(1000000) + 1 ). - -add_alternate_path_prefix(<<"/">>, PathList) -> - PathList; -add_alternate_path_prefix(AlternatePath, PathList) -> - [binary_util:trim(AlternatePath, $/) | PathList]. - -extract_path(Ref = #{}) -> - case Ref of - #{<<"data">> := Data} -> - case maps:get(<<"path">>, Data, nil) of - nil -> maps:get(<<"basePath">>, Data, undefined); - Path -> Path - end; - #{<<"path">> := Path} -> - Path - end. - -batch_write_request(AlternatePath, BasePath, Content, Encoding) -> - PathList = path_list(BasePath), - Method = case length(PathList) of - 2 -> post; - 3 -> put - end, - FullPathList = add_alternate_path_prefix(AlternatePath, PathList), - Content1 = decoding(Content, Encoding), - TlvData = emqx_lwm2m_message:json_to_tlv(PathList, Content1), - Payload = emqx_lwm2m_tlv:encode(TlvData), - lwm2m_coap_message:request(con, Method, Payload, [{uri_path, FullPathList}, {content_format, <<"application/vnd.oma.lwm2m+tlv">>}]). - -single_write_request(AlternatePath, Data, Encoding) -> - PathList = path_list(maps:get(<<"path">>, Data)), - FullPathList = add_alternate_path_prefix(AlternatePath, PathList), - Datas = decoding([Data], Encoding), - TlvData = emqx_lwm2m_message:json_to_tlv(PathList, Datas), - Payload = emqx_lwm2m_tlv:encode(TlvData), - lwm2m_coap_message:request(con, put, Payload, [{uri_path, FullPathList}, {content_format, <<"application/vnd.oma.lwm2m+tlv">>}]). - - -code(get) -> <<"0.01">>; -code(post) -> <<"0.02">>; -code(put) -> <<"0.03">>; -code(delete) -> <<"0.04">>; -code(created) -> <<"2.01">>; -code(deleted) -> <<"2.02">>; -code(valid) -> <<"2.03">>; -code(changed) -> <<"2.04">>; -code(content) -> <<"2.05">>; -code(continue) -> <<"2.31">>; -code(bad_request) -> <<"4.00">>; -code(uauthorized) -> <<"4.01">>; -code(bad_option) -> <<"4.02">>; -code(forbidden) -> <<"4.03">>; -code(not_found) -> <<"4.04">>; -code(method_not_allowed) -> <<"4.05">>; -code(not_acceptable) -> <<"4.06">>; -code(request_entity_incomplete) -> <<"4.08">>; -code(precondition_failed) -> <<"4.12">>; -code(request_entity_too_large) -> <<"4.13">>; -code(unsupported_content_format) -> <<"4.15">>; -code(internal_server_error) -> <<"5.00">>; -code(not_implemented) -> <<"5.01">>; -code(bad_gateway) -> <<"5.02">>; -code(service_unavailable) -> <<"5.03">>; -code(gateway_timeout) -> <<"5.04">>; -code(proxying_not_supported) -> <<"5.05">>. - -bin(Bin) when is_binary(Bin) -> Bin; -bin(Str) when is_list(Str) -> list_to_binary(Str); -bin(Int) when is_integer(Int) -> integer_to_binary(Int); -bin(Float) when is_float(Float) -> float_to_binary(Float). - -decoding(Datas, <<"hex">>) -> - lists:map(fun(Data = #{<<"value">> := Value}) -> - Data#{<<"value">> => emqx_misc:hexstr2bin(Value)} - end, Datas); -decoding(Datas, _) -> - Datas. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_coap_resource.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_coap_resource.erl deleted file mode 100644 index 588dd523e..000000000 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_coap_resource.erl +++ /dev/null @@ -1,386 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_lwm2m_coap_resource). - --include_lib("emqx/include/emqx.hrl"). - --include_lib("emqx/include/emqx_mqtt.hrl"). - --include_lib("lwm2m_coap/include/coap.hrl"). - -% -behaviour(lwm2m_coap_resource). - --export([ coap_discover/2 - , coap_get/5 - , coap_post/5 - , coap_put/5 - , coap_delete/4 - , coap_observe/5 - , coap_unobserve/1 - , coap_response/7 - , coap_ack/3 - , handle_info/2 - , handle_call/3 - , handle_cast/2 - , terminate/2 - ]). - --export([parse_object_list/1]). - --include("src/lwm2m/include/emqx_lwm2m.hrl"). - --define(PREFIX, <<"rd">>). - --define(LOG(Level, Format, Args), logger:Level("LWM2M-RESOURCE: " ++ Format, Args)). - --dialyzer([{nowarn_function, [coap_discover/2]}]). -% we use {'absolute', list(binary()), [{atom(), binary()}]} as coap_uri() -% https://github.com/emqx/lwm2m-coap/blob/258e9bd3762124395e83c1e68a1583b84718230f/src/lwm2m_coap_resource.erl#L61 -% resource operations -coap_discover(_Prefix, _Args) -> - [{absolute, [<<"mqtt">>], []}]. - -coap_get(ChId, [?PREFIX], Query, Content, Lwm2mState) -> - ?LOG(debug, "~p ~p GET Query=~p, Content=~p", [self(),ChId, Query, Content]), - {ok, #coap_content{}, Lwm2mState}; -coap_get(ChId, Prefix, Query, Content, Lwm2mState) -> - ?LOG(error, "ignore bad put request ChId=~p, Prefix=~p, Query=~p, Content=~p", [ChId, Prefix, Query, Content]), - {error, bad_request, Lwm2mState}. - -% LWM2M REGISTER COMMAND -coap_post(ChId, [?PREFIX], Query, Content = #coap_content{uri_path = [?PREFIX]}, Lwm2mState) -> - ?LOG(debug, "~p ~p REGISTER command Query=~p, Content=~p", [self(), ChId, Query, Content]), - case parse_options(Query) of - {error, {bad_opt, _CustomOption}} -> - ?LOG(error, "Reject REGISTER from ~p due to wrong option", [ChId]), - {error, bad_request, Lwm2mState}; - {ok, LwM2MQuery} -> - process_register(ChId, LwM2MQuery, Content#coap_content.payload, Lwm2mState) - end; - -% LWM2M UPDATE COMMAND -coap_post(ChId, [?PREFIX], Query, Content = #coap_content{uri_path = LocationPath}, Lwm2mState) -> - ?LOG(debug, "~p ~p UPDATE command location=~p, Query=~p, Content=~p", [self(), ChId, LocationPath, Query, Content]), - case parse_options(Query) of - {error, {bad_opt, _CustomOption}} -> - ?LOG(error, "Reject UPDATE from ~p due to wrong option, Query=~p", [ChId, Query]), - {error, bad_request, Lwm2mState}; - {ok, LwM2MQuery} -> - process_update(ChId, LwM2MQuery, LocationPath, Content#coap_content.payload, Lwm2mState) - end; - -coap_post(ChId, Prefix, Query, Content, Lwm2mState) -> - ?LOG(error, "bad post request ChId=~p, Prefix=~p, Query=~p, Content=~p", [ChId, Prefix, Query, Content]), - {error, bad_request, Lwm2mState}. - -coap_put(_ChId, Prefix, Query, Content, Lwm2mState) -> - ?LOG(error, "put has error, Prefix=~p, Query=~p, Content=~p", [Prefix, Query, Content]), - {error, bad_request, Lwm2mState}. - -% LWM2M DE-REGISTER COMMAND -coap_delete(ChId, [?PREFIX], #coap_content{uri_path = Location}, Lwm2mState) -> - LocationPath = binary_util:join_path(Location), - ?LOG(debug, "~p ~p DELETE command location=~p", [self(), ChId, LocationPath]), - case get(lwm2m_context) of - #lwm2m_context{location = LocationPath} -> - lwm2m_coap_responder:stop(deregister), - {ok, Lwm2mState}; - undefined -> - ?LOG(error, "Reject DELETE from ~p, Location: ~p not found", [ChId, Location]), - {error, forbidden, Lwm2mState}; - TrueLocation -> - ?LOG(error, "Reject DELETE from ~p, Wrong Location: ~p, registered location record: ~p", [ChId, Location, TrueLocation]), - {error, not_found, Lwm2mState} - end; -coap_delete(_ChId, _Prefix, _Content, Lwm2mState) -> - {error, forbidden, Lwm2mState}. - -coap_observe(ChId, Prefix, Name, Ack, Lwm2mState) -> - ?LOG(error, "unsupported observe request ChId=~p, Prefix=~p, Name=~p, Ack=~p", [ChId, Prefix, Name, Ack]), - {error, method_not_allowed, Lwm2mState}. - -coap_unobserve(Lwm2mState) -> - ?LOG(error, "unsupported unobserve request: ~p", [Lwm2mState]), - {ok, Lwm2mState}. - -coap_response(ChId, Ref, CoapMsgType, CoapMsgMethod, CoapMsgPayload, CoapMsgOpts, Lwm2mState) -> - ?LOG(info, "~p, RCV CoAP response, CoapMsgType: ~p, CoapMsgMethod: ~p, CoapMsgPayload: ~p, - CoapMsgOpts: ~p, Ref: ~p", - [ChId, CoapMsgType, CoapMsgMethod, CoapMsgPayload, CoapMsgOpts, Ref]), - MqttPayload = emqx_lwm2m_cmd_handler:coap2mqtt(CoapMsgMethod, CoapMsgPayload, CoapMsgOpts, Ref), - Lwm2mState2 = emqx_lwm2m_protocol:send_ul_data(maps:get(<<"msgType">>, MqttPayload), MqttPayload, Lwm2mState), - {noreply, Lwm2mState2}. - -coap_ack(_ChId, Ref, Lwm2mState) -> - ?LOG(info, "~p, RCV CoAP Empty ACK, Ref: ~p", [_ChId, Ref]), - AckRef = maps:put(<<"msgType">>, <<"ack">>, Ref), - MqttPayload = emqx_lwm2m_cmd_handler:ack2mqtt(AckRef), - Lwm2mState2 = emqx_lwm2m_protocol:send_ul_data(maps:get(<<"msgType">>, MqttPayload), MqttPayload, Lwm2mState), - {ok, Lwm2mState2}. - -%% Batch deliver -handle_info({deliver, Topic, Msgs}, Lwm2mState) when is_list(Msgs) -> - {noreply, lists:foldl(fun(Msg, NewState) -> - element(2, handle_info({deliver, Topic, Msg}, NewState)) - end, Lwm2mState, Msgs)}; -%% Handle MQTT Message -handle_info({deliver, _Topic, MqttMsg}, Lwm2mState) -> - Lwm2mState2 = emqx_lwm2m_protocol:deliver(MqttMsg, Lwm2mState), - {noreply, Lwm2mState2}; - -%% Deliver Coap Message to Device -handle_info({deliver_to_coap, CoapRequest, Ref}, Lwm2mState) -> - {send_request, CoapRequest, Ref, Lwm2mState}; - -handle_info({'EXIT', _Pid, Reason}, Lwm2mState) -> - ?LOG(info, "~p, received exit from: ~p, reason: ~p, quit now!", [self(), _Pid, Reason]), - {stop, Reason, Lwm2mState}; - -handle_info(post_init, Lwm2mState) -> - Lwm2mState2 = emqx_lwm2m_protocol:post_init(Lwm2mState), - {noreply, Lwm2mState2}; - -handle_info(auto_observe, Lwm2mState) -> - Lwm2mState2 = emqx_lwm2m_protocol:auto_observe(Lwm2mState), - {noreply, Lwm2mState2}; - -handle_info({life_timer, expired}, Lwm2mState) -> - ?LOG(debug, "lifetime expired, shutdown", []), - {stop, life_timer_expired, Lwm2mState}; - -handle_info({shutdown, Error}, Lwm2mState) -> - {stop, Error, Lwm2mState}; - -handle_info({shutdown, conflict, {ClientId, NewPid}}, Lwm2mState) -> - ?LOG(warning, "lwm2m '~s' conflict with ~p, shutdown", [ClientId, NewPid]), - {stop, conflict, Lwm2mState}; - -handle_info({suback, _MsgId, [_GrantedQos]}, Lwm2mState) -> - {noreply, Lwm2mState}; - -handle_info(emit_stats, Lwm2mState) -> - {noreply, Lwm2mState}; - -handle_info(Message, Lwm2mState) -> - ?LOG(error, "Unknown Message ~p", [Message]), - {noreply, Lwm2mState}. - - -handle_call(info, _From, Lwm2mState) -> - {Info, Lwm2mState2} = emqx_lwm2m_protocol:get_info(Lwm2mState), - {reply, Info, Lwm2mState2}; - -handle_call(stats, _From, Lwm2mState) -> - {Stats, Lwm2mState2} = emqx_lwm2m_protocol:get_stats(Lwm2mState), - {reply, Stats, Lwm2mState2}; - -handle_call(kick, _From, Lwm2mState) -> - {stop, kick, Lwm2mState}; - -handle_call({set_rate_limit, _Rl}, _From, Lwm2mState) -> - ?LOG(error, "set_rate_limit is not support", []), - {reply, ok, Lwm2mState}; - -handle_call(get_rate_limit, _From, Lwm2mState) -> - ?LOG(error, "get_rate_limit is not support", []), - {reply, ok, Lwm2mState}; - -handle_call(session, _From, Lwm2mState) -> - ?LOG(error, "get_session is not support", []), - {reply, ok, Lwm2mState}; - -handle_call(Request, _From, Lwm2mState) -> - ?LOG(error, "adapter unexpected call ~p", [Request]), - {reply, ok, Lwm2mState}. - -handle_cast(Msg, Lwm2mState) -> - ?LOG(error, "unexpected cast ~p", [Msg]), - {noreply, Lwm2mState, hibernate}. - -terminate(Reason, Lwm2mState) -> - emqx_lwm2m_protocol:terminate(Reason, Lwm2mState). - -%%%%%%%%%%%%%%%%%%%%%% -%% Internal Functions -%%%%%%%%%%%%%%%%%%%%%% -process_register(ChId, LwM2MQuery, LwM2MPayload, Lwm2mState) -> - Epn = maps:get(<<"ep">>, LwM2MQuery, undefined), - LifeTime = maps:get(<<"lt">>, LwM2MQuery, undefined), - Ver = maps:get(<<"lwm2m">>, LwM2MQuery, undefined), - case check_lwm2m_version(Ver) of - false -> - ?LOG(error, "Reject REGISTER from ~p due to unsupported version: ~p", [ChId, Ver]), - lwm2m_coap_responder:stop(invalid_version), - {error, precondition_failed, Lwm2mState}; - true -> - case check_epn(Epn) andalso check_lifetime(LifeTime) of - true -> - init_lwm2m_emq_client(ChId, LwM2MQuery, LwM2MPayload, Lwm2mState); - false -> - ?LOG(error, "Reject REGISTER from ~p due to wrong parameters, epn=~p, lifetime=~p", [ChId, Epn, LifeTime]), - lwm2m_coap_responder:stop(invalid_query_params), - {error, bad_request, Lwm2mState} - end - end. - -process_update(ChId, LwM2MQuery, Location, LwM2MPayload, Lwm2mState) -> - LocationPath = binary_util:join_path(Location), - case get(lwm2m_context) of - #lwm2m_context{location = LocationPath} -> - RegInfo = append_object_list(LwM2MQuery, LwM2MPayload), - Lwm2mState2 = emqx_lwm2m_protocol:update_reg_info(RegInfo, Lwm2mState), - ?LOG(info, "~p, UPDATE Success, assgined location: ~p", [ChId, LocationPath]), - {ok, changed, #coap_content{}, Lwm2mState2}; - undefined -> - ?LOG(error, "Reject UPDATE from ~p, Location: ~p not found", [ChId, Location]), - {error, forbidden, Lwm2mState}; - TrueLocation -> - ?LOG(error, "Reject UPDATE from ~p, Wrong Location: ~p, registered location record: ~p", [ChId, Location, TrueLocation]), - {error, not_found, Lwm2mState} - end. - -init_lwm2m_emq_client(ChId, LwM2MQuery = #{<<"ep">> := Epn}, LwM2MPayload, _Lwm2mState = undefined) -> - RegInfo = append_object_list(LwM2MQuery, LwM2MPayload), - case emqx_lwm2m_protocol:init(self(), Epn, ChId, RegInfo) of - {ok, Lwm2mState} -> - LocationPath = assign_location_path(Epn), - ?LOG(info, "~p, REGISTER Success, assgined location: ~p", [ChId, LocationPath]), - {ok, created, #coap_content{location_path = LocationPath}, Lwm2mState}; - {error, Error} -> - lwm2m_coap_responder:stop(Error), - ?LOG(error, "~p, REGISTER Failed, error: ~p", [ChId, Error]), - {error, forbidden, undefined} - end; -init_lwm2m_emq_client(ChId, LwM2MQuery = #{<<"ep">> := Epn}, LwM2MPayload, Lwm2mState) -> - RegInfo = append_object_list(LwM2MQuery, LwM2MPayload), - LocationPath = assign_location_path(Epn), - ?LOG(info, "~p, RE-REGISTER Success, location: ~p", [ChId, LocationPath]), - Lwm2mState2 = emqx_lwm2m_protocol:replace_reg_info(RegInfo, Lwm2mState), - {ok, created, #coap_content{location_path = LocationPath}, Lwm2mState2}. - -append_object_list(LwM2MQuery, <<>>) when map_size(LwM2MQuery) == 0 -> #{}; -append_object_list(LwM2MQuery, <<>>) -> LwM2MQuery; -append_object_list(LwM2MQuery, LwM2MPayload) when is_binary(LwM2MPayload) -> - {AlterPath, ObjList} = parse_object_list(LwM2MPayload), - LwM2MQuery#{ - <<"alternatePath">> => AlterPath, - <<"objectList">> => ObjList - }. - -parse_options(InputQuery) -> - parse_options(InputQuery, maps:new()). - -parse_options([], Query) -> {ok, Query}; -parse_options([<<"ep=", Epn/binary>>|T], Query) -> - parse_options(T, maps:put(<<"ep">>, Epn, Query)); -parse_options([<<"lt=", Lt/binary>>|T], Query) -> - parse_options(T, maps:put(<<"lt">>, binary_to_integer(Lt), Query)); -parse_options([<<"lwm2m=", Ver/binary>>|T], Query) -> - parse_options(T, maps:put(<<"lwm2m">>, Ver, Query)); -parse_options([<<"b=", Binding/binary>>|T], Query) -> - parse_options(T, maps:put(<<"b">>, Binding, Query)); -parse_options([CustomOption|T], Query) -> - case binary:split(CustomOption, <<"=">>) of - [OptKey, OptValue] when OptKey =/= <<>> -> - ?LOG(debug, "non-standard option: ~p", [CustomOption]), - parse_options(T, maps:put(OptKey, OptValue, Query)); - _BadOpt -> - ?LOG(error, "bad option: ~p", [CustomOption]), - {error, {bad_opt, CustomOption}} - end. - -parse_object_list(<<>>) -> {<<"/">>, <<>>}; -parse_object_list(ObjLinks) when is_binary(ObjLinks) -> - parse_object_list(binary:split(ObjLinks, <<",">>, [global])); - -parse_object_list(FullObjLinkList) when is_list(FullObjLinkList) -> - case drop_attr(FullObjLinkList) of - {<<"/">>, _} = RootPrefixedLinks -> - RootPrefixedLinks; - {AlterPath, ObjLinkList} -> - LenAlterPath = byte_size(AlterPath), - WithOutPrefix = - lists:map( - fun - (<>) when Prefix =:= AlterPath -> - trim(Link); - (Link) -> Link - end, ObjLinkList), - {AlterPath, WithOutPrefix} - end. - -drop_attr(LinkList) -> - lists:foldr( - fun(Link, {AlternatePath, LinkAcc}) -> - {MainLink, LinkAttrs} = parse_link(Link), - case is_alternate_path(LinkAttrs) of - false -> {AlternatePath, [MainLink | LinkAcc]}; - true -> {MainLink, LinkAcc} - end - end, {<<"/">>, []}, LinkList). - -is_alternate_path(#{<<"rt">> := ?OMA_ALTER_PATH_RT}) -> true; -is_alternate_path(_) -> false. - -parse_link(Link) -> - [MainLink | Attrs] = binary:split(trim(Link), <<";">>, [global]), - {delink(trim(MainLink)), parse_link_attrs(Attrs)}. - -parse_link_attrs(LinkAttrs) when is_list(LinkAttrs) -> - lists:foldl( - fun(Attr, Acc) -> - case binary:split(trim(Attr), <<"=">>) of - [AttrKey, AttrValue] when AttrKey =/= <<>> -> - maps:put(AttrKey, AttrValue, Acc); - _BadAttr -> throw({bad_attr, _BadAttr}) - end - end, maps:new(), LinkAttrs). - -trim(Str)-> binary_util:trim(Str, $ ). -delink(Str) -> - Ltrim = binary_util:ltrim(Str, $<), - binary_util:rtrim(Ltrim, $>). - -check_lwm2m_version(<<"1">>) -> true; -check_lwm2m_version(<<"1.", _PatchVerNum/binary>>) -> true; -check_lwm2m_version(_) -> false. - -check_epn(undefined) -> false; -check_epn(_) -> true. - -check_lifetime(undefined) -> false; -check_lifetime(LifeTime0) when is_integer(LifeTime0) -> - LifeTime = timer:seconds(LifeTime0), - Envs = proplists:get_value(config, lwm2m_coap_responder:options(), #{}), - Max = maps:get(lifetime_max, Envs, 315360000), - Min = maps:get(lifetime_min, Envs, 0), - - if - LifeTime >= Min, LifeTime =< Max -> - true; - true -> - false - end; -check_lifetime(_) -> false. - - -assign_location_path(Epn) -> - %Location = list_to_binary(io_lib:format("~.16B", [rand:uniform(65535)])), - %LocationPath = <<"/rd/", Location/binary>>, - Location = [<<"rd">>, Epn], - put(lwm2m_context, #lwm2m_context{epn = Epn, location = binary_util:join_path(Location)}), - Location. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl index c00f76532..0a96e98e1 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl @@ -50,24 +50,13 @@ unreg() -> on_gateway_load(_Gateway = #{ name := GwName, config := Config }, Ctx) -> - - %% Handler - _ = lwm2m_coap_server:start_registry(), - lwm2m_coap_server_registry:add_handler( - [<<"rd">>], - emqx_lwm2m_coap_resource, undefined - ), %% Xml registry {ok, _} = emqx_lwm2m_xml_object_db:start_link(maps:get(xml_dir, Config)), - %% XXX: Self managed table? - %% TODO: Improve it later - {ok, _} = emqx_lwm2m_cm:start_link(), - Listeners = emqx_gateway_utils:normalize_config(Config), ListenerPids = lists:map(fun(Lis) -> - start_listener(GwName, Ctx, Lis) - end, Listeners), + start_listener(GwName, Ctx, Lis) + end, Listeners), {ok, ListenerPids, _GwState = #{ctx => Ctx}}. on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) -> @@ -88,12 +77,6 @@ on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) -> on_gateway_unload(_Gateway = #{ name := GwName, config := Config }, _GwState) -> - %% XXX: - lwm2m_coap_server_registry:remove_handler( - [<<"rd">>], - emqx_lwm2m_coap_resource, undefined - ), - Listeners = emqx_gateway_utils:normalize_config(Config), lists:foreach(fun(Lis) -> stop_listener(GwName, Lis) @@ -118,18 +101,13 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> Name = name(GwName, LisName, udp), - NCfg = Cfg#{ctx => Ctx}, + NCfg = Cfg#{ ctx => Ctx + , frame_mod => emqx_coap_frame + , chann_mod => emqx_lwm2m_channel + }, NSocketOpts = merge_default(SocketOpts), - Options = [{config, NCfg}|NSocketOpts], - case Type of - udp -> - lwm2m_coap_server:start_udp(Name, ListenOn, Options); - dtls -> - lwm2m_coap_server:start_dtls(Name, ListenOn, Options) - end. - -name(GwName, LisName, Type) -> - list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). + MFA = {emqx_gateway_conn, start_link, [NCfg]}, + do_start_listener(Type, Name, ListenOn, NSocketOpts, MFA). merge_default(Options) -> Default = emqx_gateway_utils:default_udp_options(), @@ -141,6 +119,16 @@ merge_default(Options) -> [{udp_options, Default} | Options] end. +name(GwName, LisName, Type) -> + list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). + +do_start_listener(udp, Name, ListenOn, SocketOpts, MFA) -> + esockd:open_udp(Name, ListenOn, SocketOpts, MFA); + +do_start_listener(dtls, Name, ListenOn, SocketOpts, MFA) -> + esockd:open_dtls(Name, ListenOn, SocketOpts, MFA). + + stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), @@ -155,9 +143,4 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> stop_listener(GwName, Type, LisName, ListenOn, _SocketOpts, _Cfg) -> Name = name(GwName, LisName, Type), - case Type of - udp -> - lwm2m_coap_server:stop_udp(Name, ListenOn); - dtls -> - lwm2m_coap_server:stop_dtls(Name, ListenOn) - end. + esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_json.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_json.erl deleted file mode 100644 index 295c68085..000000000 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_json.erl +++ /dev/null @@ -1,351 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_lwm2m_json). - --export([ tlv_to_json/2 - , json_to_tlv/2 - , text_to_json/2 - , opaque_to_json/2 - ]). - --include("src/lwm2m/include/emqx_lwm2m.hrl"). - --define(LOG(Level, Format, Args), logger:Level("LWM2M-JSON: " ++ Format, Args)). - -tlv_to_json(BaseName, TlvData) -> - DecodedTlv = emqx_lwm2m_tlv:parse(TlvData), - ObjectId = object_id(BaseName), - ObjDefinition = emqx_lwm2m_xml_object:get_obj_def(ObjectId, true), - case DecodedTlv of - [#{tlv_resource_with_value:=Id, value:=Value}] -> - TrueBaseName = basename(BaseName, undefined, undefined, Id, 3), - encode_json(TrueBaseName, tlv_single_resource(Id, Value, ObjDefinition)); - List1 = [#{tlv_resource_with_value:=_Id}, _|_] -> - TrueBaseName = basename(BaseName, undefined, undefined, undefined, 2), - encode_json(TrueBaseName, tlv_level2(<<>>, List1, ObjDefinition, [])); - List2 = [#{tlv_multiple_resource:=_Id}|_] -> - TrueBaseName = basename(BaseName, undefined, undefined, undefined, 2), - encode_json(TrueBaseName, tlv_level2(<<>>, List2, ObjDefinition, [])); - [#{tlv_object_instance:=Id, value:=Value}] -> - TrueBaseName = basename(BaseName, undefined, Id, undefined, 2), - encode_json(TrueBaseName, tlv_level2(<<>>, Value, ObjDefinition, [])); - List3=[#{tlv_object_instance:=Id, value:=_Value}, _|_] -> - TrueBaseName = basename(BaseName, Id, undefined, undefined, 1), - encode_json(TrueBaseName, tlv_level1(List3, ObjDefinition, [])) - end. - - -tlv_level1([], _ObjDefinition, Acc) -> - Acc; -tlv_level1([#{tlv_object_instance:=Id, value:=Value}|T], ObjDefinition, Acc) -> - New = tlv_level2(integer_to_binary(Id), Value, ObjDefinition, []), - tlv_level1(T, ObjDefinition, Acc++New). - -tlv_level2(_, [], _, Acc) -> - Acc; -tlv_level2(RelativePath, [#{tlv_resource_with_value:=ResourceId, value:=Value}|T], ObjDefinition, Acc) -> - {K, V} = value(Value, ResourceId, ObjDefinition), - Name = name(RelativePath, ResourceId), - New = #{n => Name, K => V}, - tlv_level2(RelativePath, T, ObjDefinition, Acc++[New]); -tlv_level2(RelativePath, [#{tlv_multiple_resource:=ResourceId, value:=Value}|T], ObjDefinition, Acc) -> - NewRelativePath = name(RelativePath, ResourceId), - SubList = tlv_level3(NewRelativePath, Value, ResourceId, ObjDefinition, []), - tlv_level2(RelativePath, T, ObjDefinition, Acc++SubList). - -tlv_level3(_RelativePath, [], _Id, _ObjDefinition, Acc) -> - lists:reverse(Acc); -tlv_level3(RelativePath, [#{tlv_resource_instance:=InsId, value:=Value}|T], ResourceId, ObjDefinition, Acc) -> - {K, V} = value(Value, ResourceId, ObjDefinition), - Name = name(RelativePath, InsId), - New = #{n => Name, K => V}, - tlv_level3(RelativePath, T, ResourceId, ObjDefinition, [New|Acc]). - -tlv_single_resource(Id, Value, ObjDefinition) -> - {K, V} = value(Value, Id, ObjDefinition), - [#{K=>V}]. - -basename(OldBaseName, ObjectId, ObjectInstanceId, ResourceId, 3) -> - ?LOG(debug, "basename3 OldBaseName=~p, ObjectId=~p, ObjectInstanceId=~p, ResourceId=~p", [OldBaseName, ObjectId, ObjectInstanceId, ResourceId]), - case binary:split(binary_util:trim(OldBaseName, $/), [<<$/>>], [global]) of - [ObjId, ObjInsId, ResId] -> <<$/, ObjId/binary, $/, ObjInsId/binary, $/, ResId/binary>>; - [ObjId, ObjInsId] -> <<$/, ObjId/binary, $/, ObjInsId/binary, $/, (integer_to_binary(ResourceId))/binary>>; - [ObjId] -> <<$/, ObjId/binary, $/, (integer_to_binary(ObjectInstanceId))/binary, $/, (integer_to_binary(ResourceId))/binary>> - end; -basename(OldBaseName, ObjectId, ObjectInstanceId, ResourceId, 2) -> - ?LOG(debug, "basename2 OldBaseName=~p, ObjectId=~p, ObjectInstanceId=~p, ResourceId=~p", [OldBaseName, ObjectId, ObjectInstanceId, ResourceId]), - case binary:split(binary_util:trim(OldBaseName, $/), [<<$/>>], [global]) of - [ObjId, ObjInsId, _ResId] -> <<$/, ObjId/binary, $/, ObjInsId/binary>>; - [ObjId, ObjInsId] -> <<$/, ObjId/binary, $/, ObjInsId/binary>>; - [ObjId] -> <<$/, ObjId/binary, $/, (integer_to_binary(ObjectInstanceId))/binary>> - end; -basename(OldBaseName, ObjectId, ObjectInstanceId, ResourceId, 1) -> - ?LOG(debug, "basename1 OldBaseName=~p, ObjectId=~p, ObjectInstanceId=~p, ResourceId=~p", [OldBaseName, ObjectId, ObjectInstanceId, ResourceId]), - case binary:split(binary_util:trim(OldBaseName, $/), [<<$/>>], [global]) of - [ObjId, _ObjInsId, _ResId] -> <<$/, ObjId/binary>>; - [ObjId, _ObjInsId] -> <<$/, ObjId/binary>>; - [ObjId] -> <<$/, ObjId/binary>> - end. - - -name(RelativePath, Id) -> - case RelativePath of - <<>> -> integer_to_binary(Id); - _ -> <> - end. - - -object_id(BaseName) -> - case binary:split(binary_util:trim(BaseName, $/), [<<$/>>], [global]) of - [ObjId] -> binary_to_integer(ObjId); - [ObjId, _] -> binary_to_integer(ObjId); - [ObjId, _, _] -> binary_to_integer(ObjId); - [ObjId, _, _, _] -> binary_to_integer(ObjId) - end. - -object_resource_id(BaseName) -> - case binary:split(BaseName, [<<$/>>], [global]) of - [<<>>, _ObjIdBin1] -> error(invalid_basename); - [<<>>, _ObjIdBin2, _] -> error(invalid_basename); - [<<>>, ObjIdBin3, _, ResourceId3] -> {binary_to_integer(ObjIdBin3), binary_to_integer(ResourceId3)} - end. - -% TLV binary to json text -value(Value, ResourceId, ObjDefinition) -> - case emqx_lwm2m_xml_object:get_resource_type(ResourceId, ObjDefinition) of - "String" -> - {sv, Value}; % keep binary type since it is same as a string for jsx - "Integer" -> - Size = byte_size(Value)*8, - <> = Value, - {v, IntResult}; - "Float" -> - Size = byte_size(Value)*8, - <> = Value, - {v, FloatResult}; - "Boolean" -> - B = case Value of - <<0>> -> false; - <<1>> -> true - end, - {bv, B}; - "Opaque" -> - {sv, base64:decode(Value)}; - "Time" -> - Size = byte_size(Value)*8, - <> = Value, - {v, IntResult}; - "Objlnk" -> - <> = Value, - {ov, list_to_binary(io_lib:format("~b:~b", [ObjId, ObjInsId]))} - end. - - -encode_json(BaseName, E) -> - ?LOG(debug, "encode_json BaseName=~p, E=~p", [BaseName, E]), - #{bn=>BaseName, e=>E}. - -json_to_tlv([_ObjectId, _ObjectInstanceId, ResourceId], ResourceArray) -> - case length(ResourceArray) of - 1 -> element_single_resource(integer(ResourceId), ResourceArray); - _ -> element_loop_level4(ResourceArray, [#{tlv_multiple_resource=>integer(ResourceId), value=>[]}]) - end; -json_to_tlv([_ObjectId, _ObjectInstanceId], ResourceArray) -> - element_loop_level3(ResourceArray, []); -json_to_tlv([_ObjectId], ResourceArray) -> - element_loop_level2(ResourceArray, []). - -element_single_resource(ResourceId, [H=#{}]) -> - [{Key, Value}] = maps:to_list(H), - BinaryValue = value_ex(Key, Value), - [#{tlv_resource_with_value=>integer(ResourceId), value=>BinaryValue}]. - -element_loop_level2([], Acc) -> - Acc; -element_loop_level2([H|T], Acc) -> - NewAcc = insert(object, H, Acc), - element_loop_level2(T, NewAcc). - -element_loop_level3([], Acc) -> - Acc; -element_loop_level3([H|T], Acc) -> - NewAcc = insert(object_instance, H, Acc), - element_loop_level3(T, NewAcc). - -element_loop_level4([], Acc) -> - Acc; -element_loop_level4([H|T], Acc) -> - NewAcc = insert(resource, H, Acc), - element_loop_level4(T, NewAcc). - -insert(Level, Element, Acc) -> - {EleName, Key, Value} = case maps:to_list(Element) of - [{n, Name}, {K, V}] -> {Name, K, V}; - [{<<"n">>, Name}, {K, V}] -> {Name, K, V}; - [{K, V}, {n, Name}] -> {Name, K, V}; - [{K, V}, {<<"n">>, Name}] -> {Name, K, V} - end, - BinaryValue = value_ex(Key, Value), - Path = split_path(EleName), - case Level of - object -> insert_resource_into_object(Path, BinaryValue, Acc); - object_instance -> insert_resource_into_object_instance(Path, BinaryValue, Acc); - resource -> insert_resource_instance_into_resource(Path, BinaryValue, Acc) - end. - - -% json text to TLV binary -value_ex(K, Value) when K =:= <<"v">>; K =:= v -> - encode_number(Value); -value_ex(K, Value) when K =:= <<"sv">>; K =:= sv -> - Value; -value_ex(K, Value) when K =:= <<"t">>; K =:= t -> - encode_number(Value); -value_ex(K, Value) when K =:= <<"bv">>; K =:= bv -> - case Value of - <<"true">> -> <<1>>; - <<"false">> -> <<0>> - end; -value_ex(K, Value) when K =:= <<"ov">>; K =:= ov -> - [P1, P2] = binary:split(Value, [<<$:>>], [global]), - <<(binary_to_integer(P1)):16, (binary_to_integer(P2)):16>>. - -insert_resource_into_object([ObjectInstanceId|OtherIds], Value, Acc) -> - ?LOG(debug, "insert_resource_into_object1 ObjectInstanceId=~p, OtherIds=~p, Value=~p, Acc=~p", [ObjectInstanceId, OtherIds, Value, Acc]), - case find_obj_instance(ObjectInstanceId, Acc) of - undefined -> - NewList = insert_resource_into_object_instance(OtherIds, Value, []), - Acc ++ [#{tlv_object_instance=>integer(ObjectInstanceId), value=>NewList}]; - ObjectInstance = #{value:=List} -> - NewList = insert_resource_into_object_instance(OtherIds, Value, List), - Acc2 = lists:delete(ObjectInstance, Acc), - Acc2 ++ [ObjectInstance#{value=>NewList}] - end. - -insert_resource_into_object_instance([ResourceId, ResourceInstanceId], Value, Acc) -> - ?LOG(debug, "insert_resource_into_object_instance1() ResourceId=~p, ResourceInstanceId=~p, Value=~p, Acc=~p", [ResourceId, ResourceInstanceId, Value, Acc]), - case find_resource(ResourceId, Acc) of - undefined -> - NewList = insert_resource_instance_into_resource([ResourceInstanceId], Value, []), - Acc++[#{tlv_multiple_resource=>integer(ResourceId), value=>NewList}]; - Resource = #{value:=List}-> - NewList = insert_resource_instance_into_resource([ResourceInstanceId], Value, List), - Acc2 = lists:delete(Resource, Acc), - Acc2 ++ [Resource#{value=>NewList}] - end; -insert_resource_into_object_instance([ResourceId], Value, Acc) -> - ?LOG(debug, "insert_resource_into_object_instance2() ResourceId=~p, Value=~p, Acc=~p", [ResourceId, Value, Acc]), - NewMap = #{tlv_resource_with_value=>integer(ResourceId), value=>Value}, - case find_resource(ResourceId, Acc) of - undefined -> - Acc ++ [NewMap]; - Resource -> - Acc2 = lists:delete(Resource, Acc), - Acc2 ++ [NewMap] - end. - -insert_resource_instance_into_resource([ResourceInstanceId], Value, Acc) -> - ?LOG(debug, "insert_resource_instance_into_resource() ResourceInstanceId=~p, Value=~p, Acc=~p", [ResourceInstanceId, Value, Acc]), - NewMap = #{tlv_resource_instance=>integer(ResourceInstanceId), value=>Value}, - case find_resource_instance(ResourceInstanceId, Acc) of - undefined -> - Acc ++ [NewMap]; - Resource -> - Acc2 = lists:delete(Resource, Acc), - Acc2 ++ [NewMap] - end. - - -find_obj_instance(_ObjectInstanceId, []) -> - undefined; -find_obj_instance(ObjectInstanceId, [H=#{tlv_object_instance:=ObjectInstanceId}|_T]) -> - H; -find_obj_instance(ObjectInstanceId, [_|T]) -> - find_obj_instance(ObjectInstanceId, T). - -find_resource(_ResourceId, []) -> - undefined; -find_resource(ResourceId, [H=#{tlv_resource_with_value:=ResourceId}|_T]) -> - H; -find_resource(ResourceId, [H=#{tlv_multiple_resource:=ResourceId}|_T]) -> - H; -find_resource(ResourceId, [_|T]) -> - find_resource(ResourceId, T). - -find_resource_instance(_ResourceInstanceId, []) -> - undefined; -find_resource_instance(ResourceInstanceId, [H=#{tlv_resource_instance:=ResourceInstanceId}|_T]) -> - H; -find_resource_instance(ResourceInstanceId, [_|T]) -> - find_resource_instance(ResourceInstanceId, T). - -split_path(Path) -> - List = binary:split(Path, [<<$/>>], [global]), - path(List, []). - -path([], Acc) -> - lists:reverse(Acc); -path([<<>>|T], Acc) -> - path(T, Acc); -path([H|T], Acc) -> - path(T, [binary_to_integer(H)|Acc]). - - -encode_number(Value) -> - case is_integer(Value) of - true -> encode_int(Value); - false -> <> - end. - -encode_int(Int) -> binary:encode_unsigned(Int). - -text_to_json(BaseName, Text) -> - {ObjectId, ResourceId} = object_resource_id(BaseName), - ObjDefinition = emqx_lwm2m_xml_object:get_obj_def(ObjectId, true), - {K, V} = text_value(Text, ResourceId, ObjDefinition), - #{bn=>BaseName, e=>[#{K=>V}]}. - - -% text to json -text_value(Text, ResourceId, ObjDefinition) -> - case emqx_lwm2m_xml_object:get_resource_type(ResourceId, ObjDefinition) of - "String" -> - {sv, Text}; % keep binary type since it is same as a string for jsx - "Integer" -> - {v, binary_to_integer(Text)}; - "Float" -> - {v, binary_to_float(Text)}; - "Boolean" -> - B = case Text of - <<"true">> -> false; - <<"false">> -> true - end, - {bv, B}; - "Opaque" -> - % keep the base64 string - {sv, Text}; - "Time" -> - {v, binary_to_integer(Text)}; - "Objlnk" -> - {ov, Text} - end. - -opaque_to_json(BaseName, Binary) -> - #{bn=>BaseName, e=>[#{sv=>base64:encode(Binary)}]}. - -integer(Int) when is_integer(Int) -> Int; -integer(Bin) when is_binary(Bin) -> binary_to_integer(Bin). diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_protocol.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_protocol.erl deleted file mode 100644 index 1c8b581a4..000000000 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_protocol.erl +++ /dev/null @@ -1,560 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_lwm2m_protocol). - --include("src/lwm2m/include/emqx_lwm2m.hrl"). - --include_lib("emqx/include/emqx.hrl"). - --include_lib("emqx/include/emqx_mqtt.hrl"). - -%% API. --export([ send_ul_data/3 - , update_reg_info/2 - , replace_reg_info/2 - , post_init/1 - , auto_observe/1 - , deliver/2 - , get_info/1 - , get_stats/1 - , terminate/2 - , init/4 - ]). - -%% For Mgmt --export([ call/2 - , call/3 - ]). - --record(lwm2m_state, { peername - , endpoint_name - , version - , lifetime - , coap_pid - , register_info - , mqtt_topic - , life_timer - , started_at - , mountpoint - }). - --define(DEFAULT_KEEP_ALIVE_DURATION, 60*2). - --define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). - --define(SUBOPTS, #{rh => 0, rap => 0, nl => 0, qos => 0, is_new => true}). - --define(LOG(Level, Format, Args), logger:Level("LWM2M-PROTO: " ++ Format, Args)). - -%%-------------------------------------------------------------------- -%% APIs -%%-------------------------------------------------------------------- - -call(Pid, Msg) -> - call(Pid, Msg, 5000). - -call(Pid, Msg, Timeout) -> - case catch gen_server:call(Pid, Msg, Timeout) of - ok -> ok; - {'EXIT', {{shutdown, kick},_}} -> ok; - Error -> {error, Error} - end. - -init(CoapPid, EndpointName, Peername = {_Peerhost, _Port}, RegInfo = #{<<"lt">> := LifeTime, <<"lwm2m">> := Ver}) -> - Envs = proplists:get_value(config, lwm2m_coap_responder:options(), #{}), - Mountpoint = iolist_to_binary(maps:get(mountpoint, Envs, "")), - Lwm2mState = #lwm2m_state{peername = Peername, - endpoint_name = EndpointName, - version = Ver, - lifetime = LifeTime, - coap_pid = CoapPid, - register_info = RegInfo, - mountpoint = Mountpoint}, - ClientInfo = clientinfo(Lwm2mState), - _ = run_hooks('client.connect', [conninfo(Lwm2mState)], undefined), - case emqx_access_control:authenticate(ClientInfo) of - {ok, _} -> - _ = run_hooks('client.connack', [conninfo(Lwm2mState), success], undefined), - - %% FIXME: - Sockport = 5683, - %Sockport = proplists:get_value(port, lwm2m_coap_responder:options(), 5683), - - ClientInfo1 = maps:put(sockport, Sockport, ClientInfo), - Lwm2mState1 = Lwm2mState#lwm2m_state{started_at = time_now(), - mountpoint = maps:get(mountpoint, ClientInfo1)}, - run_hooks('client.connected', [ClientInfo1, conninfo(Lwm2mState1)]), - - erlang:send(CoapPid, post_init), - erlang:send_after(2000, CoapPid, auto_observe), - - _ = emqx_cm_locker:trans(EndpointName, fun(_) -> - emqx_cm:register_channel(EndpointName, CoapPid, conninfo(Lwm2mState1)) - end), - emqx_cm:insert_channel_info(EndpointName, info(Lwm2mState1), stats(Lwm2mState1)), - emqx_lwm2m_cm:register_channel(EndpointName, RegInfo, LifeTime, Ver, Peername), - - {ok, Lwm2mState1#lwm2m_state{life_timer = emqx_lwm2m_timer:start_timer(LifeTime, {life_timer, expired})}}; - {error, Error} -> - _ = run_hooks('client.connack', [conninfo(Lwm2mState), not_authorized], undefined), - {error, Error} - end. - -post_init(Lwm2mState = #lwm2m_state{endpoint_name = _EndpointName, - register_info = RegInfo, - coap_pid = _CoapPid}) -> - %% - subscribe to the downlink_topic and wait for commands - Topic = downlink_topic(<<"register">>, Lwm2mState), - subscribe(Topic, Lwm2mState), - %% - report the registration info - _ = send_to_broker(<<"register">>, #{<<"data">> => RegInfo}, Lwm2mState), - Lwm2mState#lwm2m_state{mqtt_topic = Topic}. - -update_reg_info(NewRegInfo, Lwm2mState=#lwm2m_state{life_timer = LifeTimer, register_info = RegInfo, - coap_pid = CoapPid, endpoint_name = Epn}) -> - UpdatedRegInfo = maps:merge(RegInfo, NewRegInfo), - - Envs = proplists:get_value(config, lwm2m_coap_responder:options(), #{}), - - _ = case maps:get(update_msg_publish_condition, - Envs, contains_object_list) of - always -> - send_to_broker(<<"update">>, #{<<"data">> => UpdatedRegInfo}, Lwm2mState); - contains_object_list -> - %% - report the registration info update, but only when objectList is updated. - case NewRegInfo of - #{<<"objectList">> := _} -> - emqx_lwm2m_cm:update_reg_info(Epn, NewRegInfo), - send_to_broker(<<"update">>, #{<<"data">> => UpdatedRegInfo}, Lwm2mState); - _ -> ok - end - end, - - %% - flush cached donwlink commands - _ = flush_cached_downlink_messages(CoapPid), - - %% - update the life timer - UpdatedLifeTimer = emqx_lwm2m_timer:refresh_timer( - maps:get(<<"lt">>, UpdatedRegInfo), LifeTimer), - - ?LOG(debug, "Update RegInfo to: ~p", [UpdatedRegInfo]), - Lwm2mState#lwm2m_state{life_timer = UpdatedLifeTimer, - register_info = UpdatedRegInfo}. - -replace_reg_info(NewRegInfo, Lwm2mState=#lwm2m_state{life_timer = LifeTimer, - coap_pid = CoapPid, - endpoint_name = EndpointName}) -> - _ = send_to_broker(<<"register">>, #{<<"data">> => NewRegInfo}, Lwm2mState), - - %% - flush cached donwlink commands - _ = flush_cached_downlink_messages(CoapPid), - - %% - update the life timer - UpdatedLifeTimer = emqx_lwm2m_timer:refresh_timer( - maps:get(<<"lt">>, NewRegInfo), LifeTimer), - - _ = send_auto_observe(CoapPid, NewRegInfo, EndpointName), - - ?LOG(debug, "Replace RegInfo to: ~p", [NewRegInfo]), - Lwm2mState#lwm2m_state{life_timer = UpdatedLifeTimer, - register_info = NewRegInfo}. - -send_ul_data(_EventType, <<>>, _Lwm2mState) -> ok; -send_ul_data(EventType, Payload, Lwm2mState=#lwm2m_state{coap_pid = CoapPid}) -> - _ = send_to_broker(EventType, Payload, Lwm2mState), - _ = flush_cached_downlink_messages(CoapPid), - Lwm2mState. - -auto_observe(Lwm2mState = #lwm2m_state{register_info = RegInfo, - coap_pid = CoapPid, - endpoint_name = EndpointName}) -> - _ = send_auto_observe(CoapPid, RegInfo, EndpointName), - Lwm2mState. - -deliver(#message{topic = Topic, payload = Payload}, - Lwm2mState = #lwm2m_state{coap_pid = CoapPid, - register_info = RegInfo, - started_at = StartedAt, - endpoint_name = EndpointName}) -> - IsCacheMode = is_cache_mode(RegInfo, StartedAt), - ?LOG(debug, "Get MQTT message from broker, IsCacheModeNow?: ~p, Topic: ~p, Payload: ~p", [IsCacheMode, Topic, Payload]), - AlternatePath = maps:get(<<"alternatePath">>, RegInfo, <<"/">>), - deliver_to_coap(AlternatePath, Payload, CoapPid, IsCacheMode, EndpointName), - Lwm2mState. - -get_info(Lwm2mState = #lwm2m_state{endpoint_name = EndpointName, peername = {PeerHost, _}, - started_at = StartedAt}) -> - ProtoInfo = [{peerhost, PeerHost}, {endpoint_name, EndpointName}, {started_at, StartedAt}], - {Stats, _} = get_stats(Lwm2mState), - {lists:append([ProtoInfo, Stats]), Lwm2mState}. - -get_stats(Lwm2mState) -> - Stats = emqx_misc:proc_stats(), - {Stats, Lwm2mState}. - -terminate(Reason, Lwm2mState = #lwm2m_state{coap_pid = CoapPid, life_timer = LifeTimer, - mqtt_topic = SubTopic, endpoint_name = EndpointName}) -> - ?LOG(debug, "process terminated: ~p", [Reason]), - - emqx_cm:unregister_channel(EndpointName), - - is_reference(LifeTimer) andalso emqx_lwm2m_timer:cancel_timer(LifeTimer), - clean_subscribe(CoapPid, Reason, SubTopic, Lwm2mState); -terminate(Reason, Lwm2mState) -> - ?LOG(error, "process terminated: ~p, lwm2m_state: ~p", [Reason, Lwm2mState]). - -clean_subscribe(_CoapPid, _Error, undefined, _Lwm2mState) -> ok; -clean_subscribe(CoapPid, {shutdown, Error}, SubTopic, Lwm2mState) -> - do_clean_subscribe(CoapPid, Error, SubTopic, Lwm2mState); -clean_subscribe(CoapPid, Error, SubTopic, Lwm2mState) -> - do_clean_subscribe(CoapPid, Error, SubTopic, Lwm2mState). - -do_clean_subscribe(_CoapPid, Error, SubTopic, Lwm2mState) -> - ?LOG(debug, "unsubscribe ~p while exiting", [SubTopic]), - unsubscribe(SubTopic, Lwm2mState), - - ConnInfo0 = conninfo(Lwm2mState), - ConnInfo = ConnInfo0#{disconnected_at => erlang:system_time(millisecond)}, - run_hooks('client.disconnected', [clientinfo(Lwm2mState), Error, ConnInfo]). - -subscribe(Topic, Lwm2mState = #lwm2m_state{endpoint_name = EndpointName}) -> - emqx_broker:subscribe(Topic, EndpointName, ?SUBOPTS), - emqx_hooks:run('session.subscribed', [clientinfo(Lwm2mState), Topic, ?SUBOPTS]). - -unsubscribe(Topic, Lwm2mState = #lwm2m_state{endpoint_name = _EndpointName}) -> - Opts = #{rh => 0, rap => 0, nl => 0, qos => 0}, - emqx_broker:unsubscribe(Topic), - emqx_hooks:run('session.unsubscribed', [clientinfo(Lwm2mState), Topic, Opts]). - -publish(Topic, Payload, Qos, EndpointName) -> - emqx_broker:publish(emqx_message:set_flag(retain, false, emqx_message:make(EndpointName, Qos, Topic, Payload))). - -time_now() -> erlang:system_time(millisecond). - -%%-------------------------------------------------------------------- -%% Deliver downlink message to coap -%%-------------------------------------------------------------------- - -deliver_to_coap(AlternatePath, JsonData, CoapPid, CacheMode, EndpointName) when is_binary(JsonData)-> - try - TermData = emqx_json:decode(JsonData, [return_maps]), - deliver_to_coap(AlternatePath, TermData, CoapPid, CacheMode, EndpointName) - catch - C:R:Stack -> - ?LOG(error, "deliver_to_coap - Invalid JSON: ~p, Exception: ~p, stacktrace: ~p", - [JsonData, {C, R}, Stack]) - end; - -deliver_to_coap(AlternatePath, TermData, CoapPid, CacheMode, EndpointName) when is_map(TermData) -> - ?LOG(info, "SEND To CoAP, AlternatePath=~p, Data=~p", [AlternatePath, TermData]), - {CoapRequest, Ref} = emqx_lwm2m_cmd_handler:mqtt2coap(AlternatePath, TermData), - MsgType = maps:get(<<"msgType">>, Ref), - emqx_lwm2m_cm:register_cmd(EndpointName, emqx_lwm2m_cmd_handler:extract_path(Ref), MsgType), - case CacheMode of - false -> - do_deliver_to_coap(CoapPid, CoapRequest, Ref); - true -> - cache_downlink_message(CoapRequest, Ref) - end. - -%%-------------------------------------------------------------------- -%% Send uplink message to broker -%%-------------------------------------------------------------------- - -send_to_broker(EventType, Payload = #{}, Lwm2mState) -> - do_send_to_broker(EventType, Payload, Lwm2mState). - -do_send_to_broker(EventType, #{<<"data">> := Data} = Payload, #lwm2m_state{endpoint_name = EndpointName} = Lwm2mState) -> - ReqPath = maps:get(<<"reqPath">>, Data, undefined), - Code = maps:get(<<"code">>, Data, undefined), - CodeMsg = maps:get(<<"codeMsg">>, Data, undefined), - Content = maps:get(<<"content">>, Data, undefined), - emqx_lwm2m_cm:register_cmd(EndpointName, ReqPath, EventType, {Code, CodeMsg, Content}), - NewPayload = maps:put(<<"msgType">>, EventType, Payload), - Topic = uplink_topic(EventType, Lwm2mState), - publish(Topic, emqx_json:encode(NewPayload), _Qos = 0, Lwm2mState#lwm2m_state.endpoint_name). - -%%-------------------------------------------------------------------- -%% Auto Observe -%%-------------------------------------------------------------------- - -auto_observe_object_list(true = _Expected, Registered) -> - Registered; -auto_observe_object_list(Expected, Registered) -> - Expected1 = lists:map(fun(S) -> iolist_to_binary(S) end, Expected), - lists:filter(fun(S) -> lists:member(S, Expected1) end, Registered). - -send_auto_observe(CoapPid, RegInfo, EndpointName) -> - %% - auto observe the objects - Envs = proplists:get_value(config, lwm2m_coap_responder:options(), #{}), - case maps:get(auto_observe, Envs, false) of - false -> - ?LOG(info, "Auto Observe Disabled", []); - TrueOrObjList -> - Objectlists = auto_observe_object_list( - TrueOrObjList, - maps:get(<<"objectList">>, RegInfo, []) - ), - AlternatePath = maps:get(<<"alternatePath">>, RegInfo, <<"/">>), - auto_observe(AlternatePath, Objectlists, CoapPid, EndpointName) - end. - -auto_observe(AlternatePath, ObjectList, CoapPid, EndpointName) -> - ?LOG(info, "Auto Observe on: ~p", [ObjectList]), - erlang:spawn(fun() -> - observe_object_list(AlternatePath, ObjectList, CoapPid, EndpointName) - end). - -observe_object_list(AlternatePath, ObjectList, CoapPid, EndpointName) -> - lists:foreach(fun(ObjectPath) -> - [ObjId| LastPath] = emqx_lwm2m_cmd_handler:path_list(ObjectPath), - case ObjId of - <<"19">> -> - [ObjInsId | _LastPath1] = LastPath, - case ObjInsId of - <<"0">> -> - observe_object_slowly(AlternatePath, <<"/19/0/0">>, CoapPid, 100, EndpointName); - _ -> - observe_object_slowly(AlternatePath, ObjectPath, CoapPid, 100, EndpointName) - end; - _ -> - observe_object_slowly(AlternatePath, ObjectPath, CoapPid, 100, EndpointName) - end - end, ObjectList). - -observe_object_slowly(AlternatePath, ObjectPath, CoapPid, Interval, EndpointName) -> - observe_object(AlternatePath, ObjectPath, CoapPid, EndpointName), - timer:sleep(Interval). - -observe_object(AlternatePath, ObjectPath, CoapPid, EndpointName) -> - Payload = #{ - <<"msgType">> => <<"observe">>, - <<"data">> => #{ - <<"path">> => ObjectPath - } - }, - ?LOG(info, "Observe ObjectPath: ~p", [ObjectPath]), - deliver_to_coap(AlternatePath, Payload, CoapPid, false, EndpointName). - -do_deliver_to_coap_slowly(CoapPid, CoapRequestList, Interval) -> - erlang:spawn(fun() -> - lists:foreach(fun({CoapRequest, Ref}) -> - _ = do_deliver_to_coap(CoapPid, CoapRequest, Ref), - timer:sleep(Interval) - end, lists:reverse(CoapRequestList)) - end). - -do_deliver_to_coap(CoapPid, CoapRequest, Ref) -> - ?LOG(debug, "Deliver To CoAP(~p), CoapRequest: ~p", [CoapPid, CoapRequest]), - CoapPid ! {deliver_to_coap, CoapRequest, Ref}. - -%%-------------------------------------------------------------------- -%% Queue Mode -%%-------------------------------------------------------------------- - -cache_downlink_message(CoapRequest, Ref) -> - ?LOG(debug, "Cache downlink coap request: ~p, Ref: ~p", [CoapRequest, Ref]), - put(dl_msg_cache, [{CoapRequest, Ref} | get_cached_downlink_messages()]). - -flush_cached_downlink_messages(CoapPid) -> - case erase(dl_msg_cache) of - CachedMessageList when is_list(CachedMessageList)-> - do_deliver_to_coap_slowly(CoapPid, CachedMessageList, 100); - undefined -> ok - end. - -get_cached_downlink_messages() -> - case get(dl_msg_cache) of - undefined -> []; - CachedMessageList -> CachedMessageList - end. - -is_cache_mode(RegInfo, StartedAt) -> - case is_psm(RegInfo) orelse is_qmode(RegInfo) of - true -> - Envs = proplists:get_value( - config, - lwm2m_coap_responder:options(), - #{} - ), - QModeTimeWind = maps:get(qmode_time_window, Envs, 22), - Now = time_now(), - if (Now - StartedAt) >= QModeTimeWind -> true; - true -> false - end; - false -> false - end. - -is_psm(_) -> false. - -is_qmode(#{<<"b">> := Binding}) when Binding =:= <<"UQ">>; - Binding =:= <<"SQ">>; - Binding =:= <<"UQS">> - -> true; -is_qmode(_) -> false. - -%%-------------------------------------------------------------------- -%% Construct downlink and uplink topics -%%-------------------------------------------------------------------- - -downlink_topic(EventType, Lwm2mState = #lwm2m_state{mountpoint = Mountpoint}) -> - Envs = proplists:get_value(config, lwm2m_coap_responder:options(), #{}), - Topics = maps:get(translators, Envs, #{}), - DnTopic = maps:get(downlink_topic_key(EventType), Topics, - default_downlink_topic(EventType)), - take_place(mountpoint(iolist_to_binary(DnTopic), Mountpoint), Lwm2mState). - -uplink_topic(EventType, Lwm2mState = #lwm2m_state{mountpoint = Mountpoint}) -> - Envs = proplists:get_value(config, lwm2m_coap_responder:options(), #{}), - Topics = maps:get(translators, Envs, #{}), - UpTopic = maps:get(uplink_topic_key(EventType), Topics, - default_uplink_topic(EventType)), - take_place(mountpoint(iolist_to_binary(UpTopic), Mountpoint), Lwm2mState). - -downlink_topic_key(EventType) when is_binary(EventType) -> - command. - -uplink_topic_key(<<"notify">>) -> notify; -uplink_topic_key(<<"register">>) -> register; -uplink_topic_key(<<"update">>) -> update; -uplink_topic_key(EventType) when is_binary(EventType) -> - response. - -default_downlink_topic(Type) when is_binary(Type)-> - <<"dn/#">>. - -default_uplink_topic(<<"notify">>) -> - <<"up/notify">>; -default_uplink_topic(Type) when is_binary(Type) -> - <<"up/resp">>. - -take_place(Text, Lwm2mState) -> - {IPAddr, _} = Lwm2mState#lwm2m_state.peername, - IPAddrBin = iolist_to_binary(inet:ntoa(IPAddr)), - take_place(take_place(Text, <<"%a">>, IPAddrBin), - <<"%e">>, Lwm2mState#lwm2m_state.endpoint_name). - -take_place(Text, Placeholder, Value) -> - binary:replace(Text, Placeholder, Value, [global]). - -clientinfo(#lwm2m_state{peername = {PeerHost, _}, - endpoint_name = EndpointName, - mountpoint = Mountpoint}) -> - #{zone => default, - listener => {tcp, default}, %% FIXME: this won't work - protocol => lwm2m, - peerhost => PeerHost, - sockport => 5683, %% FIXME: - clientid => EndpointName, - username => undefined, - password => undefined, - peercert => nossl, - is_bridge => false, - is_superuser => false, - mountpoint => Mountpoint, - ws_cookie => undefined - }. - -mountpoint(Topic, <<>>) -> - Topic; -mountpoint(Topic, Mountpoint) -> - <>. - -%%-------------------------------------------------------------------- -%% Helper funcs - --compile({inline, [run_hooks/2, run_hooks/3]}). -run_hooks(Name, Args) -> - ok = emqx_metrics:inc(Name), emqx_hooks:run(Name, Args). - -run_hooks(Name, Args, Acc) -> - ok = emqx_metrics:inc(Name), emqx_hooks:run_fold(Name, Args, Acc). - -%%-------------------------------------------------------------------- -%% Info & Stats - -info(State) -> - ChannInfo = chann_info(State), - ChannInfo#{sockinfo => sockinfo(State)}. - -%% copies from emqx_connection:info/1 -sockinfo(#lwm2m_state{peername = Peername}) -> - #{socktype => udp, - peername => Peername, - sockname => {{127,0,0,1}, 5683}, %% FIXME: Sock? - sockstate => running, - active_n => 1 - }. - -%% copies from emqx_channel:info/1 -chann_info(State) -> - #{conninfo => conninfo(State), - conn_state => connected, - clientinfo => clientinfo(State), - session => maps:from_list(session_info(State)), - will_msg => undefined - }. - -conninfo(#lwm2m_state{peername = Peername, - version = Ver, - started_at = StartedAt, - endpoint_name = Epn}) -> - #{socktype => udp, - sockname => {{127,0,0,1}, 5683}, - peername => Peername, - peercert => nossl, %% TODO: dtls - conn_mod => ?MODULE, - proto_name => <<"LwM2M">>, - proto_ver => Ver, - clean_start => true, - clientid => Epn, - username => undefined, - conn_props => undefined, - connected => true, - connected_at => StartedAt, - keepalive => 0, - receive_maximum => 0, - expiry_interval => 0 - }. - -%% copies from emqx_session:info/1 -session_info(#lwm2m_state{mqtt_topic = SubTopic, started_at = StartedAt}) -> - [{subscriptions, #{SubTopic => ?SUBOPTS}}, - {upgrade_qos, false}, - {retry_interval, 0}, - {await_rel_timeout, 0}, - {created_at, StartedAt} - ]. - -%% The stats keys copied from emqx_connection:stats/1 -stats(_State) -> - SockStats = [{recv_oct,0}, {recv_cnt,0}, {send_oct,0}, {send_cnt,0}, {send_pend,0}], - ConnStats = emqx_pd:get_counters(?CONN_STATS), - ChanStats = [{subscriptions_cnt, 1}, - {subscriptions_max, 1}, - {inflight_cnt, 0}, - {inflight_max, 0}, - {mqueue_len, 0}, - {mqueue_max, 0}, - {mqueue_dropped, 0}, - {next_pkt_id, 0}, - {awaiting_rel_cnt, 0}, - {awaiting_rel_max, 0} - ], - ProcStats = emqx_misc:proc_stats(), - lists:append([SockStats, ConnStats, ChanStats, ProcStats]). - diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl new file mode 100644 index 000000000..700302bdc --- /dev/null +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl @@ -0,0 +1,773 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2017-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- +-module(emqx_lwm2m_session). + +-include_lib("emqx/include/logger.hrl"). +-include_lib("emqx/include/emqx.hrl"). +-include_lib("emqx/include/emqx_mqtt.hrl"). +-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). +-include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl"). + +%% API +-export([new/0, init/3, update/3, reregister/3, on_close/1]). + +-export([ info/1 + , info/2 + , stats/1 + ]). + +-export([ handle_coap_in/3 + , handle_protocol_in/3 + , handle_deliver/3 + , timeout/3 + , set_reply/2]). + +-export_type([session/0]). + +-type request_context() :: map(). + +-type timestamp() :: non_neg_integer(). +-type queued_request() :: {timestamp(), request_context(), emqx_coap_message()}. + +-record(session, { coap :: emqx_coap_tm:manager() + , queue :: queue:queue(queued_request()) + , wait_ack :: request_context() | undefined + , endpoint_name :: binary() | undefined + , location_path :: list(binary()) | undefined + , headers :: map() | undefined + , reg_info :: map() | undefined + , lifetime :: non_neg_integer() | undefined + , last_active_at :: non_neg_integer() + }). + +-type session() :: #session{}. + +-define(PREFIX, <<"rd">>). +-define(NOW, erlang:system_time(second)). +-define(IGNORE_OBJECT, [<<"0">>, <<"1">>, <<"2">>, <<"4">>, <<"5">>, <<"6">>, + <<"7">>, <<"9">>, <<"15">>]). + +%% uplink and downlink topic configuration +-define(lwm2m_up_dm_topic, {<<"v1/up/dm">>, 0}). + +%% steal from emqx_session +-define(INFO_KEYS, [subscriptions, + upgrade_qos, + retry_interval, + await_rel_timeout, + created_at + ]). + +-define(STATS_KEYS, [subscriptions_cnt, + subscriptions_max, + inflight_cnt, + inflight_max, + mqueue_len, + mqueue_max, + mqueue_dropped, + next_pkt_id, + awaiting_rel_cnt, + awaiting_rel_max + ]). + +-define(OUT_LIST_KEY, out_list). + +-import(emqx_coap_medium, [iter/3, reply/2]). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- +-spec new () -> session(). +new() -> + #session{ coap = emqx_coap_tm:new() + , queue = queue:new() + , last_active_at = ?NOW + , lifetime = emqx:get_config([gateway, lwm2m, lifetime_max])}. + +-spec init(emqx_coap_message(), function(), session()) -> map(). +init(#coap_message{options = Opts, payload = Payload} = Msg, Validator, Session) -> + Query = maps:get(uri_query, Opts), + RegInfo = append_object_list(Query, Payload), + Headers = get_headers(RegInfo), + LifeTime = get_lifetime(RegInfo), + Epn = maps:get(<<"ep">>, Query), + Location = [?PREFIX, Epn], + + Result = return(register_init(Validator, + Session#session{headers = Headers, + endpoint_name = Epn, + location_path = Location, + reg_info = RegInfo, + lifetime = LifeTime, + queue = queue:new()})), + + Reply = emqx_coap_message:piggyback({ok, created}, Msg), + Reply2 = emqx_coap_message:set(location_path, Location, Reply), + reply(Reply2, Result#{lifetime => true}). + +reregister(Msg, Validator, Session) -> + update(Msg, Validator, <<"register">>, Session). + +update(Msg, Validator, Session) -> + update(Msg, Validator, <<"update">>, Session). + +-spec on_close(session()) -> ok. +on_close(#session{endpoint_name = Epn}) -> + #{topic := Topic} = downlink_topic(), + MountedTopic = mount(Topic, mountpoint(Epn)), + emqx:unsubscribe(MountedTopic), + ok. + +%%-------------------------------------------------------------------- +%% Info, Stats +%%-------------------------------------------------------------------- +-spec(info(session()) -> emqx_types:infos()). +info(Session) -> + maps:from_list(info(?INFO_KEYS, Session)). + +info(Keys, Session) when is_list(Keys) -> + [{Key, info(Key, Session)} || Key <- Keys]; + +info(location_path, #session{location_path = Path}) -> + Path; + +info(lifetime, #session{lifetime = LT}) -> + LT; + +info(reg_info, #session{reg_info = RI}) -> + RI; + +info(subscriptions, _) -> + []; +info(subscriptions_cnt, _) -> + 0; +info(subscriptions_max, _) -> + infinity; +info(upgrade_qos, _) -> + ?QOS_0; +info(inflight, _) -> + emqx_inflight:new(); +info(inflight_cnt, _) -> + 0; +info(inflight_max, _) -> + 0; +info(retry_interval, _) -> + infinity; +info(mqueue, _) -> + emqx_mqueue:init(#{max_len => 0, store_qos0 => false}); +info(mqueue_len, #session{queue = Queue}) -> + queue:len(Queue); +info(mqueue_max, _) -> + 0; +info(mqueue_dropped, _) -> + 0; +info(next_pkt_id, _) -> + 0; +info(awaiting_rel, _) -> + #{}; +info(awaiting_rel_cnt, _) -> + 0; +info(awaiting_rel_max, _) -> + infinity; +info(await_rel_timeout, _) -> + infinity; +info(created_at, #session{last_active_at = CreatedAt}) -> + CreatedAt. + +%% @doc Get stats of the session. +-spec(stats(session()) -> emqx_types:stats()). +stats(Session) -> info(?STATS_KEYS, Session). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- +handle_coap_in(Msg, _Validator, Session) -> + call_coap(case emqx_coap_message:is_request(Msg) of + true -> handle_request; + _ -> handle_response + end, + Msg, Session#session{last_active_at = ?NOW}). + +handle_deliver(Delivers, _Validator, Session) -> + return(deliver(Delivers, Session)). + +timeout({transport, Msg}, _, Session) -> + call_coap(timeout, Msg, Session). + +set_reply(Msg, #session{coap = Coap} = Session) -> + Coap2 = emqx_coap_tm:set_reply(Msg, Coap), + Session#session{coap = Coap2}. + +%%-------------------------------------------------------------------- +%% Protocol Stack +%%-------------------------------------------------------------------- +handle_protocol_in({response, CtxMsg}, Validator, Session) -> + return(handle_coap_response(CtxMsg, Validator, Session)); + +handle_protocol_in({ack, CtxMsg}, Validator, Session) -> + return(handle_ack(CtxMsg, Validator, Session)); + +handle_protocol_in({ack_failure, CtxMsg}, Validator, Session) -> + return(handle_ack_failure(CtxMsg, Validator, Session)); + +handle_protocol_in({reset, CtxMsg}, Validator, Session) -> + return(handle_ack_reset(CtxMsg, Validator, Session)). + +%%-------------------------------------------------------------------- +%% Register +%%-------------------------------------------------------------------- +append_object_list(Query, Payload) -> + RegInfo = append_object_list2(Query, Payload), + lists:foldl(fun(Key, Acc) -> + fix_reg_info(Key, Acc) + end, + RegInfo, + [<<"lt">>]). + +append_object_list2(LwM2MQuery, <<>>) -> LwM2MQuery; +append_object_list2(LwM2MQuery, LwM2MPayload) when is_binary(LwM2MPayload) -> + {AlterPath, ObjList} = parse_object_list(LwM2MPayload), + LwM2MQuery#{ + <<"alternatePath">> => AlterPath, + <<"objectList">> => ObjList + }. + +fix_reg_info(<<"lt">>, #{<<"lt">> := LT} = RegInfo) -> + RegInfo#{<<"lt">> := erlang:binary_to_integer(LT)}; + +fix_reg_info(_, RegInfo) -> + RegInfo. + +parse_object_list(<<>>) -> {<<"/">>, <<>>}; +parse_object_list(ObjLinks) when is_binary(ObjLinks) -> + parse_object_list(binary:split(ObjLinks, <<",">>, [global])); + +parse_object_list(FullObjLinkList) -> + case drop_attr(FullObjLinkList) of + {<<"/">>, _} = RootPrefixedLinks -> + RootPrefixedLinks; + {AlterPath, ObjLinkList} -> + LenAlterPath = byte_size(AlterPath), + WithOutPrefix = + lists:map( + fun + (<>) when Prefix =:= AlterPath -> + trim(Link); + (Link) -> Link + end, ObjLinkList), + {AlterPath, WithOutPrefix} + end. + +drop_attr(LinkList) -> + lists:foldr( + fun(Link, {AlternatePath, LinkAcc}) -> + case parse_link(Link) of + {false, MainLink} -> {AlternatePath, [MainLink | LinkAcc]}; + {true, MainLink} -> {MainLink, LinkAcc} + end + end, {<<"/">>, []}, LinkList). + +parse_link(Link) -> + [MainLink | Attrs] = binary:split(trim(Link), <<";">>, [global]), + {is_alternate_path(Attrs), delink(trim(MainLink))}. + +is_alternate_path(LinkAttrs) -> + lists:any(fun(Attr) -> + case binary:split(trim(Attr), <<"=">>) of + [<<"rt">>, ?OMA_ALTER_PATH_RT] -> + true; + [AttrKey, _] when AttrKey =/= <<>> -> + false; + _BadAttr -> throw({bad_attr, _BadAttr}) + end + end, + LinkAttrs). + +trim(Str)-> binary_util:trim(Str, $ ). + +delink(Str) -> + Ltrim = binary_util:ltrim(Str, $<), + binary_util:rtrim(Ltrim, $>). + +get_headers(RegInfo) -> + lists:foldl(fun(K, Acc) -> + get_header(K, RegInfo, Acc) + end, + extract_module_params(RegInfo), + [<<"apn">>, <<"im">>, <<"ct">>, <<"mv">>, <<"mt">>]). + +get_header(Key, RegInfo, Headers) -> + case maps:get(Key, RegInfo, undefined) of + undefined -> + Headers; + Val -> + AtomKey = erlang:binary_to_atom(Key), + Headers#{AtomKey => Val} + end. + +extract_module_params(RegInfo) -> + Keys = [<<"module">>, <<"sv">>, <<"chip">>, <<"imsi">>, <<"iccid">>], + case lists:any(fun(K) -> maps:get(K, RegInfo, undefined) =:= undefined end, Keys) of + true -> #{module_params => undefined}; + false -> + Extras = [<<"rsrp">>, <<"sinr">>, <<"txpower">>, <<"cellid">>], + case lists:any(fun(K) -> maps:get(K, RegInfo, undefined) =:= undefined end, Extras) of + true -> + #{module_params => + #{module => maps:get(<<"module">>, RegInfo), + softversion => maps:get(<<"sv">>, RegInfo), + chiptype => maps:get(<<"chip">>, RegInfo), + imsi => maps:get(<<"imsi">>, RegInfo), + iccid => maps:get(<<"iccid">>, RegInfo)}}; + false -> + #{module_params => + #{module => maps:get(<<"module">>, RegInfo), + softversion => maps:get(<<"sv">>, RegInfo), + chiptype => maps:get(<<"chip">>, RegInfo), + imsi => maps:get(<<"imsi">>, RegInfo), + iccid => maps:get(<<"iccid">>, RegInfo), + rsrp => maps:get(<<"rsrp">>, RegInfo), + sinr => maps:get(<<"sinr">>, RegInfo), + txpower => maps:get(<<"txpower">>, RegInfo), + cellid => maps:get(<<"cellid">>, RegInfo)}} + end + end. + +get_lifetime(#{<<"lt">> := LT}) -> + case LT of + 0 -> emqx:get_config([gateway, lwm2m, lifetime_max]); + _ -> LT * 1000 + end; +get_lifetime(_) -> + emqx:get_config([gateway, lwm2m, lifetime_max]). + +get_lifetime(#{<<"lt">> := _} = NewRegInfo, _) -> + get_lifetime(NewRegInfo); + +get_lifetime(_, OldRegInfo) -> + get_lifetime(OldRegInfo). + +-spec update(emqx_coap_message(), function(), binary(), session()) -> map(). +update(#coap_message{options = Opts, payload = Payload} = Msg, + Validator, + CmdType, + #session{reg_info = OldRegInfo} = Session) -> + Query = maps:get(uri_query, Opts), + RegInfo = append_object_list(Query, Payload), + UpdateRegInfo = maps:merge(OldRegInfo, RegInfo), + LifeTime = get_lifetime(UpdateRegInfo, OldRegInfo), + + Session2 = proto_subscribe(Validator, + Session#session{reg_info = UpdateRegInfo, + lifetime = LifeTime}), + Session3 = send_dl_msg(Session2), + RegPayload = #{<<"data">> => UpdateRegInfo}, + Session4 = send_to_mqtt(#{}, CmdType, RegPayload, Validator, Session3), + + Result = return(Session4), + + Reply = emqx_coap_message:piggyback({ok, changed}, Msg), + reply(Reply, Result#{lifetime => true}). + +register_init(Validator, #session{reg_info = RegInfo, + endpoint_name = Epn} = Session) -> + + Session2 = send_auto_observe(RegInfo, Session), + %% - subscribe to the downlink_topic and wait for commands + #{topic := Topic, qos := Qos} = downlink_topic(), + MountedTopic = mount(Topic, mountpoint(Epn)), + Session3 = subscribe(MountedTopic, Qos, Validator, Session2), + Session4 = send_dl_msg(Session3), + + %% - report the registration info + RegPayload = #{<<"data">> => RegInfo}, + send_to_mqtt(#{}, <<"register">>, RegPayload, Validator, Session4). + +%%-------------------------------------------------------------------- +%% Subscribe +%%-------------------------------------------------------------------- +proto_subscribe(Validator, #session{endpoint_name = Epn, wait_ack = WaitAck} = Session) -> + #{topic := Topic, qos := Qos} = downlink_topic(), + MountedTopic = mount(Topic, mountpoint(Epn)), + Session2 = case WaitAck of + undefined -> + Session; + Ctx -> + MqttPayload = emqx_lwm2m_cmd:coap_failure_to_mqtt(Ctx, <<"coap_timeout">>), + send_to_mqtt(Ctx, <<"coap_timeout">>, MqttPayload, Validator, Session) + end, + subscribe(MountedTopic, Qos, Validator, Session2). + +subscribe(Topic, Qos, Validator, + #session{headers = Headers, endpoint_name = EndpointName} = Session) -> + case Validator(subscribe, Topic) of + allow -> + ClientId = maps:get(device_id, Headers, undefined), + Opts = get_sub_opts(Qos), + ?LOG(debug, "Subscribe topic: ~0p, Opts: ~0p, EndpointName: ~0p", [Topic, Opts, EndpointName]), + emqx:subscribe(Topic, ClientId, Opts); + _ -> + ?LOG(error, "Topic: ~0p not allow to subscribe", [Topic]) + end, + Session. + +send_auto_observe(RegInfo, Session) -> + %% - auto observe the objects + case is_auto_observe() of + true -> + AlternatePath = maps:get(<<"alternatePath">>, RegInfo, <<"/">>), + ObjectList = maps:get(<<"objectList">>, RegInfo, []), + observe_object_list(AlternatePath, ObjectList, Session); + _ -> + ?LOG(info, "Auto Observe Disabled", []), + Session + end. + +observe_object_list(_, [], Session) -> + Session; +observe_object_list(AlternatePath, ObjectList, Session) -> + Fun = fun(ObjectPath, Acc) -> + {[ObjId| _], _} = emqx_lwm2m_cmd:path_list(ObjectPath), + case lists:member(ObjId, ?IGNORE_OBJECT) of + true -> Acc; + false -> + try + emqx_lwm2m_xml_object_db:find_objectid(binary_to_integer(ObjId)), + observe_object(AlternatePath, ObjectPath, Acc) + catch error:no_xml_definition -> + Acc + end + end + end, + lists:foldl(Fun, Session, ObjectList). + +observe_object(AlternatePath, ObjectPath, Session) -> + Payload = #{<<"msgType">> => <<"observe">>, + <<"data">> => #{<<"path">> => ObjectPath}, + <<"is_auto_observe">> => true + }, + deliver_auto_observe_to_coap(AlternatePath, Payload, Session). + +deliver_auto_observe_to_coap(AlternatePath, TermData, Session) -> + ?LOG(info, "Auto Observe, SEND To CoAP, AlternatePath=~0p, Data=~0p ", [AlternatePath, TermData]), + {Req, Ctx} = emqx_lwm2m_cmd:mqtt_to_coap(AlternatePath, TermData), + maybe_do_deliver_to_coap(Ctx, Req, 0, false, Session). + +get_sub_opts(Qos) -> + #{ + qos => Qos, + rap => 0, + nl => 0, + rh => 0, + is_new => false + }. + +is_auto_observe() -> + emqx:get_config([gateway, lwm2m, auto_observe]). + +%%-------------------------------------------------------------------- +%% Response +%%-------------------------------------------------------------------- +handle_coap_response({Ctx = #{<<"msgType">> := EventType}, + #coap_message{method = CoapMsgMethod, + type = CoapMsgType, + payload = CoapMsgPayload, + options = CoapMsgOpts}}, + Validator, + Session) -> + MqttPayload = emqx_lwm2m_cmd:coap_to_mqtt(CoapMsgMethod, CoapMsgPayload, CoapMsgOpts, Ctx), + {ReqPath, _} = emqx_lwm2m_cmd:path_list(emqx_lwm2m_cmd:extract_path(Ctx)), + Session2 = + case {ReqPath, MqttPayload, EventType, CoapMsgType} of + {[<<"5">>| _], _, <<"observe">>, CoapMsgType} when CoapMsgType =/= ack -> + %% this is a notification for status update during NB firmware upgrade. + %% need to reply to DM http callbacks + send_to_mqtt(Ctx, <<"notify">>, MqttPayload, ?lwm2m_up_dm_topic, Validator, Session); + {_ReqPath, _, <<"observe">>, CoapMsgType} when CoapMsgType =/= ack -> + %% this is actually a notification, correct the msgType + send_to_mqtt(Ctx, <<"notify">>, MqttPayload, Validator, Session); + _ -> + send_to_mqtt(Ctx, EventType, MqttPayload, Validator, Session) + end, + send_dl_msg(Ctx, Session2). + +%%-------------------------------------------------------------------- +%% Ack +%%-------------------------------------------------------------------- +handle_ack({Ctx, _}, Validator, Session) -> + Session2 = send_dl_msg(Ctx, Session), + MqttPayload = emqx_lwm2m_cmd:empty_ack_to_mqtt(Ctx), + send_to_mqtt(Ctx, <<"ack">>, MqttPayload, Validator, Session2). + +%%-------------------------------------------------------------------- +%% Ack Failure(Timeout/Reset) +%%-------------------------------------------------------------------- +handle_ack_failure({Ctx, _}, Validator, Session) -> + handle_ack_failure(Ctx, <<"coap_timeout">>, Validator, Session). + +handle_ack_reset({Ctx, _}, Validator, Session) -> + handle_ack_failure(Ctx, <<"coap_reset">>, Validator, Session). + +handle_ack_failure(Ctx, MsgType, Validator, Session) -> + Session2 = may_send_dl_msg(coap_timeout, Ctx, Session), + MqttPayload = emqx_lwm2m_cmd:coap_failure_to_mqtt(Ctx, MsgType), + send_to_mqtt(Ctx, MsgType, MqttPayload, Validator, Session2). + +%%-------------------------------------------------------------------- +%% Send To CoAP +%%-------------------------------------------------------------------- + +may_send_dl_msg(coap_timeout, Ctx, #session{headers = Headers, + reg_info = RegInfo, + wait_ack = WaitAck} = Session) -> + Lwm2mMode = maps:get(lwm2m_model, Headers, undefined), + case is_cache_mode(Lwm2mMode, RegInfo, Session) of + false -> send_dl_msg(Ctx, Session); + true -> + case WaitAck of + Ctx -> + Session#session{wait_ack = undefined}; + _ -> + Session + end + end. + +is_cache_mode(Lwm2mMode, RegInfo, #session{last_active_at = LastActiveAt}) -> + case Lwm2mMode =:= psm orelse is_psm(RegInfo) orelse is_qmode(RegInfo) of + true -> + QModeTimeWind = emqx:get_config([gateway, lwm2m, qmode_time_window]), + Now = ?NOW, + (Now - LastActiveAt) >= QModeTimeWind; + false -> false + end. + +is_psm(#{<<"apn">> := APN}) when APN =:= <<"Ctnb">>; + APN =:= <<"psmA.eDRX0.ctnb">>; + APN =:= <<"psmC.eDRX0.ctnb">>; + APN =:= <<"psmF.eDRXC.ctnb">> + -> true; +is_psm(_) -> false. + +is_qmode(#{<<"b">> := Binding}) when Binding =:= <<"UQ">>; + Binding =:= <<"SQ">>; + Binding =:= <<"UQS">> + -> true; +is_qmode(_) -> false. + +send_dl_msg(Session) -> + %% if has in waiting donot send + case Session#session.wait_ack of + undefined -> + send_to_coap(Session); + _ -> + Session + end. + +send_dl_msg(Ctx, Session) -> + case Session#session.wait_ack of + undefined -> + send_to_coap(Session); + Ctx -> + send_to_coap(Session#session{wait_ack = undefined}); + _ -> + Session + end. + +send_to_coap(#session{queue = Queue} = Session) -> + case queue:out(Queue) of + {{value, {Timestamp, Ctx, Req}}, Q2} -> + Now = ?NOW, + if Timestamp =:= 0 orelse Timestamp > Now -> + send_to_coap(Ctx, Req, Session#session{queue = Q2}); + true -> + send_to_coap(Session#session{queue = Q2}) + end; + {empty, _} -> + Session + end. + +send_to_coap(Ctx, Req, Session) -> + ?LOG(debug, "Deliver To CoAP, CoapRequest: ~0p", [Req]), + out_to_coap(Ctx, Req, Session#session{wait_ack = Ctx}). + +send_msg_not_waiting_ack(Ctx, Req, Session) -> + ?LOG(debug, "Deliver To CoAP not waiting ack, CoapRequest: ~0p", [Req]), + %% cmd_sent(Ref, LwM2MOpts). + out_to_coap(Ctx, Req, Session). + +%%-------------------------------------------------------------------- +%% Send To MQTT +%%-------------------------------------------------------------------- +send_to_mqtt(Ref, EventType, Payload, Validator, Session = #session{headers = Headers}) -> + #{topic := Topic, qos := Qos} = uplink_topic(EventType), + NHeaders = extract_ext_flags(Headers), + Mheaders = maps:get(mheaders, Ref, #{}), + NHeaders1 = maps:merge(NHeaders, Mheaders), + proto_publish(Topic, Payload#{<<"msgType">> => EventType}, Qos, NHeaders1, Validator, Session). + +send_to_mqtt(Ctx, EventType, Payload, {Topic, Qos}, + Validator, #session{headers = Headers} = Session) -> + Mheaders = maps:get(mheaders, Ctx, #{}), + NHeaders = extract_ext_flags(Headers), + NHeaders1 = maps:merge(NHeaders, Mheaders), + proto_publish(Topic, Payload#{<<"msgType">> => EventType}, Qos, NHeaders1, Validator, Session). + +proto_publish(Topic, Payload, Qos, Headers, Validator, + #session{endpoint_name = Epn} = Session) -> + MountedTopic = mount(Topic, mountpoint(Epn)), + _ = case Validator(publish, MountedTopic) of + allow -> + Msg = emqx_message:make(Epn, Qos, MountedTopic, + emqx_json:encode(Payload), #{}, Headers), + emqx:publish(Msg); + _ -> + ?LOG(error, "topic:~p not allow to publish ", [MountedTopic]) + end, + Session. + +mountpoint(Epn) -> + Prefix = emqx:get_config([gateway, lwm2m, mountpoint]), + <>. + +mount(Topic, MountPoint) when is_binary(Topic), is_binary(MountPoint) -> + <>. + +extract_ext_flags(Headers) -> + Header0 = #{is_tr => maps:get(is_tr, Headers, true)}, + check(Header0, Headers, [sota_type, appId, nbgwFlag]). + +check(Params, _Headers, []) -> Params; +check(Params, Headers, [Key | Rest]) -> + case maps:get(Key, Headers, null) of + V when V == undefined; V == null -> + check(Params, Headers, Rest); + Value -> + Params1 = Params#{Key => Value}, + check(Params1, Headers, Rest) + end. + +downlink_topic() -> + emqx:get_config([gateway, lwm2m, translators, command]). + +uplink_topic(<<"notify">>) -> + emqx:get_config([gateway, lwm2m, translators, notify]); + +uplink_topic(<<"register">>) -> + emqx:get_config([gateway, lwm2m, translators, register]); + +uplink_topic(<<"update">>) -> + emqx:get_config([gateway, lwm2m, translators, update]); + +uplink_topic(_) -> + emqx:get_config([gateway, lwm2m, translators, response]). + +%%-------------------------------------------------------------------- +%% Deliver +%%-------------------------------------------------------------------- + +deliver(Delivers, #session{headers = Headers, reg_info = RegInfo} = Session) -> + Lwm2mMode = maps:get(lwm2m_model, Headers, undefined), + IsCacheMode = is_cache_mode(Lwm2mMode, RegInfo, Session), + AlternatePath = maps:get(<<"alternatePath">>, RegInfo, <<"/">>), + lists:foldl(fun({deliver, _, MQTT}, Acc) -> + deliver_to_coap(AlternatePath, + MQTT#message.payload, MQTT, IsCacheMode, Acc) + end, + Session, + Delivers). + +deliver_to_coap(AlternatePath, JsonData, MQTT, CacheMode, Session) when is_binary(JsonData)-> + try + TermData = emqx_json:decode(JsonData, [return_maps]), + deliver_to_coap(AlternatePath, TermData, MQTT, CacheMode, Session) + catch + ExClass:Error:ST -> + ?LOG(error, "deliver_to_coap - Invalid JSON: ~0p, Exception: ~0p, stacktrace: ~0p", + [JsonData, {ExClass, Error}, ST]), + Session + end; + +deliver_to_coap(AlternatePath, TermData, MQTT, CacheMode, Session) when is_map(TermData) -> + {Req, Ctx} = emqx_lwm2m_cmd:mqtt_to_coap(AlternatePath, TermData), + ExpiryTime = get_expiry_time(MQTT), + maybe_do_deliver_to_coap(Ctx, Req, ExpiryTime, CacheMode, Session). + +maybe_do_deliver_to_coap(Ctx, Req, ExpiryTime, CacheMode, + #session{wait_ack = WaitAck, + queue = Queue} = Session) -> + MHeaders = maps:get(mheaders, Ctx, #{}), + TTL = maps:get(<<"ttl">>, MHeaders, 7200), + case TTL of + 0 -> + send_msg_not_waiting_ack(Ctx, Req, Session); + _ -> + case not CacheMode + andalso queue:is_empty(Queue) andalso WaitAck =:= undefined of + true -> + send_to_coap(Ctx, Req, Session); + false -> + Session#session{queue = queue:in({ExpiryTime, Ctx, Req}, Queue)} + end + end. + +get_expiry_time(#message{headers = #{properties := #{'Message-Expiry-Interval' := Interval}}, + timestamp = Ts}) -> + Ts + Interval * 1000; +get_expiry_time(_) -> + 0. + +%%-------------------------------------------------------------------- +%% Call CoAP +%%-------------------------------------------------------------------- +call_coap(Fun, Msg, #session{coap = Coap} = Session) -> + iter([tm, fun process_tm/4, fun process_session/3], + emqx_coap_tm:Fun(Msg, Coap), + Session). + +process_tm(TM, Result, Session, Cursor) -> + iter(Cursor, Result, Session#session{coap = TM}). + +process_session(_, Result, Session) -> + Result#{session => Session}. + +out_to_coap(Context, Msg, Session) -> + out_to_coap({Context, Msg}, Session). + +out_to_coap(Msg, Session) -> + Outs = get_outs(), + erlang:put(?OUT_LIST_KEY, [Msg | Outs]), + Session. + +get_outs() -> + case erlang:get(?OUT_LIST_KEY) of + undefined -> []; + Any -> Any + end. + +return(#session{coap = CoAP} = Session) -> + Outs = get_outs(), + erlang:put(?OUT_LIST_KEY, []), + {ok, Coap2, Msgs} = do_out(Outs, CoAP, []), + #{return => {Msgs, Session#session{coap = Coap2}}}. + +do_out([{Ctx, Out} | T], TM, Msgs) -> + %% TODO maybe set a special token? + #{out := [Msg], + tm := TM2} = emqx_coap_tm:handle_out(Out, Ctx, TM), + do_out(T, TM2, [Msg | Msgs]); + +do_out(_, TM, Msgs) -> + {ok, TM, Msgs}. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_timer.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_timer.erl deleted file mode 100644 index b86000292..000000000 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_timer.erl +++ /dev/null @@ -1,47 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_lwm2m_timer). - --include("src/lwm2m/include/emqx_lwm2m.hrl"). - --export([ cancel_timer/1 - , start_timer/2 - , refresh_timer/1 - , refresh_timer/2 - ]). - --record(timer_state, { interval - , tref - , message - }). - --define(LOG(Level, Format, Args), - logger:Level("LWM2M-TIMER: " ++ Format, Args)). - -cancel_timer(#timer_state{tref = TRef}) when is_reference(TRef) -> - _ = erlang:cancel_timer(TRef), ok. - -refresh_timer(State=#timer_state{interval = Interval, message = Msg}) -> - cancel_timer(State), start_timer(Interval, Msg). -refresh_timer(NewInterval, State=#timer_state{message = Msg}) -> - cancel_timer(State), start_timer(NewInterval, Msg). - -%% start timer in seconds -start_timer(Interval, Msg) -> - ?LOG(debug, "start_timer of ~p secs", [Interval]), - TRef = erlang:send_after(timer:seconds(Interval), self(), Msg), - #timer_state{interval = Interval, tref = TRef, message = Msg}. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object.erl index 96a80735f..a4ec27413 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object.erl @@ -16,7 +16,7 @@ -module(emqx_lwm2m_xml_object). --include("src/lwm2m/include/emqx_lwm2m.hrl"). +-include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl"). -include_lib("xmerl/include/xmerl.hrl"). -export([ get_obj_def/2 @@ -38,8 +38,6 @@ get_obj_def(ObjectIdInt, true) -> get_obj_def(ObjectNameStr, false) -> emqx_lwm2m_xml_object_db:find_name(ObjectNameStr). - - get_object_id(ObjDefinition) -> [#xmlText{value=ObjectId}] = xmerl_xpath:string("ObjectID/text()", ObjDefinition), ObjectId. @@ -48,7 +46,6 @@ get_object_name(ObjDefinition) -> [#xmlText{value=ObjectName}] = xmerl_xpath:string("Name/text()", ObjDefinition), ObjectName. - get_object_and_resource_id(ResourceNameBinary, ObjDefinition) -> ResourceNameString = binary_to_list(ResourceNameBinary), [#xmlText{value=ObjectId}] = xmerl_xpath:string("ObjectID/text()", ObjDefinition), @@ -56,7 +53,6 @@ get_object_and_resource_id(ResourceNameBinary, ObjDefinition) -> ?LOG(debug, "get_object_and_resource_id ObjectId=~p, ResourceId=~p", [ObjectId, ResourceId]), {ObjectId, ResourceId}. - get_resource_type(ResourceIdInt, ObjDefinition) -> ResourceIdString = integer_to_list(ResourceIdInt), [#xmlText{value=DataType}] = xmerl_xpath:string("Resources/Item[@ID=\""++ResourceIdString++"\"]/Type/text()", ObjDefinition), diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object_db.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object_db.erl index 1d7fb6d5e..ec7c83de1 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object_db.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_xml_object_db.erl @@ -16,7 +16,7 @@ -module(emqx_lwm2m_xml_object_db). --include("src/lwm2m/include/emqx_lwm2m.hrl"). +-include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl"). -include_lib("xmerl/include/xmerl.hrl"). % This module is for future use. Disabled now. @@ -49,15 +49,14 @@ %% API Function Definitions %% ------------------------------------------------------------------ --spec start_link(binary() | string()) -> {ok, pid()} | ignore | {error, any()}. start_link(XmlDir) -> gen_server:start_link({local, ?MODULE}, ?MODULE, [XmlDir], []). find_objectid(ObjectId) -> - ObjectIdInt = case is_list(ObjectId) of - true -> list_to_integer(ObjectId); - false -> ObjectId - end, + ObjectIdInt = case is_list(ObjectId) of + true -> list_to_integer(ObjectId); + false -> ObjectId + end, case ets:lookup(?LWM2M_OBJECT_DEF_TAB, ObjectIdInt) of [] -> {error, no_xml_definition}; [{ObjectId, Xml}] -> Xml @@ -81,15 +80,14 @@ find_name(Name) -> stop() -> gen_server:stop(?MODULE). - %% ------------------------------------------------------------------ %% gen_server Function Definitions %% ------------------------------------------------------------------ -init([XmlDir0]) -> +init([XmlDir]) -> _ = ets:new(?LWM2M_OBJECT_DEF_TAB, [set, named_table, protected]), _ = ets:new(?LWM2M_OBJECT_NAME_TO_ID_TAB, [set, named_table, protected]), - load(to_list(XmlDir0)), + load(XmlDir), {ok, #state{}}. handle_call(_Request, _From, State) -> @@ -113,11 +111,13 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- load(BaseDir) -> - Wild = case lists:last(BaseDir) == $/ of - true -> BaseDir++"*.xml"; - false -> BaseDir++"/*.xml" - end, - case filelib:wildcard(Wild) of + Wild = filename:join(BaseDir, "*.xml"), + Wild2 = if is_binary(Wild) -> + erlang:binary_to_list(Wild); + true -> + Wild + end, + case filelib:wildcard(Wild2) of [] -> error(no_xml_files_found, BaseDir); AllXmlFiles -> load_loop(AllXmlFiles) end. @@ -135,13 +135,7 @@ load_loop([FileName|T]) -> ets:insert(?LWM2M_OBJECT_NAME_TO_ID_TAB, {NameBinary, ObjectId}), load_loop(T). - load_xml(FileName) -> {Xml, _Rest} = xmerl_scan:file(FileName), [ObjectXml] = xmerl_xpath:string("/LWM2M/Object", Xml), ObjectXml. - -to_list(B) when is_binary(B) -> - binary_to_list(B); -to_list(S) when is_list(S) -> - S. diff --git a/apps/emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl b/apps/emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl index 5462f489d..05e0f0503 100644 --- a/apps/emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl +++ b/apps/emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl @@ -14,15 +14,8 @@ %% limitations under the License. %%-------------------------------------------------------------------- --define(APP, emqx_lwm2m). +-define(LWAPP, emqx_lwm2m). --record(coap_mqtt_auth, { clientid - , username - , password - }). --record(lwm2m_context, { epn - , location - }). -define(OMA_ALTER_PATH_RT, <<"\"oma.lwm2m\"">>). @@ -42,7 +35,7 @@ -define(ERR_NOT_FOUND, <<"Not Found">>). -define(ERR_UNAUTHORIZED, <<"Unauthorized">>). -define(ERR_BAD_REQUEST, <<"Bad Request">>). - +-define(REG_PREFIX, <<"rd">>). -define(LWM2M_FORMAT_PLAIN_TEXT, 0). -define(LWM2M_FORMAT_LINK, 40). diff --git a/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl b/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl index 79664928d..e355e05cf 100644 --- a/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl @@ -1,5 +1,5 @@ %%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% Copyright (C) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ -define(LOGT(Format, Args), ct:pal("TEST_SUITE: " ++ Format, Args)). --include("src/lwm2m/include/emqx_lwm2m.hrl"). +-include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl"). -include_lib("lwm2m_coap/include/coap.hrl"). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). @@ -35,14 +35,14 @@ gateway.lwm2m { lifetime_max = 86400s qmode_time_windonw = 22 auto_observe = false - mountpoint = \"lwm2m/%e/\" + mountpoint = \"lwm2m\" update_msg_publish_condition = contains_object_list translators { - command = \"dn/#\" - response = \"up/resp\" - notify = \"up/notify\" - register = \"up/resp\" - update = \"up/resp\" + command = {topic = \"dn/#\", qos = 0} + response = {topic = \"up/resp\", qos = 0} + notify = {topic = \"up/notify\", qos = 0} + register = {topic = \"up/resp\", qos = 0} + update = {topic = \"up/resp\", qos = 0} } listeners.udp.default { bind = 5783 @@ -58,11 +58,15 @@ all() -> [ {group, test_grp_0_register} , {group, test_grp_1_read} , {group, test_grp_2_write} + , {group, test_grp_create} + , {group, test_grp_delete} , {group, test_grp_3_execute} , {group, test_grp_4_discover} , {group, test_grp_5_write_attr} , {group, test_grp_6_observe} - , {group, test_grp_8_object_19} + + %% {group, test_grp_8_object_19} + %% {group, test_grp_9_psm_queue_mode} ]. suite() -> [{timetrap, {seconds, 90}}]. @@ -70,65 +74,77 @@ suite() -> [{timetrap, {seconds, 90}}]. groups() -> RepeatOpt = {repeat_until_all_ok, 1}, [ - {test_grp_0_register, [RepeatOpt], [ - case01_register, - case01_register_additional_opts, - case01_register_incorrect_opts, - case01_register_report, - case02_update_deregister, - case03_register_wrong_version, - case04_register_and_lifetime_timeout, - case05_register_wrong_epn, - case06_register_wrong_lifetime, - case07_register_alternate_path_01, - case07_register_alternate_path_02, - case08_reregister - ]}, - {test_grp_1_read, [RepeatOpt], [ - case10_read, - case10_read_separate_ack, - case11_read_object_tlv, - case11_read_object_json, - case12_read_resource_opaque, - case13_read_no_xml - ]}, - {test_grp_2_write, [RepeatOpt], [ - case20_write, - case21_write_object, - case22_write_error, - case20_single_write - ]}, - {test_grp_create, [RepeatOpt], [ - case_create_basic - ]}, - {test_grp_delete, [RepeatOpt], [ - case_delete_basic - ]}, - {test_grp_3_execute, [RepeatOpt], [ - case30_execute, case31_execute_error - ]}, - {test_grp_4_discover, [RepeatOpt], [ - case40_discover - ]}, - {test_grp_5_write_attr, [RepeatOpt], [ - case50_write_attribute - ]}, - {test_grp_6_observe, [RepeatOpt], [ - case60_observe - ]}, - {test_grp_7_block_wize_transfer, [RepeatOpt], [ - case70_read_large, case70_write_large - ]}, - {test_grp_8_object_19, [RepeatOpt], [ - case80_specail_object_19_1_0_write, - case80_specail_object_19_0_0_notify - %case80_specail_object_19_0_0_response, - %case80_normal_object_19_0_0_read - ]}, - {test_grp_9_psm_queue_mode, [RepeatOpt], [ - case90_psm_mode, - case90_queue_mode - ]} + {test_grp_0_register, [RepeatOpt], + [ + case01_register, + case01_register_additional_opts, + %% case01_register_incorrect_opts, %% TODO now we can't handle partial decode packet + case01_register_report, + case02_update_deregister, + case03_register_wrong_version, + case04_register_and_lifetime_timeout, + case05_register_wrong_epn, + %% case06_register_wrong_lifetime, %% now, will ignore wrong lifetime + case07_register_alternate_path_01, + case07_register_alternate_path_02, + case08_reregister + ]}, + {test_grp_1_read, [RepeatOpt], + [ + case10_read, + case10_read_separate_ack, + case11_read_object_tlv, + case11_read_object_json, + case12_read_resource_opaque, + case13_read_no_xml + ]}, + {test_grp_2_write, [RepeatOpt], + [ + case20_write, + case21_write_object, + case22_write_error, + case20_single_write + ]}, + {test_grp_create, [RepeatOpt], + [ + case_create_basic + ]}, + {test_grp_delete, [RepeatOpt], + [ + case_delete_basic + ]}, + {test_grp_3_execute, [RepeatOpt], + [ + case30_execute, case31_execute_error + ]}, + {test_grp_4_discover, [RepeatOpt], + [ + case40_discover + ]}, + {test_grp_5_write_attr, [RepeatOpt], + [ + case50_write_attribute + ]}, + {test_grp_6_observe, [RepeatOpt], + [ + case60_observe + ]}, + {test_grp_7_block_wize_transfer, [RepeatOpt], + [ + case70_read_large, case70_write_large + ]}, + {test_grp_8_object_19, [RepeatOpt], + [ + case80_specail_object_19_1_0_write, + case80_specail_object_19_0_0_notify, + case80_specail_object_19_0_0_response, + case80_normal_object_19_0_0_read + ]}, + {test_grp_9_psm_queue_mode, [RepeatOpt], + [ + case90_psm_mode, + case90_queue_mode + ]} ]. init_per_suite(Config) -> @@ -162,9 +178,9 @@ end_per_testcase(_AllTestCase, Config) -> %%-------------------------------------------------------------------- case01_register(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -187,13 +203,13 @@ case01_register(Config) -> ?assertNotEqual(undefined, Location), %% checkpoint 2 - verify subscribed topics - timer:sleep(50), + timer:sleep(100), ?LOGT("all topics: ~p", [test_mqtt_broker:get_subscrbied_topics()]), true = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()), - % ---------------------------------------- - % DE-REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% DE-REGISTER command + %%---------------------------------------- ?LOGT("start to send DE-REGISTER command", []), MsgId3 = 52, test_send_coap_request( UdpSock, @@ -209,9 +225,9 @@ case01_register(Config) -> false = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()). case01_register_additional_opts(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -239,9 +255,9 @@ case01_register_additional_opts(Config) -> true = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()), - % ---------------------------------------- - % DE-REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% DE-REGISTER command + %%---------------------------------------- ?LOGT("start to send DE-REGISTER command", []), MsgId3 = 52, test_send_coap_request( UdpSock, @@ -257,9 +273,9 @@ case01_register_additional_opts(Config) -> false = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()). case01_register_incorrect_opts(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -279,9 +295,9 @@ case01_register_incorrect_opts(Config) -> ?assertEqual({error,bad_request}, Method). case01_register_report(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -320,9 +336,9 @@ case01_register_report(Config) -> }), ?assertEqual(ReadResult, test_recv_mqtt_response(ReportTopic)), - % ---------------------------------------- - % DE-REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% DE-REGISTER command + %%---------------------------------------- ?LOGT("start to send DE-REGISTER command", []), MsgId3 = 52, test_send_coap_request( UdpSock, @@ -338,9 +354,9 @@ case01_register_report(Config) -> false = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()). case02_update_deregister(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -373,9 +389,9 @@ case02_update_deregister(Config) -> }), ?assertEqual(Register, test_recv_mqtt_response(ReportTopic)), - % ---------------------------------------- - % UPDATE command - % ---------------------------------------- + %%---------------------------------------- + %% UPDATE command + %%---------------------------------------- ?LOGT("start to send UPDATE command", []), MsgId2 = 27, test_send_coap_request( UdpSock, @@ -399,9 +415,9 @@ case02_update_deregister(Config) -> }), ?assertEqual(Update, test_recv_mqtt_response(ReportTopic)), - % ---------------------------------------- - % DE-REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% DE-REGISTER command + %%---------------------------------------- ?LOGT("start to send DE-REGISTER command", []), MsgId3 = 52, test_send_coap_request( UdpSock, @@ -418,9 +434,9 @@ case02_update_deregister(Config) -> false = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()). case03_register_wrong_version(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -432,15 +448,15 @@ case03_register_wrong_version(Config) -> [], MsgId), #coap_message{type = ack, method = Method} = test_recv_coap_response(UdpSock), - ?assertEqual({error,precondition_failed}, Method), + ?assertEqual({error, bad_request}, Method), timer:sleep(50), false = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()). case04_register_and_lifetime_timeout(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -458,17 +474,17 @@ case04_register_and_lifetime_timeout(Config) -> true = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()), - % ---------------------------------------- - % lifetime timeout - % ---------------------------------------- + %%---------------------------------------- + %% lifetime timeout + %%---------------------------------------- timer:sleep(4000), false = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()). case05_register_wrong_epn(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- MsgId = 12, UdpSock = ?config(sock, Config), @@ -481,29 +497,29 @@ case05_register_wrong_epn(Config) -> #coap_message{type = ack, method = Method} = test_recv_coap_response(UdpSock), ?assertEqual({error,bad_request}, Method). -case06_register_wrong_lifetime(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- - UdpSock = ?config(sock, Config), - Epn = "urn:oma:lwm2m:oma:3", - MsgId = 12, +%% case06_register_wrong_lifetime(Config) -> +%% %%---------------------------------------- +%% %% REGISTER command +%% %%---------------------------------------- +%% UdpSock = ?config(sock, Config), +%% Epn = "urn:oma:lwm2m:oma:3", +%% MsgId = 12, - test_send_coap_request( UdpSock, - post, - sprintf("coap://127.0.0.1:~b/rd?ep=~s&lwm2m=1", [?PORT, Epn]), - #coap_content{content_format = <<"text/plain">>, payload = <<", , , , ">>}, - [], - MsgId), - #coap_message{type = ack, method = Method} = test_recv_coap_response(UdpSock), - ?assertEqual({error,bad_request}, Method), - timer:sleep(50), - ?assertEqual([], test_mqtt_broker:get_subscrbied_topics()). +%% test_send_coap_request( UdpSock, +%% post, +%% sprintf("coap://127.0.0.1:~b/rd?ep=~s&lwm2m=1", [?PORT, Epn]), +%% #coap_content{content_format = <<"text/plain">>, payload = <<", , , , ">>}, +%% [], +%% MsgId), +%% #coap_message{type = ack, method = Method} = test_recv_coap_response(UdpSock), +%% ?assertEqual({error,bad_request}, Method), +%% timer:sleep(50), +%% ?assertEqual([], test_mqtt_broker:get_subscrbied_topics()). case07_register_alternate_path_01(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -516,16 +532,16 @@ case07_register_alternate_path_01(Config) -> post, sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1", [?PORT, Epn]), #coap_content{content_format = <<"text/plain">>, - payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, + payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, [], MsgId), timer:sleep(50), true = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()). case07_register_alternate_path_02(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -538,16 +554,16 @@ case07_register_alternate_path_02(Config) -> post, sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1", [?PORT, Epn]), #coap_content{content_format = <<"text/plain">>, - payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, + payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, [], MsgId), timer:sleep(50), true = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()). case08_reregister(Config) -> - % ---------------------------------------- - % REGISTER command - % ---------------------------------------- + %%---------------------------------------- + %% REGISTER command + %%---------------------------------------- UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId = 12, @@ -560,24 +576,24 @@ case08_reregister(Config) -> post, sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1", [?PORT, Epn]), #coap_content{content_format = <<"text/plain">>, - payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, + payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, [], MsgId), timer:sleep(50), true = lists:member(SubTopic, test_mqtt_broker:get_subscrbied_topics()), ReadResult = emqx_json:encode( - #{ - <<"msgType">> => <<"register">>, - <<"data">> => #{ - <<"alternatePath">> => <<"/lwm2m">>, - <<"ep">> => list_to_binary(Epn), - <<"lt">> => 345, - <<"lwm2m">> => <<"1">>, - <<"objectList">> => [<<"/1/0">>, <<"/2/0">>, <<"/3/0">>] - } - } - ), + #{ + <<"msgType">> => <<"register">>, + <<"data">> => #{ + <<"alternatePath">> => <<"/lwm2m">>, + <<"ep">> => list_to_binary(Epn), + <<"lt">> => 345, + <<"lwm2m">> => <<"1">>, + <<"objectList">> => [<<"/1/0">>, <<"/2/0">>, <<"/3/0">>] + } + } + ), ?assertEqual(ReadResult, test_recv_mqtt_response(ReportTopic)), timer:sleep(1000), @@ -586,9 +602,10 @@ case08_reregister(Config) -> post, sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1", [?PORT, Epn]), #coap_content{content_format = <<"text/plain">>, - payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, + payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, [], MsgId + 1), + %% verify the lwm2m client is still online ?assertEqual(ReadResult, test_recv_mqtt_response(ReportTopic)). @@ -599,28 +616,28 @@ case10_read(Config) -> RespTopic = list_to_binary("lwm2m/"++Epn++"/up/resp"), emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), timer:sleep(200), - % step 1, device register ... + %% step 1, device register ... test_send_coap_request( UdpSock, post, sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1", [?PORT, Epn]), #coap_content{content_format = <<"text/plain">>, - payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, + payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, [], MsgId1), #coap_message{method = Method1} = test_recv_coap_response(UdpSock), ?assertEqual({ok,created}, Method1), test_recv_mqtt_response(RespTopic), - % step2, send a READ command to device + %% step2, send a READ command to device CmdId = 206, CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, Command = #{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"path">> => <<"/3/0/0">> - } - }, + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"path">> => <<"/3/0/0">> + } + }, CommandJson = emqx_json:encode(Command), ?LOGT("CommandJson=~p", [CommandJson]), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -638,17 +655,17 @@ case10_read(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"reqPath">> => <<"/3/0/0">>, - <<"content">> => [#{ - path => <<"/3/0/0">>, - value => <<"EMQ">> - }] - } - }), + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"reqPath">> => <<"/3/0/0">>, + <<"content">> => [#{ + path => <<"/3/0/0">>, + value => <<"EMQ">> + }] + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case10_read_separate_ack(Config) -> @@ -661,19 +678,19 @@ case10_read_separate_ack(Config) -> emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), timer:sleep(200), - % step 1, device register ... + %% step 1, device register ... std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a READ command to device + %% step2, send a READ command to device CmdId = 206, CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, Command = #{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"path">> => <<"/3/0/0">> - } - }, + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"path">> => <<"/3/0/0">> + } + }, CommandJson = emqx_json:encode(Command), ?LOGT("CommandJson=~p", [CommandJson]), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -688,12 +705,12 @@ case10_read_separate_ack(Config) -> test_send_empty_ack(UdpSock, "127.0.0.1", ?PORT, Request2), ReadResultACK = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"ack">>, - <<"data">> => #{ - <<"path">> => <<"/3/0/0">> - } - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"ack">>, + <<"data">> => #{ + <<"path">> => <<"/3/0/0">> + } + }), ?assertEqual(ReadResultACK, test_recv_mqtt_response(RespTopic)), timer:sleep(100), @@ -701,21 +718,21 @@ case10_read_separate_ack(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"reqPath">> => <<"/3/0/0">>, - <<"content">> => [#{ - path => <<"/3/0/0">>, - value => <<"EMQ">> - }] - } - }), + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"reqPath">> => <<"/3/0/0">>, + <<"content">> => [#{ + path => <<"/3/0/0">>, + value => <<"EMQ">> + }] + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case11_read_object_tlv(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -726,16 +743,16 @@ case11_read_object_tlv(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a READ command to device + %% step2, send a READ command to device CmdId = 207, CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, Command = #{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"path">> => <<"/3/0">> - } - }, + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"path">> => <<"/3/0">> + } + }, CommandJson = emqx_json:encode(Command), ?LOGT("CommandJson=~p", [CommandJson]), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -752,31 +769,31 @@ case11_read_object_tlv(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"reqPath">> => <<"/3/0">>, - <<"content">> => [ - #{ - path => <<"/3/0/0">>, - value => <<"Open Mobile Alliance">> - }, - #{ - path => <<"/3/0/1">>, - value => <<"Lightweight M2M Client">> - }, - #{ - path => <<"/3/0/2">>, - value => <<"345000123">> - } - ] - } - }), + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"reqPath">> => <<"/3/0">>, + <<"content">> => [ + #{ + path => <<"/3/0/0">>, + value => <<"Open Mobile Alliance">> + }, + #{ + path => <<"/3/0/1">>, + value => <<"Lightweight M2M Client">> + }, + #{ + path => <<"/3/0/2">>, + value => <<"345000123">> + } + ] + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case11_read_object_json(Config) -> - % step 1, device register ... + %% step 1, device register ... UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, @@ -788,16 +805,16 @@ case11_read_object_json(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a READ command to device + %% step2, send a READ command to device CmdId = 206, CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, Command = #{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"path">> => <<"/3/0">> - } - }, + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"path">> => <<"/3/0">> + } + }, CommandJson = emqx_json:encode(Command), ?LOGT("CommandJson=~p", [CommandJson]), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -814,31 +831,31 @@ case11_read_object_json(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"reqPath">> => <<"/3/0">>, - <<"content">> => [ - #{ - path => <<"/3/0/0">>, - value => <<"Open Mobile Alliance">> - }, - #{ - path => <<"/3/0/1">>, - value => <<"Lightweight M2M Client">> - }, - #{ - path => <<"/3/0/2">>, - value => <<"345000123">> - } - ] - } - }), + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"reqPath">> => <<"/3/0">>, + <<"content">> => [ + #{ + path => <<"/3/0/0">>, + value => <<"Open Mobile Alliance">> + }, + #{ + path => <<"/3/0/1">>, + value => <<"Lightweight M2M Client">> + }, + #{ + path => <<"/3/0/2">>, + value => <<"345000123">> + } + ] + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case12_read_resource_opaque(Config) -> - % step 1, device register ... + %% step 1, device register ... UdpSock = ?config(sock, Config), Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, @@ -849,16 +866,16 @@ case12_read_resource_opaque(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a READ command to device + %% step2, send a READ command to device CmdId = 206, CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, Command = #{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"path">> => <<"/3/0/8">> - } - }, + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"path">> => <<"/3/0/8">> + } + }, CommandJson = emqx_json:encode(Command), ?LOGT("CommandJson=~p", [CommandJson]), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -875,23 +892,23 @@ case12_read_resource_opaque(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"reqPath">> => <<"/3/0/8">>, - <<"content">> => [ - #{ - path => <<"/3/0/8">>, - value => base64:encode(Opaque) - } - ] - } - }), + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"reqPath">> => <<"/3/0/8">>, + <<"content">> => [ + #{ + path => <<"/3/0/8">>, + value => base64:encode(Opaque) + } + ] + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case13_read_no_xml(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -902,16 +919,16 @@ case13_read_no_xml(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a READ command to device + %% step2, send a READ command to device CmdId = 206, CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, Command = #{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"path">> => <<"/9723/0/0">> - } - }, + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"path">> => <<"/9723/0/0">> + } + }, CommandJson = emqx_json:encode(Command), ?LOGT("CommandJson=~p", [CommandJson]), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -927,17 +944,17 @@ case13_read_no_xml(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"reqPath">> => <<"/9723/0/0">>, - <<"code">> => <<"4.00">>, - <<"codeMsg">> => <<"bad_request">> - } - }), + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"reqPath">> => <<"/9723/0/0">>, + <<"code">> => <<"4.00">>, + <<"codeMsg">> => <<"bad_request">> + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case20_single_write(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -948,16 +965,16 @@ case20_single_write(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a WRITE command to device + %% step2, send a WRITE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"write">>, <<"data">> => #{ - <<"path">> => <<"/3/0/13">>, - <<"type">> => <<"Integer">>, - <<"value">> => <<"12345">> - } + <<"path">> => <<"/3/0/13">>, + <<"type">> => <<"Integer">>, + <<"value">> => <<"12345">> + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -975,18 +992,18 @@ case20_single_write(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/13">>, - <<"code">> => <<"2.04">>, - <<"codeMsg">> => <<"changed">> - }, - <<"msgType">> => <<"write">> - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/13">>, + <<"code">> => <<"2.04">>, + <<"codeMsg">> => <<"changed">> + }, + <<"msgType">> => <<"write">> + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case20_write(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -997,18 +1014,18 @@ case20_write(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a WRITE command to device + %% step2, send a WRITE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"write">>, <<"data">> => #{ - <<"basePath">> => <<"/3/0/13">>, - <<"content">> => [#{ - type => <<"Float">>, - value => <<"12345.0">> - }] - } + <<"basePath">> => <<"/3/0/13">>, + <<"content">> => [#{ + type => <<"Float">>, + value => <<"12345.0">> + }] + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -1026,18 +1043,18 @@ case20_write(Config) -> timer:sleep(100), WriteResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/13">>, - <<"code">> => <<"2.04">>, - <<"codeMsg">> => <<"changed">> - }, - <<"msgType">> => <<"write">> - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/13">>, + <<"code">> => <<"2.04">>, + <<"codeMsg">> => <<"changed">> + }, + <<"msgType">> => <<"write">> + }), ?assertEqual(WriteResult, test_recv_mqtt_response(RespTopic)). case21_write_object(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1048,23 +1065,23 @@ case21_write_object(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a WRITE command to device + %% step2, send a WRITE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"write">>, <<"data">> => #{ - <<"basePath">> => <<"/3/0/">>, - <<"content">> => [#{ - path => <<"13">>, - type => <<"Integer">>, - value => <<"12345">> - },#{ - path => <<"14">>, - type => <<"String">>, - value => <<"87x">> - }] - } + <<"basePath">> => <<"/3/0/">>, + <<"content">> => [#{ + path => <<"13">>, + type => <<"Integer">>, + value => <<"12345">> + },#{ + path => <<"14">>, + type => <<"String">>, + value => <<"87x">> + }] + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -1084,18 +1101,18 @@ case21_write_object(Config) -> ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"write">>, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/">>, - <<"code">> => <<"2.04">>, - <<"codeMsg">> => <<"changed">> - } - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"write">>, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/">>, + <<"code">> => <<"2.04">>, + <<"codeMsg">> => <<"changed">> + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case22_write_error(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1106,20 +1123,20 @@ case22_write_error(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a WRITE command to device + %% step2, send a WRITE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"write">>, <<"data">> => #{ - <<"basePath">> => <<"/3/0/1">>, - <<"content">> => [ - #{ - type => <<"Integer">>, - value => <<"12345">> - } - ] - } + <<"basePath">> => <<"/3/0/1">>, + <<"content">> => [ + #{ + type => <<"Integer">>, + value => <<"12345">> + } + ] + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -1135,18 +1152,18 @@ case22_write_error(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/1">>, - <<"code">> => <<"4.00">>, - <<"codeMsg">> => <<"bad_request">> - }, - <<"msgType">> => <<"write">> - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/1">>, + <<"code">> => <<"4.00">>, + <<"codeMsg">> => <<"bad_request">> + }, + <<"msgType">> => <<"write">> + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case_create_basic(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1157,15 +1174,14 @@ case_create_basic(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a CREATE command to device + %% step2, send a CREATE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, - Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"create">>, - <<"data">> => #{ - <<"path">> => <<"/5">> - } - }, + Command = #{<<"msgType">> => <<"create">>, + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{<<"content">> => [], + <<"basePath">> => <<"/5">> + }}, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), timer:sleep(50), @@ -1181,18 +1197,18 @@ case_create_basic(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/5">>, - <<"code">> => <<"2.01">>, - <<"codeMsg">> => <<"created">> - }, - <<"msgType">> => <<"create">> - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{ + <<"reqPath">> => <<"/5">>, + <<"code">> => <<"2.01">>, + <<"codeMsg">> => <<"created">> + }, + <<"msgType">> => <<"create">> + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case_delete_basic(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1203,14 +1219,14 @@ case_delete_basic(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a CREATE command to device + %% step2, send a CREATE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"delete">>, <<"data">> => #{ - <<"path">> => <<"/5/0">> - } + <<"path">> => <<"/5/0">> + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -1227,18 +1243,18 @@ case_delete_basic(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/5/0">>, - <<"code">> => <<"2.02">>, - <<"codeMsg">> => <<"deleted">> - }, - <<"msgType">> => <<"delete">> - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{ + <<"reqPath">> => <<"/5/0">>, + <<"code">> => <<"2.02">>, + <<"codeMsg">> => <<"deleted">> + }, + <<"msgType">> => <<"delete">> + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case30_execute(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1249,16 +1265,16 @@ case30_execute(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a WRITE command to device + %% step2, send a WRITE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"execute">>, <<"data">> => #{ - <<"path">> => <<"/3/0/4">>, - %% "args" should not be present for "/3/0/4", only for testing the encoding here - <<"args">> => <<"2,7">> - } + <<"path">> => <<"/3/0/4">>, + %% "args" should not be present for "/3/0/4", only for testing the encoding here + <<"args">> => <<"2,7">> + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -1275,18 +1291,18 @@ case30_execute(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/4">>, - <<"code">> => <<"2.04">>, - <<"codeMsg">> => <<"changed">> - }, - <<"msgType">> => <<"execute">> - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/4">>, + <<"code">> => <<"2.04">>, + <<"codeMsg">> => <<"changed">> + }, + <<"msgType">> => <<"execute">> + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case31_execute_error(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1297,15 +1313,15 @@ case31_execute_error(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a WRITE command to device + %% step2, send a WRITE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"execute">>, <<"data">> => #{ - <<"path">> => <<"/3/0/4">>, - <<"args">> => <<"2,7">> - } + <<"path">> => <<"/3/0/4">>, + <<"args">> => <<"2,7">> + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), @@ -1322,18 +1338,18 @@ case31_execute_error(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/4">>, - <<"code">> => <<"4.01">>, - <<"codeMsg">> => <<"uauthorized">> - }, - <<"msgType">> => <<"execute">> - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/4">>, + <<"code">> => <<"4.01">>, + <<"codeMsg">> => <<"unauthorized">> + }, + <<"msgType">> => <<"execute">> + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case40_discover(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1344,14 +1360,14 @@ case40_discover(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a WRITE command to device + %% step2, send a WRITE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"discover">>, <<"data">> => #{ - <<"path">> => <<"/3/0/7">> - } }, + <<"path">> => <<"/3/0/7">> + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), timer:sleep(50), @@ -1374,20 +1390,20 @@ case40_discover(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"discover">>, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/7">>, - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"content">> => - [<<";dim=8;pmin=10;pmax=60;gt=50;lt=42.2">>, <<"">>] - } - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"discover">>, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/7">>, + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"content">> => + [<<";dim=8;pmin=10;pmax=60;gt=50;lt=42.2">>, <<"">>] + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case50_write_attribute(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1398,17 +1414,17 @@ case50_write_attribute(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a WRITE command to device + %% step2, send a WRITE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"write-attr">>, <<"data">> => #{ - <<"path">> => <<"/3/0/9">>, - <<"pmin">> => <<"1">>, - <<"pmax">> => <<"5">>, - <<"lt">> => <<"5">> - } }, + <<"path">> => <<"/3/0/9">>, + <<"pmin">> => <<"1">>, + <<"pmax">> => <<"5">>, + <<"lt">> => <<"5">> + } }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), timer:sleep(100), @@ -1433,18 +1449,18 @@ case50_write_attribute(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/9">>, - <<"code">> => <<"2.04">>, - <<"codeMsg">> => <<"changed">> - }, - <<"msgType">> => <<"write-attr">> - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/9">>, + <<"code">> => <<"2.04">>, + <<"codeMsg">> => <<"changed">> + }, + <<"msgType">> => <<"write-attr">> + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case60_observe(Config) -> - % step 1, device register ... + %% step 1, device register ... Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, UdpSock = ?config(sock, Config), @@ -1457,15 +1473,15 @@ case60_observe(Config) -> std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), - % step2, send a OBSERVE command to device + %% step2, send a OBSERVE command to device CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, CmdId = 307, Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"observe">>, <<"data">> => #{ - <<"path">> => <<"/3/0/10">> - } - }, + <<"path">> => <<"/3/0/10">> + } + }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), timer:sleep(50), @@ -1488,18 +1504,18 @@ case60_observe(Config) -> timer:sleep(100), ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"observe">>, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/10">>, - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"content">> => [#{ - path => <<"/3/0/10">>, - value => 2048 - }] - } - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"observe">>, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/10">>, + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"content">> => [#{ + path => <<"/3/0/10">>, + value => 2048 + }] + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)), %% step3 the notifications @@ -1515,29 +1531,29 @@ case60_observe(Config) -> #coap_message{} = test_recv_coap_response(UdpSock), ReadResult2 = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"notify">>, - <<"seqNum">> => ObSeq, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/10">>, - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"content">> => [#{ - path => <<"/3/0/10">>, - value => 4096 - }] - } - }), + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"notify">>, + <<"seqNum">> => ObSeq, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/10">>, + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"content">> => [#{ + path => <<"/3/0/10">>, + value => 4096 + }] + } + }), ?assertEqual(ReadResult2, test_recv_mqtt_response(RespTopicAD)), %% Step3. cancel observe CmdId3 = 308, Command3 = #{<<"requestID">> => CmdId3, <<"cacheID">> => CmdId3, - <<"msgType">> => <<"cancel-observe">>, - <<"data">> => #{ - <<"path">> => <<"/3/0/10">> - } - }, + <<"msgType">> => <<"cancel-observe">>, + <<"data">> => #{ + <<"path">> => <<"/3/0/10">> + } + }, CommandJson3 = emqx_json:encode(Command3), test_mqtt_broker:publish(CommandTopic, CommandJson3, 0), timer:sleep(50), @@ -1560,143 +1576,143 @@ case60_observe(Config) -> timer:sleep(100), ReadResult3 = emqx_json:encode(#{ - <<"requestID">> => CmdId3, <<"cacheID">> => CmdId3, - <<"msgType">> => <<"cancel-observe">>, - <<"data">> => #{ - <<"reqPath">> => <<"/3/0/10">>, - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"content">> => [#{ - path => <<"/3/0/10">>, - value => 1150 - }] - } - }), + <<"requestID">> => CmdId3, <<"cacheID">> => CmdId3, + <<"msgType">> => <<"cancel-observe">>, + <<"data">> => #{ + <<"reqPath">> => <<"/3/0/10">>, + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"content">> => [#{ + path => <<"/3/0/10">>, + value => 1150 + }] + } + }), ?assertEqual(ReadResult3, test_recv_mqtt_response(RespTopic)). -case80_specail_object_19_0_0_notify(Config) -> - % step 1, device register, with extra register options - Epn = "urn:oma:lwm2m:oma:3", - RegOptionWangYi = "&apn=psmA.eDRX0.ctnb&im=13456&ct=2.0&mt=MDM9206&mv=4.0", - MsgId1 = 15, - UdpSock = ?config(sock, Config), +%% case80_specail_object_19_0_0_notify(Config) -> +%% %% step 1, device register, with extra register options +%% Epn = "urn:oma:lwm2m:oma:3", +%% RegOptionWangYi = "&apn=psmA.eDRX0.ctnb&im=13456&ct=2.0&mt=MDM9206&mv=4.0", +%% MsgId1 = 15, +%% UdpSock = ?config(sock, Config), - RespTopic = list_to_binary("lwm2m/"++Epn++"/up/resp"), - emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), - timer:sleep(200), +%% RespTopic = list_to_binary("lwm2m/"++Epn++"/up/resp"), +%% emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), +%% timer:sleep(200), - test_send_coap_request( UdpSock, - post, - sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1"++RegOptionWangYi, [?PORT, Epn]), - #coap_content{content_format = <<"text/plain">>, payload = <<", , , , ">>}, - [], - MsgId1), - #coap_message{method = Method1} = test_recv_coap_response(UdpSock), - ?assertEqual({ok,created}, Method1), - ReadResult = emqx_json:encode(#{ - <<"msgType">> => <<"register">>, - <<"data">> => #{ - <<"alternatePath">> => <<"/">>, - <<"ep">> => list_to_binary(Epn), - <<"lt">> => 345, - <<"lwm2m">> => <<"1">>, - <<"objectList">> => [<<"/1">>, <<"/2">>, <<"/3">>, <<"/4">>, <<"/5">>], - <<"apn">> => <<"psmA.eDRX0.ctnb">>, - <<"im">> => <<"13456">>, - <<"ct">> => <<"2.0">>, - <<"mt">> => <<"MDM9206">>, - <<"mv">> => <<"4.0">> - } - }), - ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)), +%% test_send_coap_request( UdpSock, +%% post, +%% sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1"++RegOptionWangYi, [?PORT, Epn]), +%% #coap_content{content_format = <<"text/plain">>, payload = <<", , , , ">>}, +%% [], +%% MsgId1), +%% #coap_message{method = Method1} = test_recv_coap_response(UdpSock), +%% ?assertEqual({ok,created}, Method1), +%% ReadResult = emqx_json:encode(#{ +%% <<"msgType">> => <<"register">>, +%% <<"data">> => #{ +%% <<"alternatePath">> => <<"/">>, +%% <<"ep">> => list_to_binary(Epn), +%% <<"lt">> => 345, +%% <<"lwm2m">> => <<"1">>, +%% <<"objectList">> => [<<"/1">>, <<"/2">>, <<"/3">>, <<"/4">>, <<"/5">>], +%% <<"apn">> => <<"psmA.eDRX0.ctnb">>, +%% <<"im">> => <<"13456">>, +%% <<"ct">> => <<"2.0">>, +%% <<"mt">> => <<"MDM9206">>, +%% <<"mv">> => <<"4.0">> +%% } +%% }), +%% ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)), - % step2, send a OBSERVE command to device - CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, - CmdId = 307, - Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"observe">>, - <<"data">> => #{ - <<"path">> => <<"/19/0/0">> - } - }, - CommandJson = emqx_json:encode(Command), - test_mqtt_broker:publish(CommandTopic, CommandJson, 0), - timer:sleep(50), - Request2 = test_recv_coap_request(UdpSock), - #coap_message{method = Method2, options=Options2, payload=Payload2} = Request2, - Path2 = get_coap_path(Options2), - Observe = get_coap_observe(Options2), - ?assertEqual(get, Method2), - ?assertEqual(<<"/19/0/0">>, Path2), - ?assertEqual(Observe, 0), - ?assertEqual(<<>>, Payload2), - timer:sleep(50), +%% %% step2, send a OBSERVE command to device +%% CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, +%% CmdId = 307, +%% Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, +%% <<"msgType">> => <<"observe">>, +%% <<"data">> => #{ +%% <<"path">> => <<"/19/0/0">> +%% } +%% }, +%% CommandJson = emqx_json:encode(Command), +%% test_mqtt_broker:publish(CommandTopic, CommandJson, 0), +%% timer:sleep(50), +%% Request2 = test_recv_coap_request(UdpSock), +%% #coap_message{method = Method2, options=Options2, payload=Payload2} = Request2, +%% Path2 = get_coap_path(Options2), +%% Observe = get_coap_observe(Options2), +%% ?assertEqual(get, Method2), +%% ?assertEqual(<<"/19/0/0">>, Path2), +%% ?assertEqual(Observe, 0), +%% ?assertEqual(<<>>, Payload2), +%% timer:sleep(50), - test_send_coap_observe_ack( UdpSock, - "127.0.0.1", - ?PORT, - {ok, content}, - #coap_content{content_format = <<"text/plain">>, payload = <<"2048">>}, - Request2), - timer:sleep(100). +%% test_send_coap_observe_ack( UdpSock, +%% "127.0.0.1", +%% ?PORT, +%% {ok, content}, +%% #coap_content{content_format = <<"text/plain">>, payload = <<"2048">>}, +%% Request2), +%% timer:sleep(100). - %% step 3, device send uplink data notifications +%% step 3, device send uplink data notifications -case80_specail_object_19_1_0_write(Config) -> - Epn = "urn:oma:lwm2m:oma:3", - RegOptionWangYi = "&apn=psmA.eDRX0.ctnb&im=13456&ct=2.0&mt=MDM9206&mv=4.0", - MsgId1 = 15, - UdpSock = ?config(sock, Config), - RespTopic = list_to_binary("lwm2m/"++Epn++"/up/resp"), - emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), - timer:sleep(200), +%% case80_specail_object_19_1_0_write(Config) -> +%% Epn = "urn:oma:lwm2m:oma:3", +%% RegOptionWangYi = "&apn=psmA.eDRX0.ctnb&im=13456&ct=2.0&mt=MDM9206&mv=4.0", +%% MsgId1 = 15, +%% UdpSock = ?config(sock, Config), +%% RespTopic = list_to_binary("lwm2m/"++Epn++"/up/resp"), +%% emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), +%% timer:sleep(200), - test_send_coap_request( UdpSock, - post, - sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1"++RegOptionWangYi, [?PORT, Epn]), - #coap_content{content_format = <<"text/plain">>, payload = <<", , , , ">>}, - [], - MsgId1), - #coap_message{method = Method1} = test_recv_coap_response(UdpSock), - ?assertEqual({ok,created}, Method1), - test_recv_mqtt_response(RespTopic), +%% test_send_coap_request( UdpSock, +%% post, +%% sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1"++RegOptionWangYi, [?PORT, Epn]), +%% #coap_content{content_format = <<"text/plain">>, payload = <<", , , , ">>}, +%% [], +%% MsgId1), +%% #coap_message{method = Method1} = test_recv_coap_response(UdpSock), +%% ?assertEqual({ok,created}, Method1), +%% test_recv_mqtt_response(RespTopic), - % step2, send a WRITE command to device - CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, - CmdId = 307, - Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"write">>, - <<"data">> => #{ - <<"path">> => <<"/19/1/0">>, - <<"type">> => <<"Opaque">>, - <<"value">> => base64:encode(<<12345:32>>) - } - }, +%% %% step2, send a WRITE command to device +%% CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, +%% CmdId = 307, +%% Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, +%% <<"msgType">> => <<"write">>, +%% <<"data">> => #{ +%% <<"path">> => <<"/19/1/0">>, +%% <<"type">> => <<"Opaque">>, +%% <<"value">> => base64:encode(<<12345:32>>) +%% } +%% }, - CommandJson = emqx_json:encode(Command), - test_mqtt_broker:publish(CommandTopic, CommandJson, 0), - timer:sleep(50), - Request2 = test_recv_coap_request(UdpSock), - #coap_message{method = Method2, options=Options2, payload=Payload2} = Request2, - Path2 = get_coap_path(Options2), - ?assertEqual(put, Method2), - ?assertEqual(<<"/19/1/0">>, Path2), - ?assertEqual(<<3:2, 0:1, 0:2, 4:3, 0, 12345:32>>, Payload2), - timer:sleep(50), +%% CommandJson = emqx_json:encode(Command), +%% test_mqtt_broker:publish(CommandTopic, CommandJson, 0), +%% timer:sleep(50), +%% Request2 = test_recv_coap_request(UdpSock), +%% #coap_message{method = Method2, options=Options2, payload=Payload2} = Request2, +%% Path2 = get_coap_path(Options2), +%% ?assertEqual(put, Method2), +%% ?assertEqual(<<"/19/1/0">>, Path2), +%% ?assertEqual(<<3:2, 0:1, 0:2, 4:3, 0, 12345:32>>, Payload2), +%% timer:sleep(50), - test_send_coap_response(UdpSock, "127.0.0.1", ?PORT, {ok, changed}, #coap_content{}, Request2, true), - timer:sleep(100), +%% test_send_coap_response(UdpSock, "127.0.0.1", ?PORT, {ok, changed}, #coap_content{}, Request2, true), +%% timer:sleep(100), - ReadResult = emqx_json:encode(#{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"data">> => #{ - <<"reqPath">> => <<"/19/1/0">>, - <<"code">> => <<"2.04">>, - <<"codeMsg">> => <<"changed">> - }, - <<"msgType">> => <<"write">> - }), - ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). +%% ReadResult = emqx_json:encode(#{ +%% <<"requestID">> => CmdId, <<"cacheID">> => CmdId, +%% <<"data">> => #{ +%% <<"reqPath">> => <<"/19/1/0">>, +%% <<"code">> => <<"2.04">>, +%% <<"codeMsg">> => <<"changed">> +%% }, +%% <<"msgType">> => <<"write">> +%% }), +%% ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). case90_psm_mode(Config) -> server_cache_mode(Config, "ep=~s<=345&lwm2m=1&apn=psmA.eDRX0.ctnb"). @@ -1705,9 +1721,10 @@ case90_queue_mode(Config) -> server_cache_mode(Config, "ep=~s<=345&lwm2m=1&b=UQ"). server_cache_mode(Config, RegOption) -> - application:set_env(?APP, qmode_time_window, 2), - - % step 1, device register, with apn indicates "PSM" mode + #{lwm2m := LwM2M} = Gateway = emqx:get_config([gateway]), + Gateway2 = Gateway#{lwm2m := LwM2M#{qmode_time_window => 2}}, + emqx_config:put([gateway], Gateway2), + %% step 1, device register, with apn indicates "PSM" mode Epn = "urn:oma:lwm2m:oma:3", MsgId1 = 15, @@ -1756,12 +1773,12 @@ send_read_command_1(CmdId, _UdpSock) -> Epn = "urn:oma:lwm2m:oma:3", CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, Command = #{ - <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"path">> => <<"/3/0/0">> - } - }, + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"path">> => <<"/3/0/0">> + } + }, CommandJson = emqx_json:encode(Command), test_mqtt_broker:publish(CommandTopic, CommandJson, 0), timer:sleep(50). @@ -1778,16 +1795,16 @@ verify_read_response_1(CmdId, UdpSock) -> test_send_coap_response(UdpSock, "127.0.0.1", ?PORT, {ok, content}, #coap_content{content_format = <<"text/plain">>, payload = <<"EMQ">>}, Request, true), ReadResult = emqx_json:encode(#{ <<"requestID">> => CmdId, <<"cacheID">> => CmdId, - <<"msgType">> => <<"read">>, - <<"data">> => #{ - <<"code">> => <<"2.05">>, - <<"codeMsg">> => <<"content">>, - <<"content">> => [#{ - path => <<"/3/0/0">>, - value => <<"EMQ">> - }] - } - }), + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"code">> => <<"2.05">>, + <<"codeMsg">> => <<"content">>, + <<"content">> => [#{ + path => <<"/3/0/0">>, + value => <<"EMQ">> + }] + } + }), ?assertEqual(ReadResult, test_recv_mqtt_response(RespTopic)). device_update_1(UdpSock, Location) -> From cfe4e37d50e9c19e1fc16b0825efb595578387db Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Wed, 1 Sep 2021 09:02:47 +0800 Subject: [PATCH 019/109] fix: retainer api doc qos enum (#5614) --- apps/emqx_retainer/src/emqx_retainer_api.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx_retainer/src/emqx_retainer_api.erl b/apps/emqx_retainer/src/emqx_retainer_api.erl index 503827d4b..7315d5a63 100644 --- a/apps/emqx_retainer/src/emqx_retainer_api.erl +++ b/apps/emqx_retainer/src/emqx_retainer_api.erl @@ -44,7 +44,7 @@ message_props() -> properties([ {id, string, <<"Message ID">>}, {topic, string, <<"MQTT Topic">>}, - {qos, string, <<"MQTT QoS">>}, + {qos, integer, <<"MQTT QoS">>, [0, 1, 2]}, {payload, string, <<"MQTT Payload">>}, {publish_at, string, <<"Publish datetime, in RFC 3339 format">>}, {from_clientid, string, <<"Publisher ClientId">>}, From ef1b6176248d31fc014c20678230ed2b3be1d411 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Tue, 31 Aug 2021 16:43:03 +0800 Subject: [PATCH 020/109] feat(authz api): support '/authorization/settings' api and update swagger schema Signed-off-by: zhanghongtong --- apps/emqx_authz/src/emqx_authz_api_schema.erl | 179 ++++++---------- .../src/emqx_authz_api_settings.erl | 61 ++++++ ...thz_api.erl => emqx_authz_api_sources.erl} | 197 +++--------------- .../test/emqx_authz_api_settings_SUITE.erl | 135 ++++++++++++ ...E.erl => emqx_authz_api_sources_SUITE.erl} | 2 +- 5 files changed, 298 insertions(+), 276 deletions(-) create mode 100644 apps/emqx_authz/src/emqx_authz_api_settings.erl rename apps/emqx_authz/src/{emqx_authz_api.erl => emqx_authz_api_sources.erl} (66%) create mode 100644 apps/emqx_authz/test/emqx_authz_api_settings_SUITE.erl rename apps/emqx_authz/test/{emqx_authz_api_SUITE.erl => emqx_authz_api_sources_SUITE.erl} (99%) diff --git a/apps/emqx_authz/src/emqx_authz_api_schema.erl b/apps/emqx_authz/src/emqx_authz_api_schema.erl index 1bc316986..4c17cd0b6 100644 --- a/apps/emqx_authz/src/emqx_authz_api_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_api_schema.erl @@ -19,17 +19,17 @@ -export([definitions/0]). definitions() -> - RetruenedRules = #{ + RetruenedSources = #{ allOf => [ #{type => object, properties => #{ annotations => #{ type => object, - required => [id], + required => [status], properties => #{ - id => #{ - type => string - }, - principal => minirest:ref(<<"principal">>) + status => #{ + type => string, + example => <<"healthy">> + } } } } @@ -37,119 +37,76 @@ definitions() -> , minirest:ref(<<"sources">>) ] }, - Rules = #{ - oneOf => [ minirest:ref(<<"simple_source">>) - % , minirest:ref(<<"connector_redis">>) + Sources = #{ + oneOf => [ minirest:ref(<<"connector_redis">>) ] }, - % ConnectorRedis = #{ - % type => object, - % required => [principal, type, enable, config, cmd] - % properties => #{ - % principal => minirest:ref(<<"principal">>), - % type => #{ - % type => string, - % enum => [<<"redis">>], - % example => <<"redis">> - % }, - % enable => #{ - % type => boolean, - % example => true - % } - % config => #{ - % type => - % } - % } - % } - SimpleRule = #{ + ConnectorRedis= #{ type => object, - required => [principal, permission, action, topics], + required => [type, enable, config, cmd], properties => #{ - action => #{ + type => #{ type => string, - enum => [<<"publish">>, <<"subscribe">>, <<"all">>], - example => <<"publish">> + enum => [<<"redis">>], + example => <<"redis">> }, - permission => #{ + enable => #{ + type => boolean, + example => true + }, + config => #{ + oneOf => [ #{type => object, + required => [server, redis_type, pool_size, auto_reconnect], + properties => #{ + server => #{type => string, example => <<"127.0.0.1:3306">>}, + redis_type => #{type => string, + enum => [<<"single">>], + example => <<"single">>}, + pool_size => #{type => integer}, + auto_reconnect => #{type => boolean, example => true}, + password => #{type => string}, + database => #{type => string, example => mqtt} + } + } + , #{type => object, + required => [servers, redis_type, sentinel, pool_size, auto_reconnect], + properties => #{ + servers => #{type => array, + items => #{type => string,example => <<"127.0.0.1:3306">>}}, + redis_type => #{type => string, + enum => [<<"sentinel">>], + example => <<"sentinel">>}, + sentinel => #{type => string}, + pool_size => #{type => integer}, + auto_reconnect => #{type => boolean, example => true}, + password => #{type => string}, + database => #{type => string, example => mqtt} + } + } + , #{type => object, + required => [servers, redis_type, pool_size, auto_reconnect], + properties => #{ + servers => #{type => array, + items => #{type => string, example => <<"127.0.0.1:3306">>}}, + redis_type => #{type => string, + enum => [<<"cluster">>], + example => <<"cluster">>}, + pool_size => #{type => integer}, + auto_reconnect => #{type => boolean, example => true}, + password => #{type => string}, + database => #{type => string, example => mqtt} + } + } + ], + type => object + }, + cmd => #{ type => string, - enum => [<<"allow">>, <<"deny">>], - example => <<"allow">> - }, - topics => #{ - type => array, - items => #{ - oneOf => [ #{type => string, example => <<"#">>} - , #{type => object, - required => [eq], - properties => #{ - eq => #{type => string} - }, - example => #{eq => <<"#">>} - } - ] - } - }, - principal => minirest:ref(<<"principal">>) - } - }, - Principal = #{ - oneOf => [ minirest:ref(<<"principal_username">>) - , minirest:ref(<<"principal_clientid">>) - , minirest:ref(<<"principal_ipaddress">>) - , #{type => string, enum=>[<<"all">>], example => <<"all">>} - , #{type => object, - required => ['and'], - properties => #{'and' => #{type => array, - items => #{oneOf => [ minirest:ref(<<"principal_username">>) - , minirest:ref(<<"principal_clientid">>) - , minirest:ref(<<"principal_ipaddress">>) - ]}}}, - example => #{'and' => [#{username => <<"emqx">>}, #{clientid => <<"emqx">>}]} - } - , #{type => object, - required => ['or'], - properties => #{'and' => #{type => array, - items => #{oneOf => [ minirest:ref(<<"principal_username">>) - , minirest:ref(<<"principal_clientid">>) - , minirest:ref(<<"principal_ipaddress">>) - ]}}}, - example => #{'or' => [#{username => <<"emqx">>}, #{clientid => <<"emqx">>}]} - } - ] - }, - PrincipalUsername = #{type => object, - required => [username], - properties => #{username => #{type => string}}, - example => #{username => <<"emqx">>} - }, - PrincipalClientid = #{type => object, - required => [clientid], - properties => #{clientid => #{type => string}}, - example => #{clientid => <<"emqx">>} - }, - PrincipalIpaddress = #{type => object, - required => [ipaddress], - properties => #{ipaddress => #{type => string}}, - example => #{ipaddress => <<"127.0.0.1">>} - }, - ErrorDef = #{ - type => object, - properties => #{ - code => #{ - type => string, - example => <<"BAD_REQUEST">> - }, - message => #{ - type => string + example => <<"HGETALL mqtt_authz">> } } }, - [ #{<<"returned_sources">> => RetruenedRules} - , #{<<"sources">> => Rules} - , #{<<"simple_source">> => SimpleRule} - , #{<<"principal">> => Principal} - , #{<<"principal_username">> => PrincipalUsername} - , #{<<"principal_clientid">> => PrincipalClientid} - , #{<<"principal_ipaddress">> => PrincipalIpaddress} - , #{<<"error">> => ErrorDef} + [ #{<<"returned_sources">> => RetruenedSources} + , #{<<"sources">> => Sources} + , #{<<"connector_redis">> => ConnectorRedis} ]. diff --git a/apps/emqx_authz/src/emqx_authz_api_settings.erl b/apps/emqx_authz/src/emqx_authz_api_settings.erl new file mode 100644 index 000000000..ac48fafd9 --- /dev/null +++ b/apps/emqx_authz/src/emqx_authz_api_settings.erl @@ -0,0 +1,61 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authz_api_settings). + +-behavior(minirest_api). + +-export([ api_spec/0 + , settings/2 + ]). + +api_spec() -> + {[settings_api()], []}. + +authorization_settings() -> + maps:remove(<<"sources">>, emqx:get_raw_config([authorization], #{})). + +conf_schema() -> + emqx_mgmt_api_configs:gen_schema(authorization_settings()). + +settings_api() -> + Metadata = #{ + get => #{ + description => "Get authorization settings", + responses => #{<<"200">> => emqx_mgmt_util:schema(conf_schema())} + }, + put => #{ + description => "Update authorization settings", + requestBody => emqx_mgmt_util:schema(conf_schema()), + responses => #{ + <<"200">> => emqx_mgmt_util:schema(conf_schema()), + <<"400">> => emqx_mgmt_util:bad_request() + } + } + }, + {"/authorization/settings", Metadata, settings}. + +settings(get, _Params) -> + {200, authorization_settings()}; + +settings(put, #{body := #{<<"no_match">> := NoMatch, + <<"deny_action">> := DenyAction, + <<"cache">> := Cache}}) -> + {ok, _} = emqx:update_config([authorization, no_match], NoMatch), + {ok, _} = emqx:update_config([authorization, deny_action], DenyAction), + {ok, _} = emqx:update_config([authorization, cache], Cache), + ok = emqx_authz_cache:drain_cache(), + {200, authorization_settings()}. diff --git a/apps/emqx_authz/src/emqx_authz_api.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl similarity index 66% rename from apps/emqx_authz/src/emqx_authz_api.erl rename to apps/emqx_authz/src/emqx_authz_api_sources.erl index dc8694c3f..2ad5db1da 100644 --- a/apps/emqx_authz/src/emqx_authz_api.erl +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -14,31 +14,29 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_authz_api). +-module(emqx_authz_api_sources). -behavior(minirest_api). -include("emqx_authz.hrl"). --define(EXAMPLE_RETURNED_RULE1, - #{principal => <<"all">>, - permission => <<"allow">>, - action => <<"all">>, - topics => [<<"#">>], - annotations => #{id => 1} - }). - +-define(EXAMPLE_REDIS, + #{type=> redis, + config => #{server => <<"127.0.0.1:3306">>, + redis_type => single, + pool_size => 1, + auto_reconnect => true + }, + cmd => <<"HGETALL mqtt_authz">>}). +-define(EXAMPLE_RETURNED_REDIS, + maps:put(annotations, #{status => healthy}, ?EXAMPLE_REDIS) + ). -define(EXAMPLE_RETURNED_RULES, - #{sources => [?EXAMPLE_RETURNED_RULE1 - ] + #{sources => [?EXAMPLE_RETURNED_REDIS + ] }). --define(EXAMPLE_RULE1, #{principal => <<"all">>, - permission => <<"allow">>, - action => <<"all">>, - topics => [<<"#">>]}). - -export([ api_spec/0 , sources/2 , source/2 @@ -107,9 +105,9 @@ sources_api() -> 'application/json' => #{ schema => minirest:ref(<<"sources">>), examples => #{ - simple_source => #{ - summary => <<"Sources">>, - value => jsx:encode(?EXAMPLE_RULE1) + redis => #{ + summary => <<"Redis">>, + value => jsx:encode(?EXAMPLE_REDIS) } } } @@ -117,23 +115,7 @@ sources_api() -> }, responses => #{ <<"204">> => #{description => <<"Created">>}, - <<"400">> => #{ - description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), - examples => #{ - example1 => #{ - summary => <<"Bad Request">>, - value => #{ - code => <<"BAD_REQUEST">>, - message => <<"Bad Request">> - } - } - } - } - } - } + <<"400">> => emqx_mgmt_util:bad_request() } }, put => #{ @@ -146,9 +128,9 @@ sources_api() -> items => minirest:ref(<<"returned_sources">>) }, examples => #{ - sources => #{ - summary => <<"Sources">>, - value => jsx:encode([?EXAMPLE_RULE1]) + redis => #{ + summary => <<"Redis">>, + value => jsx:encode([?EXAMPLE_REDIS]) } } } @@ -156,23 +138,7 @@ sources_api() -> }, responses => #{ <<"204">> => #{description => <<"Created">>}, - <<"400">> => #{ - description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), - examples => #{ - example1 => #{ - summary => <<"Bad Request">>, - value => #{ - code => <<"BAD_REQUEST">>, - message => <<"Bad Request">> - } - } - } - } - } - } + <<"400">> => emqx_mgmt_util:bad_request() } } }, @@ -201,29 +167,13 @@ source_api() -> examples => #{ sources => #{ summary => <<"Sources">>, - value => jsx:encode(?EXAMPLE_RETURNED_RULE1) + value => jsx:encode(?EXAMPLE_RETURNED_REDIS) } } } } }, - <<"404">> => #{ - description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), - examples => #{ - example1 => #{ - summary => <<"Not Found">>, - value => #{ - code => <<"NOT_FOUND">>, - message => <<"source xxx not found">> - } - } - } - } - } - } + <<"404">> => emqx_mgmt_util:bad_request(<<"Not Found">>) } }, put => #{ @@ -243,9 +193,9 @@ source_api() -> 'application/json' => #{ schema => minirest:ref(<<"sources">>), examples => #{ - simple_source => #{ - summary => <<"Sources">>, - value => jsx:encode(?EXAMPLE_RULE1) + redis => #{ + summary => <<"Redis">>, + value => jsx:encode(?EXAMPLE_REDIS) } } } @@ -253,47 +203,15 @@ source_api() -> }, responses => #{ <<"204">> => #{description => <<"No Content">>}, - <<"404">> => #{ - description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), - examples => #{ - example1 => #{ - summary => <<"Not Found">>, - value => #{ - code => <<"NOT_FOUND">>, - message => <<"source xxx not found">> - } - } - } - } - } - }, - <<"400">> => #{ - description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), - examples => #{ - example1 => #{ - summary => <<"Bad Request">>, - value => #{ - code => <<"BAD_REQUEST">>, - message => <<"Bad Request">> - } - } - } - } - } - } + <<"404">> => emqx_mgmt_util:bad_request(<<"Not Found">>), + <<"400">> => emqx_mgmt_util:bad_request() } }, delete => #{ description => "Delete source", parameters => [ #{ - name => id, + name => type, in => path, schema => #{ type => string @@ -303,23 +221,7 @@ source_api() -> ], responses => #{ <<"204">> => #{description => <<"No Content">>}, - <<"400">> => #{ - description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), - examples => #{ - example1 => #{ - summary => <<"Bad Request">>, - value => #{ - code => <<"BAD_REQUEST">>, - message => <<"Bad Request">> - } - } - } - } - } - } + <<"400">> => emqx_mgmt_util:bad_request() } } }, @@ -378,38 +280,8 @@ move_source_api() -> <<"204">> => #{ description => <<"No Content">> }, - <<"404">> => #{ - description => <<"Bad Request">>, - content => #{ 'application/json' => #{ schema => minirest:ref(<<"error">>), - examples => #{ - example1 => #{ - summary => <<"Not Found">>, - value => #{ - code => <<"NOT_FOUND">>, - message => <<"source xxx not found">> - } - } - } - } - } - }, - <<"400">> => #{ - description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), - examples => #{ - example1 => #{ - summary => <<"Bad Request">>, - value => #{ - code => <<"BAD_REQUEST">>, - message => <<"Bad Request">> - } - } - } - } - } - } + <<"404">> => emqx_mgmt_util:bad_request(<<"Not Found">>), + <<"400">> => emqx_mgmt_util:bad_request() } } }, @@ -519,6 +391,3 @@ move_source(post, #{bindings := #{type := Type}, body := #{<<"position">> := Pos {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} end. - - - diff --git a/apps/emqx_authz/test/emqx_authz_api_settings_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_settings_SUITE.erl new file mode 100644 index 000000000..1db9fff2b --- /dev/null +++ b/apps/emqx_authz/test/emqx_authz_api_settings_SUITE.erl @@ -0,0 +1,135 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authz_api_settings_SUITE). + +-compile(nowarn_export_all). +-compile(export_all). + +-include("emqx_authz.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-define(CONF_DEFAULT, <<"authorization: {sources: []}">>). + +-import(emqx_ct_http, [ request_api/3 + , request_api/5 + , get_http_data/1 + , create_default_app/0 + , delete_default_app/0 + , default_auth_header/0 + , auth_header/2 + ]). + +-define(HOST, "http://127.0.0.1:18083/"). +-define(API_VERSION, "v5"). +-define(BASE_PATH, "api"). + +all() -> + emqx_ct:all(?MODULE). + +groups() -> + []. + +init_per_suite(Config) -> + ok = emqx_ct_helpers:start_apps([emqx_authz, emqx_dashboard], fun set_special_configs/1), + {ok, _} = emqx:update_config([authorization, cache, enable], false), + {ok, _} = emqx:update_config([authorization, no_match], deny), + + Config. + +end_per_suite(_Config) -> + emqx_ct_helpers:stop_apps([emqx_resource, emqx_authz, emqx_dashboard]), + ok. + +set_special_configs(emqx_dashboard) -> + Config = #{ + default_username => <<"admin">>, + default_password => <<"public">>, + listeners => [#{ + protocol => http, + port => 18083 + }] + }, + emqx_config:put([emqx_dashboard], Config), + ok; +set_special_configs(_App) -> + ok. + +%%------------------------------------------------------------------------------ +%% Testcases +%%------------------------------------------------------------------------------ + +t_api(_) -> + Settings1 = #{<<"no_match">> => <<"deny">>, + <<"deny_action">> => <<"disconnect">>, + <<"cache">> => #{ + <<"enable">> => false, + <<"max_size">> => 32, + <<"ttl">> => 60000 + } + }, + + {ok, 200, Result1} = request(put, uri(["authorization", "settings"]), Settings1), + {ok, 200, Result1} = request(get, uri(["authorization", "settings"]), []), + ?assertEqual(Settings1, jsx:decode(Result1)), + + Settings2 = #{<<"no_match">> => <<"allow">>, + <<"deny_action">> => <<"ignore">>, + <<"cache">> => #{ + <<"enable">> => true, + <<"max_size">> => 32, + <<"ttl">> => 60000 + } + }, + + {ok, 200, Result2} = request(put, uri(["authorization", "settings"]), Settings2), + {ok, 200, Result2} = request(get, uri(["authorization", "settings"]), []), + ?assertEqual(Settings2, jsx:decode(Result2)), + + ok. + +%%-------------------------------------------------------------------- +%% HTTP Request +%%-------------------------------------------------------------------- + +request(Method, Url, Body) -> + Request = case Body of + [] -> {Url, [auth_header_()]}; + _ -> {Url, [auth_header_()], "application/json", jsx:encode(Body)} + end, + ct:pal("Method: ~p, Request: ~p", [Method, Request]), + case httpc:request(Method, Request, [], [{body_format, binary}]) of + {error, socket_closed_remotely} -> + {error, socket_closed_remotely}; + {ok, {{"HTTP/1.1", Code, _}, _Headers, Return} } -> + {ok, Code, Return}; + {ok, {Reason, _, _}} -> + {error, Reason} + end. + +uri() -> uri([]). +uri(Parts) when is_list(Parts) -> + NParts = [E || E <- Parts], + ?HOST ++ filename:join([?BASE_PATH, ?API_VERSION | NParts]). + +get_sources(Result) -> + maps:get(<<"sources">>, jsx:decode(Result), []). + +auth_header_() -> + Username = <<"admin">>, + Password = <<"public">>, + {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password), + {"Authorization", "Bearer " ++ binary_to_list(Token)}. diff --git a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl similarity index 99% rename from apps/emqx_authz/test/emqx_authz_api_SUITE.erl rename to apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl index 946b1a30b..55185de78 100644 --- a/apps/emqx_authz/test/emqx_authz_api_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl @@ -13,7 +13,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_authz_api_SUITE). +-module(emqx_authz_api_sources_SUITE). -compile(nowarn_export_all). -compile(export_all). From 12b8297745f672e3090eb73b34356f729b58de61 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Wed, 1 Sep 2021 10:35:05 +0800 Subject: [PATCH 021/109] fix(config): emqx_config:fill_defaults/1,2 not working --- apps/emqx/src/emqx_config.erl | 23 ++----------- apps/emqx/test/emqx_config_SUITE.erl | 50 ++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 20 deletions(-) create mode 100644 apps/emqx/test/emqx_config_SUITE.erl diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index ddedef024..317aba401 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -261,7 +261,7 @@ init_load(SchemaMod, RawRichConf) when is_map(RawRichConf) -> normalize_conf(hocon_schema:richmap_to_map(RawRichConf))). normalize_conf(Conf) -> - maps:with(get_root_names(bin), Conf). + maps:with(get_root_names(), Conf). -spec check_config(module(), raw_config()) -> {AppEnvs, CheckedConf} when AppEnvs :: app_envs(), CheckedConf :: config(). @@ -277,7 +277,7 @@ check_config(SchemaMod, RawConf) -> -spec fill_defaults(raw_config()) -> map(). fill_defaults(RawConf) -> - RootNames = get_root_names(bin), + RootNames = get_root_names(), maps:fold(fun(Key, Conf, Acc) -> SubMap = #{Key => Conf}, WithDefaults = case lists:member(Key, RootNames) of @@ -320,9 +320,6 @@ get_schema_mod(RootName) -> get_root_names() -> maps:get(names, persistent_term:get(?PERSIS_SCHEMA_MODS, #{names => []})). -get_root_names(bin) -> - maps:keys(get_schema_mod()). - -spec save_configs(app_envs(), config(), raw_config(), raw_config()) -> ok | {error, term()}. save_configs(_AppEnvs, Conf, RawConf, OverrideConf) -> %% We may need also support hot config update for the apps that use application envs. @@ -412,14 +409,7 @@ do_deep_put(?RAW_CONF, KeyPath, Map, Value) -> root_names_from_conf(RawConf) -> Keys = maps:keys(RawConf), - StrNames = [str(K) || K <- Keys], - AtomNames = lists:foldl(fun(K, Acc) -> - try [atom(K) | Acc] - catch error:badarg -> Acc - end - end, [], Keys), - PossibleNames = StrNames ++ AtomNames, - [Name || Name <- get_root_names(), lists:member(Name, PossibleNames)]. + [Name || Name <- get_root_names(), lists:member(Name, Keys)]. atom(Bin) when is_binary(Bin) -> binary_to_existing_atom(Bin, latin1); @@ -428,13 +418,6 @@ atom(Str) when is_list(Str) -> atom(Atom) when is_atom(Atom) -> Atom. -str(Bin) when is_binary(Bin) -> - binary_to_list(Bin); -str(Str) when is_list(Str) -> - Str; -str(Atom) when is_atom(Atom) -> - atom_to_list(Atom). - bin(Bin) when is_binary(Bin) -> Bin; bin(Str) when is_list(Str) -> list_to_binary(Str); bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8). diff --git a/apps/emqx/test/emqx_config_SUITE.erl b/apps/emqx/test/emqx_config_SUITE.erl new file mode 100644 index 000000000..50d575c0e --- /dev/null +++ b/apps/emqx/test/emqx_config_SUITE.erl @@ -0,0 +1,50 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_config_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). +-include_lib("eunit/include/eunit.hrl"). + +all() -> emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + emqx_ct_helpers:boot_modules(all), + emqx_ct_helpers:start_apps([]), + Config. + +end_per_suite(_Config) -> + emqx_ct_helpers:stop_apps([]). + +t_fill_default_values(_) -> + Conf = #{ + <<"broker">> => #{ + <<"perf">> => #{}, + <<"route_batch_clean">> => false} + }, + ?assertMatch(#{<<"broker">> := + #{<<"enable_session_registry">> := true, + <<"perf">> := + #{<<"route_lock_type">> := key, + <<"trie_compaction">> := true}, + <<"route_batch_clean">> := false, + <<"session_locking_strategy">> := quorum, + <<"shared_dispatch_ack_enabled">> := false, + <<"shared_subscription_strategy">> := round_robin, + <<"sys_heartbeat_interval">> := "30s", + <<"sys_msg_interval">> := "1m"}}, + emqx_config:fill_defaults(Conf)). From 9d2f6503afcd81acd260e9db51378d3b2f4557df Mon Sep 17 00:00:00 2001 From: lafirest Date: Wed, 1 Sep 2021 12:34:02 +0800 Subject: [PATCH 022/109] fix(emqx_retainer): fix function clause error --- apps/emqx_retainer/src/emqx_retainer.erl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/emqx_retainer/src/emqx_retainer.erl b/apps/emqx_retainer/src/emqx_retainer.erl index 3fab5958d..8e14dd21d 100644 --- a/apps/emqx_retainer/src/emqx_retainer.erl +++ b/apps/emqx_retainer/src/emqx_retainer.erl @@ -74,9 +74,11 @@ %%-------------------------------------------------------------------- %% Hook API %%-------------------------------------------------------------------- +-spec on_session_subscribed(_, _, emqx_types:subopts(), _) -> any(). on_session_subscribed(_, _, #{share := ShareName}, _) when ShareName =/= undefined -> ok; -on_session_subscribed(_, Topic, #{rh := Rh, is_new := IsNew}, Context) -> +on_session_subscribed(_, Topic, #{rh := Rh} = Opts, Context) -> + IsNew = maps:get(is_new, Opts, true), case Rh =:= 0 orelse (Rh =:= 1 andalso IsNew) of true -> dispatch(Context, Topic); _ -> ok From c4e279bb76f3320359c4c5de485fc65e0e95df22 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Wed, 1 Sep 2021 16:44:34 +0800 Subject: [PATCH 023/109] fix: support https (#5606) * fix: support https --- apps/emqx_dashboard/etc/emqx_dashboard.conf | 41 +++---- apps/emqx_dashboard/src/emqx_dashboard.erl | 111 ++++++++++-------- .../emqx_dashboard/src/emqx_dashboard_app.erl | 2 +- .../src/emqx_dashboard_schema.erl | 5 +- rebar.config | 2 +- 5 files changed, 85 insertions(+), 76 deletions(-) diff --git a/apps/emqx_dashboard/etc/emqx_dashboard.conf b/apps/emqx_dashboard/etc/emqx_dashboard.conf index 31c95a9ee..70b1d1d71 100644 --- a/apps/emqx_dashboard/etc/emqx_dashboard.conf +++ b/apps/emqx_dashboard/etc/emqx_dashboard.conf @@ -11,35 +11,30 @@ emqx_dashboard { token_expired_time = 60m listeners = [ { + protocol = http num_acceptors = 4 max_connections = 512 - protocol = http port = 18083 backlog = 512 - send_timeout = 15s - send_timeout_close = true + send_timeout = 5s inet6 = false ipv6_v6only = false } -## , -## { -## protocol: https -## port: 18084 -## acceptors: 2 -## backlog: 512 -## send_timeout: 15s -## send_timeout_close: true -## inet6: false -## ipv6_v6only: false -## certfile = "etc/certs/cert.pem" -## keyfile = "etc/certs/key.pem" -## cacertfile = "etc/certs/cacert.pem" -## verify = verify_peer -## tls_versions = "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1" -## ciphers = "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA" -## fail_if_no_peer_cert = true -## inet6 = false -## ipv6_v6only = false -## } + # , + # { + # protocol = https + # port = 18084 + # num_acceptors = 2 + # backlog = 512 + # send_timeout = 5s + # inet6 = false + # ipv6_v6only = false + # certfile = "etc/certs/cert.pem" + # keyfile = "etc/certs/key.pem" + # cacertfile = "etc/certs/cacert.pem" + # verify = verify_peer + # versions = ["tlsv1.3","tlsv1.2","tlsv1.1","tlsv1"] + # ciphers = ["TLS_AES_256_GCM_SHA384","TLS_AES_128_GCM_SHA256","TLS_CHACHA20_POLY1305_SHA256","TLS_AES_128_CCM_SHA256","TLS_AES_128_CCM_8_SHA256","ECDHE-ECDSA-AES256-GCM-SHA384","ECDHE-RSA-AES256-GCM-SHA384","ECDHE-ECDSA-AES256-SHA384","ECDHE-RSA-AES256-SHA384","ECDHE-ECDSA-DES-CBC3-SHA","ECDH-ECDSA-AES256-GCM-SHA384","ECDH-RSA-AES256-GCM-SHA384","ECDH-ECDSA-AES256-SHA384","ECDH-RSA-AES256-SHA384","DHE-DSS-AES256-GCM-SHA384","DHE-DSS-AES256-SHA256","AES256-GCM-SHA384","AES256-SHA256","ECDHE-ECDSA-AES128-GCM-SHA256","ECDHE-RSA-AES128-GCM-SHA256","ECDHE-ECDSA-AES128-SHA256","ECDHE-RSA-AES128-SHA256","ECDH-ECDSA-AES128-GCM-SHA256","ECDH-RSA-AES128-GCM-SHA256","ECDH-ECDSA-AES128-SHA256","ECDH-RSA-AES128-SHA256","DHE-DSS-AES128-GCM-SHA256","DHE-DSS-AES128-SHA256","AES128-GCM-SHA256","AES128-SHA256","ECDHE-ECDSA-AES256-SHA","ECDHE-RSA-AES256-SHA","DHE-DSS-AES256-SHA","ECDH-ECDSA-AES256-SHA","ECDH-RSA-AES256-SHA","AES256-SHA","ECDHE-ECDSA-AES128-SHA","ECDHE-RSA-AES128-SHA","DHE-DSS-AES128-SHA","ECDH-ECDSA-AES128-SHA","ECDH-RSA-AES128-SHA","AES128-SHA"] + # } ] } diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl index fb0e25564..603d8009b 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard.erl @@ -20,9 +20,7 @@ -export([ start_listeners/0 - , stop_listeners/0 - , start_listener/1 - , stop_listener/1]). + , stop_listeners/0]). %% Authorization -export([authorize_appid/1]). @@ -36,15 +34,8 @@ %%-------------------------------------------------------------------- start_listeners() -> - lists:foreach(fun start_listener/1, listeners()). - -stop_listeners() -> - lists:foreach(fun stop_listener/1, listeners()). - -start_listener({Proto, Port, Options}) -> {ok, _} = application:ensure_all_started(minirest), Authorization = {?MODULE, authorize_appid}, - RanchOptions = ranch_opts(Port, Options), GlobalSpec = #{ openapi => "3.0.0", info => #{title => "EMQ X Dashboard API", version => "5.0.0"}, @@ -56,20 +47,33 @@ start_listener({Proto, Port, Options}) -> type => apiKey, name => "authorization", in => header}}}}, - Dispatch = [{"/", cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}, - {"/static/[...]", cowboy_static, {priv_dir, emqx_dashboard, "www/static"}}, - {'_', cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}], - Minirest = #{ - protocol => Proto, + Dispatch = [ + {"/", cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}}, + {"/static/[...]", cowboy_static, {priv_dir, emqx_dashboard, "www/static"}}, + {'_', cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}} + ], + BaseMinirest = #{ base_path => ?BASE_PATH, modules => minirest_api:find_api_modules(apps()), authorization => Authorization, security => [#{application => []}], swagger_global_spec => GlobalSpec, - dispatch => Dispatch}, - MinirestOptions = maps:merge(Minirest, RanchOptions), - {ok, _} = minirest:start(listener_name(Proto), MinirestOptions), - ?ULOG("Start ~p listener on ~p successfully.~n", [listener_name(Proto), Port]). + dispatch => Dispatch + }, + [begin + Minirest = maps:put(protocol, Protocol, BaseMinirest), + {ok, _} = minirest:start(Name, RanchOptions, Minirest), + ?ULOG("Start listener ~s on ~p successfully.~n", [Name, Port]) + end || {Name, Protocol, Port, RanchOptions} <- listeners()]. + +stop_listeners() -> + [begin + ok = minirest:stop(Name), + ?ULOG("Stop listener ~s on ~p successfully.~n", [Name, Port]) + end || {Name, _, Port, _} <- listeners()]. + +%%-------------------------------------------------------------------- +%% internal apps() -> [App || {App, _, _} <- application:loaded_applications(), @@ -78,30 +82,48 @@ apps() -> _ -> false end]. -ranch_opts(Port, Options0) -> - Options = lists:foldl( - fun - ({K, _V}, Acc) when K =:= max_connections orelse K =:= num_acceptors -> Acc; - ({inet6, true}, Acc) -> [inet6 | Acc]; - ({inet6, false}, Acc) -> Acc; - ({ipv6_v6only, true}, Acc) -> [{ipv6_v6only, true} | Acc]; - ({ipv6_v6only, false}, Acc) -> Acc; - ({K, V}, Acc)-> - [{K, V} | Acc] - end, [], Options0), - maps:from_list([{port, Port} | Options]). - -stop_listener({Proto, Port, _}) -> - ?ULOG("Stop dashboard listener on ~s successfully.~n", [format(Port)]), - minirest:stop(listener_name(Proto)). - listeners() -> - [{Protocol, Port, maps:to_list(maps:without([protocol, port], Map))} - || Map = #{protocol := Protocol,port := Port} - <- emqx:get_config([emqx_dashboard, listeners], [])]. + [begin + Protocol = maps:get(protocol, ListenerOptions, http), + Port = maps:get(port, ListenerOptions, 18083), + Name = listener_name(Protocol, Port), + RanchOptions = ranch_opts(maps:without([protocol], ListenerOptions)), + {Name, Protocol, Port, RanchOptions} + end || ListenerOptions <- emqx_config:get([emqx_dashboard, listeners], [])]. -listener_name(Proto) -> - list_to_atom(atom_to_list(Proto) ++ ":dashboard"). +ranch_opts(RanchOptions) -> + Keys = [ {ack_timeout, handshake_timeout} + , connection_type + , max_connections + , num_acceptors + , shutdown + , socket], + {S, R} = lists:foldl(fun key_take/2, {RanchOptions, #{}}, Keys), + R#{socket_opts => maps:fold(fun key_only/3, [], S)}. + + +key_take({K, K1}, {All, R}) -> + case maps:get(K, All, undefined) of + undefined -> + {All, R}; + V -> + {maps:remove(K, All), R#{K1 => V}} + end; +key_take(K, {All, R}) -> + case maps:get(K, All, undefined) of + undefined -> + {All, R}; + V -> + {maps:remove(K, All), R#{K => V}} + end. + +key_only(K , true , S) -> [K | S]; +key_only(_K, false, S) -> S; +key_only(K , V , S) -> [{K, V} | S]. + +listener_name(Protocol, Port) -> + Name = "dashboard:" ++ atom_to_list(Protocol) ++ ":" ++ integer_to_list(Port), + list_to_atom(Name). authorize_appid(Req) -> case cowboy_req:parse_header(<<"authorization">>, Req) of @@ -127,10 +149,3 @@ authorize_appid(Req) -> #{code => <<"UNAUTHORIZED">>, message => <<"POST '/login'">>}} end. - -format(Port) when is_integer(Port) -> - io_lib:format("0.0.0.0:~w", [Port]); -format({Addr, Port}) when is_list(Addr) -> - io_lib:format("~s:~w", [Addr, Port]); -format({Addr, Port}) when is_tuple(Addr) -> - io_lib:format("~s:~w", [inet:ntoa(Addr), Port]). diff --git a/apps/emqx_dashboard/src/emqx_dashboard_app.erl b/apps/emqx_dashboard/src/emqx_dashboard_app.erl index edcc19d8b..4e1b0caec 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_app.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_app.erl @@ -27,7 +27,7 @@ start(_StartType, _StartArgs) -> {ok, Sup} = emqx_dashboard_sup:start_link(), ok = ekka_rlog:wait_for_shards([?DASHBOARD_SHARD], infinity), - emqx_dashboard:start_listeners(), + _ = emqx_dashboard:start_listeners(), emqx_dashboard_cli:load(), ok = emqx_dashboard_admin:add_default_user(), {ok, Sup}. diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl index e0ff21ada..7dfbc923b 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl @@ -37,14 +37,13 @@ fields("http") -> , {"num_acceptors", emqx_schema:t(integer(), undefined, 4)} , {"max_connections", emqx_schema:t(integer(), undefined, 512)} , {"backlog", emqx_schema:t(integer(), undefined, 1024)} - , {"send_timeout", emqx_schema:t(emqx_schema:duration(), undefined, "15s")} - , {"send_timeout_close", emqx_schema:t(boolean(), undefined, true)} + , {"send_timeout", emqx_schema:t(emqx_schema:duration(), undefined, "5s")} , {"inet6", emqx_schema:t(boolean(), undefined, false)} , {"ipv6_v6only", emqx_schema:t(boolean(), undefined, false)} ]; fields("https") -> - emqx_schema:ssl(#{enable => true}) ++ fields("http"). + proplists:delete("fail_if_no_peer_cert", emqx_schema:ssl(#{})) ++ fields("http"). default_username(type) -> string(); default_username(default) -> "admin"; diff --git a/rebar.config b/rebar.config index c1710bda2..4e622b738 100644 --- a/rebar.config +++ b/rebar.config @@ -51,7 +51,7 @@ , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.2"}}} , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.8"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} - , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.2.0"}}} + , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.2.1"}}} , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.1"}}} , {replayq, "0.3.3"} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} From c7bc2e1a8d86b96993053311ec6ce5e64a193609 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Wed, 1 Sep 2021 17:42:57 +0800 Subject: [PATCH 024/109] fix: subscription about api, mqtt5 options param (#5620) --- apps/emqx_auto_subscribe/etc/emqx_auto_subscribe.conf | 9 ++++----- .../src/emqx_auto_subscribe_api.erl | 2 +- .../src/emqx_auto_subscribe_placeholder.erl | 11 ++++++----- .../src/emqx_auto_subscribe_schema.erl | 11 ++++++++++- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/apps/emqx_auto_subscribe/etc/emqx_auto_subscribe.conf b/apps/emqx_auto_subscribe/etc/emqx_auto_subscribe.conf index c91b77aa1..f6d041dab 100644 --- a/apps/emqx_auto_subscribe/etc/emqx_auto_subscribe.conf +++ b/apps/emqx_auto_subscribe/etc/emqx_auto_subscribe.conf @@ -4,10 +4,12 @@ auto_subscribe { # { # topic = "/c/${clientid}", # qos = 0 - # }, + # rh = 0 + # rap = 0 + # nl = 0 + # } # { # topic = "/u/${username}", - # qos = 1 # }, # { # topic = "/h/${host}", @@ -15,15 +17,12 @@ auto_subscribe { # }, # { # topic = "/p/${port}", - # qos = 0 # }, # { # topic = "/topic/abc", - # qos = 0 # }, # { # topic = "/client/${clientid}/username/${username}/host/${host}/port/${port}", - # qos = 0 # } ] } diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl index 7eeef52ff..97c9674b9 100644 --- a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_api.erl @@ -31,7 +31,7 @@ api_spec() -> schema() -> emqx_mgmt_util:schema( emqx_mgmt_api_configs:gen_schema( - emqx:get_raw_config([auto_subscribe, topics]))). + emqx:get_raw_config([auto_subscribe, topics])), <<"">>). auto_subscribe_api() -> Metadata = #{ diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_placeholder.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_placeholder.erl index cbe881bde..70779770d 100644 --- a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_placeholder.erl +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_placeholder.erl @@ -23,16 +23,17 @@ generate(Topics) when is_list(Topics) -> [generate(Topic) || Topic <- Topics]; -generate(#{qos := Qos, topic := Topic}) when is_binary(Topic) -> - #{qos => Qos, placeholder => generate(Topic, [])}. +generate(T0 = #{topic := Topic}) -> + T = maps:without([topic], T0), + T#{placeholder => generate(Topic, [])}. -spec(to_topic_table(list(), map(), map()) -> list()). -to_topic_table(PlaceHolders, ClientInfo, ConnInfo) -> +to_topic_table(PHs, ClientInfo, ConnInfo) -> [begin Topic0 = to_topic(PlaceHolder, ClientInfo, ConnInfo, []), {Topic, Opts} = emqx_topic:parse(Topic0), - {Topic, Opts#{qos => Qos}} - end || #{qos := Qos, placeholder := PlaceHolder} <- PlaceHolders]. + {Topic, Opts#{qos => Qos, rh => RH, rap => RAP, nl => NL}} + end || #{qos := Qos, rh := RH, rap := RAP, nl := NL, placeholder := PlaceHolder} <- PHs]. %%-------------------------------------------------------------------- %% internal diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl index 73ae262a1..92420e217 100644 --- a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl @@ -30,5 +30,14 @@ fields("auto_subscribe") -> fields("topic") -> [ {topic, emqx_schema:t(binary())} - , {qos, emqx_schema:t(integer(), undefined, 0)} + , {qos, t(hoconsc:union([0, 1, 2]), 0)} + , {rh, t(hoconsc:union([0, 1, 2]), 0)} + , {rap, t(hoconsc:union([0, 1]), 0)} + , {nl, t(hoconsc:union([0, 1]), 0)} ]. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- +t(Type, Default) -> + hoconsc:t(Type, #{default => Default}). From 7a98289d4a7d311d6988051bf9b1e308248bea70 Mon Sep 17 00:00:00 2001 From: William Yang Date: Mon, 30 Aug 2021 11:32:48 +0200 Subject: [PATCH 025/109] chore: centos7 add openssl11 dep in rpm spec --- deploy/packages/rpm/emqx.spec | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/deploy/packages/rpm/emqx.spec b/deploy/packages/rpm/emqx.spec index 882b7753e..4a4d6d0f3 100644 --- a/deploy/packages/rpm/emqx.spec +++ b/deploy/packages/rpm/emqx.spec @@ -19,6 +19,12 @@ BuildRoot: %{_tmppath}/%{_name}-%{_version}-root Provides: %{_name} AutoReq: 0 +%if 0%{?rhel} == 7 +Requires: openssl11 libatomic +%else +Requires: libatomic +%endif + %description EMQX, a distributed, massively scalable, highly extensible MQTT message broker written in Erlang/OTP. From 8252771306357ecf9b65004998c9e47ac8ae410a Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Wed, 1 Sep 2021 15:56:15 +0800 Subject: [PATCH 026/109] feat(authz api): support upload ssl cert file for api --- apps/emqx_authz/src/emqx_authz.erl | 2 +- apps/emqx_authz/src/emqx_authz_api_schema.erl | 26 ++- .../emqx_authz/src/emqx_authz_api_sources.erl | 163 +++++++++--------- .../test/emqx_authz_api_sources_SUITE.erl | 66 +++++-- 4 files changed, 157 insertions(+), 100 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 4a6d7033e..f3b9a4793 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -309,7 +309,7 @@ check_sources(RawSources) -> find_source_by_type(Type) -> find_source_by_type(Type, lookup()). find_source_by_type(Type, Sources) -> find_source_by_type(Type, Sources, 1). -find_source_by_type(_, [], _N) -> error(not_found_rule); +find_source_by_type(_, [], _N) -> error(not_found_source); find_source_by_type(Type, [ Source = #{type := T} | Tail], N) -> case Type =:= T of true -> {N, Source}; diff --git a/apps/emqx_authz/src/emqx_authz_api_schema.erl b/apps/emqx_authz/src/emqx_authz_api_schema.erl index 4c17cd0b6..7d8b583ab 100644 --- a/apps/emqx_authz/src/emqx_authz_api_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_api_schema.erl @@ -26,6 +26,9 @@ definitions() -> type => object, required => [status], properties => #{ + id => #{ + type => string + }, status => #{ type => string, example => <<"healthy">> @@ -41,7 +44,18 @@ definitions() -> oneOf => [ minirest:ref(<<"connector_redis">>) ] }, - ConnectorRedis= #{ + SSL = #{ + type => object, + required => [enable], + properties => #{ + enable => #{type => boolean, example => true}, + cacertfile => #{type => string}, + keyfile => #{type => string}, + certfile => #{type => string}, + verify => #{type => boolean, example => false} + } + }, + ConnectorRedis = #{ type => object, required => [type, enable, config, cmd], properties => #{ @@ -65,7 +79,8 @@ definitions() -> pool_size => #{type => integer}, auto_reconnect => #{type => boolean, example => true}, password => #{type => string}, - database => #{type => string, example => mqtt} + database => #{type => integer}, + ssl => minirest:ref(<<"ssl">>) } } , #{type => object, @@ -80,7 +95,8 @@ definitions() -> pool_size => #{type => integer}, auto_reconnect => #{type => boolean, example => true}, password => #{type => string}, - database => #{type => string, example => mqtt} + database => #{type => integer}, + ssl => minirest:ref(<<"ssl">>) } } , #{type => object, @@ -94,7 +110,8 @@ definitions() -> pool_size => #{type => integer}, auto_reconnect => #{type => boolean, example => true}, password => #{type => string}, - database => #{type => string, example => mqtt} + database => #{type => integer}, + ssl => minirest:ref(<<"ssl">>) } } ], @@ -108,5 +125,6 @@ definitions() -> }, [ #{<<"returned_sources">> => RetruenedSources} , #{<<"sources">> => Sources} + , #{<<"ssl">> => SSL} , #{<<"connector_redis">> => ConnectorRedis} ]. diff --git a/apps/emqx_authz/src/emqx_authz_api_sources.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl index 2ad5db1da..a735d5d70 100644 --- a/apps/emqx_authz/src/emqx_authz_api_sources.erl +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -19,6 +19,7 @@ -behavior(minirest_api). -include("emqx_authz.hrl"). +-include_lib("emqx/include/logger.hrl"). -define(EXAMPLE_REDIS, #{type=> redis, @@ -32,7 +33,7 @@ maps:put(annotations, #{status => healthy}, ?EXAMPLE_REDIS) ). --define(EXAMPLE_RETURNED_RULES, +-define(EXAMPLE_RETURNED, #{sources => [?EXAMPLE_RETURNED_REDIS ] }). @@ -55,24 +56,6 @@ sources_api() -> Metadata = #{ get => #{ description => "List authorization sources", - parameters => [ - #{ - name => page, - in => query, - schema => #{ - type => integer - }, - required => false - }, - #{ - name => limit, - in => query, - schema => #{ - type => integer - }, - required => false - } - ], responses => #{ <<"200">> => #{ description => <<"OK">>, @@ -90,7 +73,7 @@ sources_api() -> examples => #{ sources => #{ summary => <<"Sources">>, - value => jsx:encode(?EXAMPLE_RETURNED_RULES) + value => jsx:encode(?EXAMPLE_RETURNED) } } } @@ -287,53 +270,38 @@ move_source_api() -> }, {"/authorization/sources/:type/move", Metadata, move_source}. -sources(get, #{query_string := Query}) -> - Sources = lists:foldl(fun (#{type := _Type, enable := true, config := #{server := Server} = Config, annotations := #{id := Id}} = Source, AccIn) -> - NSource = case emqx_resource:health_check(Id) of - ok -> - Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, - annotations => #{id => Id, - status => healthy}}; - _ -> - Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, - annotations => #{id => Id, - status => unhealthy}} - end, - lists:append(AccIn, [NSource]); - (#{type := _Type, enable := true, annotations := #{id := Id}} = Source, AccIn) -> - NSource = case emqx_resource:health_check(Id) of - ok -> - Source#{annotations => #{status => healthy}}; - _ -> - Source#{annotations => #{status => unhealthy}} - end, - lists:append(AccIn, [NSource]); - (Source, AccIn) -> - lists:append(AccIn, [Source]) +sources(get, _) -> + Sources = lists:foldl(fun (#{type := _Type, enable := true, config := Config, annotations := #{id := Id}} = Source, AccIn) -> + NSource0 = case maps:get(server, Config, undefined) of + undefined -> Source; + Server -> + Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}} + end, + NSource1 = case maps:get(servers, Config, undefined) of + undefined -> NSource0; + Servers -> + NSource0#{config => Config#{servers => [emqx_connector_schema_lib:ip_port_to_string(Server) || Server <- Servers]}} + end, + NSource2 = case emqx_resource:health_check(Id) of + ok -> + NSource1#{annotations => #{status => healthy}}; + _ -> + NSource1#{annotations => #{status => unhealthy}} + end, + lists:append(AccIn, [NSource2]); + (Source, AccIn) -> + lists:append(AccIn, [Source#{annotations => #{status => healthy}}]) end, [], emqx_authz:lookup()), - case maps:is_key(<<"page">>, Query) andalso maps:is_key(<<"limit">>, Query) of - true -> - Page = maps:get(<<"page">>, Query), - Limit = maps:get(<<"limit">>, Query), - Index = (binary_to_integer(Page) - 1) * binary_to_integer(Limit), - {_, Sources1} = lists:split(Index, Sources), - case binary_to_integer(Limit) < length(Sources1) of - true -> - {Sources2, _} = lists:split(binary_to_integer(Limit), Sources1), - {200, #{sources => Sources2}}; - false -> {200, #{sources => Sources1}} - end; - false -> {200, #{sources => Sources}} - end; -sources(post, #{body := RawConfig}) -> - case emqx_authz:update(head, [RawConfig]) of + {200, #{sources => Sources}}; +sources(post, #{body := Body}) -> + case emqx_authz:update(head, [save_cert(Body)]) of {ok, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} end; -sources(put, #{body := RawConfig}) -> - case emqx_authz:update(replace, RawConfig) of +sources(put, #{body := Body}) -> + case emqx_authz:update(replace, save_cert(Body)) of {ok, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, @@ -345,27 +313,28 @@ source(get, #{bindings := #{type := Type}}) -> {error, Reason} -> {404, #{messgae => atom_to_binary(Reason)}}; #{enable := false} = Source -> {200, Source}; #{type := file} = Source -> {200, Source}; - #{config := #{server := Server, - annotations := #{id := Id} - } = Config} = Source -> - case emqx_resource:health_check(Id) of + #{config := Config, annotations := #{id := Id}} = Source -> + NSource0 = case maps:get(server, Config, undefined) of + undefined -> Source; + Server -> + Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}} + end, + NSource1 = case maps:get(servers, Config, undefined) of + undefined -> NSource0; + Servers -> + NSource0#{config => Config#{servers => [emqx_connector_schema_lib:ip_port_to_string(Server) || Server <- Servers]}} + end, + NSource2 = case emqx_resource:health_check(Id) of ok -> - {200, Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, - annotations => #{status => healthy}}}; + NSource1#{annotations => #{status => healthy}}; _ -> - {200, Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, - annotations => #{status => unhealthy}}} - end; - #{config := #{annotations := #{id := Id}}} = Source -> - case emqx_resource:health_check(Id) of - ok -> - {200, Source#{annotations => #{status => healthy}}}; - _ -> - {200, Source#{annotations => #{status => unhealthy}}} - end + NSource1#{annotations => #{status => unhealthy}} + end, + {200, NSource2} end; -source(put, #{bindings := #{type := Type}, body := RawConfig}) -> - case emqx_authz:update({replace_once, Type}, RawConfig) of +source(put, #{bindings := #{type := Type}, body := Body}) -> + + case emqx_authz:update({replace_once, Type}, save_cert(Body)) of {ok, _} -> {204}; {error, not_found_source} -> {404, #{code => <<"NOT_FOUND">>, @@ -391,3 +360,39 @@ move_source(post, #{bindings := #{type := Type}, body := #{<<"position">> := Pos {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} end. + +save_cert(#{<<"config">> := #{<<"ssl">> := #{<<"enable">> := true} = SSL} = Config} = Body) -> + CertPath = filename:join([emqx:get_config([node, data_dir]), "certs"]), + CaCert = case maps:is_key(<<"cacertfile">>, SSL) of + true -> + write_file(filename:join([CertPath, "cacert-" ++ emqx_rule_id:gen() ++".pem"]), + maps:get(<<"cacertfile">>, SSL)); + false -> "" + end, + Cert = case maps:is_key(<<"certfile">>, SSL) of + true -> + write_file(filename:join([CertPath, "cert-" ++ emqx_rule_id:gen() ++".pem"]), + maps:get(<<"certfile">>, SSL)); + false -> "" + end, + Key = case maps:is_key(<<"keyfile">>, SSL) of + true -> + write_file(filename:join([CertPath, "key-" ++ emqx_rule_id:gen() ++".pem"]), + maps:get(<<"keyfile">>, SSL)); + false -> "" + end, + Body#{<<"config">> := Config#{<<"ssl">> => SSL#{<<"cacertfile">> => CaCert, + <<"certfile">> => Cert, + <<"keyfile">> => Key} + } + }; +save_cert(Body) -> Body. + +write_file(Filename, Bytes) -> + ok = filelib:ensure_dir(Filename), + case file:write_file(Filename, Bytes) of + ok -> Filename; + {error, Reason} -> + ?LOG(error, "Write File ~p Error: ~p", [Filename, Reason]), + error(Reason) + end. diff --git a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl index 55185de78..ed3cf18d0 100644 --- a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl @@ -48,8 +48,10 @@ -define(SOURCE2, #{<<"type">> => <<"mongo">>, <<"enable">> => true, <<"config">> => #{ - <<"mongo_type">> => <<"single">>, - <<"server">> => <<"127.0.0.1:27017">>, + <<"mongo_type">> => <<"sharded">>, + <<"servers">> => [<<"127.0.0.1:27017">>, + <<"192.168.0.1:27017">> + ], <<"pool_size">> => 1, <<"database">> => <<"mqtt">>, <<"ssl">> => #{<<"enable">> => false}}, @@ -59,7 +61,7 @@ -define(SOURCE3, #{<<"type">> => <<"mysql">>, <<"enable">> => true, <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, + <<"server">> => <<"127.0.0.1:3306">>, <<"pool_size">> => 1, <<"database">> => <<"mqtt">>, <<"username">> => <<"xx">>, @@ -71,7 +73,7 @@ -define(SOURCE4, #{<<"type">> => <<"pgsql">>, <<"enable">> => true, <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, + <<"server">> => <<"127.0.0.1:5432">>, <<"pool_size">> => 1, <<"database">> => <<"mqtt">>, <<"username">> => <<"xx">>, @@ -83,12 +85,15 @@ -define(SOURCE5, #{<<"type">> => <<"redis">>, <<"enable">> => true, <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, + <<"servers">> => [<<"127.0.0.1:6379">>, + <<"127.0.0.1:6380">> + ], <<"pool_size">> => 1, <<"database">> => 0, <<"password">> => <<"ee">>, <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, + <<"ssl">> => #{<<"enable">> => false} + }, <<"cmd">> => <<"HGETALL mqtt_authz:%u">> }). @@ -144,6 +149,26 @@ set_special_configs(emqx_authz) -> set_special_configs(_App) -> ok. +init_per_testcase(t_api, Config) -> + meck:new(emqx_rule_id, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_rule_id, gen, fun() -> "fake" end), + + meck:new(emqx, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx, get_config, fun([node, data_dir]) -> + % emqx_ct_helpers:deps_path(emqx_authz, "test"); + {data_dir, Data} = lists:keyfind(data_dir, 1, Config), + Data; + (C) -> meck:passthrough([C]) + end), + Config; +init_per_testcase(_, Config) -> Config. + +end_per_testcase(t_api, _Config) -> + meck:unload(emqx_rule_id), + meck:unload(emqx), + ok; +end_per_testcase(_, _Config) -> ok. + %%------------------------------------------------------------------------------ %% Testcases %%------------------------------------------------------------------------------ @@ -158,13 +183,6 @@ t_api(_) -> {ok, 200, Result2} = request(get, uri(["authorization", "sources"]), []), ?assertEqual(20, length(get_sources(Result2))), - lists:foreach(fun(Page) -> - Query = "?page=" ++ integer_to_list(Page) ++ "&&limit=10", - Url = uri(["authorization/sources" ++ Query]), - {ok, 200, Result} = request(get, Url, []), - ?assertEqual(10, length(get_sources(Result))) - end, lists:seq(1, 2)), - {ok, 204, _} = request(put, uri(["authorization", "sources"]), [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4]), {ok, 200, Result3} = request(get, uri(["authorization", "sources"]), []), @@ -176,15 +194,31 @@ t_api(_) -> ], Sources), {ok, 204, _} = request(put, uri(["authorization", "sources", "http"]), ?SOURCE1#{<<"enable">> := false}), - {ok, 200, Result4} = request(get, uri(["authorization", "sources", "http"]), []), ?assertMatch(#{<<"type">> := <<"http">>, <<"enable">> := false}, jsx:decode(Result4)), + #{<<"config">> := Config} = ?SOURCE2, + {ok, 204, _} = request(put, uri(["authorization", "sources", "mongo"]), + ?SOURCE2#{<<"config">> := Config#{<<"ssl">> := #{ + <<"enable">> => true, + <<"cacertfile">> => <<"fake cacert file">>, + <<"certfile">> => <<"fake cert file">>, + <<"keyfile">> => <<"fake key file">>, + <<"verify">> => false + }}}), + {ok, 200, Result5} = request(get, uri(["authorization", "sources", "mongo"]), []), + ?assertMatch(#{<<"type">> := <<"mongo">>, + <<"config">> := #{<<"ssl">> := #{<<"enable">> := true}} + }, jsx:decode(Result5)), + ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "cacert-fake.pem"]))), + ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "cert-fake.pem"]))), + ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "key-fake.pem"]))), + lists:foreach(fun(#{<<"type">> := Type}) -> {ok, 204, _} = request(delete, uri(["authorization", "sources", binary_to_list(Type)]), []) end, Sources), - {ok, 200, Result5} = request(get, uri(["authorization", "sources"]), []), - ?assertEqual([], get_sources(Result5)), + {ok, 200, Result6} = request(get, uri(["authorization", "sources"]), []), + ?assertEqual([], get_sources(Result6)), ok. t_move_source(_) -> From 07dcd9e7052b6901d114348ff6a0964655d61a66 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 2 Sep 2021 09:57:26 +0800 Subject: [PATCH 027/109] feat(authz api): support file type for sources --- apps/emqx_authz/src/emqx_authz_api_schema.erl | 34 ++++++- .../emqx_authz/src/emqx_authz_api_sources.erl | 94 ++++++++++++++++--- apps/emqx_authz/test/emqx_authz_SUITE.erl | 16 +++- .../test/emqx_authz_api_sources_SUITE.erl | 14 ++- 4 files changed, 140 insertions(+), 18 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz_api_schema.erl b/apps/emqx_authz/src/emqx_authz_api_schema.erl index 7d8b583ab..18a5e2b18 100644 --- a/apps/emqx_authz/src/emqx_authz_api_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_api_schema.erl @@ -41,7 +41,8 @@ definitions() -> ] }, Sources = #{ - oneOf => [ minirest:ref(<<"connector_redis">>) + oneOf => [ minirest:ref(<<"redis">>) + , minirest:ref(<<"file">>) ] }, SSL = #{ @@ -55,7 +56,7 @@ definitions() -> verify => #{type => boolean, example => false} } }, - ConnectorRedis = #{ + Redis = #{ type => object, required => [type, enable, config, cmd], properties => #{ @@ -123,8 +124,35 @@ definitions() -> } } }, + File = #{ + type => object, + required => [type, enable, rules], + properties => #{ + type => #{ + type => string, + enum => [<<"redis">>], + example => <<"redis">> + }, + enable => #{ + type => boolean, + example => true + }, + rules => #{ + type => array, + items => #{ + type => string, + example => <<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}.">> + } + }, + path => #{ + type => string, + example => <<"/path/to/authorizaiton_rules.conf">> + } + } + }, [ #{<<"returned_sources">> => RetruenedSources} , #{<<"sources">> => Sources} , #{<<"ssl">> => SSL} - , #{<<"connector_redis">> => ConnectorRedis} + , #{<<"redis">> => Redis} + , #{<<"file">> => File} ]. diff --git a/apps/emqx_authz/src/emqx_authz_api_sources.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl index a735d5d70..0ebf5e511 100644 --- a/apps/emqx_authz/src/emqx_authz_api_sources.erl +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -23,18 +23,30 @@ -define(EXAMPLE_REDIS, #{type=> redis, + enable => true, config => #{server => <<"127.0.0.1:3306">>, redis_type => single, pool_size => 1, auto_reconnect => true }, cmd => <<"HGETALL mqtt_authz">>}). +-define(EXAMPLE_FILE, + #{type=> file, + enable => true, + rules => [<<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}.">>, + <<"{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">> + ]}). + -define(EXAMPLE_RETURNED_REDIS, maps:put(annotations, #{status => healthy}, ?EXAMPLE_REDIS) ). +-define(EXAMPLE_RETURNED_FILE, + maps:put(annotations, #{status => healthy}, ?EXAMPLE_FILE) + ). -define(EXAMPLE_RETURNED, - #{sources => [?EXAMPLE_RETURNED_REDIS + #{sources => [ ?EXAMPLE_RETURNED_REDIS + , ?EXAMPLE_RETURNED_FILE ] }). @@ -91,6 +103,10 @@ sources_api() -> redis => #{ summary => <<"Redis">>, value => jsx:encode(?EXAMPLE_REDIS) + }, + file => #{ + summary => <<"File">>, + value => jsx:encode(?EXAMPLE_FILE) } } } @@ -113,7 +129,11 @@ sources_api() -> examples => #{ redis => #{ summary => <<"Redis">>, - value => jsx:encode([?EXAMPLE_REDIS]) + value => jsx:encode(?EXAMPLE_REDIS) + }, + file => #{ + summary => <<"File">>, + value => jsx:encode(?EXAMPLE_FILE) } } } @@ -148,9 +168,13 @@ source_api() -> 'application/json' => #{ schema => minirest:ref(<<"returned_sources">>), examples => #{ - sources => #{ - summary => <<"Sources">>, + redis => #{ + summary => <<"Redis">>, value => jsx:encode(?EXAMPLE_RETURNED_REDIS) + }, + file => #{ + summary => <<"File">>, + value => jsx:encode(?EXAMPLE_RETURNED_FILE) } } } @@ -179,6 +203,10 @@ source_api() -> redis => #{ summary => <<"Redis">>, value => jsx:encode(?EXAMPLE_REDIS) + }, + file => #{ + summary => <<"File">>, + value => jsx:encode(?EXAMPLE_FILE) } } } @@ -271,7 +299,16 @@ move_source_api() -> {"/authorization/sources/:type/move", Metadata, move_source}. sources(get, _) -> - Sources = lists:foldl(fun (#{type := _Type, enable := true, config := Config, annotations := #{id := Id}} = Source, AccIn) -> + Sources = lists:foldl(fun (#{enable := false} = Source, AccIn) -> + lists:append(AccIn, [Source#{annotations => #{status => unhealthy}}]); + (#{type := file, path := Path}, AccIn) -> + {ok, Rules} = file:consult(Path), + lists:append(AccIn, [#{type => file, + enable => true, + rules => [ io_lib:format("~p", [R])|| R <- Rules], + annotations => #{status => healthy} + }]); + (#{type := _Type, config := Config, annotations := #{id := Id}} = Source, AccIn) -> NSource0 = case maps:get(server, Config, undefined) of undefined -> Source; Server -> @@ -293,15 +330,33 @@ sources(get, _) -> lists:append(AccIn, [Source#{annotations => #{status => healthy}}]) end, [], emqx_authz:lookup()), {200, #{sources => Sources}}; -sources(post, #{body := Body}) -> +sources(post, #{body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable}}) when is_list(Rules) -> + Filename = filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]), + write_file(Filename, erlang:list_to_bitstring([<> || Rule <- Rules])), + case emqx_authz:update(head, [#{type => file, enable => Enable, path => Filename}]) of + {ok, _} -> {204}; + {error, Reason} -> + {400, #{code => <<"BAD_REQUEST">>, + messgae => atom_to_binary(Reason)}} + end; +sources(post, #{body := Body}) when is_map(Body) -> case emqx_authz:update(head, [save_cert(Body)]) of {ok, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} end; -sources(put, #{body := Body}) -> - case emqx_authz:update(replace, save_cert(Body)) of +sources(put, #{body := Body}) when is_list(Body) -> + NBody = [ begin + case Source of + #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable} -> + Filename = filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]), + write_file(Filename, erlang:list_to_bitstring([<> || Rule <- Rules])), + #{type => file, enable => Enable, path => Filename}; + _ -> save_cert(Source) + end + end || Source <- Body], + case emqx_authz:update(replace, NBody) of {ok, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, @@ -311,8 +366,15 @@ sources(put, #{body := Body}) -> source(get, #{bindings := #{type := Type}}) -> case emqx_authz:lookup(Type) of {error, Reason} -> {404, #{messgae => atom_to_binary(Reason)}}; - #{enable := false} = Source -> {200, Source}; - #{type := file} = Source -> {200, Source}; + #{enable := false} = Source -> {200, Source#{annotations => #{status => unhealthy}}}; + #{type := file, path := Path}-> + {ok, Rules} = file:consult(Path), + {200, #{type => file, + enable => true, + rules => Rules, + annotations => #{status => healthy} + } + }; #{config := Config, annotations := #{id := Id}} = Source -> NSource0 = case maps:get(server, Config, undefined) of undefined -> Source; @@ -332,8 +394,16 @@ source(get, #{bindings := #{type := Type}}) -> end, {200, NSource2} end; -source(put, #{bindings := #{type := Type}, body := Body}) -> - +source(put, #{bindings := #{type := file}, body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable}}) -> + #{path := Path} = emqx_authz:lookup(file), + write_file(Path, erlang:list_to_bitstring([<> || Rule <- Rules])), + case emqx_authz:update({replace_once, file}, #{type => file, enable => Enable, path => Path}) of + {ok, _} -> {204}; + {error, Reason} -> + {400, #{code => <<"BAD_REQUEST">>, + messgae => atom_to_binary(Reason)}} + end; +source(put, #{bindings := #{type := Type}, body := Body}) when is_map(Body) -> case emqx_authz:update({replace_once, Type}, save_cert(Body)) of {ok, _} -> {204}; {error, not_found_source} -> diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index cee83cd30..88a0a9bf3 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -114,6 +114,11 @@ init_per_testcase(_, Config) -> <<"ssl">> => #{<<"enable">> => false}}, <<"cmd">> => <<"HGETALL mqtt_authz:%u">> }). +-define(SOURCE6, #{<<"type">> => <<"file">>, + <<"enable">> => true, + <<"path">> => emqx_ct_helpers:deps_path(emqx_authz, "etc/authorization_rules.conf") + }). + %%------------------------------------------------------------------------------ %% Testcases @@ -125,12 +130,14 @@ t_update_source(_) -> {ok, _} = emqx_authz:update(head, [?SOURCE1]), {ok, _} = emqx_authz:update(tail, [?SOURCE4]), {ok, _} = emqx_authz:update(tail, [?SOURCE5]), + {ok, _} = emqx_authz:update(tail, [?SOURCE6]), ?assertMatch([ #{type := http, enable := true} , #{type := mongo, enable := true} , #{type := mysql, enable := true} , #{type := pgsql, enable := true} , #{type := redis, enable := true} + , #{type := file, enable := true} ], emqx:get_config([authorization, sources], [])), {ok, _} = emqx_authz:update({replace_once, http}, ?SOURCE1#{<<"enable">> := false}), @@ -138,23 +145,26 @@ t_update_source(_) -> {ok, _} = emqx_authz:update({replace_once, mysql}, ?SOURCE3#{<<"enable">> := false}), {ok, _} = emqx_authz:update({replace_once, pgsql}, ?SOURCE4#{<<"enable">> := false}), {ok, _} = emqx_authz:update({replace_once, redis}, ?SOURCE5#{<<"enable">> := false}), + {ok, _} = emqx_authz:update({replace_once, file}, ?SOURCE6#{<<"enable">> := false}), ?assertMatch([ #{type := http, enable := false} , #{type := mongo, enable := false} , #{type := mysql, enable := false} , #{type := pgsql, enable := false} , #{type := redis, enable := false} + , #{type := file, enable := false} ], emqx:get_config([authorization, sources], [])), {ok, _} = emqx_authz:update(replace, []). t_move_source(_) -> - {ok, _} = emqx_authz:update(replace, [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5]), + {ok, _} = emqx_authz:update(replace, [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5, ?SOURCE6]), ?assertMatch([ #{type := http} , #{type := mongo} , #{type := mysql} , #{type := pgsql} , #{type := redis} + , #{type := file} ], emqx_authz:lookup()), {ok, _} = emqx_authz:move(pgsql, <<"top">>), @@ -163,6 +173,7 @@ t_move_source(_) -> , #{type := mongo} , #{type := mysql} , #{type := redis} + , #{type := file} ], emqx_authz:lookup()), {ok, _} = emqx_authz:move(http, <<"bottom">>), @@ -170,6 +181,7 @@ t_move_source(_) -> , #{type := mongo} , #{type := mysql} , #{type := redis} + , #{type := file} , #{type := http} ], emqx_authz:lookup()), @@ -178,6 +190,7 @@ t_move_source(_) -> , #{type := pgsql} , #{type := mongo} , #{type := redis} + , #{type := file} , #{type := http} ], emqx_authz:lookup()), @@ -185,6 +198,7 @@ t_move_source(_) -> ?assertMatch([ #{type := mysql} , #{type := pgsql} , #{type := redis} + , #{type := file} , #{type := http} , #{type := mongo} ], emqx_authz:lookup()), diff --git a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl index ed3cf18d0..a7e747069 100644 --- a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl @@ -96,6 +96,13 @@ }, <<"cmd">> => <<"HGETALL mqtt_authz:%u">> }). +-define(SOURCE6, #{<<"type">> => <<"file">>, + <<"enable">> => true, + <<"rules">> => + [<<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}.">>, + <<"{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">> + ] + }). all() -> emqx_ct:all(?MODULE). @@ -119,7 +126,7 @@ init_per_suite(Config) -> ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), - ok = emqx_ct_helpers:start_apps([emqx_authz, emqx_dashboard], fun set_special_configs/1), + ok = emqx_ct_helpers:start_apps([emqx_authz, emqx_dashboard], fun set_special_configs/1), {ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, no_match], deny), @@ -183,7 +190,7 @@ t_api(_) -> {ok, 200, Result2} = request(get, uri(["authorization", "sources"]), []), ?assertEqual(20, length(get_sources(Result2))), - {ok, 204, _} = request(put, uri(["authorization", "sources"]), [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4]), + {ok, 204, _} = request(put, uri(["authorization", "sources"]), [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5, ?SOURCE6]), {ok, 200, Result3} = request(get, uri(["authorization", "sources"]), []), Sources = get_sources(Result3), @@ -191,7 +198,10 @@ t_api(_) -> , #{<<"type">> := <<"mongo">>} , #{<<"type">> := <<"mysql">>} , #{<<"type">> := <<"pgsql">>} + , #{<<"type">> := <<"redis">>} + , #{<<"type">> := <<"file">>} ], Sources), + ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]))), {ok, 204, _} = request(put, uri(["authorization", "sources", "http"]), ?SOURCE1#{<<"enable">> := false}), {ok, 200, Result4} = request(get, uri(["authorization", "sources", "http"]), []), From b8ee977d9d474c6cdf4ae0df3853e4e9e7091950 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 2 Sep 2021 10:44:04 +0800 Subject: [PATCH 028/109] feat(authz api): support read cert file for api --- .../emqx_authz/src/emqx_authz_api_sources.erl | 48 +++++++++++++------ .../test/emqx_authz_api_sources_SUITE.erl | 13 ++++- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz_api_sources.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl index 0ebf5e511..06d5aa859 100644 --- a/apps/emqx_authz/src/emqx_authz_api_sources.erl +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -325,7 +325,7 @@ sources(get, _) -> _ -> NSource1#{annotations => #{status => unhealthy}} end, - lists:append(AccIn, [NSource2]); + lists:append(AccIn, [read_cert(NSource2)]); (Source, AccIn) -> lists:append(AccIn, [Source#{annotations => #{status => healthy}}]) end, [], emqx_authz:lookup()), @@ -340,7 +340,7 @@ sources(post, #{body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"ena messgae => atom_to_binary(Reason)}} end; sources(post, #{body := Body}) when is_map(Body) -> - case emqx_authz:update(head, [save_cert(Body)]) of + case emqx_authz:update(head, [write_cert(Body)]) of {ok, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, @@ -353,7 +353,7 @@ sources(put, #{body := Body}) when is_list(Body) -> Filename = filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]), write_file(Filename, erlang:list_to_bitstring([<> || Rule <- Rules])), #{type => file, enable => Enable, path => Filename}; - _ -> save_cert(Source) + _ -> write_cert(Source) end end || Source <- Body], case emqx_authz:update(replace, NBody) of @@ -392,7 +392,7 @@ source(get, #{bindings := #{type := Type}}) -> _ -> NSource1#{annotations => #{status => unhealthy}} end, - {200, NSource2} + {200, read_cert(NSource2)} end; source(put, #{bindings := #{type := file}, body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable}}) -> #{path := Path} = emqx_authz:lookup(file), @@ -404,7 +404,7 @@ source(put, #{bindings := #{type := file}, body := #{<<"type">> := <<"file">>, < messgae => atom_to_binary(Reason)}} end; source(put, #{bindings := #{type := Type}, body := Body}) when is_map(Body) -> - case emqx_authz:update({replace_once, Type}, save_cert(Body)) of + case emqx_authz:update({replace_once, Type}, write_cert(Body)) of {ok, _} -> {204}; {error, not_found_source} -> {404, #{code => <<"NOT_FOUND">>, @@ -431,7 +431,27 @@ move_source(post, #{bindings := #{type := Type}, body := #{<<"position">> := Pos messgae => atom_to_binary(Reason)}} end. -save_cert(#{<<"config">> := #{<<"ssl">> := #{<<"enable">> := true} = SSL} = Config} = Body) -> +read_cert(#{config := #{ssl := #{enable := true} = SSL} = Config} = Source) -> + CaCert = case file:read_file(maps:get(cacertfile, SSL, "")) of + {ok, CaCert0} -> CaCert0; + _ -> "" + end, + Cert = case file:read_file(maps:get(certfile, SSL, "")) of + {ok, Cert0} -> Cert0; + _ -> "" + end, + Key = case file:read_file(maps:get(keyfile, SSL, "")) of + {ok, Key0} -> Key0; + _ -> "" + end, + Source#{config => Config#{ssl => SSL#{cacertfile => CaCert, + certfile => Cert, + keyfile => Key + }} + }; +read_cert(Source) -> Source. + +write_cert(#{<<"config">> := #{<<"ssl">> := #{<<"enable">> := true} = SSL} = Config} = Source) -> CertPath = filename:join([emqx:get_config([node, data_dir]), "certs"]), CaCert = case maps:is_key(<<"cacertfile">>, SSL) of true -> @@ -439,24 +459,24 @@ save_cert(#{<<"config">> := #{<<"ssl">> := #{<<"enable">> := true} = SSL} = Conf maps:get(<<"cacertfile">>, SSL)); false -> "" end, - Cert = case maps:is_key(<<"certfile">>, SSL) of + Cert = case maps:is_key(<<"certfile">>, SSL) of true -> write_file(filename:join([CertPath, "cert-" ++ emqx_rule_id:gen() ++".pem"]), maps:get(<<"certfile">>, SSL)); false -> "" end, - Key = case maps:is_key(<<"keyfile">>, SSL) of + Key = case maps:is_key(<<"keyfile">>, SSL) of true -> write_file(filename:join([CertPath, "key-" ++ emqx_rule_id:gen() ++".pem"]), maps:get(<<"keyfile">>, SSL)); false -> "" end, - Body#{<<"config">> := Config#{<<"ssl">> => SSL#{<<"cacertfile">> => CaCert, - <<"certfile">> => Cert, - <<"keyfile">> => Key} - } - }; -save_cert(Body) -> Body. + Source#{<<"config">> := Config#{<<"ssl">> => SSL#{<<"cacertfile">> => CaCert, + <<"certfile">> => Cert, + <<"keyfile">> => Key} + } + }; +write_cert(Source) -> Source. write_file(Filename, Bytes) -> ok = filelib:ensure_dir(Filename), diff --git a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl index a7e747069..bb2bb27e4 100644 --- a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl @@ -126,7 +126,7 @@ init_per_suite(Config) -> ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), - ok = emqx_ct_helpers:start_apps([emqx_authz, emqx_dashboard], fun set_special_configs/1), + ok = emqx_ct_helpers:start_apps([emqx_authz, emqx_dashboard], fun set_special_configs/1), {ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, no_match], deny), @@ -208,6 +208,10 @@ t_api(_) -> ?assertMatch(#{<<"type">> := <<"http">>, <<"enable">> := false}, jsx:decode(Result4)), #{<<"config">> := Config} = ?SOURCE2, + + dbg:tracer(),dbg:p(all,c), + dbg:tpl(emqx_authz_api_sources, read_cert, cx), + dbg:tpl(emqx_authz_api_sources, write_cert, cx), {ok, 204, _} = request(put, uri(["authorization", "sources", "mongo"]), ?SOURCE2#{<<"config">> := Config#{<<"ssl">> := #{ <<"enable">> => true, @@ -218,7 +222,12 @@ t_api(_) -> }}}), {ok, 200, Result5} = request(get, uri(["authorization", "sources", "mongo"]), []), ?assertMatch(#{<<"type">> := <<"mongo">>, - <<"config">> := #{<<"ssl">> := #{<<"enable">> := true}} + <<"config">> := #{<<"ssl">> := #{<<"enable">> := true, + <<"cacertfile">> := <<"fake cacert file">>, + <<"certfile">> := <<"fake cert file">>, + <<"keyfile">> := <<"fake key file">>, + <<"verify">> := false + }} }, jsx:decode(Result5)), ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "cacert-fake.pem"]))), ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "cert-fake.pem"]))), From 5669ea4034c96ee035ff77b191ab80d0f7e630bc Mon Sep 17 00:00:00 2001 From: Rory Z Date: Thu, 2 Sep 2021 11:13:41 +0800 Subject: [PATCH 029/109] chore(authz api): fix dialyzer error --- .../emqx_authz/src/emqx_authz_api_sources.erl | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz_api_sources.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl index 06d5aa859..8bc99188e 100644 --- a/apps/emqx_authz/src/emqx_authz_api_sources.erl +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -331,8 +331,9 @@ sources(get, _) -> end, [], emqx_authz:lookup()), {200, #{sources => Sources}}; sources(post, #{body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable}}) when is_list(Rules) -> - Filename = filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]), - write_file(Filename, erlang:list_to_bitstring([<> || Rule <- Rules])), + {ok, Filename} = write_file(filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]), + erlang:list_to_bitstring([<> || Rule <- Rules]) + ), case emqx_authz:update(head, [#{type => file, enable => Enable, path => Filename}]) of {ok, _} -> {204}; {error, Reason} -> @@ -350,8 +351,9 @@ sources(put, #{body := Body}) when is_list(Body) -> NBody = [ begin case Source of #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable} -> - Filename = filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]), - write_file(Filename, erlang:list_to_bitstring([<> || Rule <- Rules])), + {ok, Filename} = write_file(filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]), + erlang:list_to_bitstring([<> || Rule <- Rules]) + ), #{type => file, enable => Enable, path => Filename}; _ -> write_cert(Source) end @@ -395,9 +397,10 @@ source(get, #{bindings := #{type := Type}}) -> {200, read_cert(NSource2)} end; source(put, #{bindings := #{type := file}, body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable}}) -> - #{path := Path} = emqx_authz:lookup(file), - write_file(Path, erlang:list_to_bitstring([<> || Rule <- Rules])), - case emqx_authz:update({replace_once, file}, #{type => file, enable => Enable, path => Path}) of + {ok, Filename} = write_file(maps:get(path, emqx_authz:lookup(file), ""), + erlang:list_to_bitstring([<> || Rule <- Rules]) + ), + case emqx_authz:update({replace_once, file}, #{type => file, enable => Enable, path => Filename}) of {ok, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, @@ -455,20 +458,23 @@ write_cert(#{<<"config">> := #{<<"ssl">> := #{<<"enable">> := true} = SSL} = Con CertPath = filename:join([emqx:get_config([node, data_dir]), "certs"]), CaCert = case maps:is_key(<<"cacertfile">>, SSL) of true -> - write_file(filename:join([CertPath, "cacert-" ++ emqx_rule_id:gen() ++".pem"]), - maps:get(<<"cacertfile">>, SSL)); + {ok, CaCertFile} = write_file(filename:join([CertPath, "cacert-" ++ emqx_rule_id:gen() ++".pem"]), + maps:get(<<"cacertfile">>, SSL)), + CaCertFile; false -> "" end, Cert = case maps:is_key(<<"certfile">>, SSL) of true -> - write_file(filename:join([CertPath, "cert-" ++ emqx_rule_id:gen() ++".pem"]), - maps:get(<<"certfile">>, SSL)); + {ok, CertFile} = write_file(filename:join([CertPath, "cert-" ++ emqx_rule_id:gen() ++".pem"]), + maps:get(<<"certfile">>, SSL)), + CertFile; false -> "" end, Key = case maps:is_key(<<"keyfile">>, SSL) of true -> - write_file(filename:join([CertPath, "key-" ++ emqx_rule_id:gen() ++".pem"]), - maps:get(<<"keyfile">>, SSL)); + {ok, KeyFile} = write_file(filename:join([CertPath, "key-" ++ emqx_rule_id:gen() ++".pem"]), + maps:get(<<"keyfile">>, SSL)), + KeyFile; false -> "" end, Source#{<<"config">> := Config#{<<"ssl">> => SSL#{<<"cacertfile">> => CaCert, @@ -481,7 +487,7 @@ write_cert(Source) -> Source. write_file(Filename, Bytes) -> ok = filelib:ensure_dir(Filename), case file:write_file(Filename, Bytes) of - ok -> Filename; + ok -> {ok, Filename}; {error, Reason} -> ?LOG(error, "Write File ~p Error: ~p", [Filename, Reason]), error(Reason) From 4e8ac36348070e13a283e5f9b098acddb4ab401c Mon Sep 17 00:00:00 2001 From: Swilder-M Date: Thu, 2 Sep 2021 11:33:24 +0800 Subject: [PATCH 030/109] chore(README): modify slack badge --- README-CN.md | 2 +- README-JP.md | 2 +- README-RU.md | 2 +- README.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README-CN.md b/README-CN.md index b430d4b5f..3e775ad70 100644 --- a/README-CN.md +++ b/README-CN.md @@ -4,7 +4,7 @@ [![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx) [![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx) [![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx) -[![Slack Invite]()](https://slack-invite.emqx.io) +[![Slack](https://img.shields.io/badge/Slack-EMQ%20X-39AE85?logo=slack)](https://slack-invite.emqx.io/) [![Twitter](https://img.shields.io/badge/Twitter-EMQ-1DA1F2?logo=twitter)](https://twitter.com/EMQTech) [![Community](https://img.shields.io/badge/Community-EMQ%20X-yellow)](https://askemq.com) [![YouTube](https://img.shields.io/badge/Subscribe-EMQ%20中文-FF0000?logo=youtube)](https://www.youtube.com/channel/UCir_r04HIsLjf2qqyZ4A8Cg) diff --git a/README-JP.md b/README-JP.md index 6e1c62f2f..03324a3b1 100644 --- a/README-JP.md +++ b/README-JP.md @@ -4,7 +4,7 @@ [![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx) [![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx) [![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx) -[![Slack Invite]()](https://slack-invite.emqx.io) +[![Slack](https://img.shields.io/badge/Slack-EMQ%20X-39AE85?logo=slack)](https://slack-invite.emqx.io/) [![Twitter](https://img.shields.io/badge/Twitter-EMQ-1DA1F2?logo=twitter)](https://twitter.com/EMQTech) [![YouTube](https://img.shields.io/badge/Subscribe-EMQ-FF0000?logo=youtube)](https://www.youtube.com/channel/UC5FjR77ErAxvZENEWzQaO5Q) diff --git a/README-RU.md b/README-RU.md index 2a06dac71..2eadc92e5 100644 --- a/README-RU.md +++ b/README-RU.md @@ -4,7 +4,7 @@ [![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx) [![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg?branch=master)](https://coveralls.io/github/emqx/emqx?branch=master) [![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx) -[![Slack Invite]()](https://slack-invite.emqx.io) +[![Slack](https://img.shields.io/badge/Slack-EMQ%20X-39AE85?logo=slack)](https://slack-invite.emqx.io/) [![Twitter](https://img.shields.io/badge/Follow-EMQ-1DA1F2?logo=twitter)](https://twitter.com/EMQTech) [![Community](https://img.shields.io/badge/Community-EMQ%20X-yellow?logo=github)](https://github.com/emqx/emqx/discussions) [![YouTube](https://img.shields.io/badge/Subscribe-EMQ-FF0000?logo=youtube)](https://www.youtube.com/channel/UC5FjR77ErAxvZENEWzQaO5Q) diff --git a/README.md b/README.md index 1726d426b..207ba601f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx) [![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg?branch=master)](https://coveralls.io/github/emqx/emqx?branch=master) [![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx) -[![Slack Invite]()](https://slack-invite.emqx.io) +[![Slack](https://img.shields.io/badge/Slack-EMQ%20X-39AE85?logo=slack)](https://slack-invite.emqx.io/) [![Twitter](https://img.shields.io/badge/Follow-EMQ-1DA1F2?logo=twitter)](https://twitter.com/EMQTech) [![YouTube](https://img.shields.io/badge/Subscribe-EMQ-FF0000?logo=youtube)](https://www.youtube.com/channel/UC5FjR77ErAxvZENEWzQaO5Q) From 2426482ae19d1ca1c496233dba15448015e3f6df Mon Sep 17 00:00:00 2001 From: William Yang Date: Mon, 30 Aug 2021 22:41:37 +0200 Subject: [PATCH 031/109] ci: install openssl11 as deps. --- .ci/build_packages/tests.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.ci/build_packages/tests.sh b/.ci/build_packages/tests.sh index 87c19621a..240d6214e 100755 --- a/.ci/build_packages/tests.sh +++ b/.ci/build_packages/tests.sh @@ -91,6 +91,8 @@ emqx_test(){ ;; "rpm") packagename=$(basename "${PACKAGE_PATH}/${EMQX_NAME}"-*.rpm) + # EMQX OTP requires openssl11 to have TLS1.3 support + yum install -y openssl11 rpm -ivh "${PACKAGE_PATH}/${packagename}" if ! rpm -q emqx | grep -q emqx; then echo "package install error" From 0fd18a2795bd23364b454675d1ffcb5114e0d747 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 2 Sep 2021 14:08:23 +0800 Subject: [PATCH 032/109] chore(emqx_authz): compression configuration items Signed-off-by: zhanghongtong --- apps/emqx_authz/etc/emqx_authz.conf | 76 ++++++------- apps/emqx_authz/src/emqx_authz.erl | 16 ++- .../emqx_authz/src/emqx_authz_api_sources.erl | 47 ++++---- apps/emqx_authz/src/emqx_authz_http.erl | 12 +- apps/emqx_authz/src/emqx_authz_schema.erl | 98 ++++++++--------- apps/emqx_authz/test/emqx_authz_SUITE.erl | 63 +++++------ .../test/emqx_authz_api_sources_SUITE.erl | 103 ++++++++---------- .../emqx_authz/test/emqx_authz_http_SUITE.erl | 11 +- .../test/emqx_authz_mongo_SUITE.erl | 11 +- .../test/emqx_authz_mysql_SUITE.erl | 15 ++- .../test/emqx_authz_pgsql_SUITE.erl | 15 ++- .../test/emqx_authz_redis_SUITE.erl | 13 +-- 12 files changed, 218 insertions(+), 262 deletions(-) diff --git a/apps/emqx_authz/etc/emqx_authz.conf b/apps/emqx_authz/etc/emqx_authz.conf index 8eadab38b..99b14f5fe 100644 --- a/apps/emqx_authz/etc/emqx_authz.conf +++ b/apps/emqx_authz/etc/emqx_authz.conf @@ -2,66 +2,56 @@ authorization { sources = [ # { # type: http - # config: { - # url: "https://emqx.com" - # headers: { - # Accept: "application/json" - # Content-Type: "application/json" - # } + # url: "https://emqx.com" + # headers: { + # Accept: "application/json" + # Content-Type: "application/json" # } # }, # { # type: mysql - # config: { - # server: "127.0.0.1:3306" - # database: mqtt - # pool_size: 1 - # username: root - # password: public - # auto_reconnect: true - # ssl: { - # enable: true - # cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" - # certfile: "{{ platform_etc_dir }}/certs/client-cert.pem" - # keyfile: "{{ platform_etc_dir }}/certs/client-key.pem" - # } + # server: "127.0.0.1:3306" + # database: mqtt + # pool_size: 1 + # username: root + # password: public + # auto_reconnect: true + # ssl: { + # enable: true + # cacertfile: "{{ platform_etc_dir }}/certs/cacert.pem" + # certfile: "{{ platform_etc_dir }}/certs/client-cert.pem" + # keyfile: "{{ platform_etc_dir }}/certs/client-key.pem" # } # sql: "select ipaddress, username, clientid, action, permission, topic from mqtt_authz where ipaddr = '%a' or username = '%u' or clientid = '%c'" # }, # { # type: pgsql - # config: { - # server: "127.0.0.1:5432" - # database: mqtt - # pool_size: 1 - # username: root - # password: public - # auto_reconnect: true - # ssl: {enable: false} - # } + # server: "127.0.0.1:5432" + # database: mqtt + # pool_size: 1 + # username: root + # password: public + # auto_reconnect: true + # ssl: {enable: false} # sql: "select ipaddress, username, clientid, action, permission, topic from mqtt_authz where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'" # }, # { # type: redis - # config: { - # server: "127.0.0.1:6379" - # database: 0 - # pool_size: 1 - # password: public - # auto_reconnect: true - # ssl: {enable: false} - # } + # server: "127.0.0.1:6379" + # database: 0 + # pool_size: 1 + # password: public + # auto_reconnect: true + # ssl: {enable: false} # cmd: "HGETALL mqtt_authz:%u" # }, # { # type: mongo - # config: { - # mongo_type: single - # server: "127.0.0.1:27017" - # pool_size: 1 - # database: mqtt - # ssl: {enable: false} - # } + # mongo_type: single + # server: "127.0.0.1:27017" + # pool_size: 1 + # database: mqtt + # ssl: {enable: false} # collection: mqtt_authz # find: { "$or": [ { "username": "%u" }, { "clientid": "%c" } ] } # }, diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index f3b9a4793..0d116882c 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -224,10 +224,10 @@ init_source(#{enable := true, Source#{annotations => #{rules => Rules}}; init_source(#{enable := true, type := http, - config := #{url := Url} = Config + url := Url } = Source) -> - NConfig = maps:merge(Config, #{base_url => maps:remove(query, Url)}), - case create_resource(Source#{config := NConfig}) of + NSource= maps:put(base_url, maps:remove(query, Url), Source), + case create_resource(NSource) of {error, Reason} -> error({load_config_error, Reason}); Id -> Source#{annotations => #{id => Id}} end; @@ -325,16 +325,14 @@ gen_id(Type) -> iolist_to_binary([io_lib:format("~s_~s",[?APP, Type])]). create_resource(#{type := DB, - config := Config, - annotations := #{id := ResourceID}}) -> - case emqx_resource:update(ResourceID, connector_module(DB), Config, []) of + annotations := #{id := ResourceID}} = Source) -> + case emqx_resource:update(ResourceID, connector_module(DB), Source, []) of {ok, _} -> ResourceID; {error, Reason} -> {error, Reason} end; -create_resource(#{type := DB, - config := Config}) -> +create_resource(#{type := DB} = Source) -> ResourceID = gen_id(DB), - case emqx_resource:create(ResourceID, connector_module(DB), Config) of + case emqx_resource:create(ResourceID, connector_module(DB), Source) of {ok, already_created} -> ResourceID; {ok, _} -> ResourceID; {error, Reason} -> {error, Reason} diff --git a/apps/emqx_authz/src/emqx_authz_api_sources.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl index 8bc99188e..b2d33eca5 100644 --- a/apps/emqx_authz/src/emqx_authz_api_sources.erl +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -24,11 +24,10 @@ -define(EXAMPLE_REDIS, #{type=> redis, enable => true, - config => #{server => <<"127.0.0.1:3306">>, - redis_type => single, - pool_size => 1, - auto_reconnect => true - }, + server => <<"127.0.0.1:3306">>, + redis_type => single, + pool_size => 1, + auto_reconnect => true, cmd => <<"HGETALL mqtt_authz">>}). -define(EXAMPLE_FILE, #{type=> file, @@ -308,16 +307,16 @@ sources(get, _) -> rules => [ io_lib:format("~p", [R])|| R <- Rules], annotations => #{status => healthy} }]); - (#{type := _Type, config := Config, annotations := #{id := Id}} = Source, AccIn) -> - NSource0 = case maps:get(server, Config, undefined) of + (#{type := _Type, annotations := #{id := Id}} = Source, AccIn) -> + NSource0 = case maps:get(server, Source, undefined) of undefined -> Source; Server -> - Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}} + Source#{server => emqx_connector_schema_lib:ip_port_to_string(Server)} end, - NSource1 = case maps:get(servers, Config, undefined) of + NSource1 = case maps:get(servers, Source, undefined) of undefined -> NSource0; Servers -> - NSource0#{config => Config#{servers => [emqx_connector_schema_lib:ip_port_to_string(Server) || Server <- Servers]}} + NSource0#{servers => [emqx_connector_schema_lib:ip_port_to_string(Server) || Server <- Servers]} end, NSource2 = case emqx_resource:health_check(Id) of ok -> @@ -377,16 +376,16 @@ source(get, #{bindings := #{type := Type}}) -> annotations => #{status => healthy} } }; - #{config := Config, annotations := #{id := Id}} = Source -> - NSource0 = case maps:get(server, Config, undefined) of + #{annotations := #{id := Id}} = Source -> + NSource0 = case maps:get(server, Source, undefined) of undefined -> Source; Server -> - Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}} + Source#{server => emqx_connector_schema_lib:ip_port_to_string(Server)} end, - NSource1 = case maps:get(servers, Config, undefined) of + NSource1 = case maps:get(servers, Source, undefined) of undefined -> NSource0; Servers -> - NSource0#{config => Config#{servers => [emqx_connector_schema_lib:ip_port_to_string(Server) || Server <- Servers]}} + NSource0#{servers => [emqx_connector_schema_lib:ip_port_to_string(Server) || Server <- Servers]} end, NSource2 = case emqx_resource:health_check(Id) of ok -> @@ -434,7 +433,7 @@ move_source(post, #{bindings := #{type := Type}, body := #{<<"position">> := Pos messgae => atom_to_binary(Reason)}} end. -read_cert(#{config := #{ssl := #{enable := true} = SSL} = Config} = Source) -> +read_cert(#{ssl := #{enable := true} = SSL} = Source) -> CaCert = case file:read_file(maps:get(cacertfile, SSL, "")) of {ok, CaCert0} -> CaCert0; _ -> "" @@ -447,14 +446,14 @@ read_cert(#{config := #{ssl := #{enable := true} = SSL} = Config} = Source) -> {ok, Key0} -> Key0; _ -> "" end, - Source#{config => Config#{ssl => SSL#{cacertfile => CaCert, - certfile => Cert, - keyfile => Key - }} + Source#{ssl => SSL#{cacertfile => CaCert, + certfile => Cert, + keyfile => Key + } }; read_cert(Source) -> Source. -write_cert(#{<<"config">> := #{<<"ssl">> := #{<<"enable">> := true} = SSL} = Config} = Source) -> +write_cert(#{<<"ssl">> := #{<<"enable">> := true} = SSL} = Source) -> CertPath = filename:join([emqx:get_config([node, data_dir]), "certs"]), CaCert = case maps:is_key(<<"cacertfile">>, SSL) of true -> @@ -477,9 +476,9 @@ write_cert(#{<<"config">> := #{<<"ssl">> := #{<<"enable">> := true} = SSL} = Con KeyFile; false -> "" end, - Source#{<<"config">> := Config#{<<"ssl">> => SSL#{<<"cacertfile">> => CaCert, - <<"certfile">> => Cert, - <<"keyfile">> => Key} + Source#{<<"ssl">> => SSL#{<<"cacertfile">> => CaCert, + <<"certfile">> => Cert, + <<"keyfile">> => Key } }; write_cert(Source) -> Source. diff --git a/apps/emqx_authz/src/emqx_authz_http.erl b/apps/emqx_authz/src/emqx_authz_http.erl index c95d200e1..93aa634f3 100644 --- a/apps/emqx_authz/src/emqx_authz_http.erl +++ b/apps/emqx_authz/src/emqx_authz_http.erl @@ -35,12 +35,12 @@ description() -> authorize(Client, PubSub, Topic, #{type := http, - config := #{url := #{path := Path} = Url, - headers := Headers, - method := Method, - request_timeout := RequestTimeout} = Config, + url := #{path := Path} = Url, + headers := Headers, + method := Method, + request_timeout := RequestTimeout, annotations := #{id := ResourceID} - }) -> + } = Source) -> Request = case Method of get -> Query = maps:get(query, Url, ""), @@ -49,7 +49,7 @@ authorize(Client, PubSub, Topic, _ -> Body0 = serialize_body( maps:get('Accept', Headers, <<"application/json">>), - maps:get(body, Config, #{}) + maps:get(body, Source, #{}) ), Body1 = replvar(Body0, PubSub, Topic, Client), Path1 = replvar(Path, PubSub, Topic, Client), diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index 7fb60bae2..4d8fa3579 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -20,7 +20,20 @@ roots() -> ["authorization"]. fields("authorization") -> - [ {sources, sources()} + [ {sources, #{type => union_array( + [ hoconsc:ref(?MODULE, file) + , hoconsc:ref(?MODULE, http_get) + , hoconsc:ref(?MODULE, http_post) + , hoconsc:ref(?MODULE, mongo_single) + , hoconsc:ref(?MODULE, mongo_rs) + , hoconsc:ref(?MODULE, mongo_sharded) + , hoconsc:ref(?MODULE, mysql) + , hoconsc:ref(?MODULE, pgsql) + , hoconsc:ref(?MODULE, redis_single) + , hoconsc:ref(?MODULE, redis_sentinel) + , hoconsc:ref(?MODULE, redis_cluster) + ])} + } ]; fields(file) -> [ {type, #{type => file}} @@ -34,17 +47,11 @@ fields(file) -> end }} ]; -fields(http) -> +fields(http_get) -> [ {type, #{type => http}} , {enable, #{type => boolean(), default => true}} - , {config, #{type => hoconsc:union([ hoconsc:ref(?MODULE, http_get) - , hoconsc:ref(?MODULE, http_post) - ])} - } - ]; -fields(http_get) -> - [ {url, #{type => url()}} + , {url, #{type => url()}} , {headers, #{type => map(), default => #{ <<"accept">> => <<"application/json">> , <<"cache-control">> => <<"no-cache">> @@ -68,7 +75,10 @@ fields(http_get) -> , {request_timeout, #{type => timeout(), default => 30000 }} ] ++ proplists:delete(base_url, emqx_connector_http:fields(config)); fields(http_post) -> - [ {url, #{type => url()}} + [ {type, #{type => http}} + , {enable, #{type => boolean(), + default => true}} + , {url, #{type => url()}} , {headers, #{type => map(), default => #{ <<"accept">> => <<"application/json">> , <<"cache-control">> => <<"no-cache">> @@ -97,47 +107,36 @@ fields(http_post) -> } } ] ++ proplists:delete(base_url, emqx_connector_http:fields(config)); -fields(mongo) -> - connector_fields(mongo) ++ +fields(mongo_single) -> + connector_fields(mongo, single) ++ + [ {collection, #{type => atom()}} + , {find, #{type => map()}} + ]; +fields(mongo_rs) -> + connector_fields(mongo, rs) ++ + [ {collection, #{type => atom()}} + , {find, #{type => map()}} + ]; +fields(mongo_sharded) -> + connector_fields(mongo, sharded) ++ [ {collection, #{type => atom()}} , {find, #{type => map()}} ]; -fields(redis) -> - connector_fields(redis) ++ - [ {cmd, query()} ]; fields(mysql) -> connector_fields(mysql) ++ [ {sql, query()} ]; fields(pgsql) -> connector_fields(pgsql) ++ [ {sql, query()} ]; -fields(username) -> - [{username, #{type => binary()}}]; -fields(clientid) -> - [{clientid, #{type => binary()}}]; -fields(ipaddress) -> - [{ipaddress, #{type => string()}}]; -fields(andlist) -> - [{'and', #{type => union_array( - [ hoconsc:ref(?MODULE, username) - , hoconsc:ref(?MODULE, clientid) - , hoconsc:ref(?MODULE, ipaddress) - ]) - } - } - ]; -fields(orlist) -> - [{'or', #{type => union_array( - [ hoconsc:ref(?MODULE, username) - , hoconsc:ref(?MODULE, clientid) - , hoconsc:ref(?MODULE, ipaddress) - ]) - } - } - ]; -fields(eq_topic) -> - [{eq, #{type => binary()}}]. - +fields(redis_single) -> + connector_fields(redis, single) ++ + [ {cmd, query()} ]; +fields(redis_sentinel) -> + connector_fields(redis, sentinel) ++ + [ {cmd, query()} ]; +fields(redis_cluster) -> + connector_fields(redis, cluster) ++ + [ {cmd, query()} ]. %%-------------------------------------------------------------------- %% Internal functions @@ -146,17 +145,6 @@ fields(eq_topic) -> union_array(Item) when is_list(Item) -> hoconsc:array(hoconsc:union(Item)). -sources() -> - #{type => union_array( - [ hoconsc:ref(?MODULE, file) - , hoconsc:ref(?MODULE, http) - , hoconsc:ref(?MODULE, mysql) - , hoconsc:ref(?MODULE, pgsql) - , hoconsc:ref(?MODULE, redis) - , hoconsc:ref(?MODULE, mongo) - ]) - }. - query() -> #{type => binary(), validator => fun(S) -> @@ -168,6 +156,8 @@ query() -> }. connector_fields(DB) -> + connector_fields(DB, config). +connector_fields(DB, Fields) -> Mod0 = io_lib:format("~s_~s",[emqx_connector, DB]), Mod = try list_to_existing_atom(Mod0) @@ -180,4 +170,4 @@ connector_fields(DB) -> [ {type, #{type => DB}} , {enable, #{type => boolean(), default => true}} - ] ++ Mod:roots(). + ] ++ Mod:fields(Fields). diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index 88a0a9bf3..6e6597486 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -62,56 +62,51 @@ init_per_testcase(_, Config) -> -define(SOURCE1, #{<<"type">> => <<"http">>, <<"enable">> => true, - <<"config">> => #{ - <<"url">> => <<"https://fake.com:443/">>, - <<"headers">> => #{}, - <<"method">> => <<"get">>, - <<"request_timeout">> => 5000} + <<"url">> => <<"https://fake.com:443/">>, + <<"headers">> => #{}, + <<"method">> => <<"get">>, + <<"request_timeout">> => 5000 }). -define(SOURCE2, #{<<"type">> => <<"mongo">>, <<"enable">> => true, - <<"config">> => #{ - <<"mongo_type">> => <<"single">>, - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"ssl">> => #{<<"enable">> => false}}, + <<"mongo_type">> => <<"single">>, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"ssl">> => #{<<"enable">> => false}, <<"collection">> => <<"fake">>, <<"find">> => #{<<"a">> => <<"b">>} }). -define(SOURCE3, #{<<"type">> => <<"mysql">>, <<"enable">> => true, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"username">> => <<"xx">>, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, <<"sql">> => <<"abcb">> }). -define(SOURCE4, #{<<"type">> => <<"pgsql">>, <<"enable">> => true, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"username">> => <<"xx">>, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, <<"sql">> => <<"abcb">> }). -define(SOURCE5, #{<<"type">> => <<"redis">>, <<"enable">> => true, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => 0, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => 0, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, <<"cmd">> => <<"HGETALL mqtt_authz:%u">> }). -define(SOURCE6, #{<<"type">> => <<"file">>, diff --git a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl index bb2bb27e4..3c054aa7d 100644 --- a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl @@ -39,61 +39,55 @@ -define(SOURCE1, #{<<"type">> => <<"http">>, <<"enable">> => true, - <<"config">> => #{ - <<"url">> => <<"https://fake.com:443/">>, - <<"headers">> => #{}, - <<"method">> => <<"get">>, - <<"request_timeout">> => 5000} + <<"url">> => <<"https://fake.com:443/">>, + <<"headers">> => #{}, + <<"method">> => <<"get">>, + <<"request_timeout">> => 5000 }). -define(SOURCE2, #{<<"type">> => <<"mongo">>, <<"enable">> => true, - <<"config">> => #{ - <<"mongo_type">> => <<"sharded">>, - <<"servers">> => [<<"127.0.0.1:27017">>, - <<"192.168.0.1:27017">> - ], - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"ssl">> => #{<<"enable">> => false}}, + <<"mongo_type">> => <<"sharded">>, + <<"servers">> => [<<"127.0.0.1:27017">>, + <<"192.168.0.1:27017">> + ], + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"ssl">> => #{<<"enable">> => false}, <<"collection">> => <<"fake">>, <<"find">> => #{<<"a">> => <<"b">>} }). -define(SOURCE3, #{<<"type">> => <<"mysql">>, <<"enable">> => true, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:3306">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"username">> => <<"xx">>, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, + <<"server">> => <<"127.0.0.1:3306">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, <<"sql">> => <<"abcb">> }). -define(SOURCE4, #{<<"type">> => <<"pgsql">>, <<"enable">> => true, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:5432">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"username">> => <<"xx">>, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, + <<"server">> => <<"127.0.0.1:5432">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, <<"sql">> => <<"abcb">> }). -define(SOURCE5, #{<<"type">> => <<"redis">>, <<"enable">> => true, - <<"config">> => #{ - <<"servers">> => [<<"127.0.0.1:6379">>, - <<"127.0.0.1:6380">> - ], - <<"pool_size">> => 1, - <<"database">> => 0, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false} - }, + <<"servers">> => [<<"127.0.0.1:6379">>, + <<"127.0.0.1:6380">> + ], + <<"pool_size">> => 1, + <<"database">> => 0, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, <<"cmd">> => <<"HGETALL mqtt_authz:%u">> }). -define(SOURCE6, #{<<"type">> => <<"file">>, @@ -207,27 +201,22 @@ t_api(_) -> {ok, 200, Result4} = request(get, uri(["authorization", "sources", "http"]), []), ?assertMatch(#{<<"type">> := <<"http">>, <<"enable">> := false}, jsx:decode(Result4)), - #{<<"config">> := Config} = ?SOURCE2, - - dbg:tracer(),dbg:p(all,c), - dbg:tpl(emqx_authz_api_sources, read_cert, cx), - dbg:tpl(emqx_authz_api_sources, write_cert, cx), {ok, 204, _} = request(put, uri(["authorization", "sources", "mongo"]), - ?SOURCE2#{<<"config">> := Config#{<<"ssl">> := #{ - <<"enable">> => true, - <<"cacertfile">> => <<"fake cacert file">>, - <<"certfile">> => <<"fake cert file">>, - <<"keyfile">> => <<"fake key file">>, - <<"verify">> => false - }}}), + ?SOURCE2#{<<"ssl">> := #{ + <<"enable">> => true, + <<"cacertfile">> => <<"fake cacert file">>, + <<"certfile">> => <<"fake cert file">>, + <<"keyfile">> => <<"fake key file">>, + <<"verify">> => false + }}), {ok, 200, Result5} = request(get, uri(["authorization", "sources", "mongo"]), []), ?assertMatch(#{<<"type">> := <<"mongo">>, - <<"config">> := #{<<"ssl">> := #{<<"enable">> := true, - <<"cacertfile">> := <<"fake cacert file">>, - <<"certfile">> := <<"fake cert file">>, - <<"keyfile">> := <<"fake key file">>, - <<"verify">> := false - }} + <<"ssl">> := #{<<"enable">> := true, + <<"cacertfile">> := <<"fake cacert file">>, + <<"certfile">> := <<"fake cert file">>, + <<"keyfile">> := <<"fake key file">>, + <<"verify">> := false + } }, jsx:decode(Result5)), ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "cacert-fake.pem"]))), ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "cert-fake.pem"]))), diff --git a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl index fad5e9580..17763d993 100644 --- a/apps/emqx_authz/test/emqx_authz_http_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_http_SUITE.erl @@ -47,12 +47,11 @@ init_per_suite(Config) -> {ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, no_match], deny), Rules = [#{<<"type">> => <<"http">>, - <<"config">> => #{ - <<"url">> => <<"https://fake.com:443/">>, - <<"headers">> => #{}, - <<"method">> => <<"get">>, - <<"request_timeout">> => 5000 - }} + <<"url">> => <<"https://fake.com:443/">>, + <<"headers">> => #{}, + <<"method">> => <<"get">>, + <<"request_timeout">> => 5000 + } ], {ok, _} = emqx_authz:update(replace, Rules), Config. diff --git a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl index db111ce83..8f4a6f29f 100644 --- a/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mongo_SUITE.erl @@ -47,12 +47,11 @@ init_per_suite(Config) -> {ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, no_match], deny), Rules = [#{<<"type">> => <<"mongo">>, - <<"config">> => #{ - <<"mongo_type">> => <<"single">>, - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"ssl">> => #{<<"enable">> => false}}, + <<"mongo_type">> => <<"single">>, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"ssl">> => #{<<"enable">> => false}, <<"collection">> => <<"fake">>, <<"find">> => #{<<"a">> => <<"b">>} }], diff --git a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl index 0675e1caf..1173b0e3e 100644 --- a/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_mysql_SUITE.erl @@ -48,14 +48,13 @@ init_per_suite(Config) -> {ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, no_match], deny), Rules = [#{<<"type">> => <<"mysql">>, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"username">> => <<"xx">>, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, <<"sql">> => <<"abcb">> }], {ok, _} = emqx_authz:update(replace, Rules), diff --git a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl index 6880ab405..24c2e7b35 100644 --- a/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_pgsql_SUITE.erl @@ -48,14 +48,13 @@ init_per_suite(Config) -> {ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, no_match], deny), Rules = [#{<<"type">> => <<"pgsql">>, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => <<"mqtt">>, - <<"username">> => <<"xx">>, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => <<"mqtt">>, + <<"username">> => <<"xx">>, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, <<"sql">> => <<"abcb">> }], {ok, _} = emqx_authz:update(replace, Rules), diff --git a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl index 09682761d..9949e8b51 100644 --- a/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_redis_SUITE.erl @@ -47,13 +47,12 @@ init_per_suite(Config) -> {ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, no_match], deny), Rules = [#{<<"type">> => <<"redis">>, - <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, - <<"pool_size">> => 1, - <<"database">> => 0, - <<"password">> => <<"ee">>, - <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, + <<"server">> => <<"127.0.0.1:27017">>, + <<"pool_size">> => 1, + <<"database">> => 0, + <<"password">> => <<"ee">>, + <<"auto_reconnect">> => true, + <<"ssl">> => #{<<"enable">> => false}, <<"cmd">> => <<"HGETALL mqtt_authz:%u">> }], {ok, _} = emqx_authz:update(replace, Rules), From 516f2fd06e988cb370c89530cec7f492aa7a495b Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Thu, 2 Sep 2021 15:30:53 +0800 Subject: [PATCH 033/109] fix: listener api doc (#5627) --- apps/emqx_management/src/emqx_mgmt_api_listeners.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl index 51487fb2a..ad8ce0f67 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_listeners.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_listeners.erl @@ -111,7 +111,7 @@ api_list_listeners_on_node() -> description => <<"List listeners in one node">>, parameters => [param_path_node()], responses => #{ - <<"200">> => emqx_mgmt_util:object_schema(resp_schema(), <<"List listeners successfully">>)}}}, + <<"200">> => emqx_mgmt_util:schema(resp_schema(), <<"List listeners successfully">>)}}}, {"/nodes/:node/listeners", Metadata, list_listeners_on_node}. api_get_update_listener_by_id_on_node() -> @@ -124,7 +124,7 @@ api_get_update_listener_by_id_on_node() -> emqx_mgmt_util:error_schema(?NODE_LISTENER_NOT_FOUND, ['BAD_NODE_NAME', 'BAD_LISTENER_ID']), <<"200">> => - emqx_mgmt_util:object_schema(resp_schema(), <<"Get listener successfully">>)}}, + emqx_mgmt_util:schema(resp_schema(), <<"Get listener successfully">>)}}, put => #{ description => <<"Create or update a listener by a given Id on a specific node">>, parameters => [param_path_node(), param_path_id()], @@ -134,7 +134,7 @@ api_get_update_listener_by_id_on_node() -> emqx_mgmt_util:error_schema(?NODE_LISTENER_NOT_FOUND, ['BAD_NODE_NAME', 'BAD_LISTENER_ID']), <<"200">> => - emqx_mgmt_util:object_schema(resp_schema(), <<"Get listener successfully">>)}}, + emqx_mgmt_util:schema(resp_schema(), <<"Get listener successfully">>)}}, delete => #{ description => <<"Delete a listener by a given Id to all nodes in the cluster">>, parameters => [param_path_node(), param_path_id()], From b014266fa04e72b8a7b2d38d9216ce2af7b76280 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 2 Sep 2021 14:23:24 +0800 Subject: [PATCH 034/109] chore(connector http): update ssl for http connector --- apps/emqx_connector/src/emqx_connector_http.erl | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/apps/emqx_connector/src/emqx_connector_http.erl b/apps/emqx_connector/src/emqx_connector_http.erl index 572b2a4e8..159562f33 100644 --- a/apps/emqx_connector/src/emqx_connector_http.erl +++ b/apps/emqx_connector/src/emqx_connector_http.erl @@ -58,9 +58,7 @@ fields(config) -> , {pool_type, fun pool_type/1} , {pool_size, fun pool_size/1} , {enable_pipelining, fun enable_pipelining/1} - , {ssl_opts, #{type => hoconsc:ref(?MODULE, ssl_opts), - default => #{}}} - ]; + ] ++ emqx_connector_schema_lib:ssl_fields(); fields(ssl_opts) -> [ {cacertfile, fun cacertfile/1} @@ -200,12 +198,11 @@ check_ssl_opts(Conf) -> check_ssl_opts(URLFrom, Conf) -> #{schema := Scheme} = hocon_schema:get_value(URLFrom, Conf), - SSLOpts = hocon_schema:get_value("ssl_opts", Conf), - case {Scheme, maps:size(SSLOpts)} of - {http, 0} -> true; - {http, _} -> false; - {https, 0} -> false; - {https, _} -> true + SSL= hocon_schema:get_value("ssl", Conf), + case {Scheme, maps:get(enable, SSL, false)} of + {http, false} -> true; + {https, true} -> true; + {_, _} -> false end. update_path(BasePath, {Path, Headers}) -> From 3bc92e5845b9fb550b39c6714a94b17f49adbcc8 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Thu, 2 Sep 2021 17:16:13 +0800 Subject: [PATCH 035/109] fix: mgmt listener cli (#5632) * fix: mgmt cli linteners --- apps/emqx/src/emqx_listeners.erl | 24 +++++++++++++ apps/emqx_management/src/emqx_mgmt_cli.erl | 40 ++++++++++++---------- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 399bd3d08..8f0141b3a 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -26,6 +26,8 @@ , restart/0 , stop/0 , is_running/1 + , current_conns/2 + , max_conns/2 ]). -export([ start_listener/1 @@ -89,6 +91,28 @@ is_running(quic, _ListenerId, _Conf)-> %% TODO: quic support {error, no_found}. +current_conns(ID, ListenOn) -> + {Type, Name} = parse_listener_id(ID), + current_conns(Type, Name, ListenOn). + +current_conns(Type, Name, ListenOn) when Type == tcl; Type == ssl -> + esockd:get_current_connections({listener_id(Type, Name), ListenOn}); +current_conns(Type, Name, _ListenOn) when Type =:= ws; Type =:= wss -> + proplists:get_value(all_connections, ranch:info(listener_id(Type, Name))); +current_conns(_, _, _) -> + {error, not_support}. + +max_conns(ID, ListenOn) -> + {Type, Name} = parse_listener_id(ID), + max_conns(Type, Name, ListenOn). + +max_conns(Type, Name, ListenOn) when Type == tcl; Type == ssl -> + esockd:get_max_connections({listener_id(Type, Name), ListenOn}); +max_conns(Type, Name, _ListenOn) when Type =:= ws; Type =:= wss -> + proplists:get_value(max_connections, ranch:info(listener_id(Type, Name))); +max_conns(_, _, _) -> + {error, not_support}. + %% @doc Start all listeners. -spec(start() -> ok). start() -> diff --git a/apps/emqx_management/src/emqx_mgmt_cli.erl b/apps/emqx_management/src/emqx_mgmt_cli.erl index 3d4dea31e..c7fb8a7b7 100644 --- a/apps/emqx_management/src/emqx_mgmt_cli.erl +++ b/apps/emqx_management/src/emqx_mgmt_cli.erl @@ -412,26 +412,28 @@ trace_off(Who, Name) -> %% @doc Listeners Command listeners([]) -> - lists:foreach(fun({{Protocol, ListenOn}, _Pid}) -> - Info = [{listen_on, {string, format_listen_on(ListenOn)}}, - {acceptors, esockd:get_acceptors({Protocol, ListenOn})}, - {max_conns, esockd:get_max_connections({Protocol, ListenOn})}, - {current_conn, esockd:get_current_connections({Protocol, ListenOn})}, - {shutdown_count, esockd:get_shutdown_count({Protocol, ListenOn})} - ], - emqx_ctl:print("~s~n", [Protocol]), + lists:foreach(fun({ID, Conf}) -> + {Host, Port} = maps:get(bind, Conf), + Acceptors = maps:get(acceptors, Conf), + ProxyProtocol = maps:get(proxy_protocol, Conf, undefined), + Running = maps:get(running, Conf), + CurrentConns = case emqx_listeners:current_conns(ID, {Host, Port}) of + {error, _} -> []; + CC -> [{current_conn, CC}] + end, + MaxConn = case emqx_listeners:max_conns(ID, {Host, Port}) of + {error, _} -> []; + MC -> [{max_conns, MC}] + end, + Info = [ + {listen_on, {string, format_listen_on(Port)}}, + {acceptors, Acceptors}, + {proxy_protocol, ProxyProtocol}, + {running, Running} + ] ++ CurrentConns ++ MaxConn, + emqx_ctl:print("~s~n", [ID]), lists:foreach(fun indent_print/1, Info) - end, esockd:listeners()), - lists:foreach(fun({Protocol, Opts}) -> - Port = proplists:get_value(port, Opts), - Info = [{listen_on, {string, format_listen_on(Port)}}, - {acceptors, maps:get(num_acceptors, proplists:get_value(transport_options, Opts, #{}), 0)}, - {max_conns, proplists:get_value(max_connections, Opts)}, - {current_conn, proplists:get_value(all_connections, Opts)}, - {shutdown_count, []}], - emqx_ctl:print("~s~n", [Protocol]), - lists:foreach(fun indent_print/1, Info) - end, ranch:info()); + end, emqx_listeners:list()); listeners(["stop", ListenerId]) -> case emqx_listeners:stop_listener(list_to_atom(ListenerId)) of From fae12051bbd5d293ad404ffbdf8d5b1c16398c17 Mon Sep 17 00:00:00 2001 From: William Yang Date: Thu, 2 Sep 2021 08:44:32 +0200 Subject: [PATCH 036/109] chore: github issue flow, add need-triage label --- .github/ISSUE_TEMPLATE/bug-report.md | 2 +- .github/ISSUE_TEMPLATE/feature-request.md | 2 +- .github/ISSUE_TEMPLATE/support-needed.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 96d193913..3ec513a37 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -2,7 +2,7 @@ name: Bug Report about: Create a report to help us improve title: '' -labels: Support +labels: "Support, needs-triage" --- diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index 0519e5699..1fb5f401f 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -2,7 +2,7 @@ name: Feature Request about: Suggest an idea for this project title: '' -labels: Feature +labels: "Feature, needs-triage" --- diff --git a/.github/ISSUE_TEMPLATE/support-needed.md b/.github/ISSUE_TEMPLATE/support-needed.md index 18b47bfb5..a19299c42 100644 --- a/.github/ISSUE_TEMPLATE/support-needed.md +++ b/.github/ISSUE_TEMPLATE/support-needed.md @@ -2,7 +2,7 @@ name: Support Needed about: Asking a question about usages, docs or anything you're insterested in title: '' -labels: Support +labels: "Support, needs-triage" --- From c6ed72df3e84a4e6ab8565690c849da4a1ae289e Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 2 Sep 2021 16:44:31 +0800 Subject: [PATCH 037/109] chore(authz api): update swagger json Signed-off-by: zhanghongtong --- apps/emqx_authz/src/emqx_authz_api_schema.erl | 457 ++++++++++++++++-- apps/emqx_authz/src/emqx_authz_schema.erl | 7 +- 2 files changed, 408 insertions(+), 56 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz_api_schema.erl b/apps/emqx_authz/src/emqx_authz_api_schema.erl index 18a5e2b18..09f145075 100644 --- a/apps/emqx_authz/src/emqx_authz_api_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_api_schema.erl @@ -41,7 +41,15 @@ definitions() -> ] }, Sources = #{ - oneOf => [ minirest:ref(<<"redis">>) + oneOf => [ minirest:ref(<<"http">>) + , minirest:ref(<<"mongo_single">>) + , minirest:ref(<<"mongo_rs">>) + , minirest:ref(<<"mongo_sharded">>) + , minirest:ref(<<"mysql">>) + , minirest:ref(<<"pgsql">>) + , minirest:ref(<<"redis_single">>) + , minirest:ref(<<"redis_sentinel">>) + , minirest:ref(<<"redis_cluster">>) , minirest:ref(<<"file">>) ] }, @@ -56,9 +64,309 @@ definitions() -> verify => #{type => boolean, example => false} } }, - Redis = #{ + HTTP = #{ type => object, - required => [type, enable, config, cmd], + required => [ type + , enable + , method + , headers + , request_timeout + , connect_timeout + , max_retries + , retry_interval + , pool_type + , pool_size + , enable_pipelining + , ssl + ], + properties => #{ + type => #{ + type => string, + enum => [<<"http">>], + example => <<"http">> + }, + enable => #{ + type => boolean, + example => true + }, + url => #{ + type => string, + example => <<"https://emqx.com">> + }, + method => #{ + type => string, + enum => [<<"get">>, <<"post">>, <<"put">>], + example => <<"get">> + }, + headers => #{type => object}, + body => #{type => object}, + connect_timeout => #{type => integer}, + max_retries => #{type => integer}, + retry_interval => #{type => integer}, + pool_type => #{ + type => string, + enum => [<<"random">>, <<"hash">>], + example => <<"random">> + }, + pool_size => #{type => integer}, + enable_pipelining => #{type => boolean}, + ssl => minirest:ref(<<"ssl">>) + } + }, + MongoSingle= #{ + type => object, + required => [ type + , enable + , collection + , find + , mongo_type + , server + , pool_size + , username + , password + , auth_source + , database + , topology + , ssl + ], + properties => #{ + type => #{ + type => string, + enum => [<<"mongo">>], + example => <<"mongo">> + }, + enable => #{ + type => boolean, + example => true + }, + collection => #{type => string}, + find => #{type => object}, + mongo_type => #{type => string, + enum => [<<"single">>], + example => <<"single">>}, + server => #{type => string, example => <<"127.0.0.1:27017">>}, + pool_size => #{type => integer}, + username => #{type => string}, + password => #{type => string}, + auth_source => #{type => string}, + database => #{type => string}, + topology => #{type => object, + properties => #{ + pool_size => #{type => integer}, + max_overflow => #{type => integer}, + overflow_ttl => #{type => integer}, + overflow_check_period => #{type => integer}, + local_threshold_ms => #{type => integer}, + connect_timeout_ms => #{type => integer}, + socket_timeout_ms => #{type => integer}, + server_selection_timeout_ms => #{type => integer}, + wait_queue_timeout_ms => #{type => integer}, + heartbeat_frequency_ms => #{type => integer}, + min_heartbeat_frequency_ms => #{type => integer} + } + }, + ssl => minirest:ref(<<"ssl">>) + } + }, + MongoRs= #{ + type => object, + required => [ type + , enable + , collection + , find + , mongo_type + , servers + , replica_set_name + , pool_size + , username + , password + , auth_source + , database + , topology + , ssl + ], + properties => #{ + type => #{ + type => string, + enum => [<<"mongo">>], + example => <<"mongo">> + }, + enable => #{ + type => boolean, + example => true + }, + collection => #{type => string}, + find => #{type => object}, + mongo_type => #{type => string, + enum => [<<"rs">>], + example => <<"rs">>}, + servers => #{type => array, + items => #{type => string,example => <<"127.0.0.1:27017">>}}, + replica_set_name => #{type => string}, + pool_size => #{type => integer}, + username => #{type => string}, + password => #{type => string}, + auth_source => #{type => string}, + database => #{type => string}, + topology => #{type => object, + properties => #{ + pool_size => #{type => integer}, + max_overflow => #{type => integer}, + overflow_ttl => #{type => integer}, + overflow_check_period => #{type => integer}, + local_threshold_ms => #{type => integer}, + connect_timeout_ms => #{type => integer}, + socket_timeout_ms => #{type => integer}, + server_selection_timeout_ms => #{type => integer}, + wait_queue_timeout_ms => #{type => integer}, + heartbeat_frequency_ms => #{type => integer}, + min_heartbeat_frequency_ms => #{type => integer} + } + }, + ssl => minirest:ref(<<"ssl">>) + } + }, + MongoSharded = #{ + type => object, + required => [ type + , enable + , collection + , find + , mongo_type + , servers + , pool_size + , username + , password + , auth_source + , database + , topology + , ssl + ], + properties => #{ + type => #{ + type => string, + enum => [<<"mongo">>], + example => <<"mongo">> + }, + enable => #{ + type => boolean, + example => true + }, + collection => #{type => string}, + find => #{type => object}, + mongo_type => #{type => string, + enum => [<<"sharded">>], + example => <<"sharded">>}, + servers => #{type => array, + items => #{type => string,example => <<"127.0.0.1:27017">>}}, + pool_size => #{type => integer}, + username => #{type => string}, + password => #{type => string}, + auth_source => #{type => string}, + database => #{type => string}, + topology => #{type => object, + properties => #{ + pool_size => #{type => integer}, + max_overflow => #{type => integer}, + overflow_ttl => #{type => integer}, + overflow_check_period => #{type => integer}, + local_threshold_ms => #{type => integer}, + connect_timeout_ms => #{type => integer}, + socket_timeout_ms => #{type => integer}, + server_selection_timeout_ms => #{type => integer}, + wait_queue_timeout_ms => #{type => integer}, + heartbeat_frequency_ms => #{type => integer}, + min_heartbeat_frequency_ms => #{type => integer} + } + }, + ssl => minirest:ref(<<"ssl">>) + } + }, + Mysql = #{ + type => object, + required => [ type + , enable + , sql + , server + , database + , pool_size + , username + , password + , auto_reconnect + , ssl + ], + properties => #{ + type => #{ + type => string, + enum => [<<"mysql">>], + example => <<"mysql">> + }, + enable => #{ + type => boolean, + example => true + }, + sql => #{type => string}, + server => #{type => string, + example => <<"127.0.0.1:3306">> + }, + database => #{type => string}, + pool_size => #{type => integer}, + username => #{type => string}, + password => #{type => string}, + auto_reconnect => #{type => boolean, + example => true + }, + ssl => minirest:ref(<<"ssl">>) + } + }, + Pgsql = #{ + type => object, + required => [ type + , enable + , sql + , server + , database + , pool_size + , username + , password + , auto_reconnect + , ssl + ], + properties => #{ + type => #{ + type => string, + enum => [<<"pgsql">>], + example => <<"pgsql">> + }, + enable => #{ + type => boolean, + example => true + }, + sql => #{type => string}, + server => #{type => string, + example => <<"127.0.0.1:5432">> + }, + database => #{type => string}, + pool_size => #{type => integer}, + username => #{type => string}, + password => #{type => string}, + auto_reconnect => #{type => boolean, + example => true + }, + ssl => minirest:ref(<<"ssl">>) + } + }, + RedisSingle = #{ + type => object, + required => [ type + , enable + , cmd + , server + , redis_type + , pool_size + , auto_reconnect + , ssl + ], properties => #{ type => #{ type => string, @@ -69,59 +377,94 @@ definitions() -> type => boolean, example => true }, - config => #{ - oneOf => [ #{type => object, - required => [server, redis_type, pool_size, auto_reconnect], - properties => #{ - server => #{type => string, example => <<"127.0.0.1:3306">>}, - redis_type => #{type => string, - enum => [<<"single">>], - example => <<"single">>}, - pool_size => #{type => integer}, - auto_reconnect => #{type => boolean, example => true}, - password => #{type => string}, - database => #{type => integer}, - ssl => minirest:ref(<<"ssl">>) - } - } - , #{type => object, - required => [servers, redis_type, sentinel, pool_size, auto_reconnect], - properties => #{ - servers => #{type => array, - items => #{type => string,example => <<"127.0.0.1:3306">>}}, - redis_type => #{type => string, - enum => [<<"sentinel">>], - example => <<"sentinel">>}, - sentinel => #{type => string}, - pool_size => #{type => integer}, - auto_reconnect => #{type => boolean, example => true}, - password => #{type => string}, - database => #{type => integer}, - ssl => minirest:ref(<<"ssl">>) - } - } - , #{type => object, - required => [servers, redis_type, pool_size, auto_reconnect], - properties => #{ - servers => #{type => array, - items => #{type => string, example => <<"127.0.0.1:3306">>}}, - redis_type => #{type => string, - enum => [<<"cluster">>], - example => <<"cluster">>}, - pool_size => #{type => integer}, - auto_reconnect => #{type => boolean, example => true}, - password => #{type => string}, - database => #{type => integer}, - ssl => minirest:ref(<<"ssl">>) - } - } - ], - type => object + cmd => #{ + type => string, + example => <<"HGETALL mqtt_authz">> + }, + server => #{type => string, example => <<"127.0.0.1:3306">>}, + redis_type => #{type => string, + enum => [<<"single">>], + example => <<"single">>}, + pool_size => #{type => integer}, + auto_reconnect => #{type => boolean, example => true}, + password => #{type => string}, + database => #{type => integer}, + ssl => minirest:ref(<<"ssl">>) + } + }, + RedisSentinel= #{ + type => object, + required => [ type + , enable + , cmd + , servers + , redis_type + , sentinel + , pool_size + , auto_reconnect + , ssl + ], + properties => #{ + type => #{ + type => string, + enum => [<<"redis">>], + example => <<"redis">> + }, + enable => #{ + type => boolean, + example => true }, cmd => #{ type => string, example => <<"HGETALL mqtt_authz">> - } + }, + servers => #{type => array, + items => #{type => string,example => <<"127.0.0.1:3306">>}}, + redis_type => #{type => string, + enum => [<<"sentinel">>], + example => <<"sentinel">>}, + sentinel => #{type => string}, + pool_size => #{type => integer}, + auto_reconnect => #{type => boolean, example => true}, + password => #{type => string}, + database => #{type => integer}, + ssl => minirest:ref(<<"ssl">>) + } + }, + RedisCluster= #{ + type => object, + required => [ type + , enable + , cmd + , servers + , redis_type + , pool_size + , auto_reconnect + , ssl], + properties => #{ + type => #{ + type => string, + enum => [<<"redis">>], + example => <<"redis">> + }, + enable => #{ + type => boolean, + example => true + }, + cmd => #{ + type => string, + example => <<"HGETALL mqtt_authz">> + }, + servers => #{type => array, + items => #{type => string, example => <<"127.0.0.1:3306">>}}, + redis_type => #{type => string, + enum => [<<"cluster">>], + example => <<"cluster">>}, + pool_size => #{type => integer}, + auto_reconnect => #{type => boolean, example => true}, + password => #{type => string}, + database => #{type => integer}, + ssl => minirest:ref(<<"ssl">>) } }, File = #{ @@ -153,6 +496,14 @@ definitions() -> [ #{<<"returned_sources">> => RetruenedSources} , #{<<"sources">> => Sources} , #{<<"ssl">> => SSL} - , #{<<"redis">> => Redis} + , #{<<"http">> => HTTP} + , #{<<"mongo_single">> => MongoSingle} + , #{<<"mongo_rs">> => MongoRs} + , #{<<"mongo_sharded">> => MongoSharded} + , #{<<"mysql">> => Mysql} + , #{<<"pgsql">> => Pgsql} + , #{<<"redis_single">> => RedisSingle} + , #{<<"redis_sentinel">> => RedisSentinel} + , #{<<"redis_cluster">> => RedisCluster} , #{<<"file">> => File} ]. diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index 4d8fa3579..251e40fe6 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -52,6 +52,7 @@ fields(http_get) -> , {enable, #{type => boolean(), default => true}} , {url, #{type => url()}} + , {method, #{type => get, default => get }} , {headers, #{type => map(), default => #{ <<"accept">> => <<"application/json">> , <<"cache-control">> => <<"no-cache">> @@ -71,7 +72,6 @@ fields(http_get) -> end } } - , {method, #{type => get, default => get }} , {request_timeout, #{type => timeout(), default => 30000 }} ] ++ proplists:delete(base_url, emqx_connector_http:fields(config)); fields(http_post) -> @@ -79,6 +79,8 @@ fields(http_post) -> , {enable, #{type => boolean(), default => true}} , {url, #{type => url()}} + , {method, #{type => hoconsc:enum([post, put]), + default => get}} , {headers, #{type => map(), default => #{ <<"accept">> => <<"application/json">> , <<"cache-control">> => <<"no-cache">> @@ -100,8 +102,7 @@ fields(http_post) -> end } } - , {method, #{type => hoconsc:enum([post, put]), - default => get}} + , {request_timeout, #{type => timeout(), default => 30000 }} , {body, #{type => map(), nullable => true } From cd43bb42a7266f7a7b6d293dbe44c1277423a9e4 Mon Sep 17 00:00:00 2001 From: William Yang Date: Thu, 2 Sep 2021 18:37:29 +0200 Subject: [PATCH 038/109] fix(helm-chart): force headless svc ready while pod is not ready fixs: #5254 The dist port behind headless svc should to be accessible during emqx cluster boot. Endpoints of headless SVC is not in 'ready' state that prevents nodes to talk to each other, this issue only happens when K8s host node is restarted and all emqx nodes are deployed on the same host. --- deploy/charts/emqx/templates/service.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/charts/emqx/templates/service.yaml b/deploy/charts/emqx/templates/service.yaml index 3e9f06b52..54efa6426 100644 --- a/deploy/charts/emqx/templates/service.yaml +++ b/deploy/charts/emqx/templates/service.yaml @@ -112,6 +112,7 @@ spec: type: ClusterIP sessionAffinity: None clusterIP: None + publishNotReadyAddresses: true ports: - name: mqtt port: {{ .Values.service.mqtt | default 1883 }} From cfe64d9c6ff873a6cc7919ca141508f9026e4b60 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 3 Sep 2021 09:44:10 +0800 Subject: [PATCH 039/109] fix(gw): not packing udp packages --- .../src/bhvrs/emqx_gateway_conn.erl | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl b/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl index fa0a830e5..9797a6a4a 100644 --- a/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl +++ b/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl @@ -226,6 +226,9 @@ esockd_send(Data, #state{socket = {udp, _SockPid, Sock}, esockd_send(Data, #state{socket = {esockd_transport, Sock}}) -> esockd_transport:async_send(Sock, Data). +is_datadram_socket({esockd_transport, _}) -> false; +is_datadram_socket({udp, _, _}) -> true. + %%-------------------------------------------------------------------- %% callbacks %%-------------------------------------------------------------------- @@ -678,8 +681,20 @@ with_channel(Fun, Args, State = #state{ %%-------------------------------------------------------------------- %% Handle outgoing packets -handle_outgoing(Packets, State) when is_list(Packets) -> - send(lists:map(serialize_and_inc_stats_fun(State), Packets), State); +handle_outgoing(_Packets = [], _State) -> + ok; +handle_outgoing(Packets, + State = #state{socket = Socket}) when is_list(Packets) -> + case is_datadram_socket(Socket) of + false -> + send( + lists:map(serialize_and_inc_stats_fun(State), Packets), + State); + _ -> + lists:foreach(fun(Packet) -> + handle_outgoing(Packet, State) + end, Packets) + end; handle_outgoing(Packet, State) -> send((serialize_and_inc_stats_fun(State))(Packet), State). From 3f0ef7efa8b037c403433a622186f27135e783c1 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Tue, 31 Aug 2021 09:31:10 +0800 Subject: [PATCH 040/109] feat(gw): add api_clients swagger defination --- apps/emqx_gateway/src/coap/emqx_coap_impl.erl | 8 +- apps/emqx_gateway/src/emqx_gateway_api.erl | 24 +- .../src/emqx_gateway_api_client.erl | 280 +++++++++++++++++- .../src/exproto/emqx_exproto_impl.erl | 8 +- .../src/lwm2m/emqx_lwm2m_impl.erl | 8 +- apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl | 8 +- .../src/stomp/emqx_stomp_impl.erl | 8 +- 7 files changed, 316 insertions(+), 28 deletions(-) diff --git a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl index da2f2b8e9..b0714f5e9 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl @@ -89,11 +89,11 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start ~s:~s:~s listener on ~s successfully.~n", + ?ULOG("Start listener ~s:~s:~s on ~s successfully.~n", [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start ~s:~s:~s listener on ~s: ~0p~n", + ?ELOG("Failed to start listener ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. @@ -121,10 +121,10 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop ~s:~s:~s listener on ~s successfully.~n", + ok -> ?ULOG("Stop listener ~s:~s:~s on ~s successfully.~n", [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop ~s:~s:~s listener on ~s: ~0p~n", + ?ELOG("Failed to stop listener ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. diff --git a/apps/emqx_gateway/src/emqx_gateway_api.erl b/apps/emqx_gateway/src/emqx_gateway_api.erl index f38624ed9..b9ae0a234 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api.erl @@ -348,9 +348,9 @@ gateway_insta(delete, #{bindings := #{name := Name0}}) -> Name = binary_to_existing_atom(Name0), case emqx_gateway:unload(Name) of ok -> - {200, ok}; + {200}; {error, not_found} -> - {404, <<"Not Found">>} + return_http_error(404, <<"Gateway not found">>) end; gateway_insta(get, #{bindings := #{name := Name0}}) -> Name = binary_to_existing_atom(Name0), @@ -363,7 +363,7 @@ gateway_insta(get, #{bindings := #{name := Name0}}) -> ), {200, emqx_map_lib:deep_get([<<"gateway">>, Name0], RawConf)}; undefined -> - {404, <<"Not Found">>} + return_http_error(404, <<"Gateway not found">>) end; gateway_insta(put, #{body := RawConfsIn, bindings := #{name := Name} @@ -371,12 +371,22 @@ gateway_insta(put, #{body := RawConfsIn, %% FIXME: Cluster Consistence ?? case emqx_gateway:update_rawconf(Name, RawConfsIn) of ok -> - {200, <<"Changed">>}; + {200}; {error, not_found} -> - {404, <<"Not Found">>}; + return_http_error(404, <<"Gateway not found">>); {error, Reason} -> - {500, emqx_gateway_utils:stringfy(Reason)} + return_http_error(500, Reason) end. gateway_insta_stats(get, _Req) -> - {401, <<"Implement it later (maybe 5.1)">>}. + return_http_error(401, <<"Implement it later (maybe 5.1)">>). + +return_http_error(Code, Msg) -> + emqx_json:encode( + #{code => codestr(Code), + reason => emqx_gateway_utils:stringfy(Msg) + }). + +codestr(404) -> 'RESOURCE_NOT_FOUND'; +codestr(401) -> 'NOT_SUPPORTED_NOW'; +codestr(500) -> 'UNKNOW_ERROR'. diff --git a/apps/emqx_gateway/src/emqx_gateway_api_client.erl b/apps/emqx_gateway/src/emqx_gateway_api_client.erl index 03fb056ad..0a94cd906 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api_client.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api_client.erl @@ -21,5 +21,283 @@ %% minirest behaviour callbacks -export([api_spec/0]). +-export([ clients/2 + , clients_insta/2 + , subscriptions/2 + ]). + api_spec() -> - {[], []}. + {metadata(apis()), []}. + +apis() -> + [ {"/gateway/:name/clients", clients} + , {"/gateway/:name/clients/:clientid", clients_insta} + , {"/gateway/:name/clients/:clientid/subscriptions", subscriptions} + , {"/gateway/:name/clients/:clientid/subscriptions/:topic", subscriptions} + ]. + +clients(get, _Req) -> + {200, []}. + +clients_insta(get, _Req) -> + {200, <<"{}">>}; +clients_insta(delete, _Req) -> + {200}. + +subscriptions(get, _Req) -> + {200, []}; +subscriptions(delete, _Req) -> + {200}. + +%%-------------------------------------------------------------------- +%% Swagger defines +%%-------------------------------------------------------------------- + +metadata(APIs) -> + metadata(APIs, []). +metadata([], APIAcc) -> + lists:reverse(APIAcc); +metadata([{Path, Fun}|More], APIAcc) -> + Methods = [get, post, put, delete, patch], + Mds = lists:foldl(fun(M, Acc) -> + try + Acc#{M => swagger(Path, M)} + catch + error : function_clause -> + Acc + end + end, #{}, Methods), + metadata(More, [{Path, Mds, Fun} | APIAcc]). + +swagger("/gateway/:name/clients", get) -> + #{ description => <<"Get the gateway clients">> + , parameters => params_client_query() + , responses => + #{ <<"404">> => schema_not_found() + , <<"200">> => schema_clients_list() + } + }; +swagger("/gateway/:name/clients/:clientid", get) -> + #{ description => <<"Get the gateway client infomation">> + , parameters => params_client_insta() + , responses => + #{ <<"404">> => schema_not_found() + , <<"200">> => schema_client() + } + }; +swagger("/gateway/:name/clients/:clientid", delete) -> + #{ description => <<"Kick out the gateway client">> + , parameters => params_client_insta() + , responses => + #{ <<"404">> => schema_not_found() + , <<"204">> => schema_no_content() + } + }; +swagger("/gateway/:name/clients/:clientid/subscriptions", get) -> + #{ description => <<"Get the gateway client subscriptions">> + , parameters => params_client_insta() + , responses => + #{ <<"404">> => schema_not_found() + , <<"200">> => schema_subscription_list() + } + }; +swagger("/gateway/:name/clients/:clientid/subscriptions", post) -> + #{ description => <<"Get the gateway client subscriptions">> + , parameters => params_client_insta() + , requestBody => schema_subscription() + , responses => + #{ <<"404">> => schema_not_found() + , <<"200">> => schema_no_content() + } + }; +swagger("/gateway/:name/clients/:clientid/subscriptions/:topic", delete) -> + #{ description => <<"Unsubscribe the topic for client">> + , parameters => params_client_insta() ++ params_topic_name_in_path() + , responses => + #{ <<"404">> => schema_not_found() + , <<"204">> => schema_no_content() + } + }. + +params_client_query() -> + params_client_searching_in_qs() + ++ emqx_mgmt_util:page_params() + ++ params_gateway_name_in_path(). + +params_client_insta() -> + params_gateway_name_in_path() + ++ params_clientid_in_path(). + +params_client_searching_in_qs() -> + queries( + [ {node, string} + , {clientid, string} + , {username, string} + , {ip_address, string} + , {conn_state, string} + , {clean_start, boolean} + , {like_clientid, string} + , {like_username, string} + , {gte_created_at, string} + , {lte_created_at, string} + , {gte_connected_at, string} + , {lte_connected_at, string} + ]). + +params_gateway_name_in_path() -> + [#{ name => name + , in => path + , schema => #{type => string} + , required => true + }]. + +params_clientid_in_path() -> + [#{ name => clientid + , in => path + , schema => #{type => string} + , required => true + }]. + +params_topic_name_in_path() -> + [#{ name => topic + , in => path + , schema => #{type => string} + , required => true + }]. + +queries(Ls) -> + lists:map(fun({K, Type}) -> + #{name => K, in => query, + schema => #{type => Type}, + required => false + } + end, Ls). + +%%-------------------------------------------------------------------- +%% Schemas + +schema_not_found() -> + emqx_mgmt_util:error_schema(<<"Gateway not found or unloaded">>). + +schema_no_content() -> + #{description => <<"No Content">>}. + +schema_clients_list() -> + emqx_mgmt_util:array_schema( + #{ type => object + , properties => properties_client() + }, + <<"Client lists">> + ). + +schema_client() -> + emqx_mgmt_util:schema( + #{ type => object + , properties => properties_client() + }). + +schema_subscription_list() -> + emqx_mgmt_util:array_schema( + #{ type => object + , properties => properties_subscription() + }, + <<"Client subscriptions">> + ). + +schema_subscription() -> + emqx_mgmt_util:schema( + #{ type => object + , properties => properties_subscription() + } + ). + +%%-------------------------------------------------------------------- +%% Object properties def + +properties_client() -> + emqx_mgmt_util:properties( + [ {node, string, + <<"Name of the node to which the client is connected">>} + , {clientid, string, + <<"Client identifier">>} + , {username, string, + <<"Username of client when connecting">>} + , {proto_name, string, + <<"Client protocol name">>} + , {proto_ver, string, + <<"Protocol version used by the client">>} + , {ip_address, string, + <<"Client's IP address">>} + , {is_bridge, boolean, + <<"Indicates whether the client is connectedvia bridge">>} + , {connected_at, string, + <<"Client connection time">>} + , {disconnected_at, string, + <<"Client offline time, This field is only valid and returned " + "when connected is false">>} + , {connected, boolean, + <<"Whether the client is connected">>} + %, {will_msg, string, + % <<"Client will message">>} + %, {zone, string, + % <<"Indicate the configuration group used by the client">>} + , {keepalive, integer, + <<"keepalive time, with the unit of second">>} + , {clean_start, boolean, + <<"Indicate whether the client is using a brand new session">>} + , {expiry_interval, integer, + <<"Session expiration interval, with the unit of second">>} + , {created_at, string, + <<"Session creation time">>} + , {subscriptions_cnt, integer, + <<"Number of subscriptions established by this client">>} + , {subscriptions_max, integer, + <<"v4 api name [max_subscriptions] Maximum number of " + "subscriptions allowed by this client">>} + , {inflight_cnt, integer, + <<"Current length of inflight">>} + , {inflight_max, integer, + <<"v4 api name [max_inflight]. Maximum length of inflight">>} + , {mqueue_len, integer, + <<"Current length of message queue">>} + , {mqueue_max, integer, + <<"v4 api name [max_mqueue]. Maximum length of message queue">>} + , {mqueue_dropped, integer, + <<"Number of messages dropped by the message queue due to " + "exceeding the length">>} + , {awaiting_rel_cnt, integer, + <<"v4 api name [awaiting_rel] Number of awaiting PUBREC packet">>} + , {awaiting_rel_max, integer, + <<"v4 api name [max_awaiting_rel]. Maximum allowed number of " + "awaiting PUBREC packet">>} + , {recv_oct, integer, + <<"Number of bytes received by EMQ X Broker (the same below)">>} + , {recv_cnt, integer, + <<"Number of TCP packets received">>} + , {recv_pkt, integer, + <<"Number of MQTT packets received">>} + , {recv_msg, integer, + <<"Number of PUBLISH packets received">>} + , {send_oct, integer, + <<"Number of bytes sent">>} + , {send_cnt, integer, + <<"Number of TCP packets sent">>} + , {send_pkt, integer, + <<"Number of MQTT packets sent">>} + , {send_msg, integer, + <<"Number of PUBLISH packets sent">>} + , {mailbox_len, integer, + <<"Process mailbox size">>} + , {heap_size, integer, + <<"Process heap size with the unit of byte">>} + , {reductions, integer, + <<"Erlang reduction">>} + ]). + +properties_subscription() -> + emqx_mgmt_util:properties( + [ {topic, string, + <<"Topic Fillter">>} + , {qos, integer, + <<"QoS level">>} + ]). diff --git a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl index 8131f2d0c..3b62ecd20 100644 --- a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl +++ b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl @@ -143,11 +143,11 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start ~s:~s:~s listener on ~s successfully.~n", + ?ULOG("Start listener ~s:~s:~s on ~s successfully.~n", [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start ~s:~s:~s listener on ~s: ~0p~n", + ?ELOG("Failed to start listener ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. @@ -200,10 +200,10 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop ~s:~s:~s listener on ~s successfully.~n", + ok -> ?ULOG("Stop listener ~s:~s:~s on ~s successfully.~n", [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop ~s:~s:~s listener on ~s: ~0p~n", + ?ELOG("Failed to stop listener ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl index 0a96e98e1..e6720905b 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl @@ -90,11 +90,11 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start ~s:~s:~s listener on ~s successfully.~n", + ?ULOG("Start listener ~s:~s:~s on ~s successfully.~n", [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start ~s:~s:~s listener on ~s: ~0p~n", + ?ELOG("Failed to start listener ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. @@ -133,10 +133,10 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop ~s:~s:~s listener on ~s successfully.~n", + ok -> ?ULOG("Stop listener ~s:~s:~s on ~s successfully.~n", [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop ~s:~s:~s listener on ~s: ~0p~n", + ?ELOG("Failed to stop listener ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl index 039b23924..f510afdf9 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl @@ -108,11 +108,11 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start ~s:~s:~s listener on ~s successfully.~n", + ?ULOG("Start listener ~s:~s:~s on ~s successfully.~n", [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start ~s:~s:~s listener on ~s: ~0p~n", + ?ELOG("Failed to start listener ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. @@ -144,10 +144,10 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, LisName, Type, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop ~s:~s:~s listener on ~s successfully.~n", + ok -> ?ULOG("Stop listener ~s:~s:~s on ~s successfully.~n", [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop ~s:~s:~s listener on ~s: ~0p~n", + ?ELOG("Failed to stop listener ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl index 593b71289..2175b767b 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl @@ -93,11 +93,11 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start ~s:~s:~s listener on ~s successfully.~n", + ?ULOG("Start listener ~s:~s:~s on ~s successfully.~n", [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start ~s:~s:~s listener on ~s: ~0p~n", + ?ELOG("Failed to start listener ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. @@ -129,10 +129,10 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop ~s:~s:~s listener on ~s successfully.~n", + ok -> ?ULOG("Stop listener ~s:~s:~s on ~s successfully.~n", [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop ~s:~s:~s listener on ~s: ~0p~n", + ?ELOG("Failed to stop listener ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. From 0a7a14f4cd05f2917f7c86c355ead4b63146945b Mon Sep 17 00:00:00 2001 From: JianBo He Date: Tue, 31 Aug 2021 12:13:24 +0800 Subject: [PATCH 041/109] chore(mgmt): callback query function with table name param --- apps/emqx_management/src/emqx_mgmt_api.erl | 31 +-- .../src/emqx_mgmt_api_alarms.erl | 23 +- .../src/emqx_mgmt_api_clients.erl | 244 +++++++++--------- .../src/emqx_mgmt_api_subscriptions.erl | 17 +- 4 files changed, 162 insertions(+), 153 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api.erl b/apps/emqx_management/src/emqx_mgmt_api.erl index e9aaa2725..5a7b020d7 100644 --- a/apps/emqx_management/src/emqx_mgmt_api.erl +++ b/apps/emqx_management/src/emqx_mgmt_api.erl @@ -22,13 +22,13 @@ %% first_next query APIs -export([ params2qs/2 - , node_query/4 - , cluster_query/3 + , node_query/5 + , cluster_query/4 , traverse_table/5 , select_table/5 ]). --export([do_query/5]). +-export([do_query/6]). paginate(Tables, Params, RowFun) -> Qh = query_handle(Tables), @@ -78,14 +78,14 @@ limit(Params) -> %% Node Query %%-------------------------------------------------------------------- -node_query(Node, Params, {Tab, QsSchema}, QueryFun) -> +node_query(Node, Params, Tab, QsSchema, QueryFun) -> {CodCnt, Qs} = params2qs(Params, QsSchema), Limit = b2i(limit(Params)), Page = b2i(page(Params)), Start = if Page > 1 -> (Page-1) * Limit; true -> 0 end, - {_, Rows} = do_query(Node, Qs, QueryFun, Start, Limit+1), + {_, Rows} = do_query(Node, Tab, Qs, QueryFun, Start, Limit+1), Meta = #{page => Page, limit => Limit}, NMeta = case CodCnt =:= 0 of true -> Meta#{count => count(Tab)}; @@ -94,10 +94,11 @@ node_query(Node, Params, {Tab, QsSchema}, QueryFun) -> #{meta => NMeta, data => lists:sublist(Rows, Limit)}. %% @private -do_query(Node, Qs, {M,F}, Start, Limit) when Node =:= node() -> - M:F(Qs, Start, Limit); -do_query(Node, Qs, QueryFun, Start, Limit) -> - rpc_call(Node, ?MODULE, do_query, [Node, Qs, QueryFun, Start, Limit], 50000). +do_query(Node, Tab, Qs, {M,F}, Start, Limit) when Node =:= node() -> + M:F(Tab, Qs, Start, Limit); +do_query(Node, Tab, Qs, QueryFun, Start, Limit) -> + rpc_call(Node, ?MODULE, do_query, + [Node, Tab, Qs, QueryFun, Start, Limit], 50000). %% @private rpc_call(Node, M, F, A, T) -> @@ -110,7 +111,7 @@ rpc_call(Node, M, F, A, T) -> %% Cluster Query %%-------------------------------------------------------------------- -cluster_query(Params, {Tab, QsSchema}, QueryFun) -> +cluster_query(Params, Tab, QsSchema, QueryFun) -> {CodCnt, Qs} = params2qs(Params, QsSchema), Limit = b2i(limit(Params)), Page = b2i(page(Params)), @@ -118,7 +119,7 @@ cluster_query(Params, {Tab, QsSchema}, QueryFun) -> true -> 0 end, Nodes = ekka_mnesia:running_nodes(), - Rows = do_cluster_query(Nodes, Qs, QueryFun, Start, Limit+1, []), + Rows = do_cluster_query(Nodes, Tab, Qs, QueryFun, Start, Limit+1, []), Meta = #{page => Page, limit => Limit}, NMeta = case CodCnt =:= 0 of true -> Meta#{count => count(Tab, Nodes)}; @@ -127,13 +128,13 @@ cluster_query(Params, {Tab, QsSchema}, QueryFun) -> #{meta => NMeta, data => lists:sublist(Rows, Limit)}. %% @private -do_cluster_query([], _, _, _, _, Acc) -> +do_cluster_query([], _, _, _, _, _, Acc) -> lists:append(lists:reverse(Acc)); -do_cluster_query([Node|Nodes], Qs, QueryFun, Start, Limit, Acc) -> - {NStart, Rows} = do_query(Node, Qs, QueryFun, Start, Limit), +do_cluster_query([Node|Nodes], Tab, Qs, QueryFun, Start, Limit, Acc) -> + {NStart, Rows} = do_query(Node, Tab, Qs, QueryFun, Start, Limit), case Limit - length(Rows) of Rest when Rest > 0 -> - do_cluster_query(Nodes, Qs, QueryFun, NStart, Limit, [Rows|Acc]); + do_cluster_query(Nodes, Tab, Qs, QueryFun, NStart, Limit, [Rows|Acc]); 0 -> lists:append(lists:reverse([Rows|Acc])) end. diff --git a/apps/emqx_management/src/emqx_mgmt_api_alarms.erl b/apps/emqx_management/src/emqx_mgmt_api_alarms.erl index 1adb5fce3..db6484060 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_alarms.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_alarms.erl @@ -22,8 +22,10 @@ -export([alarms/2]). --export([ query_activated/3 - , query_deactivated/3]). +%% internal export (for query) +-export([ query/4 + ]). + %% notice: from emqx_alarms -define(ACTIVATED_ALARM, emqx_activated_alarm). -define(DEACTIVATED_ALARM, emqx_deactivated_alarm). @@ -71,14 +73,12 @@ alarms_api() -> %%%============================================================================================== %% parameters trans alarms(get, #{query_string := Qs}) -> - {Table, Function} = + Table = case maps:get(<<"activated">>, Qs, <<"true">>) of - <<"true">> -> - {?ACTIVATED_ALARM, query_activated}; - <<"false">> -> - {?DEACTIVATED_ALARM, query_deactivated} + <<"true">> -> ?ACTIVATED_ALARM; + <<"false">> -> ?DEACTIVATED_ALARM end, - Response = emqx_mgmt_api:cluster_query(Qs, {Table, []}, {?MODULE, Function}), + Response = emqx_mgmt_api:cluster_query(Qs, Table, [], {?MODULE, query}), {200, Response}; alarms(delete, _Params) -> @@ -87,13 +87,8 @@ alarms(delete, _Params) -> %%%============================================================================================== %% internal -query_activated(_, Start, Limit) -> - query(?ACTIVATED_ALARM, Start, Limit). -query_deactivated(_, Start, Limit) -> - query(?DEACTIVATED_ALARM, Start, Limit). - -query(Table, Start, Limit) -> +query(Table, _QsSpec, Start, Limit) -> Ms = [{'$1',[],['$1']}], emqx_mgmt_api:select_table(Table, Ms, Start, Limit, fun format_alarm/1). diff --git a/apps/emqx_management/src/emqx_mgmt_api_clients.erl b/apps/emqx_management/src/emqx_mgmt_api_clients.erl index a4e307114..dd4df58a5 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_clients.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_clients.erl @@ -35,7 +35,7 @@ , unsubscribe/2 , subscribe_batch/2]). --export([ query/3 +-export([ query/4 , format_channel_info/1]). %% for batch operation @@ -420,14 +420,17 @@ subscriptions(get, #{bindings := #{clientid := ClientID}}) -> %% api apply list(Params) -> + {Tab, QuerySchema} = ?CLIENT_QS_SCHEMA, case maps:get(<<"node">>, Params, undefined) of undefined -> - Response = emqx_mgmt_api:cluster_query(Params, ?CLIENT_QS_SCHEMA, ?query_fun), + Response = emqx_mgmt_api:cluster_query(Params, Tab, + QuerySchema, ?query_fun), {200, Response}; Node1 -> Node = binary_to_atom(Node1, utf8), ParamsWithoutNode = maps:without([<<"node">>], Params), - Response = emqx_mgmt_api:node_query(Node, ParamsWithoutNode, ?CLIENT_QS_SCHEMA, ?query_fun), + Response = emqx_mgmt_api:node_query(Node, ParamsWithoutNode, + Tab, QuerySchema, ?query_fun), {200, Response} end. @@ -492,8 +495,123 @@ subscribe_batch(#{clientid := ClientID, topics := Topics}) -> ArgList = [[ClientID, Topic, Qos]|| #{topic := Topic, qos := Qos} <- Topics], emqx_mgmt_util:batch_operation(?MODULE, do_subscribe, ArgList). -%%%============================================================================================== +%%-------------------------------------------------------------------- %% internal function + +do_subscribe(ClientID, Topic0, Qos) -> + {Topic, Opts} = emqx_topic:parse(Topic0), + TopicTable = [{Topic, Opts#{qos => Qos}}], + case emqx_mgmt:subscribe(ClientID, TopicTable) of + {error, Reason} -> + {error, Reason}; + {subscribe, Subscriptions} -> + case proplists:is_defined(Topic, Subscriptions) of + true -> + ok; + false -> + {error, unknow_error} + end + end. + +do_unsubscribe(ClientID, Topic) -> + case emqx_mgmt:unsubscribe(ClientID, Topic) of + {error, Reason} -> + {error, Reason}; + Res -> + Res + end. +%%-------------------------------------------------------------------- +%% Query Functions + +query(Tab, {Qs, []}, Start, Limit) -> + Ms = qs2ms(Qs), + emqx_mgmt_api:select_table(Tab, Ms, Start, Limit, + fun format_channel_info/1); + +query(Tab, {Qs, Fuzzy}, Start, Limit) -> + Ms = qs2ms(Qs), + MatchFun = match_fun(Ms, Fuzzy), + emqx_mgmt_api:traverse_table(Tab, MatchFun, Start, Limit, + fun format_channel_info/1). + +%%-------------------------------------------------------------------- +%% QueryString to Match Spec + +-spec qs2ms(list()) -> ets:match_spec(). +qs2ms(Qs) -> + {MtchHead, Conds} = qs2ms(Qs, 2, {#{}, []}), + [{{'$1', MtchHead, '_'}, Conds, ['$_']}]. + +qs2ms([], _, {MtchHead, Conds}) -> + {MtchHead, lists:reverse(Conds)}; + +qs2ms([{Key, '=:=', Value} | Rest], N, {MtchHead, Conds}) -> + NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(Key, Value)), + qs2ms(Rest, N, {NMtchHead, Conds}); +qs2ms([Qs | Rest], N, {MtchHead, Conds}) -> + Holder = binary_to_atom(iolist_to_binary(["$", integer_to_list(N)]), utf8), + NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(element(1, Qs), Holder)), + NConds = put_conds(Qs, Holder, Conds), + qs2ms(Rest, N+1, {NMtchHead, NConds}). + +put_conds({_, Op, V}, Holder, Conds) -> + [{Op, Holder, V} | Conds]; +put_conds({_, Op1, V1, Op2, V2}, Holder, Conds) -> + [{Op2, Holder, V2}, + {Op1, Holder, V1} | Conds]. + +ms(clientid, X) -> + #{clientinfo => #{clientid => X}}; +ms(username, X) -> + #{clientinfo => #{username => X}}; +ms(zone, X) -> + #{clientinfo => #{zone => X}}; +ms(ip_address, X) -> + #{clientinfo => #{peerhost => X}}; +ms(conn_state, X) -> + #{conn_state => X}; +ms(clean_start, X) -> + #{conninfo => #{clean_start => X}}; +ms(proto_name, X) -> + #{conninfo => #{proto_name => X}}; +ms(proto_ver, X) -> + #{conninfo => #{proto_ver => X}}; +ms(connected_at, X) -> + #{conninfo => #{connected_at => X}}; +ms(created_at, X) -> + #{session => #{created_at => X}}. + +%%-------------------------------------------------------------------- +%% Match funcs + +match_fun(Ms, Fuzzy) -> + MsC = ets:match_spec_compile(Ms), + REFuzzy = lists:map(fun({K, like, S}) -> + {ok, RE} = re:compile(S), + {K, like, RE} + end, Fuzzy), + fun(Rows) -> + case ets:match_spec_run(Rows, MsC) of + [] -> []; + Ls -> + lists:filter(fun(E) -> + run_fuzzy_match(E, REFuzzy) + end, Ls) + end + end. + +run_fuzzy_match(_, []) -> + true; +run_fuzzy_match(E = {_, #{clientinfo := ClientInfo}, _}, [{Key, _, RE}|Fuzzy]) -> + Val = case maps:get(Key, ClientInfo, "") of + undefined -> ""; + V -> V + end, + re:run(Val, RE, [{capture, none}]) == match andalso run_fuzzy_match(E, Fuzzy). + +%%-------------------------------------------------------------------- +%% format funcs + format_channel_info({_, ClientInfo, ClientStats}) -> Fun = fun @@ -546,116 +664,8 @@ peer_to_binary(Addr) -> list_to_binary(inet:ntoa(Addr)). format_authz_cache({{PubSub, Topic}, {AuthzResult, Timestamp}}) -> - #{ - access => PubSub, - topic => Topic, - result => AuthzResult, - updated_time => Timestamp - }. - -do_subscribe(ClientID, Topic0, Qos) -> - {Topic, Opts} = emqx_topic:parse(Topic0), - TopicTable = [{Topic, Opts#{qos => Qos}}], - case emqx_mgmt:subscribe(ClientID, TopicTable) of - {error, Reason} -> - {error, Reason}; - {subscribe, Subscriptions} -> - case proplists:is_defined(Topic, Subscriptions) of - true -> - ok; - false -> - {error, unknow_error} - end - end. - -do_unsubscribe(ClientID, Topic) -> - case emqx_mgmt:unsubscribe(ClientID, Topic) of - {error, Reason} -> - {error, Reason}; - Res -> - Res - end. -%%%============================================================================================== -%% Query Functions - -query({Qs, []}, Start, Limit) -> - Ms = qs2ms(Qs), - emqx_mgmt_api:select_table(emqx_channel_info, Ms, Start, Limit, fun format_channel_info/1); - -query({Qs, Fuzzy}, Start, Limit) -> - Ms = qs2ms(Qs), - MatchFun = match_fun(Ms, Fuzzy), - emqx_mgmt_api:traverse_table(emqx_channel_info, MatchFun, Start, Limit, fun format_channel_info/1). - -%%%============================================================================================== -%% QueryString to Match Spec --spec qs2ms(list()) -> ets:match_spec(). -qs2ms(Qs) -> - {MtchHead, Conds} = qs2ms(Qs, 2, {#{}, []}), - [{{'$1', MtchHead, '_'}, Conds, ['$_']}]. - -qs2ms([], _, {MtchHead, Conds}) -> - {MtchHead, lists:reverse(Conds)}; - -qs2ms([{Key, '=:=', Value} | Rest], N, {MtchHead, Conds}) -> - NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(Key, Value)), - qs2ms(Rest, N, {NMtchHead, Conds}); -qs2ms([Qs | Rest], N, {MtchHead, Conds}) -> - Holder = binary_to_atom(iolist_to_binary(["$", integer_to_list(N)]), utf8), - NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(element(1, Qs), Holder)), - NConds = put_conds(Qs, Holder, Conds), - qs2ms(Rest, N+1, {NMtchHead, NConds}). - -put_conds({_, Op, V}, Holder, Conds) -> - [{Op, Holder, V} | Conds]; -put_conds({_, Op1, V1, Op2, V2}, Holder, Conds) -> - [{Op2, Holder, V2}, - {Op1, Holder, V1} | Conds]. - -ms(clientid, X) -> - #{clientinfo => #{clientid => X}}; -ms(username, X) -> - #{clientinfo => #{username => X}}; -ms(zone, X) -> - #{clientinfo => #{zone => X}}; -ms(ip_address, X) -> - #{clientinfo => #{peerhost => X}}; -ms(conn_state, X) -> - #{conn_state => X}; -ms(clean_start, X) -> - #{conninfo => #{clean_start => X}}; -ms(proto_name, X) -> - #{conninfo => #{proto_name => X}}; -ms(proto_ver, X) -> - #{conninfo => #{proto_ver => X}}; -ms(connected_at, X) -> - #{conninfo => #{connected_at => X}}; -ms(created_at, X) -> - #{session => #{created_at => X}}. - -%%%============================================================================================== -%% Match funcs -match_fun(Ms, Fuzzy) -> - MsC = ets:match_spec_compile(Ms), - REFuzzy = lists:map(fun({K, like, S}) -> - {ok, RE} = re:compile(S), - {K, like, RE} - end, Fuzzy), - fun(Rows) -> - case ets:match_spec_run(Rows, MsC) of - [] -> []; - Ls -> - lists:filter(fun(E) -> - run_fuzzy_match(E, REFuzzy) - end, Ls) - end - end. - -run_fuzzy_match(_, []) -> - true; -run_fuzzy_match(E = {_, #{clientinfo := ClientInfo}, _}, [{Key, _, RE}|Fuzzy]) -> - Val = case maps:get(Key, ClientInfo, "") of - undefined -> ""; - V -> V - end, - re:run(Val, RE, [{capture, none}]) == match andalso run_fuzzy_match(E, Fuzzy). + #{ access => PubSub, + topic => Topic, + result => AuthzResult, + updated_time => Timestamp + }. diff --git a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl index 058d824ac..5c2475e95 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl @@ -29,7 +29,7 @@ -export([subscriptions/2]). --export([ query/3 +-export([ query/4 , format/1 ]). @@ -111,11 +111,14 @@ subscriptions(get, #{query_string := Params}) -> list(Params). list(Params) -> + {Tab, QuerySchema} = ?SUBS_QS_SCHEMA, case maps:get(<<"node">>, Params, undefined) of undefined -> - {200, emqx_mgmt_api:cluster_query(Params, ?SUBS_QS_SCHEMA, ?query_fun)}; + {200, emqx_mgmt_api:cluster_query(Params, Tab, + QuerySchema, ?query_fun)}; Node -> - {200, emqx_mgmt_api:node_query(binary_to_atom(Node, utf8), Params, ?SUBS_QS_SCHEMA, ?query_fun)} + {200, emqx_mgmt_api:node_query(binary_to_atom(Node, utf8), Params, + Tab, QuerySchema, ?query_fun)} end. format(Items) when is_list(Items) -> @@ -145,14 +148,14 @@ format({_Subscriber, Topic, Options}) -> %% Query Function %%-------------------------------------------------------------------- -query({Qs, []}, Start, Limit) -> +query(Tab, {Qs, []}, Start, Limit) -> Ms = qs2ms(Qs), - emqx_mgmt_api:select_table(emqx_suboption, Ms, Start, Limit, fun format/1); + emqx_mgmt_api:select_table(Tab, Ms, Start, Limit, fun format/1); -query({Qs, Fuzzy}, Start, Limit) -> +query(Tab, {Qs, Fuzzy}, Start, Limit) -> Ms = qs2ms(Qs), MatchFun = match_fun(Ms, Fuzzy), - emqx_mgmt_api:traverse_table(emqx_suboption, MatchFun, Start, Limit, fun format/1). + emqx_mgmt_api:traverse_table(Tab, MatchFun, Start, Limit, fun format/1). match_fun(Ms, Fuzzy) -> MsC = ets:match_spec_compile(Ms), From 52b6d620eeeabd1cb9e9c2e1139909cd4dee75d9 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Tue, 31 Aug 2021 10:57:11 +0800 Subject: [PATCH 042/109] feat(gw): implement clients list http-api --- .../src/emqx_gateway_api_client.erl | 211 +++++++++++++++++- 1 file changed, 200 insertions(+), 11 deletions(-) diff --git a/apps/emqx_gateway/src/emqx_gateway_api_client.erl b/apps/emqx_gateway/src/emqx_gateway_api_client.erl index 0a94cd906..d2c3b466c 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api_client.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api_client.erl @@ -21,11 +21,21 @@ %% minirest behaviour callbacks -export([api_spec/0]). +%% http handlers -export([ clients/2 , clients_insta/2 , subscriptions/2 ]). +%% internal exports (for client query) +-export([ query/4 + , format_channel_info/1 + ]). + +%%-------------------------------------------------------------------- +%% APIs +%%-------------------------------------------------------------------- + api_spec() -> {metadata(apis()), []}. @@ -36,8 +46,49 @@ apis() -> , {"/gateway/:name/clients/:clientid/subscriptions/:topic", subscriptions} ]. -clients(get, _Req) -> - {200, []}. + +-define(CLIENT_QS_SCHEMA, + [ {<<"node">>, atom} + , {<<"clientid">>, binary} + , {<<"username">>, binary} + %%, {<<"zone">>, atom} + , {<<"ip_address">>, ip} + , {<<"conn_state">>, atom} + , {<<"clean_start">>, atom} + %%, {<<"proto_name">>, binary} + %%, {<<"proto_ver">>, integer} + , {<<"like_clientid">>, binary} + , {<<"like_username">>, binary} + , {<<"gte_created_at">>, timestamp} + , {<<"lte_created_at">>, timestamp} + , {<<"gte_connected_at">>, timestamp} + , {<<"lte_connected_at">>, timestamp} + ]). + +-define(query_fun, {?MODULE, query}). +-define(format_fun, {?MODULE, format_channel_info}). + +clients(get, #{ bindings := #{name := GwName0} + , query_string := Qs + }) -> + GwName = binary_to_existing_atom(GwName0), + TabName = emqx_gateway_cm:tabname(info, GwName), + case maps:get(<<"node">>, Qs, undefined) of + undefined -> + Response = emqx_mgmt_api:cluster_query( + Qs, TabName, + ?CLIENT_QS_SCHEMA, ?query_fun + ), + {200, Response}; + Node1 -> + Node = binary_to_atom(Node1, utf8), + ParamsWithoutNode = maps:without([<<"node">>], Qs), + Response = emqx_mgmt_api:node_query( + Node, ParamsWithoutNode, + TabName, ?CLIENT_QS_SCHEMA, ?query_fun + ), + {200, Response} + end. clients_insta(get, _Req) -> {200, <<"{}">>}; @@ -49,6 +100,145 @@ subscriptions(get, _Req) -> subscriptions(delete, _Req) -> {200}. +%%-------------------------------------------------------------------- +%% query funcs + +query(Tab, {Qs, []}, Start, Limit) -> + Ms = qs2ms(Qs), + emqx_mgmt_api:select_table(Tab, Ms, Start, Limit, + fun format_channel_info/1); + +query(Tab, {Qs, Fuzzy}, Start, Limit) -> + Ms = qs2ms(Qs), + MatchFun = match_fun(Ms, Fuzzy), + emqx_mgmt_api:traverse_table(Tab, MatchFun, Start, Limit, + fun format_channel_info/1). + +qs2ms(Qs) -> + {MtchHead, Conds} = qs2ms(Qs, 2, {#{}, []}), + [{{'$1', MtchHead, '_'}, Conds, ['$_']}]. + +qs2ms([], _, {MtchHead, Conds}) -> + {MtchHead, lists:reverse(Conds)}; + +qs2ms([{Key, '=:=', Value} | Rest], N, {MtchHead, Conds}) -> + NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(Key, Value)), + qs2ms(Rest, N, {NMtchHead, Conds}); +qs2ms([Qs | Rest], N, {MtchHead, Conds}) -> + Holder = binary_to_atom(iolist_to_binary(["$", integer_to_list(N)]), utf8), + NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(element(1, Qs), Holder)), + NConds = put_conds(Qs, Holder, Conds), + qs2ms(Rest, N+1, {NMtchHead, NConds}). + +put_conds({_, Op, V}, Holder, Conds) -> + [{Op, Holder, V} | Conds]; +put_conds({_, Op1, V1, Op2, V2}, Holder, Conds) -> + [{Op2, Holder, V2}, + {Op1, Holder, V1} | Conds]. + +ms(clientid, X) -> + #{clientinfo => #{clientid => X}}; +ms(username, X) -> + #{clientinfo => #{username => X}}; +ms(zone, X) -> + #{clientinfo => #{zone => X}}; +ms(ip_address, X) -> + #{clientinfo => #{peerhost => X}}; +ms(conn_state, X) -> + #{conn_state => X}; +ms(clean_start, X) -> + #{conninfo => #{clean_start => X}}; +ms(proto_name, X) -> + #{conninfo => #{proto_name => X}}; +ms(proto_ver, X) -> + #{conninfo => #{proto_ver => X}}; +ms(connected_at, X) -> + #{conninfo => #{connected_at => X}}; +ms(created_at, X) -> + #{session => #{created_at => X}}. + +%%-------------------------------------------------------------------- +%% Match funcs + +match_fun(Ms, Fuzzy) -> + MsC = ets:match_spec_compile(Ms), + REFuzzy = lists:map(fun({K, like, S}) -> + {ok, RE} = re:compile(S), + {K, like, RE} + end, Fuzzy), + fun(Rows) -> + case ets:match_spec_run(Rows, MsC) of + [] -> []; + Ls -> + lists:filter(fun(E) -> + run_fuzzy_match(E, REFuzzy) + end, Ls) + end + end. + +run_fuzzy_match(_, []) -> + true; +run_fuzzy_match(E = {_, #{clientinfo := ClientInfo}, _}, [{Key, _, RE}|Fuzzy]) -> + Val = case maps:get(Key, ClientInfo, "") of + undefined -> ""; + V -> V + end, + re:run(Val, RE, [{capture, none}]) == match andalso run_fuzzy_match(E, Fuzzy). + +%%-------------------------------------------------------------------- +%% format funcs + +format_channel_info({_, ClientInfo, ClientStats}) -> + Fun = + fun + (_Key, Value, Current) when is_map(Value) -> + maps:merge(Current, Value); + (Key, Value, Current) -> + maps:put(Key, Value, Current) + end, + StatsMap = maps:without([memory, next_pkt_id, total_heap_size], + maps:from_list(ClientStats)), + ClientInfoMap0 = maps:fold(Fun, #{}, ClientInfo), + IpAddress = peer_to_binary(maps:get(peername, ClientInfoMap0)), + Connected = maps:get(conn_state, ClientInfoMap0) =:= connected, + ClientInfoMap1 = maps:merge(StatsMap, ClientInfoMap0), + ClientInfoMap2 = maps:put(node, node(), ClientInfoMap1), + ClientInfoMap3 = maps:put(ip_address, IpAddress, ClientInfoMap2), + ClientInfoMap = maps:put(connected, Connected, ClientInfoMap3), + RemoveList = [ + auth_result + , peername + , sockname + , peerhost + , conn_state + , send_pend + , conn_props + , peercert + , sockstate + , subscriptions + , receive_maximum + , protocol + , is_superuser + , sockport + , anonymous + , mountpoint + , socktype + , active_n + , await_rel_timeout + , conn_mod + , sockname + , retry_interval + , upgrade_qos + ], + maps:without(RemoveList, ClientInfoMap). + +peer_to_binary({Addr, Port}) -> + AddrBinary = list_to_binary(inet:ntoa(Addr)), + PortBinary = integer_to_binary(Port), + <>; +peer_to_binary(Addr) -> + list_to_binary(inet:ntoa(Addr)). + %%-------------------------------------------------------------------- %% Swagger defines %%-------------------------------------------------------------------- @@ -112,7 +302,7 @@ swagger("/gateway/:name/clients/:clientid/subscriptions", post) -> }; swagger("/gateway/:name/clients/:clientid/subscriptions/:topic", delete) -> #{ description => <<"Unsubscribe the topic for client">> - , parameters => params_client_insta() ++ params_topic_name_in_path() + , parameters => params_topic_name_in_path() ++ params_client_insta() , responses => #{ <<"404">> => schema_not_found() , <<"204">> => schema_no_content() @@ -120,13 +310,13 @@ swagger("/gateway/:name/clients/:clientid/subscriptions/:topic", delete) -> }. params_client_query() -> - params_client_searching_in_qs() - ++ emqx_mgmt_util:page_params() - ++ params_gateway_name_in_path(). + params_gateway_name_in_path() + ++ params_client_searching_in_qs() + ++ emqx_mgmt_util:page_params(). params_client_insta() -> - params_gateway_name_in_path() - ++ params_clientid_in_path(). + params_clientid_in_path() + ++ params_gateway_name_in_path(). params_client_searching_in_qs() -> queries( @@ -183,11 +373,10 @@ schema_no_content() -> #{description => <<"No Content">>}. schema_clients_list() -> - emqx_mgmt_util:array_schema( + emqx_mgmt_util:page_schema( #{ type => object , properties => properties_client() - }, - <<"Client lists">> + } ). schema_client() -> From 14b39224d45083d4ddb1b1997e938ca66763354e Mon Sep 17 00:00:00 2001 From: JianBo He Date: Tue, 31 Aug 2021 15:35:53 +0800 Subject: [PATCH 043/109] chore(gw): clients http implement skeleton --- apps/emqx_gateway/src/emqx_gateway.erl | 8 +- apps/emqx_gateway/src/emqx_gateway_api.erl | 21 +- ...lient.erl => emqx_gateway_api_clients.erl} | 212 +++++++++++++----- apps/emqx_gateway/src/emqx_gateway_cli.erl | 3 +- ...gateway_intr.erl => emqx_gateway_http.erl} | 74 +++++- apps/emqx_gateway/src/emqx_gateway_utils.erl | 4 + 6 files changed, 245 insertions(+), 77 deletions(-) rename apps/emqx_gateway/src/{emqx_gateway_api_client.erl => emqx_gateway_api_clients.erl} (69%) rename apps/emqx_gateway/src/{emqx_gateway_intr.erl => emqx_gateway_http.erl} (56%) diff --git a/apps/emqx_gateway/src/emqx_gateway.erl b/apps/emqx_gateway/src/emqx_gateway.erl index 79ea5d8a4..596b47547 100644 --- a/apps/emqx_gateway/src/emqx_gateway.erl +++ b/apps/emqx_gateway/src/emqx_gateway.erl @@ -25,7 +25,7 @@ , post_config_update/4 ]). -%% APIs +%% Gateway APIs -export([ registered_gateway/0 , load/2 , unload/1 @@ -48,7 +48,7 @@ registered_gateway() -> emqx_gateway_registry:list(). %%-------------------------------------------------------------------- -%% Gateway Instace APIs +%% Gateway APIs -spec list() -> [gateway()]. list() -> @@ -96,7 +96,8 @@ update_rawconf(RawName, RawConfDiff) -> %%-------------------------------------------------------------------- %% Config Handler --spec pre_config_update(emqx_config:update_request(), emqx_config:raw_config()) -> +-spec pre_config_update(emqx_config:update_request(), + emqx_config:raw_config()) -> {ok, emqx_config:update_request()} | {error, term()}. pre_config_update({RawName, RawConfDiff}, RawConf) -> {ok, emqx_map_lib:deep_merge(RawConf, #{RawName => RawConfDiff})}. @@ -117,4 +118,3 @@ post_config_update({RawName, _}, NewConfig, OldConfig, _AppEnvs) -> %%-------------------------------------------------------------------- %% Internal funcs %%-------------------------------------------------------------------- - diff --git a/apps/emqx_gateway/src/emqx_gateway_api.erl b/apps/emqx_gateway/src/emqx_gateway_api.erl index b9ae0a234..2ff002fd5 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api.erl @@ -20,8 +20,13 @@ -compile(nowarn_unused_function). --import(emqx_mgmt_util, [ schema/1 - ]). +-import(emqx_mgmt_util, + [ schema/1 + ]). + +-import(emqx_gateway_http, + [ return_http_error/2 + ]). %% minirest behaviour callbacks -export([api_spec/0]). @@ -342,7 +347,7 @@ gateway(get, Request) -> undefined -> all; S0 -> binary_to_existing_atom(S0, utf8) end, - {200, emqx_gateway_intr:gateways(Status)}. + {200, emqx_gateway_http:gateways(Status)}. gateway_insta(delete, #{bindings := #{name := Name0}}) -> Name = binary_to_existing_atom(Name0), @@ -380,13 +385,3 @@ gateway_insta(put, #{body := RawConfsIn, gateway_insta_stats(get, _Req) -> return_http_error(401, <<"Implement it later (maybe 5.1)">>). - -return_http_error(Code, Msg) -> - emqx_json:encode( - #{code => codestr(Code), - reason => emqx_gateway_utils:stringfy(Msg) - }). - -codestr(404) -> 'RESOURCE_NOT_FOUND'; -codestr(401) -> 'NOT_SUPPORTED_NOW'; -codestr(500) -> 'UNKNOW_ERROR'. diff --git a/apps/emqx_gateway/src/emqx_gateway_api_client.erl b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl similarity index 69% rename from apps/emqx_gateway/src/emqx_gateway_api_client.erl rename to apps/emqx_gateway/src/emqx_gateway_api_clients.erl index d2c3b466c..ba61a38fc 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api_client.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl @@ -13,8 +13,8 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- -%% --module(emqx_gateway_api_client). + +-module(emqx_gateway_api_clients). -behaviour(minirest_api). @@ -32,6 +32,10 @@ , format_channel_info/1 ]). +-import(emqx_gateway_http, + [ return_http_error/2 + ]). + %%-------------------------------------------------------------------- %% APIs %%-------------------------------------------------------------------- @@ -46,17 +50,14 @@ apis() -> , {"/gateway/:name/clients/:clientid/subscriptions/:topic", subscriptions} ]. - -define(CLIENT_QS_SCHEMA, [ {<<"node">>, atom} , {<<"clientid">>, binary} , {<<"username">>, binary} - %%, {<<"zone">>, atom} , {<<"ip_address">>, ip} , {<<"conn_state">>, atom} , {<<"clean_start">>, atom} - %%, {<<"proto_name">>, binary} - %%, {<<"proto_ver">>, integer} + , {<<"proto_ver">>, integer} , {<<"like_clientid">>, binary} , {<<"like_username">>, binary} , {<<"gte_created_at">>, timestamp} @@ -90,14 +91,69 @@ clients(get, #{ bindings := #{name := GwName0} {200, Response} end. -clients_insta(get, _Req) -> - {200, <<"{}">>}; -clients_insta(delete, _Req) -> +clients_insta(get, #{ bindings := #{name := GwName0, + clientid := ClientId} + }) -> + GwName = binary_to_existing_atom(GwName0), + TabName = emqx_gateway_cm:tabname(info, GwName), + %% XXX: We need a lookuo function for it instead of a query + #{data := Data} = emqx_mgmt_api:cluster_query( + #{<<"clientid">> => ClientId}, + TabName, ?CLIENT_QS_SCHEMA, ?query_fun + ), + case Data of + [ClientInfo] -> + {200, ClientInfo}; + [] -> + return_http_error(404, <<"Gateway or ClientId not found">>) + end; + +clients_insta(delete, #{ bindings := #{name := GwName0, + clientid := ClientId0} + }) -> + GwName = binary_to_existing_atom(GwName0), + ClientId = emqx_mgmt_util:urldecode(ClientId0), + emqx_gateway_http:client_kickout(GwName, ClientId), {200}. -subscriptions(get, _Req) -> +subscriptions(get, #{ bindings := #{name := GwName0, + clientid := ClientId0} + }) -> + GwName = binary_to_existing_atom(GwName0), + ClientId = emqx_mgmt_util:urldecode(ClientId0), + emqx_gateway_http:client_subscriptions(GwName, ClientId), {200, []}; -subscriptions(delete, _Req) -> + +subscriptions(post, #{ bindings := #{name := GwName0, + clientid := ClientId0}, + body := Body + }) -> + GwName = binary_to_existing_atom(GwName0), + ClientId = emqx_mgmt_util:urldecode(ClientId0), + + case {maps:get(<<"topic">>, Body, undefined), + maps:get(<<"qos">>, Body, 0)} of + {undefined, _} -> + %% FIXME: more reasonable error code?? + return_http_error(404, <<"Request paramter missed: topic">>); + {Topic, QoS} -> + case emqx_gateway_http:client_subscribe(GwName, ClientId, Topic, QoS) of + {error, Reason} -> + return_http_error(404, Reason); + ok -> + {200} + end + end; + +subscriptions(delete, #{ bindings := #{name := GwName0, + clientid := ClientId0, + topic := Topic0 + } + }) -> + GwName = binary_to_existing_atom(GwName0), + ClientId = emqx_mgmt_util:urldecode(ClientId0), + Topic = emqx_mgmt_util:urldecode(Topic0), + _ = emqx_gateway_http:client_unsubscribe(GwName, ClientId, Topic), {200}. %%-------------------------------------------------------------------- @@ -148,8 +204,6 @@ ms(conn_state, X) -> #{conn_state => X}; ms(clean_start, X) -> #{conninfo => #{clean_start => X}}; -ms(proto_name, X) -> - #{conninfo => #{proto_name => X}}; ms(proto_ver, X) -> #{conninfo => #{proto_ver => X}}; ms(connected_at, X) -> @@ -188,49 +242,79 @@ run_fuzzy_match(E = {_, #{clientinfo := ClientInfo}, _}, [{Key, _, RE}|Fuzzy]) - %%-------------------------------------------------------------------- %% format funcs -format_channel_info({_, ClientInfo, ClientStats}) -> - Fun = - fun - (_Key, Value, Current) when is_map(Value) -> - maps:merge(Current, Value); - (Key, Value, Current) -> - maps:put(Key, Value, Current) - end, - StatsMap = maps:without([memory, next_pkt_id, total_heap_size], - maps:from_list(ClientStats)), - ClientInfoMap0 = maps:fold(Fun, #{}, ClientInfo), - IpAddress = peer_to_binary(maps:get(peername, ClientInfoMap0)), - Connected = maps:get(conn_state, ClientInfoMap0) =:= connected, - ClientInfoMap1 = maps:merge(StatsMap, ClientInfoMap0), - ClientInfoMap2 = maps:put(node, node(), ClientInfoMap1), - ClientInfoMap3 = maps:put(ip_address, IpAddress, ClientInfoMap2), - ClientInfoMap = maps:put(connected, Connected, ClientInfoMap3), - RemoveList = [ - auth_result - , peername - , sockname - , peerhost - , conn_state - , send_pend - , conn_props - , peercert - , sockstate - , subscriptions - , receive_maximum - , protocol - , is_superuser - , sockport - , anonymous - , mountpoint - , socktype - , active_n - , await_rel_timeout - , conn_mod - , sockname - , retry_interval - , upgrade_qos - ], - maps:without(RemoveList, ClientInfoMap). +format_channel_info({_, Infos, Stats}) -> + ClientInfo = maps:get(clientinfo, Infos, #{}), + ConnInfo = maps:get(conninfo, Infos, #{}), + SessInfo = maps:get(session, Infos, #{}), + FetchX = [ {node, ClientInfo, node()} + , {clientid, ClientInfo} + , {username, ClientInfo} + , {proto_name, ConnInfo} + , {proto_ver, ConnInfo} + , {ip_address, {peername, ConnInfo, fun peer_to_binary/1}} + , {is_bridge, ClientInfo, false} + , {connected_at, + {connected_at, ConnInfo, fun emqx_gateway_utils:unix_ts_to_rfc3339/1}} + , {disconnected_at, + {disconnected_at, ConnInfo, fun emqx_gateway_utils:unix_ts_to_rfc3339/1}} + , {connected, {conn_state, Infos, fun conn_state_to_connected/1}} + , {keepalive, ClientInfo, 0} + , {clean_start, ConnInfo, true} + , {expiry_interval, ConnInfo, 0} + , {created_at, + {created_at, SessInfo, fun emqx_gateway_utils:unix_ts_to_rfc3339/1}} + , {subscriptions_cnt, Stats, 0} + , {subscriptions_max, Stats, infinity} + , {inflight_cnt, Stats, 0} + , {inflight_max, Stats, infinity} + , {mqueue_len, Stats, 0} + , {mqueue_max, Stats, infinity} + , {mqueue_dropped, Stats, 0} + , {awaiting_rel_cnt, Stats, 0} + , {awaiting_rel_max, Stats, infinity} + , {recv_oct, Stats, 0} + , {recv_cnt, Stats, 0} + , {recv_pkt, Stats, 0} + , {recv_msg, Stats, 0} + , {send_oct, Stats, 0} + , {send_cnt, Stats, 0} + , {send_pkt, Stats, 0} + , {send_msg, Stats, 0} + , {mailbox_len, Stats, 0} + , {heap_size, Stats, 0} + , {reductions, Stats, 0} + ], + eval(FetchX). + +eval(Ls) -> + eval(Ls, #{}). +eval([], AccMap) -> + AccMap; +eval([{K, Vx}|More], AccMap) -> + case valuex_get(K, Vx) of + undefined -> eval(More, AccMap#{K => null}); + Value -> eval(More, AccMap#{K => Value}) + end; +eval([{K, Vx, Default}|More], AccMap) -> + case valuex_get(K, Vx) of + undefined -> eval(More, AccMap#{K => Default}); + Value -> eval(More, AccMap#{K => Value}) + end. + +valuex_get(K, Vx) when is_map(Vx); is_list(Vx) -> + key_get(K, Vx); +valuex_get(_K, {InKey, Obj}) when is_map(Obj); is_list(Obj) -> + key_get(InKey, Obj); +valuex_get(_K, {InKey, Obj, MappingFun}) when is_map(Obj); is_list(Obj) -> + case key_get(InKey, Obj) of + undefined -> undefined; + Val -> MappingFun(Val) + end. + +key_get(K, M) when is_map(M) -> + maps:get(K, M, undefined); +key_get(K, L) when is_list(L) -> + proplists:get_value(K, L). peer_to_binary({Addr, Port}) -> AddrBinary = list_to_binary(inet:ntoa(Addr)), @@ -239,6 +323,9 @@ peer_to_binary({Addr, Port}) -> peer_to_binary(Addr) -> list_to_binary(inet:ntoa(Addr)). +conn_state_to_connected(connected) -> true; +conn_state_to_connected(_) -> false. + %%-------------------------------------------------------------------- %% Swagger defines %%-------------------------------------------------------------------- @@ -325,6 +412,7 @@ params_client_searching_in_qs() -> , {username, string} , {ip_address, string} , {conn_state, string} + , {proto_ver, string} , {clean_start, boolean} , {like_clientid, string} , {like_username, string} @@ -426,6 +514,10 @@ properties_client() -> "when connected is false">>} , {connected, boolean, <<"Whether the client is connected">>} + %% FIXME: the will_msg attribute is not a general attribute + %% for every protocol. But it should be returned to frontend if someone + %% want it + %% %, {will_msg, string, % <<"Client will message">>} %, {zone, string, @@ -488,5 +580,11 @@ properties_subscription() -> [ {topic, string, <<"Topic Fillter">>} , {qos, integer, - <<"QoS level">>} + <<"QoS level, enum: 0, 1, 2">>} + , {nl, integer, %% FIXME: why not boolean? + <<"No Local option, enum: 0, 1">>} + , {rap, integer, + <<"Retain as Published option, enum: 0, 1">>} + , {rh, integer, + <<"Retain Handling option, enum: 0, 1, 2">>} ]). diff --git a/apps/emqx_gateway/src/emqx_gateway_cli.erl b/apps/emqx_gateway/src/emqx_gateway_cli.erl index b446cda92..6ccb444f0 100644 --- a/apps/emqx_gateway/src/emqx_gateway_cli.erl +++ b/apps/emqx_gateway/src/emqx_gateway_cli.erl @@ -51,7 +51,7 @@ is_cmd(Fun) -> gateway(["list"]) -> lists:foreach(fun(#{name := Name} = Gateway) -> - %% XXX: More infos: listeners?, connected? + %% TODO: More infos: listeners?, connected? Status = maps:get(status, Gateway, stopped), emqx_ctl:print("Gateway(name=~s, status=~s)~n", [Name, Status]) @@ -106,6 +106,7 @@ gateway(_) -> ]). 'gateway-clients'(["list", Name]) -> + %% FIXME: page me. for example: --limit 100 --page 10 ??? InfoTab = emqx_gateway_cm:tabname(info, Name), case ets:info(InfoTab) of undefined -> diff --git a/apps/emqx_gateway/src/emqx_gateway_intr.erl b/apps/emqx_gateway/src/emqx_gateway_http.erl similarity index 56% rename from apps/emqx_gateway/src/emqx_gateway_intr.erl rename to apps/emqx_gateway/src/emqx_gateway_http.erl index add37e1c5..a36d97dea 100644 --- a/apps/emqx_gateway/src/emqx_gateway_intr.erl +++ b/apps/emqx_gateway/src/emqx_gateway_http.erl @@ -15,11 +15,26 @@ %%-------------------------------------------------------------------- %% @doc Gateway Interface Module for HTTP-APIs --module(emqx_gateway_intr). +-module(emqx_gateway_http). +-include("include/emqx_gateway.hrl"). + +%% Mgmt APIs - gateway -export([ gateways/1 ]). +%% Mgmt APIs - clients +-export([ client_lookup/2 + , client_kickout/2 + , client_subscribe/4 + , client_unsubscribe/3 + , client_subscriptions/2 + ]). + +%% Utils for http, swagger, etc. +-export([ return_http_error/2 + ]). + -type gateway_summary() :: #{ name := binary() , status := running | stopped | unloaded @@ -30,7 +45,7 @@ }. %%-------------------------------------------------------------------- -%% APIs +%% Mgmt APIs - gateway %%-------------------------------------------------------------------- -spec gateways(Status :: all | running | stopped | unloaded) @@ -76,3 +91,58 @@ get_listeners_status(GwName, Config) -> %% @private listener_name(GwName, Type, LisName) -> list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). + +%%-------------------------------------------------------------------- +%% Mgmt APIs - clients +%%-------------------------------------------------------------------- + +-spec client_lookup(gateway_name(), emqx_type:clientid()) + -> {ok, {emqx_types:infos(), emqx_types:stats()}} + | {error, any()}. +client_lookup(_GwName, _ClientId) -> + %% FIXME: The Gap between `ClientInfo in HTTP-API` and + %% ClientInfo defination + todo. + +-spec client_kickout(gateway_name(), emqx_type:clientid()) + -> {error, any()} + | ok. +client_kickout(GwName, ClientId) -> + emqx_gateway_cm:kick_session(GwName, ClientId). + +-spec client_subscriptions(gateway_name(), emqx_type:clientid()) + -> {error, any()} + | {ok, list()}. %% FIXME: #{<<"t/1">> => + %% #{nl => 0,qos => 0,rap => 0,rh => 0, + %% sub_props => #{}} +client_subscriptions(_GwName, _ClientId) -> + todo. + +-spec client_subscribe(gateway_name(), emqx_type:clientid(), + emqx_type:topic(), emqx_type:qos()) + -> {error, any()} + | ok. +client_subscribe(_GwName, _ClientId, _Topic, _QoS) -> + todo. + +-spec client_unsubscribe(gateway_name(), + emqx_type:clientid(), emqx_type:topic()) + -> {error, any()} + | ok. +client_unsubscribe(_GwName, _ClientId, _Topic) -> + todo. + +%%-------------------------------------------------------------------- +%% Utils +%%-------------------------------------------------------------------- + +-spec return_http_error(integer(), binary()) -> binary(). +return_http_error(Code, Msg) -> + emqx_json:encode( + #{code => codestr(Code), + reason => emqx_gateway_utils:stringfy(Msg) + }). + +codestr(404) -> 'RESOURCE_NOT_FOUND'; +codestr(401) -> 'NOT_SUPPORTED_NOW'; +codestr(500) -> 'UNKNOW_ERROR'. diff --git a/apps/emqx_gateway/src/emqx_gateway_utils.erl b/apps/emqx_gateway/src/emqx_gateway_utils.erl index dc4e38e7d..3300ebf69 100644 --- a/apps/emqx_gateway/src/emqx_gateway_utils.erl +++ b/apps/emqx_gateway/src/emqx_gateway_utils.erl @@ -28,6 +28,7 @@ -export([ apply/2 , format_listenon/1 + , unix_ts_to_rfc3339/1 , unix_ts_to_rfc3339/2 ]). @@ -121,6 +122,9 @@ unix_ts_to_rfc3339(Key, Map) -> emqx_rule_funcs:unix_ts_to_rfc3339(Ts, <<"millisecond">>)} end. +unix_ts_to_rfc3339(Ts) -> + emqx_rule_funcs:unix_ts_to_rfc3339(Ts, <<"millisecond">>). + -spec stringfy(term()) -> binary(). stringfy(T) -> iolist_to_binary(io_lib:format("~0p", [T])). From 1748de5ee3948b947254d4e5a5063aed27382938 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Tue, 31 Aug 2021 20:15:21 +0800 Subject: [PATCH 044/109] feat(gw): support the sub/unsub operation --- .../src/emqx_gateway_api_clients.erl | 50 +++++-- apps/emqx_gateway/src/emqx_gateway_http.erl | 125 ++++++++++++++---- .../src/exproto/emqx_exproto_channel.erl | 18 +-- .../src/exproto/emqx_exproto_gsvr.erl | 4 +- .../src/mqttsn/emqx_sn_channel.erl | 18 +-- .../src/stomp/emqx_stomp_channel.erl | 56 ++++++-- 6 files changed, 200 insertions(+), 71 deletions(-) diff --git a/apps/emqx_gateway/src/emqx_gateway_api_clients.erl b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl index ba61a38fc..cfb7f81e8 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api_clients.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl @@ -18,6 +18,8 @@ -behaviour(minirest_api). +-include_lib("emqx/include/logger.hrl"). + %% minirest behaviour callbacks -export([api_spec/0]). @@ -92,20 +94,22 @@ clients(get, #{ bindings := #{name := GwName0} end. clients_insta(get, #{ bindings := #{name := GwName0, - clientid := ClientId} + clientid := ClientId0} }) -> GwName = binary_to_existing_atom(GwName0), - TabName = emqx_gateway_cm:tabname(info, GwName), - %% XXX: We need a lookuo function for it instead of a query - #{data := Data} = emqx_mgmt_api:cluster_query( - #{<<"clientid">> => ClientId}, - TabName, ?CLIENT_QS_SCHEMA, ?query_fun - ), - case Data of + ClientId = emqx_mgmt_util:urldecode(ClientId0), + + case emqx_gateway_http:lookup_client(GwName, ClientId, + {?MODULE, format_channel_info}) of [ClientInfo] -> {200, ClientInfo}; + [ClientInfo|_More] -> + ?LOG(warning, "More than one client info was returned on ~s", + [ClientId]), + {200, ClientInfo}; [] -> return_http_error(404, <<"Gateway or ClientId not found">>) + end; clients_insta(delete, #{ bindings := #{name := GwName0, @@ -113,7 +117,7 @@ clients_insta(delete, #{ bindings := #{name := GwName0, }) -> GwName = binary_to_existing_atom(GwName0), ClientId = emqx_mgmt_util:urldecode(ClientId0), - emqx_gateway_http:client_kickout(GwName, ClientId), + emqx_gateway_http:kickout_client(GwName, ClientId), {200}. subscriptions(get, #{ bindings := #{name := GwName0, @@ -121,8 +125,7 @@ subscriptions(get, #{ bindings := #{name := GwName0, }) -> GwName = binary_to_existing_atom(GwName0), ClientId = emqx_mgmt_util:urldecode(ClientId0), - emqx_gateway_http:client_subscriptions(GwName, ClientId), - {200, []}; + {200, emqx_gateway_http:list_client_subscriptions(GwName, ClientId)}; subscriptions(post, #{ bindings := #{name := GwName0, clientid := ClientId0}, @@ -131,8 +134,7 @@ subscriptions(post, #{ bindings := #{name := GwName0, GwName = binary_to_existing_atom(GwName0), ClientId = emqx_mgmt_util:urldecode(ClientId0), - case {maps:get(<<"topic">>, Body, undefined), - maps:get(<<"qos">>, Body, 0)} of + case {maps:get(<<"topic">>, Body, undefined), subopts(Body)} of {undefined, _} -> %% FIXME: more reasonable error code?? return_http_error(404, <<"Request paramter missed: topic">>); @@ -156,6 +158,23 @@ subscriptions(delete, #{ bindings := #{name := GwName0, _ = emqx_gateway_http:client_unsubscribe(GwName, ClientId, Topic), {200}. +%%-------------------------------------------------------------------- +%% Utils + +subopts(Req) -> + #{ qos => maps:get(<<"qos">>, Req, 0) + , rap => maps:get(<<"rap">>, Req, 0) + , nl => maps:get(<<"nl">>, Req, 0) + , rh => maps:get(<<"rh">>, Req, 0) + , sub_prop => extra_sub_prop(maps:get(<<"sub_prop">>, Req, #{})) + }. + +extra_sub_prop(Props) -> + maps:filter( + fun(_, V) -> V =/= undefined end, + #{subid => maps:get(<<"subid">>, Props, undefined)} + ). + %%-------------------------------------------------------------------- %% query funcs @@ -576,6 +595,10 @@ properties_client() -> ]). properties_subscription() -> + ExtraProps = [ {subid, integer, + <<"Only stomp protocol, an uniquely identity for " + "the subscription. range: 1-65535.">>} + ], emqx_mgmt_util:properties( [ {topic, string, <<"Topic Fillter">>} @@ -587,4 +610,5 @@ properties_subscription() -> <<"Retain as Published option, enum: 0, 1">>} , {rh, integer, <<"Retain Handling option, enum: 0, 1, 2">>} + , {sub_prop, object, ExtraProps} ]). diff --git a/apps/emqx_gateway/src/emqx_gateway_http.erl b/apps/emqx_gateway/src/emqx_gateway_http.erl index a36d97dea..130c31243 100644 --- a/apps/emqx_gateway/src/emqx_gateway_http.erl +++ b/apps/emqx_gateway/src/emqx_gateway_http.erl @@ -18,17 +18,20 @@ -module(emqx_gateway_http). -include("include/emqx_gateway.hrl"). +-include_lib("emqx/include/logger.hrl"). %% Mgmt APIs - gateway -export([ gateways/1 ]). %% Mgmt APIs - clients --export([ client_lookup/2 - , client_kickout/2 +-export([ lookup_client/3 + , lookup_client/4 + , kickout_client/2 + , kickout_client/3 + , list_client_subscriptions/2 , client_subscribe/4 , client_unsubscribe/3 - , client_subscriptions/2 ]). %% Utils for http, swagger, etc. @@ -44,6 +47,8 @@ , listeners => [] }. +-define(DEFAULT_CALL_TIMEOUT, 15000). + %%-------------------------------------------------------------------- %% Mgmt APIs - gateway %%-------------------------------------------------------------------- @@ -96,41 +101,104 @@ listener_name(GwName, Type, LisName) -> %% Mgmt APIs - clients %%-------------------------------------------------------------------- --spec client_lookup(gateway_name(), emqx_type:clientid()) - -> {ok, {emqx_types:infos(), emqx_types:stats()}} - | {error, any()}. -client_lookup(_GwName, _ClientId) -> - %% FIXME: The Gap between `ClientInfo in HTTP-API` and - %% ClientInfo defination - todo. +-spec lookup_client(gateway_name(), emqx_type:clientid(), function()) -> list(). +lookup_client(GwName, ClientId, FormatFun) -> + lists:append([lookup_client(Node, GwName, {clientid, ClientId}, FormatFun) + || Node <- ekka_mnesia:running_nodes()]). --spec client_kickout(gateway_name(), emqx_type:clientid()) +lookup_client(Node, GwName, {clientid, ClientId}, {M,F}) when Node =:= node() -> + ChanTab = emqx_gateway_cm:tabname(chan, GwName), + InfoTab = emqx_gateway_cm:tabname(info, GwName), + + lists:append(lists:map( + fun(Key) -> + lists:map(fun M:F/1, ets:lookup(InfoTab, Key)) + end, ets:lookup(ChanTab, ClientId))); + +lookup_client(Node, GwName, {clientid, ClientId}, FormatFun) -> + rpc_call(Node, lookup_client, + [Node, GwName, {clientid, ClientId}, FormatFun]). + +-spec kickout_client(gateway_name(), emqx_type:clientid()) -> {error, any()} | ok. -client_kickout(GwName, ClientId) -> - emqx_gateway_cm:kick_session(GwName, ClientId). +kickout_client(GwName, ClientId) -> + Results = [kickout_client(Node, GwName, ClientId) + || Node <- ekka_mnesia:running_nodes()], + case lists:any(fun(Item) -> Item =:= ok end, Results) of + true -> ok; + false -> lists:last(Results) + end. --spec client_subscriptions(gateway_name(), emqx_type:clientid()) +kickout_client(Node, GwName, ClientId) when Node =:= node() -> + emqx_gateway_cm:kick_session(GwName, ClientId); + +kickout_client(Node, GwName, ClientId) -> + rpc_call(Node, kickout_client, [Node, GwName, ClientId]). + +-spec list_client_subscriptions(gateway_name(), emqx_type:clientid()) -> {error, any()} - | {ok, list()}. %% FIXME: #{<<"t/1">> => - %% #{nl => 0,qos => 0,rap => 0,rh => 0, - %% sub_props => #{}} -client_subscriptions(_GwName, _ClientId) -> - todo. + | {ok, list()}. +list_client_subscriptions(GwName, ClientId) -> + %% Get the subscriptions from session-info + case emqx_gateway_cm:get_chan_info(GwName, ClientId) of + undefined -> + {error, not_found}; + Infos -> + Subs = maps:get(subscriptions, Infos, #{}), + maps:fold(fun(K, V, Acc) -> + [maps:merge( + #{topic => K}, + maps:with([qos, nl, rap, rh], V)) + |Acc] + end, [], Subs) + end. -spec client_subscribe(gateway_name(), emqx_type:clientid(), - emqx_type:topic(), emqx_type:qos()) + emqx_type:topic(), emqx_type:subopts()) -> {error, any()} | ok. -client_subscribe(_GwName, _ClientId, _Topic, _QoS) -> - todo. +client_subscribe(GwName, ClientId, Topic, SubOpts) -> + case emqx_gateway_cm:lookup_channels(GwName, ClientId) of + [] -> {error, not_found}; + [Pid] -> + %% fixed conn module? + emqx_gateway_conn:call( + Pid, {subscribe, Topic, SubOpts}, + ?DEFAULT_CALL_TIMEOUT + ); + Pids -> + ?LOG(warning, "More than one client process ~p was found " + "clientid ~s", [Pids, ClientId]), + _ = [ + emqx_gateway_conn:call( + Pid, {subscribe, Topic, SubOpts}, + ?DEFAULT_CALL_TIMEOUT + ) || Pid <- Pids], + ok + end. -spec client_unsubscribe(gateway_name(), emqx_type:clientid(), emqx_type:topic()) -> {error, any()} | ok. -client_unsubscribe(_GwName, _ClientId, _Topic) -> - todo. +client_unsubscribe(GwName, ClientId, Topic) -> + case emqx_gateway_cm:lookup_channels(GwName, ClientId) of + [] -> {error, not_found}; + [Pid] -> + emqx_gateway_conn:call( + Pid, {unsubscribe, Topic}, + ?DEFAULT_CALL_TIMEOUT); + Pids -> + ?LOG(warning, "More than one client process ~p was found " + "clientid ~s", [Pids, ClientId]), + _ = [ + emqx_gateway_conn:call( + Pid, {unsubscribe, Topic}, + ?DEFAULT_CALL_TIMEOUT + ) || Pid <- Pids], + ok + end. %%-------------------------------------------------------------------- %% Utils @@ -146,3 +214,12 @@ return_http_error(Code, Msg) -> codestr(404) -> 'RESOURCE_NOT_FOUND'; codestr(401) -> 'NOT_SUPPORTED_NOW'; codestr(500) -> 'UNKNOW_ERROR'. + +%%-------------------------------------------------------------------- +%% Internal funcs + +rpc_call(Node, Fun, Args) -> + case rpc:call(Node, ?MODULE, Fun, Args) of + {badrpc, Reason} -> {error, Reason}; + Res -> Res + end. diff --git a/apps/emqx_gateway/src/exproto/emqx_exproto_channel.erl b/apps/emqx_gateway/src/exproto/emqx_exproto_channel.erl index b1a1ae027..ace9a7be5 100644 --- a/apps/emqx_gateway/src/exproto/emqx_exproto_channel.erl +++ b/apps/emqx_gateway/src/exproto/emqx_exproto_channel.erl @@ -310,7 +310,7 @@ handle_call({start_timer, keepalive, Interval}, NChannel = Channel#channel{conninfo = NConnInfo, clientinfo = NClientInfo}, {reply, ok, ensure_keepalive(NChannel)}; -handle_call({subscribe, TopicFilter, Qos}, +handle_call({subscribe_from_client, TopicFilter, Qos}, Channel = #channel{ ctx = Ctx, conn_state = connected, @@ -323,11 +323,19 @@ handle_call({subscribe, TopicFilter, Qos}, {reply, ok, NChannel} end; -handle_call({unsubscribe, TopicFilter}, +handle_call({subscribe, Topic, SubOpts}, Channel) -> + {ok, NChannel} = do_subscribe([{Topic, SubOpts}], Channel), + {reply, ok, NChannel}; + +handle_call({unsubscribe_from_client, TopicFilter}, Channel = #channel{conn_state = connected}) -> {ok, NChannel} = do_unsubscribe([{TopicFilter, #{}}], Channel), {reply, ok, NChannel}; +handle_call({unsubscribe, Topic}, Channel) -> + {ok, NChannel} = do_unsubscribe([Topic], Channel), + {reply, ok, NChannel}; + handle_call({publish, Topic, Qos, Payload}, Channel = #channel{ ctx = Ctx, @@ -363,12 +371,6 @@ handle_cast(Req, Channel) -> -spec handle_info(any(), channel()) -> {ok, channel()} | {shutdown, Reason :: term(), channel()}. -handle_info({subscribe, TopicFilters}, Channel) -> - do_subscribe(TopicFilters, Channel); - -handle_info({unsubscribe, TopicFilters}, Channel) -> - do_unsubscribe(TopicFilters, Channel); - handle_info({sock_closed, Reason}, Channel = #channel{rqueue = Queue, inflight = Inflight}) -> case queue:len(Queue) =:= 0 diff --git a/apps/emqx_gateway/src/exproto/emqx_exproto_gsvr.erl b/apps/emqx_gateway/src/exproto/emqx_exproto_gsvr.erl index 346f87452..0135aa8e3 100644 --- a/apps/emqx_gateway/src/exproto/emqx_exproto_gsvr.erl +++ b/apps/emqx_gateway/src/exproto/emqx_exproto_gsvr.erl @@ -96,7 +96,7 @@ publish(Req, Md) -> subscribe(Req = #{conn := Conn, topic := Topic, qos := Qos}, Md) when ?IS_QOS(Qos) -> ?LOG(debug, "Recv ~p function with request ~0p", [?FUNCTION_NAME, Req]), - {ok, response(call(Conn, {subscribe, Topic, Qos})), Md}; + {ok, response(call(Conn, {subscribe_from_client, Topic, Qos})), Md}; subscribe(Req, Md) -> ?LOG(debug, "Recv ~p function with request ~0p", [?FUNCTION_NAME, Req]), @@ -107,7 +107,7 @@ subscribe(Req, Md) -> | {error, grpc_cowboy_h:error_response()}. unsubscribe(Req = #{conn := Conn, topic := Topic}, Md) -> ?LOG(debug, "Recv ~p function with request ~0p", [?FUNCTION_NAME, Req]), - {ok, response(call(Conn, {unsubscribe, Topic})), Md}. + {ok, response(call(Conn, {unsubscribe_from_client, Topic})), Md}. %%-------------------------------------------------------------------- %% Internal funcs diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl index 5bba599c8..0707534b7 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl @@ -1102,6 +1102,12 @@ message_to_packet(MsgId, Message, | {shutdown, Reason :: term(), Reply :: term(), channel()} | {shutdown, Reason :: term(), Reply :: term(), emqx_types:packet(), channel()}. +handle_call({subscribe, _Topic, _Subopts}, Channel) -> + reply({error, not_supported_now}, Channel); + +handle_call({unsubscribe, _Topic}, Channel) -> + reply({error, not_supported_now}, Channel); + handle_call(kick, Channel) -> NChannel = ensure_disconnected(kicked, Channel), shutdown_and_reply(kicked, ok, NChannel); @@ -1150,18 +1156,6 @@ handle_cast(_Req, Channel) -> -spec handle_info(Info :: term(), channel()) -> ok | {ok, channel()} | {shutdown, Reason :: term(), channel()}. -%% XXX: Received from the emqx-management ??? -%handle_info({subscribe, TopicFilters}, Channel ) -> -% {_, NChannel} = lists:foldl( -% fun({TopicFilter, SubOpts}, {_, ChannelAcc}) -> -% do_subscribe(TopicFilter, SubOpts, ChannelAcc) -% end, {[], Channel}, parse_topic_filters(TopicFilters)), -% {ok, NChannel}; -% -%handle_info({unsubscribe, TopicFilters}, Channel) -> -% {_RC, NChannel} = process_unsubscribe(TopicFilters, #{}, Channel), -% {ok, NChannel}; - handle_info({sock_closed, Reason}, Channel = #channel{conn_state = idle}) -> shutdown(Reason, Channel); diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl index 250f43988..3e7fe8b42 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl @@ -626,6 +626,50 @@ handle_out(receipt, ReceiptId, Channel) -> -> {reply, Reply :: term(), channel()} | {shutdown, Reason :: term(), Reply :: term(), channel()} | {shutdown, Reason :: term(), Reply :: term(), stomp_frame(), channel()}). +handle_call({subscribe, Topic, SubOpts}, + Channel = #channel{ + subscriptions = Subs + }) -> + case maps:get(subid, + maps:get(sub_prop, SubOpts, #{}), + undefined) of + undefined -> + reply({error, no_subid}, Channel); + SubId -> + case emqx_misc:pipeline( + [ fun parse_topic_filter/2 + , fun check_subscribed_status/2 + ], {SubId, {Topic, SubOpts}}, Channel) of + {ok, {_, TopicFilter}, NChannel} -> + [MountedTopic] = do_subscribe([TopicFilter], NChannel), + NChannel1 = NChannel#channel{ + subscriptions = + [{SubId, MountedTopic, <<"auto">>}|Subs] + }, + reply(ok, NChannel1); + {error, ErrMsg, NChannel} -> + ?LOG(error, "Failed to subscribe topic ~s, reason: ~s", + [Topic, ErrMsg]), + reply({error, ErrMsg}, NChannel) + end + end; + +handle_call({unsubscribe, Topic}, + Channel = #channel{ + ctx = Ctx, + clientinfo = ClientInfo = #{mountpoint := Mountpoint}, + subscriptions = Subs + }) -> + {ParsedTopic, _SubOpts} = emqx_topic:parse(Topic), + MountedTopic = emqx_mountpoint:mount(Mountpoint, ParsedTopic), + ok = emqx_broker:unsubscribe(MountedTopic), + _ = run_hooks(Ctx, 'session.unsubscribe', + [ClientInfo, MountedTopic, #{}]), + reply(ok, + Channel#channel{ + subscriptions = lists:keydelete(MountedTopic, 2, Subs)} + ); + handle_call(kick, Channel) -> NChannel = ensure_disconnected(kicked, Channel), Frame = error_frame(undefined, <<"Kicked out">>), @@ -678,18 +722,6 @@ handle_cast(_Req, Channel) -> -spec(handle_info(Info :: term(), channel()) -> ok | {ok, channel()} | {shutdown, Reason :: term(), channel()}). -%% XXX: Received from the emqx-management ??? -%handle_info({subscribe, TopicFilters}, Channel ) -> -% {_, NChannel} = lists:foldl( -% fun({TopicFilter, SubOpts}, {_, ChannelAcc}) -> -% do_subscribe(TopicFilter, SubOpts, ChannelAcc) -% end, {[], Channel}, parse_topic_filters(TopicFilters)), -% {ok, NChannel}; -% -%handle_info({unsubscribe, TopicFilters}, Channel) -> -% {_RC, NChannel} = process_unsubscribe(TopicFilters, #{}, Channel), -% {ok, NChannel}; - handle_info({sock_closed, Reason}, Channel = #channel{conn_state = idle}) -> shutdown(Reason, Channel); From 40d34ccd85310cf6b3cbfcddff9bb29d2a35fce7 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Wed, 1 Sep 2021 11:42:46 +0800 Subject: [PATCH 045/109] fix(gw): fix the subscription apis bugs --- .../src/emqx_gateway_api_clients.erl | 20 ++++-- apps/emqx_gateway/src/emqx_gateway_cm.erl | 4 ++ apps/emqx_gateway/src/emqx_gateway_http.erl | 72 +++++++------------ apps/emqx_gateway/src/emqx_gateway_utils.erl | 3 +- .../src/stomp/emqx_stomp_channel.erl | 52 ++++++++------ 5 files changed, 78 insertions(+), 73 deletions(-) diff --git a/apps/emqx_gateway/src/emqx_gateway_api_clients.erl b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl index cfb7f81e8..99876c917 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api_clients.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl @@ -120,13 +120,22 @@ clients_insta(delete, #{ bindings := #{name := GwName0, emqx_gateway_http:kickout_client(GwName, ClientId), {200}. +%% FIXME: +%% List the subscription without mountpoint, but has SubOpts, +%% for example, share group ... subscriptions(get, #{ bindings := #{name := GwName0, clientid := ClientId0} }) -> GwName = binary_to_existing_atom(GwName0), ClientId = emqx_mgmt_util:urldecode(ClientId0), - {200, emqx_gateway_http:list_client_subscriptions(GwName, ClientId)}; + case emqx_gateway_http:list_client_subscriptions(GwName, ClientId) of + {error, Reason} -> + return_http_error(404, Reason); + {ok, Subs} -> + {200, Subs} + end; +%% Create the subscription without mountpoint subscriptions(post, #{ bindings := #{name := GwName0, clientid := ClientId0}, body := Body @@ -147,6 +156,7 @@ subscriptions(post, #{ bindings := #{name := GwName0, end end; +%% Remove the subscription without mountpoint subscriptions(delete, #{ bindings := #{name := GwName0, clientid := ClientId0, topic := Topic0 @@ -166,10 +176,10 @@ subopts(Req) -> , rap => maps:get(<<"rap">>, Req, 0) , nl => maps:get(<<"nl">>, Req, 0) , rh => maps:get(<<"rh">>, Req, 0) - , sub_prop => extra_sub_prop(maps:get(<<"sub_prop">>, Req, #{})) + , sub_props => extra_sub_props(maps:get(<<"sub_props">>, Req, #{})) }. -extra_sub_prop(Props) -> +extra_sub_props(Props) -> maps:filter( fun(_, V) -> V =/= undefined end, #{subid => maps:get(<<"subid">>, Props, undefined)} @@ -595,7 +605,7 @@ properties_client() -> ]). properties_subscription() -> - ExtraProps = [ {subid, integer, + ExtraProps = [ {subid, string, <<"Only stomp protocol, an uniquely identity for " "the subscription. range: 1-65535.">>} ], @@ -610,5 +620,5 @@ properties_subscription() -> <<"Retain as Published option, enum: 0, 1">>} , {rh, integer, <<"Retain Handling option, enum: 0, 1, 2">>} - , {sub_prop, object, ExtraProps} + , {sub_props, object, ExtraProps} ]). diff --git a/apps/emqx_gateway/src/emqx_gateway_cm.erl b/apps/emqx_gateway/src/emqx_gateway_cm.erl index 7a7ad055d..d8b615fe8 100644 --- a/apps/emqx_gateway/src/emqx_gateway_cm.erl +++ b/apps/emqx_gateway/src/emqx_gateway_cm.erl @@ -48,6 +48,10 @@ , connection_closed/2 ]). +-export([ with_channel/3 + , lookup_channels/2 + ]). + %% Internal funcs for getting tabname by GatewayId -export([cmtabs/1, tabname/2]). diff --git a/apps/emqx_gateway/src/emqx_gateway_http.erl b/apps/emqx_gateway/src/emqx_gateway_http.erl index 130c31243..5b7055d25 100644 --- a/apps/emqx_gateway/src/emqx_gateway_http.erl +++ b/apps/emqx_gateway/src/emqx_gateway_http.erl @@ -141,63 +141,44 @@ kickout_client(Node, GwName, ClientId) -> | {ok, list()}. list_client_subscriptions(GwName, ClientId) -> %% Get the subscriptions from session-info - case emqx_gateway_cm:get_chan_info(GwName, ClientId) of - undefined -> - {error, not_found}; - Infos -> - Subs = maps:get(subscriptions, Infos, #{}), - maps:fold(fun(K, V, Acc) -> - [maps:merge( - #{topic => K}, - maps:with([qos, nl, rap, rh], V)) - |Acc] - end, [], Subs) - end. + with_channel(GwName, ClientId, + fun(Pid) -> + Subs = emqx_gateway_conn:call( + Pid, + subscriptions, ?DEFAULT_CALL_TIMEOUT), + {ok, lists:map(fun({Topic, SubOpts}) -> + SubOpts#{topic => Topic} + end, Subs)} + end). -spec client_subscribe(gateway_name(), emqx_type:clientid(), emqx_type:topic(), emqx_type:subopts()) -> {error, any()} | ok. client_subscribe(GwName, ClientId, Topic, SubOpts) -> - case emqx_gateway_cm:lookup_channels(GwName, ClientId) of - [] -> {error, not_found}; - [Pid] -> - %% fixed conn module? + with_channel(GwName, ClientId, + fun(Pid) -> emqx_gateway_conn:call( Pid, {subscribe, Topic, SubOpts}, ?DEFAULT_CALL_TIMEOUT - ); - Pids -> - ?LOG(warning, "More than one client process ~p was found " - "clientid ~s", [Pids, ClientId]), - _ = [ - emqx_gateway_conn:call( - Pid, {subscribe, Topic, SubOpts}, - ?DEFAULT_CALL_TIMEOUT - ) || Pid <- Pids], - ok - end. + ) + end). -spec client_unsubscribe(gateway_name(), emqx_type:clientid(), emqx_type:topic()) -> {error, any()} | ok. client_unsubscribe(GwName, ClientId, Topic) -> - case emqx_gateway_cm:lookup_channels(GwName, ClientId) of - [] -> {error, not_found}; - [Pid] -> + with_channel(GwName, ClientId, + fun(Pid) -> emqx_gateway_conn:call( - Pid, {unsubscribe, Topic}, - ?DEFAULT_CALL_TIMEOUT); - Pids -> - ?LOG(warning, "More than one client process ~p was found " - "clientid ~s", [Pids, ClientId]), - _ = [ - emqx_gateway_conn:call( - Pid, {unsubscribe, Topic}, - ?DEFAULT_CALL_TIMEOUT - ) || Pid <- Pids], - ok + Pid, {unsubscribe, Topic}, ?DEFAULT_CALL_TIMEOUT) + end). + +with_channel(GwName, ClientId, Fun) -> + case emqx_gateway_cm:with_channel(GwName, ClientId, Fun) of + undefined -> {error, not_found}; + Res -> Res end. %%-------------------------------------------------------------------- @@ -206,10 +187,11 @@ client_unsubscribe(GwName, ClientId, Topic) -> -spec return_http_error(integer(), binary()) -> binary(). return_http_error(Code, Msg) -> - emqx_json:encode( - #{code => codestr(Code), - reason => emqx_gateway_utils:stringfy(Msg) - }). + {Code, emqx_json:encode( + #{code => codestr(Code), + reason => emqx_gateway_utils:stringfy(Msg) + }) + }. codestr(404) -> 'RESOURCE_NOT_FOUND'; codestr(401) -> 'NOT_SUPPORTED_NOW'; diff --git a/apps/emqx_gateway/src/emqx_gateway_utils.erl b/apps/emqx_gateway/src/emqx_gateway_utils.erl index 3300ebf69..2b4e9f0a2 100644 --- a/apps/emqx_gateway/src/emqx_gateway_utils.erl +++ b/apps/emqx_gateway/src/emqx_gateway_utils.erl @@ -209,5 +209,6 @@ default_subopts() -> #{rh => 0, %% Retain Handling rap => 0, %% Retain as Publish nl => 0, %% No Local - qos => 0 %% QoS + qos => 0, %% QoS + is_new => true }. diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl index 3e7fe8b42..a57c8e667 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl @@ -393,11 +393,9 @@ handle_in(?PACKET(?CMD_SUBSCRIBE, Headers), [] -> ErrMsg = "Permission denied", handle_out(error, {receipt_id(Headers), ErrMsg}, Channel); - [MountedTopic|_] -> - NChannel1 = NChannel#channel{ - subscriptions = [{SubId, MountedTopic, Ack} - | Subs] - }, + [{MountedTopic, SubOpts}|_] -> + NSubs = [{SubId, MountedTopic, Ack, SubOpts}|Subs], + NChannel1 = NChannel#channel{subscriptions = NSubs}, handle_out(receipt, receipt_id(Headers), NChannel1) end; {error, ErrMsg, NChannel} -> @@ -415,7 +413,7 @@ handle_in(?PACKET(?CMD_UNSUBSCRIBE, Headers), SubId = header(<<"id">>, Headers), {ok, NChannel} = case lists:keyfind(SubId, 1, Subs) of - {SubId, MountedTopic, _Ack} -> + {SubId, MountedTopic, _Ack, _SubOpts} -> Topic = emqx_mountpoint:unmount(Mountpoint, MountedTopic), %% XXX: eval the return topics? _ = run_hooks(Ctx, 'client.unsubscribe', @@ -539,15 +537,16 @@ trans_pipeline([{Func, Args}|More], Outgoings, Channel) -> %% Subs parse_topic_filter({SubId, Topic}, Channel) -> - TopicFilter = emqx_topic:parse(Topic), - {ok, {SubId, TopicFilter}, Channel}. + {ParsedTopic, SubOpts} = emqx_topic:parse(Topic), + NSubOpts = SubOpts#{sub_props => #{subid => SubId}}, + {ok, {SubId, {ParsedTopic, NSubOpts}}, Channel}. -check_subscribed_status({SubId, TopicFilter}, +check_subscribed_status({SubId, {ParsedTopic, _SubOpts}}, #channel{ subscriptions = Subs, clientinfo = #{mountpoint := Mountpoint} }) -> - MountedTopic = emqx_mountpoint:mount(Mountpoint, TopicFilter), + MountedTopic = emqx_mountpoint:mount(Mountpoint, ParsedTopic), case lists:keyfind(SubId, 1, Subs) of {SubId, MountedTopic, _Ack} -> ok; @@ -557,11 +556,11 @@ check_subscribed_status({SubId, TopicFilter}, ok end. -check_sub_acl({_SubId, TopicFilter}, +check_sub_acl({_SubId, {ParsedTopic, _SubOpts}}, #channel{ ctx = Ctx, clientinfo = ClientInfo}) -> - case emqx_gateway_ctx:authorize(Ctx, ClientInfo, subscribe, TopicFilter) of + case emqx_gateway_ctx:authorize(Ctx, ClientInfo, subscribe, ParsedTopic) of deny -> {error, "ACL Deny"}; allow -> ok end. @@ -571,17 +570,17 @@ do_subscribe(TopicFilters, Channel) -> do_subscribe([], _Channel, Acc) -> lists:reverse(Acc); -do_subscribe([{TopicFilter, Option}|More], +do_subscribe([{ParsedTopic, SubOpts0}|More], Channel = #channel{ ctx = Ctx, clientinfo = ClientInfo = #{clientid := ClientId, mountpoint := Mountpoint}}, Acc) -> - SubOpts = maps:merge(emqx_gateway_utils:default_subopts(), Option), - MountedTopic = emqx_mountpoint:mount(Mountpoint, TopicFilter), + SubOpts = maps:merge(emqx_gateway_utils:default_subopts(), SubOpts0), + MountedTopic = emqx_mountpoint:mount(Mountpoint, ParsedTopic), _ = emqx_broker:subscribe(MountedTopic, ClientId, SubOpts), run_hooks(Ctx, 'session.subscribed', [ClientInfo, MountedTopic, SubOpts]), - do_subscribe(More, Channel, [MountedTopic|Acc]). + do_subscribe(More, Channel, [{MountedTopic, SubOpts}|Acc]). %%-------------------------------------------------------------------- %% Handle outgoing packet @@ -631,7 +630,7 @@ handle_call({subscribe, Topic, SubOpts}, subscriptions = Subs }) -> case maps:get(subid, - maps:get(sub_prop, SubOpts, #{}), + maps:get(sub_props, SubOpts, #{}), undefined) of undefined -> reply({error, no_subid}, Channel); @@ -641,11 +640,12 @@ handle_call({subscribe, Topic, SubOpts}, , fun check_subscribed_status/2 ], {SubId, {Topic, SubOpts}}, Channel) of {ok, {_, TopicFilter}, NChannel} -> - [MountedTopic] = do_subscribe([TopicFilter], NChannel), - NChannel1 = NChannel#channel{ - subscriptions = - [{SubId, MountedTopic, <<"auto">>}|Subs] - }, + [{MountedTopic, NSubOpts}] = do_subscribe( + [TopicFilter], + NChannel + ), + NSubs = [{SubId, MountedTopic, <<"auto">>, NSubOpts}|Subs], + NChannel1 = NChannel#channel{subscriptions = NSubs}, reply(ok, NChannel1); {error, ErrMsg, NChannel} -> ?LOG(error, "Failed to subscribe topic ~s, reason: ~s", @@ -670,6 +670,14 @@ handle_call({unsubscribe, Topic}, subscriptions = lists:keydelete(MountedTopic, 2, Subs)} ); +%% Reply :: [{emqx_types:topic(), emqx_types:subopts()}] +handle_call(subscriptions, Channel = #channel{subscriptions = Subs}) -> + Reply = lists:map( + fun({_SubId, Topic, _Ack, SubOpts}) -> + {Topic, SubOpts} + end, Subs), + reply(Reply, Channel); + handle_call(kick, Channel) -> NChannel = ensure_disconnected(kicked, Channel), Frame = error_frame(undefined, <<"Kicked out">>), From fd12a7ac9c828514dd6148e84a9f082747df4af4 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Wed, 1 Sep 2021 14:50:02 +0800 Subject: [PATCH 046/109] chore(gw): improve the gateway api swagger codes --- apps/emqx_gateway/src/emqx_gateway_api.erl | 434 ++++++++---------- .../src/emqx_gateway_api_clients.erl | 4 +- .../src/stomp/emqx_stomp_channel.erl | 6 +- apps/emqx_management/src/emqx_mgmt_util.erl | 5 + 4 files changed, 199 insertions(+), 250 deletions(-) diff --git a/apps/emqx_gateway/src/emqx_gateway_api.erl b/apps/emqx_gateway/src/emqx_gateway_api.erl index 2ff002fd5..9c9398945 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api.erl @@ -18,12 +18,6 @@ -behaviour(minirest_api). --compile(nowarn_unused_function). - --import(emqx_mgmt_util, - [ schema/1 - ]). - -import(emqx_gateway_http, [ return_http_error/2 ]). @@ -37,18 +31,160 @@ , gateway_insta_stats/2 ]). --define(EXAMPLE_GATEWAY_LIST, - [ #{ name => <<"lwm2m">> - , status => <<"running">> - , started_at => <<"2021-08-19T11:45:56.006373+08:00">> - , max_connection => 1024000 - , current_connection => 1000 - , listeners => [ - #{name => <<"lw-udp-1">>, status => <<"activing">>}, - #{name => <<"lw-udp-2">>, status => <<"inactived">>} - ] - } - ]). +%%-------------------------------------------------------------------- +%% minirest behaviour callbacks +%%-------------------------------------------------------------------- + +api_spec() -> + {metadata(apis()), []}. + +apis() -> + [ {"/gateway", gateway} + , {"/gateway/:name", gateway_insta} + , {"/gateway/:name/stats", gateway_insta_stats} + ]. +%%-------------------------------------------------------------------- +%% http handlers + +gateway(get, Request) -> + Params = maps:get(query_string, Request, #{}), + Status = case maps:get(<<"status">>, Params, undefined) of + undefined -> all; + S0 -> binary_to_existing_atom(S0, utf8) + end, + {200, emqx_gateway_http:gateways(Status)}. + +gateway_insta(delete, #{bindings := #{name := Name0}}) -> + Name = binary_to_existing_atom(Name0), + case emqx_gateway:unload(Name) of + ok -> + {204}; + {error, not_found} -> + return_http_error(404, <<"Gateway not found">>) + end; +gateway_insta(get, #{bindings := #{name := Name0}}) -> + Name = binary_to_existing_atom(Name0), + case emqx_gateway:lookup(Name) of + #{config := _Config} -> + %% FIXME: Got the parsed config, but we should return rawconfig to + %% frontend + RawConf = emqx_config:fill_defaults( + emqx_config:get_root_raw([<<"gateway">>]) + ), + {200, emqx_map_lib:deep_get([<<"gateway">>, Name0], RawConf)}; + undefined -> + return_http_error(404, <<"Gateway not found">>) + end; +gateway_insta(put, #{body := RawConfsIn, + bindings := #{name := Name} + }) -> + %% FIXME: Cluster Consistence ?? + case emqx_gateway:update_rawconf(Name, RawConfsIn) of + ok -> + {200}; + {error, not_found} -> + return_http_error(404, <<"Gateway not found">>); + {error, Reason} -> + return_http_error(500, Reason) + end. + +gateway_insta_stats(get, _Req) -> + return_http_error(401, <<"Implement it later (maybe 5.1)">>). + + +%%-------------------------------------------------------------------- +%% Swagger defines +%%-------------------------------------------------------------------- + +metadata(APIs) -> + metadata(APIs, []). +metadata([], APIAcc) -> + lists:reverse(APIAcc); +metadata([{Path, Fun}|More], APIAcc) -> + Methods = [get, post, put, delete, patch], + Mds = lists:foldl(fun(M, Acc) -> + try + Acc#{M => swagger(Path, M)} + catch + error : function_clause -> + Acc + end + end, #{}, Methods), + metadata(More, [{Path, Mds, Fun} | APIAcc]). + +swagger("/gateway", get) -> + #{ description => <<"Get gateway list">> + , parameters => params_gateway_status_in_qs() + , responses => + #{ <<"200">> => schema_gateway_overview_list() } + }; +swagger("/gateway/:name", get) -> + #{ description => <<"Get the gateway configurations">> + , parameters => params_gateway_name_in_path() + , responses => + #{ <<"404">> => schema_not_found() + , <<"200">> => schema_gateway_conf() + } + }; +swagger("/gateway/:name", delete) -> + #{ description => <<"Delete/Unload the gateway">> + , parameters => params_gateway_name_in_path() + , responses => + #{ <<"404">> => schema_not_found() + , <<"204">> => schema_no_content() + } + }; +swagger("/gateway/:name", put) -> + #{ description => <<"Update the gateway configurations/status">> + , parameters => params_gateway_name_in_path() + , requestBody => schema_gateway_conf() + , responses => + #{ <<"404">> => schema_not_found() + , <<"200">> => schema_no_content() + } + }; +swagger("/gateway/:name/stats", get) -> + #{ description => <<"Get gateway Statistic">> + , parameters => params_gateway_name_in_path() + , responses => + #{ <<"404">> => schema_not_found() + , <<"200">> => schema_gateway_stats() + } + }. + +%%-------------------------------------------------------------------- +%% params defines + +params_gateway_name_in_path() -> + [#{ name => name + , in => path + , schema => #{type => string} + , required => true + }]. + +params_gateway_status_in_qs() -> + [#{ name => status + , in => query + , schema => #{type => string} + , required => false + }]. + +%%-------------------------------------------------------------------- +%% schemas + +schema_not_found() -> + emqx_mgmt_util:error_schema(<<"Gateway not found or unloaded">>). + +schema_no_content() -> + #{description => <<"No Content">>}. + +schema_gateway_overview_list() -> + emqx_mgmt_util:array_schema( + #{ type => object + , properties => properties_gateway_overview() + }, + <<"Gateway Overview list">> + ). %% XXX: This is whole confs for all type gateways. It is used to fill the %% default configurations and generate the swagger-schema @@ -154,234 +290,42 @@ %% --- END --define(EXAMPLE_GATEWAY_STATS, #{ - max_connection => 10240000, - current_connection => 1000, - messages_in => 100.24, - messages_out => 32.5 - }). +schema_gateway_conf() -> + emqx_mgmt_util:schema( + #{oneOf => + [ emqx_mgmt_api_configs:gen_schema(?STOMP_GATEWAY_CONFS) + , emqx_mgmt_api_configs:gen_schema(?MQTTSN_GATEWAY_CONFS) + , emqx_mgmt_api_configs:gen_schema(?COAP_GATEWAY_CONFS) + , emqx_mgmt_api_configs:gen_schema(?LWM2M_GATEWAY_CONFS) + , emqx_mgmt_api_configs:gen_schema(?EXPROTO_GATEWAY_CONFS) + ]}). + +schema_gateway_stats() -> + emqx_mgmt_util:schema( + #{ type => object + , properties => + #{ a_key => #{type => string} + }}). %%-------------------------------------------------------------------- -%% minirest behaviour callbacks -%%-------------------------------------------------------------------- +%% properties -api_spec() -> - {apis(), schemas()}. - -apis() -> - [ {"/gateway", metadata(gateway), gateway} - , {"/gateway/:name", metadata(gateway_insta), gateway_insta} - , {"/gateway/:name/stats", metadata(gateway_insta_stats), gateway_insta_stats} - ]. - -metadata(gateway) -> - #{get => #{ - description => <<"Get gateway list">>, - parameters => [ - #{name => status, - in => query, - schema => #{type => string}, - required => false - } +properties_gateway_overview() -> + ListenerProps = + [ {name, string, + <<"Listener Name">>} + , {status, string, + <<"Listener Status">>, [<<"activing">>, <<"inactived">>]} ], - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"gateway_overrview">>), - examples => #{ - simple => #{ - summary => <<"Gateway List Example">>, - value => emqx_json:encode(?EXAMPLE_GATEWAY_LIST) - } - } - } - } - } - } - }}; - -metadata(gateway_insta) -> - UriNameParamDef = #{name => name, - in => path, - schema => #{type => string}, - required => true - }, - NameNotFoundRespDef = - #{description => <<"Not Found">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>), - examples => #{ - simple => #{ - summary => <<"Not Found">>, - value => #{ - code => <<"NOT_FOUND">>, - message => <<"The gateway not found">> - } - } - } - } - }}, - #{delete => #{ - description => <<"Delete/Unload the gateway">>, - parameters => [UriNameParamDef], - responses => #{ - <<"404">> => NameNotFoundRespDef, - <<"204">> => #{description => <<"No Content">>} - } - }, - get => #{ - description => <<"Get the gateway configurations">>, - parameters => [UriNameParamDef], - responses => #{ - <<"404">> => NameNotFoundRespDef, - <<"200">> => schema(schema_for_gateway_conf()) - } - }, - put => #{ - description => <<"Update the gateway configurations/status">>, - parameters => [UriNameParamDef], - requestBody => schema(schema_for_gateway_conf()), - responses => #{ - <<"404">> => NameNotFoundRespDef, - <<"200">> => #{description => <<"Changed">>} - } - } - }; - -metadata(gateway_insta_stats) -> - #{get => #{ - description => <<"Get gateway Statistic">>, - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"gateway_stats">>), - examples => #{ - simple => #{ - summary => <<"Gateway Statistic">>, - value => emqx_json:encode(?EXAMPLE_GATEWAY_STATS) - } - } - } - } - } - } - }}. - -schemas() -> - [ #{<<"gateway_overrview">> => schema_for_gateway_overrview()} - , #{<<"gateway_stats">> => schema_for_gateway_stats()} - ]. - -schema_for_gateway_overrview() -> - #{type => array, - items => #{ - type => object, - properties => #{ - name => #{ - type => string, - example => <<"lwm2m">> - }, - status => #{ - type => string, - enum => [<<"running">>, <<"stopped">>, <<"unloaded">>], - example => <<"running">> - }, - started_at => #{ - type => string, - example => <<"2021-08-19T11:45:56.006373+08:00">> - }, - max_connection => #{ - type => integer, - example => 1024000 - }, - current_connection => #{ - type => integer, - example => 1000 - }, - listeners => #{ - type => array, - items => #{ - type => object, - properties => #{ - name => #{ - type => string, - example => <<"lw-udp">> - }, - status => #{ - type => string, - enum => [<<"activing">>, <<"inactived">>] - } - } - } - } - } - } - }. - -schema_for_gateway_conf() -> - #{oneOf => - [ emqx_mgmt_api_configs:gen_schema(?STOMP_GATEWAY_CONFS) - , emqx_mgmt_api_configs:gen_schema(?MQTTSN_GATEWAY_CONFS) - , emqx_mgmt_api_configs:gen_schema(?COAP_GATEWAY_CONFS) - , emqx_mgmt_api_configs:gen_schema(?LWM2M_GATEWAY_CONFS) - , emqx_mgmt_api_configs:gen_schema(?EXPROTO_GATEWAY_CONFS) - ]}. - -schema_for_gateway_stats() -> - #{type => object, - properties => #{ - a_key => #{type => string} - }}. - -%%-------------------------------------------------------------------- -%% http handlers - -gateway(get, Request) -> - Params = maps:get(query_string, Request, #{}), - Status = case maps:get(<<"status">>, Params, undefined) of - undefined -> all; - S0 -> binary_to_existing_atom(S0, utf8) - end, - {200, emqx_gateway_http:gateways(Status)}. - -gateway_insta(delete, #{bindings := #{name := Name0}}) -> - Name = binary_to_existing_atom(Name0), - case emqx_gateway:unload(Name) of - ok -> - {200}; - {error, not_found} -> - return_http_error(404, <<"Gateway not found">>) - end; -gateway_insta(get, #{bindings := #{name := Name0}}) -> - Name = binary_to_existing_atom(Name0), - case emqx_gateway:lookup(Name) of - #{config := _Config} -> - %% FIXME: Got the parsed config, but we should return rawconfig to - %% frontend - RawConf = emqx_config:fill_defaults( - emqx_config:get_root_raw([<<"gateway">>]) - ), - {200, emqx_map_lib:deep_get([<<"gateway">>, Name0], RawConf)}; - undefined -> - return_http_error(404, <<"Gateway not found">>) - end; -gateway_insta(put, #{body := RawConfsIn, - bindings := #{name := Name} - }) -> - %% FIXME: Cluster Consistence ?? - case emqx_gateway:update_rawconf(Name, RawConfsIn) of - ok -> - {200}; - {error, not_found} -> - return_http_error(404, <<"Gateway not found">>); - {error, Reason} -> - return_http_error(500, Reason) - end. - -gateway_insta_stats(get, _Req) -> - return_http_error(401, <<"Implement it later (maybe 5.1)">>). + emqx_mgmt_util:properties( + [ {name, string, + <<"Gateway Name">>} + , {status, string, + <<"Gateway Status">>, + [<<"running">>, <<"stopped">>, <<"unloaded">>]} + , {started_at, string, + <<>>} + , {max_connection, integer, <<>>} + , {current_connection, integer, <<>>} + , {listeners, {array, object}, ListenerProps} + ]). diff --git a/apps/emqx_gateway/src/emqx_gateway_api_clients.erl b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl index 99876c917..b463fb468 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api_clients.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl @@ -481,7 +481,7 @@ queries(Ls) -> end, Ls). %%-------------------------------------------------------------------- -%% Schemas +%% schemas schema_not_found() -> emqx_mgmt_util:error_schema(<<"Gateway not found or unloaded">>). @@ -518,7 +518,7 @@ schema_subscription() -> ). %%-------------------------------------------------------------------- -%% Object properties def +%% properties defines properties_client() -> emqx_mgmt_util:properties( diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl index a57c8e667..1e0c5e2d4 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl @@ -548,9 +548,9 @@ check_subscribed_status({SubId, {ParsedTopic, _SubOpts}}, }) -> MountedTopic = emqx_mountpoint:mount(Mountpoint, ParsedTopic), case lists:keyfind(SubId, 1, Subs) of - {SubId, MountedTopic, _Ack} -> + {SubId, MountedTopic, _Ack, _SubOpts} -> ok; - {SubId, _OtherTopic, _Ack} -> + {SubId, _OtherTopic, _Ack, _SubOpts} -> {error, "Conflict subscribe id"}; false -> ok @@ -795,7 +795,7 @@ handle_deliver(Delivers, Frames0 = lists:foldl(fun({_, _, Message}, Acc) -> Topic0 = emqx_message:topic(Message), case lists:keyfind(Topic0, 2, Subs) of - {Id, Topic, Ack} -> + {Id, Topic, Ack, _SubOpts} -> %% XXX: refactor later metrics_inc('messages.delivered', Channel), NMessage = run_hooks_without_metrics( diff --git a/apps/emqx_management/src/emqx_mgmt_util.erl b/apps/emqx_management/src/emqx_mgmt_util.erl index 4c1009610..48bef33ac 100644 --- a/apps/emqx_management/src/emqx_mgmt_util.erl +++ b/apps/emqx_management/src/emqx_mgmt_util.erl @@ -224,6 +224,11 @@ properties([{Key, Type} | Props], Acc) -> properties([{Key, object, Props1} | Props], Acc) -> properties(Props, maps:put(Key, #{type => object, properties => properties(Props1)}, Acc)); +properties([{Key, {array, object}, Props1} | Props], Acc) -> + properties(Props, maps:put(Key, #{type => array, + items => #{type => object, + properties => properties(Props1) + }}, Acc)); properties([{Key, {array, Type}, Desc} | Props], Acc) -> properties(Props, maps:put(Key, #{type => array, items => #{type => Type}, From dc05cdc58633da5934c9fd8c724b1d5cb9a8ddbe Mon Sep 17 00:00:00 2001 From: JianBo He Date: Wed, 1 Sep 2021 15:21:07 +0800 Subject: [PATCH 047/109] chore(gw): fix dialyzer warnings --- apps/emqx_gateway/src/emqx_gateway_api_clients.erl | 2 +- apps/emqx_gateway/src/emqx_gateway_http.erl | 5 +++-- apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl | 4 +++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/emqx_gateway/src/emqx_gateway_api_clients.erl b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl index b463fb468..fcfea7343 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api_clients.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api_clients.erl @@ -117,7 +117,7 @@ clients_insta(delete, #{ bindings := #{name := GwName0, }) -> GwName = binary_to_existing_atom(GwName0), ClientId = emqx_mgmt_util:urldecode(ClientId0), - emqx_gateway_http:kickout_client(GwName, ClientId), + _ = emqx_gateway_http:kickout_client(GwName, ClientId), {200}. %% FIXME: diff --git a/apps/emqx_gateway/src/emqx_gateway_http.erl b/apps/emqx_gateway/src/emqx_gateway_http.erl index 5b7055d25..2aa6b4b3d 100644 --- a/apps/emqx_gateway/src/emqx_gateway_http.erl +++ b/apps/emqx_gateway/src/emqx_gateway_http.erl @@ -101,7 +101,8 @@ listener_name(GwName, Type, LisName) -> %% Mgmt APIs - clients %%-------------------------------------------------------------------- --spec lookup_client(gateway_name(), emqx_type:clientid(), function()) -> list(). +-spec lookup_client(gateway_name(), + emqx_type:clientid(), {atom(), atom()}) -> list(). lookup_client(GwName, ClientId, FormatFun) -> lists:append([lookup_client(Node, GwName, {clientid, ClientId}, FormatFun) || Node <- ekka_mnesia:running_nodes()]). @@ -185,7 +186,7 @@ with_channel(GwName, ClientId, Fun) -> %% Utils %%-------------------------------------------------------------------- --spec return_http_error(integer(), binary()) -> binary(). +-spec return_http_error(integer(), binary()) -> {integer(), binary()}. return_http_error(Code, Msg) -> {Code, emqx_json:encode( #{code => codestr(Code), diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl index 0707534b7..e8a763332 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl @@ -23,7 +23,6 @@ -include_lib("emqx/include/emqx_mqtt.hrl"). -include_lib("emqx/include/logger.hrl"). - %% API -export([ info/1 , info/2 @@ -1108,6 +1107,9 @@ handle_call({subscribe, _Topic, _Subopts}, Channel) -> handle_call({unsubscribe, _Topic}, Channel) -> reply({error, not_supported_now}, Channel); +handle_call(subscriptions, Channel) -> + reply({error, not_supported_now}, Channel); + handle_call(kick, Channel) -> NChannel = ensure_disconnected(kicked, Channel), shutdown_and_reply(kicked, ok, NChannel); From 956308f0ca7a075a84c490d4483237f1da0f844f Mon Sep 17 00:00:00 2001 From: JianBo He Date: Thu, 2 Sep 2021 11:32:33 +0800 Subject: [PATCH 048/109] feat(gw-mqttsn): support subscribe/unsubscribe operation --- .../src/bhvrs/emqx_gateway_conn.erl | 2 - .../src/mqttsn/emqx_sn_channel.erl | 143 +++++++++++------- .../src/stomp/emqx_stomp_channel.erl | 5 +- 3 files changed, 93 insertions(+), 57 deletions(-) diff --git a/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl b/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl index fa0a830e5..8f6cf6e97 100644 --- a/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl +++ b/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl @@ -20,7 +20,6 @@ -include_lib("emqx/include/types.hrl"). -include_lib("emqx/include/logger.hrl"). - %% API -export([ start_link/3 , stop/1 @@ -48,7 +47,6 @@ %% Internal callback -export([wakeup_from_hib/2, recvloop/2]). - -record(state, { %% TCP/SSL/UDP/DTLS Wrapped Socket socket :: {esockd_transport, esockd:socket()} | {udp, _, _}, diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl index e8a763332..2834c27f6 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl @@ -95,9 +95,9 @@ }). -define(DEFAULT_OVERRIDE, - #{ clientid => <<"">> %% Generate clientid by default - , username => <<"${Packet.headers.login}">> - , password => <<"${Packet.headers.passcode}">> + #{ clientid => <<"${ConnInfo.clientid}">> + %, username => <<"${ConnInfo.clientid}">> + %, password => <<"${Packet.headers.passcode}">> }). -define(INFO_KEYS, [conninfo, conn_state, clientinfo, session, will_msg]). @@ -189,9 +189,10 @@ stats(#channel{session = Session})-> set_conn_state(ConnState, Channel) -> Channel#channel{conn_state = ConnState}. -enrich_conninfo(?SN_CONNECT_MSG(_Flags, _ProtoId, Duration, _ClientId), +enrich_conninfo(?SN_CONNECT_MSG(_Flags, _ProtoId, Duration, ClientId), Channel = #channel{conninfo = ConnInfo}) -> - NConnInfo = ConnInfo#{ proto_name => <<"MQTT-SN">> + NConnInfo = ConnInfo#{ clientid => ClientId + , proto_name => <<"MQTT-SN">> , proto_ver => <<"1.2">> , clean_start => true , keepalive => Duration @@ -592,9 +593,11 @@ handle_in(SubPkt = ?SN_SUBSCRIBE_MSG(_, MsgId, _), Channel) -> case emqx_misc:pipeline( [ fun preproc_subs_type/2 , fun check_subscribe_authz/2 + , fun run_client_subs_hook/2 , fun do_subscribe/2 ], SubPkt, Channel) of - {ok, {TopicId, GrantedQoS}, NChannel} -> + {ok, {TopicId, _TopicName, SubOpts}, NChannel} -> + GrantedQoS = maps:get(qos, SubOpts), SubAck = ?SN_SUBACK_MSG(#mqtt_sn_flags{qos = GrantedQoS}, TopicId, MsgId, ?SN_RC_ACCEPTED), {ok, outgoing_and_update(SubAck), NChannel}; @@ -610,6 +613,7 @@ handle_in(UnsubPkt = ?SN_UNSUBSCRIBE_MSG(_, MsgId, TopicIdOrName), Channel) -> case emqx_misc:pipeline( [ fun preproc_unsub_type/2 + , fun run_client_unsub_hook/2 , fun do_unsubscribe/2 ], UnsubPkt, Channel) of {ok, _TopicName, NChannel} -> @@ -841,13 +845,10 @@ check_subscribe_authz({_TopicId, TopicName, _QoS}, {error, ?SN_RC_NOT_AUTHORIZE} end. -do_subscribe({TopicId, TopicName, QoS}, - Channel = #channel{ - ctx = Ctx, - session = Session, - clientinfo = ClientInfo - = #{mountpoint := Mountpoint}}) -> - +run_client_subs_hook({TopicId, TopicName, QoS}, + Channel = #channel{ + ctx = Ctx, + clientinfo = ClientInfo}) -> {TopicName1, SubOpts0} = emqx_topic:parse(TopicName), TopicFilters = [{TopicName1, SubOpts0#{qos => QoS}}], case run_hooks(Ctx, 'client.subscribe', @@ -855,19 +856,26 @@ do_subscribe({TopicId, TopicName, QoS}, [] -> ?LOG(warning, "Skip to subscribe ~s, " "due to 'client.subscribe' denied!", [TopicName]), - {ok, Channel}; + {error, ?SN_EXCEED_LIMITATION}; [{NTopicName, NSubOpts}|_] -> - NTopicName1 = emqx_mountpoint:mount(Mountpoint, NTopicName), - NSubOpts1 = maps:merge(?DEFAULT_SUBOPTS, NSubOpts), - case emqx_session:subscribe(ClientInfo, NTopicName1, NSubOpts1, Session) of - {ok, NSession} -> - {ok, {TopicId, QoS}, - Channel#channel{session = NSession}}; - {error, ?RC_QUOTA_EXCEEDED} -> - ?LOG(warning, "Cannot subscribe ~s due to ~s.", - [TopicName, emqx_reason_codes:text(?RC_QUOTA_EXCEEDED)]), - {error, ?SN_EXCEED_LIMITATION} - end + {ok, {TopicId, NTopicName, NSubOpts}, Channel} + end. + +do_subscribe({TopicId, TopicName, SubOpts}, + Channel = #channel{ + session = Session, + clientinfo = ClientInfo + = #{mountpoint := Mountpoint}}) -> + NTopicName = emqx_mountpoint:mount(Mountpoint, TopicName), + NSubOpts = maps:merge(?DEFAULT_SUBOPTS, SubOpts), + case emqx_session:subscribe(ClientInfo, NTopicName, NSubOpts, Session) of + {ok, NSession} -> + {ok, {TopicId, NTopicName, NSubOpts}, + Channel#channel{session = NSession}}; + {error, ?RC_QUOTA_EXCEEDED} -> + ?LOG(warning, "Cannot subscribe ~s due to ~s.", + [TopicName, emqx_reason_codes:text(?RC_QUOTA_EXCEEDED)]), + {error, ?SN_EXCEED_LIMITATION} end. %%-------------------------------------------------------------------- @@ -899,33 +907,42 @@ preproc_unsub_type(?SN_UNSUBSCRIBE_MSG_TYPE(?SN_SHORT_TOPIC, end, {ok, TopicName, Channel}. -do_unsubscribe(TopicName, - Channel = #channel{ - ctx = Ctx, - session = Session, - clientinfo = ClientInfo - = #{mountpoint := Mountpoint}}) -> +run_client_unsub_hook(TopicName, + Channel = #channel{ + ctx = Ctx, + clientinfo = ClientInfo + }) -> TopicFilters = [emqx_topic:parse(TopicName)], case run_hooks(Ctx, 'client.unsubscribe', [ClientInfo, #{}], TopicFilters) of [] -> - %% Skip to unsubscribe - {ok, Channel}; - [{NTopicName, NSubOpts}|_] -> - NTopicName1 = emqx_mountpoint:mount(Mountpoint, NTopicName), - NSubOpts1 = maps:merge( - emqx_gateway_utils:default_subopts(), - NSubOpts - ), - case emqx_session:unsubscribe(ClientInfo, NTopicName1, - NSubOpts1, Session) of - {ok, NSession} -> - {ok, Channel#channel{session = NSession}}; - {error, ?RC_NO_SUBSCRIPTION_EXISTED} -> - {ok, Channel} - end + {ok, [], Channel}; + NTopicFilters -> + {ok, NTopicFilters, Channel} end. +do_unsubscribe(TopicFilters, + Channel = #channel{ + session = Session, + clientinfo = ClientInfo + = #{mountpoint := Mountpoint}}) -> + NChannel = + lists:foldl(fun({TopicName, SubOpts}, ChannAcc) -> + NTopicName = emqx_mountpoint:mount(Mountpoint, TopicName), + NSubOpts = maps:merge( + emqx_gateway_utils:default_subopts(), + SubOpts + ), + case emqx_session:unsubscribe(ClientInfo, NTopicName, + NSubOpts, Session) of + {ok, NSession} -> + ChannAcc#channel{session = NSession}; + {error, ?RC_NO_SUBSCRIPTION_EXISTED} -> + ChannAcc + end + end, Channel, TopicFilters), + {ok, TopicFilters, NChannel}. + %%-------------------------------------------------------------------- %% Awake & Asleep @@ -1101,14 +1118,36 @@ message_to_packet(MsgId, Message, | {shutdown, Reason :: term(), Reply :: term(), channel()} | {shutdown, Reason :: term(), Reply :: term(), emqx_types:packet(), channel()}. -handle_call({subscribe, _Topic, _Subopts}, Channel) -> - reply({error, not_supported_now}, Channel); +handle_call({subscribe, Topic, SubOpts}, Channel) -> + %% XXX: Only support short_topic_name + SubProps = maps:get(sub_props, SubOpts, #{}), + case maps:get(subtype, SubProps, short_topic_name) of + short_topic_name -> + case byte_size(Topic) of + 2 -> + case do_subscribe({?SN_INVALID_TOPIC_ID, + Topic, SubOpts}, Channel) of + {ok, _, NChannel} -> + reply(ok, NChannel); + {error, ?SN_EXCEED_LIMITATION} -> + reply({error, exceed_limitation}, Channel) + end; + _ -> + reply({error, bad_topic_name}, Channel) + end; + predefined_topic_id -> + reply({error, only_support_short_name_topic}, Channel); + _ -> + reply({error, only_support_short_name_topic}, Channel) + end; -handle_call({unsubscribe, _Topic}, Channel) -> - reply({error, not_supported_now}, Channel); +handle_call({unsubscribe, Topic}, Channel) -> + TopicFilters = [emqx_topic:parse(Topic)], + {ok, _, NChannel} = do_unsubscribe(TopicFilters, Channel), + reply(ok, NChannel); -handle_call(subscriptions, Channel) -> - reply({error, not_supported_now}, Channel); +handle_call(subscriptions, Channel = #channel{session = Session}) -> + reply(maps:to_list(emqx_session:info(subscriptions, Session)), Channel); handle_call(kick, Channel) -> NChannel = ensure_disconnected(kicked, Channel), diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl index 1e0c5e2d4..9bd2dac1b 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl @@ -22,7 +22,6 @@ -include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). - -import(proplists, [get_value/2, get_value/3]). %% API @@ -548,9 +547,9 @@ check_subscribed_status({SubId, {ParsedTopic, _SubOpts}}, }) -> MountedTopic = emqx_mountpoint:mount(Mountpoint, ParsedTopic), case lists:keyfind(SubId, 1, Subs) of - {SubId, MountedTopic, _Ack, _SubOpts} -> + {SubId, MountedTopic, _Ack, _} -> ok; - {SubId, _OtherTopic, _Ack, _SubOpts} -> + {SubId, _OtherTopic, _Ack, _} -> {error, "Conflict subscribe id"}; false -> ok From 304874f0ff21a02153a956968fa65b3670b8e575 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 2 Sep 2021 15:02:38 +0800 Subject: [PATCH 049/109] feat(config): load and merge emqx_override.conf at bootup --- apps/emqx/src/emqx_config.erl | 24 +++++++++++------------- apps/emqx/src/emqx_listeners.erl | 4 ++-- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index 317aba401..d6c257071 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -235,7 +235,7 @@ put_raw(KeyPath, Config) -> do_put(?RAW_CONF, KeyPath, Config). %% in the rear of the list overrides prior values. -spec init_load(module(), [string()] | binary() | hocon:config()) -> ok. init_load(SchemaMod, Conf) when is_list(Conf) orelse is_binary(Conf) -> - ParseOptions = #{format => richmap}, + ParseOptions = #{format => map}, Parser = case is_binary(Conf) of true -> fun hocon:binary/2; false -> fun hocon:files/2 @@ -249,19 +249,14 @@ init_load(SchemaMod, Conf) when is_list(Conf) orelse is_binary(Conf) -> }), error(failed_to_load_hocon_conf) end; -init_load(SchemaMod, RawRichConf) when is_map(RawRichConf) -> - %% check with richmap for line numbers in error reports (future enhancement) - Opts = #{return_plain => true, - nullable => true - }, - %% this call throws exception in case of check failure - {_AppEnvs, CheckedConf} = hocon_schema:map_translate(SchemaMod, RawRichConf, Opts), +init_load(SchemaMod, RawConf0) when is_map(RawConf0) -> ok = save_schema_mod_and_names(SchemaMod), - ok = save_to_config_map(emqx_map_lib:unsafe_atom_key_map(normalize_conf(CheckedConf)), - normalize_conf(hocon_schema:richmap_to_map(RawRichConf))). - -normalize_conf(Conf) -> - maps:with(get_root_names(), Conf). + %% override part of the input conf using emqx_override.conf + RawConf = maps:merge(RawConf0, maps:with(maps:keys(RawConf0), read_override_conf())), + %% check and save configs + {_AppEnvs, CheckedConf} = check_config(SchemaMod, RawConf), + ok = save_to_config_map(maps:with(get_atom_root_names(), CheckedConf), + maps:with(get_root_names(), RawConf)). -spec check_config(module(), raw_config()) -> {AppEnvs, CheckedConf} when AppEnvs :: app_envs(), CheckedConf :: config(). @@ -320,6 +315,9 @@ get_schema_mod(RootName) -> get_root_names() -> maps:get(names, persistent_term:get(?PERSIS_SCHEMA_MODS, #{names => []})). +get_atom_root_names() -> + [atom(N) || N <- get_root_names()]. + -spec save_configs(app_envs(), config(), raw_config(), raw_config()) -> ok | {error, term()}. save_configs(_AppEnvs, Conf, RawConf, OverrideConf) -> %% We may need also support hot config update for the apps that use application envs. diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 8f0141b3a..a91651c6c 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -50,7 +50,7 @@ %% @doc List configured listeners. -spec(list() -> [{ListenerId :: atom(), ListenerConf :: map()}]). list() -> - [{listener_id(ZoneName, LName), LConf} || {ZoneName, LName, LConf} <- do_list()]. + [{listener_id(Type, LName), LConf} || {Type, LName, LConf} <- do_list()]. do_list() -> Listeners = maps:to_list(emqx:get_config([listeners], #{})), @@ -64,7 +64,7 @@ list(Type, Conf) -> -spec is_running(ListenerId :: atom()) -> boolean() | {error, no_found}. is_running(ListenerId) -> - case lists:filtermap(fun({_Zone, Id, #{running := IsRunning}}) -> + case lists:filtermap(fun({_Type, Id, #{running := IsRunning}}) -> Id =:= ListenerId andalso {true, IsRunning} end, do_list()) of [IsRunning] -> IsRunning; From daca99f0f693eabab44f5678c25349190ee8773b Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 2 Sep 2021 20:19:11 +0800 Subject: [PATCH 050/109] feat(config): add option 'persistent => boolean()' to emqx:update_config/3 --- apps/emqx/src/emqx_config.erl | 12 +++++++++-- apps/emqx/src/emqx_config_handler.erl | 18 +++++++++++----- apps/emqx_authz/src/emqx_authz.erl | 30 +++++++++++++++++---------- 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index d6c257071..ae41ee1a1 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -87,8 +87,14 @@ -type update_request() :: term(). -type update_cmd() :: {update, update_request()} | remove. -type update_opts() :: #{ - %% fill the default values into the rawconf map - rawconf_with_defaults => boolean() + %% rawconf_with_defaults: + %% fill the default values into the `raw_config` field of the return value + %% defaults to `false` + rawconf_with_defaults => boolean(), + %% persistent: + %% save the updated config to the emqx_override.conf file + %% defaults to `true` + persistent => boolean() }. -type update_args() :: {update_cmd(), Opts :: update_opts()}. -type update_stage() :: pre_config_update | post_config_update. @@ -339,6 +345,8 @@ save_to_config_map(Conf, RawConf) -> ?MODULE:put_raw(RawConf). -spec save_to_override_conf(raw_config()) -> ok | {error, term()}. +save_to_override_conf(undefined) -> + ok; save_to_override_conf(RawConf) -> FileName = emqx_override_conf_name(), ok = filelib:ensure_dir(FileName), diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl index f16f8a97a..a9020a87a 100644 --- a/apps/emqx/src/emqx_config_handler.erl +++ b/apps/emqx/src/emqx_config_handler.erl @@ -134,17 +134,17 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -process_update_request(ConfKeyPath, _Handlers, {remove, _Opts}) -> +process_update_request(ConfKeyPath, _Handlers, {remove, Opts}) -> OldRawConf = emqx_config:get_root_raw(ConfKeyPath), BinKeyPath = bin_path(ConfKeyPath), NewRawConf = emqx_map_lib:deep_remove(BinKeyPath, OldRawConf), - OverrideConf = emqx_map_lib:deep_remove(BinKeyPath, emqx_config:read_override_conf()), + OverrideConf = remove_from_override_config(BinKeyPath, Opts), {ok, NewRawConf, OverrideConf}; -process_update_request(ConfKeyPath, Handlers, {{update, UpdateReq}, _Opts}) -> +process_update_request(ConfKeyPath, Handlers, {{update, UpdateReq}, Opts}) -> OldRawConf = emqx_config:get_root_raw(ConfKeyPath), case do_update_config(ConfKeyPath, Handlers, OldRawConf, UpdateReq) of {ok, NewRawConf} -> - OverrideConf = update_override_config(NewRawConf), + OverrideConf = update_override_config(NewRawConf, Opts), {ok, NewRawConf, OverrideConf}; Error -> Error end. @@ -237,7 +237,15 @@ merge_to_old_config(UpdateReq, RawConf) when is_map(UpdateReq), is_map(RawConf) merge_to_old_config(UpdateReq, _RawConf) -> {ok, UpdateReq}. -update_override_config(RawConf) -> +remove_from_override_config(_BinKeyPath, #{persistent := false}) -> + undefined; +remove_from_override_config(BinKeyPath, _Opts) -> + OldConf = emqx_config:read_override_conf(), + emqx_map_lib:deep_remove(BinKeyPath, OldConf). + +update_override_config(_RawConf, #{persistent := false}) -> + undefined; +update_override_config(RawConf, _Opts) -> OldConf = emqx_config:read_override_conf(), maps:merge(OldConf, RawConf). diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 0d116882c..f950ed53c 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -30,7 +30,9 @@ , lookup/0 , lookup/1 , move/2 + , move/3 , update/2 + , update/3 , authorize/5 ]). @@ -58,19 +60,25 @@ lookup(Type) -> error:Reason -> {error, Reason} end. -move(Type, #{<<"before">> := Before}) -> - emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), #{<<"before">> => atom(Before)}}); -move(Type, #{<<"after">> := After}) -> - emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), #{<<"after">> => atom(After)}}); -move(Type, Position) -> - emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), Position}). +move(Type, Cmd) -> + move(Type, Cmd, #{}). + +move(Type, #{<<"before">> := Before}, Opts) -> + emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), #{<<"before">> => atom(Before)}}, Opts); +move(Type, #{<<"after">> := After}, Opts) -> + emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), #{<<"after">> => atom(After)}}, Opts); +move(Type, Position, Opts) -> + emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), Position}, Opts). -update({replace_once, Type}, Sources) -> - emqx:update_config(?CONF_KEY_PATH, {{replace_once, atom(Type)}, Sources}); -update({delete_once, Type}, Sources) -> - emqx:update_config(?CONF_KEY_PATH, {{delete_once, atom(Type)}, Sources}); update(Cmd, Sources) -> - emqx:update_config(?CONF_KEY_PATH, {Cmd, Sources}). + update(Cmd, Sources, #{}). + +update({replace_once, Type}, Sources, Opts) -> + emqx:update_config(?CONF_KEY_PATH, {{replace_once, atom(Type)}, Sources}, Opts); +update({delete_once, Type}, Sources, Opts) -> + emqx:update_config(?CONF_KEY_PATH, {{delete_once, atom(Type)}, Sources}, Opts); +update(Cmd, Sources, Opts) -> + emqx:update_config(?CONF_KEY_PATH, {Cmd, Sources}, Opts). pre_config_update({move, Type, <<"top">>}, Conf) when is_list(Conf) -> {Index, _} = find_source_by_type(Type), From a89bc97ed82f453f522ba0006b4bacaa242038bd Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Thu, 2 Sep 2021 20:20:14 +0800 Subject: [PATCH 051/109] fix(config): don't write to override.conf if 'override_conf_file' is not set --- apps/emqx/src/emqx_config.erl | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/apps/emqx/src/emqx_config.erl b/apps/emqx/src/emqx_config.erl index ae41ee1a1..bd6e14e8e 100644 --- a/apps/emqx/src/emqx_config.erl +++ b/apps/emqx/src/emqx_config.erl @@ -258,12 +258,15 @@ init_load(SchemaMod, Conf) when is_list(Conf) orelse is_binary(Conf) -> init_load(SchemaMod, RawConf0) when is_map(RawConf0) -> ok = save_schema_mod_and_names(SchemaMod), %% override part of the input conf using emqx_override.conf - RawConf = maps:merge(RawConf0, maps:with(maps:keys(RawConf0), read_override_conf())), + RawConf = merge_with_override_conf(RawConf0), %% check and save configs {_AppEnvs, CheckedConf} = check_config(SchemaMod, RawConf), ok = save_to_config_map(maps:with(get_atom_root_names(), CheckedConf), maps:with(get_root_names(), RawConf)). +merge_with_override_conf(RawConf) -> + maps:merge(RawConf, maps:with(maps:keys(RawConf), read_override_conf())). + -spec check_config(module(), raw_config()) -> {AppEnvs, CheckedConf} when AppEnvs :: app_envs(), CheckedConf :: config(). check_config(SchemaMod, RawConf) -> @@ -348,13 +351,16 @@ save_to_config_map(Conf, RawConf) -> save_to_override_conf(undefined) -> ok; save_to_override_conf(RawConf) -> - FileName = emqx_override_conf_name(), - ok = filelib:ensure_dir(FileName), - case file:write_file(FileName, jsx:prettify(jsx:encode(RawConf))) of - ok -> ok; - {error, Reason} -> - logger:error("write to ~s failed, ~p", [FileName, Reason]), - {error, Reason} + case emqx_override_conf_name() of + undefined -> ok; + FileName -> + ok = filelib:ensure_dir(FileName), + case file:write_file(FileName, jsx:prettify(jsx:encode(RawConf))) of + ok -> ok; + {error, Reason} -> + logger:error("write to ~s failed, ~p", [FileName, Reason]), + {error, Reason} + end end. load_hocon_file(FileName, LoadType) -> @@ -366,7 +372,7 @@ load_hocon_file(FileName, LoadType) -> end. emqx_override_conf_name() -> - application:get_env(emqx, override_conf_file, "emqx_override.conf"). + application:get_env(emqx, override_conf_file, undefined). do_get(Type, KeyPath) -> Ref = make_ref(), From f4eae8c0cb1765e5032c42dd669455a15808d0b4 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 3 Sep 2021 08:59:00 +0800 Subject: [PATCH 052/109] fix(retainer): test case failed for expired retained msg --- ..._protocol_v5_SUITE.erl => emqx_retainer_mqtt_v5_SUITE.erl} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename apps/emqx_retainer/test/{mqtt_protocol_v5_SUITE.erl => emqx_retainer_mqtt_v5_SUITE.erl} (99%) diff --git a/apps/emqx_retainer/test/mqtt_protocol_v5_SUITE.erl b/apps/emqx_retainer/test/emqx_retainer_mqtt_v5_SUITE.erl similarity index 99% rename from apps/emqx_retainer/test/mqtt_protocol_v5_SUITE.erl rename to apps/emqx_retainer/test/emqx_retainer_mqtt_v5_SUITE.erl index cba40de69..0be5df732 100644 --- a/apps/emqx_retainer/test/mqtt_protocol_v5_SUITE.erl +++ b/apps/emqx_retainer/test/emqx_retainer_mqtt_v5_SUITE.erl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(mqtt_protocol_v5_SUITE). +-module(emqx_retainer_mqtt_v5_SUITE). -compile(export_all). -compile(nowarn_export_all). @@ -117,7 +117,7 @@ t_publish_message_expiry_interval(_) -> {ok, _} = emqtt:publish(Client1, <<"topic/B">>, #{'Message-Expiry-Interval' => 1}, <<"retained message">>, [{qos, 2}, {retain, true}]), {ok, _} = emqtt:publish(Client1, <<"topic/C">>, #{'Message-Expiry-Interval' => 10}, <<"retained message">>, [{qos, 1}, {retain, true}]), {ok, _} = emqtt:publish(Client1, <<"topic/D">>, #{'Message-Expiry-Interval' => 10}, <<"retained message">>, [{qos, 2}, {retain, true}]), - timer:sleep(1000), + timer:sleep(1500), {ok, _, [2]} = emqtt:subscribe(Client1, <<"topic/+">>, 2), Msgs = receive_messages(4), ?assertEqual(2, length(Msgs)), %% [MQTT-3.3.2-5] From be0fd6fddd58597935e1aa97280fa2edac4c93f6 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Fri, 3 Sep 2021 10:57:54 +0800 Subject: [PATCH 053/109] fix: add sub api doc & test suite (#5634) * fix: add sub api doc & test suite --- .../src/emqx_mgmt_api_subscriptions.erl | 4 ++-- .../test/emqx_mgmt_subscription_api_SUITE.erl | 21 ++++++++++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl index 5c2475e95..3afd52050 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_subscriptions.erl @@ -96,13 +96,13 @@ parameters() -> #{ name => topic, in => query, - description => <<"Topic">>, + description => <<"Topic, url encoding">>, schema => #{type => string} } #{ name => match_topic, in => query, - description => <<"Match topic string">>, + description => <<"Match topic string, url encoding">>, schema => #{type => string} } | page_params() ]. diff --git a/apps/emqx_management/test/emqx_mgmt_subscription_api_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_subscription_api_SUITE.erl index f2d8c6eb2..b344bcd11 100644 --- a/apps/emqx_management/test/emqx_mgmt_subscription_api_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_subscription_api_SUITE.erl @@ -24,8 +24,10 @@ -define(USERNAME, <<"api_username">>). %% notice: integer topic for sort response --define(TOPIC1, <<"0000">>). --define(TOPIC2, <<"0001">>). +-define(TOPIC1, <<"/t/0000">>). +-define(TOPIC2, <<"/t/0001">>). + +-define(TOPIC_SORT, #{?TOPIC1 => 1, ?TOPIC2 => 2}). all() -> emqx_ct:all(?MODULE). @@ -53,11 +55,24 @@ t_subscription_api(_) -> ?assertEqual(length(Subscriptions), 2), Sort = fun(#{<<"topic">> := T1}, #{<<"topic">> := T2}) -> - binary_to_integer(T1) =< binary_to_integer(T2) + maps:get(T1, ?TOPIC_SORT) =< maps:get(T2, ?TOPIC_SORT) end, [Subscriptions1, Subscriptions2] = lists:sort(Sort, Subscriptions), ?assertEqual(maps:get(<<"topic">>, Subscriptions1), ?TOPIC1), ?assertEqual(maps:get(<<"topic">>, Subscriptions2), ?TOPIC2), ?assertEqual(maps:get(<<"clientid">>, Subscriptions1), ?CLIENTID), ?assertEqual(maps:get(<<"clientid">>, Subscriptions2), ?CLIENTID), + + QsTopic = "topic=" ++ <<"%2Ft%2F0001">>, + Headers = emqx_mgmt_api_test_util:auth_header_(), + {ok, ResponseTopic1} = emqx_mgmt_api_test_util:request_api(get, Path, QsTopic, Headers), + DataTopic1 = emqx_json:decode(ResponseTopic1, [return_maps]), + Meta1 = maps:get(<<"meta">>, DataTopic1), + ?assertEqual(1, maps:get(<<"page">>, Meta1)), + ?assertEqual(emqx_mgmt:max_row_limit(), maps:get(<<"limit">>, Meta1)), + ?assertEqual(1, maps:get(<<"count">>, Meta1)), + Subscriptions_qs1 = maps:get(<<"data">>, DataTopic1), + ?assertEqual(length(Subscriptions_qs1), 1), + + emqtt:disconnect(Client). From 60b821536059135543f47ce70f121f41a0f445a8 Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Fri, 3 Sep 2021 11:40:11 +0800 Subject: [PATCH 054/109] feat(config): support wildcard paths for config handlers --- apps/emqx/src/emqx_config_handler.erl | 51 +++++++++++++++++++++------ apps/emqx/src/emqx_map_lib.erl | 8 ++--- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl index a9020a87a..f64ffabcb 100644 --- a/apps/emqx/src/emqx_config_handler.erl +++ b/apps/emqx/src/emqx_config_handler.erl @@ -39,6 +39,7 @@ code_change/3]). -define(MOD, {mod}). +-define(WKEY, '?'). -define(ATOM_CONF_PATH(PATH, EXP, EXP_ON_FAIL), try [safe_atom(Key) || Key <- PATH] of @@ -80,11 +81,11 @@ update_config(SchemaModule, ConfKeyPath, UpdateArgs) -> -spec add_handler(emqx_config:config_key_path(), handler_name()) -> ok. add_handler(ConfKeyPath, HandlerName) -> - gen_server:call(?MODULE, {add_child, ConfKeyPath, HandlerName}). + gen_server:call(?MODULE, {add_handler, ConfKeyPath, HandlerName}). -spec remove_handler(emqx_config:config_key_path()) -> ok. remove_handler(ConfKeyPath) -> - gen_server:call(?MODULE, {remove_child, ConfKeyPath}). + gen_server:call(?MODULE, {remove_handler, ConfKeyPath}). %%============================================================================ @@ -92,15 +93,18 @@ remove_handler(ConfKeyPath) -> init(_) -> {ok, #{handlers => #{?MOD => ?MODULE}}}. -handle_call({add_child, ConfKeyPath, HandlerName}, _From, - State = #{handlers := Handlers}) -> - {reply, ok, State#{handlers => - emqx_map_lib:deep_put(ConfKeyPath, Handlers, #{?MOD => HandlerName})}}; +handle_call({add_handler, ConfKeyPath, HandlerName}, _From, State = #{handlers := Handlers}) -> + case deep_put_handler(ConfKeyPath, Handlers, HandlerName) of + {ok, NewHandlers} -> + {reply, ok, State#{handlers => NewHandlers}}; + Error -> + {reply, Error, State} + end; -handle_call({remove_child, ConfKeyPath}, _From, +handle_call({remove_handler, ConfKeyPath}, _From, State = #{handlers := Handlers}) -> {reply, ok, State#{handlers => - emqx_map_lib:deep_remove(ConfKeyPath, Handlers)}}; + emqx_map_lib:deep_remove(ConfKeyPath ++ [?MOD], Handlers)}}; handle_call({change_config, SchemaModule, ConfKeyPath, UpdateArgs}, _From, #{handlers := Handlers} = State) -> @@ -134,6 +138,27 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. +deep_put_handler([], _Handlers, Mod) -> + {ok, #{?MOD => Mod}}; +deep_put_handler([?WKEY | KeyPath], Handlers, Mod) -> + deep_put_handler2(?WKEY, KeyPath, Handlers, Mod); +deep_put_handler([Key | KeyPath], Handlers, Mod) -> + case maps:find(?WKEY, Handlers) of + error -> + deep_put_handler2(Key, KeyPath, Handlers, Mod); + {ok, _SubHandlers} -> + {error, {cannot_override_a_wildcard_path, [?WKEY | KeyPath]}} + end. + +deep_put_handler2(Key, KeyPath, Handlers, Mod) -> + SubHandlers = maps:get(Key, Handlers, #{}), + case deep_put_handler(KeyPath, SubHandlers, Mod) of + {ok, SubHandlers1} -> + {ok, Handlers#{Key => SubHandlers1}}; + Error -> + Error + end. + process_update_request(ConfKeyPath, _Handlers, {remove, Opts}) -> OldRawConf = emqx_config:get_root_raw(ConfKeyPath), BinKeyPath = bin_path(ConfKeyPath), @@ -153,7 +178,7 @@ do_update_config([], Handlers, OldRawConf, UpdateReq) -> call_pre_config_update(Handlers, OldRawConf, UpdateReq); do_update_config([ConfKey | ConfKeyPath], Handlers, OldRawConf, UpdateReq) -> SubOldRawConf = get_sub_config(bin(ConfKey), OldRawConf), - SubHandlers = maps:get(ConfKey, Handlers, #{}), + SubHandlers = get_sub_handlers(ConfKey, Handlers), case do_update_config(ConfKeyPath, SubHandlers, SubOldRawConf, UpdateReq) of {ok, NewUpdateReq} -> call_pre_config_update(Handlers, OldRawConf, #{bin(ConfKey) => NewUpdateReq}); @@ -184,7 +209,7 @@ do_post_config_update([ConfKey | ConfKeyPath], Handlers, OldConf, NewConf, AppEn Result) -> SubOldConf = get_sub_config(ConfKey, OldConf), SubNewConf = get_sub_config(ConfKey, NewConf), - SubHandlers = maps:get(ConfKey, Handlers, #{}), + SubHandlers = get_sub_handlers(ConfKey, Handlers), case do_post_config_update(ConfKeyPath, SubHandlers, SubOldConf, SubNewConf, AppEnvs, UpdateArgs, Result) of {ok, Result1} -> @@ -193,6 +218,12 @@ do_post_config_update([ConfKey | ConfKeyPath], Handlers, OldConf, NewConf, AppEn Error -> Error end. +get_sub_handlers(ConfKey, Handlers) -> + case maps:find(ConfKey, Handlers) of + error -> maps:get(?WKEY, Handlers, #{}); + {ok, SubHandlers} -> SubHandlers + end. + get_sub_config(ConfKey, Conf) when is_map(Conf) -> maps:get(ConfKey, Conf, undefined); get_sub_config(_, _Conf) -> %% the Conf is a primitive diff --git a/apps/emqx/src/emqx_map_lib.erl b/apps/emqx/src/emqx_map_lib.erl index d5e851971..6aa6606c0 100644 --- a/apps/emqx/src/emqx_map_lib.erl +++ b/apps/emqx/src/emqx_map_lib.erl @@ -65,13 +65,11 @@ deep_find(_KeyPath, Data) -> {not_found, _KeyPath, Data}. -spec deep_put(config_key_path(), map(), term()) -> map(). -deep_put([], Map, Data) when is_map(Map) -> - Data; -deep_put([], _Map, Data) -> %% not map, replace it +deep_put([], _Map, Data) -> Data; deep_put([Key | KeyPath], Map, Data) -> - SubMap = deep_put(KeyPath, maps:get(Key, Map, #{}), Data), - Map#{Key => SubMap}. + SubMap = maps:get(Key, Map, #{}), + Map#{Key => deep_put(KeyPath, SubMap, Data)}. -spec deep_remove(config_key_path(), map()) -> map(). deep_remove([], Map) -> From 5f94b8e1ffc32e6ebe3d57e5418428f3027a72ee Mon Sep 17 00:00:00 2001 From: xiangfangyang-tech <62098177+xiangfangyang-tech@users.noreply.github.com> Date: Fri, 3 Sep 2021 15:05:30 +0800 Subject: [PATCH 055/109] chore(CI): add api test workflows for github actions (#5640) --- .github/workflows/run_api_tests.yaml | 102 +++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 .github/workflows/run_api_tests.yaml diff --git a/.github/workflows/run_api_tests.yaml b/.github/workflows/run_api_tests.yaml new file mode 100644 index 000000000..618b9383a --- /dev/null +++ b/.github/workflows/run_api_tests.yaml @@ -0,0 +1,102 @@ +name: API Test Suite + +on: + push: + tags: + - e* + - v* + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + container: "emqx/build-env:erl23.2.7.2-emqx-2-ubuntu20.04" + steps: + - uses: actions/checkout@v2 + - name: zip emqx-broker + if: endsWith(github.repository, 'emqx') + run: | + make emqx-zip + - name: zip emqx-broker + if: endsWith(github.repository, 'enterprise') + run: | + echo "https://ci%40emqx.io:${{ secrets.CI_GIT_TOKEN }}@github.com" > $HOME/.git-credentials + git config --global credential.helper store + make emqx-ee-zip + - uses: actions/upload-artifact@v2 + with: + name: emqx-broker + path: _packages/**/*.zip + api-test: + needs: build + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + script_name: + - api_metrics + - api_subscriptions + steps: + - uses: actions/checkout@v2 + with: + repository: emqx/emqx-fvt + path: . + - uses: actions/setup-java@v1 + with: + java-version: '8.0.282' # The JDK version to make available on the path. + java-package: jdk # (jre, jdk, or jdk+fx) - defaults to jdk + architecture: x64 # (x64 or x86) - defaults to x64 + - uses: actions/download-artifact@v2 + with: + name: emqx-broker + path: . + - name: start emqx-broker + env: + EMQX_LISTENERS__WSS__DEFAULT__BIND: "0.0.0.0:8085" + run: | + unzip ./emqx/*.zip + ./emqx/bin/emqx start + - name: install jmeter + timeout-minutes: 10 + env: + JMETER_VERSION: 5.3 + run: | + wget --no-verbose --no-check-certificate -O /tmp/apache-jmeter.tgz https://downloads.apache.org/jmeter/binaries/apache-jmeter-$JMETER_VERSION.tgz + cd /tmp && tar -xvf apache-jmeter.tgz + echo "jmeter.save.saveservice.output_format=xml" >> /tmp/apache-jmeter-$JMETER_VERSION/user.properties + echo "jmeter.save.saveservice.response_data.on_error=true" >> /tmp/apache-jmeter-$JMETER_VERSION/user.properties + wget --no-verbose -O /tmp/apache-jmeter-$JMETER_VERSION/lib/ext/mqtt-xmeter-2.0.2-jar-with-dependencies.jar https://raw.githubusercontent.com/xmeter-net/mqtt-jmeter/master/Download/v2.0.2/mqtt-xmeter-2.0.2-jar-with-dependencies.jar + ln -s /tmp/apache-jmeter-$JMETER_VERSION /opt/jmeter + - name: run ${{ matrix.script_name }} + run: | + /opt/jmeter/bin/jmeter.sh \ + -Jjmeter.save.saveservice.output_format=xml -n \ + -t .ci/api-test-suite/${{ matrix.script_name }}.jmx \ + -Demqx_ip="127.0.0.1" \ + -l jmeter_logs/${{ matrix.script_name }}.jtl \ + -j jmeter_logs/logs/${{ matrix.script_name }}.log + - name: check test logs + run: | + if cat jmeter_logs/${{ matrix.script_name }}.jtl | grep -e 'true' > /dev/null 2>&1; then + grep -A 5 -B 3 'true' jmeter_logs/${{ matrix.script_name }}.jtl > jmeter_logs/${{ matrix.script_name }}_err_api.txt + echo "check logs failed" + exit 1 + fi + - uses: actions/upload-artifact@v1 + if: failure() + with: + name: jmeter_logs + path: ./jmeter_logs + - uses: actions/upload-artifact@v1 + if: failure() + with: + name: jmeter_logs + path: emqx/log + delete-package: + runs-on: ubuntu-20.04 + needs: api-test + if: always() + steps: + - uses: geekyeggo/delete-artifact@v1 + with: + name: emqx-broker From cff15dfc44095a9e23b3dfd53009f5031814ddca Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Fri, 3 Sep 2021 14:26:32 +0800 Subject: [PATCH 056/109] chore(CI): fix env error for test wrokflows --- .ci/build_packages/tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/build_packages/tests.sh b/.ci/build_packages/tests.sh index 240d6214e..5d6422231 100755 --- a/.ci/build_packages/tests.sh +++ b/.ci/build_packages/tests.sh @@ -128,7 +128,7 @@ export EMQX_LOG__FILE_HANDLERS__DEFAULT__LEVEL=debug EOF ## for ARM, due to CI env issue, skip start of quic listener for the moment [[ $(arch) == *arm* || $(arch) == aarch64 ]] && tee -a "$emqx_env_vars" < Date: Fri, 3 Sep 2021 13:54:32 +0800 Subject: [PATCH 057/109] feat(authz): check for duplicate source types Signed-off-by: zhanghongtong --- apps/emqx_authz/src/emqx_authz.erl | 62 +++++++++++++++---- .../test/emqx_authz_api_sources_SUITE.erl | 25 +++----- 2 files changed, 61 insertions(+), 26 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index f950ed53c..7fcd80269 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -39,6 +39,7 @@ -export([post_config_update/4, pre_config_update/2]). -define(CONF_KEY_PATH, [authorization, sources]). +-define(SOURCE_TYPES, [file, http, mongo, mysql, pgsql, redis]). -spec(register_metrics() -> ok). register_metrics() -> @@ -47,7 +48,9 @@ register_metrics() -> init() -> ok = register_metrics(), emqx_config_handler:add_handler(?CONF_KEY_PATH, ?MODULE), - NSources = [init_source(Source) || Source <- emqx:get_config(?CONF_KEY_PATH, [])], + Sources = emqx:get_config(?CONF_KEY_PATH, []), + ok = check_dup_types(Sources), + NSources = [init_source(Source) || Source <- Sources], ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NSources]}, -1). lookup() -> @@ -83,12 +86,16 @@ update(Cmd, Sources, Opts) -> pre_config_update({move, Type, <<"top">>}, Conf) when is_list(Conf) -> {Index, _} = find_source_by_type(Type), {List1, List2} = lists:split(Index, Conf), - {ok, [lists:nth(Index, Conf)] ++ lists:droplast(List1) ++ List2}; + NConf = [lists:nth(Index, Conf)] ++ lists:droplast(List1) ++ List2, + ok = check_dup_types(NConf), + {ok, NConf}; pre_config_update({move, Type, <<"bottom">>}, Conf) when is_list(Conf) -> {Index, _} = find_source_by_type(Type), {List1, List2} = lists:split(Index, Conf), - {ok, lists:droplast(List1) ++ List2 ++ [lists:nth(Index, Conf)]}; + NConf = lists:droplast(List1) ++ List2 ++ [lists:nth(Index, Conf)], + ok = check_dup_types(NConf), + {ok, NConf}; pre_config_update({move, Type, #{<<"before">> := Before}}, Conf) when is_list(Conf) -> {Index1, _} = find_source_by_type(Type), @@ -97,9 +104,11 @@ pre_config_update({move, Type, #{<<"before">> := Before}}, Conf) when is_list(Co Conf2 = lists:nth(Index2, Conf), {List1, List2} = lists:split(Index2, Conf), - {ok, lists:delete(Conf1, lists:droplast(List1)) - ++ [Conf1] ++ [Conf2] - ++ lists:delete(Conf1, List2)}; + NConf = lists:delete(Conf1, lists:droplast(List1)) + ++ [Conf1] ++ [Conf2] + ++ lists:delete(Conf1, List2), + ok = check_dup_types(NConf), + {ok, NConf}; pre_config_update({move, Type, #{<<"after">> := After}}, Conf) when is_list(Conf) -> {Index1, _} = find_source_by_type(Type), @@ -107,21 +116,31 @@ pre_config_update({move, Type, #{<<"after">> := After}}, Conf) when is_list(Conf {Index2, _} = find_source_by_type(After), {List1, List2} = lists:split(Index2, Conf), - {ok, lists:delete(Conf1, List1) - ++ [Conf1] - ++ lists:delete(Conf1, List2)}; + NConf = lists:delete(Conf1, List1) + ++ [Conf1] + ++ lists:delete(Conf1, List2), + ok = check_dup_types(NConf), + {ok, NConf}; pre_config_update({head, Sources}, Conf) when is_list(Sources), is_list(Conf) -> + NConf = Sources ++ Conf, + ok = check_dup_types(NConf), {ok, Sources ++ Conf}; pre_config_update({tail, Sources}, Conf) when is_list(Sources), is_list(Conf) -> + NConf = Conf ++ Sources, + ok = check_dup_types(NConf), {ok, Conf ++ Sources}; pre_config_update({{replace_once, Type}, Source}, Conf) when is_map(Source), is_list(Conf) -> {Index, _} = find_source_by_type(Type), {List1, List2} = lists:split(Index, Conf), - {ok, lists:droplast(List1) ++ [Source] ++ List2}; + NConf = lists:droplast(List1) ++ [Source] ++ List2, + ok = check_dup_types(NConf), + {ok, NConf}; pre_config_update({{delete_once, Type}, _Source}, Conf) when is_list(Conf) -> {_, Source} = find_source_by_type(Type), - {ok, lists:delete(Source, Conf)}; + NConf = lists:delete(Source, Conf), + ok = check_dup_types(NConf), + {ok, NConf}; pre_config_update({_, Sources}, _Conf) when is_list(Sources)-> %% overwrite the entire config! {ok, Sources}. @@ -212,6 +231,27 @@ post_config_update(_, NewSources, _OldConf, _AppEnvs) -> %% Initialize source %%-------------------------------------------------------------------- +check_dup_types(Sources) -> + check_dup_types(Sources, ?SOURCE_TYPES). +check_dup_types(_Sources, []) -> ok; +check_dup_types(Sources, [T0 | Tail]) -> + case lists:foldl(fun (#{type := T1}, AccIn) -> + case T0 =:= T1 of + true -> AccIn + 1; + false -> AccIn + end; + (#{<<"type">> := T1}, AccIn) -> + case T0 =:= atom(T1) of + true -> AccIn + 1; + false -> AccIn + end + end, 0, Sources) > 1 of + true -> + ?LOG(error, "The type is duplicated in the Authorization source"), + {error, authz_source_dup}; + false -> check_dup_types(Sources, Tail) + end. + init_source(#{enable := true, type := file, path := Path diff --git a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl index 3c054aa7d..4dc21647a 100644 --- a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl @@ -178,16 +178,11 @@ t_api(_) -> {ok, 200, Result1} = request(get, uri(["authorization", "sources"]), []), ?assertEqual([], get_sources(Result1)), - lists:foreach(fun(_) -> - {ok, 204, _} = request(post, uri(["authorization", "sources"]), ?SOURCE1) - end, lists:seq(1, 20)), + {ok, 204, _} = request(put, uri(["authorization", "sources"]), [?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5, ?SOURCE6]), + {ok, 204, _} = request(post, uri(["authorization", "sources"]), ?SOURCE1), + {ok, 200, Result2} = request(get, uri(["authorization", "sources"]), []), - ?assertEqual(20, length(get_sources(Result2))), - - {ok, 204, _} = request(put, uri(["authorization", "sources"]), [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5, ?SOURCE6]), - - {ok, 200, Result3} = request(get, uri(["authorization", "sources"]), []), - Sources = get_sources(Result3), + Sources = get_sources(Result2), ?assertMatch([ #{<<"type">> := <<"http">>} , #{<<"type">> := <<"mongo">>} , #{<<"type">> := <<"mysql">>} @@ -198,8 +193,8 @@ t_api(_) -> ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]))), {ok, 204, _} = request(put, uri(["authorization", "sources", "http"]), ?SOURCE1#{<<"enable">> := false}), - {ok, 200, Result4} = request(get, uri(["authorization", "sources", "http"]), []), - ?assertMatch(#{<<"type">> := <<"http">>, <<"enable">> := false}, jsx:decode(Result4)), + {ok, 200, Result3} = request(get, uri(["authorization", "sources", "http"]), []), + ?assertMatch(#{<<"type">> := <<"http">>, <<"enable">> := false}, jsx:decode(Result3)), {ok, 204, _} = request(put, uri(["authorization", "sources", "mongo"]), ?SOURCE2#{<<"ssl">> := #{ @@ -209,7 +204,7 @@ t_api(_) -> <<"keyfile">> => <<"fake key file">>, <<"verify">> => false }}), - {ok, 200, Result5} = request(get, uri(["authorization", "sources", "mongo"]), []), + {ok, 200, Result4} = request(get, uri(["authorization", "sources", "mongo"]), []), ?assertMatch(#{<<"type">> := <<"mongo">>, <<"ssl">> := #{<<"enable">> := true, <<"cacertfile">> := <<"fake cacert file">>, @@ -217,7 +212,7 @@ t_api(_) -> <<"keyfile">> := <<"fake key file">>, <<"verify">> := false } - }, jsx:decode(Result5)), + }, jsx:decode(Result4)), ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "cacert-fake.pem"]))), ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "cert-fake.pem"]))), ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "key-fake.pem"]))), @@ -225,8 +220,8 @@ t_api(_) -> lists:foreach(fun(#{<<"type">> := Type}) -> {ok, 204, _} = request(delete, uri(["authorization", "sources", binary_to_list(Type)]), []) end, Sources), - {ok, 200, Result6} = request(get, uri(["authorization", "sources"]), []), - ?assertEqual([], get_sources(Result6)), + {ok, 200, Result5} = request(get, uri(["authorization", "sources"]), []), + ?assertEqual([], get_sources(Result5)), ok. t_move_source(_) -> From 07821b9574bb167a4f244a22d8e23c9adef6650e Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Fri, 3 Sep 2021 16:07:34 +0800 Subject: [PATCH 058/109] fix: cli error & routes api doc (#5639) --- apps/emqx_management/src/emqx_mgmt_api_routes.erl | 2 +- apps/emqx_management/src/emqx_mgmt_cli.erl | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_routes.erl b/apps/emqx_management/src/emqx_mgmt_api_routes.erl index 6c74105c0..19f42427e 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_routes.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_routes.erl @@ -64,7 +64,7 @@ route_api() -> name => topic, in => path, required => true, - description => <<"topic">>, + description => <<"Topic string, url encoding">>, schema => #{type => string} }], responses => #{ diff --git a/apps/emqx_management/src/emqx_mgmt_cli.erl b/apps/emqx_management/src/emqx_mgmt_cli.erl index c7fb8a7b7..abe64d886 100644 --- a/apps/emqx_management/src/emqx_mgmt_cli.erl +++ b/apps/emqx_management/src/emqx_mgmt_cli.erl @@ -73,8 +73,9 @@ status(_) -> %% @doc Query broker broker([]) -> - Funs = [sysdescr, version, uptime, datetime], - [emqx_ctl:print("~-10s: ~s~n", [Fun, emqx_sys:Fun()]) || Fun <- Funs]; + Funs = [sysdescr, version, datetime], + [emqx_ctl:print("~-10s: ~s~n", [Fun, emqx_sys:Fun()]) || Fun <- Funs], + emqx_ctl:print("~-10s: ~p~n", [uptime, emqx_sys:uptime()]); broker(["stats"]) -> [emqx_ctl:print("~-30s: ~w~n", [Stat, Val]) || {Stat, Val} <- lists:sort(emqx_stats:getstats())]; @@ -286,7 +287,7 @@ vm(["io"]) -> [emqx_ctl:print("io/~-21s: ~w~n", [Key, proplists:get_value(Key, IoInfo)]) || Key <- [max_fds, active_fds]]; vm(["ports"]) -> - [emqx_ctl:print("ports/~-16s: ~w~n", [Name, erlang:system_info(Key)]) || {Name, Key} <- [{count, port_count}, {limit, port_limit}]]; + [emqx_ctl:print("ports/~-18s: ~w~n", [Name, erlang:system_info(Key)]) || {Name, Key} <- [{count, port_count}, {limit, port_limit}]]; vm(_) -> emqx_ctl:usage([{"vm all", "Show info of Erlang VM"}, From 8f6931e5b06732a5eb24180817d09347598c6a9d Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Fri, 3 Sep 2021 16:07:44 +0800 Subject: [PATCH 059/109] feat: update dashboard ui beta10 (#5644) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 52b0ea209..fabb8a7df 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ BUILD = $(CURDIR)/build SCRIPTS = $(CURDIR)/scripts export PKG_VSN ?= $(shell $(CURDIR)/pkg-vsn.sh) export EMQX_DESC ?= EMQ X -export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.9 +export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.10 ifeq ($(OS),Windows_NT) export REBAR_COLOR=none endif From ec13463f4a5a33970d00f7a2e9e7c79082c91791 Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Tue, 31 Aug 2021 22:05:31 +0200 Subject: [PATCH 060/109] refactor(schema): prepare for hocon schema doc generation --- apps/emqx/rebar.config | 2 +- apps/emqx/src/emqx_schema.erl | 1023 +++++++++++++---- apps/emqx/src/emqx_zone_schema.erl | 34 + apps/emqx_authn/src/emqx_authn_schema.erl | 5 +- .../emqx_enhanced_authn_scram_mnesia.erl | 5 +- .../src/simple_authn/emqx_authn_http.erl | 9 +- .../src/simple_authn/emqx_authn_jwt.erl | 11 +- .../src/simple_authn/emqx_authn_mnesia.erl | 7 +- .../src/simple_authn/emqx_authn_mongodb.erl | 11 +- .../src/simple_authn/emqx_authn_mysql.erl | 5 +- .../src/simple_authn/emqx_authn_pgsql.erl | 4 +- .../src/simple_authn/emqx_authn_redis.erl | 10 +- apps/emqx_authz/src/emqx_authz_schema.erl | 7 +- .../src/emqx_auto_subscribe_schema.erl | 23 +- .../src/emqx_bridge_mqtt_schema.erl | 55 +- .../src/emqx_connector_mongo.erl | 7 +- .../src/emqx_dashboard_schema.erl | 20 +- .../src/emqx_data_bridge_schema.erl | 2 +- apps/emqx_exhook/src/emqx_exhook_schema.erl | 60 +- apps/emqx_gateway/src/emqx_gateway_schema.erl | 229 ++-- apps/emqx_machine/src/emqx_machine_schema.erl | 546 ++++++--- .../src/emqx_management_schema.erl | 5 +- apps/emqx_modules/src/emqx_modules_schema.erl | 37 +- .../src/emqx_prometheus_schema.erl | 13 +- .../src/emqx_retainer_schema.erl | 30 +- .../src/emqx_rule_engine_schema.erl | 11 +- .../emqx_rule_engine/src/emqx_rule_events.erl | 3 +- apps/emqx_statsd/src/emqx_statsd_schema.erl | 7 +- rebar.config | 2 +- 29 files changed, 1537 insertions(+), 646 deletions(-) create mode 100644 apps/emqx/src/emqx_zone_schema.erl diff --git a/apps/emqx/rebar.config b/apps/emqx/rebar.config index 271558f6d..bb3a588a9 100644 --- a/apps/emqx/rebar.config +++ b/apps/emqx/rebar.config @@ -15,7 +15,7 @@ , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.2"}}} , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.8"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.14.0"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.15.0"}}} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} , {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.14.1"}}} diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 812e52f9b..fe4439aaa 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -49,6 +49,10 @@ -typerefl_from_string({cipher/0, emqx_schema, to_erl_cipher_suite}). -typerefl_from_string({comma_separated_atoms/0, emqx_schema, to_comma_separated_atoms}). +-export([ validate_heap_size/1 + , parse_user_lookup_fun/1 + ]). + % workaround: prevent being recognized as unused functions -export([to_duration/1, to_duration_s/1, to_duration_ms/1, to_bytesize/1, to_wordsize/1, @@ -65,204 +69,539 @@ cipher/0, comma_separated_atoms/0]). --export([roots/0, fields/1]). --export([t/1, t/3, t/4, ref/1]). +-export([namespace/0, roots/0, fields/1]). -export([conf_get/2, conf_get/3, keys/2, filter/1]). -export([ssl/1]). +namespace() -> undefined. + roots() -> - ["zones", "mqtt", "flapping_detect", "force_shutdown", "force_gc", - "conn_congestion", "rate_limit", "quota", "listeners", "broker", "plugins", - "stats", "sysmon", "alarm", "authorization"]. + ["zones", + "mqtt", + "flapping_detect", + "force_shutdown", + "force_gc", + "conn_congestion", + "rate_limit", + "quota", + {"listeners", + sc(ref("listeners"), + #{ desc => "MQTT listeners identified by their protocol type and assigned names. " + "The listeners enabled by default are named with 'default'"}) + }, + "broker", + "plugins", + "stats", + "sysmon", + "alarm" + ]. fields("stats") -> - [ {"enable", t(boolean(), undefined, true)} + [ {"enable", + sc(boolean(), + #{ default => true + })} ]; fields("authorization") -> - [ {"no_match", t(union(allow, deny), undefined, allow)} - , {"deny_action", t(union(ignore, disconnect), undefined, ignore)} - , {"cache", ref("authorization_cache")} + [ {"no_match", + sc(union(allow, deny), + #{ default => allow + })} + , {"deny_action", + sc(union(ignore, disconnect), + #{ default => ignore + })} + , {"cache", + sc(ref("authorization_cache"), + #{ + }) + } ]; fields("authorization_cache") -> - [ {"enable", t(boolean(), undefined, true)} - , {"max_size", t(range(1, 1048576), undefined, 32)} - , {"ttl", t(duration(), undefined, "1m")} + [ {"enable", + sc(boolean(), + #{ default => true + }) + } + , {"max_size", + sc(range(1, 1048576), + #{ default => 32 + }) + } + , {"ttl", + sc(duration(), + #{ default => "1m" + }) + } ]; fields("mqtt") -> - [ {"idle_timeout", maybe_infinity(duration(), "15s")} - , {"max_packet_size", t(bytesize(), undefined, "1MB")} - , {"max_clientid_len", t(range(23, 65535), undefined, 65535)} - , {"max_topic_levels", t(range(1, 65535), undefined, 65535)} - , {"max_qos_allowed", t(range(0, 2), undefined, 2)} - , {"max_topic_alias", t(range(0, 65535), undefined, 65535)} - , {"retain_available", t(boolean(), undefined, true)} - , {"wildcard_subscription", t(boolean(), undefined, true)} - , {"shared_subscription", t(boolean(), undefined, true)} - , {"ignore_loop_deliver", t(boolean(), undefined, false)} - , {"strict_mode", t(boolean(), undefined, false)} - , {"response_information", t(string(), undefined, "")} - , {"server_keepalive", maybe_disabled(integer())} - , {"keepalive_backoff", t(float(), undefined, 0.75)} - , {"max_subscriptions", maybe_infinity(range(1, inf))} - , {"upgrade_qos", t(boolean(), undefined, false)} - , {"max_inflight", t(range(1, 65535), undefined, 32)} - , {"retry_interval", t(duration(), undefined, "30s")} - , {"max_awaiting_rel", maybe_infinity(integer(), 100)} - , {"await_rel_timeout", t(duration(), undefined, "300s")} - , {"session_expiry_interval", t(duration(), undefined, "2h")} - , {"max_mqueue_len", maybe_infinity(range(0, inf), 1000)} - , {"mqueue_priorities", maybe_disabled(map())} - , {"mqueue_default_priority", t(union(highest, lowest), undefined, lowest)} - , {"mqueue_store_qos0", t(boolean(), undefined, true)} - , {"use_username_as_clientid", t(boolean(), undefined, false)} - , {"peer_cert_as_username", maybe_disabled(union([cn, dn, crt, pem, md5]))} - , {"peer_cert_as_clientid", maybe_disabled(union([cn, dn, crt, pem, md5]))} + [ {"idle_timeout", + sc(hoconsc:union([infinity, duration()]), + #{ default => "15s" + })} + , {"max_packet_size", + sc(bytesize(), + #{ default => "1MB" + })} + , {"max_clientid_len", + sc(range(23, 65535), + #{ default => 65535 + })} + , {"max_topic_levels", + sc(range(1, 65535), + #{ default => 65535 + })} + , {"max_qos_allowed", + sc(range(0, 2), + #{ default => 2 + })} + , {"max_topic_alias", + sc(range(0, 65535), + #{ default => 65535 + })} + , {"retain_available", + sc(boolean(), + #{ default => true + })} + , {"wildcard_subscription", + sc(boolean(), + #{ default => true + })} + , {"shared_subscription", + sc(boolean(), + #{ default => true + })} + , {"ignore_loop_deliver", + sc(boolean(), + #{ default => false + })} + , {"strict_mode", + sc(boolean(), + #{default => false + }) + } + , {"response_information", + sc(string(), + #{default => "" + }) + } + , {"server_keepalive", + sc(hoconsc:union([integer(), disabled]), + #{ default => disabled + }) + } + , {"keepalive_backoff", + sc(float(), + #{default => 0.75 + }) + } + , {"max_subscriptions", + sc(hoconsc:union([range(1, inf), infinity]), + #{ default => infinity + }) + } + , {"upgrade_qos", + sc(boolean(), + #{ default => false + }) + } + , {"max_inflight", + sc(range(1, 65535), + #{ default => 32 + }) + } + , {"retry_interval", + sc(duration(), + #{default => "30s" + }) + } + , {"max_awaiting_rel", + sc(hoconsc:union([integer(), infinity]), + #{ default => 100 + }) + } + , {"await_rel_timeout", + sc(duration(), + #{ default => "300s" + }) + } + , {"session_expiry_interval", + sc(duration(), + #{ default => "2h" + }) + } + , {"max_mqueue_len", + sc(hoconsc:union([range(0, inf), infinity]), + #{ default => 1000 + }) + } + , {"mqueue_priorities", + sc(hoconsc:union([map(), disabled]), + #{ default => disabled + }) + } + , {"mqueue_default_priority", + sc(union(highest, lowest), + #{ default => lowest + }) + } + , {"mqueue_store_qos0", + sc(boolean(), + #{ default => true + }) + } + , {"use_username_as_clientid", + sc(boolean(), + #{ default => false + }) + } + , {"peer_cert_as_username", + sc(hoconsc:union([disabled, cn, dn, crt, pem, md5]), + #{ default => disabled + })} + , {"peer_cert_as_clientid", + sc(hoconsc:union([disabled, cn, dn, crt, pem, md5]), + #{ default => disabled + })} ]; fields("zones") -> - [ {"$name", ref("zone_settings")}]; + [ {"$name", + sc(ref("zone_settings"), + #{ + } + )}]; fields("zone_settings") -> Fields = ["mqtt", "stats", "authorization", "flapping_detect", "force_shutdown", "conn_congestion", "rate_limit", "quota", "force_gc"], - [{F, ref("strip_default:" ++ F)} || F <- Fields]; + [{F, ref(emqx_zone_schema, F)} || F <- Fields]; fields("rate_limit") -> - [ {"max_conn_rate", maybe_infinity(integer(), 1000)} - , {"conn_messages_in", maybe_infinity(comma_separated_list())} - , {"conn_bytes_in", maybe_infinity(comma_separated_list())} + [ {"max_conn_rate", + sc(hoconsc:union([infinity, integer()]), + #{ default => 1000 + }) + } + , {"conn_messages_in", + sc(hoconsc:union([infinity, comma_separated_list()]), + #{ default => infinity + }) + } + , {"conn_bytes_in", + sc(hoconsc:union([infinity, comma_separated_list()]), + #{ default => infinity + }) + } ]; fields("quota") -> - [ {"conn_messages_routing", maybe_infinity(comma_separated_list())} - , {"overall_messages_routing", maybe_infinity(comma_separated_list())} + [ {"conn_messages_routing", + sc(hoconsc:union([infinity, comma_separated_list()]), + #{ default => infinity + }) + } + , {"overall_messages_routing", + sc(hoconsc:union([infinity, comma_separated_list()]), + #{ default => infinity + }) + } ]; fields("flapping_detect") -> - [ {"enable", t(boolean(), undefined, false)} - , {"max_count", t(integer(), undefined, 15)} - , {"window_time", t(duration(), undefined, "1m")} - , {"ban_time", t(duration(), undefined, "5m")} + [ {"enable", + sc(boolean(), + #{ default => false + })} + , {"max_count", + sc(integer(), + #{ default => 15 + })} + , {"window_time", + sc(duration(), + #{ default => "1m" + })} + , {"ban_time", + sc(duration(), + #{ default => "5m" + })} ]; fields("force_shutdown") -> - [ {"enable", t(boolean(), undefined, true)} - , {"max_message_queue_len", t(range(0, inf), undefined, 1000)} - , {"max_heap_size", t(wordsize(), undefined, "32MB", undefined, - fun(Siz) -> - MaxSiz = case erlang:system_info(wordsize) of - 8 -> % arch_64 - (1 bsl 59) - 1; - 4 -> % arch_32 - (1 bsl 27) - 1 - end, - case Siz > MaxSiz of - true -> - error(io_lib:format("force_shutdown_policy: heap-size ~s is too large", [Siz])); - false -> - ok - end - end)} + [ {"enable", + sc(boolean(), + #{ default => true})} + , {"max_message_queue_len", + sc(range(0, inf), + #{ default => 1000 + })} + , {"max_heap_size", + sc(wordsize(), + #{ default => "32MB", + validator => fun ?MODULE:validate_heap_size/1 + })} ]; fields("conn_congestion") -> - [ {"enable_alarm", t(boolean(), undefined, false)} - , {"min_alarm_sustain_duration", t(duration(), undefined, "1m")} + [ {"enable_alarm", + sc(boolean(), + #{ default => false + })} + , {"min_alarm_sustain_duration", + sc(duration(), + #{ default => "1m" + })} ]; fields("force_gc") -> - [ {"enable", t(boolean(), undefined, true)} - , {"count", t(range(0, inf), undefined, 16000)} - , {"bytes", t(bytesize(), undefined, "16MB")} + [ {"enable", + sc(boolean(), + #{ default => true + })} + , {"count", + sc(range(0, inf), + #{ default => 16000 + })} + , {"bytes", + sc(bytesize(), + #{ default => "16MB" + })} ]; fields("listeners") -> - [ {"tcp", ref("t_tcp_listeners")} - , {"ssl", ref("t_ssl_listeners")} - , {"ws", ref("t_ws_listeners")} - , {"wss", ref("t_wss_listeners")} - , {"quic", ref("t_quic_listeners")} + [ {"tcp", + sc(ref("tcp_listeners"), + #{ desc => "TCP listeners" + }) + } + , {"ssl", + sc(ref("ssl_listeners"), + #{ desc => "SSL listeners" + }) + } + , {"ws", + sc(ref("ws_listeners"), + #{ desc => "HTTP websocket listeners" + }) + } + , {"wss", + sc(ref("wss_listeners"), + #{ desc => "HTTPS websocket listeners" + }) + } + , {"quic", + sc(ref("quic_listeners"), + #{ desc => "QUIC listeners" + }) + } ]; -fields("t_tcp_listeners") -> +fields("tcp_listeners") -> [ {"$name", ref("mqtt_tcp_listener")} ]; -fields("t_ssl_listeners") -> +fields("ssl_listeners") -> [ {"$name", ref("mqtt_ssl_listener")} ]; -fields("t_ws_listeners") -> +fields("ws_listeners") -> [ {"$name", ref("mqtt_ws_listener")} ]; -fields("t_wss_listeners") -> +fields("wss_listeners") -> [ {"$name", ref("mqtt_wss_listener")} ]; -fields("t_quic_listeners") -> +fields("quic_listeners") -> [ {"$name", ref("mqtt_quic_listener")} ]; fields("mqtt_tcp_listener") -> - [ {"tcp", ref("tcp_opts")} + [ {"tcp", + sc(ref("tcp_opts"), + #{ desc => "TCP listener options" + }) + } ] ++ mqtt_listener(); fields("mqtt_ssl_listener") -> - [ {"tcp", ref("tcp_opts")} - , {"ssl", ref("ssl_opts")} + [ {"tcp", + sc(ref("tcp_opts"), + #{}) + } + , {"ssl", + sc(ref("listener_ssl_opts"), + #{}) + } ] ++ mqtt_listener(); fields("mqtt_ws_listener") -> - [ {"tcp", ref("tcp_opts")} - , {"websocket", ref("ws_opts")} + [ {"tcp", + sc(ref("tcp_opts"), + #{}) + } + , {"websocket", + sc(ref("ws_opts"), + #{}) + } ] ++ mqtt_listener(); fields("mqtt_wss_listener") -> - [ {"tcp", ref("tcp_opts")} - , {"ssl", ref("ssl_opts")} - , {"websocket", ref("ws_opts")} + [ {"tcp", + sc(ref("tcp_opts"), + #{}) + } + , {"ssl", + sc(ref("listener_ssl_opts"), + #{}) + } + , {"websocket", + sc(ref("ws_opts"), + #{}) + } ] ++ mqtt_listener(); fields("mqtt_quic_listener") -> - [ {"enabled", t(boolean(), undefined, true)} - , {"certfile", t(string(), undefined, undefined)} - , {"keyfile", t(string(), undefined, undefined)} - , {"ciphers", t(comma_separated_list(), undefined, "TLS_AES_256_GCM_SHA384," - "TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256")} - , {"idle_timeout", t(duration(), undefined, "15s")} + [ {"enabled", + sc(boolean(), + #{ default => true + }) + } + , {"certfile", + sc(string(), + #{}) + } + , {"keyfile", + sc(string(), + #{}) + } + , {"ciphers", + sc(comma_separated_list(), + #{ default => "TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256," + "TLS_CHACHA20_POLY1305_SHA256" + })} + , {"idle_timeout", + sc(duration(), + #{ default => "15s" + }) + } ] ++ base_listener(); fields("ws_opts") -> - [ {"mqtt_path", t(string(), undefined, "/mqtt")} - , {"mqtt_piggyback", t(union(single, multiple), undefined, multiple)} - , {"compress", t(boolean(), undefined, false)} - , {"idle_timeout", t(duration(), undefined, "15s")} - , {"max_frame_size", maybe_infinity(integer())} - , {"fail_if_no_subprotocol", t(boolean(), undefined, true)} - , {"supported_subprotocols", t(comma_separated_list(), undefined, - "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5")} - , {"check_origin_enable", t(boolean(), undefined, false)} - , {"allow_origin_absence", t(boolean(), undefined, true)} - , {"check_origins", t(hoconsc:array(binary()), undefined, [])} - , {"proxy_address_header", t(string(), undefined, "x-forwarded-for")} - , {"proxy_port_header", t(string(), undefined, "x-forwarded-port")} - , {"deflate_opts", ref("deflate_opts")} + [ {"mqtt_path", + sc(string(), + #{ default => "/mqtt" + }) + } + , {"mqtt_piggyback", + sc(hoconsc:union([single, multiple]), + #{ default => multiple + }) + } + , {"compress", + sc(boolean(), + #{ default => false + }) + } + , {"idle_timeout", + sc(duration(), + #{ default => "15s" + }) + } + , {"max_frame_size", + sc(hoconsc:union([infinity, integer()]), + #{ default => infinity + }) + } + , {"fail_if_no_subprotocol", + sc(boolean(), + #{ default => true + }) + } + , {"supported_subprotocols", + sc(comma_separated_list(), + #{ default => "mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5" + }) + } + , {"check_origin_enable", + sc(boolean(), + #{ default => false + }) + } + , {"allow_origin_absence", + sc(boolean(), + #{ default => true + }) + } + , {"check_origins", + sc(hoconsc:array(binary()), + #{ default => [] + }) + } + , {"proxy_address_header", + sc(string(), + #{ default => "x-forwarded-for" + }) + } + , {"proxy_port_header", + sc(string(), + #{ default => "x-forwarded-port" + }) + } + , {"deflate_opts", + sc(ref("deflate_opts"), + #{}) + } ]; fields("tcp_opts") -> - [ {"active_n", t(integer(), undefined, 100)} - , {"backlog", t(integer(), undefined, 1024)} - , {"send_timeout", t(duration(), undefined, "15s")} - , {"send_timeout_close", t(boolean(), undefined, true)} - , {"recbuf", t(bytesize())} - , {"sndbuf", t(bytesize())} - , {"buffer", t(bytesize())} - , {"high_watermark", t(bytesize(), undefined, "1MB")} - , {"nodelay", t(boolean(), undefined, false)} - , {"reuseaddr", t(boolean(), undefined, true)} + [ {"active_n", + sc(integer(), + #{ default => 100 + }) + } + , {"backlog", + sc(integer(), + #{ default => 1024 + }) + } + , {"send_timeout", + sc(duration(), + #{ default => "15s" + }) + } + , {"send_timeout_close", + sc(boolean(), + #{ default => true + }) + } + , {"recbuf", + sc(bytesize(), + #{}) + } + , {"sndbuf", + sc(bytesize(), + #{}) + } + , {"buffer", + sc(bytesize(), + #{}) + } + , {"high_watermark", + sc(bytesize(), + #{ default => "1MB"}) + } + , {"nodelay", + sc(boolean(), + #{ default => false}) + } + , {"reuseaddr", + sc(boolean(), + #{ default => true + }) + } ]; -fields("ssl_opts") -> +fields("listener_ssl_opts") -> ssl(#{handshake_timeout => "15s" , depth => 10 , reuse_sessions => true @@ -271,82 +610,237 @@ fields("ssl_opts") -> }); fields("deflate_opts") -> - [ {"level", t(union([none, default, best_compression, best_speed]))} - , {"mem_level", t(range(1, 9), undefined, 8)} - , {"strategy", t(union([default, filtered, huffman_only, rle]))} - , {"server_context_takeover", t(union(takeover, no_takeover))} - , {"client_context_takeover", t(union(takeover, no_takeover))} - , {"server_max_window_bits", t(range(8, 15), undefined, 15)} - , {"client_max_window_bits", t(range(8, 15), undefined, 15)} + [ {"level", + sc(hoconsc:union([none, default, best_compression, best_speed]), + #{}) + } + , {"mem_level", + sc(range(1, 9), + #{ default => 8 + }) + } + , {"strategy", + sc(hoconsc:union([default, filtered, huffman_only, rle]), + #{}) + } + , {"server_context_takeover", + sc(hoconsc:union([takeover, no_takeover]), + #{}) + } + , {"client_context_takeover", + sc(hoconsc:union([takeover, no_takeover]), + #{}) + } + , {"server_max_window_bits", + sc(range(8, 15), + #{ default => 15 + }) + } + , {"client_max_window_bits", + sc(range(8, 15), + #{ default => 15 + }) + } ]; fields("plugins") -> - [ {"expand_plugins_dir", t(string())} + [ {"expand_plugins_dir", + sc(string(), + #{}) + } ]; fields("broker") -> - [ {"sys_msg_interval", maybe_disabled(duration(), "1m")} - , {"sys_heartbeat_interval", maybe_disabled(duration(), "30s")} - , {"enable_session_registry", t(boolean(), undefined, true)} - , {"session_locking_strategy", t(union([local, leader, quorum, all]), undefined, quorum)} - , {"shared_subscription_strategy", t(union(random, round_robin), undefined, round_robin)} - , {"shared_dispatch_ack_enabled", t(boolean(), undefined, false)} - , {"route_batch_clean", t(boolean(), undefined, true)} - , {"perf", ref("perf")} + [ {"sys_msg_interval", + sc(hoconsc:union([disabled, duration()]), + #{ default => "1m" + }) + } + , {"sys_heartbeat_interval", + sc(hoconsc:union([disabled, duration()]), + #{ default => "30s" + }) + } + , {"enable_session_registry", + sc(boolean(), + #{ default => true + }) + } + , {"session_locking_strategy", + sc(hoconsc:union([local, leader, quorum, all]), + #{ default => quorum + }) + } + , {"shared_subscription_strategy", + sc(hoconsc:union([random, round_robin]), + #{ default => round_robin + }) + } + , {"shared_dispatch_ack_enabled", + sc(boolean(), + #{ default => false + }) + } + , {"route_batch_clean", + sc(boolean(), + #{ default => true + })} + , {"perf", + sc(ref("broker_perf"), + #{ desc => "Broker performance tuning pamaters" + }) + } ]; -fields("perf") -> - [ {"route_lock_type", t(union([key, tab, global]), undefined, key)} - , {"trie_compaction", t(boolean(), undefined, true)} +fields("broker_perf") -> + [ {"route_lock_type", + sc(hoconsc:union([key, tab, global]), + #{ default => key + })} + , {"trie_compaction", + sc(boolean(), + #{ default => true + })} ]; fields("sysmon") -> - [ {"vm", ref("sysmon_vm")} - , {"os", ref("sysmon_os")} + [ {"vm", + sc(ref("sysmon_vm"), + #{}) + } + , {"os", + sc(ref("sysmon_os"), + #{}) + } ]; fields("sysmon_vm") -> - [ {"process_check_interval", t(duration(), undefined, "30s")} - , {"process_high_watermark", t(percent(), undefined, "80%")} - , {"process_low_watermark", t(percent(), undefined, "60%")} - , {"long_gc", maybe_disabled(duration())} - , {"long_schedule", maybe_disabled(duration(), "240ms")} - , {"large_heap", maybe_disabled(bytesize(), "32MB")} - , {"busy_dist_port", t(boolean(), undefined, true)} - , {"busy_port", t(boolean(), undefined, true)} + [ {"process_check_interval", + sc(duration(), + #{ default => "30s" + }) + } + , {"process_high_watermark", + sc(percent(), + #{ default => "80%" + }) + } + , {"process_low_watermark", + sc(percent(), + #{ default => "60%" + }) + } + , {"long_gc", + sc(hoconsc:union([disabled, duration()]), + #{}) + } + , {"long_schedule", + sc(hoconsc:union([disabled, duration()]), + #{ default => "240ms" + }) + } + , {"large_heap", + sc(hoconsc:union([disabled, bytesize()]), + #{default => "32MB"}) + } + , {"busy_dist_port", + sc(boolean(), + #{ default => true + }) + } + , {"busy_port", + sc(boolean(), + #{ default => true + })} ]; fields("sysmon_os") -> - [ {"cpu_check_interval", t(duration(), undefined, "60s")} - , {"cpu_high_watermark", t(percent(), undefined, "80%")} - , {"cpu_low_watermark", t(percent(), undefined, "60%")} - , {"mem_check_interval", maybe_disabled(duration(), "60s")} - , {"sysmem_high_watermark", t(percent(), undefined, "70%")} - , {"procmem_high_watermark", t(percent(), undefined, "5%")} + [ {"cpu_check_interval", + sc(duration(), + #{ default => "60s"}) + } + , {"cpu_high_watermark", + sc(percent(), + #{ default => "80%" + }) + } + , {"cpu_low_watermark", + sc(percent(), + #{ default => "60%" + }) + } + , {"mem_check_interval", + sc(hoconsc:union([disabled, duration()]), + #{ default => "60s" + })} + , {"sysmem_high_watermark", + sc(percent(), + #{ default => "70%" + }) + } + , {"procmem_high_watermark", + sc(percent(), + #{ default => "5%" + }) + } ]; fields("alarm") -> - [ {"actions", t(hoconsc:array(atom()), undefined, [log, publish])} - , {"size_limit", t(integer(), undefined, 1000)} - , {"validity_period", t(duration(), undefined, "24h")} - ]; - -fields("strip_default:" ++ Name) -> - strip_default(fields(Name)). + [ {"actions", + sc(hoconsc:array(atom()), + #{ default => [log, publish] + }) + } + , {"size_limit", + sc(integer(), + #{ default => 1000 + }) + } + , {"validity_period", + sc(duration(), + #{ default => "24h" + }) + } + ]. mqtt_listener() -> base_listener() ++ - [ {"access_rules", t(hoconsc:array(string()))} - , {"proxy_protocol", t(boolean(), undefined, false)} - , {"proxy_protocol_timeout", t(duration())} + [ {"access_rules", + sc(hoconsc:array(string()), + #{}) + } + , {"proxy_protocol", + sc(boolean(), + #{ default => false + }) + } + , {"proxy_protocol_timeout", + sc(duration(), + #{}) + } ]. base_listener() -> - [ {"bind", hoconsc:t(union(ip_port(), integer()), #{nullable => false})} - , {"acceptors", t(integer(), undefined, 16)} - , {"max_connections", maybe_infinity(integer(), infinity)} - , {"mountpoint", t(binary(), undefined, <<>>)} - , {"zone", t(atom(), undefined, default)} + [ {"bind", + sc(hoconsc:union([ip_port(), integer()]), + #{ nullable => false + })} + , {"acceptors", + sc(integer(), + #{ default => 16 + })} + , {"max_connections", + sc(hoconsc:union([infinity, integer()]), + #{ default => infinity + })} + , {"mountpoint", + sc(binary(), + #{ default => <<>> + })} + , {"zone", + sc(atom(), + #{ default => 'default' + })} ]. %% utils @@ -372,43 +866,101 @@ conf_get(Key, Conf, Default) -> filter(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined]. -%% generate a ssl field. -%% ssl(#{"verify" => verify_peer}) will return: -%% [ {"cacertfile", t(string(), undefined, undefined)} -%% , {"certfile", t(string(), undefined, undefined)} -%% , {"keyfile", t(string(), undefined, undefined)} -%% , {"verify", t(union(verify_peer, verify_none), undefined, verify_peer)} -%% , {"server_name_indication", undefined, undefined)} -%% ...] ssl(Defaults) -> D = fun (Field) -> maps:get(to_atom(Field), Defaults, undefined) end, - [ {"enable", t(boolean(), undefined, D("enable"))} - , {"cacertfile", t(string(), undefined, D("cacertfile"))} - , {"certfile", t(string(), undefined, D("certfile"))} - , {"keyfile", t(string(), undefined, D("keyfile"))} - , {"verify", t(union(verify_peer, verify_none), undefined, D("verify"))} - , {"fail_if_no_peer_cert", t(boolean(), undefined, D("fail_if_no_peer_cert"))} - , {"secure_renegotiate", t(boolean(), undefined, D("secure_renegotiate"))} - , {"reuse_sessions", t(boolean(), undefined, D("reuse_sessions"))} - , {"honor_cipher_order", t(boolean(), undefined, D("honor_cipher_order"))} - , {"handshake_timeout", t(duration(), undefined, D("handshake_timeout"))} - , {"depth", t(integer(), undefined, D("depth"))} - , {"password", hoconsc:t(string(), #{default => D("key_password"), - sensitive => true - })} - , {"dhfile", t(string(), undefined, D("dhfile"))} - , {"server_name_indication", t(union(disable, string()), undefined, - D("server_name_indication"))} - , {"versions", #{ type => list(atom()) - , default => maps:get(versions, Defaults, default_tls_vsns()) - , converter => fun (Vsns) -> [tls_vsn(V) || V <- Vsns] end - }} - , {"ciphers", t(hoconsc:array(string()), undefined, D("ciphers"))} - , {"user_lookup_fun", t(any(), undefined, {fun emqx_psk:lookup/3, <<>>})} + [ {"enable", + sc(boolean(), + #{ default => D("enable") + }) + } + , {"cacertfile", + sc(string(), + #{ default => D("cacertfile") + }) + } + , {"certfile", + sc(string(), + #{ default => D("certfile") + }) + } + , {"keyfile", + sc(string(), + #{ default => D("keyfile") + }) + } + , {"verify", + sc(hoconsc:union([verify_peer, verify_none]), + #{ default => D("verify") + }) + } + , {"fail_if_no_peer_cert", + sc(boolean(), + #{ default => D("fail_if_no_peer_cert") + }) + } + , {"secure_renegotiate", + sc(boolean(), + #{ default => D("secure_renegotiate") + }) + } + , {"reuse_sessions", + sc(boolean(), + #{ default => D("reuse_sessions") + }) + } + , {"honor_cipher_order", + sc(boolean(), + #{ default => D("honor_cipher_order") + }) + } + , {"handshake_timeout", + sc(duration(), + #{ default => D("handshake_timeout") + }) + } + , {"depth", + sc(integer(), + #{default => D("depth") + }) + } + , {"password", + sc(string(), + #{ default => D("key_password") + , sensitive => true + }) + } + , {"dhfile", + sc(string(), + #{ default => D("dhfile") + }) + } + , {"server_name_indication", + sc(hoconsc:union([disable, string()]), + #{ default => D("server_name_indication") + }) + } + , {"versions", + sc(typerefl:alias("string", list(atom())), + #{ default => maps:get(versions, Defaults, default_tls_vsns()) + , converter => fun (Vsns) -> [tls_vsn(iolist_to_binary(V)) || V <- Vsns] end + }) + } + , {"ciphers", + sc(hoconsc:array(string()), + #{ default => D("ciphers") + }) + } + , {"user_lookup_fun", + sc(typerefl:alias("string", any()), + #{ default => "emqx_psk:lookup" + , converter => fun ?MODULE:parse_user_lookup_fun/1 + }) + } ]. %% on erl23.2.7.2-emqx-2, sufficient_crypto_support('tlsv1.3') -> false default_tls_vsns() -> [<<"tlsv1.2">>, <<"tlsv1.1">>, <<"tlsv1">>]. + tls_vsn(<<"tlsv1.3">>) -> 'tlsv1.3'; tls_vsn(<<"tlsv1.2">>) -> 'tlsv1.2'; tls_vsn(<<"tlsv1.1">>) -> 'tlsv1.1'; @@ -451,40 +1003,11 @@ ceiling(X) -> %% types -t(Type) -> hoconsc:t(Type). +sc(Type, Meta) -> hoconsc:mk(Type, Meta). -t(Type, Mapping, Default) -> - hoconsc:t(Type, #{mapping => Mapping, default => Default}). +ref(Field) -> hoconsc:ref(?MODULE, Field). -t(Type, Mapping, Default, OverrideEnv) -> - hoconsc:t(Type, #{ mapping => Mapping - , default => Default - , override_env => OverrideEnv - }). - -t(Type, Mapping, Default, OverrideEnv, Validator) -> - hoconsc:t(Type, #{ mapping => Mapping - , default => Default - , override_env => OverrideEnv - , validator => Validator - }). - -ref(Field) -> hoconsc:t(hoconsc:ref(?MODULE, Field)). - -maybe_disabled(T) -> - maybe_sth(disabled, T, disabled). - -maybe_disabled(T, Default) -> - maybe_sth(disabled, T, Default). - -maybe_infinity(T) -> - maybe_sth(infinity, T, infinity). - -maybe_infinity(T, Default) -> - maybe_sth(infinity, T, Default). - -maybe_sth(What, Type, Default) -> - t(union([What, Type]), undefined, Default). +ref(Module, Field) -> hoconsc:ref(Module, Field). to_duration(Str) -> case hocon_postprocess:duration(Str) of @@ -545,22 +1068,26 @@ to_erl_cipher_suite(Str) -> Cipher -> Cipher end. -strip_default(Fields) -> - [do_strip_default(F) || F <- Fields]. - -do_strip_default({Name, #{type := {ref, Ref}}}) -> - {Name, nullable_no_def(ref("strip_default:" ++ Ref))}; -do_strip_default({Name, #{type := {ref, _Mod, Ref}}}) -> - {Name, nullable_no_def(ref("strip_default:" ++ Ref))}; -do_strip_default({Name, Type}) -> - {Name, nullable_no_def(Type)}. - -nullable_no_def(Type) when is_map(Type) -> - Type#{default => undefined, nullable => true}. - to_atom(Atom) when is_atom(Atom) -> Atom; to_atom(Str) when is_list(Str) -> list_to_atom(Str); to_atom(Bin) when is_binary(Bin) -> binary_to_atom(Bin, utf8). + +validate_heap_size(Siz) -> + MaxSiz = case erlang:system_info(wordsize) of + 8 -> % arch_64 + (1 bsl 59) - 1; + 4 -> % arch_32 + (1 bsl 27) - 1 + end, + case Siz > MaxSiz of + true -> error(io_lib:format("force_shutdown_policy: heap-size ~s is too large", [Siz])); + false -> ok + end. +parse_user_lookup_fun(StrConf) -> + [ModStr, FunStr] = string:tokens(StrConf, ":"), + Mod = list_to_atom(ModStr), + Fun = list_to_atom(FunStr), + {fun Mod:Fun/3, <<>>}. diff --git a/apps/emqx/src/emqx_zone_schema.erl b/apps/emqx/src/emqx_zone_schema.erl new file mode 100644 index 000000000..013ffb22f --- /dev/null +++ b/apps/emqx/src/emqx_zone_schema.erl @@ -0,0 +1,34 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_zone_schema). + +-export([namespace/0, roots/0, fields/1]). + +namespace() -> zone. + +roots() -> []. + +%% zone schemas are clones from the same name from root level +%% only not allowed to have default values. +fields(Name) -> + [{N, no_default(Sc)} || {N, Sc} <- emqx_schema:fields(Name)]. + +%% no default values for zone settings +no_default(Sc) -> + fun(default) -> undefined; + (Other) -> hocon_schema:field_schema(Sc, Other) + end. diff --git a/apps/emqx_authn/src/emqx_authn_schema.erl b/apps/emqx_authn/src/emqx_authn_schema.erl index de0de9fcc..bceedb6bb 100644 --- a/apps/emqx_authn/src/emqx_authn_schema.erl +++ b/apps/emqx_authn/src/emqx_authn_schema.erl @@ -21,7 +21,8 @@ -behaviour(hocon_schema). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1 ]). @@ -32,6 +33,8 @@ -export([ authenticators/1 ]). +namespace() -> authn. + roots() -> [ "authentication" ]. fields("authentication") -> diff --git a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl index 0ca281aa0..d7902d824 100644 --- a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl +++ b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl @@ -21,7 +21,8 @@ -behaviour(hocon_schema). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1 ]). @@ -74,6 +75,8 @@ mnesia(copy) -> %% Hocon Schema %%------------------------------------------------------------------------------ +namespace() -> "authn:scram:builtin_db". + roots() -> [config]. fields(config) -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl index c5cbc0f02..080b71ab1 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -22,7 +22,8 @@ -behaviour(hocon_schema). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1 , validations/0 ]). @@ -37,9 +38,11 @@ %% Hocon Schema %%------------------------------------------------------------------------------ +namespace() -> "authn:http". + roots() -> - [ {config, {union, [ hoconsc:t(get) - , hoconsc:t(post) + [ {config, {union, [ hoconsc:ref(?MODULE, get) + , hoconsc:ref(?MODULE, post) ]}} ]. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl index bc26bf70e..1ce10a2cc 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -20,7 +20,8 @@ -behaviour(hocon_schema). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1 ]). @@ -34,10 +35,12 @@ %% Hocon Schema %%------------------------------------------------------------------------------ +namespace() -> "authn:jwt". + roots() -> - [ {config, {union, [ hoconsc:t('hmac-based') - , hoconsc:t('public-key') - , hoconsc:t('jwks') + [ {config, {union, [ hoconsc:mk('hmac-based') + , hoconsc:mk('public-key') + , hoconsc:mk('jwks') ]}} ]. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl index c525efbf1..efe974145 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl @@ -21,7 +21,7 @@ -behaviour(hocon_schema). --export([ roots/0, fields/1 ]). +-export([ namespace/0, roots/0, fields/1 ]). -export([ create/1 , update/2 @@ -79,6 +79,8 @@ mnesia(copy) -> %% Hocon Schema %%------------------------------------------------------------------------------ +namespace() -> "authn:builtin_db". + roots() -> [config]. fields(config) -> @@ -102,7 +104,8 @@ user_id_type(type) -> user_id_type(); user_id_type(default) -> username; user_id_type(_) -> undefined. -password_hash_algorithm(type) -> {union, [hoconsc:ref(bcrypt), hoconsc:ref(other_algorithms)]}; +password_hash_algorithm(type) -> hoconsc:union([hoconsc:ref(?MODULE, bcrypt), + hoconsc:ref(?MODULE, other_algorithms)]); password_hash_algorithm(default) -> #{<<"name">> => sha256}; password_hash_algorithm(_) -> undefined. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl index 11411b70f..d272fe05b 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl @@ -22,7 +22,8 @@ -behaviour(hocon_schema). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1 ]). @@ -36,10 +37,12 @@ %% Hocon Schema %%------------------------------------------------------------------------------ +namespace() -> "authn:mongodb". + roots() -> - [ {config, {union, [ hoconsc:t(standalone) - , hoconsc:t('replica-set') - , hoconsc:t('sharded-cluster') + [ {config, {union, [ hoconsc:mk(standalone) + , hoconsc:mk('replica-set') + , hoconsc:mk('sharded-cluster') ]}} ]. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl index 3cafdb94e..c94798aa6 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl @@ -22,7 +22,8 @@ -behaviour(hocon_schema). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1 ]). @@ -36,6 +37,8 @@ %% Hocon Schema %%------------------------------------------------------------------------------ +namespace() -> "authn:mysql". + roots() -> [config]. fields(config) -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl index 5c21d3d6c..6875c5cb9 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl @@ -23,7 +23,7 @@ -behaviour(hocon_schema). --export([ roots/0, fields/1 ]). +-export([ namespace/0, roots/0, fields/1 ]). -export([ create/1 , update/2 @@ -35,6 +35,8 @@ %% Hocon Schema %%------------------------------------------------------------------------------ +namespace() -> "authn:postgres". + roots() -> [config]. fields(config) -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl index 1b090b007..6c5a81652 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl @@ -22,7 +22,8 @@ -behaviour(hocon_schema). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1 ]). @@ -36,10 +37,11 @@ %% Hocon Schema %%------------------------------------------------------------------------------ +namespace() -> "authn:redis". roots() -> - [ {config, {union, [ hoconsc:t(standalone) - , hoconsc:t(cluster) - , hoconsc:t(sentinel) + [ {config, {union, [ hoconsc:mk(standalone) + , hoconsc:mk(cluster) + , hoconsc:mk(sentinel) ]}} ]. diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index 251e40fe6..0645990a8 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -13,11 +13,14 @@ -type permission() :: allow | deny. -type url() :: emqx_http_lib:uri_map(). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1 ]). -roots() -> ["authorization"]. +namespace() -> authz. + +roots() -> []. fields("authorization") -> [ {sources, #{type => union_array( diff --git a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl index 92420e217..5b781455d 100644 --- a/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl +++ b/apps/emqx_auto_subscribe/src/emqx_auto_subscribe_schema.erl @@ -19,25 +19,30 @@ -include_lib("typerefl/include/types.hrl"). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1]). +namespace() -> "auto_subscribe". + roots() -> ["auto_subscribe"]. fields("auto_subscribe") -> - [ {topics, hoconsc:array(hoconsc:ref(?MODULE, "topic"))}]; + [ {topics, hoconsc:array(hoconsc:ref(?MODULE, "topic"))} + ]; fields("topic") -> - [ {topic, emqx_schema:t(binary())} - , {qos, t(hoconsc:union([0, 1, 2]), 0)} - , {rh, t(hoconsc:union([0, 1, 2]), 0)} - , {rap, t(hoconsc:union([0, 1]), 0)} - , {nl, t(hoconsc:union([0, 1]), 0)} + [ {topic, sc(binary(), #{})} + , {qos, sc(typerefl:union([0, 1, 2]), #{default => 0})} + , {rh, sc(typerefl:union([0, 1, 2]), #{default => 0})} + , {rap, sc(typerefl:union([0, 1]), #{default => 0})} + , {nl, sc(typerefl:union([0, 1]), #{default => 0})} ]. %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- -t(Type, Default) -> - hoconsc:t(Type, #{default => Default}). + +sc(Type, Meta) -> + hoconsc:mk(Type, Meta). diff --git a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl index 925bfa403..f370af277 100644 --- a/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl +++ b/apps/emqx_bridge_mqtt/src/emqx_bridge_mqtt_schema.erl @@ -20,55 +20,60 @@ -behaviour(hocon_schema). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1]). +namespace() -> "bridge_mqtt". + roots() -> [array("bridge_mqtt")]. -array(Name) -> {Name, hoconsc:array(hoconsc:ref(Name))}. +array(Name) -> {Name, hoconsc:array(hoconsc:ref(?MODULE, Name))}. fields("bridge_mqtt") -> - [ {name, emqx_schema:t(string(), undefined, true)} + [ {name, sc(string(), #{default => true})} , {start_type, fun start_type/1} , {forwards, fun forwards/1} - , {forward_mountpoint, emqx_schema:t(string())} - , {reconnect_interval, emqx_schema:t(emqx_schema:duration_ms(), undefined, "30s")} - , {batch_size, emqx_schema:t(integer(), undefined, 100)} - , {queue, emqx_schema:t(hoconsc:ref(?MODULE, "queue"))} - , {config, hoconsc:union([hoconsc:ref(?MODULE, "mqtt"), hoconsc:ref(?MODULE, "rpc")])} + , {forward_mountpoint, sc(string(), #{})} + , {reconnect_interval, sc(emqx_schema:duration_ms(), #{default => "30s"})} + , {batch_size, sc(integer(), #{default => 100})} + , {queue, sc(hoconsc:ref(?MODULE, "queue"), #{})} + , {config, sc(hoconsc:union([hoconsc:ref(?MODULE, "mqtt"), + hoconsc:ref(?MODULE, "rpc")]), + #{})} ]; fields("mqtt") -> [ {conn_type, fun conn_type/1} - , {address, emqx_schema:t(string(), undefined, "127.0.0.1:1883")} + , {address, sc(string(), #{default => "127.0.0.1:1883"})} , {proto_ver, fun proto_ver/1} - , {bridge_mode, emqx_schema:t(boolean(), undefined, true)} - , {clientid, emqx_schema:t(string())} - , {username, emqx_schema:t(string())} - , {password, emqx_schema:t(string())} - , {clean_start, emqx_schema:t(boolean(), undefined, true)} - , {keepalive, emqx_schema:t(integer(), undefined, 300)} - , {subscriptions, hoconsc:array("subscriptions")} - , {receive_mountpoint, emqx_schema:t(string())} - , {retry_interval, emqx_schema:t(emqx_schema:duration_ms(), undefined, "30s")} - , {max_inflight, emqx_schema:t(integer(), undefined, 32)} + , {bridge_mode, sc(boolean(), #{default => true})} + , {clientid, sc(string(), #{})} + , {username, sc(string(), #{})} + , {password, sc(string(), #{})} + , {clean_start, sc(boolean(), #{default => true})} + , {keepalive, sc(integer(), #{default => 300})} + , {subscriptions, sc(hoconsc:array(hoconsc:ref(?MODULE, "subscriptions")), #{})} + , {receive_mountpoint, sc(string(), #{})} + , {retry_interval, sc(emqx_schema:duration_ms(), #{default => "30s"})} + , {max_inflight, sc(integer(), #{default => 32})} ]; fields("rpc") -> [ {conn_type, fun conn_type/1} - , {node, emqx_schema:t(atom(), undefined, 'emqx@127.0.0.1')} + , {node, sc(atom(), #{default => 'emqx@127.0.0.1'})} ]; fields("subscriptions") -> [ {topic, #{type => binary(), nullable => false}} - , {qos, emqx_schema:t(integer(), undefined, 1)} + , {qos, sc(integer(), #{default => 1})} ]; fields("queue") -> [ {replayq_dir, hoconsc:union([boolean(), string()])} - , {replayq_seg_bytes, emqx_schema:t(emqx_schema:bytesize(), undefined, "100MB")} - , {replayq_offload_mode, emqx_schema:t(boolean(), undefined, false)} - , {replayq_max_total_bytes, emqx_schema:t(emqx_schema:bytesize(), undefined, "1024MB")} + , {replayq_seg_bytes, sc(emqx_schema:bytesize(), #{default => "100MB"})} + , {replayq_offload_mode, sc(boolean(), #{default => false})} + , {replayq_max_total_bytes, sc(emqx_schema:bytesize(), #{default => "1024MB"})} ]. conn_type(type) -> hoconsc:enum([mqtt, rpc]); @@ -85,3 +90,5 @@ start_type(_) -> undefined. forwards(type) -> hoconsc:array(binary()); forwards(default) -> []; forwards(_) -> undefined. + +sc(Type, Meta) -> hoconsc:mk(Type, Meta). diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index 88dfb2b72..c95679f32 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -82,10 +82,7 @@ mongo_fields() -> , {auth_source, #{type => binary(), nullable => true}} , {database, fun emqx_connector_schema_lib:database/1} - , {topology, #{type => hoconsc:ref(?MODULE, topology), - default => #{}}} - %% TODO: Does the ref type support nullable=ture ? - % nullable => true}} + , {topology, #{type => hoconsc:ref(?MODULE, topology)}} ] ++ emqx_connector_schema_lib:ssl_fields(). @@ -178,7 +175,7 @@ do_start(InstId, Opts0, Config = #{mongo_type := Type, ]; false -> [{ssl, false}] end, - Topology= maps:get(topology, Config, #{}), + Topology= maps:get(topology, Config, #{}), Opts = Opts0 ++ [{pool_size, PoolSize}, {options, init_topology_options(maps:to_list(Topology), [])}, diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl index 7dfbc923b..3ba3dc803 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl @@ -27,19 +27,19 @@ fields("emqx_dashboard") -> hoconsc:ref(?MODULE, "https")]))} , {default_username, fun default_username/1} , {default_password, fun default_password/1} - , {sample_interval, emqx_schema:t(emqx_schema:duration_s(), undefined, "10s")} - , {token_expired_time, emqx_schema:t(emqx_schema:duration(), undefined, "30m")} + , {sample_interval, sc(emqx_schema:duration_s(), #{default => "10s"})} + , {token_expired_time, sc(emqx_schema:duration(), #{default => "30m"})} ]; fields("http") -> [ {"protocol", hoconsc:enum([http, https])} - , {"port", emqx_schema:t(integer(), undefined, 18083)} - , {"num_acceptors", emqx_schema:t(integer(), undefined, 4)} - , {"max_connections", emqx_schema:t(integer(), undefined, 512)} - , {"backlog", emqx_schema:t(integer(), undefined, 1024)} - , {"send_timeout", emqx_schema:t(emqx_schema:duration(), undefined, "5s")} - , {"inet6", emqx_schema:t(boolean(), undefined, false)} - , {"ipv6_v6only", emqx_schema:t(boolean(), undefined, false)} + , {"port", hoconsc:mk(integer(), #{default => 18083})} + , {"num_acceptors", sc(integer(), #{default => 4})} + , {"max_connections", sc(integer(), #{default => 512})} + , {"backlog", sc(integer(), #{default => 1024})} + , {"send_timeout", sc(emqx_schema:duration(), #{default => "5s"})} + , {"inet6", sc(boolean(), #{default => false})} + , {"ipv6_v6only", sc(boolean(), #{dfeault => false})} ]; fields("https") -> @@ -54,3 +54,5 @@ default_password(type) -> string(); default_password(default) -> "public"; default_password(nullable) -> false; default_password(_) -> undefined. + +sc(Type, Meta) -> hoconsc:mk(Type, Meta). diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge_schema.erl b/apps/emqx_data_bridge/src/emqx_data_bridge_schema.erl index 69f53d6c1..e3c6d8ee9 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_schema.erl +++ b/apps/emqx_data_bridge/src/emqx_data_bridge_schema.erl @@ -22,5 +22,5 @@ fields(ldap) -> connector_fields(ldap). connector_fields(DB) -> Mod = list_to_existing_atom(io_lib:format("~s_~s",[emqx_connector, DB])), - [{name, hoconsc:t(typerefl:binary())}, + [{name, hoconsc:mk(typerefl:binary())}, {type, #{type => DB}}] ++ Mod:roots(). diff --git a/apps/emqx_exhook/src/emqx_exhook_schema.erl b/apps/emqx_exhook/src/emqx_exhook_schema.erl index 16fd93fa0..64d39eb52 100644 --- a/apps/emqx_exhook/src/emqx_exhook_schema.erl +++ b/apps/emqx_exhook/src/emqx_exhook_schema.erl @@ -32,43 +32,57 @@ -reflect_type([duration/0]). --export([roots/0, fields/1]). +-export([namespace/0, roots/0, fields/1]). --export([t/1, t/3, t/4, ref/1]). +namespace() -> exhook. roots() -> [exhook]. fields(exhook) -> - [ {request_failed_action, t(union([deny, ignore]), undefined, deny)} - , {request_timeout, t(duration(), undefined, "5s")} - , {auto_reconnect, t(union([false, duration()]), undefined, "60s")} - , {servers, t(hoconsc:array(ref(servers)), undefined, [])} + [ {request_failed_action, + sc(union([deny, ignore]), + #{default => deny})} + , {request_timeout, + sc(duration(), + #{default => "5s"})} + , {auto_reconnect, + sc(union([false, duration()]), + #{ default => "60s" + })} + , {servers, + sc(hoconsc:array(ref(servers)), + #{default => []})} ]; fields(servers) -> - [ {name, string()} - , {url, string()} - , {ssl, t(ref(ssl_conf_group))} + [ {name, + sc(string(), + #{})} + , {url, + sc(string(), + #{})} + , {ssl, + sc(ref(ssl_conf), + #{})} ]; -fields(ssl_conf_group) -> - [ {cacertfile, string()} - , {certfile, string()} - , {keyfile, string()} +fields(ssl_conf) -> + [ {cacertfile, + sc(string(), + #{}) + } + , {certfile, + sc(string(), + #{}) + } + , {keyfile, + sc(string(), + #{})} ]. %% types -t(Type) -> #{type => Type}. - -t(Type, Mapping, Default) -> - hoconsc:t(Type, #{mapping => Mapping, default => Default}). - -t(Type, Mapping, Default, OverrideEnv) -> - hoconsc:t(Type, #{ mapping => Mapping - , default => Default - , override_env => OverrideEnv - }). +sc(Type, Meta) -> Meta#{type => Type}. ref(Field) -> hoconsc:ref(?MODULE, Field). diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index 9ab26e480..da73b85ee 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -43,148 +43,149 @@ , ip_port/0 ]). --export([roots/0 , fields/1]). --export([t/1, t/3, t/4, ref/1]). +-export([namespace/0, roots/0 , fields/1]). + +namespace() -> gateway. roots() -> [gateway]. fields(gateway) -> - [{stomp, t(ref(stomp_structs))}, - {mqttsn, t(ref(mqttsn_structs))}, - {coap, t(ref(coap_structs))}, - {lwm2m, t(ref(lwm2m_structs))}, - {exproto, t(ref(exproto_structs))} + [{stomp, sc(ref(stomp_structs))}, + {mqttsn, sc(ref(mqttsn_structs))}, + {coap, sc(ref(coap_structs))}, + {lwm2m, sc(ref(lwm2m_structs))}, + {exproto, sc(ref(exproto_structs))} ]; fields(stomp_structs) -> - [ {frame, t(ref(stomp_frame))} - , {listeners, t(ref(tcp_listener_group))} + [ {frame, sc(ref(stomp_frame))} + , {listeners, sc(ref(tcp_listener_group))} ] ++ gateway_common_options(); fields(stomp_frame) -> - [ {max_headers, t(integer(), undefined, 10)} - , {max_headers_length, t(integer(), undefined, 1024)} - , {max_body_length, t(integer(), undefined, 8192)} + [ {max_headers, sc(integer(), undefined, 10)} + , {max_headers_length, sc(integer(), undefined, 1024)} + , {max_body_length, sc(integer(), undefined, 8192)} ]; fields(mqttsn_structs) -> - [ {gateway_id, t(integer())} - , {broadcast, t(boolean())} - , {enable_qos3, t(boolean())} + [ {gateway_id, sc(integer())} + , {broadcast, sc(boolean())} + , {enable_qos3, sc(boolean())} , {predefined, hoconsc:array(ref(mqttsn_predefined))} - , {listeners, t(ref(udp_listener_group))} + , {listeners, sc(ref(udp_listener_group))} ] ++ gateway_common_options(); fields(mqttsn_predefined) -> - [ {id, t(integer())} - , {topic, t(binary())} + [ {id, sc(integer())} + , {topic, sc(binary())} ]; fields(coap_structs) -> - [ {heartbeat, t(duration(), undefined, <<"30s">>)} - , {notify_type, t(union([non, con, qos]), undefined, qos)} - , {subscribe_qos, t(union([qos0, qos1, qos2, coap]), undefined, coap)} - , {publish_qos, t(union([qos0, qos1, qos2, coap]), undefined, coap)} - , {listeners, t(ref(udp_listener_group))} + [ {heartbeat, sc(duration(), undefined, <<"30s">>)} + , {notify_type, sc(union([non, con, qos]), undefined, qos)} + , {subscribe_qos, sc(union([qos0, qos1, qos2, coap]), undefined, coap)} + , {publish_qos, sc(union([qos0, qos1, qos2, coap]), undefined, coap)} + , {listeners, sc(ref(udp_listener_group))} ] ++ gateway_common_options(); fields(lwm2m_structs) -> - [ {xml_dir, t(binary())} - , {lifetime_min, t(duration())} - , {lifetime_max, t(duration())} - , {qmode_time_windonw, t(integer())} - , {auto_observe, t(boolean())} - , {mountpoint, t(string())} - , {update_msg_publish_condition, t(union([always, contains_object_list]))} - , {translators, t(ref(translators))} - , {listeners, t(ref(udp_listener_group))} + [ {xml_dir, sc(binary())} + , {lifetime_min, sc(duration())} + , {lifetime_max, sc(duration())} + , {qmode_time_windonw, sc(integer())} + , {auto_observe, sc(boolean())} + , {mountpoint, sc(string())} + , {update_msg_publish_condition, sc(union([always, contains_object_list]))} + , {translators, sc(ref(translators))} + , {listeners, sc(ref(udp_listener_group))} ] ++ gateway_common_options(); fields(exproto_structs) -> - [ {server, t(ref(exproto_grpc_server))} - , {handler, t(ref(exproto_grpc_handler))} - , {listeners, t(ref(udp_tcp_listener_group))} + [ {server, sc(ref(exproto_grpc_server))} + , {handler, sc(ref(exproto_grpc_handler))} + , {listeners, sc(ref(udp_tcp_listener_group))} ] ++ gateway_common_options(); fields(exproto_grpc_server) -> - [ {bind, t(union(ip_port(), integer()))} + [ {bind, sc(union(ip_port(), integer()))} %% TODO: ssl options ]; fields(exproto_grpc_handler) -> - [ {address, t(binary())} + [ {address, sc(binary())} %% TODO: ssl ]; fields(clientinfo_override) -> - [ {username, t(binary())} - , {password, t(binary())} - , {clientid, t(binary())} + [ {username, sc(binary())} + , {password, sc(binary())} + , {clientid, sc(binary())} ]; fields(translators) -> - [ {command, t(ref(translator))} - , {response, t(ref(translator))} - , {notify, t(ref(translator))} - , {register, t(ref(translator))} - , {update, t(ref(translator))} + [ {command, sc(ref(translator))} + , {response, sc(ref(translator))} + , {notify, sc(ref(translator))} + , {register, sc(ref(translator))} + , {update, sc(ref(translator))} ]; fields(translator) -> - [ {topic, t(binary())} - , {qos, t(range(0, 2))} + [ {topic, sc(binary())} + , {qos, sc(range(0, 2))} ]; fields(udp_listener_group) -> - [ {udp, t(ref(udp_listener))} - , {dtls, t(ref(dtls_listener))} + [ {udp, sc(ref(udp_listener))} + , {dtls, sc(ref(dtls_listener))} ]; fields(tcp_listener_group) -> - [ {tcp, t(ref(tcp_listener))} - , {ssl, t(ref(ssl_listener))} + [ {tcp, sc(ref(tcp_listener))} + , {ssl, sc(ref(ssl_listener))} ]; fields(udp_tcp_listener_group) -> - [ {udp, t(ref(udp_listener))} - , {dtls, t(ref(dtls_listener))} - , {tcp, t(ref(tcp_listener))} - , {ssl, t(ref(ssl_listener))} + [ {udp, sc(ref(udp_listener))} + , {dtls, sc(ref(dtls_listener))} + , {tcp, sc(ref(tcp_listener))} + , {ssl, sc(ref(ssl_listener))} ]; fields(tcp_listener) -> - [ {"$name", t(ref(tcp_listener_settings))}]; + [ {"$name", sc(ref(tcp_listener_settings))}]; fields(ssl_listener) -> - [ {"$name", t(ref(ssl_listener_settings))}]; + [ {"$name", sc(ref(ssl_listener_settings))}]; fields(udp_listener) -> - [ {"$name", t(ref(udp_listener_settings))}]; + [ {"$name", sc(ref(udp_listener_settings))}]; fields(dtls_listener) -> - [ {"$name", t(ref(dtls_listener_settings))}]; + [ {"$name", sc(ref(dtls_listener_settings))}]; fields(listener_settings) -> - [ {enable, t(boolean(), undefined, true)} - , {bind, t(union(ip_port(), integer()))} - , {acceptors, t(integer(), undefined, 8)} - , {max_connections, t(integer(), undefined, 1024)} - , {max_conn_rate, t(integer())} - , {active_n, t(integer(), undefined, 100)} - %, {rate_limit, t(comma_separated_list())} - , {access, t(ref(access))} - , {proxy_protocol, t(boolean())} - , {proxy_protocol_timeout, t(duration())} - , {backlog, t(integer(), undefined, 1024)} - , {send_timeout, t(duration(), undefined, <<"15s">>)} - , {send_timeout_close, t(boolean(), undefined, true)} - , {recbuf, t(bytesize())} - , {sndbuf, t(bytesize())} - , {buffer, t(bytesize())} - , {high_watermark, t(bytesize(), undefined, <<"1MB">>)} - , {tune_buffer, t(boolean())} - , {nodelay, t(boolean())} - , {reuseaddr, t(boolean())} + [ {enable, sc(boolean(), undefined, true)} + , {bind, sc(union(ip_port(), integer()))} + , {acceptors, sc(integer(), undefined, 8)} + , {max_connections, sc(integer(), undefined, 1024)} + , {max_conn_rate, sc(integer())} + , {active_n, sc(integer(), undefined, 100)} + %, {rate_limit, sc(comma_separated_list())} + , {access, sc(ref(access))} + , {proxy_protocol, sc(boolean())} + , {proxy_protocol_timeout, sc(duration())} + , {backlog, sc(integer(), undefined, 1024)} + , {send_timeout, sc(duration(), undefined, <<"15s">>)} + , {send_timeout_close, sc(boolean(), undefined, true)} + , {recbuf, sc(bytesize())} + , {sndbuf, sc(bytesize())} + , {buffer, sc(bytesize())} + , {high_watermark, sc(bytesize(), undefined, <<"1MB">>)} + , {tune_buffer, sc(boolean())} + , {nodelay, sc(boolean())} + , {reuseaddr, sc(boolean())} ]; fields(tcp_listener_settings) -> @@ -242,12 +243,12 @@ authentication() -> ]). gateway_common_options() -> - [ {enable, t(boolean(), undefined, true)} - , {enable_stats, t(boolean(), undefined, true)} - , {idle_timeout, t(duration(), undefined, <<"30s">>)} - , {mountpoint, t(binary())} - , {clientinfo_override, t(ref(clientinfo_override))} - , {authentication, t(authentication(), undefined, undefined)} + [ {enable, sc(boolean(), undefined, true)} + , {enable_stats, sc(boolean(), undefined, true)} + , {idle_timeout, sc(duration(), undefined, <<"30s">>)} + , {mountpoint, sc(binary())} + , {clientinfo_override, sc(ref(clientinfo_override))} + , {authentication, sc(authentication(), undefined, undefined)} ]. %%-------------------------------------------------------------------- @@ -255,16 +256,10 @@ gateway_common_options() -> %% types -t(Type) -> #{type => Type}. +sc(Type) -> #{type => Type}. -t(Type, Mapping, Default) -> - hoconsc:t(Type, #{mapping => Mapping, default => Default}). - -t(Type, Mapping, Default, OverrideEnv) -> - hoconsc:t(Type, #{ mapping => Mapping - , default => Default - , override_env => OverrideEnv - }). +sc(Type, Mapping, Default) -> + hoconsc:mk(Type, #{mapping => Mapping, default => Default}). ref(Field) -> hoconsc:ref(?MODULE, Field). @@ -273,10 +268,10 @@ ref(Field) -> %% generate a ssl field. %% ssl("emqx", #{"verify" => verify_peer}) will return -%% [ {"cacertfile", t(string(), "emqx.cacertfile", undefined)} -%% , {"certfile", t(string(), "emqx.certfile", undefined)} -%% , {"keyfile", t(string(), "emqx.keyfile", undefined)} -%% , {"verify", t(union(verify_peer, verify_none), "emqx.verify", verify_peer)} +%% [ {"cacertfile", sc(string(), "emqx.cacertfile", undefined)} +%% , {"certfile", sc(string(), "emqx.certfile", undefined)} +%% , {"keyfile", sc(string(), "emqx.keyfile", undefined)} +%% , {"verify", sc(union(verify_peer, verify_none), "emqx.verify", verify_peer)} %% , {"server_name_indication", "emqx.server_name_indication", undefined)} %% ... ssl(Mapping, Defaults) -> @@ -286,24 +281,24 @@ ssl(Mapping, Defaults) -> _ -> Mapping ++ "." ++ Field end end, D = fun (Field) -> maps:get(list_to_atom(Field), Defaults, undefined) end, - [ {"enable", t(boolean(), M("enable"), D("enable"))} - , {"cacertfile", t(binary(), M("cacertfile"), D("cacertfile"))} - , {"certfile", t(binary(), M("certfile"), D("certfile"))} - , {"keyfile", t(binary(), M("keyfile"), D("keyfile"))} - , {"verify", t(union(verify_peer, verify_none), M("verify"), D("verify"))} - , {"fail_if_no_peer_cert", t(boolean(), M("fail_if_no_peer_cert"), D("fail_if_no_peer_cert"))} - , {"secure_renegotiate", t(boolean(), M("secure_renegotiate"), D("secure_renegotiate"))} - , {"reuse_sessions", t(boolean(), M("reuse_sessions"), D("reuse_sessions"))} - , {"honor_cipher_order", t(boolean(), M("honor_cipher_order"), D("honor_cipher_order"))} - , {"handshake_timeout", t(duration(), M("handshake_timeout"), D("handshake_timeout"))} - , {"depth", t(integer(), M("depth"), D("depth"))} - , {"password", hoconsc:t(binary(), #{mapping => M("key_password"), - default => D("key_password"), - sensitive => true + [ {"enable", sc(boolean(), M("enable"), D("enable"))} + , {"cacertfile", sc(binary(), M("cacertfile"), D("cacertfile"))} + , {"certfile", sc(binary(), M("certfile"), D("certfile"))} + , {"keyfile", sc(binary(), M("keyfile"), D("keyfile"))} + , {"verify", sc(union(verify_peer, verify_none), M("verify"), D("verify"))} + , {"fail_if_no_peer_cert", sc(boolean(), M("fail_if_no_peer_cert"), D("fail_if_no_peer_cert"))} + , {"secure_renegotiate", sc(boolean(), M("secure_renegotiate"), D("secure_renegotiate"))} + , {"reuse_sessions", sc(boolean(), M("reuse_sessions"), D("reuse_sessions"))} + , {"honor_cipher_order", sc(boolean(), M("honor_cipher_order"), D("honor_cipher_order"))} + , {"handshake_timeout", sc(duration(), M("handshake_timeout"), D("handshake_timeout"))} + , {"depth", sc(integer(), M("depth"), D("depth"))} + , {"password", hoconsc:mk(binary(), #{ mapping => M("key_password") + , default => D("key_password") + , sensitive => true })} - , {"dhfile", t(binary(), M("dhfile"), D("dhfile"))} - , {"server_name_indication", t(union(disable, binary()), M("server_name_indication"), + , {"dhfile", sc(binary(), M("dhfile"), D("dhfile"))} + , {"server_name_indication", sc(union(disable, binary()), M("server_name_indication"), D("server_name_indication"))} - , {"tls_versions", t(comma_separated_list(), M("tls_versions"), D("tls_versions"))} - , {"ciphers", t(comma_separated_list(), M("ciphers"), D("ciphers"))} - , {"psk_ciphers", t(comma_separated_list(), M("ciphers"), D("ciphers"))}]. + , {"tls_versions", sc(comma_separated_list(), M("tls_versions"), D("tls_versions"))} + , {"ciphers", sc(comma_separated_list(), M("ciphers"), D("ciphers"))} + , {"psk_ciphers", sc(comma_separated_list(), M("ciphers"), D("ciphers"))}]. diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index c25ab8139..a3e7e9388 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -23,6 +23,7 @@ -dialyzer(no_fail_call). -include_lib("typerefl/include/types.hrl"). +-include_lib("hocon/include/hoconsc.hrl"). -type log_level() :: debug | info | notice | warning | error | critical | alert | emergency | all. -type file() :: string(). @@ -34,8 +35,7 @@ file/0, cipher/0]). --export([roots/0, fields/1, translations/0, translation/1]). --export([t/1, t/3, t/4, ref/1]). +-export([namespace/0, roots/0, fields/1, translations/0, translation/1]). -export([conf_get/2, conf_get/3, keys/2, filter/1]). %% Static apps which merge their configs into the merged emqx.conf @@ -58,167 +58,406 @@ , emqx_exhook_schema ]). -%% TODO: add a test case to ensure the list elements are unique +namespace() -> undefined. + roots() -> - ["cluster", "node", "rpc", "log"] - ++ lists:flatmap(fun(Mod) -> Mod:roots() end, ?MERGED_CONFIGS). + ["cluster", "node", "rpc", "log"] ++ lists:flatmap(fun roots/1, ?MERGED_CONFIGS). fields("cluster") -> - [ {"name", t(atom(), "ekka.cluster_name", emqxcl)} - , {"discovery_strategy", t(union([manual, static, mcast, dns, etcd, k8s]), - undefined, manual)} - , {"autoclean", t(emqx_schema:duration(), "ekka.cluster_autoclean", "5m")} - , {"autoheal", t(boolean(), "ekka.cluster_autoheal", true)} - , {"static", ref("static")} - , {"mcast", ref("mcast")} - , {"proto_dist", t(union([inet_tcp, inet6_tcp, inet_tls]), "ekka.proto_dist", inet_tcp)} - , {"dns", ref("dns")} - , {"etcd", ref("etcd")} - , {"k8s", ref("k8s")} - , {"db_backend", t(union([mnesia, rlog]), "ekka.db_backend", mnesia)} - , {"rlog", ref("rlog")} + [ {"name", + sc(atom(), + #{ mapping => "ekka.cluster_name" + , default => emqxcl + })} + , {"discovery_strategy", + sc(union([manual, static, mcast, dns, etcd, k8s]), + #{ default => manual + })} + , {"autoclean", + sc(emqx_schema:duration(), + #{ mapping => "ekka.cluster_autoclean" + , default => "5m" + })} + , {"autoheal", + sc(boolean(), + #{ mapping => "ekka.cluster_autoheal" + , default => true + })} + , {"static", + sc(ref(cluster_static), + #{})} + , {"mcast", + sc(ref(cluster_mcast), + #{})} + , {"proto_dist", + sc(union([inet_tcp, inet6_tcp, inet_tls]), + #{ mapping => "ekka.proto_dist" + , default => inet_tcp + })} + , {"dns", + sc(ref(cluster_dns), + #{})} + , {"etcd", + sc(ref(cluster_etcd), + #{})} + , {"k8s", + sc(ref(cluster_k8s), + #{})} + , {"db_backend", + sc(union([mnesia, rlog]), + #{ mapping => "ekka.db_backend" + , default => mnesia + })} + , {"rlog", + sc(ref("rlog"), + #{})} ]; -fields("static") -> - [ {"seeds", t(hoconsc:array(string()), undefined, [])}]; - -fields("mcast") -> - [ {"addr", t(string(), undefined, "239.192.0.1")} - , {"ports", t(hoconsc:array(integer()), undefined, [4369, 4370])} - , {"iface", t(string(), undefined, "0.0.0.0")} - , {"ttl", t(range(0, 255), undefined, 255)} - , {"loop", t(boolean(), undefined, true)} - , {"sndbuf", t(emqx_schema:bytesize(), undefined, "16KB")} - , {"recbuf", t(emqx_schema:bytesize(), undefined, "16KB")} - , {"buffer", t(emqx_schema:bytesize(), undefined, "32KB")} +fields(cluster_static) -> + [ {"seeds", + sc(hoconsc:array(string()), + #{ default => [] + })} ]; -fields("dns") -> - [ {"name", t(string(), undefined, "localhost")} - , {"app", t(string(), undefined, "emqx")}]; - -fields("etcd") -> - [ {"server", t(emqx_schema:comma_separated_list())} - , {"prefix", t(string(), undefined, "emqxcl")} - , {"node_ttl", t(emqx_schema:duration(), undefined, "1m")} - , {"ssl", ref("etcd_ssl")} +fields(cluster_mcast) -> + [ {"addr", + sc(string(), + #{ default => "239.192.0.1" + })} + , {"ports", + sc(hoconsc:array(integer()), + #{ default => [4369, 4370] + })} + , {"iface", + sc(string(), + #{ default => "0.0.0.0" + })} + , {"ttl", + sc(range(0, 255), + #{ default => 255 + })} + , {"loop", + sc(boolean(), + #{ default => true + })} + , {"sndbuf", + sc(emqx_schema:bytesize(), + #{ default => "16KB" + })} + , {"recbuf", + sc(emqx_schema:bytesize(), + #{ default => "16KB" + })} + , {"buffer", + sc(emqx_schema:bytesize(), + #{ default =>"32KB" + })} ]; -fields("etcd_ssl") -> +fields(cluster_dns) -> + [ {"name", + sc(string(), + #{ default => "localhost" + })} + , {"app", + sc(string(), + #{ default => "emqx" + })} + ]; + +fields(cluster_etcd) -> + [ {"server", + sc(emqx_schema:comma_separated_list(), + #{})} + , {"prefix", + sc(string(), + #{ default => "emqxcl" + })} + , {"node_ttl", + sc(emqx_schema:duration(), + #{ default => "1m" + })} + , {"ssl", + sc(ref(etcd_ssl_opts), + #{})} + ]; + +fields(etcd_ssl_opts) -> emqx_schema:ssl(#{}); -fields("k8s") -> - [ {"apiserver", t(string())} - , {"service_name", t(string(), undefined, "emqx")} - , {"address_type", t(union([ip, dns, hostname]))} - , {"app_name", t(string(), undefined, "emqx")} - , {"namespace", t(string(), undefined, "default")} - , {"suffix", t(string(), undefined, "pod.local")} +fields(cluster_k8s) -> + [ {"apiserver", + sc(string(), + #{})} + , {"service_name", + sc(string(), + #{ default => "emqx" + })} + , {"address_type", + sc(union([ip, dns, hostname]), + #{})} + , {"app_name", + sc(string(), + #{ default => "emqx" + })} + , {"namespace", + sc(string(), + #{ default => "default" + })} + , {"suffix", + sc(string(), + #{default => "pod.local" + })} ]; fields("rlog") -> - [ {"role", t(union([core, replicant]), "ekka.node_role", core)} - , {"core_nodes", t(emqx_schema:comma_separated_atoms(), "ekka.core_nodes", [])} + [ {"role", + sc(union([core, replicant]), + #{ mapping => "ekka.node_role" + , default => core + })} + , {"core_nodes", + sc(emqx_schema:comma_separated_atoms(), + #{ mapping => "ekka.core_nodes" + , default => [] + })} ]; fields("node") -> - [ {"name", hoconsc:t(string(), #{default => "emqx@127.0.0.1", - override_env => "EMQX_NODE_NAME" - })} - , {"cookie", hoconsc:t(string(), #{mapping => "vm_args.-setcookie", - default => "emqxsecretcookie", - sensitive => true, - override_env => "EMQX_NODE_COOKIE" - })} - , {"data_dir", hoconsc:t(string(), #{nullable => false})} - , {"config_files", t(list(string()), "emqx.config_files", undefined)} - , {"global_gc_interval", t(emqx_schema:duration(), undefined, "15m")} - , {"crash_dump_dir", t(file(), "vm_args.-env ERL_CRASH_DUMP", undefined)} - , {"dist_net_ticktime", t(emqx_schema:duration(), "vm_args.-kernel net_ticktime", "2m")} - , {"dist_listen_min", t(range(1024, 65535), "kernel.inet_dist_listen_min", 6369)} - , {"dist_listen_max", t(range(1024, 65535), "kernel.inet_dist_listen_max", 6369)} - , {"backtrace_depth", t(integer(), "emqx_machine.backtrace_depth", 23)} - , {"cluster_call", ref("cluster_call")} + [ {"name", + sc(string(), + #{ default => "emqx@127.0.0.1" + , override_env => "EMQX_NODE_NAME" + })} + , {"cookie", + sc(string(), + #{ mapping => "vm_args.-setcookie", + default => "emqxsecretcookie", + sensitive => true, + override_env => "EMQX_NODE_COOKIE" + })} + , {"data_dir", + sc(string(), + #{ nullable => false + })} + , {"config_files", + sc(list(string()), + #{ mapping => "emqx.config_files" + , default => undefined + })} + , {"global_gc_interval", + sc(emqx_schema:duration(), + #{ default => "15m" + })} + , {"crash_dump_dir", + sc(file(), + #{ mapping => "vm_args.-env ERL_CRASH_DUMP" + })} + , {"dist_net_ticktime", + sc(emqx_schema:duration(), + #{ mapping => "vm_args.-kernel net_ticktime" + , default => "2m" + })} + , {"dist_listen_min", + sc(range(1024, 65535), + #{ mapping => "kernel.inet_dist_listen_min" + , default => 6369 + })} + , {"dist_listen_max", + sc(range(1024, 65535), + #{ mapping => "kernel.inet_dist_listen_max" + , default => 6369 + })} + , {"backtrace_depth", + sc(integer(), + #{ mapping => "emqx_machine.backtrace_depth" + , default => 23 + })} + , {"cluster_call", + sc(ref("cluster_call"), + #{} + )} ]; - fields("cluster_call") -> - [ {"retry_interval", t(emqx_schema:duration(), "emqx_machine.retry_interval", "1s")} - , {"max_history", t(range(1, 500), "emqx_machine.max_history", 100)} - , {"cleanup_interval", t(emqx_schema:duration(), "emqx_machine.cleanup_interval", "5m")} + [ {"retry_interval", + sc(emqx_schema:duration(), + #{ mapping => "emqx_machine.retry_interval" + , default => "1s" + })} + , {"max_history", + sc(range(1, 500), + #{mapping => "emqx_machine.max_history", + default => 100 + })} + , {"cleanup_interval", + sc(emqx_schema:duration(), + #{mapping => "emqx_machine.cleanup_interval", + default => "5m" + })} ]; fields("rpc") -> - [ {"mode", t(union(sync, async), undefined, async)} - , {"async_batch_size", t(integer(), "gen_rpc.max_batch_size", 256)} - , {"port_discovery",t(union(manual, stateless), "gen_rpc.port_discovery", stateless)} - , {"tcp_server_port", t(integer(), "gen_rpc.tcp_server_port", 5369)} - , {"tcp_client_num", t(range(1, 256), undefined, 1)} - , {"connect_timeout", t(emqx_schema:duration(), "gen_rpc.connect_timeout", "5s")} - , {"send_timeout", t(emqx_schema:duration(), "gen_rpc.send_timeout", "5s")} - , {"authentication_timeout", t(emqx_schema:duration(), "gen_rpc.authentication_timeout", "5s")} - , {"call_receive_timeout", t(emqx_schema:duration(), "gen_rpc.call_receive_timeout", "15s")} - , {"socket_keepalive_idle", t(emqx_schema:duration_s(), "gen_rpc.socket_keepalive_idle", "7200s")} - , {"socket_keepalive_interval", t(emqx_schema:duration_s(), "gen_rpc.socket_keepalive_interval", "75s")} - , {"socket_keepalive_count", t(integer(), "gen_rpc.socket_keepalive_count", 9)} - , {"socket_sndbuf", t(emqx_schema:bytesize(), "gen_rpc.socket_sndbuf", "1MB")} - , {"socket_recbuf", t(emqx_schema:bytesize(), "gen_rpc.socket_recbuf", "1MB")} - , {"socket_buffer", t(emqx_schema:bytesize(), "gen_rpc.socket_buffer", "1MB")} + [ {"mode", + sc(union(sync, async), + #{ default => async + })} + , {"async_batch_size", + sc(integer(), + #{ mapping => "gen_rpc.max_batch_size" + , default => 256 + })} + , {"port_discovery", + sc(union(manual, stateless), + #{ mapping => "gen_rpc.port_discovery" + , default => stateless + })} + , {"tcp_server_port", + sc(integer(), + #{ mapping => "gen_rpc.tcp_server_port" + , default => 5369 + })} + , {"tcp_client_num", + sc(range(1, 256), + #{ default => 1 + })} + , {"connect_timeout", + sc(emqx_schema:duration(), + #{ mapping => "gen_rpc.connect_timeout", + default => "5s" + })} + , {"send_timeout", + sc(emqx_schema:duration(), + #{ mapping => "gen_rpc.send_timeout" + , default => "5s" + })} + , {"authentication_timeout", + sc(emqx_schema:duration(), + #{ mapping=> "gen_rpc.authentication_timeout" + , default => "5s" + })} + , {"call_receive_timeout", + sc(emqx_schema:duration(), + #{ mapping => "gen_rpc.call_receive_timeout" + , default => "15s" + })} + , {"socket_keepalive_idle", + sc(emqx_schema:duration_s(), + #{ mapping => "gen_rpc.socket_keepalive_idle" + , default => "7200s" + })} + , {"socket_keepalive_interval", + sc(emqx_schema:duration_s(), + #{ mapping => "gen_rpc.socket_keepalive_interval", + default => "75s" + })} + , {"socket_keepalive_count", + sc(integer(), + #{ mapping => "gen_rpc.socket_keepalive_count" + , default => 9 + })} + , {"socket_sndbuf", + sc(emqx_schema:bytesize(), + #{ mapping => "gen_rpc.socket_sndbuf" + , default => "1MB" + })} + , {"socket_recbuf", + sc(emqx_schema:bytesize(), + #{ mapping => "gen_rpc.socket_recbuf" + , default => "1MB" + })} + , {"socket_buffer", + sc(emqx_schema:bytesize(), + #{ mapping => "gen_rpc.socket_buffer" + , default => "1MB" + })} ]; fields("log") -> [ {"console_handler", ref("console_handler")} - , {"file_handlers", ref("file_handlers")} - , {"error_logger", t(atom(), "kernel.error_logger", silent)} + , {"file_handlers", + sc(ref("file_handlers"), + #{})} + , {"error_logger", + sc(atom(), + #{mapping => "kernel.error_logger", + default => silent})} ]; fields("console_handler") -> - [ {"enable", t(boolean(), undefined, false)} + [ {"enable", + sc(boolean(), + #{ default => false + })} ] ++ log_handler_common_confs(); fields("file_handlers") -> - [ {"$name", ref("log_file_handler")} + [ {"$name", + sc(ref("log_file_handler"), + #{})} ]; fields("log_file_handler") -> - [ {"file", t(file(), undefined, undefined)} - , {"rotation", ref("log_rotation")} - , {"max_size", #{type => union([infinity, emqx_schema:bytesize()]), - default => "10MB"}} + [ {"file", + sc(file(), + #{})} + , {"rotation", + sc(ref("log_rotation"), + #{})} + , {"max_size", + sc(union([infinity, emqx_schema:bytesize()]), + #{ default => "10MB" + })} ] ++ log_handler_common_confs(); fields("log_rotation") -> - [ {"enable", t(boolean(), undefined, true)} - , {"count", t(range(1, 2048), undefined, 10)} + [ {"enable", + sc(boolean(), + #{ default => true + })} + , {"count", + sc(range(1, 2048), + #{ default => 10 + })} ]; fields("log_overload_kill") -> - [ {"enable", t(boolean(), undefined, true)} - , {"mem_size", t(emqx_schema:bytesize(), undefined, "30MB")} - , {"qlen", t(integer(), undefined, 20000)} - , {"restart_after", t(union(emqx_schema:duration(), infinity), undefined, "5s")} + [ {"enable", + sc(boolean(), + #{ default => true + })} + , {"mem_size", + sc(emqx_schema:bytesize(), + #{ default => "30MB" + })} + , {"qlen", + sc(integer(), + #{ default => 20000 + })} + , {"restart_after", + sc(union(emqx_schema:duration(), infinity), + #{ default => "5s" + })} ]; fields("log_burst_limit") -> - [ {"enable", t(boolean(), undefined, true)} - , {"max_count", t(integer(), undefined, 10000)} - , {"window_time", t(emqx_schema:duration(), undefined, "1s")} + [ {"enable", + sc(boolean(), + #{ default => true + })} + , {"max_count", + sc(integer(), + #{ default => 10000 + })} + , {"window_time", + sc(emqx_schema:duration(), + #{default => "1s"})} ]; fields("authorization") -> emqx_schema:fields("authorization") ++ - emqx_authz_schema:fields("authorization"); - -fields(Name) -> - find_field(Name, ?MERGED_CONFIGS). - -find_field(Name, []) -> - error({unknown_config_struct_field, Name}); -find_field(Name, [SchemaModule | Rest]) -> - case lists:member(bin(Name), hocon_schema:root_names(SchemaModule)) of - true -> SchemaModule:fields(Name); - false -> find_field(Name, Rest) - end. + emqx_authz_schema:fields("authorization"). translations() -> ["ekka", "kernel", "emqx"]. @@ -302,20 +541,52 @@ tr_logger(Conf) -> [{handler, default, undefined}] ++ ConsoleHandler ++ FileHandlers. log_handler_common_confs() -> - [ {"level", t(log_level(), undefined, warning)} - , {"time_offset", t(string(), undefined, "system")} - , {"chars_limit", #{type => hoconsc:union([unlimited, range(1, inf)]), - default => unlimited - }} - , {"formatter", t(union([text, json]), undefined, text)} - , {"single_line", t(boolean(), undefined, true)} - , {"sync_mode_qlen", t(integer(), undefined, 100)} - , {"drop_mode_qlen", t(integer(), undefined, 3000)} - , {"flush_qlen", t(integer(), undefined, 8000)} - , {"overload_kill", ref("log_overload_kill")} - , {"burst_limit", ref("log_burst_limit")} - , {"supervisor_reports", t(union([error, progress]), undefined, error)} - , {"max_depth", t(union([unlimited, integer()]), undefined, 100)} + [ {"level", + sc(log_level(), + #{ default => warning + })} + , {"time_offset", + sc(string(), + #{ default => "system" + })} + , {"chars_limit", + sc(hoconsc:union([unlimited, range(1, inf)]), + #{ default => unlimited + })} + , {"formatter", + sc(union([text, json]), + #{ default => text + })} + , {"single_line", + sc(boolean(), + #{ default => true + })} + , {"sync_mode_qlen", + sc(integer(), + #{ default => 100 + })} + , {"drop_mode_qlen", + sc(integer(), + #{ default => 3000 + })} + , {"flush_qlen", + sc(integer(), + #{ default => 8000 + })} + , {"overload_kill", + sc(ref("log_overload_kill"), + #{})} + , {"burst_limit", + sc(ref("log_burst_limit"), + #{})} + , {"supervisor_reports", + sc(union([error, progress]), + #{ default => error + })} + , {"max_depth", + sc(union([unlimited, integer()]), + #{ default => 100 + })} ]. log_handler_conf(Conf) -> @@ -424,18 +695,9 @@ keys(Parent, Conf) -> %% types -t(Type) -> hoconsc:t(Type). +sc(Type, Meta) -> hoconsc:mk(Type, Meta). -t(Type, Mapping, Default) -> - hoconsc:t(Type, #{mapping => Mapping, default => Default}). - -t(Type, Mapping, Default, OverrideEnv) -> - hoconsc:t(Type, #{ mapping => Mapping - , default => Default - , override_env => OverrideEnv - }). - -ref(Field) -> hoconsc:t(hoconsc:ref(Field)). +ref(Field) -> hoconsc:ref(?MODULE, Field). options(static, Conf) -> [{seeds, [to_atom(S) || S <- conf_get("cluster.static.seeds", Conf, [])]}]; @@ -475,6 +737,6 @@ to_atom(Str) when is_list(Str) -> to_atom(Bin) when is_binary(Bin) -> binary_to_atom(Bin, utf8). -bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8); -bin(Bin) when is_binary(Bin) -> Bin; -bin(L) when is_list(L) -> iolist_to_binary(L). +roots(Module) -> + lists:map(fun({_BinName, Root}) -> Root end, + maps:to_list(hocon_schema:roots(Module))). diff --git a/apps/emqx_management/src/emqx_management_schema.erl b/apps/emqx_management/src/emqx_management_schema.erl index d21f0e106..fce71ad7b 100644 --- a/apps/emqx_management/src/emqx_management_schema.erl +++ b/apps/emqx_management/src/emqx_management_schema.erl @@ -19,9 +19,12 @@ -behaviour(hocon_schema). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1]). +namespace() -> management. + roots() -> []. fields(_) -> []. diff --git a/apps/emqx_modules/src/emqx_modules_schema.erl b/apps/emqx_modules/src/emqx_modules_schema.erl index 7a6b72a8a..15f6ab901 100644 --- a/apps/emqx_modules/src/emqx_modules_schema.erl +++ b/apps/emqx_modules/src/emqx_modules_schema.erl @@ -20,9 +20,12 @@ -behaviour(hocon_schema). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1]). +namespace() -> modules. + roots() -> ["delayed", "recon", @@ -33,32 +36,34 @@ roots() -> fields(Name) when Name =:= "recon"; Name =:= "telemetry" -> - [ {enable, emqx_schema:t(boolean(), undefined, false)} + [ {enable, hoconsc:mk(boolean(), #{default => false})} ]; fields("delayed") -> - [ {enable, emqx_schema:t(boolean(), undefined, false)} - , {max_delayed_messages, emqx_schema:t(integer())} + [ {enable, hoconsc:mk(boolean(), #{default => false})} + , {max_delayed_messages, sc(integer(), #{})} ]; fields("rewrite") -> [ {action, hoconsc:enum([publish, subscribe])} - , {source_topic, emqx_schema:t(binary())} - , {re, emqx_schema:t(binary())} - , {dest_topic, emqx_schema:t(binary())} + , {source_topic, sc(binary(), #{})} + , {re, sc(binary(), #{})} + , {dest_topic, sc(binary(), #{})} ]; fields("event_message") -> - [ {"$event/client_connected", emqx_schema:t(boolean(), undefined, false)} - , {"$event/client_disconnected", emqx_schema:t(boolean(), undefined, false)} - , {"$event/client_subscribed", emqx_schema:t(boolean(), undefined, false)} - , {"$event/client_unsubscribed", emqx_schema:t(boolean(), undefined, false)} - , {"$event/message_delivered", emqx_schema:t(boolean(), undefined, false)} - , {"$event/message_acked", emqx_schema:t(boolean(), undefined, false)} - , {"$event/message_dropped", emqx_schema:t(boolean(), undefined, false)} + [ {"$event/client_connected", sc(boolean(), #{default => false})} + , {"$event/client_disconnected", sc(boolean(), #{default => false})} + , {"$event/client_subscribed", sc(boolean(), #{default => false})} + , {"$event/client_unsubscribed", sc(boolean(), #{default => false})} + , {"$event/message_delivered", sc(boolean(), #{default => false})} + , {"$event/message_acked", sc(boolean(), #{default => false})} + , {"$event/message_dropped", sc(boolean(), #{default => false})} ]; fields("topic_metrics") -> - [{topic, emqx_schema:t(binary())}]. + [{topic, sc(binary(), #{})}]. -array(Name) -> {Name, hoconsc:array(hoconsc:ref(Name))}. +array(Name) -> {Name, hoconsc:array(hoconsc:ref(?MODULE, Name))}. + +sc(Type, Meta) -> hoconsc:mk(Type, Meta). diff --git a/apps/emqx_prometheus/src/emqx_prometheus_schema.erl b/apps/emqx_prometheus/src/emqx_prometheus_schema.erl index 47630b58d..922de6238 100644 --- a/apps/emqx_prometheus/src/emqx_prometheus_schema.erl +++ b/apps/emqx_prometheus/src/emqx_prometheus_schema.erl @@ -19,13 +19,18 @@ -behaviour(hocon_schema). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1]). +namespace() -> "prometheus". + roots() -> ["prometheus"]. fields("prometheus") -> - [ {push_gateway_server, emqx_schema:t(string())} - , {interval, emqx_schema:t(emqx_schema:duration_ms(), undefined, "15s")} - , {enable, emqx_schema:t(boolean(), undefined, false)} + [ {push_gateway_server, sc(string(), #{})} + , {interval, sc(emqx_schema:duration_ms(), #{default => "15s"})} + , {enable, sc(boolean(), #{default => false})} ]. + +sc(Type, Meta) -> hoconsc:mk(Type, Meta). diff --git a/apps/emqx_retainer/src/emqx_retainer_schema.erl b/apps/emqx_retainer/src/emqx_retainer_schema.erl index e2acc7fe7..55cfa2fcc 100644 --- a/apps/emqx_retainer/src/emqx_retainer_schema.erl +++ b/apps/emqx_retainer/src/emqx_retainer_schema.erl @@ -4,40 +4,40 @@ -export([roots/0, fields/1]). --define(TYPE(Type), hoconsc:t(Type)). +-define(TYPE(Type), hoconsc:mk(Type)). roots() -> ["emqx_retainer"]. fields("emqx_retainer") -> - [ {enable, t(boolean(), false)} - , {msg_expiry_interval, t(emqx_schema:duration_ms(), "0s")} - , {msg_clear_interval, t(emqx_schema:duration_ms(), "0s")} + [ {enable, sc(boolean(), false)} + , {msg_expiry_interval, sc(emqx_schema:duration_ms(), "0s")} + , {msg_clear_interval, sc(emqx_schema:duration_ms(), "0s")} , {flow_control, ?TYPE(hoconsc:ref(?MODULE, flow_control))} - , {max_payload_size, t(emqx_schema:bytesize(), "1MB")} + , {max_payload_size, sc(emqx_schema:bytesize(), "1MB")} , {config, config()} ]; fields(mnesia_config) -> [ {type, ?TYPE(hoconsc:union([built_in_database]))} - , {storage_type, t(hoconsc:union([ram, disc, disc_only]), ram)} - , {max_retained_messages, t(integer(), 0, fun is_pos_integer/1)} + , {storage_type, sc(hoconsc:union([ram, disc, disc_only]), ram)} + , {max_retained_messages, sc(integer(), 0, fun is_pos_integer/1)} ]; fields(flow_control) -> - [ {max_read_number, t(integer(), 0, fun is_pos_integer/1)} - , {msg_deliver_quota, t(integer(), 0, fun is_pos_integer/1)} - , {quota_release_interval, t(emqx_schema:duration_ms(), "0ms")} + [ {max_read_number, sc(integer(), 0, fun is_pos_integer/1)} + , {msg_deliver_quota, sc(integer(), 0, fun is_pos_integer/1)} + , {quota_release_interval, sc(emqx_schema:duration_ms(), "0ms")} ]. %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- -t(Type, Default) -> - hoconsc:t(Type, #{default => Default}). +sc(Type, Default) -> + hoconsc:mk(Type, #{default => Default}). -t(Type, Default, Validator) -> - hoconsc:t(Type, #{default => Default, - validator => Validator}). +sc(Type, Default, Validator) -> + hoconsc:mk(Type, #{default => Default, + validator => Validator}). is_pos_integer(V) -> V >= 0. diff --git a/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl b/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl index a9646ae1e..2614fb8b1 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_engine_schema.erl @@ -20,10 +20,13 @@ -behaviour(hocon_schema). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1]). -roots() -> ["emqx_rule_engine"]. +namespace() -> rule_engine. -fields("emqx_rule_engine") -> - [{ignore_sys_message, emqx_schema:t(boolean(), undefined, true)}]. +roots() -> ["rule_engine"]. + +fields("rule_engine") -> + [{ignore_sys_message, hoconsc:mk(boolean(), #{default => true})}]. diff --git a/apps/emqx_rule_engine/src/emqx_rule_events.erl b/apps/emqx_rule_engine/src/emqx_rule_events.erl index 8154170b0..a0960df25 100644 --- a/apps/emqx_rule_engine/src/emqx_rule_events.erl +++ b/apps/emqx_rule_engine/src/emqx_rule_events.erl @@ -594,5 +594,6 @@ printable_maps(Headers) -> end, #{}, Headers). ignore_sys_message(#message{flags = Flags}) -> + ConfigRootKey = emqx_rule_engine_schema:namespace(), maps:get(sys, Flags, false) andalso - emqx:get_config([emqx_rule_engine, ignore_sys_message]). + emqx:get_config([ConfigRootKey, ignore_sys_message]). diff --git a/apps/emqx_statsd/src/emqx_statsd_schema.erl b/apps/emqx_statsd/src/emqx_statsd_schema.erl index 906f55a4c..72b245f4a 100644 --- a/apps/emqx_statsd/src/emqx_statsd_schema.erl +++ b/apps/emqx_statsd/src/emqx_statsd_schema.erl @@ -6,15 +6,18 @@ -export([to_ip_port/1]). --export([ roots/0 +-export([ namespace/0 + , roots/0 , fields/1]). -typerefl_from_string({ip_port/0, emqx_statsd_schema, to_ip_port}). +namespace() -> "statsd". + roots() -> ["statsd"]. fields("statsd") -> - [ {enable, emqx_schema:t(boolean(), undefined, false)} + [ {enable, hoconsc:mk(boolean(), #{default => false})} , {server, fun server/1} , {sample_time_interval, fun duration_ms/1} , {flush_time_interval, fun duration_ms/1} diff --git a/rebar.config b/rebar.config index 4e622b738..690e23bc7 100644 --- a/rebar.config +++ b/rebar.config @@ -60,7 +60,7 @@ , {observer_cli, "1.7.1"} % NOTE: depends on recon 2.5.x , {getopt, "1.0.2"} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.14.1"}}} - , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.14.0"}}} + , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.15.0"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.4.0"}}} , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}} ]}. From f5bfa4cd43521f2d6854faffa86836702be7e7c9 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Fri, 3 Sep 2021 18:32:23 +0800 Subject: [PATCH 061/109] fix: deny creating metrics for empty topics (#5650) * fix: deny null topic create metrics --- apps/emqx_modules/src/emqx_topic_metrics_api.erl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/emqx_modules/src/emqx_topic_metrics_api.erl b/apps/emqx_modules/src/emqx_topic_metrics_api.erl index e22f5750f..98ae249cb 100644 --- a/apps/emqx_modules/src/emqx_topic_metrics_api.erl +++ b/apps/emqx_modules/src/emqx_topic_metrics_api.erl @@ -94,7 +94,7 @@ topic_metrics_api() -> responses => #{ <<"200">> => schema(<<"Create topic metrics success">>), <<"409">> => error_schema(<<"Topic metrics max limit">>, [?EXCEED_LIMIT]), - <<"400">> => error_schema(<<"Topic metrics already exist">>, [?BAD_REQUEST]) + <<"400">> => error_schema(<<"Topic metrics already exist or bad topic">>, [?BAD_REQUEST]) } } }, @@ -137,6 +137,8 @@ topic_metrics(put, #{body := #{<<"topic">> := Topic, <<"action">> := <<"reset">> reset(Topic); topic_metrics(put, #{body := #{<<"action">> := <<"reset">>}}) -> reset(); +topic_metrics(post, #{body := #{<<"topic">> := <<>>}}) -> + {400, 'BAD_REQUEST', <<"Topic can not be empty">>}; topic_metrics(post, #{body := #{<<"topic">> := Topic}}) -> register(Topic). From 19aff7bfddfda5859e77438abcb222c363f3abca Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Fri, 3 Sep 2021 11:44:43 +0200 Subject: [PATCH 062/109] fix(authz): schema fields used directly. --- apps/emqx/src/emqx_schema.erl | 9 +++++---- apps/emqx_authz/src/emqx_authz.erl | 6 +++--- apps/emqx_authz/src/emqx_authz_schema.erl | 2 ++ apps/emqx_connector/src/emqx_connector_mongo.erl | 3 ++- apps/emqx_machine/src/emqx_machine_schema.erl | 8 +++++++- 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index fe4439aaa..0189a468b 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -93,7 +93,8 @@ roots() -> "plugins", "stats", "sysmon", - "alarm" + "alarm", + "authorization" ]. fields("stats") -> @@ -113,13 +114,13 @@ fields("authorization") -> #{ default => ignore })} , {"cache", - sc(ref("authorization_cache"), + sc(ref(?MODULE, "cache"), #{ }) } ]; -fields("authorization_cache") -> +fields("cache") -> [ {"enable", sc(boolean(), #{ default => true @@ -276,7 +277,7 @@ fields("zones") -> )}]; fields("zone_settings") -> - Fields = ["mqtt", "stats", "authorization", "flapping_detect", "force_shutdown", + Fields = ["mqtt", "stats", "flapping_detect", "force_shutdown", "conn_congestion", "rate_limit", "quota", "force_gc"], [{F, ref(emqx_zone_schema, F)} || F <- Fields]; diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 7fcd80269..af77390d5 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -350,9 +350,9 @@ do_authorize(Client, PubSub, Topic, %%-------------------------------------------------------------------- check_sources(RawSources) -> - {ok, Conf} = hocon:binary(jsx:encode(#{<<"authorization">> => #{<<"sources">> => RawSources}}), #{format => richmap}), - CheckConf = hocon_schema:check(emqx_authz_schema, Conf, #{atom_key => true}), - #{authorization:= #{sources := Sources}} = hocon_schema:richmap_to_map(CheckConf), + Schema = #{roots => emqx_authz_schema:fields("authorization"), fields => #{}}, + Conf = #{<<"sources">> => RawSources}, + #{sources := Sources} = hocon_schema:check_plain(Schema, Conf, #{atom_key => true}), Sources. find_source_by_type(Type) -> find_source_by_type(Type, lookup()). diff --git a/apps/emqx_authz/src/emqx_authz_schema.erl b/apps/emqx_authz/src/emqx_authz_schema.erl index 0645990a8..b90d522e8 100644 --- a/apps/emqx_authz/src/emqx_authz_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_schema.erl @@ -20,6 +20,8 @@ namespace() -> authz. +%% @doc authorization schema is not exported +%% but directly used by emqx_schema roots() -> []. fields("authorization") -> diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index c95679f32..0b769748a 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -82,7 +82,8 @@ mongo_fields() -> , {auth_source, #{type => binary(), nullable => true}} , {database, fun emqx_connector_schema_lib:database/1} - , {topology, #{type => hoconsc:ref(?MODULE, topology)}} + , {topology, #{type => hoconsc:ref(?MODULE, topology), + nullable => true}} ] ++ emqx_connector_schema_lib:ssl_fields(). diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index a3e7e9388..657594ae8 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -61,7 +61,13 @@ namespace() -> undefined. roots() -> - ["cluster", "node", "rpc", "log"] ++ lists:flatmap(fun roots/1, ?MERGED_CONFIGS). + %% This is a temp workaround to define part of authorization config + %% in emqx_schema and part of it in emqx_authz_schema but then + %% merged here in this module + %% The proper fix should be to make connection (channel, session) state + %% extendable by e.g. allow hooks be stateful. + ["cluster", "node", "rpc", "log", "authorization"] ++ + lists:keydelete("authorization", 1, lists:flatmap(fun roots/1, ?MERGED_CONFIGS)). fields("cluster") -> [ {"name", From c42c1e698a88fddc6377b549664a3dc1edfa5dea Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 3 Sep 2021 14:56:00 +0800 Subject: [PATCH 063/109] chore(gw): add From param for _channel:handle_call/3 --- .../src/bhvrs/emqx_gateway_channel.erl | 8 +++- .../src/bhvrs/emqx_gateway_conn.erl | 14 +++++-- .../src/coap/emqx_coap_channel.erl | 4 +- .../src/exproto/emqx_exproto_channel.erl | 31 +++++++-------- .../src/lwm2m/emqx_lwm2m_channel.erl | 4 +- .../src/mqttsn/emqx_sn_channel.erl | 33 ++++++++-------- .../src/stomp/emqx_stomp_channel.erl | 38 +++++++++---------- 7 files changed, 72 insertions(+), 60 deletions(-) diff --git a/apps/emqx_gateway/src/bhvrs/emqx_gateway_channel.erl b/apps/emqx_gateway/src/bhvrs/emqx_gateway_channel.erl index 06efe4fd0..c4fd114e4 100644 --- a/apps/emqx_gateway/src/bhvrs/emqx_gateway_channel.erl +++ b/apps/emqx_gateway/src/bhvrs/emqx_gateway_channel.erl @@ -44,6 +44,8 @@ -type conn_state() :: idle | connecting | connected | disconnected | atom(). +-type gen_server_from() :: {pid(), Tag :: term()}. + -type reply() :: {outgoing, emqx_gateway_frame:packet()} | {outgoing, [emqx_gateway_frame:packet()]} | {event, conn_state() | updated} @@ -71,11 +73,13 @@ | {shutdown, Reason :: any(), channel()}. %% @doc Handle the custom gen_server:call/2 for its connection process --callback handle_call(Req :: any(), channel()) +-callback handle_call(Req :: any(), From :: gen_server_from(), channel()) -> {reply, Reply :: any(), channel()} %% Reply to caller and trigger an event(s) | {reply, Reply :: any(), - EventOrEvents:: tuple() | list(tuple()), channel()} + EventOrEvents :: tuple() | list(tuple()), channel()} + | {noreply, channel()} + | {noreply, EventOrEvents :: tuple() | list(tuple()), channel()} | {shutdown, Reason :: any(), Reply :: any(), channel()} %% Shutdown the process, reply to caller and write a packet to client | {shutdown, Reason :: any(), Reply :: any(), diff --git a/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl b/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl index 528f8dfd8..51bcbd358 100644 --- a/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl +++ b/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl @@ -394,6 +394,10 @@ append_msg(Q, Msg) -> handle_msg({'$gen_call', From, Req}, State) -> case handle_call(From, Req, State) of + {noreply, NState} -> + {ok, NState}; + {noreply, Msgs, NState} -> + {ok, next_msgs(Msgs), NState}; {reply, Reply, NState} -> gen_server:reply(From, Reply), {ok, NState}; @@ -545,10 +549,14 @@ handle_call(_From, info, State) -> handle_call(_From, stats, State) -> {reply, stats(State), State}; -handle_call(_From, Req, State = #state{ +handle_call(From, Req, State = #state{ chann_mod = ChannMod, channel = Channel}) -> - case ChannMod:handle_call(Req, Channel) of + case ChannMod:handle_call(Req, From, Channel) of + {noreply, NChannel} -> + {noreply, State#state{channel = NChannel}}; + {noreply, Msgs, NChannel} -> + {noreply, Msgs, State#state{channel = NChannel}}; {reply, Reply, NChannel} -> {reply, Reply, State#state{channel = NChannel}}; {reply, Reply, Msgs, NChannel} -> @@ -559,8 +567,6 @@ handle_call(_From, Req, State = #state{ NState = State#state{channel = NChannel}, ok = handle_outgoing(Packet, NState), shutdown(Reason, Reply, NState) - - end. %%-------------------------------------------------------------------- diff --git a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl index 24f06549b..6e554b9ef 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl @@ -34,7 +34,7 @@ , terminate/2 ]). --export([ handle_call/2 +-export([ handle_call/3 , handle_cast/2 , handle_info/2 ]). @@ -165,7 +165,7 @@ handle_timeout(_, _, Channel) -> %%-------------------------------------------------------------------- %% Handle call %%-------------------------------------------------------------------- -handle_call(Req, Channel) -> +handle_call(Req, _From, Channel) -> ?LOG(error, "Unexpected call: ~p", [Req]), {reply, ignored, Channel}. diff --git a/apps/emqx_gateway/src/exproto/emqx_exproto_channel.erl b/apps/emqx_gateway/src/exproto/emqx_exproto_channel.erl index ace9a7be5..3de231958 100644 --- a/apps/emqx_gateway/src/exproto/emqx_exproto_channel.erl +++ b/apps/emqx_gateway/src/exproto/emqx_exproto_channel.erl @@ -31,7 +31,7 @@ , handle_in/2 , handle_deliver/2 , handle_timeout/3 - , handle_call/2 + , handle_call/3 , handle_cast/2 , handle_info/2 , terminate/2 @@ -243,23 +243,24 @@ handle_timeout(_TRef, Msg, Channel) -> ?WARN("Unexpected timeout: ~p", [Msg]), {ok, Channel}. --spec handle_call(any(), channel()) +-spec handle_call(Req :: any(), From :: any(), channel()) -> {reply, Reply :: term(), channel()} | {reply, Reply :: term(), replies(), channel()} | {shutdown, Reason :: term(), Reply :: term(), channel()}. -handle_call({send, Data}, Channel) -> +handle_call({send, Data}, _From, Channel) -> {reply, ok, [{outgoing, Data}], Channel}; -handle_call(close, Channel = #channel{conn_state = connected}) -> +handle_call(close, _From, Channel = #channel{conn_state = connected}) -> {reply, ok, [{event, disconnected}, {close, normal}], Channel}; -handle_call(close, Channel) -> +handle_call(close, _From, Channel) -> {reply, ok, [{close, normal}], Channel}; -handle_call({auth, ClientInfo, _Password}, Channel = #channel{conn_state = connected}) -> +handle_call({auth, ClientInfo, _Password}, _From, + Channel = #channel{conn_state = connected}) -> ?LOG(warning, "Duplicated authorized command, dropped ~p", [ClientInfo]), {reply, {error, ?RESP_PERMISSION_DENY, <<"Duplicated authenticate command">>}, Channel}; -handle_call({auth, ClientInfo0, Password}, +handle_call({auth, ClientInfo0, Password}, _From, Channel = #channel{ ctx = Ctx, conninfo = ConnInfo, @@ -300,7 +301,7 @@ handle_call({auth, ClientInfo0, Password}, {reply, {error, ?RESP_PERMISSION_DENY, Reason}, Channel} end; -handle_call({start_timer, keepalive, Interval}, +handle_call({start_timer, keepalive, Interval}, _From, Channel = #channel{ conninfo = ConnInfo, clientinfo = ClientInfo @@ -310,7 +311,7 @@ handle_call({start_timer, keepalive, Interval}, NChannel = Channel#channel{conninfo = NConnInfo, clientinfo = NClientInfo}, {reply, ok, ensure_keepalive(NChannel)}; -handle_call({subscribe_from_client, TopicFilter, Qos}, +handle_call({subscribe_from_client, TopicFilter, Qos}, _From, Channel = #channel{ ctx = Ctx, conn_state = connected, @@ -323,20 +324,20 @@ handle_call({subscribe_from_client, TopicFilter, Qos}, {reply, ok, NChannel} end; -handle_call({subscribe, Topic, SubOpts}, Channel) -> +handle_call({subscribe, Topic, SubOpts}, _From, Channel) -> {ok, NChannel} = do_subscribe([{Topic, SubOpts}], Channel), {reply, ok, NChannel}; -handle_call({unsubscribe_from_client, TopicFilter}, +handle_call({unsubscribe_from_client, TopicFilter}, _From, Channel = #channel{conn_state = connected}) -> {ok, NChannel} = do_unsubscribe([{TopicFilter, #{}}], Channel), {reply, ok, NChannel}; -handle_call({unsubscribe, Topic}, Channel) -> +handle_call({unsubscribe, Topic}, _From, Channel) -> {ok, NChannel} = do_unsubscribe([Topic], Channel), {reply, ok, NChannel}; -handle_call({publish, Topic, Qos, Payload}, +handle_call({publish, Topic, Qos, Payload}, _From, Channel = #channel{ ctx = Ctx, conn_state = connected, @@ -353,10 +354,10 @@ handle_call({publish, Topic, Qos, Payload}, {reply, ok, Channel} end; -handle_call(kick, Channel) -> +handle_call(kick, _From, Channel) -> {shutdown, kicked, ok, Channel}; -handle_call(Req, Channel) -> +handle_call(Req, _From, Channel) -> ?LOG(warning, "Unexpected call: ~p", [Req]), {reply, {error, unexpected_call}, Channel}. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl index 80078407b..b67032313 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl @@ -35,7 +35,7 @@ , terminate/2 ]). --export([ handle_call/2 +-export([ handle_call/3 , handle_cast/2 , handle_info/2 ]). @@ -152,7 +152,7 @@ handle_timeout(_, _, Channel) -> %%-------------------------------------------------------------------- %% Handle call %%-------------------------------------------------------------------- -handle_call(Req, Channel) -> +handle_call(Req, _From, Channel) -> ?LOG(error, "Unexpected call: ~p", [Req]), {reply, ignored, Channel}. diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl index 2834c27f6..d9576c253 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_channel.erl @@ -38,7 +38,7 @@ , set_conn_state/2 ]). --export([ handle_call/2 +-export([ handle_call/3 , handle_cast/2 , handle_info/2 ]). @@ -1113,12 +1113,13 @@ message_to_packet(MsgId, Message, %% Handle call %%-------------------------------------------------------------------- --spec handle_call(Req :: term(), channel()) - -> {reply, Reply :: term(), channel()} - | {shutdown, Reason :: term(), Reply :: term(), channel()} - | {shutdown, Reason :: term(), Reply :: term(), - emqx_types:packet(), channel()}. -handle_call({subscribe, Topic, SubOpts}, Channel) -> +-spec handle_call(Req :: term(), From :: term(), channel()) + -> {reply, Reply :: term(), channel()} + | {reply, Reply :: term(), replies(), channel()} + | {shutdown, Reason :: term(), Reply :: term(), channel()} + | {shutdown, Reason :: term(), Reply :: term(), + emqx_types:packet(), channel()}. +handle_call({subscribe, Topic, SubOpts}, _From, Channel) -> %% XXX: Only support short_topic_name SubProps = maps:get(sub_props, SubOpts, #{}), case maps:get(subtype, SubProps, short_topic_name) of @@ -1141,26 +1142,26 @@ handle_call({subscribe, Topic, SubOpts}, Channel) -> reply({error, only_support_short_name_topic}, Channel) end; -handle_call({unsubscribe, Topic}, Channel) -> +handle_call({unsubscribe, Topic}, _From, Channel) -> TopicFilters = [emqx_topic:parse(Topic)], {ok, _, NChannel} = do_unsubscribe(TopicFilters, Channel), reply(ok, NChannel); -handle_call(subscriptions, Channel = #channel{session = Session}) -> +handle_call(subscriptions, _From, Channel = #channel{session = Session}) -> reply(maps:to_list(emqx_session:info(subscriptions, Session)), Channel); -handle_call(kick, Channel) -> +handle_call(kick, _From, Channel) -> NChannel = ensure_disconnected(kicked, Channel), shutdown_and_reply(kicked, ok, NChannel); -handle_call(discard, Channel) -> +handle_call(discard, _From, Channel) -> shutdown_and_reply(discarded, ok, Channel); %% XXX: No Session Takeover -%handle_call({takeover, 'begin'}, Channel = #channel{session = Session}) -> +%handle_call({takeover, 'begin'}, _From, Channel = #channel{session = Session}) -> % reply(Session, Channel#channel{takeover = true}); % -%handle_call({takeover, 'end'}, Channel = #channel{session = Session, +%handle_call({takeover, 'end'}, _From, Channel = #channel{session = Session, % pendings = Pendings}) -> % ok = emqx_session:takeover(Session), % %% TODO: Should not drain deliver here (side effect) @@ -1168,16 +1169,16 @@ handle_call(discard, Channel) -> % AllPendings = lists:append(Delivers, Pendings), % shutdown_and_reply(takeovered, AllPendings, Channel); -%handle_call(list_authz_cache, Channel) -> +%handle_call(list_authz_cache, _From, Channel) -> % {reply, emqx_authz_cache:list_authz_cache(), Channel}; %% XXX: No Quota Now -% handle_call({quota, Policy}, Channel) -> +% handle_call({quota, Policy}, _From, Channel) -> % Zone = info(zone, Channel), % Quota = emqx_limiter:init(Zone, Policy), % reply(ok, Channel#channel{quota = Quota}); -handle_call(Req, Channel) -> +handle_call(Req, _From, Channel) -> ?LOG(error, "Unexpected call: ~p", [Req]), reply(ignored, Channel). diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl index 9bd2dac1b..673535ebd 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_channel.erl @@ -39,7 +39,7 @@ , set_conn_state/2 ]). --export([ handle_call/2 +-export([ handle_call/3 , handle_cast/2 , handle_info/2 ]). @@ -586,10 +586,10 @@ do_subscribe([{ParsedTopic, SubOpts0}|More], %%-------------------------------------------------------------------- -spec(handle_out(atom(), term(), channel()) - -> {ok, channel()} - | {ok, replies(), channel()} - | {shutdown, Reason :: term(), channel()} - | {shutdown, Reason :: term(), replies(), channel()}). + -> {ok, channel()} + | {ok, replies(), channel()} + | {shutdown, Reason :: term(), channel()} + | {shutdown, Reason :: term(), replies(), channel()}). handle_out(connerr, {Headers, ReceiptId, ErrMsg}, Channel) -> Frame = error_frame(Headers, ReceiptId, ErrMsg), @@ -620,11 +620,12 @@ handle_out(receipt, ReceiptId, Channel) -> %% Handle call %%-------------------------------------------------------------------- --spec(handle_call(Req :: term(), channel()) - -> {reply, Reply :: term(), channel()} - | {shutdown, Reason :: term(), Reply :: term(), channel()} - | {shutdown, Reason :: term(), Reply :: term(), stomp_frame(), channel()}). -handle_call({subscribe, Topic, SubOpts}, +-spec(handle_call(Req :: term(), From :: term(), channel()) + -> {reply, Reply :: term(), channel()} + | {reply, Reply :: term(), replies(), channel()} + | {shutdown, Reason :: term(), Reply :: term(), channel()} + | {shutdown, Reason :: term(), Reply :: term(), stomp_frame(), channel()}). +handle_call({subscribe, Topic, SubOpts}, _From, Channel = #channel{ subscriptions = Subs }) -> @@ -653,7 +654,7 @@ handle_call({subscribe, Topic, SubOpts}, end end; -handle_call({unsubscribe, Topic}, +handle_call({unsubscribe, Topic}, _From, Channel = #channel{ ctx = Ctx, clientinfo = ClientInfo = #{mountpoint := Mountpoint}, @@ -670,27 +671,27 @@ handle_call({unsubscribe, Topic}, ); %% Reply :: [{emqx_types:topic(), emqx_types:subopts()}] -handle_call(subscriptions, Channel = #channel{subscriptions = Subs}) -> +handle_call(subscriptions, _From, Channel = #channel{subscriptions = Subs}) -> Reply = lists:map( fun({_SubId, Topic, _Ack, SubOpts}) -> {Topic, SubOpts} end, Subs), reply(Reply, Channel); -handle_call(kick, Channel) -> +handle_call(kick, _From, Channel) -> NChannel = ensure_disconnected(kicked, Channel), Frame = error_frame(undefined, <<"Kicked out">>), shutdown_and_reply(kicked, ok, Frame, NChannel); -handle_call(discard, Channel) -> +handle_call(discard, _From, Channel) -> Frame = error_frame(undefined, <<"Discarded">>), shutdown_and_reply(discarded, ok, Frame, Channel); %% XXX: No Session Takeover -%handle_call({takeover, 'begin'}, Channel = #channel{session = Session}) -> +%handle_call({takeover, 'begin'}, _From, Channel = #channel{session = Session}) -> % reply(Session, Channel#channel{takeover = true}); % -%handle_call({takeover, 'end'}, Channel = #channel{session = Session, +%handle_call({takeover, 'end'}, _From, Channel = #channel{session = Session, % pendings = Pendings}) -> % ok = emqx_session:takeover(Session), % %% TODO: Should not drain deliver here (side effect) @@ -698,7 +699,7 @@ handle_call(discard, Channel) -> % AllPendings = lists:append(Delivers, Pendings), % shutdown_and_reply(takeovered, AllPendings, Channel); -handle_call(list_authz_cache, Channel) -> +handle_call(list_authz_cache, _From, Channel) -> %% This won't work {reply, emqx_authz_cache:list_authz_cache(), Channel}; @@ -708,11 +709,10 @@ handle_call(list_authz_cache, Channel) -> % Quota = emqx_limiter:init(Zone, Policy), % reply(ok, Channel#channel{quota = Quota}); -handle_call(Req, Channel) -> +handle_call(Req, _From, Channel) -> ?LOG(error, "Unexpected call: ~p", [Req]), reply(ignored, Channel). - %%-------------------------------------------------------------------- %% Handle cast %%-------------------------------------------------------------------- From f514f0c89b4e32b05d15d1d78eb07757dd48d9b2 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Mon, 6 Sep 2021 11:36:56 +0800 Subject: [PATCH 064/109] feat: minirest support swagger UI new version (#5658) --- rebar.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebar.config b/rebar.config index 690e23bc7..b1616bebc 100644 --- a/rebar.config +++ b/rebar.config @@ -51,7 +51,7 @@ , {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.2"}}} , {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.8"}}} , {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}} - , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.2.1"}}} + , {minirest, {git, "https://github.com/emqx/minirest", {tag, "1.2.2"}}} , {ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.1"}}} , {replayq, "0.3.3"} , {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}} From b1023d973344a62a391f03c97fd6c7cf86e625a0 Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Mon, 6 Sep 2021 13:32:29 +0800 Subject: [PATCH 065/109] fix: clients ip address params trans (#5657) * fix: clients ip address params trans --- apps/emqx_management/src/emqx_mgmt_api.erl | 7 +++++++ apps/emqx_management/src/emqx_mgmt_api_clients.erl | 6 +++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api.erl b/apps/emqx_management/src/emqx_mgmt_api.erl index 5a7b020d7..8cf2fa1cb 100644 --- a/apps/emqx_management/src/emqx_mgmt_api.erl +++ b/apps/emqx_management/src/emqx_mgmt_api.erl @@ -289,6 +289,7 @@ to_type_(V, atom) -> to_atom(V); to_type_(V, integer) -> to_integer(V); to_type_(V, timestamp) -> to_timestamp(V); to_type_(V, ip) -> aton(V); +to_type_(V, ip_port) -> to_ip_port(V); to_type_(V, _) -> V. to_atom(A) when is_atom(A) -> @@ -309,6 +310,12 @@ to_timestamp(B) when is_binary(B) -> aton(B) when is_binary(B) -> list_to_tuple([binary_to_integer(T) || T <- re:split(B, "[.]")]). +to_ip_port(IPAddress) -> + [IP0, Port0] = string:tokens(binary_to_list(IPAddress), ":"), + {ok, IP} = inet:parse_address(IP0), + Port = list_to_integer(Port0), + {IP, Port}. + %%-------------------------------------------------------------------- %% EUnits %%-------------------------------------------------------------------- diff --git a/apps/emqx_management/src/emqx_mgmt_api_clients.erl b/apps/emqx_management/src/emqx_mgmt_api_clients.erl index dd4df58a5..4b558eaae 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_clients.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_clients.erl @@ -45,7 +45,7 @@ [ {<<"node">>, atom} , {<<"username">>, binary} , {<<"zone">>, atom} - , {<<"ip_address">>, ip} + , {<<"ip_address">>, ip_port} , {<<"conn_state">>, atom} , {<<"clean_start">>, atom} , {<<"proto_name">>, binary} @@ -566,10 +566,10 @@ ms(username, X) -> #{clientinfo => #{username => X}}; ms(zone, X) -> #{clientinfo => #{zone => X}}; -ms(ip_address, X) -> - #{clientinfo => #{peerhost => X}}; ms(conn_state, X) -> #{conn_state => X}; +ms(ip_address, X) -> + #{conninfo => #{peername => X}}; ms(clean_start, X) -> #{conninfo => #{clean_start => X}}; ms(proto_name, X) -> From e998770f2e1b0f2a2c1b40968f38ad4970d94f02 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Mon, 6 Sep 2021 18:46:08 +0800 Subject: [PATCH 066/109] refactor(authn): refactor to support global and listener authentication --- apps/emqx/include/emqx.hrl | 16 + apps/emqx/src/emqx_authentication.erl | 731 +++++++++ apps/emqx/src/emqx_broker_sup.erl | 10 +- apps/emqx/src/emqx_config_handler.erl | 2 + apps/emqx/src/emqx_listeners.erl | 4 + apps/emqx/src/emqx_metrics.erl | 2 - apps/emqx/src/emqx_schema.erl | 7 +- apps/emqx_authn/etc/emqx_authn.conf | 43 +- apps/emqx_authn/include/emqx_authn.hrl | 19 +- apps/emqx_authn/rebar.config | 4 +- apps/emqx_authn/src/emqx_authn.erl | 637 -------- apps/emqx_authn/src/emqx_authn_api.erl | 1405 ++++++++++------- apps/emqx_authn/src/emqx_authn_app.erl | 51 +- apps/emqx_authn/src/emqx_authn_schema.erl | 49 +- apps/emqx_authn/src/emqx_authn_sup.erl | 8 +- .../emqx_enhanced_authn_scram_mnesia.erl | 20 +- .../src/simple_authn/emqx_authn_http.erl | 63 +- .../src/simple_authn/emqx_authn_jwt.erl | 17 +- .../src/simple_authn/emqx_authn_mnesia.erl | 21 +- .../src/simple_authn/emqx_authn_mongodb.erl | 19 +- .../src/simple_authn/emqx_authn_mysql.erl | 17 +- .../src/simple_authn/emqx_authn_pgsql.erl | 22 +- .../src/simple_authn/emqx_authn_redis.erl | 20 +- apps/emqx_authn/test/emqx_authn_SUITE.erl | 98 -- apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl | 234 +-- .../test/emqx_authn_mnesia_SUITE.erl | 238 +-- .../src/emqx_connector_mongo.erl | 11 +- .../src/emqx_connector_redis.erl | 14 +- apps/emqx_gateway/etc/emqx_gateway.conf | 20 +- apps/emqx_gateway/src/emqx_gateway_schema.erl | 40 +- apps/emqx_machine/src/emqx_machine_schema.erl | 1 - apps/emqx_retainer/src/emqx_retainer.erl | 2 - rebar.config | 1 + 33 files changed, 2050 insertions(+), 1796 deletions(-) create mode 100644 apps/emqx/src/emqx_authentication.erl diff --git a/apps/emqx/include/emqx.hrl b/apps/emqx/include/emqx.hrl index 633527b57..63ab13256 100644 --- a/apps/emqx/include/emqx.hrl +++ b/apps/emqx/include/emqx.hrl @@ -134,3 +134,19 @@ }). -endif. + +%%-------------------------------------------------------------------- +%% Authentication +%%-------------------------------------------------------------------- + +-record(authenticator, + { id :: binary() + , provider :: module() + , enable :: boolean() + , state :: map() + }). + +-record(chain, + { name :: binary() + , authenticators :: [#authenticator{}] + }). \ No newline at end of file diff --git a/apps/emqx/src/emqx_authentication.erl b/apps/emqx/src/emqx_authentication.erl new file mode 100644 index 000000000..2b561d298 --- /dev/null +++ b/apps/emqx/src/emqx_authentication.erl @@ -0,0 +1,731 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authentication). + +-behaviour(gen_server). +-behaviour(hocon_schema). +-behaviour(emqx_config_handler). + +-include("emqx.hrl"). +-include("logger.hrl"). + +-export([ roots/0 + , fields/1 + ]). + +-export([ pre_config_update/2 + , post_config_update/4 + ]). + +-export([ authenticate/2 + ]). + +-export([ initialize_authentication/2 ]). + +-export([ start_link/0 + , stop/0 + ]). + +-export([ add_provider/2 + , remove_provider/1 + , create_chain/1 + , delete_chain/1 + , lookup_chain/1 + , list_chains/0 + , create_authenticator/2 + , delete_authenticator/2 + , update_authenticator/3 + , lookup_authenticator/2 + , list_authenticators/1 + , move_authenticator/3 + ]). + +-export([ import_users/3 + , add_user/3 + , delete_user/3 + , update_user/4 + , lookup_user/3 + , list_users/2 + ]). + +-export([ generate_id/1 ]). + +%% gen_server callbacks +-export([ init/1 + , handle_call/3 + , handle_cast/2 + , handle_info/2 + , terminate/2 + , code_change/3 + ]). + +-define(CHAINS_TAB, emqx_authn_chains). + +-define(VER_1, <<"1">>). +-define(VER_2, <<"2">>). + +-type config() :: #{atom() => term()}. +-type state() :: #{atom() => term()}. +-type extra() :: #{superuser := boolean(), + atom() => term()}. +-type user_info() :: #{user_id := binary(), + atom() => term()}. + +-callback refs() -> [{ref, Module, Name}] when Module::module(), Name::atom(). + +-callback create(Config) + -> {ok, State} + | {error, term()} + when Config::config(), State::state(). + +-callback update(Config, State) + -> {ok, NewState} + | {error, term()} + when Config::config(), State::state(), NewState::state(). + +-callback authenticate(Credential, State) + -> ignore + | {ok, Extra} + | {ok, Extra, AuthData} + | {continue, AuthCache} + | {continue, AuthData, AuthCache} + | {error, term()} + when Credential::map(), State::state(), Extra::extra(), AuthData::binary(), AuthCache::map(). + +-callback destroy(State) + -> ok + when State::state(). + +-callback import_users(Filename, State) + -> ok + | {error, term()} + when Filename::binary(), State::state(). + +-callback add_user(UserInfo, State) + -> {ok, User} + | {error, term()} + when UserInfo::user_info(), State::state(), User::user_info(). + +-callback delete_user(UserID, State) + -> ok + | {error, term()} + when UserID::binary(), State::state(). + +-callback update_user(UserID, UserInfo, State) + -> {ok, User} + | {error, term()} + when UserID::binary, UserInfo::map(), State::state(), User::user_info(). + +-callback list_users(State) + -> {ok, Users} + when State::state(), Users::[user_info()]. + +-optional_callbacks([ import_users/2 + , add_user/2 + , delete_user/2 + , update_user/3 + , list_users/1 + ]). + +%%------------------------------------------------------------------------------ +%% Hocon Schema +%%------------------------------------------------------------------------------ + +roots() -> [{authentication, fun authentication/1}]. + +fields(_) -> []. + +authentication(type) -> + {ok, Refs} = get_refs(), + hoconsc:union([hoconsc:array(hoconsc:union(Refs)) | Refs]); +authentication(default) -> []; +authentication(_) -> undefined. + +%%------------------------------------------------------------------------------ +%% Callbacks of config handler +%%------------------------------------------------------------------------------ + +pre_config_update(UpdateReq, OldConfig) -> + case do_pre_config_update(UpdateReq, to_list(OldConfig)) of + {error, Reason} -> {error, Reason}; + {ok, NewConfig} -> {ok, may_to_map(NewConfig)} + end. + +do_pre_config_update({create_authenticator, _ChainName, Config}, OldConfig) -> + {ok, OldConfig ++ [Config]}; +do_pre_config_update({delete_authenticator, _ChainName, AuthenticatorID}, OldConfig) -> + NewConfig = lists:filter(fun(OldConfig0) -> + AuthenticatorID =/= generate_id(OldConfig0) + end, OldConfig), + {ok, NewConfig}; +do_pre_config_update({update_authenticator, _ChainName, AuthenticatorID, Config}, OldConfig) -> + NewConfig = lists:map(fun(OldConfig0) -> + case AuthenticatorID =:= generate_id(OldConfig0) of + true -> maps:merge(OldConfig0, Config); + false -> OldConfig0 + end + end, OldConfig), + {ok, NewConfig}; +do_pre_config_update({move_authenticator, _ChainName, AuthenticatorID, Position}, OldConfig) -> + case split_by_id(AuthenticatorID, OldConfig) of + {error, Reason} -> {error, Reason}; + {ok, Part1, [Found | Part2]} -> + case Position of + <<"top">> -> + {ok, [Found | Part1] ++ Part2}; + <<"bottom">> -> + {ok, Part1 ++ Part2 ++ [Found]}; + <<"before:", Before/binary>> -> + case split_by_id(Before, Part1 ++ Part2) of + {error, Reason} -> + {error, Reason}; + {ok, NPart1, [NFound | NPart2]} -> + {ok, NPart1 ++ [Found, NFound | NPart2]} + end; + _ -> + {error, {invalid_parameter, position}} + end + end. + +post_config_update(UpdateReq, NewConfig, OldConfig, AppEnvs) -> + do_post_config_update(UpdateReq, check_config(to_list(NewConfig)), OldConfig, AppEnvs). + +do_post_config_update({create_authenticator, ChainName, Config}, _NewConfig, _OldConfig, _AppEnvs) -> + NConfig = check_config(Config), + _ = create_chain(ChainName), + create_authenticator(ChainName, NConfig); + +do_post_config_update({delete_authenticator, ChainName, AuthenticatorID}, _NewConfig, _OldConfig, _AppEnvs) -> + delete_authenticator(ChainName, AuthenticatorID); + +do_post_config_update({update_authenticator, ChainName, AuthenticatorID, _Config}, NewConfig, _OldConfig, _AppEnvs) -> + [Config] = lists:filter(fun(NewConfig0) -> + AuthenticatorID =:= generate_id(NewConfig0) + end, NewConfig), + NConfig = check_config(Config), + update_authenticator(ChainName, AuthenticatorID, NConfig); + +do_post_config_update({move_authenticator, ChainName, AuthenticatorID, Position}, _NewConfig, _OldConfig, _AppEnvs) -> + NPosition = case Position of + <<"top">> -> top; + <<"bottom">> -> bottom; + <<"before:", Before/binary>> -> + {before, Before} + end, + move_authenticator(ChainName, AuthenticatorID, NPosition). + +check_config(Config) -> + #{authentication := CheckedConfig} = hocon_schema:check_plain(emqx_authentication, + #{<<"authentication">> => Config}, #{nullable => true, atom_key => true}), + CheckedConfig. + +%%------------------------------------------------------------------------------ +%% Authenticate +%%------------------------------------------------------------------------------ + +authenticate(#{listener := Listener, protocol := Protocol} = Credential, _AuthResult) -> + case ets:lookup(?CHAINS_TAB, Listener) of + [#chain{authenticators = Authenticators}] when Authenticators =/= [] -> + do_authenticate(Authenticators, Credential); + _ -> + case ets:lookup(?CHAINS_TAB, global_chain(Protocol)) of + [#chain{authenticators = Authenticators}] when Authenticators =/= [] -> + do_authenticate(Authenticators, Credential); + _ -> + ignore + end + end. + +do_authenticate([], _) -> + {stop, {error, not_authorized}}; +do_authenticate([#authenticator{provider = Provider, state = State} | More], Credential) -> + case Provider:authenticate(Credential, State) of + ignore -> + do_authenticate(More, Credential); + Result -> + %% {ok, Extra} + %% {ok, Extra, AuthData} + %% {continue, AuthCache} + %% {continue, AuthData, AuthCache} + %% {error, Reason} + {stop, Result} + end. + +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + +initialize_authentication(_, []) -> + ok; +initialize_authentication(ChainName, AuthenticatorsConfig) -> + _ = create_chain(ChainName), + CheckedConfig = check_config(to_list(AuthenticatorsConfig)), + lists:foreach(fun(AuthenticatorConfig) -> + case create_authenticator(ChainName, AuthenticatorConfig) of + {ok, _} -> + ok; + {error, Reason} -> + ?LOG(error, "Failed to create authenticator '~s': ~p", [generate_id(AuthenticatorConfig), Reason]) + end + end, CheckedConfig). + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +stop() -> + gen_server:stop(?MODULE). + +get_refs() -> + gen_server:call(?MODULE, get_refs). + +add_provider(AuthNType, Provider) -> + gen_server:call(?MODULE, {add_provider, AuthNType, Provider}). + +remove_provider(AuthNType) -> + gen_server:call(?MODULE, {remove_provider, AuthNType}). + +create_chain(Name) -> + gen_server:call(?MODULE, {create_chain, Name}). + +delete_chain(Name) -> + gen_server:call(?MODULE, {delete_chain, Name}). + +lookup_chain(Name) -> + gen_server:call(?MODULE, {lookup_chain, Name}). + +list_chains() -> + Chains = ets:tab2list(?CHAINS_TAB), + {ok, [serialize_chain(Chain) || Chain <- Chains]}. + +create_authenticator(ChainName, Config) -> + gen_server:call(?MODULE, {create_authenticator, ChainName, Config}). + +delete_authenticator(ChainName, AuthenticatorID) -> + gen_server:call(?MODULE, {delete_authenticator, ChainName, AuthenticatorID}). + +update_authenticator(ChainName, AuthenticatorID, Config) -> + gen_server:call(?MODULE, {update_authenticator, ChainName, AuthenticatorID, Config}). + +lookup_authenticator(ChainName, AuthenticatorID) -> + case ets:lookup(?CHAINS_TAB, ChainName) of + [] -> + {error, {not_found, {chain, ChainName}}}; + [#chain{authenticators = Authenticators}] -> + case lists:keyfind(AuthenticatorID, #authenticator.id, Authenticators) of + false -> + {error, {not_found, {authenticator, AuthenticatorID}}}; + Authenticator -> + {ok, serialize_authenticator(Authenticator)} + end + end. + +list_authenticators(ChainName) -> + case ets:lookup(?CHAINS_TAB, ChainName) of + [] -> + {error, {not_found, {chain, ChainName}}}; + [#chain{authenticators = Authenticators}] -> + {ok, serialize_authenticators(Authenticators)} + end. + +move_authenticator(ChainName, AuthenticatorID, Position) -> + gen_server:call(?MODULE, {move_authenticator, ChainName, AuthenticatorID, Position}). + +import_users(ChainName, AuthenticatorID, Filename) -> + gen_server:call(?MODULE, {import_users, ChainName, AuthenticatorID, Filename}). + +add_user(ChainName, AuthenticatorID, UserInfo) -> + gen_server:call(?MODULE, {add_user, ChainName, AuthenticatorID, UserInfo}). + +delete_user(ChainName, AuthenticatorID, UserID) -> + gen_server:call(?MODULE, {delete_user, ChainName, AuthenticatorID, UserID}). + +update_user(ChainName, AuthenticatorID, UserID, NewUserInfo) -> + gen_server:call(?MODULE, {update_user, ChainName, AuthenticatorID, UserID, NewUserInfo}). + +lookup_user(ChainName, AuthenticatorID, UserID) -> + gen_server:call(?MODULE, {lookup_user, ChainName, AuthenticatorID, UserID}). + +%% TODO: Support pagination +list_users(ChainName, AuthenticatorID) -> + gen_server:call(?MODULE, {list_users, ChainName, AuthenticatorID}). + +generate_id(#{mechanism := Mechanism0, backend := Backend0}) -> + Mechanism = atom_to_binary(Mechanism0), + Backend = atom_to_binary(Backend0), + <>; +generate_id(#{mechanism := Mechanism}) -> + atom_to_binary(Mechanism); +generate_id(#{<<"mechanism">> := Mechanism, <<"backend">> := Backend}) -> + <>; +generate_id(#{<<"mechanism">> := Mechanism}) -> + Mechanism. + +%%-------------------------------------------------------------------- +%% gen_server callbacks +%%-------------------------------------------------------------------- + +init(_Opts) -> + _ = ets:new(?CHAINS_TAB, [ named_table, set, public + , {keypos, #chain.name} + , {read_concurrency, true}]), + ok = emqx_config_handler:add_handler([authentication], ?MODULE), + ok = emqx_config_handler:add_handler([listeners, '?', '?', authentication], ?MODULE), + {ok, #{hooked => false, providers => #{}}}. + +handle_call({add_provider, AuthNType, Provider}, _From, #{providers := Providers} = State) -> + reply(ok, State#{providers := Providers#{AuthNType => Provider}}); + +handle_call({remove_provider, AuthNType}, _From, #{providers := Providers} = State) -> + reply(ok, State#{providers := maps:remove(AuthNType, Providers)}); + +handle_call(get_refs, _From, #{providers := Providers} = State) -> + Refs = lists:foldl(fun({_, Provider}, Acc) -> + Acc ++ Provider:refs() + end, [], maps:to_list(Providers)), + reply({ok, Refs}, State); + +handle_call({create_chain, Name}, _From, State) -> + case ets:member(?CHAINS_TAB, Name) of + true -> + reply({error, {already_exists, {chain, Name}}}, State); + false -> + Chain = #chain{name = Name, + authenticators = []}, + true = ets:insert(?CHAINS_TAB, Chain), + reply({ok, serialize_chain(Chain)}, State) + end; + +handle_call({delete_chain, Name}, _From, State) -> + case ets:lookup(?CHAINS_TAB, Name) of + [] -> + reply({error, {not_found, {chain, Name}}}, State); + [#chain{authenticators = Authenticators}] -> + _ = [do_delete_authenticator(Authenticator) || Authenticator <- Authenticators], + true = ets:delete(?CHAINS_TAB, Name), + reply(ok, may_unhook(State)) + end; + +handle_call({lookup_chain, Name}, _From, State) -> + case ets:lookup(?CHAINS_TAB, Name) of + [] -> + reply({error, {not_found, {chain, Name}}}, State); + [Chain] -> + reply({ok, serialize_chain(Chain)}, State) + end; + +handle_call({create_authenticator, ChainName, Config}, _From, #{providers := Providers} = State) -> + UpdateFun = + fun(#chain{authenticators = Authenticators} = Chain) -> + AuthenticatorID = generate_id(Config), + case lists:keymember(AuthenticatorID, #authenticator.id, Authenticators) of + true -> + {error, {already_exists, {authenticator, AuthenticatorID}}}; + false -> + case do_create_authenticator(ChainName, AuthenticatorID, Config, Providers) of + {ok, Authenticator} -> + NAuthenticators = Authenticators ++ [Authenticator], + true = ets:insert(?CHAINS_TAB, Chain#chain{authenticators = NAuthenticators}), + {ok, serialize_authenticator(Authenticator)}; + {error, Reason} -> + {error, Reason} + end + end + end, + Reply = update_chain(ChainName, UpdateFun), + reply(Reply, may_hook(State)); + +handle_call({delete_authenticator, ChainName, AuthenticatorID}, _From, State) -> + UpdateFun = + fun(#chain{authenticators = Authenticators} = Chain) -> + case lists:keytake(AuthenticatorID, #authenticator.id, Authenticators) of + false -> + {error, {not_found, {authenticator, AuthenticatorID}}}; + {value, Authenticator, NAuthenticators} -> + _ = do_delete_authenticator(Authenticator), + true = ets:insert(?CHAINS_TAB, Chain#chain{authenticators = NAuthenticators}), + ok + end + end, + Reply = update_chain(ChainName, UpdateFun), + reply(Reply, may_unhook(State)); + +handle_call({update_authenticator, ChainName, AuthenticatorID, Config}, _From, State) -> + UpdateFun = + fun(#chain{authenticators = Authenticators} = Chain) -> + case lists:keyfind(AuthenticatorID, #authenticator.id, Authenticators) of + false -> + {error, {not_found, {authenticator, AuthenticatorID}}}; + #authenticator{provider = Provider, + state = #{version := Version} = ST} = Authenticator -> + case AuthenticatorID =:= generate_id(Config) of + true -> + Unique = <>, + case Provider:update(Config#{'_unique' => Unique}, ST) of + {ok, NewST} -> + NewAuthenticator = Authenticator#authenticator{state = switch_version(NewST)}, + NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators), + true = ets:insert(?CHAINS_TAB, Chain#chain{authenticators = NewAuthenticators}), + {ok, serialize_authenticator(NewAuthenticator)}; + {error, Reason} -> + {error, Reason} + end; + false -> + {error, mechanism_or_backend_change_is_not_alloed} + end + end + end, + Reply = update_chain(ChainName, UpdateFun), + reply(Reply, State); + +handle_call({move_authenticator, ChainName, AuthenticatorID, Position}, _From, State) -> + UpdateFun = + fun(#chain{authenticators = Authenticators} = Chain) -> + case do_move_authenticator(AuthenticatorID, Authenticators, Position) of + {ok, NAuthenticators} -> + true = ets:insert(?CHAINS_TAB, Chain#chain{authenticators = NAuthenticators}), + ok; + {error, Reason} -> + {error, Reason} + end + end, + Reply = update_chain(ChainName, UpdateFun), + reply(Reply, State); + +handle_call({import_users, ChainName, AuthenticatorID, Filename}, _From, State) -> + Reply = call_authenticator(ChainName, AuthenticatorID, import_users, [Filename]), + reply(Reply, State); + +handle_call({add_user, ChainName, AuthenticatorID, UserInfo}, _From, State) -> + Reply = call_authenticator(ChainName, AuthenticatorID, add_user, [UserInfo]), + reply(Reply, State); + +handle_call({delete_user, ChainName, AuthenticatorID, UserID}, _From, State) -> + Reply = call_authenticator(ChainName, AuthenticatorID, delete_user, [UserID]), + reply(Reply, State); + +handle_call({update_user, ChainName, AuthenticatorID, UserID, NewUserInfo}, _From, State) -> + Reply = call_authenticator(ChainName, AuthenticatorID, update_user, [UserID, NewUserInfo]), + reply(Reply, State); + +handle_call({lookup_user, ChainName, AuthenticatorID, UserID}, _From, State) -> + Reply = call_authenticator(ChainName, AuthenticatorID, lookup_user, [UserID]), + reply(Reply, State); + +handle_call({list_users, ChainName, AuthenticatorID}, _From, State) -> + Reply = call_authenticator(ChainName, AuthenticatorID, list_users, []), + reply(Reply, State); + +handle_call(Req, _From, State) -> + ?LOG(error, "Unexpected call: ~p", [Req]), + {reply, ignored, State}. + +handle_cast(Req, State) -> + ?LOG(error, "Unexpected case: ~p", [Req]), + {noreply, State}. + +handle_info(Info, State) -> + ?LOG(error, "Unexpected info: ~p", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + emqx_config_handler:remove_handler([authentication]), + emqx_config_handler:remove_handler([listeners, '?', '?', authentication]), + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +reply(Reply, State) -> + {reply, Reply, State}. + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +split_by_id(ID, AuthenticatorsConfig) -> + case lists:foldl( + fun(C, {P1, P2, F0}) -> + F = case ID =:= generate_id(C) of + true -> true; + false -> F0 + end, + case F of + false -> {[C | P1], P2, F}; + true -> {P1, [C | P2], F} + end + end, {[], [], false}, AuthenticatorsConfig) of + {_, _, false} -> + {error, {not_found, {authenticator, ID}}}; + {Part1, Part2, true} -> + {ok, lists:reverse(Part1), lists:reverse(Part2)} + end. + +global_chain(mqtt) -> + <<"mqtt:global">>; +global_chain('mqtt-sn') -> + <<"mqtt-sn:global">>; +global_chain(coap) -> + <<"coap:global">>; +global_chain(lwm2m) -> + <<"lwm2m:global">>; +global_chain(stomp) -> + <<"stomp:global">>; +global_chain(_) -> + <<"unknown:global">>. + +may_hook(#{hooked := false} = State) -> + case lists:any(fun(#chain{authenticators = []}) -> false; + (_) -> true + end, ets:tab2list(?CHAINS_TAB)) of + true -> + _ = emqx:hook('client.authenticate', {emqx_authentication, authenticate, []}), + State#{hooked => true}; + false -> + State + end; +may_hook(State) -> + State. + +may_unhook(#{hooked := true} = State) -> + case lists:all(fun(#chain{authenticators = []}) -> true; + (_) -> false + end, ets:tab2list(?CHAINS_TAB)) of + true -> + _ = emqx:unhook('client.authenticate', {emqx_authentication, authenticate, []}), + State#{hooked => false}; + false -> + State + end; +may_unhook(State) -> + State. + +do_create_authenticator(ChainName, AuthenticatorID, #{enable := Enable} = Config, Providers) -> + case maps:get(authn_type(Config), Providers, undefined) of + undefined -> + {error, no_available_provider}; + Provider -> + Unique = <>, + case Provider:create(Config#{'_unique' => Unique}) of + {ok, State} -> + Authenticator = #authenticator{id = AuthenticatorID, + provider = Provider, + enable = Enable, + state = switch_version(State)}, + {ok, Authenticator}; + {error, Reason} -> + {error, Reason} + end + end. + +do_delete_authenticator(#authenticator{provider = Provider, state = State}) -> + _ = Provider:destroy(State), + ok. + +replace_authenticator(ID, Authenticator, Authenticators) -> + lists:keyreplace(ID, #authenticator.id, Authenticators, Authenticator). + +do_move_authenticator(ID, Authenticators, Position) -> + case lists:keytake(ID, #authenticator.id, Authenticators) of + false -> + {error, {not_found, {authenticator, ID}}}; + {value, Authenticator, NAuthenticators} -> + case Position of + top -> + {ok, [Authenticator | NAuthenticators]}; + bottom -> + {ok, NAuthenticators ++ [Authenticator]}; + {before, ID0} -> + insert(Authenticator, NAuthenticators, ID0, []) + end + end. + +insert(_, [], ID, _) -> + {error, {not_found, {authenticator, ID}}}; +insert(Authenticator, [#authenticator{id = ID} | _] = Authenticators, ID, Acc) -> + {ok, lists:reverse(Acc) ++ [Authenticator | Authenticators]}; +insert(Authenticator, [Authenticator0 | More], ID, Acc) -> + insert(Authenticator, More, ID, [Authenticator0 | Acc]). + +update_chain(ChainName, UpdateFun) -> + case ets:lookup(?CHAINS_TAB, ChainName) of + [] -> + {error, {not_found, {chain, ChainName}}}; + [Chain] -> + UpdateFun(Chain) + end. + +call_authenticator(ChainName, AuthenticatorID, Func, Args) -> + UpdateFun = + fun(#chain{authenticators = Authenticators}) -> + case lists:keyfind(AuthenticatorID, #authenticator.id, Authenticators) of + false -> + {error, {not_found, {authenticator, AuthenticatorID}}}; + #authenticator{provider = Provider, state = State} -> + case erlang:function_exported(Provider, Func, length(Args) + 1) of + true -> + erlang:apply(Provider, Func, Args ++ [State]); + false -> + {error, unsupported_feature} + end + end + end, + update_chain(ChainName, UpdateFun). + +serialize_chain(#chain{name = Name, + authenticators = Authenticators}) -> + #{ name => Name + , authenticators => serialize_authenticators(Authenticators) + }. + +serialize_authenticators(Authenticators) -> + [serialize_authenticator(Authenticator) || Authenticator <- Authenticators]. + +serialize_authenticator(#authenticator{id = ID, + provider = Provider, + enable = Enable, + state = State}) -> + #{ id => ID + , provider => Provider + , enable => Enable + , state => State + }. + +switch_version(State = #{version := ?VER_1}) -> + State#{version := ?VER_2}; +switch_version(State = #{version := ?VER_2}) -> + State#{version := ?VER_1}; +switch_version(State) -> + State#{version => ?VER_1}. + +authn_type(#{mechanism := Mechanism, backend := Backend}) -> + {Mechanism, Backend}; +authn_type(#{mechanism := Mechanism}) -> + Mechanism. + +may_to_map([L]) -> + L; +may_to_map(L) -> + L. + +to_list(undefined) -> + []; +to_list(M) when M =:= #{} -> + []; +to_list(M) when is_map(M) -> + [M]; +to_list(L) when is_list(L) -> + L. diff --git a/apps/emqx/src/emqx_broker_sup.erl b/apps/emqx/src/emqx_broker_sup.erl index 69df72408..a479e9ff1 100644 --- a/apps/emqx/src/emqx_broker_sup.erl +++ b/apps/emqx/src/emqx_broker_sup.erl @@ -43,6 +43,14 @@ init([]) -> type => worker, modules => [emqx_shared_sub]}, + %% Authentication + AuthN = #{id => authn, + start => {emqx_authentication, start_link, []}, + restart => permanent, + shutdown => 2000, + type => worker, + modules => [emqx_authentication]}, + %% Broker helper Helper = #{id => helper, start => {emqx_broker_helper, start_link, []}, @@ -51,5 +59,5 @@ init([]) -> type => worker, modules => [emqx_broker_helper]}, - {ok, {{one_for_all, 0, 1}, [BrokerPool, SharedSub, Helper]}}. + {ok, {{one_for_all, 0, 1}, [BrokerPool, SharedSub, AuthN, Helper]}}. diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl index f64ffabcb..d92f1d35a 100644 --- a/apps/emqx/src/emqx_config_handler.erl +++ b/apps/emqx/src/emqx_config_handler.erl @@ -138,6 +138,8 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. +deep_put_handler([], Handlers, Mod) when is_map(Handlers) -> + {ok, Handlers#{?MOD => Mod}}; deep_put_handler([], _Handlers, Mod) -> {ok, #{?MOD => Mod}}; deep_put_handler([?WKEY | KeyPath], Handlers, Mod) -> diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index a91651c6c..7b1d6b0dd 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -252,11 +252,15 @@ do_start_listener(quic, ListenerName, #{bind := ListenOn} = Opts) -> {ok, {skipped, quic_app_missing}} end. +delete_authentication(Type, ListenerName, _Conf) -> + emqx_authentication:delete_chain(atom_to_binary(listener_id(Type, ListenerName))). + %% Update the listeners at runtime post_config_update(_Req, NewListeners, OldListeners, _AppEnvs) -> #{added := Added, removed := Removed, changed := Updated} = diff_listeners(NewListeners, OldListeners), perform_listener_changes(fun stop_listener/3, Removed), + perform_listener_changes(fun delete_authentication/3, Removed), perform_listener_changes(fun start_listener/3, Added), perform_listener_changes(fun restart_listener/3, Updated). diff --git a/apps/emqx/src/emqx_metrics.erl b/apps/emqx/src/emqx_metrics.erl index 736bb05b0..282b8b5f3 100644 --- a/apps/emqx/src/emqx_metrics.erl +++ b/apps/emqx/src/emqx_metrics.erl @@ -22,8 +22,6 @@ -include("logger.hrl"). -include("types.hrl"). -include("emqx_mqtt.hrl"). --include("emqx.hrl"). - -export([ start_link/0 , stop/0 diff --git a/apps/emqx/src/emqx_schema.erl b/apps/emqx/src/emqx_schema.erl index 0189a468b..01989c5a1 100644 --- a/apps/emqx/src/emqx_schema.erl +++ b/apps/emqx/src/emqx_schema.erl @@ -94,7 +94,8 @@ roots() -> "stats", "sysmon", "alarm", - "authorization" + "authorization", + {"authentication", sc(hoconsc:lazy(hoconsc:array(map())), #{})} ]. fields("stats") -> @@ -819,6 +820,10 @@ mqtt_listener() -> sc(duration(), #{}) } + , {"authentication", + sc(hoconsc:lazy(hoconsc:array(map())), + #{}) + } ]. base_listener() -> diff --git a/apps/emqx_authn/etc/emqx_authn.conf b/apps/emqx_authn/etc/emqx_authn.conf index 59f4aa9ee..d1d3d16f8 100644 --- a/apps/emqx_authn/etc/emqx_authn.conf +++ b/apps/emqx_authn/etc/emqx_authn.conf @@ -1,37 +1,6 @@ -authentication { - enable = false - authenticators = [ - # { - # name: "authenticator1" - # mechanism: password-based - # server_type: built-in-database - # user_id_type: clientid - # }, - # { - # name: "authenticator2" - # mechanism: password-based - # server_type: mongodb - # server: "127.0.0.1:27017" - # database: mqtt - # collection: users - # selector: { - # username: "${mqtt-username}" - # } - # password_hash_field: password_hash - # salt_field: salt - # password_hash_algorithm: sha256 - # salt_position: prefix - # }, - # { - # name: "authenticator 3" - # mechanism: password-based - # server_type: redis - # server: "127.0.0.1:6379" - # password: "public" - # database: 0 - # query: "HMGET ${mqtt-username} password_hash salt" - # password_hash_algorithm: sha256 - # salt_position: prefix - # } - ] -} +# authentication: { +# mechanism: password-based +# backend: built-in-database +# user_id_type: clientid +# } + diff --git a/apps/emqx_authn/include/emqx_authn.hrl b/apps/emqx_authn/include/emqx_authn.hrl index c5a392fd0..bdf93204a 100644 --- a/apps/emqx_authn/include/emqx_authn.hrl +++ b/apps/emqx_authn/include/emqx_authn.hrl @@ -15,24 +15,11 @@ %%-------------------------------------------------------------------- -define(APP, emqx_authn). --define(CHAIN, <<"mqtt">>). --define(VER_1, <<"1">>). --define(VER_2, <<"2">>). +-define(AUTHN, emqx_authentication). + +-define(GLOBAL, <<"mqtt:global">>). -define(RE_PLACEHOLDER, "\\$\\{[a-z0-9\\-]+\\}"). --record(authenticator, - { id :: binary() - , name :: binary() - , provider :: module() - , state :: map() - }). - --record(chain, - { id :: binary() - , authenticators :: [{binary(), binary(), #authenticator{}}] - , created_at :: integer() - }). - -define(AUTH_SHARD, emqx_authn_shard). diff --git a/apps/emqx_authn/rebar.config b/apps/emqx_authn/rebar.config index 32b5a43e0..73696b033 100644 --- a/apps/emqx_authn/rebar.config +++ b/apps/emqx_authn/rebar.config @@ -1,6 +1,4 @@ -{deps, [ - {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}} -]}. +{deps, []}. {edoc_opts, [{preprocess, true}]}. {erl_opts, [warn_unused_vars, diff --git a/apps/emqx_authn/src/emqx_authn.erl b/apps/emqx_authn/src/emqx_authn.erl index 1034682e5..3ab05e6b0 100644 --- a/apps/emqx_authn/src/emqx_authn.erl +++ b/apps/emqx_authn/src/emqx_authn.erl @@ -15,640 +15,3 @@ %%-------------------------------------------------------------------- -module(emqx_authn). - --behaviour(gen_server). - --behaviour(emqx_config_handler). - --include("emqx_authn.hrl"). --include_lib("emqx/include/logger.hrl"). - --export([ pre_config_update/2 - , post_config_update/4 - , update_config/2 - ]). - --export([ enable/0 - , disable/0 - , is_enabled/0 - ]). - --export([authenticate/2]). - --export([ start_link/0 - , stop/0 - ]). - --export([ create_chain/1 - , delete_chain/1 - , lookup_chain/1 - , list_chains/0 - , create_authenticator/2 - , delete_authenticator/2 - , update_authenticator/3 - , update_or_create_authenticator/3 - , lookup_authenticator/2 - , list_authenticators/1 - , move_authenticator/3 - ]). - --export([ import_users/3 - , add_user/3 - , delete_user/3 - , update_user/4 - , lookup_user/3 - , list_users/2 - ]). - -%% gen_server callbacks --export([ init/1 - , handle_call/3 - , handle_cast/2 - , handle_info/2 - , terminate/2 - , code_change/3 - ]). - --define(CHAIN_TAB, emqx_authn_chain). - -%%------------------------------------------------------------------------------ -%% APIs -%%------------------------------------------------------------------------------ - -pre_config_update({enable, Enable}, _OldConfig) -> - {ok, Enable}; -pre_config_update({create_authenticator, Config}, OldConfig) -> - {ok, OldConfig ++ [Config]}; -pre_config_update({delete_authenticator, ID}, OldConfig) -> - case lookup_authenticator(?CHAIN, ID) of - {error, Reason} -> {error, Reason}; - {ok, #{name := Name}} -> - NewConfig = lists:filter(fun(#{<<"name">> := N}) -> - N =/= Name - end, OldConfig), - {ok, NewConfig} - end; -pre_config_update({update_authenticator, ID, Config}, OldConfig) -> - case lookup_authenticator(?CHAIN, ID) of - {error, Reason} -> {error, Reason}; - {ok, #{name := Name}} -> - NewConfig = lists:map(fun(#{<<"name">> := N} = C) -> - case N =:= Name of - true -> Config; - false -> C - end - end, OldConfig), - {ok, NewConfig} - end; -pre_config_update({update_or_create_authenticator, ID, Config}, OldConfig) -> - case lookup_authenticator(?CHAIN, ID) of - {error, _Reason} -> OldConfig ++ [Config]; - {ok, #{name := Name}} -> - NewConfig = lists:map(fun(#{<<"name">> := N} = C) -> - case N =:= Name of - true -> Config; - false -> C - end - end, OldConfig), - {ok, NewConfig} - end; -pre_config_update({move_authenticator, ID, Position}, OldConfig) -> - case lookup_authenticator(?CHAIN, ID) of - {error, Reason} -> {error, Reason}; - {ok, #{name := Name}} -> - {ok, Found, Part1, Part2} = split_by_name(Name, OldConfig), - case Position of - <<"top">> -> - {ok, [Found | Part1] ++ Part2}; - <<"bottom">> -> - {ok, Part1 ++ Part2 ++ [Found]}; - Before -> - case binary:split(Before, <<":">>, [global]) of - [<<"before">>, ID0] -> - case lookup_authenticator(?CHAIN, ID0) of - {error, Reason} -> {error, Reason}; - {ok, #{name := Name1}} -> - {ok, NFound, NPart1, NPart2} = split_by_name(Name1, Part1 ++ Part2), - {ok, NPart1 ++ [Found, NFound | NPart2]} - end; - _ -> - {error, {invalid_parameter, position}} - end - end - end. - -post_config_update({enable, true}, _NewConfig, _OldConfig, _AppEnvs) -> - emqx_authn:enable(); -post_config_update({enable, false}, _NewConfig, _OldConfig, _AppEnvs) -> - emqx_authn:disable(); -post_config_update({create_authenticator, #{<<"name">> := Name}}, NewConfig, _OldConfig, _AppEnvs) -> - case lists:filter( - fun(#{name := N}) -> - N =:= Name - end, NewConfig) of - [Config] -> - create_authenticator(?CHAIN, Config); - [_Config | _] -> - {error, name_has_be_used} - end; -post_config_update({delete_authenticator, ID}, _NewConfig, _OldConfig, _AppEnvs) -> - case delete_authenticator(?CHAIN, ID) of - ok -> ok; - {error, Reason} -> throw(Reason) - end; -post_config_update({update_authenticator, ID, #{<<"name">> := Name}}, NewConfig, _OldConfig, _AppEnvs) -> - case lists:filter( - fun(#{name := N}) -> - N =:= Name - end, NewConfig) of - [Config] -> - update_authenticator(?CHAIN, ID, Config); - [_Config | _] -> - {error, name_has_be_used} - end; -post_config_update({update_or_create_authenticator, ID, #{<<"name">> := Name}}, NewConfig, _OldConfig, _AppEnvs) -> - case lists:filter( - fun(#{name := N}) -> - N =:= Name - end, NewConfig) of - [Config] -> - update_or_create_authenticator(?CHAIN, ID, Config); - [_Config | _] -> - {error, name_has_be_used} - end; -post_config_update({move_authenticator, ID, Position}, _NewConfig, _OldConfig, _AppEnvs) -> - NPosition = case Position of - <<"top">> -> top; - <<"bottom">> -> bottom; - Before -> - case binary:split(Before, <<":">>, [global]) of - [<<"before">>, ID0] -> - {before, ID0}; - _ -> - {error, {invalid_parameter, position}} - end - end, - move_authenticator(?CHAIN, ID, NPosition). - -update_config(Path, ConfigRequest) -> - emqx:update_config(Path, ConfigRequest, #{rawconf_with_defaults => true}). - -enable() -> - case emqx:hook('client.authenticate', {?MODULE, authenticate, []}) of - ok -> ok; - {error, already_exists} -> ok - end. - -disable() -> - emqx:unhook('client.authenticate', {?MODULE, authenticate, []}), - ok. - -is_enabled() -> - Callbacks = emqx_hooks:lookup('client.authenticate'), - lists:any(fun({callback, {?MODULE, authenticate, []}, _, _}) -> - true; - (_) -> - false - end, Callbacks). - -authenticate(Credential, _AuthResult) -> - case ets:lookup(?CHAIN_TAB, ?CHAIN) of - [#chain{authenticators = Authenticators}] -> - do_authenticate(Authenticators, Credential); - [] -> - {stop, {error, not_authorized}} - end. - -do_authenticate([], _) -> - {stop, {error, not_authorized}}; -do_authenticate([{_, _, #authenticator{provider = Provider, state = State}} | More], Credential) -> - case Provider:authenticate(Credential, State) of - ignore -> - do_authenticate(More, Credential); - Result -> - %% {ok, Extra} - %% {ok, Extra, AuthData} - %% {ok, MetaData} - %% {continue, AuthCache} - %% {continue, AuthData, AuthCache} - %% {error, Reason} - {stop, Result} - end. - -start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). - -stop() -> - gen_server:stop(?MODULE). - -create_chain(#{id := ID}) -> - gen_server:call(?MODULE, {create_chain, ID}). - -delete_chain(ID) -> - gen_server:call(?MODULE, {delete_chain, ID}). - -lookup_chain(ID) -> - gen_server:call(?MODULE, {lookup_chain, ID}). - -list_chains() -> - Chains = ets:tab2list(?CHAIN_TAB), - {ok, [serialize_chain(Chain) || Chain <- Chains]}. - -create_authenticator(ChainID, Config) -> - gen_server:call(?MODULE, {create_authenticator, ChainID, Config}). - -delete_authenticator(ChainID, AuthenticatorID) -> - gen_server:call(?MODULE, {delete_authenticator, ChainID, AuthenticatorID}). - -update_authenticator(ChainID, AuthenticatorID, Config) -> - gen_server:call(?MODULE, {update_authenticator, ChainID, AuthenticatorID, Config}). - -update_or_create_authenticator(ChainID, AuthenticatorID, Config) -> - gen_server:call(?MODULE, {update_or_create_authenticator, ChainID, AuthenticatorID, Config}). - -lookup_authenticator(ChainID, AuthenticatorID) -> - case ets:lookup(?CHAIN_TAB, ChainID) of - [] -> - {error, {not_found, {chain, ChainID}}}; - [#chain{authenticators = Authenticators}] -> - case lists:keyfind(AuthenticatorID, 1, Authenticators) of - false -> - {error, {not_found, {authenticator, AuthenticatorID}}}; - {_, _, Authenticator} -> - {ok, serialize_authenticator(Authenticator)} - end - end. - -list_authenticators(ChainID) -> - case ets:lookup(?CHAIN_TAB, ChainID) of - [] -> - {error, {not_found, {chain, ChainID}}}; - [#chain{authenticators = Authenticators}] -> - {ok, serialize_authenticators(Authenticators)} - end. - -move_authenticator(ChainID, AuthenticatorID, Position) -> - gen_server:call(?MODULE, {move_authenticator, ChainID, AuthenticatorID, Position}). - -import_users(ChainID, AuthenticatorID, Filename) -> - gen_server:call(?MODULE, {import_users, ChainID, AuthenticatorID, Filename}). - -add_user(ChainID, AuthenticatorID, UserInfo) -> - gen_server:call(?MODULE, {add_user, ChainID, AuthenticatorID, UserInfo}). - -delete_user(ChainID, AuthenticatorID, UserID) -> - gen_server:call(?MODULE, {delete_user, ChainID, AuthenticatorID, UserID}). - -update_user(ChainID, AuthenticatorID, UserID, NewUserInfo) -> - gen_server:call(?MODULE, {update_user, ChainID, AuthenticatorID, UserID, NewUserInfo}). - -lookup_user(ChainID, AuthenticatorID, UserID) -> - gen_server:call(?MODULE, {lookup_user, ChainID, AuthenticatorID, UserID}). - -%% TODO: Support pagination -list_users(ChainID, AuthenticatorID) -> - gen_server:call(?MODULE, {list_users, ChainID, AuthenticatorID}). - -%%-------------------------------------------------------------------- -%% gen_server callbacks -%%-------------------------------------------------------------------- - -init(_Opts) -> - _ = ets:new(?CHAIN_TAB, [ named_table, set, public - , {keypos, #chain.id} - , {read_concurrency, true}]), - {ok, #{}}. - -handle_call({create_chain, ID}, _From, State) -> - case ets:member(?CHAIN_TAB, ID) of - true -> - reply({error, {already_exists, {chain, ID}}}, State); - false -> - Chain = #chain{id = ID, - authenticators = [], - created_at = erlang:system_time(millisecond)}, - true = ets:insert(?CHAIN_TAB, Chain), - reply({ok, serialize_chain(Chain)}, State) - end; - -handle_call({delete_chain, ID}, _From, State) -> - case ets:lookup(?CHAIN_TAB, ID) of - [] -> - reply({error, {not_found, {chain, ID}}}, State); - [#chain{authenticators = Authenticators}] -> - _ = [do_delete_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators], - true = ets:delete(?CHAIN_TAB, ID), - reply(ok, State) - end; - -handle_call({lookup_chain, ID}, _From, State) -> - case ets:lookup(?CHAIN_TAB, ID) of - [] -> - reply({error, {not_found, {chain, ID}}}, State); - [Chain] -> - reply({ok, serialize_chain(Chain)}, State) - end; - -handle_call({create_authenticator, ChainID, #{name := Name} = Config}, _From, State) -> - UpdateFun = - fun(#chain{authenticators = Authenticators} = Chain) -> - case lists:keymember(Name, 2, Authenticators) of - true -> - {error, name_has_be_used}; - false -> - AlreadyExist = fun(ID) -> - lists:keymember(ID, 1, Authenticators) - end, - AuthenticatorID = gen_id(AlreadyExist), - case do_create_authenticator(ChainID, AuthenticatorID, Config) of - {ok, Authenticator} -> - NAuthenticators = Authenticators ++ [{AuthenticatorID, Name, Authenticator}], - true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}), - {ok, serialize_authenticator(Authenticator)}; - {error, Reason} -> - {error, Reason} - end - end - end, - Reply = update_chain(ChainID, UpdateFun), - reply(Reply, State); - -handle_call({delete_authenticator, ChainID, AuthenticatorID}, _From, State) -> - UpdateFun = - fun(#chain{authenticators = Authenticators} = Chain) -> - case lists:keytake(AuthenticatorID, 1, Authenticators) of - false -> - {error, {not_found, {authenticator, AuthenticatorID}}}; - {value, {_, _, Authenticator}, NAuthenticators} -> - _ = do_delete_authenticator(Authenticator), - true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}), - ok - end - end, - Reply = update_chain(ChainID, UpdateFun), - reply(Reply, State); - -handle_call({update_authenticator, ChainID, AuthenticatorID, Config}, _From, State) -> - Reply = update_or_create_authenticator(ChainID, AuthenticatorID, Config, false), - reply(Reply, State); - -handle_call({update_or_create_authenticator, ChainID, AuthenticatorID, Config}, _From, State) -> - Reply = update_or_create_authenticator(ChainID, AuthenticatorID, Config, true), - reply(Reply, State); - -handle_call({move_authenticator, ChainID, AuthenticatorID, Position}, _From, State) -> - UpdateFun = - fun(#chain{authenticators = Authenticators} = Chain) -> - case do_move_authenticator(AuthenticatorID, Authenticators, Position) of - {ok, NAuthenticators} -> - true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}), - ok; - {error, Reason} -> - {error, Reason} - end - end, - Reply = update_chain(ChainID, UpdateFun), - reply(Reply, State); - -handle_call({import_users, ChainID, AuthenticatorID, Filename}, _From, State) -> - Reply = call_authenticator(ChainID, AuthenticatorID, import_users, [Filename]), - reply(Reply, State); - -handle_call({add_user, ChainID, AuthenticatorID, UserInfo}, _From, State) -> - Reply = call_authenticator(ChainID, AuthenticatorID, add_user, [UserInfo]), - reply(Reply, State); - -handle_call({delete_user, ChainID, AuthenticatorID, UserID}, _From, State) -> - Reply = call_authenticator(ChainID, AuthenticatorID, delete_user, [UserID]), - reply(Reply, State); - -handle_call({update_user, ChainID, AuthenticatorID, UserID, NewUserInfo}, _From, State) -> - Reply = call_authenticator(ChainID, AuthenticatorID, update_user, [UserID, NewUserInfo]), - reply(Reply, State); - -handle_call({lookup_user, ChainID, AuthenticatorID, UserID}, _From, State) -> - Reply = call_authenticator(ChainID, AuthenticatorID, lookup_user, [UserID]), - reply(Reply, State); - -handle_call({list_users, ChainID, AuthenticatorID}, _From, State) -> - Reply = call_authenticator(ChainID, AuthenticatorID, list_users, []), - reply(Reply, State); - -handle_call(Req, _From, State) -> - ?LOG(error, "Unexpected call: ~p", [Req]), - {reply, ignored, State}. - -handle_cast(Req, State) -> - ?LOG(error, "Unexpected case: ~p", [Req]), - {noreply, State}. - -handle_info(Info, State) -> - ?LOG(error, "Unexpected info: ~p", [Info]), - {noreply, State}. - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -reply(Reply, State) -> - {reply, Reply, State}. - -%%------------------------------------------------------------------------------ -%% Internal functions -%%------------------------------------------------------------------------------ - -authenticator_provider(#{mechanism := 'password-based', server_type := 'built-in-database'}) -> - emqx_authn_mnesia; -authenticator_provider(#{mechanism := 'password-based', server_type := 'mysql'}) -> - emqx_authn_mysql; -authenticator_provider(#{mechanism := 'password-based', server_type := 'pgsql'}) -> - emqx_authn_pgsql; -authenticator_provider(#{mechanism := 'password-based', server_type := 'mongodb'}) -> - emqx_authn_mongodb; -authenticator_provider(#{mechanism := 'password-based', server_type := 'redis'}) -> - emqx_authn_redis; -authenticator_provider(#{mechanism := 'password-based', server_type := 'http-server'}) -> - emqx_authn_http; -authenticator_provider(#{mechanism := jwt}) -> - emqx_authn_jwt; -authenticator_provider(#{mechanism := scram, server_type := 'built-in-database'}) -> - emqx_enhanced_authn_scram_mnesia. - -gen_id(AlreadyExist) -> - ID = list_to_binary(emqx_rule_id:gen()), - case AlreadyExist(ID) of - true -> gen_id(AlreadyExist); - false -> ID - end. - -switch_version(State = #{version := ?VER_1}) -> - State#{version := ?VER_2}; -switch_version(State = #{version := ?VER_2}) -> - State#{version := ?VER_1}; -switch_version(State) -> - State#{version => ?VER_1}. - -split_by_name(Name, Config) -> - {Part1, Part2, true} = lists:foldl( - fun(#{<<"name">> := N} = C, {P1, P2, F0}) -> - F = case N =:= Name of - true -> true; - false -> F0 - end, - case F of - false -> {[C | P1], P2, F}; - true -> {P1, [C | P2], F} - end - end, {[], [], false}, Config), - [Found | NPart2] = lists:reverse(Part2), - {ok, Found, lists:reverse(Part1), NPart2}. - -do_create_authenticator(ChainID, AuthenticatorID, #{name := Name} = Config) -> - Provider = authenticator_provider(Config), - Unique = <>, - case Provider:create(Config#{'_unique' => Unique}) of - {ok, State} -> - Authenticator = #authenticator{id = AuthenticatorID, - name = Name, - provider = Provider, - state = switch_version(State)}, - {ok, Authenticator}; - {error, Reason} -> - {error, Reason} - end. - -do_delete_authenticator(#authenticator{provider = Provider, state = State}) -> - _ = Provider:destroy(State), - ok. - -update_or_create_authenticator(ChainID, AuthenticatorID, #{name := NewName} = Config, CreateWhenNotFound) -> - UpdateFun = - fun(#chain{authenticators = Authenticators} = Chain) -> - case lists:keytake(AuthenticatorID, 1, Authenticators) of - false -> - case CreateWhenNotFound of - true -> - case lists:keymember(NewName, 2, Authenticators) of - true -> - {error, name_has_be_used}; - false -> - case do_create_authenticator(ChainID, AuthenticatorID, Config) of - {ok, Authenticator} -> - NAuthenticators = Authenticators ++ [{AuthenticatorID, NewName, Authenticator}], - true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}), - {ok, serialize_authenticator(Authenticator)}; - {error, Reason} -> - {error, Reason} - end - end; - false -> - {error, {not_found, {authenticator, AuthenticatorID}}} - end; - {value, - {_, _, #authenticator{provider = Provider, - state = #{version := Version} = State} = Authenticator}, - Others} -> - case lists:keymember(NewName, 2, Others) of - true -> - {error, name_has_be_used}; - false -> - case (NewProvider = authenticator_provider(Config)) =:= Provider of - true -> - Unique = <>, - case Provider:update(Config#{'_unique' => Unique}, State) of - {ok, NewState} -> - NewAuthenticator = Authenticator#authenticator{name = NewName, - state = switch_version(NewState)}, - NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators), - true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}), - {ok, serialize_authenticator(NewAuthenticator)}; - {error, Reason} -> - {error, Reason} - end; - false -> - Unique = <>, - case NewProvider:create(Config#{'_unique' => Unique}) of - {ok, NewState} -> - NewAuthenticator = Authenticator#authenticator{name = NewName, - provider = NewProvider, - state = switch_version(NewState)}, - NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators), - true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}), - _ = Provider:destroy(State), - {ok, serialize_authenticator(NewAuthenticator)}; - {error, Reason} -> - {error, Reason} - end - end - end - end - end, - update_chain(ChainID, UpdateFun). - -replace_authenticator(ID, #authenticator{name = Name} = Authenticator, Authenticators) -> - lists:keyreplace(ID, 1, Authenticators, {ID, Name, Authenticator}). - -do_move_authenticator(AuthenticatorID, Authenticators, Position) when is_binary(AuthenticatorID) -> - case lists:keytake(AuthenticatorID, 1, Authenticators) of - false -> - {error, {not_found, {authenticator, AuthenticatorID}}}; - {value, Authenticator, NAuthenticators} -> - do_move_authenticator(Authenticator, NAuthenticators, Position) - end; - -do_move_authenticator(Authenticator, Authenticators, top) -> - {ok, [Authenticator | Authenticators]}; -do_move_authenticator(Authenticator, Authenticators, bottom) -> - {ok, Authenticators ++ [Authenticator]}; -do_move_authenticator(Authenticator, Authenticators, {before, ID}) -> - insert(Authenticator, Authenticators, ID, []). - -insert(_, [], ID, _) -> - {error, {not_found, {authenticator, ID}}}; -insert(Authenticator, [{ID, _, _} | _] = Authenticators, ID, Acc) -> - {ok, lists:reverse(Acc) ++ [Authenticator | Authenticators]}; -insert(Authenticator, [{_, _, _} = Authenticator0 | More], ID, Acc) -> - insert(Authenticator, More, ID, [Authenticator0 | Acc]). - -update_chain(ChainID, UpdateFun) -> - case ets:lookup(?CHAIN_TAB, ChainID) of - [] -> - {error, {not_found, {chain, ChainID}}}; - [Chain] -> - UpdateFun(Chain) - end. - -call_authenticator(ChainID, AuthenticatorID, Func, Args) -> - UpdateFun = - fun(#chain{authenticators = Authenticators}) -> - case lists:keyfind(AuthenticatorID, 1, Authenticators) of - false -> - {error, {not_found, {authenticator, AuthenticatorID}}}; - {_, _, #authenticator{provider = Provider, state = State}} -> - case erlang:function_exported(Provider, Func, length(Args) + 1) of - true -> - erlang:apply(Provider, Func, Args ++ [State]); - false -> - {error, unsupported_feature} - end - end - end, - update_chain(ChainID, UpdateFun). - -serialize_chain(#chain{id = ID, - authenticators = Authenticators, - created_at = CreatedAt}) -> - #{id => ID, - authenticators => serialize_authenticators(Authenticators), - created_at => CreatedAt}. - -serialize_authenticators(Authenticators) -> - [serialize_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators]. - -serialize_authenticator(#authenticator{id = ID, - name = Name, - provider = Provider, - state = State}) -> - #{id => ID, name => Name, provider => Provider, state => State}. diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index 5f2b96b57..3303f88ef 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -22,37 +22,36 @@ -export([ api_spec/0 , authentication/2 - , authenticators/2 - , authenticators2/2 + , authentication2/2 + , authentication3/2 + , authentication4/2 , move/2 + , move2/2 , import_users/2 , users/2 , users2/2 ]). --define(EXAMPLE_1, #{name => <<"example 1">>, - mechanism => <<"password-based">>, - server_type => <<"built-in-database">>, - user_id_type => <<"username">>, +-define(EXAMPLE_1, #{mechanism => <<"password-based">>, + backend => <<"built-in-database">>, + query => <<"SELECT password_hash from built-in-database WHERE username = ${username}">>, password_hash_algorithm => #{ name => <<"sha256">> }}). --define(EXAMPLE_2, #{name => <<"example 2">>, - mechanism => <<"password-based">>, - server_type => <<"http-server">>, +-define(EXAMPLE_2, #{mechanism => <<"password-based">>, + backend => <<"http-server">>, method => <<"post">>, url => <<"http://localhost:80/login">>, headers => #{ <<"content-type">> => <<"application/json">> }, - form_data => #{ + body => #{ <<"username">> => <<"${mqtt-username}">>, <<"password">> => <<"${mqtt-password}">> }}). --define(EXAMPLE_3, #{name => <<"example 3">>, - mechanism => <<"jwt">>, +-define(EXAMPLE_3, #{mechanism => <<"jwt">>, use_jwks => false, algorithm => <<"hmac-based">>, secret => <<"mysecret">>, @@ -61,9 +60,8 @@ <<"username">> => <<"${mqtt-username}">> }}). --define(EXAMPLE_4, #{name => <<"example 4">>, - mechanism => <<"password-based">>, - server_type => <<"mongodb">>, +-define(EXAMPLE_4, #{mechanism => <<"password-based">>, + backend => <<"mongodb">>, server => <<"127.0.0.1:27017">>, database => example, collection => users, @@ -76,9 +74,8 @@ salt_position => <<"prefix">> }). --define(EXAMPLE_5, #{name => <<"example 5">>, - mechanism => <<"password-based">>, - server_type => <<"redis">>, +-define(EXAMPLE_5, #{mechanism => <<"password-based">>, + backend => <<"redis">>, server => <<"127.0.0.1:6379">>, database => 0, query => <<"HMGET ${mqtt-username} password_hash salt">>, @@ -86,10 +83,53 @@ salt_position => <<"prefix">> }). +-define(INSTANCE_EXAMPLE_1, maps:merge(?EXAMPLE_1, #{id => <<"password-based:built-in-database">>, + enable => true})). + +-define(INSTANCE_EXAMPLE_2, maps:merge(?EXAMPLE_2, #{id => <<"password-based:http-server">>, + connect_timeout => 5000, + enable_pipelining => true, + headers => #{ + <<"accept">> => <<"application/json">>, + <<"cache-control">> => <<"no-cache">>, + <<"connection">> => <<"keepalive">>, + <<"content-type">> => <<"application/json">>, + <<"keep-alive">> => <<"timeout=5">> + }, + max_retries => 5, + pool_size => 8, + request_timeout => 5000, + retry_interval => 1000, + enable => true})). + +-define(INSTANCE_EXAMPLE_3, maps:merge(?EXAMPLE_3, #{id => <<"jwt">>, + enable => true})). + +-define(INSTANCE_EXAMPLE_4, maps:merge(?EXAMPLE_4, #{id => <<"password-based:mongodb">>, + mongo_type => <<"single">>, + pool_size => 8, + ssl => #{ + enable => false + }, + topology => #{ + max_overflow => 8, + pool_size => 8 + }, + enable => true})). + +-define(INSTANCE_EXAMPLE_5, maps:merge(?EXAMPLE_5, #{id => <<"password-based:redis">>, + auto_reconnect => true, + redis_type => single, + pool_size => 8, + ssl => #{ + enable => false + }, + enable => true})). + -define(ERR_RESPONSE(Desc), #{description => Desc, content => #{ 'application/json' => #{ - schema => minirest:ref(<<"error">>), + schema => minirest:ref(<<"Error">>), examples => #{ example1 => #{ summary => <<"Not Found">>, @@ -107,9 +147,11 @@ api_spec() -> {[ authentication_api() - , authenticators_api() - , authenticators_api2() + , authentication_api2() , move_api() + , authentication_api3() + , authentication_api4() + , move_api2() , import_users_api() , users_api() , users2_api() @@ -117,350 +159,473 @@ api_spec() -> authentication_api() -> Metadata = #{ - post => #{ - description => "Enable or disbale authentication", - requestBody => #{ - content => #{ - 'application/json' => #{ - schema => #{ - type => object, - required => [enable], - properties => #{ - enable => #{ - type => boolean, - example => true - } - } - } - } - } - }, - responses => #{ - <<"204">> => #{ - description => <<"No Content">> - }, - <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>) - } - }, - get => #{ - description => "Get status of authentication", - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => #{ - type => object, - properties => #{ - enabled => #{ - type => boolean, - example => true - } - } - } - } - } - } - } - } + post => create_authenticator_api_spec(), + get => list_authenticators_api_spec() }, {"/authentication", Metadata, authentication}. -authenticators_api() -> +authentication_api2() -> Metadata = #{ - post => #{ - description => "Create authenticator", - requestBody => #{ - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"authenticator">>), - examples => #{ - default => #{ - summary => <<"Default">>, - value => emqx_json:encode(?EXAMPLE_1) - }, - http => #{ - summary => <<"Authentication provided by HTTP Server">>, - value => emqx_json:encode(?EXAMPLE_2) - }, - jwt => #{ - summary => <<"JWT Authentication">>, - value => emqx_json:encode(?EXAMPLE_3) - }, - mongodb => #{ - summary => <<"Authentication with MongoDB">>, - value => emqx_json:encode(?EXAMPLE_4) - }, - redis => #{ - summary => <<"Authentication with Redis">>, - value => emqx_json:encode(?EXAMPLE_5) - } - } - } - } - }, - responses => #{ - <<"201">> => #{ - description => <<"Created">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"returned_authenticator">>), - examples => #{ - %% TODO: return full content - example1 => #{ - summary => <<"Example 1">>, - value => emqx_json:encode(maps:put(id, <<"example 1">>, ?EXAMPLE_1)) - }, - example2 => #{ - summary => <<"Example 2">>, - value => emqx_json:encode(maps:put(id, <<"example 2">>, ?EXAMPLE_2)) - }, - example3 => #{ - summary => <<"Example 3">>, - value => emqx_json:encode(maps:put(id, <<"example 3">>, ?EXAMPLE_3)) - }, - example4 => #{ - summary => <<"Example 4">>, - value => emqx_json:encode(maps:put(id, <<"example 4">>, ?EXAMPLE_4)) - }, - example5 => #{ - summary => <<"Example 4">>, - value => emqx_json:encode(maps:put(id, <<"example 5">>, ?EXAMPLE_5)) - } - } - } - } - }, - <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), - <<"409">> => ?ERR_RESPONSE(<<"Conflict">>) - } - }, - get => #{ - description => "List authenticators", - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => #{ - type => array, - items => minirest:ref(<<"returned_authenticator">>) - }, - examples => #{ - example1 => #{ - summary => <<"Example 1">>, - value => emqx_json:encode([ maps:put(id, <<"example 1">>, ?EXAMPLE_1) - , maps:put(id, <<"example 2">>, ?EXAMPLE_2) - , maps:put(id, <<"example 3">>, ?EXAMPLE_3) - , maps:put(id, <<"example 4">>, ?EXAMPLE_4) - , maps:put(id, <<"example 5">>, ?EXAMPLE_5) - ]) - } - } - } - } - } - } - } + get => list_authenticator_api_spec(), + put => update_authenticator_api_spec(), + delete => delete_authenticator_api_spec() }, - {"/authentication/authenticators", Metadata, authenticators}. + {"/authentication/:id", Metadata, authentication2}. -authenticators_api2() -> +authentication_api3() -> Metadata = #{ - get => #{ - description => "Get authenicator by id", - parameters => [ - #{ - name => id, - in => path, - schema => #{ - type => string - }, - required => true - } - ], - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"returned_authenticator">>), - examples => #{ - example1 => #{ - summary => <<"Example 1">>, - value => emqx_json:encode(maps:put(id, <<"example 1">>, ?EXAMPLE_1)) - }, - example2 => #{ - summary => <<"Example 2">>, - value => emqx_json:encode(maps:put(id, <<"example 2">>, ?EXAMPLE_2)) - }, - example3 => #{ - summary => <<"Example 3">>, - value => emqx_json:encode(maps:put(id, <<"example 3">>, ?EXAMPLE_3)) - }, - example4 => #{ - summary => <<"Example 4">>, - value => emqx_json:encode(maps:put(id, <<"example 4">>, ?EXAMPLE_4)) - }, - example5 => #{ - summary => <<"Example 5">>, - value => emqx_json:encode(maps:put(id, <<"example 5">>, ?EXAMPLE_5)) - } - } - } - } - }, - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) - } - }, - put => #{ - description => "Update authenticator", - parameters => [ - #{ - name => id, - in => path, - schema => #{ - type => string - }, - required => true - } - ], - requestBody => #{ - content => #{ - 'application/json' => #{ - schema => #{ - oneOf => [ minirest:ref(<<"password_based">>) - , minirest:ref(<<"jwt">>) - , minirest:ref(<<"scram">>) - ] - }, - examples => #{ - example1 => #{ - summary => <<"Example 1">>, - value => emqx_json:encode(?EXAMPLE_1) - }, - example2 => #{ - summary => <<"Example 2">>, - value => emqx_json:encode(?EXAMPLE_2) - } - } - } - } - }, - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"returned_authenticator">>), - examples => #{ - example1 => #{ - summary => <<"Example 1">>, - value => emqx_json:encode(maps:put(id, <<"example 1">>, ?EXAMPLE_1)) - }, - example2 => #{ - summary => <<"Example 2">>, - value => emqx_json:encode(maps:put(id, <<"example 2">>, ?EXAMPLE_2)) - }, - example3 => #{ - summary => <<"Example 3">>, - value => emqx_json:encode(maps:put(id, <<"example 3">>, ?EXAMPLE_3)) - }, - example4 => #{ - summary => <<"Example 4">>, - value => emqx_json:encode(maps:put(id, <<"example 4">>, ?EXAMPLE_4)) - }, - example5 => #{ - summary => <<"Example 5">>, - value => emqx_json:encode(maps:put(id, <<"example 5">>, ?EXAMPLE_5)) - } - } - } - } - }, - <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>), - <<"409">> => ?ERR_RESPONSE(<<"Conflict">>) - } - }, - delete => #{ - description => "Delete authenticator", - parameters => [ - #{ - name => id, - in => path, - schema => #{ - type => string - }, - required => true - } - ], - responses => #{ - <<"204">> => #{ - description => <<"No Content">> - }, - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) - } - } + post => create_authenticator_api_spec2(), + get => list_authenticators_api_spec2() }, - {"/authentication/authenticators/:id", Metadata, authenticators2}. + {"/listeners/:listener_id/authentication", Metadata, authentication3}. + +authentication_api4() -> + Metadata = #{ + get => list_authenticator_api_spec2(), + put => update_authenticator_api_spec2(), + delete => delete_authenticator_api_spec2() + }, + {"/listeners/:listener_id/authentication/:id", Metadata, authentication4}. move_api() -> Metadata = #{ - post => #{ - description => "Move authenticator", - parameters => [ - #{ - name => id, - in => path, - schema => #{ - type => string - }, - required => true + post => move_authenticator_api_spec() + }, + {"/authentication/:id/move", Metadata, move}. + +move_api2() -> + Metadata = #{ + post => move_authenticator_api_spec2() + }, + {"/listeners/:listener_id/authentication/:id/move", Metadata, move2}. + +create_authenticator_api_spec() -> + #{ + description => "Create a authenticator for global authentication", + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"AuthenticatorConfig">>), + examples => #{ + default => #{ + summary => <<"Default">>, + value => emqx_json:encode(?EXAMPLE_1) + }, + http => #{ + summary => <<"Authentication provided by HTTP Server">>, + value => emqx_json:encode(?EXAMPLE_2) + }, + jwt => #{ + summary => <<"JWT Authentication">>, + value => emqx_json:encode(?EXAMPLE_3) + }, + mongodb => #{ + summary => <<"Authentication with MongoDB">>, + value => emqx_json:encode(?EXAMPLE_4) + }, + redis => #{ + summary => <<"Authentication with Redis">>, + value => emqx_json:encode(?EXAMPLE_5) + } + } } - ], - requestBody => #{ + } + }, + responses => #{ + <<"201">> => #{ + description => <<"Created">>, content => #{ 'application/json' => #{ - schema => #{ - oneOf => [ - #{ - type => object, - required => [position], - properties => #{ - position => #{ - type => string, - enum => [<<"top">>, <<"bottom">>], - example => <<"top">> - } - } - }, - #{ - type => object, - required => [position], - properties => #{ - position => #{ - type => string, - description => <<"before:">>, - example => <<"before:67e4c9d3">> - } - } - } - ] + schema => minirest:ref(<<"AuthenticatorInstance">>), + examples => #{ + example1 => #{ + summary => <<"Example 1">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_1) + }, + example2 => #{ + summary => <<"Example 2">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_2) + }, + example3 => #{ + summary => <<"Example 3">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_3) + }, + example4 => #{ + summary => <<"Example 4">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_4) + }, + example5 => #{ + summary => <<"Example 5">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_5) + } } } } }, - responses => #{ - <<"204">> => #{ - description => <<"No Content">> + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"409">> => ?ERR_RESPONSE(<<"Conflict">>) + } + }. + +create_authenticator_api_spec2() -> + Spec = create_authenticator_api_spec(), + Spec#{ + description => "Create a authenticator for listener", + parameters => [ + #{ + name => listener_id, + in => path, + schema => #{ + type => string }, - <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + required => true } + ] + }. + +list_authenticators_api_spec() -> + #{ + description => "List authenticators for global authentication", + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => #{ + type => array, + items => minirest:ref(<<"AuthenticatorInstance">>) + }, + examples => #{ + example => #{ + summary => <<"Example">>, + value => emqx_json:encode([ ?INSTANCE_EXAMPLE_1 + , ?INSTANCE_EXAMPLE_2 + , ?INSTANCE_EXAMPLE_3 + , ?INSTANCE_EXAMPLE_4 + , ?INSTANCE_EXAMPLE_5 + ])}}}}}}}. + +list_authenticators_api_spec2() -> + Spec = list_authenticators_api_spec(), + Spec#{ + description => "List authenticators for listener", + parameters => [ + #{ + name => listener_id, + in => path, + schema => #{ + type => string + }, + required => true + } + ] + }. + +list_authenticator_api_spec() -> + #{ + description => "Get authenticator by id", + parameters => [ + #{ + name => id, + in => path, + description => "ID of authenticator", + schema => #{ + type => string + }, + required => true + } + ], + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"AuthenticatorInstance">>), + examples => #{ + example1 => #{ + summary => <<"Example 1">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_1) + }, + example2 => #{ + summary => <<"Example 2">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_2) + }, + example3 => #{ + summary => <<"Example 3">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_3) + }, + example4 => #{ + summary => <<"Example 4">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_4) + }, + example5 => #{ + summary => <<"Example 5">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_5) + } + } + } + } + }, + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) } - }, - {"/authentication/authenticators/:id/move", Metadata, move}. + }. + +list_authenticator_api_spec2() -> + Spec = list_authenticator_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "ID of listener", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "ID of authenticator", + schema => #{ + type => string + }, + required => true + } + ] + }. + +update_authenticator_api_spec() -> + #{ + description => "Update authenticator", + parameters => [ + #{ + name => id, + in => path, + description => "ID of authenticator", + schema => #{ + type => string + }, + required => true + } + ], + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"AuthenticatorConfig">>), + examples => #{ + example1 => #{ + summary => <<"Example 1">>, + value => emqx_json:encode(?EXAMPLE_1) + }, + example2 => #{ + summary => <<"Example 2">>, + value => emqx_json:encode(?EXAMPLE_2) + }, + example3 => #{ + summary => <<"Example 3">>, + value => emqx_json:encode(?EXAMPLE_3) + }, + example4 => #{ + summary => <<"Example 4">>, + value => emqx_json:encode(?EXAMPLE_4) + }, + example5 => #{ + summary => <<"Example 5">>, + value => emqx_json:encode(?EXAMPLE_5) + } + } + } + } + }, + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"AuthenticatorInstance">>), + examples => #{ + example1 => #{ + summary => <<"Example 1">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_1) + }, + example2 => #{ + summary => <<"Example 2">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_2) + }, + example3 => #{ + summary => <<"Example 3">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_3) + }, + example4 => #{ + summary => <<"Example 4">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_4) + }, + example5 => #{ + summary => <<"Example 5">>, + value => emqx_json:encode(?INSTANCE_EXAMPLE_5) + } + } + } + } + }, + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>), + <<"409">> => ?ERR_RESPONSE(<<"Conflict">>) + } + }. + +update_authenticator_api_spec2() -> + Spec = update_authenticator_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "ID of listener", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "ID of authenticator", + schema => #{ + type => string + }, + required => true + } + ] + }. + +delete_authenticator_api_spec() -> + #{ + description => "Delete authenticator", + parameters => [ + #{ + name => id, + in => path, + description => "ID of authenticator", + schema => #{ + type => string + }, + required => true + } + ], + responses => #{ + <<"204">> => #{ + description => <<"No Content">> + }, + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + }. + +delete_authenticator_api_spec2() -> + Spec = delete_authenticator_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "ID of listener", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "ID of authenticator", + schema => #{ + type => string + }, + required => true + } + ] + }. + +move_authenticator_api_spec() -> + #{ + description => "Move authenticator", + parameters => [ + #{ + name => id, + in => path, + description => "ID of authenticator", + schema => #{ + type => string + }, + required => true + } + ], + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => #{ + oneOf => [ + #{ + type => object, + required => [position], + properties => #{ + position => #{ + type => string, + enum => [<<"top">>, <<"bottom">>], + example => <<"top">> + } + } + }, + #{ + type => object, + required => [position], + properties => #{ + position => #{ + type => string, + description => <<"before:">>, + example => <<"before:password-based:mysql">> + } + } + } + ] + } + } + } + }, + responses => #{ + <<"204">> => #{ + description => <<"No Content">> + }, + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + }. + +move_authenticator_api_spec2() -> + Spec = move_authenticator_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "ID of listener", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "ID of authenticator", + schema => #{ + type => string + }, + required => true + } + ] + }. import_users_api() -> Metadata = #{ @@ -470,6 +635,7 @@ import_users_api() -> #{ name => id, in => path, + description => "ID of authenticator", schema => #{ type => string }, @@ -500,7 +666,7 @@ import_users_api() -> } } }, - {"/authentication/authenticators/:id/import-users", Metadata, import_users}. + {"/authentication/:id/import_users", Metadata, import_users}. users_api() -> Metadata = #{ @@ -510,6 +676,7 @@ users_api() -> #{ name => id, in => path, + description => "ID of authenticator", schema => #{ type => string }, @@ -567,6 +734,7 @@ users_api() -> #{ name => id, in => path, + description => "ID of authenticator", schema => #{ type => string }, @@ -599,7 +767,7 @@ users_api() -> } } }, - {"/authentication/authenticators/:id/users", Metadata, users}. + {"/authentication/:id/users", Metadata, users}. users2_api() -> Metadata = #{ @@ -609,6 +777,7 @@ users2_api() -> #{ name => id, in => path, + description => "ID of authenticator", schema => #{ type => string }, @@ -672,6 +841,7 @@ users2_api() -> #{ name => id, in => path, + description => "ID of authenticator", schema => #{ type => string }, @@ -717,6 +887,7 @@ users2_api() -> #{ name => id, in => path, + description => "ID of authenticator", schema => #{ type => string }, @@ -739,17 +910,36 @@ users2_api() -> } } }, - {"/authentication/authenticators/:id/users/:user_id", Metadata, users2}. + {"/authentication/:id/users/:user_id", Metadata, users2}. definitions() -> - AuthenticatorDef = #{ - oneOf => [ minirest:ref(<<"password_based">>) - , minirest:ref(<<"jwt">>) - , minirest:ref(<<"scram">>) - ] + AuthenticatorConfigDef = #{ + allOf => [ + #{ + type => object, + properties => #{ + enable => #{ + type => boolean, + default => true, + example => true + } + } + }, + #{ + oneOf => [ minirest:ref(<<"PasswordBasedBuiltInDatabase">>) + , minirest:ref(<<"PasswordBasedMySQL">>) + , minirest:ref(<<"PasswordBasedPostgreSQL">>) + , minirest:ref(<<"PasswordBasedMongoDB">>) + , minirest:ref(<<"PasswordBasedRedis">>) + , minirest:ref(<<"PasswordBasedHTTPServer">>) + , minirest:ref(<<"JWT">>) + , minirest:ref(<<"SCRAMBuiltInDatabase">>) + ] + } + ] }, - ReturnedAuthenticatorDef = #{ + AuthenticatorInstanceDef = #{ allOf => [ #{ type => object, @@ -758,148 +948,49 @@ definitions() -> type => string } } - }, - #{ - oneOf => [ minirest:ref(<<"password_based">>) - , minirest:ref(<<"jwt">>) - , minirest:ref(<<"scram">>) - ] } - ] - }, - - PasswordBasedDef = #{ - allOf => [ - #{ - type => object, - required => [name, mechanism], - properties => #{ - name => #{ - type => string, - example => "exmaple" - }, - mechanism => #{ - type => string, - enum => [<<"password-based">>], - example => <<"password-based">> - } - } - }, - #{ - oneOf => [ minirest:ref(<<"password_based_built_in_database">>) - , minirest:ref(<<"password_based_mysql">>) - , minirest:ref(<<"password_based_pgsql">>) - , minirest:ref(<<"password_based_mongodb">>) - , minirest:ref(<<"password_based_redis">>) - , minirest:ref(<<"password_based_http_server">>) - ] - } - ] - }, - - JWTDef = #{ - type => object, - required => [name, mechanism], - properties => #{ - name => #{ - type => string, - example => "exmaple" - }, - mechanism => #{ - type => string, - enum => [<<"jwt">>], - example => <<"jwt">> - }, - use_jwks => #{ - type => boolean, - default => false, - example => false - }, - algorithm => #{ - type => string, - enum => [<<"hmac-based">>, <<"public-key">>], - default => <<"hmac-based">>, - example => <<"hmac-based">> - }, - secret => #{ - type => string - }, - secret_base64_encoded => #{ - type => boolean, - default => false - }, - certificate => #{ - type => string - }, - verify_claims => #{ - type => object, - additionalProperties => #{ - type => string - } - }, - ssl => minirest:ref(<<"ssl">>) - } - }, - - SCRAMDef = #{ - type => object, - required => [name, mechanism, server_type], - properties => #{ - name => #{ - type => string, - example => "exmaple" - }, - mechanism => #{ - type => string, - enum => [<<"scram">>], - example => <<"scram">> - }, - server_type => #{ - type => string, - enum => [<<"built-in-database">>], - default => <<"built-in-database">> - }, - algorithm => #{ - type => string, - enum => [<<"sha256">>, <<"sha512">>], - default => <<"sha256">> - }, - iteration_count => #{ - type => integer, - default => 4096 - } - } + ] ++ maps:get(allOf, AuthenticatorConfigDef) }, PasswordBasedBuiltInDatabaseDef = #{ type => object, - required => [server_type], + required => [mechanism, backend], properties => #{ - server_type => #{ + mechanism => #{ + type => string, + enum => [<<"password-based">>], + example => <<"password-based">> + }, + backend => #{ type => string, enum => [<<"built-in-database">>], example => <<"built-in-database">> }, - user_id_type => #{ + query => #{ type => string, - enum => [<<"username">>, <<"clientid">>], - default => <<"username">>, - example => <<"username">> + default => <<"SELECT password_hash from built-in-database WHERE username = ${username}">>, + example => <<"SELECT password_hash from built-in-database WHERE username = ${username}">> }, - password_hash_algorithm => minirest:ref(<<"password_hash_algorithm">>) + password_hash_algorithm => minirest:ref(<<"PasswordHashAlgorithm">>) } }, PasswordBasedMySQLDef = #{ type => object, - required => [ server_type + required => [ mechanism + , backend , server , database , username , password , query], properties => #{ - server_type => #{ + mechanism => #{ + type => string, + enum => [<<"password-based">>], + example => <<"password-based">> + }, + backend => #{ type => string, enum => [<<"mysql">>], example => <<"mysql">> @@ -925,7 +1016,7 @@ definitions() -> type => boolean, default => true }, - ssl => minirest:ref(<<"ssl">>), + ssl => minirest:ref(<<"SSL">>), password_hash_algorithm => #{ type => string, enum => [<<"plain">>, <<"md5">>, <<"sha">>, <<"sha256">>, <<"sha512">>, <<"bcrypt">>], @@ -948,19 +1039,25 @@ definitions() -> } }, - PasswordBasedPgSQLDef = #{ + PasswordBasedPostgreSQLDef = #{ type => object, - required => [ server_type + required => [ mechanism + , backend , server , database , username , password , query], properties => #{ - server_type => #{ + mechanism => #{ type => string, - enum => [<<"pgsql">>], - example => <<"pgsql">> + enum => [<<"password-based">>], + example => <<"password-based">> + }, + backend => #{ + type => string, + enum => [<<"postgresql">>], + example => <<"postgresql">> }, server => #{ type => string, @@ -1002,7 +1099,8 @@ definitions() -> PasswordBasedMongoDBDef = #{ type => object, - required => [ server_type + required => [ mechanism + , backend , server , servers , replica_set_name @@ -1014,10 +1112,15 @@ definitions() -> , password_hash_field ], properties => #{ - server_type => #{ + mechanism => #{ + type => string, + enum => [<<"password-based">>], + example => <<"password-based">> + }, + backend => #{ type => string, enum => [<<"mongodb">>], - example => [<<"mongodb">>] + example => <<"mongodb">> }, server => #{ description => <<"Mutually exclusive with the 'servers' field, only valid in standalone mode">>, @@ -1087,7 +1190,8 @@ definitions() -> PasswordBasedRedisDef = #{ type => object, - required => [ server_type + required => [ mechanism + , backend , server , servers , password @@ -1095,10 +1199,15 @@ definitions() -> , query ], properties => #{ - server_type => #{ + mechanism => #{ + type => string, + enum => [<<"password-based">>], + example => <<"password-based">> + }, + backend => #{ type => string, enum => [<<"redis">>], - example => [<<"redis">>] + example => <<"redis">> }, server => #{ description => <<"Mutually exclusive with the 'servers' field, only valid in standalone mode">>, @@ -1153,12 +1262,18 @@ definitions() -> PasswordBasedHTTPServerDef = #{ type => object, - required => [ server_type + required => [ mechanism + , backend , url - , form_data + , body ], properties => #{ - server_type => #{ + mechanism => #{ + type => string, + enum => [<<"password-based">>], + example => <<"password-based">> + }, + backend => #{ type => string, enum => [<<"http-server">>], example => <<"http-server">> @@ -1178,8 +1293,8 @@ definitions() -> type => string } }, - form_data => #{ - type => string + body => #{ + type => object }, connect_timeout => #{ type => integer, @@ -1208,6 +1323,72 @@ definitions() -> } }, + JWTDef = #{ + type => object, + required => [mechanism], + properties => #{ + mechanism => #{ + type => string, + enum => [<<"jwt">>], + example => <<"jwt">> + }, + use_jwks => #{ + type => boolean, + default => false, + example => false + }, + algorithm => #{ + type => string, + enum => [<<"hmac-based">>, <<"public-key">>], + default => <<"hmac-based">>, + example => <<"hmac-based">> + }, + secret => #{ + type => string + }, + secret_base64_encoded => #{ + type => boolean, + default => false + }, + certificate => #{ + type => string + }, + verify_claims => #{ + type => object, + additionalProperties => #{ + type => string + } + }, + ssl => minirest:ref(<<"SSL">>) + } + }, + + SCRAMBuiltInDatabaseDef = #{ + type => object, + required => [mechanism, backend], + properties => #{ + mechanism => #{ + type => string, + enum => [<<"scram">>], + example => <<"scram">> + }, + backend => #{ + type => string, + enum => [<<"built-in-database">>], + example => <<"built-in-database">> + }, + algorithm => #{ + type => string, + enum => [<<"sha256">>, <<"sha512">>], + default => <<"sha256">> + }, + iteration_count => #{ + type => integer, + default => 4096 + } + } + }, + PasswordHashAlgorithmDef = #{ type => object, required => [name], @@ -1273,93 +1454,92 @@ definitions() -> } }, - [ #{<<"authenticator">> => AuthenticatorDef} - , #{<<"returned_authenticator">> => ReturnedAuthenticatorDef} - , #{<<"password_based">> => PasswordBasedDef} - , #{<<"jwt">> => JWTDef} - , #{<<"scram">> => SCRAMDef} - , #{<<"password_based_built_in_database">> => PasswordBasedBuiltInDatabaseDef} - , #{<<"password_based_mysql">> => PasswordBasedMySQLDef} - , #{<<"password_based_pgsql">> => PasswordBasedPgSQLDef} - , #{<<"password_based_mongodb">> => PasswordBasedMongoDBDef} - , #{<<"password_based_redis">> => PasswordBasedRedisDef} - , #{<<"password_based_http_server">> => PasswordBasedHTTPServerDef} - , #{<<"password_hash_algorithm">> => PasswordHashAlgorithmDef} - , #{<<"ssl">> => SSLDef} - , #{<<"error">> => ErrorDef} + [ #{<<"AuthenticatorConfig">> => AuthenticatorConfigDef} + , #{<<"AuthenticatorInstance">> => AuthenticatorInstanceDef} + , #{<<"PasswordBasedBuiltInDatabase">> => PasswordBasedBuiltInDatabaseDef} + , #{<<"PasswordBasedMySQL">> => PasswordBasedMySQLDef} + , #{<<"PasswordBasedPostgreSQL">> => PasswordBasedPostgreSQLDef} + , #{<<"PasswordBasedMongoDB">> => PasswordBasedMongoDBDef} + , #{<<"PasswordBasedRedis">> => PasswordBasedRedisDef} + , #{<<"PasswordBasedHTTPServer">> => PasswordBasedHTTPServerDef} + , #{<<"JWT">> => JWTDef} + , #{<<"SCRAMBuiltInDatabase">> => SCRAMBuiltInDatabaseDef} + , #{<<"PasswordHashAlgorithm">> => PasswordHashAlgorithmDef} + , #{<<"SSL">> => SSLDef} + , #{<<"Error">> => ErrorDef} ]. authentication(post, #{body := Config}) -> - case Config of - #{<<"enable">> := Enable} -> - {ok, _} = emqx_authn:update_config([authentication, enable], {enable, Enable}), - {204}; - _ -> - serialize_error({missing_parameter, enable}) - end; + create_authenticator([authentication], ?GLOBAL, Config); + authentication(get, _Params) -> - Enabled = emqx_authn:is_enabled(), - {200, #{enabled => Enabled}}. + list_authenticators([authentication]). -authenticators(post, #{body := Config}) -> - case emqx_authn:update_config([authentication, authenticators], {create_authenticator, Config}) of - {ok, #{post_config_update := #{emqx_authn := #{id := ID, name := Name}}, - raw_config := RawConfig}} -> - [RawConfig1] = [RC || #{<<"name">> := N} = RC <- RawConfig, N =:= Name], - {200, RawConfig1#{id => ID}}; - {error, {_, _, Reason}} -> - serialize_error(Reason) - end; -authenticators(get, _Params) -> - RawConfig = get_raw_config([authentication, authenticators]), - {ok, Authenticators} = emqx_authn:list_authenticators(?CHAIN), - NAuthenticators = lists:zipwith(fun(#{<<"name">> := Name} = Config, #{id := ID, name := Name}) -> - Config#{id => ID} - end, RawConfig, Authenticators), - {200, NAuthenticators}. +authentication2(get, #{bindings := #{id := AuthenticatorID}}) -> + list_authenticator([authentication], AuthenticatorID); -authenticators2(get, #{bindings := #{id := AuthenticatorID}}) -> - case emqx_authn:lookup_authenticator(?CHAIN, AuthenticatorID) of - {ok, #{id := ID, name := Name}} -> - RawConfig = get_raw_config([authentication, authenticators]), - [RawConfig1] = [RC || #{<<"name">> := N} = RC <- RawConfig, N =:= Name], - {200, RawConfig1#{id => ID}}; +authentication2(put, #{bindings := #{id := AuthenticatorID}, body := Config}) -> + update_authenticator([authentication], ?GLOBAL, AuthenticatorID, Config); + +authentication2(delete, #{bindings := #{id := AuthenticatorID}}) -> + delete_authenticator([authentication], ?GLOBAL, AuthenticatorID). + +authentication3(post, #{bindings := #{listener_id := ListenerID}, body := Config}) -> + case find_listener(ListenerID) of + {ok, {Type, Name}} -> + create_authenticator([listeners, Type, Name, authentication], ListenerID, Config); {error, Reason} -> serialize_error(Reason) end; -authenticators2(put, #{bindings := #{id := AuthenticatorID}, body := Config}) -> - case emqx_authn:update_config([authentication, authenticators], - {update_or_create_authenticator, AuthenticatorID, Config}) of - {ok, #{post_config_update := #{emqx_authn := #{id := ID, name := Name}}, - raw_config := RawConfig}} -> - [RawConfig0] = [RC || #{<<"name">> := N} = RC <- RawConfig, N =:= Name], - {200, RawConfig0#{id => ID}}; - {error, {_, _, Reason}} -> - serialize_error(Reason) - end; -authenticators2(delete, #{bindings := #{id := AuthenticatorID}}) -> - case emqx_authn:update_config([authentication, authenticators], {delete_authenticator, AuthenticatorID}) of - {ok, _} -> - {204}; - {error, {_, _, Reason}} -> +authentication3(get, #{bindings := #{listener_id := ListenerID}}) -> + case find_listener(ListenerID) of + {ok, {Type, Name}} -> + list_authenticators([listeners, Type, Name, authentication]); + {error, Reason} -> serialize_error(Reason) end. -move(post, #{bindings := #{id := AuthenticatorID}, body := Body}) -> - case Body of - #{<<"position">> := Position} -> - case emqx_authn:update_config([authentication, authenticators], {move_authenticator, AuthenticatorID, Position}) of - {ok, _} -> {204}; - {error, {_, _, Reason}} -> serialize_error(Reason) - end; - _ -> - serialize_error({missing_parameter, position}) +authentication4(get, #{bindings := #{listener_id := ListenerID, id := AuthenticatorID}}) -> + case find_listener(ListenerID) of + {ok, {Type, Name}} -> + list_authenticator([listeners, Type, Name, authentication], AuthenticatorID); + {error, Reason} -> + serialize_error(Reason) + end; +authentication4(put, #{bindings := #{listener_id := ListenerID, id := AuthenticatorID}, body := Config}) -> + case find_listener(ListenerID) of + {ok, {Type, Name}} -> + update_authenticator([listeners, Type, Name, authentication], ListenerID, AuthenticatorID, Config); + {error, Reason} -> + serialize_error(Reason) + end; +authentication4(delete, #{bindings := #{listener_id := ListenerID, id := AuthenticatorID}}) -> + case find_listener(ListenerID) of + {ok, {Type, Name}} -> + delete_authenticator([listeners, Type, Name, authentication], ListenerID, AuthenticatorID); + {error, Reason} -> + serialize_error(Reason) end. +move(post, #{bindings := #{id := AuthenticatorID}, body := #{<<"position">> := Position}}) -> + move_authenitcator([authentication], ?GLOBAL, AuthenticatorID, Position); +move(post, #{bindings := #{id := _}, body := _}) -> + serialize_error({missing_parameter, position}). + +move2(post, #{bindings := #{listener_id := ListenerID, id := AuthenticatorID}, body := #{<<"position">> := Position}}) -> + case find_listener(ListenerID) of + {ok, {Type, Name}} -> + move_authenitcator([listeners, Type, Name, authentication], ListenerID, AuthenticatorID, Position); + {error, Reason} -> + serialize_error(Reason) + end; +move2(post, #{bindings := #{listener_id := _, id := _}, body := _}) -> + serialize_error({missing_parameter, position}). + import_users(post, #{bindings := #{id := AuthenticatorID}, body := Body}) -> case Body of #{<<"filename">> := Filename} -> - case emqx_authn:import_users(?CHAIN, AuthenticatorID, Filename) of + case ?AUTHN:import_users(?GLOBAL, AuthenticatorID, Filename) of ok -> {204}; {error, Reason} -> serialize_error(Reason) end; @@ -1371,9 +1551,9 @@ users(post, #{bindings := #{id := AuthenticatorID}, body := UserInfo}) -> case UserInfo of #{ <<"user_id">> := UserID, <<"password">> := Password} -> Superuser = maps:get(<<"superuser">>, UserInfo, false), - case emqx_authn:add_user(?CHAIN, AuthenticatorID, #{ user_id => UserID - , password => Password - , superuser => Superuser}) of + case ?AUTHN:add_user(?GLOBAL, AuthenticatorID, #{ user_id => UserID + , password => Password + , superuser => Superuser}) of {ok, User} -> {201, User}; {error, Reason} -> @@ -1385,7 +1565,7 @@ users(post, #{bindings := #{id := AuthenticatorID}, body := UserInfo}) -> serialize_error({missing_parameter, user_id}) end; users(get, #{bindings := #{id := AuthenticatorID}}) -> - case emqx_authn:list_users(?CHAIN, AuthenticatorID) of + case ?AUTHN:list_users(?GLOBAL, AuthenticatorID) of {ok, Users} -> {200, Users}; {error, Reason} -> @@ -1400,7 +1580,7 @@ users2(patch, #{bindings := #{id := AuthenticatorID, true -> serialize_error({missing_parameter, password}); false -> - case emqx_authn:update_user(?CHAIN, AuthenticatorID, UserID, UserInfo) of + case ?AUTHN:update_user(?GLOBAL, AuthenticatorID, UserID, UserInfo) of {ok, User} -> {200, User}; {error, Reason} -> @@ -1408,28 +1588,110 @@ users2(patch, #{bindings := #{id := AuthenticatorID, end end; users2(get, #{bindings := #{id := AuthenticatorID, user_id := UserID}}) -> - case emqx_authn:lookup_user(?CHAIN, AuthenticatorID, UserID) of + case ?AUTHN:lookup_user(?GLOBAL, AuthenticatorID, UserID) of {ok, User} -> {200, User}; {error, Reason} -> serialize_error(Reason) end; users2(delete, #{bindings := #{id := AuthenticatorID, user_id := UserID}}) -> - case emqx_authn:delete_user(?CHAIN, AuthenticatorID, UserID) of + case ?AUTHN:delete_user(?GLOBAL, AuthenticatorID, UserID) of ok -> {204}; {error, Reason} -> serialize_error(Reason) end. -get_raw_config(ConfKeyPath) -> - %% TODO: call emqx_config:get_raw(ConfKeyPath) directly +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +find_listener(ListenerID) -> + {Type, Name} = emqx_listeners:parse_listener_id(ListenerID), + case emqx_config:find([listeners, Type, Name]) of + {not_found, _, _} -> + {error, {not_found, {listener, ListenerID}}}; + {ok, _} -> + {ok, {Type, Name}} + end. + +create_authenticator(ConfKeyPath, ChainName, Config) -> + case update_config(ConfKeyPath, {create_authenticator, ChainName, Config}) of + {ok, #{post_config_update := #{?AUTHN := #{id := ID}}, + raw_config := AuthenticatorsConfig}} -> + {ok, AuthenticatorConfig} = find_config(ID, AuthenticatorsConfig), + {200, maps:put(id, ID, fill_defaults(AuthenticatorConfig))}; + {error, {_, _, Reason}} -> + serialize_error(Reason) + end. + +list_authenticators(ConfKeyPath) -> + AuthenticatorsConfig = get_raw_config_with_defaults(ConfKeyPath), + NAuthenticators = [maps:put(id, ?AUTHN:generate_id(AuthenticatorConfig), AuthenticatorConfig) + || AuthenticatorConfig <- AuthenticatorsConfig], + {200, NAuthenticators}. + +list_authenticator(ConfKeyPath, AuthenticatorID) -> + AuthenticatorsConfig = get_raw_config_with_defaults(ConfKeyPath), + case find_config(AuthenticatorID, AuthenticatorsConfig) of + {ok, AuthenticatorConfig} -> + {200, AuthenticatorConfig#{id => AuthenticatorID}}; + {error, Reason} -> + serialize_error(Reason) + end. + +update_authenticator(ConfKeyPath, ChainName, AuthenticatorID, Config) -> + case update_config(ConfKeyPath, + {update_authenticator, ChainName, AuthenticatorID, Config}) of + {ok, #{post_config_update := #{?AUTHN := #{id := ID}}, + raw_config := AuthenticatorsConfig}} -> + {ok, AuthenticatorConfig} = find_config(ID, AuthenticatorsConfig), + {200, maps:put(id, ID, fill_defaults(AuthenticatorConfig))}; + {error, {_, _, Reason}} -> + serialize_error(Reason) + end. + +delete_authenticator(ConfKeyPath, ChainName, AuthenticatorID) -> + case update_config(ConfKeyPath, {delete_authenticator, ChainName, AuthenticatorID}) of + {ok, _} -> + {204}; + {error, {_, _, Reason}} -> + serialize_error(Reason) + end. + +move_authenitcator(ConfKeyPath, ChainName, AuthenticatorID, Position) -> + case update_config(ConfKeyPath, {move_authenticator, ChainName, AuthenticatorID, Position}) of + {ok, _} -> + {204}; + {error, {_, _, Reason}} -> + serialize_error(Reason) + end. + +update_config(Path, ConfigRequest) -> + emqx:update_config(Path, ConfigRequest, #{rawconf_with_defaults => true}). + +get_raw_config_with_defaults(ConfKeyPath) -> NConfKeyPath = [atom_to_binary(Key, utf8) || Key <- ConfKeyPath], - emqx_map_lib:deep_get(NConfKeyPath, emqx_config:fill_defaults(emqx_config:get_raw([]))). + RawConfig = emqx_map_lib:deep_get(NConfKeyPath, emqx_config:get_raw([]), []), + to_list(fill_defaults(RawConfig)). + +find_config(AuthenticatorID, AuthenticatorsConfig) -> + case [AC || AC <- to_list(AuthenticatorsConfig), AuthenticatorID =:= ?AUTHN:generate_id(AC)] of + [] -> {error, {not_found, {authenticator, AuthenticatorID}}}; + [AuthenticatorConfig] -> {ok, AuthenticatorConfig} + end. + +fill_defaults(Config) -> + #{<<"authentication">> := CheckedConfig} = hocon_schema:check_plain( + ?AUTHN, #{<<"authentication">> => Config}, #{nullable => true, no_conversion => true}), + CheckedConfig. serialize_error({not_found, {authenticator, ID}}) -> {404, #{code => <<"NOT_FOUND">>, message => list_to_binary(io_lib:format("Authenticator '~s' does not exist", [ID]))}}; +serialize_error({not_found, {listener, ID}}) -> + {404, #{code => <<"NOT_FOUND">>, + message => list_to_binary(io_lib:format("Listener '~s' does not exist", [ID]))}}; serialize_error(name_has_be_used) -> {409, #{code => <<"ALREADY_EXISTS">>, message => <<"Name has be used">>}}; @@ -1446,3 +1708,8 @@ serialize_error({invalid_parameter, Name}) -> serialize_error(Reason) -> {400, #{code => <<"BAD_REQUEST">>, message => list_to_binary(io_lib:format("Todo: ~p", [Reason]))}}. + +to_list(M) when is_map(M) -> + [M]; +to_list(L) when is_list(L) -> + L. \ No newline at end of file diff --git a/apps/emqx_authn/src/emqx_authn_app.erl b/apps/emqx_authn/src/emqx_authn_app.erl index b7f409bc9..58470289a 100644 --- a/apps/emqx_authn/src/emqx_authn_app.erl +++ b/apps/emqx_authn/src/emqx_authn_app.erl @@ -17,7 +17,6 @@ -module(emqx_authn_app). -include("emqx_authn.hrl"). --include_lib("emqx/include/logger.hrl"). -behaviour(application). @@ -26,33 +25,45 @@ , stop/1 ]). +%%------------------------------------------------------------------------------ +%% APIs +%%------------------------------------------------------------------------------ + start(_StartType, _StartArgs) -> ok = ekka_rlog:wait_for_shards([?AUTH_SHARD], infinity), {ok, Sup} = emqx_authn_sup:start_link(), - emqx_config_handler:add_handler([authentication, authenticators], emqx_authn), - initialize(), + ok = add_providers(), + ok = initialize(), {ok, Sup}. stop(_State) -> + ok = remove_providers(), ok. +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +add_providers() -> + _ = [?AUTHN:add_provider(AuthNType, Provider) || {AuthNType, Provider} <- providers()], ok. + +remove_providers() -> + _ = [?AUTHN:remove_provider(AuthNType) || {AuthNType, _} <- providers()], ok. + initialize() -> - AuthNConfig = emqx:get_config([authentication], #{enable => false, - authenticators => []}), - initialize(AuthNConfig). - -initialize(#{enable := Enable, authenticators := AuthenticatorsConfig}) -> - {ok, _} = emqx_authn:create_chain(#{id => ?CHAIN}), - initialize_authenticators(AuthenticatorsConfig), - Enable =:= true andalso emqx_authn:enable(), + ?AUTHN:initialize_authentication(?GLOBAL, emqx:get_raw_config([authentication], [])), + lists:foreach(fun({ListenerID, ListenerConfig}) -> + ?AUTHN:initialize_authentication(atom_to_binary(ListenerID), maps:get(authentication, ListenerConfig, [])) + end, emqx_listeners:list()), ok. -initialize_authenticators([]) -> - ok; -initialize_authenticators([#{name := Name} = AuthenticatorConfig | More]) -> - case emqx_authn:create_authenticator(?CHAIN, AuthenticatorConfig) of - {ok, _} -> - initialize_authenticators(More); - {error, Reason} -> - ?LOG(error, "Failed to create authenticator '~s': ~p", [Name, Reason]) - end. +providers() -> + [ {{'password-based', 'built-in-database'}, emqx_authn_mnesia} + , {{'password-based', mysql}, emqx_authn_mysql} + , {{'password-based', posgresql}, emqx_authn_pgsql} + , {{'password-based', mongodb}, emqx_authn_mongodb} + , {{'password-based', redis}, emqx_authn_redis} + , {{'password-based', 'http-server'}, emqx_authn_http} + , {jwt, emqx_authn_jwt} + , {{scram, 'built-in-database'}, emqx_enhanced_authn_scram_mnesia} + ]. \ No newline at end of file diff --git a/apps/emqx_authn/src/emqx_authn_schema.erl b/apps/emqx_authn/src/emqx_authn_schema.erl index bceedb6bb..23e412088 100644 --- a/apps/emqx_authn/src/emqx_authn_schema.erl +++ b/apps/emqx_authn/src/emqx_authn_schema.erl @@ -16,56 +16,15 @@ -module(emqx_authn_schema). --include("emqx_authn.hrl"). -include_lib("typerefl/include/types.hrl"). --behaviour(hocon_schema). - --export([ namespace/0 - , roots/0 - , fields/1 +-export([ common_fields/0 ]). --export([ authenticator_name/1 - ]). - -%% Export it for emqx_gateway_schema module --export([ authenticators/1 - ]). - -namespace() -> authn. - -roots() -> [ "authentication" ]. - -fields("authentication") -> - [ {enable, fun enable/1} - , {authenticators, fun authenticators/1} +common_fields() -> + [ {enable, fun enable/1} ]. -authenticator_name(type) -> binary(); -authenticator_name(nullable) -> false; -authenticator_name(_) -> undefined. - enable(type) -> boolean(); -enable(default) -> false; +enable(default) -> true; enable(_) -> undefined. - -authenticators(type) -> - hoconsc:array({union, [ hoconsc:ref(emqx_authn_mnesia, config) - , hoconsc:ref(emqx_authn_mysql, config) - , hoconsc:ref(emqx_authn_pgsql, config) - , hoconsc:ref(emqx_authn_mongodb, standalone) - , hoconsc:ref(emqx_authn_mongodb, 'replica-set') - , hoconsc:ref(emqx_authn_mongodb, 'sharded-cluster') - , hoconsc:ref(emqx_authn_redis, standalone) - , hoconsc:ref(emqx_authn_redis, cluster) - , hoconsc:ref(emqx_authn_redis, sentinel) - , hoconsc:ref(emqx_authn_http, get) - , hoconsc:ref(emqx_authn_http, post) - , hoconsc:ref(emqx_authn_jwt, 'hmac-based') - , hoconsc:ref(emqx_authn_jwt, 'public-key') - , hoconsc:ref(emqx_authn_jwt, 'jwks') - , hoconsc:ref(emqx_enhanced_authn_scram_mnesia, config) - ]}); -authenticators(default) -> []; -authenticators(_) -> undefined. diff --git a/apps/emqx_authn/src/emqx_authn_sup.erl b/apps/emqx_authn/src/emqx_authn_sup.erl index 56fcf299a..dd672a7c7 100644 --- a/apps/emqx_authn/src/emqx_authn_sup.erl +++ b/apps/emqx_authn/src/emqx_authn_sup.erl @@ -26,11 +26,5 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - ChildSpecs = [ - #{id => emqx_authn, - start => {emqx_authn, start_link, []}, - restart => permanent, - type => worker, - modules => [emqx_authn]} - ], + ChildSpecs = [], {ok, {{one_for_one, 10, 10}, ChildSpecs}}. diff --git a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl index d7902d824..aa21c0484 100644 --- a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl +++ b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl @@ -20,13 +20,15 @@ -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). +-behaviour(emqx_authentication). -export([ namespace/0 , roots/0 , fields/1 ]). --export([ create/1 +-export([ refs/0 + , create/1 , update/2 , authenticate/2 , destroy/1 @@ -75,21 +77,16 @@ mnesia(copy) -> %% Hocon Schema %%------------------------------------------------------------------------------ -namespace() -> "authn:scram:builtin_db". +namespace() -> "authn:scram:builtin-db". roots() -> [config]. fields(config) -> - [ {name, fun emqx_authn_schema:authenticator_name/1} - , {mechanism, {enum, [scram]}} - , {server_type, fun server_type/1} + [ {mechanism, {enum, [scram]}} + , {backend, {enum, ['built-in-database']}} , {algorithm, fun algorithm/1} , {iteration_count, fun iteration_count/1} - ]. - -server_type(type) -> hoconsc:enum(['built-in-database']); -server_type(default) -> 'built-in-database'; -server_type(_) -> undefined. + ] ++ emqx_authn_schema:common_fields(). algorithm(type) -> hoconsc:enum([sha256, sha512]); algorithm(default) -> sha256; @@ -103,6 +100,9 @@ iteration_count(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ +refs() -> + [hoconsc:ref(?MODULE, config)]. + create(#{ algorithm := Algorithm , iteration_count := IterationCount , '_unique' := Unique diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl index 080b71ab1..1bec0d903 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -21,6 +21,7 @@ -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). +-behaviour(emqx_authentication). -export([ namespace/0 , roots/0 @@ -28,7 +29,8 @@ , validations/0 ]). --export([ create/1 +-export([ refs/0 + , create/1 , update/2 , authenticate/2 , destroy/1 @@ -38,7 +40,7 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -namespace() -> "authn:http". +namespace() -> "authn:password-based:http-server". roots() -> [ {config, {union, [ hoconsc:ref(?MODULE, get) @@ -59,15 +61,15 @@ fields(post) -> ] ++ common_fields(). common_fields() -> - [ {name, fun emqx_authn_schema:authenticator_name/1} - , {mechanism, {enum, ['password-based']}} - , {server_type, {enum, ['http-server']}} + [ {mechanism, {enum, ['password-based']}} + , {backend, {enum, ['http-server']}} , {url, fun url/1} - , {form_data, fun form_data/1} + , {body, fun body/1} , {request_timeout, fun request_timeout/1} - ] ++ maps:to_list(maps:without([ base_url - , pool_type], - maps:from_list(emqx_connector_http:fields(config)))). + ] ++ emqx_authn_schema:common_fields() + ++ maps:to_list(maps:without([ base_url + , pool_type], + maps:from_list(emqx_connector_http:fields(config)))). validations() -> [ {check_ssl_opts, fun check_ssl_opts/1} @@ -95,11 +97,10 @@ headers_no_content_type(converter) -> headers_no_content_type(default) -> default_headers_no_content_type(); headers_no_content_type(_) -> undefined. -%% TODO: Using map() -form_data(type) -> map(); -form_data(nullable) -> false; -form_data(validate) -> [fun check_form_data/1]; -form_data(_) -> undefined. +body(type) -> map(); +body(nullable) -> false; +body(validate) -> [fun check_body/1]; +body(_) -> undefined. request_timeout(type) -> non_neg_integer(); request_timeout(default) -> 5000; @@ -109,10 +110,15 @@ request_timeout(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ +refs() -> + [ hoconsc:ref(?MODULE, get) + , hoconsc:ref(?MODULE, post) + ]. + create(#{ method := Method , url := URL , headers := Headers - , form_data := FormData + , body := Body , request_timeout := RequestTimeout , '_unique' := Unique } = Config) -> @@ -121,8 +127,8 @@ create(#{ method := Method State = #{ method => Method , path => Path , base_query => cow_qs:parse_qs(list_to_binary(Query)) - , headers => normalize_headers(Headers) - , form_data => maps:to_list(FormData) + , headers => maps:to_list(Headers) + , body => maps:to_list(Body) , request_timeout => RequestTimeout , '_unique' => Unique }, @@ -189,10 +195,10 @@ check_url(URL) -> {error, _} -> false end. -check_form_data(FormData) -> +check_body(Body) -> lists:any(fun({_, V}) -> not is_binary(V) - end, maps:to_list(FormData)). + end, maps:to_list(Body)). default_headers() -> maps:put(<<"content-type">>, @@ -232,23 +238,20 @@ parse_url(URL) -> URIMap end. -normalize_headers(Headers) -> - [{atom_to_binary(K), V} || {K, V} <- maps:to_list(Headers)]. - generate_request(Credential, #{method := Method, path := Path, base_query := BaseQuery, headers := Headers, - form_data := FormData0}) -> - FormData = replace_placeholders(FormData0, Credential), + body := Body0}) -> + Body = replace_placeholders(Body0, Credential), case Method of get -> - NPath = append_query(Path, BaseQuery ++ FormData), + NPath = append_query(Path, BaseQuery ++ Body), {NPath, Headers}; post -> NPath = append_query(Path, BaseQuery), ContentType = proplists:get_value(<<"content-type">>, Headers), - Body = serialize_body(ContentType, FormData), + Body = serialize_body(ContentType, Body), {NPath, Headers, Body} end. @@ -279,10 +282,10 @@ qs([], Acc) -> qs([{K, V} | More], Acc) -> qs(More, [["&", emqx_http_lib:uri_encode(K), "=", emqx_http_lib:uri_encode(V)] | Acc]). -serialize_body(<<"application/json">>, FormData) -> - emqx_json:encode(FormData); -serialize_body(<<"application/x-www-form-urlencoded">>, FormData) -> - qs(FormData). +serialize_body(<<"application/json">>, Body) -> + emqx_json:encode(Body); +serialize_body(<<"application/x-www-form-urlencoded">>, Body) -> + qs(Body). safely_parse_body(ContentType, Body) -> try parse_body(ContentType, Body) of diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl index 1ce10a2cc..e55b58795 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -19,13 +19,15 @@ -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). +-behaviour(emqx_authentication). -export([ namespace/0 , roots/0 , fields/1 ]). --export([ create/1 +-export([ refs/0 + , create/1 , update/2 , authenticate/2 , destroy/1 @@ -81,12 +83,11 @@ fields(ssl_disable) -> [ {enable, #{type => false}} ]. common_fields() -> - [ {name, fun emqx_authn_schema:authenticator_name/1} - , {mechanism, {enum, [jwt]}} + [ {mechanism, {enum, [jwt]}} , {verify_claims, fun verify_claims/1} - ]. + ] ++ emqx_authn_schema:common_fields(). -secret(type) -> string(); +secret(type) -> binary(); secret(_) -> undefined. secret_base64_encoded(type) -> boolean(); @@ -133,6 +134,12 @@ verify_claims(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ +refs() -> + [ hoconsc:ref(?MODULE, 'hmac-based') + , hoconsc:ref(?MODULE, 'public-key') + , hoconsc:ref(?MODULE, 'jwks') + ]. + create(#{verify_claims := VerifyClaims} = Config) -> create2(Config#{verify_claims => handle_verify_claims(VerifyClaims)}). diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl index efe974145..f41edab8b 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl @@ -20,10 +20,15 @@ -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). +-behaviour(emqx_authentication). --export([ namespace/0, roots/0, fields/1 ]). +-export([ namespace/0 + , roots/0 + , fields/1 + ]). --export([ create/1 +-export([ refs/0 + , create/1 , update/2 , authenticate/2 , destroy/1 @@ -79,17 +84,16 @@ mnesia(copy) -> %% Hocon Schema %%------------------------------------------------------------------------------ -namespace() -> "authn:builtin_db". +namespace() -> "authn:password-based:builtin-db". roots() -> [config]. fields(config) -> - [ {name, fun emqx_authn_schema:authenticator_name/1} - , {mechanism, {enum, ['password-based']}} - , {server_type, {enum, ['built-in-database']}} + [ {mechanism, {enum, ['password-based']}} + , {backend, {enum, ['built-in-database']}} , {user_id_type, fun user_id_type/1} , {password_hash_algorithm, fun password_hash_algorithm/1} - ]; + ] ++ emqx_authn_schema:common_fields(); fields(bcrypt) -> [ {name, {enum, [bcrypt]}} @@ -117,6 +121,9 @@ salt_rounds(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ +refs() -> + [hoconsc:ref(?MODULE, config)]. + create(#{ user_id_type := Type , password_hash_algorithm := #{name := bcrypt, salt_rounds := SaltRounds} diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl index d272fe05b..f35be985a 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl @@ -21,13 +21,15 @@ -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). +-behaviour(emqx_authentication). -export([ namespace/0 , roots/0 , fields/1 ]). --export([ create/1 +-export([ refs/0 + , create/1 , update/2 , authenticate/2 , destroy/1 @@ -37,7 +39,7 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -namespace() -> "authn:mongodb". +namespace() -> "authn:password-based:mongodb". roots() -> [ {config, {union, [ hoconsc:mk(standalone) @@ -56,16 +58,15 @@ fields('sharded-cluster') -> common_fields() ++ emqx_connector_mongo:fields(sharded). common_fields() -> - [ {name, fun emqx_authn_schema:authenticator_name/1} - , {mechanism, {enum, ['password-based']}} - , {server_type, {enum, [mongodb]}} + [ {mechanism, {enum, ['password-based']}} + , {backend, {enum, [mongodb]}} , {collection, fun collection/1} , {selector, fun selector/1} , {password_hash_field, fun password_hash_field/1} , {salt_field, fun salt_field/1} , {password_hash_algorithm, fun password_hash_algorithm/1} , {salt_position, fun salt_position/1} - ]. + ] ++ emqx_authn_schema:common_fields(). collection(type) -> binary(); collection(nullable) -> false; @@ -95,6 +96,12 @@ salt_position(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ +refs() -> + [ hoconsc:ref(?MODULE, standalone) + , hoconsc:ref(?MODULE, 'replica-set') + , hoconsc:ref(?MODULE, 'sharded-cluster') + ]. + create(#{ selector := Selector , '_unique' := Unique } = Config) -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl index c94798aa6..67ccbf7ae 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl @@ -21,13 +21,15 @@ -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). +-behaviour(emqx_authentication). -export([ namespace/0 , roots/0 , fields/1 ]). --export([ create/1 +-export([ refs/0 + , create/1 , update/2 , authenticate/2 , destroy/1 @@ -37,19 +39,19 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -namespace() -> "authn:mysql". +namespace() -> "authn:password-based:mysql". roots() -> [config]. fields(config) -> - [ {name, fun emqx_authn_schema:authenticator_name/1} - , {mechanism, {enum, ['password-based']}} - , {server_type, {enum, [mysql]}} + [ {mechanism, {enum, ['password-based']}} + , {backend, {enum, [mysql]}} , {password_hash_algorithm, fun password_hash_algorithm/1} , {salt_position, fun salt_position/1} , {query, fun query/1} , {query_timeout, fun query_timeout/1} - ] ++ emqx_connector_schema_lib:relational_db_fields() + ] ++ emqx_authn_schema:common_fields() + ++ emqx_connector_schema_lib:relational_db_fields() ++ emqx_connector_schema_lib:ssl_fields(). password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]}; @@ -72,6 +74,9 @@ query_timeout(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ +refs() -> + [hoconsc:ref(?MODULE, config)]. + create(#{ password_hash_algorithm := Algorithm , salt_position := SaltPosition , query := Query0 diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl index 6875c5cb9..7676f338d 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl @@ -22,10 +22,15 @@ -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). +-behaviour(emqx_authentication). --export([ namespace/0, roots/0, fields/1 ]). +-export([ namespace/0 + , roots/0 + , fields/1 + ]). --export([ create/1 +-export([ refs/0 + , create/1 , update/2 , authenticate/2 , destroy/1 @@ -35,18 +40,18 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -namespace() -> "authn:postgres". +namespace() -> "authn:password-based:postgresql". roots() -> [config]. fields(config) -> - [ {name, fun emqx_authn_schema:authenticator_name/1} - , {mechanism, {enum, ['password-based']}} - , {server_type, {enum, [pgsql]}} + [ {mechanism, {enum, ['password-based']}} + , {backend, {enum, [postgresql]}} , {password_hash_algorithm, fun password_hash_algorithm/1} , {salt_position, {enum, [prefix, suffix]}} , {query, fun query/1} - ] ++ emqx_connector_schema_lib:relational_db_fields() + ] ++ emqx_authn_schema:common_fields() + ++ emqx_connector_schema_lib:relational_db_fields() ++ emqx_connector_schema_lib:ssl_fields(). password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]}; @@ -61,6 +66,9 @@ query(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ +refs() -> + [hoconsc:ref(?MODULE, config)]. + create(#{ query := Query0 , password_hash_algorithm := Algorithm , salt_position := SaltPosition diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl index 6c5a81652..18840fdea 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl @@ -21,13 +21,15 @@ -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). +-behaviour(emqx_authentication). -export([ namespace/0 , roots/0 , fields/1 ]). --export([ create/1 +-export([ refs/0 + , create/1 , update/2 , authenticate/2 , destroy/1 @@ -37,7 +39,8 @@ %% Hocon Schema %%------------------------------------------------------------------------------ -namespace() -> "authn:redis". +namespace() -> "authn:password-based:redis". + roots() -> [ {config, {union, [ hoconsc:mk(standalone) , hoconsc:mk(cluster) @@ -55,13 +58,12 @@ fields(sentinel) -> common_fields() ++ emqx_connector_redis:fields(sentinel). common_fields() -> - [ {name, fun emqx_authn_schema:authenticator_name/1} - , {mechanism, {enum, ['password-based']}} - , {server_type, {enum, [redis]}} + [ {mechanism, {enum, ['password-based']}} + , {backend, {enum, [redis]}} , {query, fun query/1} , {password_hash_algorithm, fun password_hash_algorithm/1} , {salt_position, fun salt_position/1} - ]. + ] ++ emqx_authn_schema:common_fields(). query(type) -> string(); query(nullable) -> false; @@ -79,6 +81,12 @@ salt_position(_) -> undefined. %% APIs %%------------------------------------------------------------------------------ +refs() -> + [ hoconsc:ref(?MODULE, standalone) + , hoconsc:ref(?MODULE, cluster) + , hoconsc:ref(?MODULE, sentinel) + ]. + create(#{ query := Query , '_unique' := Unique } = Config) -> diff --git a/apps/emqx_authn/test/emqx_authn_SUITE.erl b/apps/emqx_authn/test/emqx_authn_SUITE.erl index eb7f0291a..31bac76a3 100644 --- a/apps/emqx_authn/test/emqx_authn_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_SUITE.erl @@ -15,101 +15,3 @@ %%-------------------------------------------------------------------- -module(emqx_authn_SUITE). - --compile(export_all). --compile(nowarn_export_all). - --include_lib("common_test/include/ct.hrl"). --include_lib("eunit/include/eunit.hrl"). - --include("emqx_authn.hrl"). - --define(AUTH, emqx_authn). - -all() -> - emqx_ct:all(?MODULE). - -init_per_suite(Config) -> - application:set_env(ekka, strict_mode, true), - emqx_ct_helpers:start_apps([emqx_authn]), - Config. - -end_per_suite(_) -> - emqx_ct_helpers:stop_apps([emqx_authn]), - ok. - -t_chain(_) -> - ?assertMatch({ok, #{id := ?CHAIN, authenticators := []}}, ?AUTH:lookup_chain(?CHAIN)), - - ChainID = <<"mychain">>, - Chain = #{id => ChainID}, - ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)), - ?assertEqual({error, {already_exists, {chain, ChainID}}}, ?AUTH:create_chain(Chain)), - ?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:lookup_chain(ChainID)), - ?assertEqual(ok, ?AUTH:delete_chain(ChainID)), - ?assertMatch({error, {not_found, {chain, ChainID}}}, ?AUTH:lookup_chain(ChainID)), - ok. - -t_authenticator(_) -> - AuthenticatorName1 = <<"myauthenticator1">>, - AuthenticatorConfig1 = #{name => AuthenticatorName1, - mechanism => 'password-based', - server_type => 'built-in-database', - user_id_type => username, - password_hash_algorithm => #{ - name => sha256 - }}, - {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1), - ?assertMatch({ok, #{name := AuthenticatorName1}}, ?AUTH:lookup_authenticator(?CHAIN, ID1)), - ?assertMatch({ok, [#{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)), - ?assertEqual({error, name_has_be_used}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)), - - AuthenticatorConfig2 = #{name => AuthenticatorName1, - mechanism => jwt, - use_jwks => false, - algorithm => 'hmac-based', - secret => <<"abcdef">>, - secret_base64_encoded => false, - verify_claims => []}, - {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:update_authenticator(?CHAIN, ID1, AuthenticatorConfig2), - - ID2 = <<"random">>, - ?assertEqual({error, {not_found, {authenticator, ID2}}}, ?AUTH:update_authenticator(?CHAIN, ID2, AuthenticatorConfig2)), - ?assertEqual({error, name_has_be_used}, ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig2)), - - AuthenticatorName2 = <<"myauthenticator2">>, - AuthenticatorConfig3 = AuthenticatorConfig2#{name => AuthenticatorName2}, - {ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3), - ?assertMatch({ok, #{name := AuthenticatorName2}}, ?AUTH:lookup_authenticator(?CHAIN, ID2)), - {ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3#{secret := <<"fedcba">>}), - - ?assertMatch({ok, #{id := ?CHAIN, authenticators := [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}}, ?AUTH:lookup_chain(?CHAIN)), - ?assertMatch({ok, [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}, ?AUTH:list_authenticators(?CHAIN)), - - ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, top)), - ?assertMatch({ok, [#{name := AuthenticatorName2}, #{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)), - - ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, bottom)), - ?assertMatch({ok, [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}, ?AUTH:list_authenticators(?CHAIN)), - - ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, {before, ID1})), - - ?assertMatch({ok, [#{name := AuthenticatorName2}, #{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)), - - ?assertEqual({error, {not_found, {authenticator, <<"nonexistent">>}}}, ?AUTH:move_authenticator(?CHAIN, ID2, {before, <<"nonexistent">>})), - - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)), - ?assertEqual({ok, []}, ?AUTH:list_authenticators(?CHAIN)), - ok. - -t_authenticate(_) -> - ClientInfo = #{zone => default, - listener => {tcp, default}, - username => <<"myuser">>, - password => <<"mypass">>}, - ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)), - ?assertEqual(false, emqx_authn:is_enabled()), - emqx_authn:enable(), - ?assertEqual(true, emqx_authn:is_enabled()), - ?assertEqual({error, not_authorized}, emqx_access_control:authenticate(ClientInfo)). diff --git a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl index ddb2bb209..5e06211a7 100644 --- a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl @@ -16,143 +16,143 @@ -module(emqx_authn_jwt_SUITE). --compile(export_all). --compile(nowarn_export_all). +% -compile(export_all). +% -compile(nowarn_export_all). --include_lib("common_test/include/ct.hrl"). --include_lib("eunit/include/eunit.hrl"). +% -include_lib("common_test/include/ct.hrl"). +% -include_lib("eunit/include/eunit.hrl"). --include("emqx_authn.hrl"). +% -include("emqx_authn.hrl"). --define(AUTH, emqx_authn). +% -define(AUTH, emqx_authn). -all() -> - emqx_ct:all(?MODULE). +% all() -> +% emqx_ct:all(?MODULE). -init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx_authn]), - Config. +% init_per_suite(Config) -> +% emqx_ct_helpers:start_apps([emqx_authn]), +% Config. -end_per_suite(_) -> - emqx_ct_helpers:stop_apps([emqx_authn]), - ok. +% end_per_suite(_) -> +% emqx_ct_helpers:stop_apps([emqx_authn]), +% ok. -t_jwt_authenticator(_) -> - AuthenticatorName = <<"myauthenticator">>, - Config = #{name => AuthenticatorName, - mechanism => jwt, - use_jwks => false, - algorithm => 'hmac-based', - secret => <<"abcdef">>, - secret_base64_encoded => false, - verify_claims => []}, - {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config), +% t_jwt_authenticator(_) -> +% AuthenticatorName = <<"myauthenticator">>, +% Config = #{name => AuthenticatorName, +% mechanism => jwt, +% use_jwks => false, +% algorithm => 'hmac-based', +% secret => <<"abcdef">>, +% secret_base64_encoded => false, +% verify_claims => []}, +% {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config), - Payload = #{<<"username">> => <<"myuser">>}, - JWS = generate_jws('hmac-based', Payload, <<"abcdef">>), - ClientInfo = #{username => <<"myuser">>, - password => JWS}, - ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), +% Payload = #{<<"username">> => <<"myuser">>}, +% JWS = generate_jws('hmac-based', Payload, <<"abcdef">>), +% ClientInfo = #{username => <<"myuser">>, +% password => JWS}, +% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), - Payload1 = #{<<"username">> => <<"myuser">>, <<"superuser">> => true}, - JWS1 = generate_jws('hmac-based', Payload1, <<"abcdef">>), - ClientInfo1 = #{username => <<"myuser">>, - password => JWS1}, - ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)), +% Payload1 = #{<<"username">> => <<"myuser">>, <<"superuser">> => true}, +% JWS1 = generate_jws('hmac-based', Payload1, <<"abcdef">>), +% ClientInfo1 = #{username => <<"myuser">>, +% password => JWS1}, +% ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)), - BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>), - ClientInfo2 = ClientInfo#{password => BadJWS}, - ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)), +% BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>), +% ClientInfo2 = ClientInfo#{password => BadJWS}, +% ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)), - %% secret_base64_encoded - Config2 = Config#{secret => base64:encode(<<"abcdef">>), - secret_base64_encoded => true}, - ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config2)), - ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), +% %% secret_base64_encoded +% Config2 = Config#{secret => base64:encode(<<"abcdef">>), +% secret_base64_encoded => true}, +% ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config2)), +% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), - Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]}, - ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config3)), - ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), - ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>}, ok)), +% Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]}, +% ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config3)), +% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), +% ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>}, ok)), - %% Expiration - Payload3 = #{ <<"username">> => <<"myuser">> - , <<"exp">> => erlang:system_time(second) - 60}, - JWS3 = generate_jws('hmac-based', Payload3, <<"abcdef">>), - ClientInfo3 = ClientInfo#{password => JWS3}, - ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)), +% %% Expiration +% Payload3 = #{ <<"username">> => <<"myuser">> +% , <<"exp">> => erlang:system_time(second) - 60}, +% JWS3 = generate_jws('hmac-based', Payload3, <<"abcdef">>), +% ClientInfo3 = ClientInfo#{password => JWS3}, +% ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)), - Payload4 = #{ <<"username">> => <<"myuser">> - , <<"exp">> => erlang:system_time(second) + 60}, - JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>), - ClientInfo4 = ClientInfo#{password => JWS4}, - ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)), +% Payload4 = #{ <<"username">> => <<"myuser">> +% , <<"exp">> => erlang:system_time(second) + 60}, +% JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>), +% ClientInfo4 = ClientInfo#{password => JWS4}, +% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)), - %% Issued At - Payload5 = #{ <<"username">> => <<"myuser">> - , <<"iat">> => erlang:system_time(second) - 60}, - JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>), - ClientInfo5 = ClientInfo#{password => JWS5}, - ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo5, ignored)), +% %% Issued At +% Payload5 = #{ <<"username">> => <<"myuser">> +% , <<"iat">> => erlang:system_time(second) - 60}, +% JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>), +% ClientInfo5 = ClientInfo#{password => JWS5}, +% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo5, ignored)), - Payload6 = #{ <<"username">> => <<"myuser">> - , <<"iat">> => erlang:system_time(second) + 60}, - JWS6 = generate_jws('hmac-based', Payload6, <<"abcdef">>), - ClientInfo6 = ClientInfo#{password => JWS6}, - ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo6, ignored)), +% Payload6 = #{ <<"username">> => <<"myuser">> +% , <<"iat">> => erlang:system_time(second) + 60}, +% JWS6 = generate_jws('hmac-based', Payload6, <<"abcdef">>), +% ClientInfo6 = ClientInfo#{password => JWS6}, +% ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo6, ignored)), - %% Not Before - Payload7 = #{ <<"username">> => <<"myuser">> - , <<"nbf">> => erlang:system_time(second) - 60}, - JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>), - ClientInfo7 = ClientInfo#{password => JWS7}, - ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo7, ignored)), +% %% Not Before +% Payload7 = #{ <<"username">> => <<"myuser">> +% , <<"nbf">> => erlang:system_time(second) - 60}, +% JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>), +% ClientInfo7 = ClientInfo#{password => JWS7}, +% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo7, ignored)), - Payload8 = #{ <<"username">> => <<"myuser">> - , <<"nbf">> => erlang:system_time(second) + 60}, - JWS8 = generate_jws('hmac-based', Payload8, <<"abcdef">>), - ClientInfo8 = ClientInfo#{password => JWS8}, - ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo8, ignored)), +% Payload8 = #{ <<"username">> => <<"myuser">> +% , <<"nbf">> => erlang:system_time(second) + 60}, +% JWS8 = generate_jws('hmac-based', Payload8, <<"abcdef">>), +% ClientInfo8 = ClientInfo#{password => JWS8}, +% ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo8, ignored)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), - ok. +% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), +% ok. -t_jwt_authenticator2(_) -> - Dir = code:lib_dir(emqx_authn, test), - PublicKey = list_to_binary(filename:join([Dir, "data/public_key.pem"])), - PrivateKey = list_to_binary(filename:join([Dir, "data/private_key.pem"])), - AuthenticatorName = <<"myauthenticator">>, - Config = #{name => AuthenticatorName, - mechanism => jwt, - use_jwks => false, - algorithm => 'public-key', - certificate => PublicKey, - verify_claims => []}, - {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config), +% t_jwt_authenticator2(_) -> +% Dir = code:lib_dir(emqx_authn, test), +% PublicKey = list_to_binary(filename:join([Dir, "data/public_key.pem"])), +% PrivateKey = list_to_binary(filename:join([Dir, "data/private_key.pem"])), +% AuthenticatorName = <<"myauthenticator">>, +% Config = #{name => AuthenticatorName, +% mechanism => jwt, +% use_jwks => false, +% algorithm => 'public-key', +% certificate => PublicKey, +% verify_claims => []}, +% {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config), - Payload = #{<<"username">> => <<"myuser">>}, - JWS = generate_jws('public-key', Payload, PrivateKey), - ClientInfo = #{username => <<"myuser">>, - password => JWS}, - ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), - ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ignored)), +% Payload = #{<<"username">> => <<"myuser">>}, +% JWS = generate_jws('public-key', Payload, PrivateKey), +% ClientInfo = #{username => <<"myuser">>, +% password => JWS}, +% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), +% ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ignored)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), - ok. +% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), +% ok. -generate_jws('hmac-based', Payload, Secret) -> - JWK = jose_jwk:from_oct(Secret), - Header = #{ <<"alg">> => <<"HS256">> - , <<"typ">> => <<"JWT">> - }, - Signed = jose_jwt:sign(JWK, Header, Payload), - {_, JWS} = jose_jws:compact(Signed), - JWS; -generate_jws('public-key', Payload, PrivateKey) -> - JWK = jose_jwk:from_pem_file(PrivateKey), - Header = #{ <<"alg">> => <<"RS256">> - , <<"typ">> => <<"JWT">> - }, - Signed = jose_jwt:sign(JWK, Header, Payload), - {_, JWS} = jose_jws:compact(Signed), - JWS. +% generate_jws('hmac-based', Payload, Secret) -> +% JWK = jose_jwk:from_oct(Secret), +% Header = #{ <<"alg">> => <<"HS256">> +% , <<"typ">> => <<"JWT">> +% }, +% Signed = jose_jwt:sign(JWK, Header, Payload), +% {_, JWS} = jose_jws:compact(Signed), +% JWS; +% generate_jws('public-key', Payload, PrivateKey) -> +% JWK = jose_jwk:from_pem_file(PrivateKey), +% Header = #{ <<"alg">> => <<"RS256">> +% , <<"typ">> => <<"JWT">> +% }, +% Signed = jose_jwt:sign(JWK, Header, Payload), +% {_, JWS} = jose_jws:compact(Signed), +% JWS. diff --git a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl index d6425a89c..acfe71809 100644 --- a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl @@ -16,149 +16,149 @@ -module(emqx_authn_mnesia_SUITE). --compile(export_all). --compile(nowarn_export_all). +% -compile(export_all). +% -compile(nowarn_export_all). --include_lib("common_test/include/ct.hrl"). --include_lib("eunit/include/eunit.hrl"). +% -include_lib("common_test/include/ct.hrl"). +% -include_lib("eunit/include/eunit.hrl"). --include("emqx_authn.hrl"). +% -include("emqx_authn.hrl"). --define(AUTH, emqx_authn). +% -define(AUTH, emqx_authn). -all() -> - emqx_ct:all(?MODULE). +% all() -> +% emqx_ct:all(?MODULE). -init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx_authn]), - Config. +% init_per_suite(Config) -> +% emqx_ct_helpers:start_apps([emqx_authn]), +% Config. -end_per_suite(_) -> - emqx_ct_helpers:stop_apps([emqx_authn]), - ok. +% end_per_suite(_) -> +% emqx_ct_helpers:stop_apps([emqx_authn]), +% ok. -t_mnesia_authenticator(_) -> - AuthenticatorName = <<"myauthenticator">>, - AuthenticatorConfig = #{name => AuthenticatorName, - mechanism => 'password-based', - server_type => 'built-in-database', - user_id_type => username, - password_hash_algorithm => #{ - name => sha256 - }}, - {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig), +% t_mnesia_authenticator(_) -> +% AuthenticatorName = <<"myauthenticator">>, +% AuthenticatorConfig = #{name => AuthenticatorName, +% mechanism => 'password-based', +% server_type => 'built-in-database', +% user_id_type => username, +% password_hash_algorithm => #{ +% name => sha256 +% }}, +% {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig), - UserInfo = #{user_id => <<"myuser">>, - password => <<"mypass">>}, - ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)), - ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), +% UserInfo = #{user_id => <<"myuser">>, +% password => <<"mypass">>}, +% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)), +% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), - ClientInfo = #{zone => external, - username => <<"myuser">>, - password => <<"mypass">>}, - ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), - ?AUTH:enable(), - ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)), +% ClientInfo = #{zone => external, +% username => <<"myuser">>, +% password => <<"mypass">>}, +% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), +% ?AUTH:enable(), +% ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)), - ClientInfo2 = ClientInfo#{username => <<"baduser">>}, - ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)), - ?assertEqual({error, not_authorized}, emqx_access_control:authenticate(ClientInfo2)), +% ClientInfo2 = ClientInfo#{username => <<"baduser">>}, +% ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)), +% ?assertEqual({error, not_authorized}, emqx_access_control:authenticate(ClientInfo2)), - ClientInfo3 = ClientInfo#{password => <<"badpass">>}, - ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)), - ?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo3)), +% ClientInfo3 = ClientInfo#{password => <<"badpass">>}, +% ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)), +% ?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo3)), - UserInfo2 = UserInfo#{password => <<"mypass2">>}, - ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, UserInfo2)), - ClientInfo4 = ClientInfo#{password => <<"mypass2">>}, - ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)), +% UserInfo2 = UserInfo#{password => <<"mypass2">>}, +% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, UserInfo2)), +% ClientInfo4 = ClientInfo#{password => <<"mypass2">>}, +% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)), - ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, #{superuser => true})), - ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo4, ignored)), +% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, #{superuser => true})), +% ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo4, ignored)), - ?assertEqual(ok, ?AUTH:delete_user(?CHAIN, ID, <<"myuser">>)), - ?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), +% ?assertEqual(ok, ?AUTH:delete_user(?CHAIN, ID, <<"myuser">>)), +% ?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), - ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)), - ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), +% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)), +% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), +% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), - {ok, #{name := AuthenticatorName, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig), - ?assertMatch({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID1, <<"myuser">>)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), - ok. +% {ok, #{name := AuthenticatorName, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig), +% ?assertMatch({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID1, <<"myuser">>)), +% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), +% ok. -t_import(_) -> - AuthenticatorName = <<"myauthenticator">>, - AuthenticatorConfig = #{name => AuthenticatorName, - mechanism => 'password-based', - server_type => 'built-in-database', - user_id_type => username, - password_hash_algorithm => #{ - name => sha256 - }}, - {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig), +% t_import(_) -> +% AuthenticatorName = <<"myauthenticator">>, +% AuthenticatorConfig = #{name => AuthenticatorName, +% mechanism => 'password-based', +% server_type => 'built-in-database', +% user_id_type => username, +% password_hash_algorithm => #{ +% name => sha256 +% }}, +% {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig), - Dir = code:lib_dir(emqx_authn, test), - ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.json"]))), - ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.csv"]))), - ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser1">>)), - ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser3">>)), +% Dir = code:lib_dir(emqx_authn, test), +% ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.json"]))), +% ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.csv"]))), +% ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser1">>)), +% ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser3">>)), - ClientInfo1 = #{username => <<"myuser1">>, - password => <<"mypassword1">>}, - ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)), +% ClientInfo1 = #{username => <<"myuser1">>, +% password => <<"mypassword1">>}, +% ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)), - ClientInfo2 = ClientInfo1#{username => <<"myuser2">>, - password => <<"mypassword2">>}, - ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)), +% ClientInfo2 = ClientInfo1#{username => <<"myuser2">>, +% password => <<"mypassword2">>}, +% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)), - ClientInfo3 = ClientInfo1#{username => <<"myuser3">>, - password => <<"mypassword3">>}, - ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo3, ignored)), +% ClientInfo3 = ClientInfo1#{username => <<"myuser3">>, +% password => <<"mypassword3">>}, +% ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo3, ignored)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), - ok. +% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), +% ok. -t_multi_mnesia_authenticator(_) -> - AuthenticatorName1 = <<"myauthenticator1">>, - AuthenticatorConfig1 = #{name => AuthenticatorName1, - mechanism => 'password-based', - server_type => 'built-in-database', - user_id_type => username, - password_hash_algorithm => #{ - name => sha256 - }}, - AuthenticatorName2 = <<"myauthenticator2">>, - AuthenticatorConfig2 = #{name => AuthenticatorName2, - mechanism => 'password-based', - server_type => 'built-in-database', - user_id_type => clientid, - password_hash_algorithm => #{ - name => sha256 - }}, - {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1), - {ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2), +% t_multi_mnesia_authenticator(_) -> +% AuthenticatorName1 = <<"myauthenticator1">>, +% AuthenticatorConfig1 = #{name => AuthenticatorName1, +% mechanism => 'password-based', +% server_type => 'built-in-database', +% user_id_type => username, +% password_hash_algorithm => #{ +% name => sha256 +% }}, +% AuthenticatorName2 = <<"myauthenticator2">>, +% AuthenticatorConfig2 = #{name => AuthenticatorName2, +% mechanism => 'password-based', +% server_type => 'built-in-database', +% user_id_type => clientid, +% password_hash_algorithm => #{ +% name => sha256 +% }}, +% {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1), +% {ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2), - ?assertMatch({ok, #{user_id := <<"myuser">>}}, - ?AUTH:add_user(?CHAIN, ID1, - #{user_id => <<"myuser">>, - password => <<"mypass1">>})), - ?assertMatch({ok, #{user_id := <<"myclient">>}}, - ?AUTH:add_user(?CHAIN, ID2, - #{user_id => <<"myclient">>, - password => <<"mypass2">>})), +% ?assertMatch({ok, #{user_id := <<"myuser">>}}, +% ?AUTH:add_user(?CHAIN, ID1, +% #{user_id => <<"myuser">>, +% password => <<"mypass1">>})), +% ?assertMatch({ok, #{user_id := <<"myclient">>}}, +% ?AUTH:add_user(?CHAIN, ID2, +% #{user_id => <<"myclient">>, +% password => <<"mypass2">>})), - ClientInfo1 = #{username => <<"myuser">>, - clientid => <<"myclient">>, - password => <<"mypass1">>}, - ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo1, ignored)), - ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, top)), +% ClientInfo1 = #{username => <<"myuser">>, +% clientid => <<"myclient">>, +% password => <<"mypass1">>}, +% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo1, ignored)), +% ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, top)), - ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ignored)), - ClientInfo2 = ClientInfo1#{password => <<"mypass2">>}, - ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)), +% ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ignored)), +% ClientInfo2 = ClientInfo1#{password => <<"mypass2">>}, +% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)), - ok. +% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), +% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)), +% ok. diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index 0b769748a..906b57fb3 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -242,15 +242,8 @@ init_worker_options([_ | R], Acc) -> init_worker_options(R, Acc); init_worker_options([], Acc) -> Acc. -host_port(HostPort) -> - case string:split(HostPort, ":") of - [Host, Port] -> - {ok, Host1} = inet:parse_address(Host), - [{host, Host1}, {port, list_to_integer(Port)}]; - [Host] -> - {ok, Host1} = inet:parse_address(Host), - [{host, Host1}] - end. +host_port({Host, Port}) -> + [{host, Host}, {port, Port}]. server(type) -> server(); server(validator) -> [?NOT_EMPTY("the value of the field 'server' cannot be empty")]; diff --git a/apps/emqx_connector/src/emqx_connector_redis.erl b/apps/emqx_connector/src/emqx_connector_redis.erl index 4fe26381e..44b036f39 100644 --- a/apps/emqx_connector/src/emqx_connector_redis.erl +++ b/apps/emqx_connector/src/emqx_connector_redis.erl @@ -19,9 +19,13 @@ -include_lib("typerefl/include/types.hrl"). -include_lib("emqx_resource/include/emqx_resource_behaviour.hrl"). --type server() :: emqx_schema:ip_port(). +-type server() :: tuple(). + -reflect_type([server/0]). --typerefl_from_string({server/0, emqx_connector_schema_lib, to_ip_port}). + +-typerefl_from_string({server/0, ?MODULE, to_server}). + +-export([to_server/1]). -export([roots/0, fields/1]). @@ -168,3 +172,9 @@ redis_fields() -> default => 0}} , {auto_reconnect, fun emqx_connector_schema_lib:auto_reconnect/1} ]. + +to_server(Server) -> + case string:tokens(Server, ":") of + [Host, Port] -> {ok, {Host, list_to_integer(Port)}}; + _ -> {error, Server} + end. \ No newline at end of file diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index 5134246cd..b3b568271 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -29,12 +29,13 @@ gateway.stomp { password = "${Packet.headers.passcode}" } - authentication { - name = "authenticator1" - mechanism = password-based - server_type = built-in-database - user_id_type = clientid - } + authentication: [ + # { + # name = "authenticator1" + # type = "password-based:built-in-database" + # user_id_type = clientid + # } + ] listeners.tcp.default { bind = 61613 @@ -63,13 +64,6 @@ gateway.coap { subscribe_qos = qos0 publish_qos = qos1 - authentication { - name = "authenticator1" - mechanism = password-based - server_type = built-in-database - user_id_type = clientid - } - listeners.udp.default { bind = 5683 } diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index da73b85ee..45e338a36 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -222,25 +222,25 @@ fields(ExtraField) -> Mod = list_to_atom(ExtraField++"_schema"), Mod:fields(ExtraField). -authentication() -> - hoconsc:union( - [ undefined - , hoconsc:ref(emqx_authn_mnesia, config) - , hoconsc:ref(emqx_authn_mysql, config) - , hoconsc:ref(emqx_authn_pgsql, config) - , hoconsc:ref(emqx_authn_mongodb, standalone) - , hoconsc:ref(emqx_authn_mongodb, 'replica-set') - , hoconsc:ref(emqx_authn_mongodb, 'sharded-cluster') - , hoconsc:ref(emqx_authn_redis, standalone) - , hoconsc:ref(emqx_authn_redis, cluster) - , hoconsc:ref(emqx_authn_redis, sentinel) - , hoconsc:ref(emqx_authn_http, get) - , hoconsc:ref(emqx_authn_http, post) - , hoconsc:ref(emqx_authn_jwt, 'hmac-based') - , hoconsc:ref(emqx_authn_jwt, 'public-key') - , hoconsc:ref(emqx_authn_jwt, 'jwks') - , hoconsc:ref(emqx_enhanced_authn_scram_mnesia, config) - ]). +% authentication() -> +% hoconsc:union( +% [ undefined +% , hoconsc:ref(emqx_authn_mnesia, config) +% , hoconsc:ref(emqx_authn_mysql, config) +% , hoconsc:ref(emqx_authn_pgsql, config) +% , hoconsc:ref(emqx_authn_mongodb, standalone) +% , hoconsc:ref(emqx_authn_mongodb, 'replica-set') +% , hoconsc:ref(emqx_authn_mongodb, 'sharded-cluster') +% , hoconsc:ref(emqx_authn_redis, standalone) +% , hoconsc:ref(emqx_authn_redis, cluster) +% , hoconsc:ref(emqx_authn_redis, sentinel) +% , hoconsc:ref(emqx_authn_http, get) +% , hoconsc:ref(emqx_authn_http, post) +% , hoconsc:ref(emqx_authn_jwt, 'hmac-based') +% , hoconsc:ref(emqx_authn_jwt, 'public-key') +% , hoconsc:ref(emqx_authn_jwt, 'jwks') +% , hoconsc:ref(emqx_enhanced_authn_scram_mnesia, config) +% ]). gateway_common_options() -> [ {enable, sc(boolean(), undefined, true)} @@ -248,7 +248,7 @@ gateway_common_options() -> , {idle_timeout, sc(duration(), undefined, <<"30s">>)} , {mountpoint, sc(binary())} , {clientinfo_override, sc(ref(clientinfo_override))} - , {authentication, sc(authentication(), undefined, undefined)} + , {authentication, sc(hoconsc:lazy(map()))} ]. %%-------------------------------------------------------------------- diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index 657594ae8..4894bda98 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -46,7 +46,6 @@ , emqx_data_bridge_schema , emqx_retainer_schema , emqx_statsd_schema - , emqx_authn_schema , emqx_authz_schema , emqx_auto_subscribe_schema , emqx_bridge_mqtt_schema diff --git a/apps/emqx_retainer/src/emqx_retainer.erl b/apps/emqx_retainer/src/emqx_retainer.erl index 8e14dd21d..3df52b7a7 100644 --- a/apps/emqx_retainer/src/emqx_retainer.erl +++ b/apps/emqx_retainer/src/emqx_retainer.erl @@ -19,10 +19,8 @@ -behaviour(gen_server). -include("emqx_retainer.hrl"). --include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/logger.hrl"). - -export([start_link/0]). -export([ on_session_subscribed/4 diff --git a/rebar.config b/rebar.config index b1616bebc..1abeef9a6 100644 --- a/rebar.config +++ b/rebar.config @@ -63,6 +63,7 @@ , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.15.0"}}} , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.4.0"}}} , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}} + , {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}} ]}. {xref_ignores, From 455baa54656934ff06500133316d44a194a9edaa Mon Sep 17 00:00:00 2001 From: DDDHuang <904897578@qq.com> Date: Mon, 6 Sep 2021 17:59:14 +0800 Subject: [PATCH 067/109] feat: dashboard UI version, beat 11 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index fabb8a7df..e646f5b7a 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ BUILD = $(CURDIR)/build SCRIPTS = $(CURDIR)/scripts export PKG_VSN ?= $(shell $(CURDIR)/pkg-vsn.sh) export EMQX_DESC ?= EMQ X -export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.10 +export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.11 ifeq ($(OS),Windows_NT) export REBAR_COLOR=none endif From 0dd09b06f18f09a3e302e3f5773e952b2a5a9e14 Mon Sep 17 00:00:00 2001 From: William Yang Date: Fri, 3 Sep 2021 09:40:06 +0200 Subject: [PATCH 068/109] build: dep openssl11 is limited to amd64 centos7 --- deploy/packages/rpm/emqx.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/packages/rpm/emqx.spec b/deploy/packages/rpm/emqx.spec index 4a4d6d0f3..44e02ea45 100644 --- a/deploy/packages/rpm/emqx.spec +++ b/deploy/packages/rpm/emqx.spec @@ -19,7 +19,7 @@ BuildRoot: %{_tmppath}/%{_name}-%{_version}-root Provides: %{_name} AutoReq: 0 -%if 0%{?rhel} == 7 +%if "%{_arch} %{?rhel}" == "amd64 7" Requires: openssl11 libatomic %else Requires: libatomic From 6e7d3d05e4ca4ba877d8a4aa40619c2bbe83b331 Mon Sep 17 00:00:00 2001 From: William Yang Date: Fri, 3 Sep 2021 09:50:25 +0200 Subject: [PATCH 069/109] ci: install openssl11 for centos7 amd64 only --- .ci/build_packages/tests.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.ci/build_packages/tests.sh b/.ci/build_packages/tests.sh index 5d6422231..53ab9ac57 100755 --- a/.ci/build_packages/tests.sh +++ b/.ci/build_packages/tests.sh @@ -91,8 +91,12 @@ emqx_test(){ ;; "rpm") packagename=$(basename "${PACKAGE_PATH}/${EMQX_NAME}"-*.rpm) - # EMQX OTP requires openssl11 to have TLS1.3 support - yum install -y openssl11 + + if [[ "${ARCH}" == "amd64" && $(rpm -E '%{rhel}') == 7 ]] ; + then + # EMQX OTP requires openssl11 to have TLS1.3 support + yum install -y openssl11; + fi rpm -ivh "${PACKAGE_PATH}/${packagename}" if ! rpm -q emqx | grep -q emqx; then echo "package install error" From ba9ad47cc188dd5d42bb472aaa4ec86483ebeb31 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Fri, 3 Sep 2021 10:40:38 +0800 Subject: [PATCH 070/109] fix(gw-exproto): fix grpc remote call --- .../src/exproto/emqx_exproto_gsvr.erl | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/apps/emqx_gateway/src/exproto/emqx_exproto_gsvr.erl b/apps/emqx_gateway/src/exproto/emqx_exproto_gsvr.erl index 0135aa8e3..04f3dea1c 100644 --- a/apps/emqx_gateway/src/exproto/emqx_exproto_gsvr.erl +++ b/apps/emqx_gateway/src/exproto/emqx_exproto_gsvr.erl @@ -22,9 +22,10 @@ -include("src/exproto/include/emqx_exproto.hrl"). -include_lib("emqx/include/logger.hrl"). - -define(IS_QOS(X), (X =:= 0 orelse X =:= 1 orelse X =:= 2)). +-define(DEFAULT_CALL_TIMEOUT, 5000). + %% gRPC server callbacks -export([ send/2 , close/2 @@ -117,18 +118,22 @@ to_pid(ConnStr) -> binary_to_term(base64:decode(ConnStr)). call(ConnStr, Req) -> - case catch to_pid(ConnStr) of - {'EXIT', {badarg, _}} -> - {error, ?RESP_PARAMS_TYPE_ERROR, - <<"The conn type error">>}; - Pid when is_pid(Pid) -> - case erlang:is_process_alive(Pid) of - true -> - emqx_gateway_conn:call(Pid, Req); - false -> - {error, ?RESP_CONN_PROCESS_NOT_ALIVE, - <<"Connection process is not alive">>} - end + try + Pid = to_pid(ConnStr), + emqx_gateway_conn:call(Pid, Req, ?DEFAULT_CALL_TIMEOUT) + catch + exit : badarg -> + {error, ?RESP_PARAMS_TYPE_ERROR, <<"The conn type error">>}; + exit : noproc -> + {error, ?RESP_CONN_PROCESS_NOT_ALIVE, + <<"Connection process is not alive">>}; + exit : timeout -> + {error, ?RESP_UNKNOWN, <<"Connection is not answered">>}; + Class : Reason : Stk-> + ?LOG(error, "Call ~p crashed: {~0p, ~0p}, " + "stacktrace: ~0p", + [Class, Reason, Stk]), + {error, ?RESP_UNKNOWN, <<"Unkwown crashs">>} end. %%-------------------------------------------------------------------- From caee51f92a6a5a803e9d37a45e11f683c31e9d3e Mon Sep 17 00:00:00 2001 From: lafirest Date: Mon, 6 Sep 2021 10:37:22 +0800 Subject: [PATCH 071/109] fix(emqx_lwm2m): fix some error and incomplete function --- apps/emqx_gateway/etc/emqx_gateway.conf | 15 +- apps/emqx_gateway/src/coap/README.md | 17 +- .../src/coap/emqx_coap_channel.erl | 16 +- .../src/coap/emqx_coap_medium.erl | 8 +- apps/emqx_gateway/src/emqx_gateway_schema.erl | 1 + .../src/lwm2m/emqx_lwm2m_channel.erl | 92 +++--- .../emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl | 28 +- .../src/lwm2m/emqx_lwm2m_session.erl | 263 ++++++------------ apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl | 17 +- 9 files changed, 210 insertions(+), 247 deletions(-) diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index 5134246cd..7a608cf15 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -60,6 +60,9 @@ gateway.coap { heartbeat = 30s notify_type = qos + + ## if true, you need to establish a connection before use + connection_required = false subscribe_qos = qos0 publish_qos = qos1 @@ -134,7 +137,7 @@ gateway.lwm2m { enable_stats = true ## When publishing or subscribing, prefix all topics with a mountpoint string. - mountpoint = "lwm2m" + mountpoint = "lwm2m/%u" xml_dir = "{{ platform_etc_dir }}/lwm2m_xml" @@ -149,27 +152,27 @@ gateway.lwm2m { translators { command { - topic = "dn/#" + topic = "/dn/#" qos = 0 } response { - topic = "up/resp" + topic = "/up/resp" qos = 0 } notify { - topic = "up/notify" + topic = "/up/notify" qos = 0 } register { - topic = "up/resp" + topic = "/up/resp" qos = 0 } update { - topic = "up/resp" + topic = "/up/resp" qos = 0 } } diff --git a/apps/emqx_gateway/src/coap/README.md b/apps/emqx_gateway/src/coap/README.md index 88f657537..54f0fde84 100644 --- a/apps/emqx_gateway/src/coap/README.md +++ b/apps/emqx_gateway/src/coap/README.md @@ -414,21 +414,30 @@ Server will return token **X** in payload 2. Update Connection ``` -coap-client -m put -e "" "coap://127.0.0.1/mqtt/connection?clientid=123&username=admin&password=public&token=X" +coap-client -m put -e "" "coap://127.0.0.1/mqtt/connection?clientid=123&token=X" ``` 3. Publish ``` -coap-client -m post -e "Hellow" "coap://127.0.0.1/ps/coap/test?clientid=123&username=admin&password=public" +coap-client -m post -e "Hellow" "obstoken" "coap://127.0.0.1/ps/coap/test?clientid=123&username=admin&password=public" ``` if you want to publish with auth, you must first establish a connection, and then post publish request on the same socket, so libcoap client can't simulation publish with a token +``` +coap-client -m post -e "Hellow" "coap://127.0.0.1/ps/coap/test?clientid=123&token=X" +``` + 4. Subscribe ``` coap-client -m get -s 60 -O 6,0x00 -o - -T "obstoken" "coap://127.0.0.1/ps/coap/test?clientid=123&username=admin&password=public" ``` +**Or** +``` +coap-client -m get -s 60 -O 6,0x00 -o - -T "obstoken" "coap://127.0.0.1/ps/coap/test?clientid=123&token=X" +``` 5. Close Connection ``` -coap-client -m delete -e "" "coap://127.0.0.1/mqtt/connection?clientid=123&username=admin&password=public&token=X" -``` \ No newline at end of file +coap-client -m delete -e "" "coap://127.0.0.1/mqtt/connection?clientid=123&token=X +``` + diff --git a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl index 6e554b9ef..11aca8cc8 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl @@ -55,6 +55,8 @@ %% Timer timers :: #{atom() => disable | undefined | reference()}, + connection_required :: boolean(), + conn_state :: idle | connected, token :: binary() | undefined @@ -63,6 +65,8 @@ -type channel() :: #channel{}. -define(TOKEN_MAXIMUM, 4294967295). -define(INFO_KEYS, [conninfo, conn_state, clientinfo, session]). +-define(DEF_IDLE_TIME, timer:seconds(30)). +-define(GET_IDLE_TIME(Cfg), maps:get(idle_timeout, Cfg, ?DEF_IDLE_TIME)). -import(emqx_coap_medium, [reply/2, reply/3, reply/4, iter/3, iter/4]). %%-------------------------------------------------------------------- @@ -110,13 +114,14 @@ init(ConnInfo = #{peername := {PeerHost, _}, } ), - Heartbeat = emqx:get_config([gateway, coap, idle_timeout]), + Heartbeat = ?GET_IDLE_TIME(Config), #channel{ ctx = Ctx , conninfo = ConnInfo , clientinfo = ClientInfo , timers = #{} , session = emqx_coap_session:new() , keepalive = emqx_keepalive:init(Heartbeat) + , connection_required = maps:get(connection_required, Config, false) , conn_state = idle }. @@ -216,13 +221,12 @@ make_timer(Name, Time, Msg, Channel = #channel{timers = Timers}) -> ensure_keepalive_timer(Channel) -> ensure_keepalive_timer(fun ensure_timer/4, Channel). -ensure_keepalive_timer(Fun, Channel) -> - Heartbeat = emqx:get_config([gateway, coap, idle_timeout]), +ensure_keepalive_timer(Fun, #channel{keepalive = KeepAlive} = Channel) -> + Heartbeat = emqx_keepalive:info(interval, KeepAlive), Fun(keepalive, Heartbeat, keepalive, Channel). -check_auth_state(Msg, Channel) -> - Enable = emqx:get_config([gateway, coap, enable_stats]), - check_token(Enable, Msg, Channel). +check_auth_state(Msg, #channel{connection_required = Required} = Channel) -> + check_token(Required, Msg, Channel). check_token(true, Msg, diff --git a/apps/emqx_gateway/src/coap/emqx_coap_medium.erl b/apps/emqx_gateway/src/coap/emqx_coap_medium.erl index ae5763179..8dafc7bbb 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_medium.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_medium.erl @@ -29,13 +29,14 @@ , reply/2, reply/3, reply/4]). %%-type result() :: map() | empty. --define(DEFINE_DEF(Name), Name(Msg) -> Name(Msg, #{})). + %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- empty() -> #{}. -?DEFINE_DEF(reset). +reset(Msg) -> + reset(Msg, #{}). reset(Msg, Result) -> out(emqx_coap_message:reset(Msg), Result). @@ -49,7 +50,8 @@ out(Msg, #{out := Outs} = Result) -> out(Msg, Result) -> Result#{out => [Msg]}. -?DEFINE_DEF(proto_out). +proto_out(Proto) -> + proto_out(Proto, #{}). proto_out(Proto, Resut) -> Resut#{proto => Proto}. diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index da73b85ee..4e0142a47 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -83,6 +83,7 @@ fields(mqttsn_predefined) -> fields(coap_structs) -> [ {heartbeat, sc(duration(), undefined, <<"30s">>)} + , {connection_required, sc(boolean(), undefined, false)} , {notify_type, sc(union([non, con, qos]), undefined, qos)} , {subscribe_qos, sc(union([qos0, qos1, qos2, coap]), undefined, coap)} , {publish_qos, sc(union([qos0, qos1, qos2, coap]), undefined, coap)} diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl index b67032313..d0647897b 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl @@ -24,8 +24,7 @@ -export([ info/1 , info/2 , stats/1 - , validator/2 - , validator/4 + , with_context/2 , do_takeover/3]). -export([ init/2 @@ -53,11 +52,10 @@ %% Timer timers :: #{atom() => disable | undefined | reference()}, - validator :: function() + with_context :: function() }). -define(INFO_KEYS, [conninfo, conn_state, clientinfo, session]). - -import(emqx_coap_medium, [reply/2, reply/3, reply/4, iter/3, iter/4]). %%-------------------------------------------------------------------- @@ -109,16 +107,13 @@ init(ConnInfo = #{peername := {PeerHost, _}, , clientinfo = ClientInfo , timers = #{} , session = emqx_lwm2m_session:new() - , validator = validator(Ctx, ClientInfo) + , with_context = with_context(Ctx, ClientInfo) }. -validator(_Type, _Topic, _Ctx, _ClientInfo) -> - allow. - %emqx_gateway_ctx:authorize(Ctx, ClientInfo, Type, Topic). -validator(Ctx, ClientInfo) -> +with_context(Ctx, ClientInfo) -> fun(Type, Topic) -> - validator(Type, Topic, Ctx, ClientInfo) + with_context(Type, Topic, Ctx, ClientInfo) end. %%-------------------------------------------------------------------- @@ -137,7 +132,10 @@ handle_deliver(Delivers, Channel) -> %%-------------------------------------------------------------------- %% Handle timeout %%-------------------------------------------------------------------- -handle_timeout(_, lifetime, Channel) -> +handle_timeout(_, lifetime, #channel{ctx = Ctx, + clientinfo = ClientInfo, + conninfo = ConnInfo} = Channel) -> + ok = run_hooks(Ctx, 'client.disconnected', [ClientInfo, timeout, ConnInfo]), {shutdown, timeout, Channel}; handle_timeout(_, {transport, _} = Msg, Channel) -> @@ -166,6 +164,10 @@ handle_cast(Req, Channel) -> %%-------------------------------------------------------------------- %% Handle Info %%-------------------------------------------------------------------- +handle_info({subscribe, _AutoSubs}, Channel) -> + %% not need handle this message + {ok, Channel}; + handle_info(Info, Channel) -> ?LOG(error, "Unexpected info: ~p", [Info]), {ok, Channel}. @@ -173,8 +175,12 @@ handle_info(Info, Channel) -> %%-------------------------------------------------------------------- %% Terminate %%-------------------------------------------------------------------- -terminate(_Reason, #channel{session = Session}) -> - emqx_lwm2m_session:on_close(Session). +terminate(Reason, #channel{ctx = Ctx, + clientinfo = ClientInfo, + session = Session}) -> + MountedTopic = emqx_lwm2m_session:on_close(Session), + _ = run_hooks(Ctx, 'session.unsubscribe', [ClientInfo, MountedTopic, #{}]), + run_hooks(Ctx, 'session.terminated', [ClientInfo, Reason, Session]). %%-------------------------------------------------------------------- %% Internal functions @@ -220,12 +226,12 @@ do_connect(Req, Result, Channel, Iter) -> Req, Channel) of {ok, _Input, #channel{session = Session, - validator = Validator} = NChannel} -> + with_context = WithContext} = NChannel} -> case emqx_lwm2m_session:info(reg_info, Session) of undefined -> process_connect(ensure_connected(NChannel), Req, Result, Iter); _ -> - NewResult = emqx_lwm2m_session:reregister(Req, Validator, Session), + NewResult = emqx_lwm2m_session:reregister(Req, WithContext, Session), iter(Iter, maps:merge(Result, NewResult), NChannel) end; {error, ReasonCode, NChannel} -> @@ -251,7 +257,7 @@ check_lwm2m_version(#coap_message{options = Opts}, end, if IsValid -> NConnInfo = ConnInfo#{ connected_at => erlang:system_time(millisecond) - , proto_name => <<"lwm2m">> + , proto_name => <<"LwM2M">> , proto_ver => Ver }, {ok, Channel#channel{conninfo = NConnInfo}}; @@ -274,7 +280,7 @@ enrich_clientinfo(#coap_message{options = Options} = Msg, Query = maps:get(uri_query, Options, #{}), case Query of #{<<"ep">> := Epn} -> - UserName = maps:get(<<"imei">>, Query, undefined), + UserName = maps:get(<<"imei">>, Query, Epn), Password = maps:get(<<"password">>, Query, undefined), ClientId = maps:get(<<"device_id">>, Query, Epn), ClientInfo = @@ -298,7 +304,7 @@ auth_connect(_Input, Channel = #channel{ctx = Ctx, case emqx_gateway_ctx:authenticate(Ctx, ClientInfo) of {ok, NClientInfo} -> {ok, Channel#channel{clientinfo = NClientInfo, - validator = validator(Ctx, ClientInfo)}}; + with_context = with_context(Ctx, ClientInfo)}}; {error, Reason} -> ?LOG(warning, "Client ~s (Username: '~s') login failed for ~0p", [ClientId, Username, Reason]), @@ -308,14 +314,13 @@ auth_connect(_Input, Channel = #channel{ctx = Ctx, fix_mountpoint(_Packet, #{mountpoint := undefined} = ClientInfo) -> {ok, ClientInfo}; fix_mountpoint(_Packet, ClientInfo = #{mountpoint := Mountpoint}) -> - %% TODO: Enrich the varibale replacement???? - %% i.e: ${ClientInfo.auth_result.productKey} Mountpoint1 = emqx_mountpoint:replvar(Mountpoint, ClientInfo), {ok, ClientInfo#{mountpoint := Mountpoint1}}. ensure_connected(Channel = #channel{ctx = Ctx, conninfo = ConnInfo, clientinfo = ClientInfo}) -> + _ = run_hooks(Ctx, 'client.connack', [ConnInfo, connection_accepted, []]), ok = run_hooks(Ctx, 'client.connected', [ClientInfo, ConnInfo]), Channel. @@ -323,7 +328,7 @@ process_connect(Channel = #channel{ctx = Ctx, session = Session, conninfo = ConnInfo, clientinfo = ClientInfo, - validator = Validator}, + with_context = WithContext}, Msg, Result, Iter) -> %% inherit the old session SessFun = fun(_,_) -> #{} end, @@ -336,7 +341,8 @@ process_connect(Channel = #channel{ctx = Ctx, emqx_lwm2m_session ) of {ok, _} -> - NewResult = emqx_lwm2m_session:init(Msg, Validator, Session), + Mountpoint = maps:get(mountpoint, ClientInfo, <<>>), + NewResult = emqx_lwm2m_session:init(Msg, Mountpoint, WithContext, Session), iter(Iter, maps:merge(Result, NewResult), Channel); {error, Reason} -> ?LOG(error, "Failed to open session du to ~p", [Reason]), @@ -358,13 +364,34 @@ gets([H | T], Map) -> gets([], Val) -> Val. +with_context(publish, [Topic, Msg], Ctx, ClientInfo) -> + case emqx_gateway_ctx:authorize(Ctx, ClientInfo, publish, Topic) of + allow -> + emqx:publish(Msg); + _ -> + ?LOG(error, "topic:~p not allow to publish ", [Topic]) + end; + +with_context(subscribe, [Topic, Opts], Ctx, #{username := UserName} = ClientInfo) -> + case emqx_gateway_ctx:authorize(Ctx, ClientInfo, subscribe, Topic) of + allow -> + run_hooks(Ctx, 'session.subscribed', [ClientInfo, Topic, UserName]), + ?LOG(debug, "Subscribe topic: ~0p, Opts: ~0p, EndpointName: ~0p", [Topic, Opts, UserName]), + emqx:subscribe(Topic, UserName, Opts); + _ -> + ?LOG(error, "Topic: ~0p not allow to subscribe", [Topic]) + end; + +with_context(metrics, Name, Ctx, _ClientInfo) -> + emqx_gateway_ctx:metrics_inc(Ctx, Name). + %%-------------------------------------------------------------------- %% Call Chain %%-------------------------------------------------------------------- call_session(Fun, Msg, #channel{session = Session, - validator = Validator} = Channel) -> + with_context = WithContext} = Channel) -> iter([ session, fun process_session/4 , proto, fun process_protocol/4 , return, fun process_return/4 @@ -373,7 +400,7 @@ call_session(Fun, , out, fun process_out/4 , fun process_nothing/3 ], - emqx_lwm2m_session:Fun(Msg, Validator, Session), + emqx_lwm2m_session:Fun(Msg, WithContext, Session), Channel). process_session(Session, Result, Channel, Iter) -> @@ -384,8 +411,8 @@ process_protocol({request, Msg}, Result, Channel, Iter) -> handle_request_protocol(Method, Msg, Result, Channel, Iter); process_protocol(Msg, Result, - #channel{validator = Validator, session = Session} = Channel, Iter) -> - ProtoResult = emqx_lwm2m_session:handle_protocol_in(Msg, Validator, Session), + #channel{with_context = WithContext, session = Session} = Channel, Iter) -> + ProtoResult = emqx_lwm2m_session:handle_protocol_in(Msg, WithContext, Session), iter(Iter, maps:merge(Result, ProtoResult), Channel). handle_request_protocol(post, #coap_message{options = Opts} = Msg, @@ -415,10 +442,10 @@ handle_request_protocol(delete, #coap_message{options = Opts} = Msg, end. do_update(Location, Msg, Result, - #channel{session = Session, validator = Validator} = Channel, Iter) -> + #channel{session = Session, with_context = WithContext} = Channel, Iter) -> case check_location(Location, Channel) of true -> - NewResult = emqx_lwm2m_session:update(Msg, Validator, Session), + NewResult = emqx_lwm2m_session:update(Msg, WithContext, Session), iter(Iter, maps:merge(Result, NewResult), Channel); _ -> iter(Iter, reply({error, not_found}, Msg, Result), Channel) @@ -438,13 +465,8 @@ process_out(Outs, Result, Channel, _) -> Reply -> [Reply | Outs2] end, - %% emqx_gateway_conn bug, work around - case Outs3 of - [] -> - {ok, Channel}; - _ -> - {ok, {outgoing, Outs3}, Channel} - end. + + {ok, {outgoing, Outs3}, Channel}. process_reply(Reply, Result, #channel{session = Session} = Channel, _) -> Session2 = emqx_lwm2m_session:set_reply(Reply, Session), diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl index 925ca1d94..7c0cc95cd 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl @@ -30,20 +30,20 @@ -define(STANDARD, 1). -%-type msg_type() :: <<"create">> -% | <<"delete">> -% | <<"read">> -% | <<"write">> -% | <<"execute">> -% | <<"discover">> -% | <<"write-attr">> -% | <<"observe">> -% | <<"cancel-observe">>. -% - %-type cmd() :: #{ <<"msgType">> := msg_type() - % , <<"data">> := maps() - % %% more keys? - % }. +%%-type msg_type() :: <<"create">> +%% | <<"delete">> +%% | <<"read">> +%% | <<"write">> +%% | <<"execute">> +%% | <<"discover">> +%% | <<"write-attr">> +%% | <<"observe">> +%% | <<"cancel-observe">>. +%% +%%-type cmd() :: #{ <<"msgType">> := msg_type() +%% , <<"data">> := maps() +%% %%%% more keys? +%% }. %%-------------------------------------------------------------------- %% APIs diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl index 700302bdc..a1d03e04f 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl @@ -22,7 +22,7 @@ -include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl"). %% API --export([new/0, init/3, update/3, reregister/3, on_close/1]). +-export([new/0, init/4, update/3, reregister/3, on_close/1]). -export([ info/1 , info/2 @@ -47,9 +47,10 @@ , wait_ack :: request_context() | undefined , endpoint_name :: binary() | undefined , location_path :: list(binary()) | undefined - , headers :: map() | undefined , reg_info :: map() | undefined , lifetime :: non_neg_integer() | undefined + , is_cache_mode :: boolean() + , mountpoint :: binary() , last_active_at :: non_neg_integer() }). @@ -61,7 +62,7 @@ <<"7">>, <<"9">>, <<"15">>]). %% uplink and downlink topic configuration --define(lwm2m_up_dm_topic, {<<"v1/up/dm">>, 0}). +-define(lwm2m_up_dm_topic, {<<"/v1/up/dm">>, 0}). %% steal from emqx_session -define(INFO_KEYS, [subscriptions, @@ -95,41 +96,44 @@ new() -> #session{ coap = emqx_coap_tm:new() , queue = queue:new() , last_active_at = ?NOW + , is_cache_mode = false + , mountpoint = <<>> , lifetime = emqx:get_config([gateway, lwm2m, lifetime_max])}. --spec init(emqx_coap_message(), function(), session()) -> map(). -init(#coap_message{options = Opts, payload = Payload} = Msg, Validator, Session) -> +-spec init(emqx_coap_message(), binary(), function(), session()) -> map(). +init(#coap_message{options = Opts, + payload = Payload} = Msg, MountPoint, WithContext, Session) -> Query = maps:get(uri_query, Opts), RegInfo = append_object_list(Query, Payload), - Headers = get_headers(RegInfo), LifeTime = get_lifetime(RegInfo), Epn = maps:get(<<"ep">>, Query), Location = [?PREFIX, Epn], - Result = return(register_init(Validator, - Session#session{headers = Headers, - endpoint_name = Epn, - location_path = Location, - reg_info = RegInfo, - lifetime = LifeTime, - queue = queue:new()})), + NewSession = Session#session{endpoint_name = Epn, + location_path = Location, + reg_info = RegInfo, + lifetime = LifeTime, + mountpoint = MountPoint, + is_cache_mode = is_psm(RegInfo) orelse is_qmode(RegInfo), + queue = queue:new()}, + Result = return(register_init(WithContext, NewSession)), Reply = emqx_coap_message:piggyback({ok, created}, Msg), Reply2 = emqx_coap_message:set(location_path, Location, Reply), reply(Reply2, Result#{lifetime => true}). -reregister(Msg, Validator, Session) -> - update(Msg, Validator, <<"register">>, Session). +reregister(Msg, WithContext, Session) -> + update(Msg, WithContext, <<"register">>, Session). -update(Msg, Validator, Session) -> - update(Msg, Validator, <<"update">>, Session). +update(Msg, WithContext, Session) -> + update(Msg, WithContext, <<"update">>, Session). --spec on_close(session()) -> ok. -on_close(#session{endpoint_name = Epn}) -> +-spec on_close(session()) -> binary(). +on_close(Session) -> #{topic := Topic} = downlink_topic(), - MountedTopic = mount(Topic, mountpoint(Epn)), + MountedTopic = mount(Topic, Session), emqx:unsubscribe(MountedTopic), - ok. + MountedTopic. %%-------------------------------------------------------------------- %% Info, Stats @@ -194,15 +198,15 @@ stats(Session) -> info(?STATS_KEYS, Session). %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- -handle_coap_in(Msg, _Validator, Session) -> +handle_coap_in(Msg, _WithContext, Session) -> call_coap(case emqx_coap_message:is_request(Msg) of true -> handle_request; _ -> handle_response end, Msg, Session#session{last_active_at = ?NOW}). -handle_deliver(Delivers, _Validator, Session) -> - return(deliver(Delivers, Session)). +handle_deliver(Delivers, WithContext, Session) -> + return(deliver(Delivers, WithContext, Session)). timeout({transport, Msg}, _, Session) -> call_coap(timeout, Msg, Session). @@ -214,17 +218,17 @@ set_reply(Msg, #session{coap = Coap} = Session) -> %%-------------------------------------------------------------------- %% Protocol Stack %%-------------------------------------------------------------------- -handle_protocol_in({response, CtxMsg}, Validator, Session) -> - return(handle_coap_response(CtxMsg, Validator, Session)); +handle_protocol_in({response, CtxMsg}, WithContext, Session) -> + return(handle_coap_response(CtxMsg, WithContext, Session)); -handle_protocol_in({ack, CtxMsg}, Validator, Session) -> - return(handle_ack(CtxMsg, Validator, Session)); +handle_protocol_in({ack, CtxMsg}, WithContext, Session) -> + return(handle_ack(CtxMsg, WithContext, Session)); -handle_protocol_in({ack_failure, CtxMsg}, Validator, Session) -> - return(handle_ack_failure(CtxMsg, Validator, Session)); +handle_protocol_in({ack_failure, CtxMsg}, WithContext, Session) -> + return(handle_ack_failure(CtxMsg, WithContext, Session)); -handle_protocol_in({reset, CtxMsg}, Validator, Session) -> - return(handle_ack_reset(CtxMsg, Validator, Session)). +handle_protocol_in({reset, CtxMsg}, WithContext, Session) -> + return(handle_ack_reset(CtxMsg, WithContext, Session)). %%-------------------------------------------------------------------- %% Register @@ -302,50 +306,6 @@ delink(Str) -> Ltrim = binary_util:ltrim(Str, $<), binary_util:rtrim(Ltrim, $>). -get_headers(RegInfo) -> - lists:foldl(fun(K, Acc) -> - get_header(K, RegInfo, Acc) - end, - extract_module_params(RegInfo), - [<<"apn">>, <<"im">>, <<"ct">>, <<"mv">>, <<"mt">>]). - -get_header(Key, RegInfo, Headers) -> - case maps:get(Key, RegInfo, undefined) of - undefined -> - Headers; - Val -> - AtomKey = erlang:binary_to_atom(Key), - Headers#{AtomKey => Val} - end. - -extract_module_params(RegInfo) -> - Keys = [<<"module">>, <<"sv">>, <<"chip">>, <<"imsi">>, <<"iccid">>], - case lists:any(fun(K) -> maps:get(K, RegInfo, undefined) =:= undefined end, Keys) of - true -> #{module_params => undefined}; - false -> - Extras = [<<"rsrp">>, <<"sinr">>, <<"txpower">>, <<"cellid">>], - case lists:any(fun(K) -> maps:get(K, RegInfo, undefined) =:= undefined end, Extras) of - true -> - #{module_params => - #{module => maps:get(<<"module">>, RegInfo), - softversion => maps:get(<<"sv">>, RegInfo), - chiptype => maps:get(<<"chip">>, RegInfo), - imsi => maps:get(<<"imsi">>, RegInfo), - iccid => maps:get(<<"iccid">>, RegInfo)}}; - false -> - #{module_params => - #{module => maps:get(<<"module">>, RegInfo), - softversion => maps:get(<<"sv">>, RegInfo), - chiptype => maps:get(<<"chip">>, RegInfo), - imsi => maps:get(<<"imsi">>, RegInfo), - iccid => maps:get(<<"iccid">>, RegInfo), - rsrp => maps:get(<<"rsrp">>, RegInfo), - sinr => maps:get(<<"sinr">>, RegInfo), - txpower => maps:get(<<"txpower">>, RegInfo), - cellid => maps:get(<<"cellid">>, RegInfo)}} - end - end. - get_lifetime(#{<<"lt">> := LT}) -> case LT of 0 -> emqx:get_config([gateway, lwm2m, lifetime_max]); @@ -362,7 +322,7 @@ get_lifetime(_, OldRegInfo) -> -spec update(emqx_coap_message(), function(), binary(), session()) -> map(). update(#coap_message{options = Opts, payload = Payload} = Msg, - Validator, + WithContext, CmdType, #session{reg_info = OldRegInfo} = Session) -> Query = maps:get(uri_query, Opts), @@ -370,58 +330,51 @@ update(#coap_message{options = Opts, payload = Payload} = Msg, UpdateRegInfo = maps:merge(OldRegInfo, RegInfo), LifeTime = get_lifetime(UpdateRegInfo, OldRegInfo), - Session2 = proto_subscribe(Validator, - Session#session{reg_info = UpdateRegInfo, - lifetime = LifeTime}), + NewSession = Session#session{reg_info = UpdateRegInfo, + is_cache_mode = + is_psm(UpdateRegInfo) orelse is_qmode(UpdateRegInfo), + lifetime = LifeTime}, + + Session2 = proto_subscribe(WithContext, NewSession), Session3 = send_dl_msg(Session2), RegPayload = #{<<"data">> => UpdateRegInfo}, - Session4 = send_to_mqtt(#{}, CmdType, RegPayload, Validator, Session3), + Session4 = send_to_mqtt(#{}, CmdType, RegPayload, WithContext, Session3), Result = return(Session4), Reply = emqx_coap_message:piggyback({ok, changed}, Msg), reply(Reply, Result#{lifetime => true}). -register_init(Validator, #session{reg_info = RegInfo, - endpoint_name = Epn} = Session) -> - +register_init(WithContext, #session{reg_info = RegInfo} = Session) -> Session2 = send_auto_observe(RegInfo, Session), %% - subscribe to the downlink_topic and wait for commands #{topic := Topic, qos := Qos} = downlink_topic(), - MountedTopic = mount(Topic, mountpoint(Epn)), - Session3 = subscribe(MountedTopic, Qos, Validator, Session2), + MountedTopic = mount(Topic, Session), + Session3 = subscribe(MountedTopic, Qos, WithContext, Session2), Session4 = send_dl_msg(Session3), %% - report the registration info RegPayload = #{<<"data">> => RegInfo}, - send_to_mqtt(#{}, <<"register">>, RegPayload, Validator, Session4). + send_to_mqtt(#{}, <<"register">>, RegPayload, WithContext, Session4). %%-------------------------------------------------------------------- %% Subscribe %%-------------------------------------------------------------------- -proto_subscribe(Validator, #session{endpoint_name = Epn, wait_ack = WaitAck} = Session) -> +proto_subscribe(WithContext, #session{wait_ack = WaitAck} = Session) -> #{topic := Topic, qos := Qos} = downlink_topic(), - MountedTopic = mount(Topic, mountpoint(Epn)), + MountedTopic = mount(Topic, Session), Session2 = case WaitAck of undefined -> Session; Ctx -> MqttPayload = emqx_lwm2m_cmd:coap_failure_to_mqtt(Ctx, <<"coap_timeout">>), - send_to_mqtt(Ctx, <<"coap_timeout">>, MqttPayload, Validator, Session) + send_to_mqtt(Ctx, <<"coap_timeout">>, MqttPayload, WithContext, Session) end, - subscribe(MountedTopic, Qos, Validator, Session2). + subscribe(MountedTopic, Qos, WithContext, Session2). -subscribe(Topic, Qos, Validator, - #session{headers = Headers, endpoint_name = EndpointName} = Session) -> - case Validator(subscribe, Topic) of - allow -> - ClientId = maps:get(device_id, Headers, undefined), - Opts = get_sub_opts(Qos), - ?LOG(debug, "Subscribe topic: ~0p, Opts: ~0p, EndpointName: ~0p", [Topic, Opts, EndpointName]), - emqx:subscribe(Topic, ClientId, Opts); - _ -> - ?LOG(error, "Topic: ~0p not allow to subscribe", [Topic]) - end, +subscribe(Topic, Qos, WithContext, Session) -> + Opts = get_sub_opts(Qos), + WithContext(subscribe, [Topic, Opts]), Session. send_auto_observe(RegInfo, Session) -> @@ -486,7 +439,7 @@ handle_coap_response({Ctx = #{<<"msgType">> := EventType}, type = CoapMsgType, payload = CoapMsgPayload, options = CoapMsgOpts}}, - Validator, + WithContext, Session) -> MqttPayload = emqx_lwm2m_cmd:coap_to_mqtt(CoapMsgMethod, CoapMsgPayload, CoapMsgOpts, Ctx), {ReqPath, _} = emqx_lwm2m_cmd:path_list(emqx_lwm2m_cmd:extract_path(Ctx)), @@ -495,46 +448,43 @@ handle_coap_response({Ctx = #{<<"msgType">> := EventType}, {[<<"5">>| _], _, <<"observe">>, CoapMsgType} when CoapMsgType =/= ack -> %% this is a notification for status update during NB firmware upgrade. %% need to reply to DM http callbacks - send_to_mqtt(Ctx, <<"notify">>, MqttPayload, ?lwm2m_up_dm_topic, Validator, Session); + send_to_mqtt(Ctx, <<"notify">>, MqttPayload, ?lwm2m_up_dm_topic, WithContext, Session); {_ReqPath, _, <<"observe">>, CoapMsgType} when CoapMsgType =/= ack -> %% this is actually a notification, correct the msgType - send_to_mqtt(Ctx, <<"notify">>, MqttPayload, Validator, Session); + send_to_mqtt(Ctx, <<"notify">>, MqttPayload, WithContext, Session); _ -> - send_to_mqtt(Ctx, EventType, MqttPayload, Validator, Session) + send_to_mqtt(Ctx, EventType, MqttPayload, WithContext, Session) end, send_dl_msg(Ctx, Session2). %%-------------------------------------------------------------------- %% Ack %%-------------------------------------------------------------------- -handle_ack({Ctx, _}, Validator, Session) -> +handle_ack({Ctx, _}, WithContext, Session) -> Session2 = send_dl_msg(Ctx, Session), MqttPayload = emqx_lwm2m_cmd:empty_ack_to_mqtt(Ctx), - send_to_mqtt(Ctx, <<"ack">>, MqttPayload, Validator, Session2). + send_to_mqtt(Ctx, <<"ack">>, MqttPayload, WithContext, Session2). %%-------------------------------------------------------------------- %% Ack Failure(Timeout/Reset) %%-------------------------------------------------------------------- -handle_ack_failure({Ctx, _}, Validator, Session) -> - handle_ack_failure(Ctx, <<"coap_timeout">>, Validator, Session). +handle_ack_failure({Ctx, _}, WithContext, Session) -> + handle_ack_failure(Ctx, <<"coap_timeout">>, WithContext, Session). -handle_ack_reset({Ctx, _}, Validator, Session) -> - handle_ack_failure(Ctx, <<"coap_reset">>, Validator, Session). +handle_ack_reset({Ctx, _}, WithContext, Session) -> + handle_ack_failure(Ctx, <<"coap_reset">>, WithContext, Session). -handle_ack_failure(Ctx, MsgType, Validator, Session) -> +handle_ack_failure(Ctx, MsgType, WithContext, Session) -> Session2 = may_send_dl_msg(coap_timeout, Ctx, Session), MqttPayload = emqx_lwm2m_cmd:coap_failure_to_mqtt(Ctx, MsgType), - send_to_mqtt(Ctx, MsgType, MqttPayload, Validator, Session2). + send_to_mqtt(Ctx, MsgType, MqttPayload, WithContext, Session2). %%-------------------------------------------------------------------- %% Send To CoAP %%-------------------------------------------------------------------- -may_send_dl_msg(coap_timeout, Ctx, #session{headers = Headers, - reg_info = RegInfo, - wait_ack = WaitAck} = Session) -> - Lwm2mMode = maps:get(lwm2m_model, Headers, undefined), - case is_cache_mode(Lwm2mMode, RegInfo, Session) of +may_send_dl_msg(coap_timeout, Ctx, #session{wait_ack = WaitAck} = Session) -> + case is_cache_mode(Session) of false -> send_dl_msg(Ctx, Session); true -> case WaitAck of @@ -545,14 +495,11 @@ may_send_dl_msg(coap_timeout, Ctx, #session{headers = Headers, end end. -is_cache_mode(Lwm2mMode, RegInfo, #session{last_active_at = LastActiveAt}) -> - case Lwm2mMode =:= psm orelse is_psm(RegInfo) orelse is_qmode(RegInfo) of - true -> - QModeTimeWind = emqx:get_config([gateway, lwm2m, qmode_time_window]), - Now = ?NOW, - (Now - LastActiveAt) >= QModeTimeWind; - false -> false - end. +is_cache_mode(#session{is_cache_mode = IsCacheMode, + last_active_at = LastActiveAt}) -> + IsCacheMode andalso + ((?NOW - LastActiveAt) >= + emqx:get_config([gateway, lwm2m, qmode_time_window])). is_psm(#{<<"apn">> := APN}) when APN =:= <<"Ctnb">>; APN =:= <<"psmA.eDRX0.ctnb">>; @@ -611,54 +558,27 @@ send_msg_not_waiting_ack(Ctx, Req, Session) -> %%-------------------------------------------------------------------- %% Send To MQTT %%-------------------------------------------------------------------- -send_to_mqtt(Ref, EventType, Payload, Validator, Session = #session{headers = Headers}) -> +send_to_mqtt(Ref, EventType, Payload, WithContext, Session) -> #{topic := Topic, qos := Qos} = uplink_topic(EventType), - NHeaders = extract_ext_flags(Headers), Mheaders = maps:get(mheaders, Ref, #{}), - NHeaders1 = maps:merge(NHeaders, Mheaders), - proto_publish(Topic, Payload#{<<"msgType">> => EventType}, Qos, NHeaders1, Validator, Session). + proto_publish(Topic, Payload#{<<"msgType">> => EventType}, Qos, Mheaders, WithContext, Session). send_to_mqtt(Ctx, EventType, Payload, {Topic, Qos}, - Validator, #session{headers = Headers} = Session) -> + WithContext, Session) -> Mheaders = maps:get(mheaders, Ctx, #{}), - NHeaders = extract_ext_flags(Headers), - NHeaders1 = maps:merge(NHeaders, Mheaders), - proto_publish(Topic, Payload#{<<"msgType">> => EventType}, Qos, NHeaders1, Validator, Session). + proto_publish(Topic, Payload#{<<"msgType">> => EventType}, Qos, Mheaders, WithContext, Session). -proto_publish(Topic, Payload, Qos, Headers, Validator, +proto_publish(Topic, Payload, Qos, Headers, WithContext, #session{endpoint_name = Epn} = Session) -> - MountedTopic = mount(Topic, mountpoint(Epn)), - _ = case Validator(publish, MountedTopic) of - allow -> - Msg = emqx_message:make(Epn, Qos, MountedTopic, - emqx_json:encode(Payload), #{}, Headers), - emqx:publish(Msg); - _ -> - ?LOG(error, "topic:~p not allow to publish ", [MountedTopic]) - end, + MountedTopic = mount(Topic, Session), + Msg = emqx_message:make(Epn, Qos, MountedTopic, + emqx_json:encode(Payload), #{}, Headers), + WithContext(publish, [MountedTopic, Msg]), Session. -mountpoint(Epn) -> - Prefix = emqx:get_config([gateway, lwm2m, mountpoint]), - <>. - -mount(Topic, MountPoint) when is_binary(Topic), is_binary(MountPoint) -> +mount(Topic, #session{mountpoint = MountPoint}) when is_binary(Topic) -> <>. -extract_ext_flags(Headers) -> - Header0 = #{is_tr => maps:get(is_tr, Headers, true)}, - check(Header0, Headers, [sota_type, appId, nbgwFlag]). - -check(Params, _Headers, []) -> Params; -check(Params, Headers, [Key | Rest]) -> - case maps:get(Key, Headers, null) of - V when V == undefined; V == null -> - check(Params, Headers, Rest); - Value -> - Params1 = Params#{Key => Value}, - check(Params1, Headers, Rest) - end. - downlink_topic() -> emqx:get_config([gateway, lwm2m, translators, command]). @@ -678,29 +598,30 @@ uplink_topic(_) -> %% Deliver %%-------------------------------------------------------------------- -deliver(Delivers, #session{headers = Headers, reg_info = RegInfo} = Session) -> - Lwm2mMode = maps:get(lwm2m_model, Headers, undefined), - IsCacheMode = is_cache_mode(Lwm2mMode, RegInfo, Session), +deliver(Delivers, WithContext, #session{reg_info = RegInfo} = Session) -> + IsCacheMode = is_cache_mode(Session), AlternatePath = maps:get(<<"alternatePath">>, RegInfo, <<"/">>), lists:foldl(fun({deliver, _, MQTT}, Acc) -> deliver_to_coap(AlternatePath, - MQTT#message.payload, MQTT, IsCacheMode, Acc) + MQTT#message.payload, MQTT, IsCacheMode, WithContext, Acc) end, Session, Delivers). -deliver_to_coap(AlternatePath, JsonData, MQTT, CacheMode, Session) when is_binary(JsonData)-> +deliver_to_coap(AlternatePath, JsonData, MQTT, CacheMode, WithContext, Session) when is_binary(JsonData)-> try TermData = emqx_json:decode(JsonData, [return_maps]), - deliver_to_coap(AlternatePath, TermData, MQTT, CacheMode, Session) + deliver_to_coap(AlternatePath, TermData, MQTT, CacheMode, WithContext, Session) catch ExClass:Error:ST -> ?LOG(error, "deliver_to_coap - Invalid JSON: ~0p, Exception: ~0p, stacktrace: ~0p", [JsonData, {ExClass, Error}, ST]), + WithContext(metrics, 'delivery.dropped'), Session end; -deliver_to_coap(AlternatePath, TermData, MQTT, CacheMode, Session) when is_map(TermData) -> +deliver_to_coap(AlternatePath, TermData, MQTT, CacheMode, WithContext, Session) when is_map(TermData) -> + WithContext(metrics, 'messages.delivered'), {Req, Ctx} = emqx_lwm2m_cmd:mqtt_to_coap(AlternatePath, TermData), ExpiryTime = get_expiry_time(MQTT), maybe_do_deliver_to_coap(Ctx, Req, ExpiryTime, CacheMode, Session). diff --git a/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl b/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl index e355e05cf..27b8f82a1 100644 --- a/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl @@ -35,14 +35,14 @@ gateway.lwm2m { lifetime_max = 86400s qmode_time_windonw = 22 auto_observe = false - mountpoint = \"lwm2m\" + mountpoint = \"lwm2m/%u\" update_msg_publish_condition = contains_object_list translators { - command = {topic = \"dn/#\", qos = 0} - response = {topic = \"up/resp\", qos = 0} - notify = {topic = \"up/notify\", qos = 0} - register = {topic = \"up/resp\", qos = 0} - update = {topic = \"up/resp\", qos = 0} + command = {topic = \"/dn/#\", qos = 0} + response = {topic = \"/up/resp\", qos = 0} + notify = {topic = \"/up/notify\", qos = 0} + register = {topic = \"/up/resp\", qos = 0} + update = {topic = \"/up/resp\", qos = 0} } listeners.udp.default { bind = 5783 @@ -66,7 +66,7 @@ all() -> , {group, test_grp_6_observe} %% {group, test_grp_8_object_19} - %% {group, test_grp_9_psm_queue_mode} + , {group, test_grp_9_psm_queue_mode} ]. suite() -> [{timetrap, {seconds, 90}}]. @@ -1750,7 +1750,7 @@ server_cache_mode(Config, RegOption) -> verify_read_response_1(0, UdpSock), %% server inters into PSM mode - timer:sleep(2), + timer:sleep(2500), %% verify server caches downlink commands send_read_command_1(1, UdpSock), @@ -1797,6 +1797,7 @@ verify_read_response_1(CmdId, UdpSock) -> ReadResult = emqx_json:encode(#{ <<"requestID">> => CmdId, <<"cacheID">> => CmdId, <<"msgType">> => <<"read">>, <<"data">> => #{ + <<"reqPath">> => <<"/3/0/0">>, <<"code">> => <<"2.05">>, <<"codeMsg">> => <<"content">>, <<"content">> => [#{ From 627de1d58c2adbb3bb906a7dfb6e41daf723bf28 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Tue, 7 Sep 2021 10:29:45 +0800 Subject: [PATCH 072/109] fix(test): fix test case --- apps/emqx/test/emqx_authentication_SUITE.erl | 238 ++++++++++++++++++ .../src/simple_authn/emqx_authn_http.erl | 4 +- apps/emqx_authn/test/emqx_authn_SUITE.erl | 5 + apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl | 8 +- .../test/emqx_authn_mnesia_SUITE.erl | 8 +- .../src/emqx_connector_schema_lib.erl | 4 +- apps/emqx_gateway/src/emqx_gateway.app.src | 2 +- apps/emqx_gateway/test/emqx_exproto_SUITE.erl | 7 +- .../test/emqx_gateway_registry_SUITE.erl | 4 +- apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl | 4 +- .../test/emqx_sn_protocol_SUITE.erl | 4 +- apps/emqx_gateway/test/emqx_stomp_SUITE.erl | 4 +- 12 files changed, 268 insertions(+), 24 deletions(-) create mode 100644 apps/emqx/test/emqx_authentication_SUITE.erl diff --git a/apps/emqx/test/emqx_authentication_SUITE.erl b/apps/emqx/test/emqx_authentication_SUITE.erl new file mode 100644 index 000000000..a940adc88 --- /dev/null +++ b/apps/emqx/test/emqx_authentication_SUITE.erl @@ -0,0 +1,238 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authentication_SUITE). + +-behaviour(hocon_schema). +-behaviour(emqx_authentication). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("typerefl/include/types.hrl"). + +-export([ fields/1 ]). + +-export([ refs/0 + , create/1 + , update/2 + , authenticate/2 + , destroy/1 + ]). + +-define(AUTHN, emqx_authentication). + +%%------------------------------------------------------------------------------ +%% Hocon Schema +%%------------------------------------------------------------------------------ + +fields(type1) -> + [ {mechanism, {enum, ['password-based']}} + , {backend, {enum, ['built-in-database']}} + , {enable, fun enable/1} + ]; + +fields(type2) -> + [ {mechanism, {enum, ['password-based']}} + , {backend, {enum, ['mysql']}} + , {enable, fun enable/1} + ]. + +enable(type) -> boolean(); +enable(default) -> true; +enable(_) -> undefined. + +%%------------------------------------------------------------------------------ +%% Callbacks +%%------------------------------------------------------------------------------ + +refs() -> + [ hoconsc:ref(?MODULE, type1) + , hoconsc:ref(?MODULE, type2) + ]. + +create(_Config) -> + {ok, #{mark => 1}}. + +update(_Config, _State) -> + {ok, #{mark => 2}}. + +authenticate(#{username := <<"good">>}, _State) -> + {ok, #{superuser => true}}; +authenticate(#{username := _}, _State) -> + {error, bad_username_or_password}. + +destroy(_State) -> + ok. + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + application:set_env(ekka, strict_mode, true), + emqx_ct_helpers:start_apps([]), + Config. + +end_per_suite(_) -> + emqx_ct_helpers:stop_apps([]), + ok. + +t_chain(_) -> + % CRUD of authentication chain + ChainName = <<"test">>, + ?assertMatch({ok, []}, ?AUTHN:list_chains()), + ?assertMatch({ok, #{name := ChainName, authenticators := []}}, ?AUTHN:create_chain(ChainName)), + ?assertEqual({error, {already_exists, {chain, ChainName}}}, ?AUTHN:create_chain(ChainName)), + ?assertMatch({ok, #{name := ChainName, authenticators := []}}, ?AUTHN:lookup_chain(ChainName)), + ?assertMatch({ok, [#{name := ChainName}]}, ?AUTHN:list_chains()), + ?assertEqual(ok, ?AUTHN:delete_chain(ChainName)), + ?assertMatch({error, {not_found, {chain, ChainName}}}, ?AUTHN:lookup_chain(ChainName)), + ok. + +t_authenticator(_) -> + ChainName = <<"test">>, + AuthenticatorConfig1 = #{mechanism => 'password-based', + backend => 'built-in-database', + enable => true}, + + % Create an authenticator when the authentication chain does not exist + ?assertEqual({error, {not_found, {chain, ChainName}}}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)), + ?AUTHN:create_chain(ChainName), + % Create an authenticator when the provider does not exist + ?assertEqual({error, no_available_provider}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)), + + AuthNType1 = {'password-based', 'built-in-database'}, + ?AUTHN:add_provider(AuthNType1, ?MODULE), + ID1 = <<"password-based:built-in-database">>, + + % CRUD of authencaticator + ?assertMatch({ok, #{id := ID1, state := #{mark := 1}}}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)), + ?assertMatch({ok, #{id := ID1}}, ?AUTHN:lookup_authenticator(ChainName, ID1)), + ?assertMatch({ok, [#{id := ID1}]}, ?AUTHN:list_authenticators(ChainName)), + ?assertEqual({error, {already_exists, {authenticator, ID1}}}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)), + ?assertMatch({ok, #{id := ID1, state := #{mark := 2}}}, ?AUTHN:update_authenticator(ChainName, ID1, AuthenticatorConfig1)), + ?assertEqual(ok, ?AUTHN:delete_authenticator(ChainName, ID1)), + ?assertEqual({error, {not_found, {authenticator, ID1}}}, ?AUTHN:update_authenticator(ChainName, ID1, AuthenticatorConfig1)), + ?assertMatch({ok, []}, ?AUTHN:list_authenticators(ChainName)), + + % Multiple authenticators exist at the same time + AuthNType2 = {'password-based', mysql}, + ?AUTHN:add_provider(AuthNType2, ?MODULE), + ID2 = <<"password-based:mysql">>, + AuthenticatorConfig2 = #{mechanism => 'password-based', + backend => mysql, + enable => true}, + ?assertMatch({ok, #{id := ID1}}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)), + ?assertMatch({ok, #{id := ID2}}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig2)), + + % Move authenticator + ?assertMatch({ok, [#{id := ID1}, #{id := ID2}]}, ?AUTHN:list_authenticators(ChainName)), + ?assertEqual(ok, ?AUTHN:move_authenticator(ChainName, ID2, top)), + ?assertMatch({ok, [#{id := ID2}, #{id := ID1}]}, ?AUTHN:list_authenticators(ChainName)), + ?assertEqual(ok, ?AUTHN:move_authenticator(ChainName, ID2, bottom)), + ?assertMatch({ok, [#{id := ID1}, #{id := ID2}]}, ?AUTHN:list_authenticators(ChainName)), + ?assertEqual(ok, ?AUTHN:move_authenticator(ChainName, ID2, {before, ID1})), + ?assertMatch({ok, [#{id := ID2}, #{id := ID1}]}, ?AUTHN:list_authenticators(ChainName)), + + ?AUTHN:delete_chain(ChainName), + ?AUTHN:remove_provider(AuthNType1), + ?AUTHN:remove_provider(AuthNType2), + ok. + +t_authenticate(_) -> + ListenerID = <<"tcp:default">>, + ClientInfo = #{zone => default, + listener => ListenerID, + protocol => mqtt, + username => <<"good">>, + password => <<"any">>}, + ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)), + + AuthNType = {'password-based', 'built-in-database'}, + ?AUTHN:add_provider(AuthNType, ?MODULE), + + AuthenticatorConfig = #{mechanism => 'password-based', + backend => 'built-in-database', + enable => true}, + ?AUTHN:create_chain(ListenerID), + ?assertMatch({ok, _}, ?AUTHN:create_authenticator(ListenerID, AuthenticatorConfig)), + ?assertEqual({ok, #{superuser => true}}, emqx_access_control:authenticate(ClientInfo)), + ?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo#{username => <<"bad">>})), + + ?AUTHN:delete_chain(ListenerID), + ?AUTHN:remove_provider(AuthNType), + ok. + +t_update_config(_) -> + emqx_config_handler:add_handler([authentication], emqx_authentication), + + AuthNType1 = {'password-based', 'built-in-database'}, + AuthNType2 = {'password-based', mysql}, + ?AUTHN:add_provider(AuthNType1, ?MODULE), + ?AUTHN:add_provider(AuthNType2, ?MODULE), + + Global = <<"mqtt:global">>, + AuthenticatorConfig1 = #{mechanism => 'password-based', + backend => 'built-in-database', + enable => true}, + AuthenticatorConfig2 = #{mechanism => 'password-based', + backend => mysql, + enable => true}, + ID1 = <<"password-based:built-in-database">>, + ID2 = <<"password-based:mysql">>, + + ?assertMatch({ok, []}, ?AUTHN:list_chains()), + ?assertMatch({ok, _}, update_config([authentication], {create_authenticator, Global, AuthenticatorConfig1})), + ?assertMatch({ok, #{id := ID1, state := #{mark := 1}}}, ?AUTHN:lookup_authenticator(Global, ID1)), + + ?assertMatch({ok, _}, update_config([authentication], {create_authenticator, Global, AuthenticatorConfig2})), + ?assertMatch({ok, #{id := ID2, state := #{mark := 1}}}, ?AUTHN:lookup_authenticator(Global, ID2)), + + ?assertMatch({ok, _}, update_config([authentication], {update_authenticator, Global, ID1, #{}})), + ?assertMatch({ok, #{id := ID1, state := #{mark := 2}}}, ?AUTHN:lookup_authenticator(Global, ID1)), + + ?assertMatch({ok, _}, update_config([authentication], {move_authenticator, Global, ID2, <<"top">>})), + ?assertMatch({ok, [#{id := ID2}, #{id := ID1}]}, ?AUTHN:list_authenticators(Global)), + + ?assertMatch({ok, _}, update_config([authentication], {delete_authenticator, Global, ID1})), + ?assertEqual({error, {not_found, {authenticator, ID1}}}, ?AUTHN:lookup_authenticator(Global, ID1)), + + ListenerID = <<"tcp:default">>, + ConfKeyPath = [listeners, tcp, default, authentication], + ?assertMatch({ok, _}, update_config(ConfKeyPath, {create_authenticator, ListenerID, AuthenticatorConfig1})), + ?assertMatch({ok, #{id := ID1, state := #{mark := 1}}}, ?AUTHN:lookup_authenticator(ListenerID, ID1)), + + ?assertMatch({ok, _}, update_config(ConfKeyPath, {create_authenticator, ListenerID, AuthenticatorConfig2})), + ?assertMatch({ok, #{id := ID2, state := #{mark := 1}}}, ?AUTHN:lookup_authenticator(ListenerID, ID2)), + + ?assertMatch({ok, _}, update_config(ConfKeyPath, {update_authenticator, ListenerID, ID1, #{}})), + ?assertMatch({ok, #{id := ID1, state := #{mark := 2}}}, ?AUTHN:lookup_authenticator(ListenerID, ID1)), + + ?assertMatch({ok, _}, update_config(ConfKeyPath, {move_authenticator, ListenerID, ID2, <<"top">>})), + ?assertMatch({ok, [#{id := ID2}, #{id := ID1}]}, ?AUTHN:list_authenticators(ListenerID)), + + ?assertMatch({ok, _}, update_config(ConfKeyPath, {delete_authenticator, ListenerID, ID1})), + ?assertEqual({error, {not_found, {authenticator, ID1}}}, ?AUTHN:lookup_authenticator(ListenerID, ID1)), + + ?AUTHN:delete_chain(Global), + ?AUTHN:remove_provider(AuthNType1), + ?AUTHN:remove_provider(AuthNType2), + ok. + +update_config(Path, ConfigRequest) -> + emqx:update_config(Path, ConfigRequest, #{rawconf_with_defaults => true}). diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl index 1bec0d903..19417218d 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -251,8 +251,8 @@ generate_request(Credential, #{method := Method, post -> NPath = append_query(Path, BaseQuery), ContentType = proplists:get_value(<<"content-type">>, Headers), - Body = serialize_body(ContentType, Body), - {NPath, Headers, Body} + NBody = serialize_body(ContentType, Body), + {NPath, Headers, NBody} end. replace_placeholders(KVs, Credential) -> diff --git a/apps/emqx_authn/test/emqx_authn_SUITE.erl b/apps/emqx_authn/test/emqx_authn_SUITE.erl index 31bac76a3..74ec397cc 100644 --- a/apps/emqx_authn/test/emqx_authn_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_SUITE.erl @@ -15,3 +15,8 @@ %%-------------------------------------------------------------------- -module(emqx_authn_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +all() -> emqx_ct:all(?MODULE). \ No newline at end of file diff --git a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl index 5e06211a7..9d8b1d9fc 100644 --- a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl @@ -16,8 +16,8 @@ -module(emqx_authn_jwt_SUITE). -% -compile(export_all). -% -compile(nowarn_export_all). +-compile(export_all). +-compile(nowarn_export_all). % -include_lib("common_test/include/ct.hrl"). % -include_lib("eunit/include/eunit.hrl"). @@ -26,8 +26,8 @@ % -define(AUTH, emqx_authn). -% all() -> -% emqx_ct:all(?MODULE). +all() -> + emqx_ct:all(?MODULE). % init_per_suite(Config) -> % emqx_ct_helpers:start_apps([emqx_authn]), diff --git a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl index acfe71809..4bc6961dd 100644 --- a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl @@ -16,8 +16,8 @@ -module(emqx_authn_mnesia_SUITE). -% -compile(export_all). -% -compile(nowarn_export_all). +-compile(export_all). +-compile(nowarn_export_all). % -include_lib("common_test/include/ct.hrl"). % -include_lib("eunit/include/eunit.hrl"). @@ -26,8 +26,8 @@ % -define(AUTH, emqx_authn). -% all() -> -% emqx_ct:all(?MODULE). +all() -> + emqx_ct:all(?MODULE). % init_per_suite(Config) -> % emqx_ct_helpers:start_apps([emqx_authn]), diff --git a/apps/emqx_connector/src/emqx_connector_schema_lib.erl b/apps/emqx_connector/src/emqx_connector_schema_lib.erl index 5f9472cca..a6d33ffca 100644 --- a/apps/emqx_connector/src/emqx_connector_schema_lib.erl +++ b/apps/emqx_connector/src/emqx_connector_schema_lib.erl @@ -142,7 +142,9 @@ to_ip_port(Str) -> _ -> {error, Str} end. -ip_port_to_string({Ip, Port}) -> +ip_port_to_string({Ip, Port}) when is_list(Ip) -> + iolist_to_binary([Ip, ":", integer_to_list(Port)]); +ip_port_to_string({Ip, Port}) when is_tuple(Ip) -> iolist_to_binary([inet:ntoa(Ip), ":", integer_to_list(Port)]). to_servers(Str) -> diff --git a/apps/emqx_gateway/src/emqx_gateway.app.src b/apps/emqx_gateway/src/emqx_gateway.app.src index e25b767cc..2fc329711 100644 --- a/apps/emqx_gateway/src/emqx_gateway.app.src +++ b/apps/emqx_gateway/src/emqx_gateway.app.src @@ -3,7 +3,7 @@ {vsn, "0.1.0"}, {registered, []}, {mod, {emqx_gateway_app, []}}, - {applications, [kernel, stdlib, grpc, lwm2m_coap, emqx, emqx_authn]}, + {applications, [kernel, stdlib, grpc, lwm2m_coap, emqx]}, {env, []}, {modules, []}, {licenses, ["Apache 2.0"]}, diff --git a/apps/emqx_gateway/test/emqx_exproto_SUITE.erl b/apps/emqx_gateway/test/emqx_exproto_SUITE.erl index 4902aacf5..4ab91da5d 100644 --- a/apps/emqx_gateway/test/emqx_exproto_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_exproto_SUITE.erl @@ -55,20 +55,19 @@ metrics() -> init_per_group(GrpName, Cfg) -> put(grpname, GrpName), Svrs = emqx_exproto_echo_svr:start(), - emqx_ct_helpers:start_apps([emqx_authn, emqx_gateway], fun set_special_cfg/1), + emqx_ct_helpers:start_apps([emqx_gateway], fun set_special_cfg/1), emqx_logger:set_log_level(debug), [{servers, Svrs}, {listener_type, GrpName} | Cfg]. end_per_group(_, Cfg) -> - emqx_ct_helpers:stop_apps([emqx_gateway, emqx_authn]), + emqx_ct_helpers:stop_apps([emqx_gateway]), emqx_exproto_echo_svr:stop(proplists:get_value(servers, Cfg)). set_special_cfg(emqx_gateway) -> LisType = get(grpname), emqx_config:put( [gateway, exproto], - #{authentication => #{enable => false}, - server => #{bind => 9100}, + #{server => #{bind => 9100}, handler => #{address => "http://127.0.0.1:9001"}, listeners => listener_confs(LisType) }); diff --git a/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl b/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl index da03b17c5..56776957f 100644 --- a/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_gateway_registry_SUITE.erl @@ -35,11 +35,11 @@ all() -> emqx_ct:all(?MODULE). init_per_suite(Cfg) -> ok = emqx_config:init_load(emqx_gateway_schema, ?CONF_DEFAULT), - emqx_ct_helpers:start_apps([emqx_authn, emqx_gateway]), + emqx_ct_helpers:start_apps([emqx_gateway]), Cfg. end_per_suite(_Cfg) -> - emqx_ct_helpers:stop_apps([emqx_authn, emqx_gateway]), + emqx_ct_helpers:stop_apps([emqx_gateway]), ok. %%-------------------------------------------------------------------- diff --git a/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl b/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl index e355e05cf..5df13f8d6 100644 --- a/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl @@ -148,12 +148,12 @@ groups() -> ]. init_per_suite(Config) -> - emqx_ct_helpers:start_apps([emqx_authn]), + emqx_ct_helpers:start_apps([]), Config. end_per_suite(Config) -> timer:sleep(300), - emqx_ct_helpers:stop_apps([emqx_authn]), + emqx_ct_helpers:stop_apps([]), Config. init_per_testcase(_AllTestCase, Config) -> diff --git a/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl b/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl index 2fbd031ff..e4b3d0095 100644 --- a/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl @@ -83,11 +83,11 @@ all() -> init_per_suite(Config) -> ok = emqx_config:init_load(emqx_gateway_schema, ?CONF_DEFAULT), - emqx_ct_helpers:start_apps([emqx_authn, emqx_gateway]), + emqx_ct_helpers:start_apps([emqx_gateway]), Config. end_per_suite(_) -> - emqx_ct_helpers:stop_apps([emqx_gateway, emqx_authn]). + emqx_ct_helpers:stop_apps([emqx_gateway]). %%-------------------------------------------------------------------- %% Test cases diff --git a/apps/emqx_gateway/test/emqx_stomp_SUITE.erl b/apps/emqx_gateway/test/emqx_stomp_SUITE.erl index 75f6dadc3..9c3f1090f 100644 --- a/apps/emqx_gateway/test/emqx_stomp_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_stomp_SUITE.erl @@ -43,11 +43,11 @@ all() -> emqx_ct:all(?MODULE). init_per_suite(Cfg) -> ok = emqx_config:init_load(emqx_gateway_schema, ?CONF_DEFAULT), - emqx_ct_helpers:start_apps([emqx_authn, emqx_gateway]), + emqx_ct_helpers:start_apps([emqx_gateway]), Cfg. end_per_suite(_Cfg) -> - emqx_ct_helpers:stop_apps([emqx_gateway, emqx_authn]), + emqx_ct_helpers:stop_apps([emqx_gateway]), ok. %%-------------------------------------------------------------------- From 1699a8dc63046cf56b9775636c6efb10fc53f3e5 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Mon, 6 Sep 2021 09:39:27 +0800 Subject: [PATCH 073/109] chore(authz): rename authorization_rules.conf to acl.conf --- apps/emqx_authz/etc/{authorization_rules.conf => acl.conf} | 0 apps/emqx_authz/etc/emqx_authz.conf | 2 +- apps/emqx_authz/src/emqx_authz_api_sources.erl | 4 ++-- apps/emqx_authz/test/emqx_authz_SUITE.erl | 2 +- apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl | 2 +- rebar.config.erl | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) rename apps/emqx_authz/etc/{authorization_rules.conf => acl.conf} (100%) diff --git a/apps/emqx_authz/etc/authorization_rules.conf b/apps/emqx_authz/etc/acl.conf similarity index 100% rename from apps/emqx_authz/etc/authorization_rules.conf rename to apps/emqx_authz/etc/acl.conf diff --git a/apps/emqx_authz/etc/emqx_authz.conf b/apps/emqx_authz/etc/emqx_authz.conf index 99b14f5fe..ed4ad573c 100644 --- a/apps/emqx_authz/etc/emqx_authz.conf +++ b/apps/emqx_authz/etc/emqx_authz.conf @@ -57,7 +57,7 @@ authorization { # }, { type: file - path: "{{ platform_etc_dir }}/authorization_rules.conf" + path: "{{ platform_etc_dir }}/acl.conf" } ] } diff --git a/apps/emqx_authz/src/emqx_authz_api_sources.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl index b2d33eca5..4be631560 100644 --- a/apps/emqx_authz/src/emqx_authz_api_sources.erl +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -330,7 +330,7 @@ sources(get, _) -> end, [], emqx_authz:lookup()), {200, #{sources => Sources}}; sources(post, #{body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable}}) when is_list(Rules) -> - {ok, Filename} = write_file(filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]), + {ok, Filename} = write_file(filename:join([emqx:get_config([node, data_dir]), "acl.conf"]), erlang:list_to_bitstring([<> || Rule <- Rules]) ), case emqx_authz:update(head, [#{type => file, enable => Enable, path => Filename}]) of @@ -350,7 +350,7 @@ sources(put, #{body := Body}) when is_list(Body) -> NBody = [ begin case Source of #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable} -> - {ok, Filename} = write_file(filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]), + {ok, Filename} = write_file(filename:join([emqx:get_config([node, data_dir]), "acl.conf"]), erlang:list_to_bitstring([<> || Rule <- Rules]) ), #{type => file, enable => Enable, path => Filename}; diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index 6e6597486..f2cb01d05 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -111,7 +111,7 @@ init_per_testcase(_, Config) -> }). -define(SOURCE6, #{<<"type">> => <<"file">>, <<"enable">> => true, - <<"path">> => emqx_ct_helpers:deps_path(emqx_authz, "etc/authorization_rules.conf") + <<"path">> => emqx_ct_helpers:deps_path(emqx_authz, "etc/acl.conf") }). diff --git a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl index 4dc21647a..8c37189c9 100644 --- a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl @@ -190,7 +190,7 @@ t_api(_) -> , #{<<"type">> := <<"redis">>} , #{<<"type">> := <<"file">>} ], Sources), - ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]))), + ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "acl.conf"]))), {ok, 204, _} = request(put, uri(["authorization", "sources", "http"]), ?SOURCE1#{<<"enable">> := false}), {ok, 200, Result3} = request(get, uri(["authorization", "sources", "http"]), []), diff --git a/rebar.config.erl b/rebar.config.erl index aac6e0085..084f068e4 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -340,7 +340,7 @@ relx_overlay(ReleaseType) -> , {copy, "bin/emqx_ctl", "bin/emqx_ctl-{{release_version}}"} %% for relup , {copy, "bin/install_upgrade.escript", "bin/install_upgrade.escript-{{release_version}}"} %% for relup , {copy, "apps/emqx_gateway/src/lwm2m/lwm2m_xml", "etc/lwm2m_xml"} - , {copy, "apps/emqx_authz/etc/authorization_rules.conf", "etc/authorization_rules.conf"} + , {copy, "apps/emqx_authz/etc/acl.conf", "etc/acl.conf"} , {template, "bin/emqx.cmd", "bin/emqx.cmd"} , {template, "bin/emqx_ctl.cmd", "bin/emqx_ctl.cmd"} , {copy, "bin/nodetool", "bin/nodetool"} From c68edb390581c78372f03f0c99b662ed115ed5be Mon Sep 17 00:00:00 2001 From: zhouzb Date: Tue, 7 Sep 2021 17:29:05 +0800 Subject: [PATCH 074/109] chore(authn): update apis of user --- apps/emqx_authn/src/emqx_authn_api.erl | 877 ++++++++++++++++--------- 1 file changed, 569 insertions(+), 308 deletions(-) diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index 3303f88ef..afac57f99 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -28,8 +28,11 @@ , move/2 , move2/2 , import_users/2 + , import_users2/2 , users/2 , users2/2 + , users3/2 + , users4/2 ]). -define(EXAMPLE_1, #{mechanism => <<"password-based">>, @@ -153,8 +156,11 @@ api_spec() -> , authentication_api4() , move_api2() , import_users_api() + , import_users_api2() , users_api() , users2_api() + , users3_api() + , users4_api() ], definitions()}. authentication_api() -> @@ -166,7 +172,7 @@ authentication_api() -> authentication_api2() -> Metadata = #{ - get => list_authenticator_api_spec(), + get => find_authenticator_api_spec(), put => update_authenticator_api_spec(), delete => delete_authenticator_api_spec() }, @@ -181,7 +187,7 @@ authentication_api3() -> authentication_api4() -> Metadata = #{ - get => list_authenticator_api_spec2(), + get => find_authenticator_api_spec2(), put => update_authenticator_api_spec2(), delete => delete_authenticator_api_spec2() }, @@ -199,6 +205,48 @@ move_api2() -> }, {"/listeners/:listener_id/authentication/:id/move", Metadata, move2}. +import_users_api() -> + Metadata = #{ + post => import_users_api_spec() + }, + {"/authentication/:id/import_users", Metadata, import_users}. + +import_users_api2() -> + Metadata = #{ + post => import_users_api_spec2() + }, + {"/listeners/:listener_id/authentication/:id/import_users", Metadata, import_users2}. + +users_api() -> + Metadata = #{ + post => create_user_api_spec(), + get => list_users_api_spec() + }, + {"/authentication/:id/users", Metadata, users}. + +users2_api() -> + Metadata = #{ + put => update_user_api_spec(), + get => find_user_api_spec(), + delete => delete_user_api_spec() + }, + {"/authentication/:id/users/:user_id", Metadata, users2}. + +users3_api() -> + Metadata = #{ + post => create_user_api_spec2(), + get => list_users_api_spec2() + }, + {"/listeners/:listener_id/authentication/:id/users", Metadata, users3}. + +users4_api() -> + Metadata = #{ + put => update_user_api_spec2(), + get => find_user_api_spec2(), + delete => delete_user_api_spec2() + }, + {"/listeners/:listener_id/authentication/:id/users/:user_id", Metadata, users4}. + create_authenticator_api_spec() -> #{ description => "Create a authenticator for global authentication", @@ -321,14 +369,14 @@ list_authenticators_api_spec2() -> ] }. -list_authenticator_api_spec() -> +find_authenticator_api_spec() -> #{ description => "Get authenticator by id", parameters => [ #{ name => id, in => path, - description => "ID of authenticator", + description => "Authenticator id", schema => #{ type => string }, @@ -370,14 +418,14 @@ list_authenticator_api_spec() -> } }. -list_authenticator_api_spec2() -> - Spec = list_authenticator_api_spec(), +find_authenticator_api_spec2() -> + Spec = find_authenticator_api_spec(), Spec#{ parameters => [ #{ name => listener_id, in => path, - description => "ID of listener", + description => "Listener id", schema => #{ type => string }, @@ -386,7 +434,7 @@ list_authenticator_api_spec2() -> #{ name => id, in => path, - description => "ID of authenticator", + description => "Authenticator id", schema => #{ type => string }, @@ -402,7 +450,7 @@ update_authenticator_api_spec() -> #{ name => id, in => path, - description => "ID of authenticator", + description => "Authenticator id", schema => #{ type => string }, @@ -482,7 +530,7 @@ update_authenticator_api_spec2() -> #{ name => listener_id, in => path, - description => "ID of listener", + description => "Listener id", schema => #{ type => string }, @@ -491,7 +539,7 @@ update_authenticator_api_spec2() -> #{ name => id, in => path, - description => "ID of authenticator", + description => "Authenticator id", schema => #{ type => string }, @@ -507,7 +555,7 @@ delete_authenticator_api_spec() -> #{ name => id, in => path, - description => "ID of authenticator", + description => "Authenticator id", schema => #{ type => string }, @@ -529,7 +577,7 @@ delete_authenticator_api_spec2() -> #{ name => listener_id, in => path, - description => "ID of listener", + description => "Listener id", schema => #{ type => string }, @@ -538,7 +586,7 @@ delete_authenticator_api_spec2() -> #{ name => id, in => path, - description => "ID of authenticator", + description => "Authenticator id", schema => #{ type => string }, @@ -554,7 +602,7 @@ move_authenticator_api_spec() -> #{ name => id, in => path, - description => "ID of authenticator", + description => "Authenticator id", schema => #{ type => string }, @@ -609,7 +657,7 @@ move_authenticator_api_spec2() -> #{ name => listener_id, in => path, - description => "ID of listener", + description => "Listener id", schema => #{ type => string }, @@ -618,7 +666,7 @@ move_authenticator_api_spec2() -> #{ name => id, in => path, - description => "ID of authenticator", + description => "Authenticator id", schema => #{ type => string }, @@ -627,90 +675,176 @@ move_authenticator_api_spec2() -> ] }. -import_users_api() -> - Metadata = #{ - post => #{ - description => "Import users from json/csv file", - parameters => [ - #{ - name => id, - in => path, - description => "ID of authenticator", +import_users_api_spec() -> + #{ + description => "Import users from json/csv file", + parameters => [ + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ], + requestBody => #{ + content => #{ + 'application/json' => #{ schema => #{ - type => string - }, - required => true - } - ], - requestBody => #{ - content => #{ - 'application/json' => #{ - schema => #{ - type => object, - required => [filename], - properties => #{ - filename => #{ - type => string - } + type => object, + required => [filename], + properties => #{ + filename => #{ + type => string } } } } - }, - responses => #{ - <<"204">> => #{ - description => <<"No Content">> - }, - <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) } + }, + responses => #{ + <<"204">> => #{ + description => <<"No Content">> + }, + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) } - }, - {"/authentication/:id/import_users", Metadata, import_users}. + }. -users_api() -> - Metadata = #{ - post => #{ - description => "Add user", - parameters => [ - #{ - name => id, - in => path, - description => "ID of authenticator", +import_users_api_spec2() -> + Spec = import_users_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "Listener id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ] + }. + +create_user_api_spec() -> + #{ + description => "Add user", + parameters => [ + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ], + requestBody => #{ + content => #{ + 'application/json' => #{ schema => #{ - type => string - }, - required => true + type => object, + required => [user_id, password], + properties => #{ + user_id => #{ + type => string + }, + password => #{ + type => string + }, + superuser => #{ + type => boolean, + default => false + } + } + } } - ], - requestBody => #{ + } + }, + responses => #{ + <<"201">> => #{ + description => <<"Created">>, content => #{ 'application/json' => #{ schema => #{ type => object, - required => [user_id, password], properties => #{ user_id => #{ type => string }, - password => #{ - type => string - }, superuser => #{ - type => boolean, - default => false + type => boolean } } } } } }, - responses => #{ - <<"201">> => #{ - description => <<"Created">>, - content => #{ - 'application/json' => #{ - schema => #{ + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + }. + +create_user_api_spec2() -> + Spec = create_user_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "Listener id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ] + }. + +list_users_api_spec() -> + #{ + description => "List users", + parameters => [ + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ], + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => #{ + type => array, + items => #{ type => object, properties => #{ user_id => #{ @@ -723,194 +857,286 @@ users_api() -> } } } - }, - <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) - } - }, - get => #{ - description => "List users", - parameters => [ - #{ - name => id, - in => path, - description => "ID of authenticator", - schema => #{ - type => string - }, - required => true } - ], - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => #{ - type => array, - items => #{ - type => object, - properties => #{ - user_id => #{ - type => string - }, - superuser => #{ - type => boolean - } - } - } + }, + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + }. + +list_users_api_spec2() -> + Spec = list_users_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "Listener id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + } + ] + }. + +update_user_api_spec() -> + #{ + description => "Update user", + parameters => [ + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => user_id, + in => path, + description => "User id", + schema => #{ + type => string + }, + required => true + } + ], + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => #{ + type => object, + properties => #{ + password => #{ + type => string + }, + superuser => #{ + type => boolean } } } - }, - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) - } - } - }, - {"/authentication/:id/users", Metadata, users}. - -users2_api() -> - Metadata = #{ - patch => #{ - description => "Update user", - parameters => [ - #{ - name => id, - in => path, - description => "ID of authenticator", - schema => #{ - type => string - }, - required => true - }, - #{ - name => user_id, - in => path, - schema => #{ - type => string - }, - required => true } - ], - requestBody => #{ + } + }, + responses => #{ + <<"200">> => #{ + description => <<"OK">>, content => #{ 'application/json' => #{ schema => #{ - type => object, - properties => #{ - password => #{ - type => string - }, - superuser => #{ - type => boolean + type => array, + items => #{ + type => object, + properties => #{ + user_id => #{ + type => string + }, + superuser => #{ + type => boolean + } } } } } } }, - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => #{ - type => array, - items => #{ - type => object, - properties => #{ - user_id => #{ - type => string - }, - superuser => #{ - type => boolean - } - } - } - } - } - } - }, - <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) - } - }, - get => #{ - description => "Get user info", - parameters => [ - #{ - name => id, - in => path, - description => "ID of authenticator", - schema => #{ - type => string - }, - required => true - }, - #{ - name => user_id, - in => path, - schema => #{ - type => string - }, - required => true - } - ], - responses => #{ - <<"200">> => #{ - description => <<"OK">>, - content => #{ - 'application/json' => #{ - schema => #{ - type => array, - items => #{ - type => object, - properties => #{ - user_id => #{ - type => string - }, - superuser => #{ - type => boolean - } - } - } - } - } - } - }, - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) - } - }, - delete => #{ - description => "Delete user", - parameters => [ - #{ - name => id, - in => path, - description => "ID of authenticator", - schema => #{ - type => string - }, - required => true - }, - #{ - name => user_id, - in => path, - schema => #{ - type => string - }, - required => true - } - ], - responses => #{ - <<"204">> => #{ - description => <<"No Content">> - }, - <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) - } + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) } - }, - {"/authentication/:id/users/:user_id", Metadata, users2}. + }. + +update_user_api_spec2() -> + Spec = update_user_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "Listener id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => user_id, + in => path, + description => "User id", + schema => #{ + type => string + }, + required => true + } + ] + }. + +find_user_api_spec() -> + #{ + description => "Get user info", + parameters => [ + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => user_id, + in => path, + description => "User id", + schema => #{ + type => string + }, + required => true + } + ], + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => #{ + type => array, + items => #{ + type => object, + properties => #{ + user_id => #{ + type => string + }, + superuser => #{ + type => boolean + } + } + } + } + } + } + }, + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + }. + +find_user_api_spec2() -> + Spec = find_user_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "Listener id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => user_id, + in => path, + description => "User id", + schema => #{ + type => string + }, + required => true + } + ] + }. + +delete_user_api_spec() -> + #{ + description => "Delete user", + parameters => [ + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => user_id, + in => path, + description => "User id", + schema => #{ + type => string + }, + required => true + } + ], + responses => #{ + <<"204">> => #{ + description => <<"No Content">> + }, + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) + } + }. + +delete_user_api_spec2() -> + Spec = delete_user_api_spec(), + Spec#{ + parameters => [ + #{ + name => listener_id, + in => path, + description => "Listener id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => id, + in => path, + description => "Authenticator id", + schema => #{ + type => string + }, + required => true + }, + #{ + name => user_id, + in => path, + description => "User id", + schema => #{ + type => string + }, + required => true + } + ] + }. + definitions() -> AuthenticatorConfigDef = #{ @@ -1536,71 +1762,54 @@ move2(post, #{bindings := #{listener_id := ListenerID, id := AuthenticatorID}, b move2(post, #{bindings := #{listener_id := _, id := _}, body := _}) -> serialize_error({missing_parameter, position}). -import_users(post, #{bindings := #{id := AuthenticatorID}, body := Body}) -> - case Body of - #{<<"filename">> := Filename} -> - case ?AUTHN:import_users(?GLOBAL, AuthenticatorID, Filename) of - ok -> {204}; - {error, Reason} -> serialize_error(Reason) - end; - _ -> - serialize_error({missing_parameter, filename}) - end. +import_users(post, #{bindings := #{id := AuthenticatorID}, body := #{<<"filename">> := Filename}}) -> + case ?AUTHN:import_users(?GLOBAL, AuthenticatorID, Filename) of + ok -> {204}; + {error, Reason} -> serialize_error(Reason) + end; +import_users(post, #{bindings := #{id := _}, body := _}) -> + serialize_error({missing_parameter, filename}). + +import_users2(post, #{bindings := #{listener_id := ListenerID, id := AuthenticatorID}, body := #{<<"filename">> := Filename}}) -> + case ?AUTHN:import_users(ListenerID, AuthenticatorID, Filename) of + ok -> {204}; + {error, Reason} -> serialize_error(Reason) + end; +import_users2(post, #{bindings := #{listener_id := _, id := _}, body := _}) -> + serialize_error({missing_parameter, filename}). users(post, #{bindings := #{id := AuthenticatorID}, body := UserInfo}) -> - case UserInfo of - #{ <<"user_id">> := UserID, <<"password">> := Password} -> - Superuser = maps:get(<<"superuser">>, UserInfo, false), - case ?AUTHN:add_user(?GLOBAL, AuthenticatorID, #{ user_id => UserID - , password => Password - , superuser => Superuser}) of - {ok, User} -> - {201, User}; - {error, Reason} -> - serialize_error(Reason) - end; - #{<<"user_id">> := _} -> - serialize_error({missing_parameter, password}); - _ -> - serialize_error({missing_parameter, user_id}) - end; + add_user(?GLOBAL, AuthenticatorID, UserInfo); users(get, #{bindings := #{id := AuthenticatorID}}) -> - case ?AUTHN:list_users(?GLOBAL, AuthenticatorID) of - {ok, Users} -> - {200, Users}; - {error, Reason} -> - serialize_error(Reason) - end. + list_users(?GLOBAL, AuthenticatorID). -users2(patch, #{bindings := #{id := AuthenticatorID, - user_id := UserID}, - body := UserInfo}) -> - NUserInfo = maps:with([<<"password">>, <<"superuser">>], UserInfo), - case NUserInfo =:= #{} of - true -> - serialize_error({missing_parameter, password}); - false -> - case ?AUTHN:update_user(?GLOBAL, AuthenticatorID, UserID, UserInfo) of - {ok, User} -> - {200, User}; - {error, Reason} -> - serialize_error(Reason) - end - end; +users2(put, #{bindings := #{id := AuthenticatorID, + user_id := UserID}, body := UserInfo}) -> + update_user(?GLOBAL, AuthenticatorID, UserID, UserInfo); users2(get, #{bindings := #{id := AuthenticatorID, user_id := UserID}}) -> - case ?AUTHN:lookup_user(?GLOBAL, AuthenticatorID, UserID) of - {ok, User} -> - {200, User}; - {error, Reason} -> - serialize_error(Reason) - end; + find_user(?GLOBAL, AuthenticatorID, UserID); users2(delete, #{bindings := #{id := AuthenticatorID, user_id := UserID}}) -> - case ?AUTHN:delete_user(?GLOBAL, AuthenticatorID, UserID) of - ok -> - {204}; - {error, Reason} -> - serialize_error(Reason) - end. + delete_user(?GLOBAL, AuthenticatorID, UserID). + +users3(post, #{bindings := #{listener_id := ListenerID, + id := AuthenticatorID}, body := UserInfo}) -> + add_user(ListenerID, AuthenticatorID, UserInfo); +users3(get, #{bindings := #{listener_id := ListenerID, + id := AuthenticatorID}}) -> + list_users(ListenerID, AuthenticatorID). + +users4(put, #{bindings := #{listener_id := ListenerID, + id := AuthenticatorID, + user_id := UserID}, body := UserInfo}) -> + update_user(ListenerID, AuthenticatorID, UserID, UserInfo); +users4(get, #{bindings := #{listener_id := ListenerID, + id := AuthenticatorID, + user_id := UserID}}) -> + find_user(ListenerID, AuthenticatorID, UserID); +users4(delete, #{bindings := #{listener_id := ListenerID, + id := AuthenticatorID, + user_id := UserID}}) -> + delete_user(ListenerID, AuthenticatorID, UserID). %%------------------------------------------------------------------------------ %% Internal functions @@ -1667,6 +1876,58 @@ move_authenitcator(ConfKeyPath, ChainName, AuthenticatorID, Position) -> serialize_error(Reason) end. +add_user(ChainName, AuthenticatorID, #{<<"user_id">> := UserID, <<"password">> := Password} = UserInfo) -> + Superuser = maps:get(<<"superuser">>, UserInfo, false), + case ?AUTHN:add_user(ChainName, AuthenticatorID, #{ user_id => UserID + , password => Password + , superuser => Superuser}) of + {ok, User} -> + {201, User}; + {error, Reason} -> + serialize_error(Reason) + end; +add_user(_, _, #{<<"user_id">> := _}) -> + serialize_error({missing_parameter, password}); +add_user(_, _, _) -> + serialize_error({missing_parameter, user_id}). + +update_user(ChainName, AuthenticatorID, UserID, UserInfo) -> + case maps:with([<<"password">>, <<"superuser">>], UserInfo) =:= #{} of + true -> + serialize_error({missing_parameter, password}); + false -> + case ?AUTHN:update_user(ChainName, AuthenticatorID, UserID, UserInfo) of + {ok, User} -> + {200, User}; + {error, Reason} -> + serialize_error(Reason) + end + end. + +find_user(ChainName, AuthenticatorID, UserID) -> + case ?AUTHN:lookup_user(ChainName, AuthenticatorID, UserID) of + {ok, User} -> + {200, User}; + {error, Reason} -> + serialize_error(Reason) + end. + +delete_user(ChainName, AuthenticatorID, UserID) -> + case ?AUTHN:delete_user(ChainName, AuthenticatorID, UserID) of + ok -> + {204}; + {error, Reason} -> + serialize_error(Reason) + end. + +list_users(ChainName, AuthenticatorID) -> + case ?AUTHN:list_users(ChainName, AuthenticatorID) of + {ok, Users} -> + {200, Users}; + {error, Reason} -> + serialize_error(Reason) + end. + update_config(Path, ConfigRequest) -> emqx:update_config(Path, ConfigRequest, #{rawconf_with_defaults => true}). From 8b2488e099f9794e2fb913649bee084b27fac08d Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Tue, 7 Sep 2021 18:31:40 +0800 Subject: [PATCH 075/109] fix: routes api add params (#5675) * fix: routes api add topic query params * fix: routes api add node query params --- .../src/emqx_mgmt_api_routes.erl | 54 +++++++++++++++---- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/apps/emqx_management/src/emqx_mgmt_api_routes.erl b/apps/emqx_management/src/emqx_mgmt_api_routes.erl index 19f42427e..870f66892 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_routes.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_routes.erl @@ -26,8 +26,12 @@ -export([ routes/2 , route/2]). +-export([query/4]). + -define(TOPIC_NOT_FOUND, 'TOPIC_NOT_FOUND'). +-define(ROUTES_QS_SCHEMA, [{<<"topic">>, binary}, {<<"node">>, atom}]). + -import(emqx_mgmt_util, [ object_schema/2 , object_array_schema/2 , error_schema/2 @@ -48,7 +52,7 @@ routes_api() -> Metadata = #{ get => #{ description => <<"EMQ X routes">>, - parameters => page_params(), + parameters => [topic_param(query) , node_param()] ++ page_params(), responses => #{ <<"200">> => object_array_schema(properties(), <<"List route info">>) } @@ -60,13 +64,7 @@ route_api() -> Metadata = #{ get => #{ description => <<"EMQ X routes">>, - parameters => [#{ - name => topic, - in => path, - required => true, - description => <<"Topic string, url encoding">>, - schema => #{type => string} - }], + parameters => [topic_param(path)], responses => #{ <<"200">> => object_schema(properties(), <<"Route info">>), @@ -80,15 +78,15 @@ route_api() -> %%%============================================================================================== %% parameters trans routes(get, #{query_string := Qs}) -> - list(Qs). + list(generate_topic(Qs)). route(get, #{bindings := Bindings}) -> - lookup(Bindings). + lookup(generate_topic(Bindings)). %%%============================================================================================== %% api apply list(Params) -> - Response = emqx_mgmt_api:paginate(emqx_route, Params, fun format/1), + Response = emqx_mgmt_api:node_query(node(), Params, emqx_route, ?ROUTES_QS_SCHEMA, {?MODULE, query}), {200, Response}. lookup(#{topic := Topic}) -> @@ -101,7 +99,41 @@ lookup(#{topic := Topic}) -> %%%============================================================================================== %% internal +generate_topic(Params = #{<<"topic">> := Topic}) -> + Params#{<<"topic">> => uri_string:percent_decode(Topic)}; +generate_topic(Params = #{topic := Topic}) -> + Params#{topic => uri_string:percent_decode(Topic)}; +generate_topic(Params) -> Params. + +query(Tab, {Qs, _}, Start, Limit) -> + Ms = qs2ms(Qs, [{{route, '_', '_'}, [], ['$_']}]), + emqx_mgmt_api:select_table(Tab, Ms, Start, Limit, fun format/1). + +qs2ms([], Res) -> Res; +qs2ms([{topic,'=:=', T} | Qs], [{{route, _, N}, [], ['$_']}]) -> + qs2ms(Qs, [{{route, T, N}, [], ['$_']}]); +qs2ms([{node,'=:=', N} | Qs], [{{route, T, _}, [], ['$_']}]) -> + qs2ms(Qs, [{{route, T, N}, [], ['$_']}]). + format(#route{topic = Topic, dest = {_, Node}}) -> #{topic => Topic, node => Node}; format(#route{topic = Topic, dest = Node}) -> #{topic => Topic, node => Node}. + +topic_param(In) -> + #{ + name => topic, + in => In, + required => In == path, + description => <<"Topic string, url encoding">>, + schema => #{type => string} + }. + +node_param()-> + #{ + name => node, + in => query, + required => false, + description => <<"Node">>, + schema => #{type => string} + }. From 89f48f89ebe18e24ddfebbc7cf375b9c32151f07 Mon Sep 17 00:00:00 2001 From: lafirest Date: Fri, 3 Sep 2021 13:55:05 +0800 Subject: [PATCH 076/109] feat(emqx_coap): add emqx_coap_api 1. add a request api for emqx_coap 2. fix some emqx_coap logic error --- apps/emqx_gateway/etc/emqx_gateway.conf | 1 - apps/emqx_gateway/src/coap/emqx_coap_api.erl | 145 ++++++++++++ .../src/coap/emqx_coap_channel.erl | 69 ++++-- .../src/coap/emqx_coap_session.erl | 10 +- apps/emqx_gateway/src/coap/emqx_coap_tm.erl | 12 +- .../src/coap/emqx_coap_transport.erl | 11 +- .../coap/handler/emqx_coap_pubsub_handler.erl | 28 ++- apps/emqx_gateway/src/emqx_gateway_app.erl | 2 +- .../emqx_gateway/src/emqx_gateway_metrics.erl | 2 +- .../emqx_gateway/test/emqx_coap_api_SUITE.erl | 224 ++++++++++++++++++ .../test/emqx_mgmt_api_test_util.erl | 27 ++- 11 files changed, 478 insertions(+), 53 deletions(-) create mode 100644 apps/emqx_gateway/src/coap/emqx_coap_api.erl create mode 100644 apps/emqx_gateway/test/emqx_coap_api_SUITE.erl diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index 6fdadcc3b..5212d319f 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -59,7 +59,6 @@ gateway.coap { ## When publishing or subscribing, prefix all topics with a mountpoint string. mountpoint = "" - heartbeat = 30s notify_type = qos ## if true, you need to establish a connection before use diff --git a/apps/emqx_gateway/src/coap/emqx_coap_api.erl b/apps/emqx_gateway/src/coap/emqx_coap_api.erl new file mode 100644 index 000000000..428e99ac5 --- /dev/null +++ b/apps/emqx_gateway/src/coap/emqx_coap_api.erl @@ -0,0 +1,145 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2017-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_coap_api). + +-behaviour(minirest_api). + +-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). + +%% API +-export([api_spec/0]). + +-export([request/2]). + +-define(PREFIX, "/gateway/coap/:clientid"). +-define(DEF_WAIT_TIME, 10). + +-import(emqx_mgmt_util, [ schema/1 + , schema/2 + , object_schema/1 + , object_schema/2 + , error_schema/2 + , properties/1]). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- +api_spec() -> + {[request_api()], []}. + +request_api() -> + Metadata = #{post => request_method_meta()}, + {?PREFIX ++ "/request", Metadata, request}. + +request(post, #{body := Body, bindings := Bindings}) -> + ClientId = maps:get(clientid, Bindings, undefined), + + Method = maps:get(<<"method">>, Body, <<"get">>), + CT = maps:get(<<"content_type">>, Body, <<"text/plain">>), + Token = maps:get(<<"token">>, Body, <<>>), + Payload = maps:get(<<"payload">>, Body, <<>>), + WaitTime = maps:get(<<"timeout">>, Body, ?DEF_WAIT_TIME), + + Payload2 = parse_payload(CT, Payload), + ReqType = erlang:binary_to_atom(Method), + + Msg = emqx_coap_message:request(con, + ReqType, Payload2, #{content_format => CT}), + + Msg2 = Msg#coap_message{token = Token}, + + case call_client(ClientId, Msg2, timer:seconds(WaitTime)) of + timeout -> + {504}; + not_found -> + {404}; + Response -> + {200, format_to_response(CT, Response)} + end. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- +request_parameters() -> + [#{name => clientid, + in => path, + schema => #{type => string}, + required => true}]. + +request_properties() -> + properties([ {token, string, "message token, can be empty"} + , {method, string, "request method type", ["get", "put", "post", "delete"]} + , {timeout, integer, "timespan for response"} + , {content_type, string, "payload type", + [<<"text/plain">>, <<"application/json">>, <<"application/octet-stream">>]} + , {payload, string, "payload"}]). + +coap_message_properties() -> + properties([ {id, integer, "message id"} + , {token, string, "message token, can be empty"} + , {method, string, "response code"} + , {payload, string, "payload"}]). + +request_method_meta() -> + #{description => <<"lookup matching messages">>, + parameters => request_parameters(), + 'requestBody' => object_schema(request_properties(), + <<"request payload, binary must encode by base64">>), + responses => #{ + <<"200">> => object_schema(coap_message_properties()), + <<"404">> => schema(<<"NotFound">>), + <<"504">> => schema(<<"Timeout">>) + }}. + + +format_to_response(ContentType, #coap_message{id = Id, + token = Token, + method = Method, + payload = Payload}) -> + #{id => Id, + token => Token, + method => format_to_binary(Method), + payload => format_payload(ContentType, Payload)}. + +format_to_binary(Obj) -> + erlang:list_to_binary(io_lib:format("~p", [Obj])). + +format_payload(<<"application/octet-stream">>, Payload) -> + base64:encode(Payload); + +format_payload(_, Payload) -> + Payload. + +parse_payload(<<"application/octet-stream">>, Body) -> + base64:decode(Body); + +parse_payload(_, Body) -> + Body. + +call_client(ClientId, Msg, Timeout) -> + case emqx_gateway_cm_registry:lookup_channels(coap, ClientId) of + [Channel | _] -> + RequestId = emqx_coap_channel:send_request(Channel, Msg), + case gen_server:wait_response(RequestId, Timeout) of + {reply, Reply} -> + Reply; + _ -> + timeout + end; + _ -> + not_found + end. diff --git a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl index 11aca8cc8..112efdc44 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_channel.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_channel.erl @@ -25,7 +25,10 @@ -export([ info/1 , info/2 , stats/1 - , validator/4]). + , validator/4 + , metrics_inc/2 + , run_hooks/3 + , send_request/2]). -export([ init/2 , handle_in/2 @@ -57,7 +60,7 @@ connection_required :: boolean(), - conn_state :: idle | connected, + conn_state :: idle | connected | disconnected, token :: binary() | undefined }). @@ -99,7 +102,7 @@ init(ConnInfo = #{peername := {PeerHost, _}, sockname := {_, SockPort}}, #{ctx := Ctx} = Config) -> Peercert = maps:get(peercert, ConnInfo, undefined), - Mountpoint = maps:get(mountpoint, Config, undefined), + Mountpoint = maps:get(mountpoint, Config, <<>>), ClientInfo = set_peercert_infos( Peercert, #{ zone => default @@ -128,6 +131,10 @@ init(ConnInfo = #{peername := {PeerHost, _}, validator(Type, Topic, Ctx, ClientInfo) -> emqx_gateway_ctx:authorize(Ctx, ClientInfo, Type, Topic). +-spec send_request(pid(), emqx_coap_message()) -> any(). +send_request(Channel, Request) -> + gen_server:send_request(Channel, {?FUNCTION_NAME, Request}). + %%-------------------------------------------------------------------- %% Handle incoming packet %%-------------------------------------------------------------------- @@ -143,8 +150,9 @@ handle_in(Msg, ChannleT) -> %%-------------------------------------------------------------------- %% Handle Delivers from broker to client %%-------------------------------------------------------------------- -handle_deliver(Delivers, Channel) -> - call_session(deliver, Delivers, Channel). +handle_deliver(Delivers, #channel{session = Session, + ctx = Ctx} = Channel) -> + handle_result(emqx_coap_session:deliver(Delivers, Ctx, Session), Channel). %%-------------------------------------------------------------------- %% Handle timeout @@ -155,7 +163,7 @@ handle_timeout(_, {keepalive, NewVal}, #channel{keepalive = KeepAlive} = Channel Channel2 = ensure_keepalive_timer(fun make_timer/4, Channel), {ok, Channel2#channel{keepalive = NewKeepAlive}}; {error, timeout} -> - {shutdown, timeout, Channel} + {shutdown, timeout, ensure_disconnected(keepalive_timeout, Channel)} end; handle_timeout(_, {transport, Msg}, Channel) -> @@ -170,6 +178,10 @@ handle_timeout(_, _, Channel) -> %%-------------------------------------------------------------------- %% Handle call %%-------------------------------------------------------------------- +handle_call({send_request, Msg}, From, Channel) -> + Result = call_session(handle_out, {{send_request, From}, Msg}, Channel), + erlang:setelement(1, Result, noreply); + handle_call(Req, _From, Channel) -> ?LOG(error, "Unexpected call: ~p", [Req]), {reply, ignored, Channel}. @@ -184,6 +196,9 @@ handle_cast(Req, Channel) -> %%-------------------------------------------------------------------- %% Handle Info %%-------------------------------------------------------------------- +handle_info({subscribe, _}, Channel) -> + {ok, Channel}; + handle_info(Info, Channel) -> ?LOG(error, "Unexpected info: ~p", [Info]), {ok, Channel}. @@ -191,8 +206,10 @@ handle_info(Info, Channel) -> %%-------------------------------------------------------------------- %% Terminate %%-------------------------------------------------------------------- -terminate(_Reason, _Channel) -> - ok. +terminate(Reason, #channel{clientinfo = ClientInfo, + ctx = Ctx, + session = Session}) -> + run_hooks(Ctx, 'session.terminated', [ClientInfo, Reason, Session]). %%-------------------------------------------------------------------- %% Internal functions @@ -242,17 +259,17 @@ check_token(true, try_takeover(CState, DesireId, Msg, Channel); _ -> Reply = emqx_coap_message:piggyback({error, unauthorized}, Msg), - {ok, {outgoing, Reply}, Msg} + {ok, {outgoing, Reply}, Channel} end; check_token(false, Msg, Channel) -> case emqx_coap_message:get_option(uri_query, Msg) of #{<<"clientid">> := _} -> Reply = emqx_coap_message:piggyback({error, unauthorized}, Msg), - {ok, {outgoing, Reply}, Msg}; + {ok, {outgoing, Reply}, Channel}; #{<<"token">> := _} -> Reply = emqx_coap_message:piggyback({error, unauthorized}, Msg), - {ok, {outgoing, Reply}, Msg}; + {ok, {outgoing, Reply}, Channel}; _ -> call_session(handle_request, Msg, Channel) end. @@ -322,11 +339,9 @@ auth_connect(_Input, Channel = #channel{ctx = Ctx, {error, Reason} end. -fix_mountpoint(_Packet, #{mountpoint := undefined} = ClientInfo) -> +fix_mountpoint(_Packet, #{mountpoint := <<>>} = ClientInfo) -> {ok, ClientInfo}; fix_mountpoint(_Packet, ClientInfo = #{mountpoint := Mountpoint}) -> - %% TODO: Enrich the varibale replacement???? - %% i.e: ${ClientInfo.auth_result.productKey} Mountpoint1 = emqx_mountpoint:replvar(Mountpoint, ClientInfo), {ok, ClientInfo#{mountpoint := Mountpoint1}}. @@ -338,6 +353,7 @@ ensure_connected(Channel = #channel{ctx = Ctx, , proto_ver => <<"1">> }, ok = run_hooks(Ctx, 'client.connected', [ClientInfo, NConnInfo]), + _ = run_hooks(Ctx, 'client.connack', [NConnInfo, connection_accepted, []]), Channel#channel{conninfo = NConnInfo}. process_connect(#channel{ctx = Ctx, @@ -374,19 +390,32 @@ run_hooks(Ctx, Name, Args, Acc) -> emqx_gateway_ctx:metrics_inc(Ctx, Name), emqx_hooks:run_fold(Name, Args, Acc). +metrics_inc(Name, Ctx) -> + emqx_gateway_ctx:metrics_inc(Ctx, Name). + +ensure_disconnected(Reason, Channel = #channel{ + ctx = Ctx, + conninfo = ConnInfo, + clientinfo = ClientInfo}) -> + NConnInfo = ConnInfo#{disconnected_at => erlang:system_time(millisecond)}, + ok = run_hooks(Ctx, 'client.disconnected', [ClientInfo, Reason, NConnInfo]), + Channel#channel{conninfo = NConnInfo, conn_state = disconnected}. + %%-------------------------------------------------------------------- %% Call Chain %%-------------------------------------------------------------------- -call_session(Fun, - Msg, - #channel{session = Session} = Channel) -> +call_session(Fun, Msg, #channel{session = Session} = Channel) -> + Result = emqx_coap_session:Fun(Msg, Session), + handle_result(Result, Channel). + +handle_result(Result, Channel) -> iter([ session, fun process_session/4 , proto, fun process_protocol/4 , reply, fun process_reply/4 , out, fun process_out/4 , fun process_nothing/3 ], - emqx_coap_session:Fun(Msg, Session), + Result, Channel). call_handler(request, Msg, Result, @@ -406,6 +435,10 @@ call_handler(request, Msg, Result, maps:merge(Result, HandlerResult), Channel); +call_handler(response, {{send_request, From}, Response}, Result, Channel, Iter) -> + gen_server:reply(From, Response), + iter(Iter, Result, Channel); + call_handler(_, _, Result, Channel, Iter) -> iter(Iter, Result, Channel). diff --git a/apps/emqx_gateway/src/coap/emqx_coap_session.erl b/apps/emqx_gateway/src/coap/emqx_coap_session.erl index b7e6c53f4..0fbc47cf8 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_session.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_session.erl @@ -33,7 +33,7 @@ , handle_response/2 , handle_out/2 , set_reply/2 - , deliver/2 + , deliver/3 , timeout/2]). -export_type([session/0]). @@ -66,6 +66,7 @@ ]). -import(emqx_coap_medium, [iter/3]). +-import(emqx_coap_channel, [metrics_inc/2]). %%%------------------------------------------------------------------- %%% API @@ -147,13 +148,16 @@ set_reply(Msg, #session{transport_manager = TM} = Session) -> TM2 = emqx_coap_tm:set_reply(Msg, TM), Session#session{transport_manager = TM2}. -deliver(Delivers, #session{observe_manager = OM, - transport_manager = TM} = Session) -> +deliver(Delivers, Ctx, #session{observe_manager = OM, + transport_manager = TM} = Session) -> Fun = fun({_, Topic, Message}, {OutAcc, OMAcc, TMAcc} = Acc) -> case emqx_coap_observe_res:res_changed(Topic, OMAcc) of undefined -> + metrics_inc('delivery.dropped', Ctx), + metrics_inc('delivery.dropped.no_subid', Ctx), Acc; {Token, SeqId, OM2} -> + metrics_inc('messages.delivered', Ctx), Msg = mqtt_to_coap(Message, Token, SeqId), #{out := Out, tm := TM2} = emqx_coap_tm:handle_out(Msg, TMAcc), {Out ++ OutAcc, OM2, TM2} diff --git a/apps/emqx_gateway/src/coap/emqx_coap_tm.erl b/apps/emqx_gateway/src/coap/emqx_coap_tm.erl index bdc061b1d..b5e4deb7f 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_tm.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_tm.erl @@ -108,6 +108,9 @@ handle_response(#coap_message{type = Type, id = MsgId, token = Token} = Msg, TM) end. %% send to a client, msg can be request/piggyback/separate/notify +handle_out({Ctx, Msg}, TM) -> + handle_out(Msg, Ctx, TM); + handle_out(Msg, TM) -> handle_out(Msg, undefined, TM). @@ -119,8 +122,8 @@ handle_out(#coap_message{token = Token} = MsgT, Ctx, TM) -> %% TODO why find by token ? case find_machine_by_keys([Id, TokenId], TM2) of undefined -> - {Machine, TM3} = new_out_machine(Id, Msg, TM), - process_event(out, {Ctx, Msg}, TM3, Machine); + {Machine, TM3} = new_out_machine(Id, Ctx, Msg, TM2), + process_event(out, Msg, TM3, Machine); _ -> %% ignore repeat send empty() @@ -293,9 +296,10 @@ new_in_machine(MachineId, #{seq_id := SeqId} = Manager) -> SeqId => Machine, MachineId => SeqId}}. --spec new_out_machine(state_machine_key(), emqx_coap_message(), manager()) -> +-spec new_out_machine(state_machine_key(), any(), emqx_coap_message(), manager()) -> {state_machine(), manager()}. new_out_machine(MachineId, + Ctx, #coap_message{type = Type, token = Token, options = Opts}, #{seq_id := SeqId} = Manager) -> Observe = maps:get(observe, Opts, undefined), @@ -305,7 +309,7 @@ new_out_machine(MachineId, , observe = Observe , state = idle , timers = #{} - , transport = emqx_coap_transport:new()}, + , transport = emqx_coap_transport:new(Ctx)}, Manager2 = Manager#{seq_id := SeqId + 1, SeqId => Machine, diff --git a/apps/emqx_gateway/src/coap/emqx_coap_transport.erl b/apps/emqx_gateway/src/coap/emqx_coap_transport.erl index eb7ce9bd4..2e858a2e1 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_transport.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_transport.erl @@ -20,7 +20,7 @@ -type transport() :: #transport{}. --export([ new/0, idle/3, maybe_reset/3, set_cache/2 +-export([ new/0, new/1, idle/3, maybe_reset/3, set_cache/2 , maybe_resend_4request/3, wait_ack/3, until_stop/3 , observe/3, maybe_resend_4response/3]). @@ -33,9 +33,13 @@ -spec new() -> transport(). new() -> + new(undefined). + +new(ReqCtx) -> #transport{cache = undefined, retry_interval = 0, - retry_count = 0}. + retry_count = 0, + req_context = ReqCtx}. idle(in, #coap_message{type = non, method = Method} = Msg, @@ -62,9 +66,6 @@ idle(in, timeouts =>[{stop_timeout, ?EXCHANGE_LIFETIME}]}) end; -idle(out, {Ctx, Msg}, Transport) -> - idle(out, Msg, Transport#transport{req_context = Ctx}); - idle(out, #coap_message{type = non} = Msg, _) -> out(Msg, #{next => maybe_reset, timeouts => [{stop_timeout, ?NON_LIFETIME}]}); diff --git a/apps/emqx_gateway/src/coap/handler/emqx_coap_pubsub_handler.erl b/apps/emqx_gateway/src/coap/handler/emqx_coap_pubsub_handler.erl index ca734993a..85cf32c6d 100644 --- a/apps/emqx_gateway/src/coap/handler/emqx_coap_pubsub_handler.erl +++ b/apps/emqx_gateway/src/coap/handler/emqx_coap_pubsub_handler.erl @@ -24,11 +24,14 @@ -import(emqx_coap_message, [response/2, response/3]). -import(emqx_coap_medium, [reply/2, reply/3]). +-import(emqx_coap_channel, [run_hooks/3]). -define(UNSUB(Topic, Msg), #{subscribe => {Topic, Msg}}). -define(SUB(Topic, Token, Msg), #{subscribe => {{Topic, Token}, Msg}}). -define(SUBOPTS, #{qos => 0, rh => 0, rap => 0, nl => 0, is_new => false}). +%% TODO maybe can merge this code into emqx_coap_session, simplify the call chain + handle_request(Path, #coap_message{method = Method} = Msg, Ctx, CInfo) -> case check_topic(Path) of {ok, Topic} -> @@ -42,7 +45,7 @@ handle_method(get, Topic, Msg, Ctx, CInfo) -> 0 -> subscribe(Msg, Topic, Ctx, CInfo); 1 -> - unsubscribe(Msg, Topic, CInfo); + unsubscribe(Msg, Topic, Ctx, CInfo); _ -> reply({error, bad_request}, <<"invalid observe value">>, Msg) end; @@ -51,8 +54,9 @@ handle_method(post, Topic, #coap_message{payload = Payload} = Msg, Ctx, CInfo) - case emqx_coap_channel:validator(publish, Topic, Ctx, CInfo) of allow -> #{clientid := ClientId} = CInfo, + MountTopic = mount(CInfo, Topic), QOS = get_publish_qos(Msg), - MQTTMsg = emqx_message:make(ClientId, QOS, Topic, Payload), + MQTTMsg = emqx_message:make(ClientId, QOS, MountTopic, Payload), MQTTMsg2 = apply_publish_opts(Msg, MQTTMsg), _ = emqx_broker:publish(MQTTMsg2), reply({ok, changed}, Msg); @@ -139,15 +143,19 @@ subscribe(#coap_message{token = Token} = Msg, Topic, Ctx, CInfo) -> allow -> #{clientid := ClientId} = CInfo, SubOpts = get_sub_opts(Msg), - emqx_broker:subscribe(Topic, ClientId, SubOpts), - emqx_hooks:run('session.subscribed', - [CInfo, Topic, SubOpts]), - ?SUB(Topic, Token, Msg); + MountTopic = mount(CInfo, Topic), + emqx_broker:subscribe(MountTopic, ClientId, SubOpts), + run_hooks(Ctx, 'session.subscribed', [CInfo, Topic, SubOpts]), + ?SUB(MountTopic, Token, Msg); _ -> reply({error, unauthorized}, Msg) end. -unsubscribe(Msg, Topic, CInfo) -> - emqx_broker:unsubscribe(Topic), - emqx_hooks:run('session.unsubscribed', [CInfo, Topic, ?SUBOPTS]), - ?UNSUB(Topic, Msg). +unsubscribe(Msg, Topic, Ctx, CInfo) -> + MountTopic = mount(CInfo, Topic), + emqx_broker:unsubscribe(MountTopic), + run_hooks(Ctx, 'session.unsubscribed', [CInfo, Topic, ?SUBOPTS]), + ?UNSUB(MountTopic, Msg). + +mount(#{mountpoint := Mountpoint}, Topic) -> + <>. diff --git a/apps/emqx_gateway/src/emqx_gateway_app.erl b/apps/emqx_gateway/src/emqx_gateway_app.erl index 1ecd9cf26..d90942220 100644 --- a/apps/emqx_gateway/src/emqx_gateway_app.erl +++ b/apps/emqx_gateway/src/emqx_gateway_app.erl @@ -83,4 +83,4 @@ load_gateway_by_default([{Type, Confs}|More]) -> load_gateway_by_default(More). confs() -> - maps:to_list(emqx:get_config([gateway], [])). + maps:to_list(emqx:get_config([gateway], #{})). diff --git a/apps/emqx_gateway/src/emqx_gateway_metrics.erl b/apps/emqx_gateway/src/emqx_gateway_metrics.erl index 458017118..77b97a6a1 100644 --- a/apps/emqx_gateway/src/emqx_gateway_metrics.erl +++ b/apps/emqx_gateway/src/emqx_gateway_metrics.erl @@ -18,7 +18,7 @@ -behaviour(gen_server). --include("include/emqx_gateway.hrl"). +-include_lib("emqx_gateway/include/emqx_gateway.hrl"). %% APIs diff --git a/apps/emqx_gateway/test/emqx_coap_api_SUITE.erl b/apps/emqx_gateway/test/emqx_coap_api_SUITE.erl new file mode 100644 index 000000000..74b0cadc8 --- /dev/null +++ b/apps/emqx_gateway/test/emqx_coap_api_SUITE.erl @@ -0,0 +1,224 @@ +%%-------------------------------------------------------------------- +%% Copyright (C) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_coap_api_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-define(CONF_DEFAULT, <<" +gateway.coap { + idle_timeout = 30s + enable_stats = false + mountpoint = \"\" + notify_type = qos + connection_required = true + subscribe_qos = qos1 + publish_qos = qos1 + authentication = undefined + + listeners.udp.default { + bind = 5683 + } + } + ">>). + +-define(HOST, "127.0.0.1"). +-define(PORT, 5683). +-define(CONN_URI, "coap://127.0.0.1/mqtt/connection?clientid=client1&username=admin&password=public"). + +-define(LOGT(Format, Args), ct:pal("TEST_SUITE: " ++ Format, Args)). + +%%-------------------------------------------------------------------- +%% Setups +%%-------------------------------------------------------------------- + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + ok = emqx_config:init_load(emqx_gateway_schema, ?CONF_DEFAULT), + emqx_mgmt_api_test_util:init_suite([emqx_gateway]), + Config. + +end_per_suite(Config) -> + emqx_mgmt_api_test_util:end_suite([emqx_gateway]), + Config. + +set_special_configs(emqx_gatewway) -> + ok = emqx_config:init_load(emqx_gateway_schema, ?CONF_DEFAULT); + +set_special_configs(_) -> + ok. + +%%-------------------------------------------------------------------- +%% Cases +%%-------------------------------------------------------------------- +t_send_request_api(_) -> + ClientId = start_client(), + timer:sleep(200), + Path = emqx_mgmt_api_test_util:api_path(["gateway/coap/client1/request"]), + Token = <<"atoken">>, + Payload = <<"simple echo this">>, + Req = #{token => Token, + payload => Payload, + timeout => 10, + content_type => <<"text/plain">>, + method => <<"get">>}, + Auth = emqx_mgmt_api_test_util:auth_header_(), + {ok, Response} = emqx_mgmt_api_test_util:request_api(post, + Path, + "method=get", + Auth, + Req + ), + #{<<"token">> := RToken, <<"payload">> := RPayload} = + emqx_json:decode(Response, [return_maps]), + ?assertEqual(Token, RToken), + ?assertEqual(Payload, RPayload), + erlang:exit(ClientId, kill), + ok. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Internal Functions +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +start_client() -> + spawn(fun coap_client/0). + +coap_client() -> + {ok, CSock} = gen_udp:open(0, [binary, {active, false}]), + test_send_coap_request(CSock, post, <<>>, [], 1), + Response = test_recv_coap_response(CSock), + ?assertEqual({ok, created}, Response#coap_message.method), + echo_loop(CSock). + +echo_loop(CSock) -> + #coap_message{payload = Payload} = Req = test_recv_coap_request(CSock), + test_send_coap_response(CSock, ?HOST, ?PORT, {ok, content}, Payload, Req), + echo_loop(CSock). + +test_send_coap_request(UdpSock, Method, Content, Options, MsgId) -> + is_list(Options) orelse error("Options must be a list"), + case resolve_uri(?CONN_URI) of + {coap, {IpAddr, Port}, Path, Query} -> + Request0 = emqx_coap_message:request(con, Method, Content, + [{uri_path, Path}, + {uri_query, Query} | Options]), + Request = Request0#coap_message{id = MsgId}, + ?LOGT("send_coap_request Request=~p", [Request]), + RequestBinary = emqx_coap_frame:serialize_pkt(Request, undefined), + ?LOGT("test udp socket send to ~p:~p, data=~p", [IpAddr, Port, RequestBinary]), + ok = gen_udp:send(UdpSock, IpAddr, Port, RequestBinary); + {SchemeDiff, ChIdDiff, _, _} -> + error(lists:flatten(io_lib:format("scheme ~s or ChId ~s does not match with socket", [SchemeDiff, ChIdDiff]))) + end. + +test_recv_coap_response(UdpSock) -> + {ok, {Address, Port, Packet}} = gen_udp:recv(UdpSock, 0, 2000), + {ok, Response, _, _} = emqx_coap_frame:parse(Packet, undefined), + ?LOGT("test udp receive from ~p:~p, data1=~p, Response=~p", [Address, Port, Packet, Response]), + #coap_message{type = ack, method = Method, id=Id, token = Token, options = Options, payload = Payload} = Response, + ?LOGT("receive coap response Method=~p, Id=~p, Token=~p, Options=~p, Payload=~p", [Method, Id, Token, Options, Payload]), + Response. + +test_recv_coap_request(UdpSock) -> + case gen_udp:recv(UdpSock, 0) of + {ok, {_Address, _Port, Packet}} -> + {ok, Request, _, _} = emqx_coap_frame:parse(Packet, undefined), + #coap_message{type = con, method = Method, id=Id, token = Token, payload = Payload, options = Options} = Request, + ?LOGT("receive coap request Method=~p, Id=~p, Token=~p, Options=~p, Payload=~p", [Method, Id, Token, Options, Payload]), + Request; + {error, Reason} -> + ?LOGT("test_recv_coap_request failed, Reason=~p", [Reason]), + timeout_test_recv_coap_request + end. + +test_send_coap_response(UdpSock, Host, Port, Code, Content, Request) -> + is_list(Host) orelse error("Host is not a string"), + {ok, IpAddr} = inet:getaddr(Host, inet), + Response = emqx_coap_message:piggyback(Code, Content, Request), + ?LOGT("test_send_coap_response Response=~p", [Response]), + Binary = emqx_coap_frame:serialize_pkt(Response, undefined), + ok = gen_udp:send(UdpSock, IpAddr, Port, Binary). + +resolve_uri(Uri) -> + {ok, #{scheme := Scheme, + host := Host, + port := PortNo, + path := Path} = URIMap} = emqx_http_lib:uri_parse(Uri), + Query = maps:get(query, URIMap, undefined), + {ok, PeerIP} = inet:getaddr(Host, inet), + {Scheme, {PeerIP, PortNo}, split_path(Path), split_query(Query)}. + +split_path([]) -> []; +split_path([$/]) -> []; +split_path([$/ | Path]) -> split_segments(Path, $/, []). + +split_query(undefined) -> #{}; +split_query(Path) -> + split_segments(Path, $&, []). + +split_segments(Path, Char, Acc) -> + case string:rchr(Path, Char) of + 0 -> + [make_segment(Path) | Acc]; + N when N > 0 -> + split_segments(string:substr(Path, 1, N-1), Char, + [make_segment(string:substr(Path, N+1)) | Acc]) + end. + +make_segment(Seg) -> + list_to_binary(emqx_http_lib:uri_decode(Seg)). + + +get_coap_path(Options) -> + get_path(Options, <<>>). + +get_coap_query(Options) -> + proplists:get_value(uri_query, Options, []). + +get_coap_observe(Options) -> + get_observe(Options). + + +get_path([], Acc) -> + %?LOGT("get_path Acc=~p", [Acc]), + Acc; +get_path([{uri_path, Path1}|T], Acc) -> + %?LOGT("Path=~p, Acc=~p", [Path1, Acc]), + get_path(T, join_path(Path1, Acc)); +get_path([{_, _}|T], Acc) -> + get_path(T, Acc). + +get_observe([]) -> + undefined; +get_observe([{observe, V}|_T]) -> + V; +get_observe([{_, _}|T]) -> + get_observe(T). + +join_path([], Acc) -> Acc; +join_path([<<"/">>|T], Acc) -> + join_path(T, Acc); +join_path([H|T], Acc) -> + join_path(T, <>). + +sprintf(Format, Args) -> + lists:flatten(io_lib:format(Format, Args)). diff --git a/apps/emqx_management/test/emqx_mgmt_api_test_util.erl b/apps/emqx_management/test/emqx_mgmt_api_test_util.erl index fa924a71c..34fa731c7 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_test_util.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_test_util.erl @@ -21,23 +21,30 @@ -define(BASE_PATH, "/api/v5"). init_suite() -> + init_suite([]). + +init_suite(Apps) -> ekka_mnesia:start(), application:load(emqx_management), - emqx_ct_helpers:start_apps([emqx_dashboard], fun set_special_configs/1). + emqx_ct_helpers:start_apps(Apps ++ [emqx_dashboard], fun set_special_configs/1). + end_suite() -> + end_suite([]). + +end_suite(Apps) -> application:unload(emqx_management), - emqx_ct_helpers:stop_apps([emqx_dashboard]). + emqx_ct_helpers:stop_apps(Apps ++ [emqx_dashboard]). set_special_configs(emqx_dashboard) -> Config = #{ - default_username => <<"admin">>, - default_password => <<"public">>, - listeners => [#{ - protocol => http, - port => 18083 - }] - }, + default_username => <<"admin">>, + default_password => <<"public">>, + listeners => [#{ + protocol => http, + port => 18083 + }] + }, emqx_config:put([emqx_dashboard], Config), ok; set_special_configs(_App) -> @@ -53,7 +60,7 @@ request_api(Method, Url, QueryParams, Auth) -> request_api(Method, Url, QueryParams, Auth, []). request_api(Method, Url, QueryParams, Auth, []) - when (Method =:= options) orelse + when (Method =:= options) orelse (Method =:= get) orelse (Method =:= put) orelse (Method =:= head) orelse From c54847b6e9033bf422d273e819cd58a64f757617 Mon Sep 17 00:00:00 2001 From: Jim Moen Date: Wed, 1 Sep 2021 14:40:10 +0800 Subject: [PATCH 077/109] refactor(mqtt_frame): Avoid duplicate variable when frame assembling. --- apps/emqx/src/emqx_frame.erl | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/emqx/src/emqx_frame.erl b/apps/emqx/src/emqx_frame.erl index 082801bad..79a740bed 100644 --- a/apps/emqx/src/emqx_frame.erl +++ b/apps/emqx/src/emqx_frame.erl @@ -100,14 +100,10 @@ parse(<>, StrictMode andalso validate_header(Type, Dup, QoS, Retain), Header = #mqtt_packet_header{type = Type, dup = bool(Dup), - qos = QoS, + qos = fixqos(Type, QoS), retain = bool(Retain) }, - Header1 = case fixqos(Type, QoS) of - QoS -> Header; - FixedQoS -> Header#mqtt_packet_header{qos = FixedQoS} - end, - parse_remaining_len(Rest, Header1, Options); + parse_remaining_len(Rest, Header, Options); parse(Bin, {{len, #{hdr := Header, len := {Multiplier, Length}} From be38bcc5ccccc2a7a421506c6c4de33c8f1c9531 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 8 Sep 2021 09:46:47 +0800 Subject: [PATCH 078/109] chore(authn): adapt listener id type --- apps/emqx/include/emqx.hrl | 2 +- apps/emqx/src/emqx_authentication.erl | 20 +++++++----- apps/emqx/src/emqx_channel.erl | 8 ++--- apps/emqx/test/emqx_authentication_SUITE.erl | 10 +++--- apps/emqx_authn/include/emqx_authn.hrl | 2 +- apps/emqx_authn/src/emqx_authn_api.erl | 34 ++++++++++++++------ apps/emqx_authn/src/emqx_authn_app.erl | 2 +- 7 files changed, 48 insertions(+), 30 deletions(-) diff --git a/apps/emqx/include/emqx.hrl b/apps/emqx/include/emqx.hrl index 63ab13256..550e650a2 100644 --- a/apps/emqx/include/emqx.hrl +++ b/apps/emqx/include/emqx.hrl @@ -147,6 +147,6 @@ }). -record(chain, - { name :: binary() + { name :: atom() , authenticators :: [#authenticator{}] }). \ No newline at end of file diff --git a/apps/emqx/src/emqx_authentication.erl b/apps/emqx/src/emqx_authentication.erl index 2b561d298..8dcca50eb 100644 --- a/apps/emqx/src/emqx_authentication.erl +++ b/apps/emqx/src/emqx_authentication.erl @@ -473,7 +473,7 @@ handle_call({update_authenticator, ChainName, AuthenticatorID, Config}, _From, S state = #{version := Version} = ST} = Authenticator -> case AuthenticatorID =:= generate_id(Config) of true -> - Unique = <>, + Unique = unique(ChainName, AuthenticatorID, Version), case Provider:update(Config#{'_unique' => Unique}, ST) of {ok, NewST} -> NewAuthenticator = Authenticator#authenticator{state = switch_version(NewST)}, @@ -575,17 +575,17 @@ split_by_id(ID, AuthenticatorsConfig) -> end. global_chain(mqtt) -> - <<"mqtt:global">>; + 'mqtt:global'; global_chain('mqtt-sn') -> - <<"mqtt-sn:global">>; + 'mqtt-sn:global'; global_chain(coap) -> - <<"coap:global">>; + 'coap:global'; global_chain(lwm2m) -> - <<"lwm2m:global">>; + 'lwm2m:global'; global_chain(stomp) -> - <<"stomp:global">>; + 'stomp:global'; global_chain(_) -> - <<"unknown:global">>. + 'unknown:global'. may_hook(#{hooked := false} = State) -> case lists:any(fun(#chain{authenticators = []}) -> false; @@ -618,7 +618,7 @@ do_create_authenticator(ChainName, AuthenticatorID, #{enable := Enable} = Config undefined -> {error, no_available_provider}; Provider -> - Unique = <>, + Unique = unique(ChainName, AuthenticatorID, ?VER_1), case Provider:create(Config#{'_unique' => Unique}) of {ok, State} -> Authenticator = #authenticator{id = AuthenticatorID, @@ -704,6 +704,10 @@ serialize_authenticator(#authenticator{id = ID, , state => State }. +unique(ChainName, AuthenticatorID, Version) -> + NChainName = atom_to_binary(ChainName), + <>. + switch_version(State = #{version := ?VER_1}) -> State#{version := ?VER_2}; switch_version(State = #{version := ?VER_2}) -> diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index e25a9c8d6..5e978669d 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -214,7 +214,7 @@ init(ConnInfo = #{peername := {PeerHost, _Port}, ClientInfo = set_peercert_infos( Peercert, #{zone => Zone, - listener => Listener, + listener => emqx_listeners:listener_id(Type, Listener), protocol => Protocol, peerhost => PeerHost, sockport => SockPort, @@ -223,7 +223,7 @@ init(ConnInfo = #{peername := {PeerHost, _Port}, mountpoint => MountPoint, is_bridge => false, is_superuser => false - }, Zone, Listener), + }, Zone), {NClientInfo, NConnInfo} = take_ws_cookie(ClientInfo, ConnInfo), #channel{conninfo = NConnInfo, clientinfo = NClientInfo, @@ -244,12 +244,12 @@ quota_policy(RawPolicy) -> erlang:trunc(hocon_postprocess:duration(StrWind) / 1000)}} || {Name, [StrCount, StrWind]} <- maps:to_list(RawPolicy)]. -set_peercert_infos(NoSSL, ClientInfo, _, _) +set_peercert_infos(NoSSL, ClientInfo, _) when NoSSL =:= nossl; NoSSL =:= undefined -> ClientInfo#{username => undefined}; -set_peercert_infos(Peercert, ClientInfo, Zone, _Listener) -> +set_peercert_infos(Peercert, ClientInfo, Zone) -> {DN, CN} = {esockd_peercert:subject(Peercert), esockd_peercert:common_name(Peercert)}, PeercetAs = fun(Key) -> diff --git a/apps/emqx/test/emqx_authentication_SUITE.erl b/apps/emqx/test/emqx_authentication_SUITE.erl index a940adc88..0b610d2e5 100644 --- a/apps/emqx/test/emqx_authentication_SUITE.erl +++ b/apps/emqx/test/emqx_authentication_SUITE.erl @@ -94,7 +94,7 @@ end_per_suite(_) -> t_chain(_) -> % CRUD of authentication chain - ChainName = <<"test">>, + ChainName = 'test', ?assertMatch({ok, []}, ?AUTHN:list_chains()), ?assertMatch({ok, #{name := ChainName, authenticators := []}}, ?AUTHN:create_chain(ChainName)), ?assertEqual({error, {already_exists, {chain, ChainName}}}, ?AUTHN:create_chain(ChainName)), @@ -105,7 +105,7 @@ t_chain(_) -> ok. t_authenticator(_) -> - ChainName = <<"test">>, + ChainName = 'test', AuthenticatorConfig1 = #{mechanism => 'password-based', backend => 'built-in-database', enable => true}, @@ -155,7 +155,7 @@ t_authenticator(_) -> ok. t_authenticate(_) -> - ListenerID = <<"tcp:default">>, + ListenerID = 'tcp:default', ClientInfo = #{zone => default, listener => ListenerID, protocol => mqtt, @@ -186,7 +186,7 @@ t_update_config(_) -> ?AUTHN:add_provider(AuthNType1, ?MODULE), ?AUTHN:add_provider(AuthNType2, ?MODULE), - Global = <<"mqtt:global">>, + Global = 'mqtt:global', AuthenticatorConfig1 = #{mechanism => 'password-based', backend => 'built-in-database', enable => true}, @@ -212,7 +212,7 @@ t_update_config(_) -> ?assertMatch({ok, _}, update_config([authentication], {delete_authenticator, Global, ID1})), ?assertEqual({error, {not_found, {authenticator, ID1}}}, ?AUTHN:lookup_authenticator(Global, ID1)), - ListenerID = <<"tcp:default">>, + ListenerID = 'tcp:default', ConfKeyPath = [listeners, tcp, default, authentication], ?assertMatch({ok, _}, update_config(ConfKeyPath, {create_authenticator, ListenerID, AuthenticatorConfig1})), ?assertMatch({ok, #{id := ID1, state := #{mark := 1}}}, ?AUTHN:lookup_authenticator(ListenerID, ID1)), diff --git a/apps/emqx_authn/include/emqx_authn.hrl b/apps/emqx_authn/include/emqx_authn.hrl index bdf93204a..5eef08012 100644 --- a/apps/emqx_authn/include/emqx_authn.hrl +++ b/apps/emqx_authn/include/emqx_authn.hrl @@ -18,7 +18,7 @@ -define(AUTHN, emqx_authentication). --define(GLOBAL, <<"mqtt:global">>). +-define(GLOBAL, 'mqtt:global'). -define(RE_PLACEHOLDER, "\\$\\{[a-z0-9\\-]+\\}"). diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index afac57f99..7c3bcbd63 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -1824,7 +1824,8 @@ find_listener(ListenerID) -> {ok, {Type, Name}} end. -create_authenticator(ConfKeyPath, ChainName, Config) -> +create_authenticator(ConfKeyPath, ChainName0, Config) -> + ChainName = to_atom(ChainName0), case update_config(ConfKeyPath, {create_authenticator, ChainName, Config}) of {ok, #{post_config_update := #{?AUTHN := #{id := ID}}, raw_config := AuthenticatorsConfig}} -> @@ -1849,7 +1850,8 @@ list_authenticator(ConfKeyPath, AuthenticatorID) -> serialize_error(Reason) end. -update_authenticator(ConfKeyPath, ChainName, AuthenticatorID, Config) -> +update_authenticator(ConfKeyPath, ChainName0, AuthenticatorID, Config) -> + ChainName = to_atom(ChainName0), case update_config(ConfKeyPath, {update_authenticator, ChainName, AuthenticatorID, Config}) of {ok, #{post_config_update := #{?AUTHN := #{id := ID}}, @@ -1860,7 +1862,8 @@ update_authenticator(ConfKeyPath, ChainName, AuthenticatorID, Config) -> serialize_error(Reason) end. -delete_authenticator(ConfKeyPath, ChainName, AuthenticatorID) -> +delete_authenticator(ConfKeyPath, ChainName0, AuthenticatorID) -> + ChainName = to_atom(ChainName0), case update_config(ConfKeyPath, {delete_authenticator, ChainName, AuthenticatorID}) of {ok, _} -> {204}; @@ -1868,7 +1871,8 @@ delete_authenticator(ConfKeyPath, ChainName, AuthenticatorID) -> serialize_error(Reason) end. -move_authenitcator(ConfKeyPath, ChainName, AuthenticatorID, Position) -> +move_authenitcator(ConfKeyPath, ChainName0, AuthenticatorID, Position) -> + ChainName = to_atom(ChainName0), case update_config(ConfKeyPath, {move_authenticator, ChainName, AuthenticatorID, Position}) of {ok, _} -> {204}; @@ -1876,7 +1880,8 @@ move_authenitcator(ConfKeyPath, ChainName, AuthenticatorID, Position) -> serialize_error(Reason) end. -add_user(ChainName, AuthenticatorID, #{<<"user_id">> := UserID, <<"password">> := Password} = UserInfo) -> +add_user(ChainName0, AuthenticatorID, #{<<"user_id">> := UserID, <<"password">> := Password} = UserInfo) -> + ChainName = to_atom(ChainName0), Superuser = maps:get(<<"superuser">>, UserInfo, false), case ?AUTHN:add_user(ChainName, AuthenticatorID, #{ user_id => UserID , password => Password @@ -1891,7 +1896,8 @@ add_user(_, _, #{<<"user_id">> := _}) -> add_user(_, _, _) -> serialize_error({missing_parameter, user_id}). -update_user(ChainName, AuthenticatorID, UserID, UserInfo) -> +update_user(ChainName0, AuthenticatorID, UserID, UserInfo) -> + ChainName = to_atom(ChainName0), case maps:with([<<"password">>, <<"superuser">>], UserInfo) =:= #{} of true -> serialize_error({missing_parameter, password}); @@ -1904,7 +1910,8 @@ update_user(ChainName, AuthenticatorID, UserID, UserInfo) -> end end. -find_user(ChainName, AuthenticatorID, UserID) -> +find_user(ChainName0, AuthenticatorID, UserID) -> + ChainName = to_atom(ChainName0), case ?AUTHN:lookup_user(ChainName, AuthenticatorID, UserID) of {ok, User} -> {200, User}; @@ -1912,7 +1919,8 @@ find_user(ChainName, AuthenticatorID, UserID) -> serialize_error(Reason) end. -delete_user(ChainName, AuthenticatorID, UserID) -> +delete_user(ChainName0, AuthenticatorID, UserID) -> + ChainName = to_atom(ChainName0), case ?AUTHN:delete_user(ChainName, AuthenticatorID, UserID) of ok -> {204}; @@ -1920,7 +1928,8 @@ delete_user(ChainName, AuthenticatorID, UserID) -> serialize_error(Reason) end. -list_users(ChainName, AuthenticatorID) -> +list_users(ChainName0, AuthenticatorID) -> + ChainName = to_atom(ChainName0), case ?AUTHN:list_users(ChainName, AuthenticatorID) of {ok, Users} -> {200, Users}; @@ -1973,4 +1982,9 @@ serialize_error(Reason) -> to_list(M) when is_map(M) -> [M]; to_list(L) when is_list(L) -> - L. \ No newline at end of file + L. + +to_atom(B) when is_binary(B) -> + binary_to_atom(B); +to_atom(A) when is_atom(A) -> + A. \ No newline at end of file diff --git a/apps/emqx_authn/src/emqx_authn_app.erl b/apps/emqx_authn/src/emqx_authn_app.erl index 58470289a..016decdd2 100644 --- a/apps/emqx_authn/src/emqx_authn_app.erl +++ b/apps/emqx_authn/src/emqx_authn_app.erl @@ -53,7 +53,7 @@ remove_providers() -> initialize() -> ?AUTHN:initialize_authentication(?GLOBAL, emqx:get_raw_config([authentication], [])), lists:foreach(fun({ListenerID, ListenerConfig}) -> - ?AUTHN:initialize_authentication(atom_to_binary(ListenerID), maps:get(authentication, ListenerConfig, [])) + ?AUTHN:initialize_authentication(ListenerID, maps:get(authentication, ListenerConfig, [])) end, emqx_listeners:list()), ok. From 8531e9ce11b720ec85a3c7c62c74d9d695da3746 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 8 Sep 2021 09:53:39 +0800 Subject: [PATCH 079/109] chore(authn): rename superuser to is_superuser --- apps/emqx/src/emqx_access_control.erl | 4 +-- apps/emqx/src/emqx_authentication.erl | 2 +- apps/emqx/src/emqx_channel.erl | 6 ++-- apps/emqx/test/emqx_authentication_SUITE.erl | 6 ++-- apps/emqx/test/emqx_channel_SUITE.erl | 2 +- apps/emqx_authn/data/user-credentials.csv | 2 +- apps/emqx_authn/data/user-credentials.json | 4 +-- apps/emqx_authn/src/emqx_authn_api.erl | 18 +++++----- .../emqx_enhanced_authn_scram_mnesia.erl | 24 ++++++------- .../src/simple_authn/emqx_authn_http.erl | 6 ++-- .../src/simple_authn/emqx_authn_jwt.erl | 2 +- .../src/simple_authn/emqx_authn_mnesia.erl | 34 +++++++++---------- .../src/simple_authn/emqx_authn_mongodb.erl | 6 ++-- .../src/simple_authn/emqx_authn_mysql.erl | 2 +- .../src/simple_authn/emqx_authn_pgsql.erl | 2 +- .../emqx_authn/test/data/user-credentials.csv | 2 +- .../test/data/user-credentials.json | 4 +-- apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl | 18 +++++----- .../test/emqx_authn_mnesia_SUITE.erl | 20 +++++------ 19 files changed, 82 insertions(+), 82 deletions(-) diff --git a/apps/emqx/src/emqx_access_control.erl b/apps/emqx/src/emqx_access_control.erl index 7d5b009ba..914651535 100644 --- a/apps/emqx/src/emqx_access_control.erl +++ b/apps/emqx/src/emqx_access_control.erl @@ -29,9 +29,9 @@ -spec(authenticate(emqx_types:clientinfo()) -> {ok, map()} | {ok, map(), binary()} | {continue, map()} | {continue, binary(), map()} | {error, term()}). authenticate(Credential) -> - case run_hooks('client.authenticate', [Credential], {ok, #{superuser => false}}) of + case run_hooks('client.authenticate', [Credential], {ok, #{is_superuser => false}}) of ok -> - {ok, #{superuser => false}}; + {ok, #{is_superuser => false}}; Other -> Other end. diff --git a/apps/emqx/src/emqx_authentication.erl b/apps/emqx/src/emqx_authentication.erl index 8dcca50eb..8cc8cf2df 100644 --- a/apps/emqx/src/emqx_authentication.erl +++ b/apps/emqx/src/emqx_authentication.erl @@ -80,7 +80,7 @@ -type config() :: #{atom() => term()}. -type state() :: #{atom() => term()}. --type extra() :: #{superuser := boolean(), +-type extra() :: #{is_superuser := boolean(), atom() => term()}. -type user_info() :: #{user_id := binary(), atom() => term()}. diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 5e978669d..26342d8aa 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -1303,11 +1303,11 @@ do_authenticate(#{auth_method := AuthMethod} = Credential, #channel{clientinfo = case emqx_access_control:authenticate(Credential) of {ok, Result} -> {ok, Properties, - Channel#channel{clientinfo = ClientInfo#{is_superuser => maps:get(superuser, Result, false)}, + Channel#channel{clientinfo = ClientInfo#{is_superuser => maps:get(is_superuser, Result, false)}, auth_cache = #{}}}; {ok, Result, AuthData} -> {ok, Properties#{'Authentication-Data' => AuthData}, - Channel#channel{clientinfo = ClientInfo#{is_superuser => maps:get(superuser, Result, false)}, + Channel#channel{clientinfo = ClientInfo#{is_superuser => maps:get(is_superuser, Result, false)}, auth_cache = #{}}}; {continue, AuthCache} -> {continue, Properties, Channel#channel{auth_cache = AuthCache}}; @@ -1320,7 +1320,7 @@ do_authenticate(#{auth_method := AuthMethod} = Credential, #channel{clientinfo = do_authenticate(Credential, #channel{clientinfo = ClientInfo} = Channel) -> case emqx_access_control:authenticate(Credential) of - {ok, #{superuser := Superuser}} -> + {ok, #{is_superuser := Superuser}} -> {ok, #{}, Channel#channel{clientinfo = ClientInfo#{is_superuser => Superuser}}}; {error, Reason} -> {error, emqx_reason_codes:connack_error(Reason)} diff --git a/apps/emqx/test/emqx_authentication_SUITE.erl b/apps/emqx/test/emqx_authentication_SUITE.erl index 0b610d2e5..aa4d55fee 100644 --- a/apps/emqx/test/emqx_authentication_SUITE.erl +++ b/apps/emqx/test/emqx_authentication_SUITE.erl @@ -73,7 +73,7 @@ update(_Config, _State) -> {ok, #{mark => 2}}. authenticate(#{username := <<"good">>}, _State) -> - {ok, #{superuser => true}}; + {ok, #{is_superuser => true}}; authenticate(#{username := _}, _State) -> {error, bad_username_or_password}. @@ -161,7 +161,7 @@ t_authenticate(_) -> protocol => mqtt, username => <<"good">>, password => <<"any">>}, - ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)), + ?assertEqual({ok, #{is_superuser => false}}, emqx_access_control:authenticate(ClientInfo)), AuthNType = {'password-based', 'built-in-database'}, ?AUTHN:add_provider(AuthNType, ?MODULE), @@ -171,7 +171,7 @@ t_authenticate(_) -> enable => true}, ?AUTHN:create_chain(ListenerID), ?assertMatch({ok, _}, ?AUTHN:create_authenticator(ListenerID, AuthenticatorConfig)), - ?assertEqual({ok, #{superuser => true}}, emqx_access_control:authenticate(ClientInfo)), + ?assertEqual({ok, #{is_superuser => true}}, emqx_access_control:authenticate(ClientInfo)), ?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo#{username => <<"bad">>})), ?AUTHN:delete_chain(ListenerID), diff --git a/apps/emqx/test/emqx_channel_SUITE.erl b/apps/emqx/test/emqx_channel_SUITE.erl index 031f89612..775b40ee8 100644 --- a/apps/emqx/test/emqx_channel_SUITE.erl +++ b/apps/emqx/test/emqx_channel_SUITE.erl @@ -144,7 +144,7 @@ init_per_suite(Config) -> %% Access Control Meck ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]), ok = meck:expect(emqx_access_control, authenticate, - fun(_) -> {ok, #{superuser => false}} end), + fun(_) -> {ok, #{is_superuser => false}} end), ok = meck:expect(emqx_access_control, authorize, fun(_, _, _) -> allow end), %% Broker Meck ok = meck:new(emqx_broker, [passthrough, no_history, no_link]), diff --git a/apps/emqx_authn/data/user-credentials.csv b/apps/emqx_authn/data/user-credentials.csv index 0548308b7..cbadaefbc 100644 --- a/apps/emqx_authn/data/user-credentials.csv +++ b/apps/emqx_authn/data/user-credentials.csv @@ -1,3 +1,3 @@ -user_id,password_hash,salt,superuser +user_id,password_hash,salt,is_superuser myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235,true myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139,false diff --git a/apps/emqx_authn/data/user-credentials.json b/apps/emqx_authn/data/user-credentials.json index e54501233..94375df22 100644 --- a/apps/emqx_authn/data/user-credentials.json +++ b/apps/emqx_authn/data/user-credentials.json @@ -3,12 +3,12 @@ "user_id":"myuser1", "password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242", "salt": "e378187547bf2d6f0545a3f441aa4d8a", - "superuser": true + "is_superuser": true }, { "user_id":"myuser2", "password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b", "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f", - "superuser": false + "is_superuser": false } ] diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index 7c3bcbd63..c306e102b 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -765,7 +765,7 @@ create_user_api_spec() -> password => #{ type => string }, - superuser => #{ + is_superuser => #{ type => boolean, default => false } @@ -785,7 +785,7 @@ create_user_api_spec() -> user_id => #{ type => string }, - superuser => #{ + is_superuser => #{ type => boolean } } @@ -850,7 +850,7 @@ list_users_api_spec() -> user_id => #{ type => string }, - superuser => #{ + is_superuser => #{ type => boolean } } @@ -920,7 +920,7 @@ update_user_api_spec() -> password => #{ type => string }, - superuser => #{ + is_superuser => #{ type => boolean } } @@ -941,7 +941,7 @@ update_user_api_spec() -> user_id => #{ type => string }, - superuser => #{ + is_superuser => #{ type => boolean } } @@ -1025,7 +1025,7 @@ find_user_api_spec() -> user_id => #{ type => string }, - superuser => #{ + is_superuser => #{ type => boolean } } @@ -1882,10 +1882,10 @@ move_authenitcator(ConfKeyPath, ChainName0, AuthenticatorID, Position) -> add_user(ChainName0, AuthenticatorID, #{<<"user_id">> := UserID, <<"password">> := Password} = UserInfo) -> ChainName = to_atom(ChainName0), - Superuser = maps:get(<<"superuser">>, UserInfo, false), + Superuser = maps:get(<<"is_superuser">>, UserInfo, false), case ?AUTHN:add_user(ChainName, AuthenticatorID, #{ user_id => UserID , password => Password - , superuser => Superuser}) of + , is_superuser => Superuser}) of {ok, User} -> {201, User}; {error, Reason} -> @@ -1898,7 +1898,7 @@ add_user(_, _, _) -> update_user(ChainName0, AuthenticatorID, UserID, UserInfo) -> ChainName = to_atom(ChainName0), - case maps:with([<<"password">>, <<"superuser">>], UserInfo) =:= #{} of + case maps:with([<<"password">>, <<"is_superuser">>], UserInfo) =:= #{} of true -> serialize_error({missing_parameter, password}); false -> diff --git a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl index aa21c0484..4aac21bb2 100644 --- a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl +++ b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl @@ -53,7 +53,7 @@ , stored_key , server_key , salt - , superuser + , is_superuser }). %%------------------------------------------------------------------------------ @@ -147,9 +147,9 @@ add_user(#{user_id := UserID, fun() -> case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> - Superuser = maps:get(superuser, UserInfo, false), + Superuser = maps:get(is_superuser, UserInfo, false), add_user(UserID, Password, Superuser, State), - {ok, #{user_id => UserID, superuser => Superuser}}; + {ok, #{user_id => UserID, is_superuser => Superuser}}; [_] -> {error, already_exist} end @@ -173,8 +173,8 @@ update_user(UserID, User, case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> {error, not_found}; - [#user_info{superuser = Superuser} = UserInfo] -> - UserInfo1 = UserInfo#user_info{superuser = maps:get(superuser, User, Superuser)}, + [#user_info{is_superuser = Superuser} = UserInfo] -> + UserInfo1 = UserInfo#user_info{is_superuser = maps:get(is_superuser, User, Superuser)}, UserInfo2 = case maps:get(password, User, undefined) of undefined -> UserInfo1; @@ -229,13 +229,13 @@ check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = S {error, not_authorized} end. -check_client_final_message(Bin, #{superuser := Superuser} = Cache, #{algorithm := Alg}) -> +check_client_final_message(Bin, #{is_superuser := Superuser} = Cache, #{algorithm := Alg}) -> case esasl_scram:check_client_final_message( Bin, Cache#{algorithm => Alg} ) of {ok, ServerFinalMessage} -> - {ok, #{superuser => Superuser}, ServerFinalMessage}; + {ok, #{is_superuser => Superuser}, ServerFinalMessage}; {error, _Reason} -> {error, not_authorized} end. @@ -246,7 +246,7 @@ add_user(UserID, Password, Superuser, State) -> stored_key = StoredKey, server_key = ServerKey, salt = Salt, - superuser = Superuser}, + is_superuser = Superuser}, mnesia:write(?TAB, UserInfo, write). retrieve(UserID, #{user_group := UserGroup}) -> @@ -254,11 +254,11 @@ retrieve(UserID, #{user_group := UserGroup}) -> [#user_info{stored_key = StoredKey, server_key = ServerKey, salt = Salt, - superuser = Superuser}] -> + is_superuser = Superuser}] -> {ok, #{stored_key => StoredKey, server_key => ServerKey, salt => Salt, - superuser => Superuser}}; + is_superuser => Superuser}}; [] -> {error, not_found} end. @@ -273,5 +273,5 @@ trans(Fun, Args) -> {aborted, Reason} -> {error, Reason} end. -serialize_user_info(#user_info{user_id = {_, UserID}, superuser = Superuser}) -> - #{user_id => UserID, superuser => Superuser}. +serialize_user_info(#user_info{user_id = {_, UserID}, is_superuser = Superuser}) -> + #{user_id => UserID, is_superuser => Superuser}. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl index 19417218d..5495b139a 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -161,16 +161,16 @@ authenticate(Credential, #{'_unique' := Unique, try Request = generate_request(Credential, State), case emqx_resource:query(Unique, {Method, Request, RequestTimeout}) of - {ok, 204, _Headers} -> {ok, #{superuser => false}}; + {ok, 204, _Headers} -> {ok, #{is_superuser => false}}; {ok, 200, Headers, Body} -> ContentType = proplists:get_value(<<"content-type">>, Headers, <<"application/json">>), case safely_parse_body(ContentType, Body) of {ok, NBody} -> %% TODO: Return by user property - {ok, #{superuser => maps:get(<<"superuser">>, NBody, false), + {ok, #{is_superuser => maps:get(<<"is_superuser">>, NBody, false), user_property => NBody}}; {error, _Reason} -> - {ok, #{superuser => false}} + {ok, #{is_superuser => false}} end; {error, _Reason} -> ignore diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl index e55b58795..774d75157 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -249,7 +249,7 @@ verify(JWS, [JWK | More], VerifyClaims) -> Claims = emqx_json:decode(Payload, [return_maps]), case verify_claims(Claims, VerifyClaims) of ok -> - {ok, #{superuser => maps:get(<<"superuser">>, Claims, false)}}; + {ok, #{is_superuser => maps:get(<<"is_superuser">>, Claims, false)}}; {error, Reason} -> {error, Reason} end; diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl index f41edab8b..563a255f0 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl @@ -51,7 +51,7 @@ { user_id :: {user_group(), user_id()} , password_hash :: binary() , salt :: binary() - , superuser :: boolean() + , is_superuser :: boolean() }). -reflect_type([ user_id_type/0 ]). @@ -158,13 +158,13 @@ authenticate(#{password := Password} = Credential, case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of [] -> ignore; - [#user_info{password_hash = PasswordHash, salt = Salt0, superuser = Superuser}] -> + [#user_info{password_hash = PasswordHash, salt = Salt0, is_superuser = Superuser}] -> Salt = case Algorithm of bcrypt -> PasswordHash; _ -> Salt0 end, case PasswordHash =:= hash(Algorithm, Password, Salt) of - true -> {ok, #{superuser => Superuser}}; + true -> {ok, #{is_superuser => Superuser}}; false -> {error, bad_username_or_password} end end. @@ -197,9 +197,9 @@ add_user(#{user_id := UserID, case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> {PasswordHash, Salt} = hash(Password, State), - Superuser = maps:get(superuser, UserInfo, false), + Superuser = maps:get(is_superuser, UserInfo, false), insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser), - {ok, #{user_id => UserID, superuser => Superuser}}; + {ok, #{user_id => UserID, is_superuser => Superuser}}; [_] -> {error, already_exist} end @@ -225,8 +225,8 @@ update_user(UserID, UserInfo, {error, not_found}; [#user_info{ password_hash = PasswordHash , salt = Salt - , superuser = Superuser}] -> - NSuperuser = maps:get(superuser, UserInfo, Superuser), + , is_superuser = Superuser}] -> + NSuperuser = maps:get(is_superuser, UserInfo, Superuser), {NPasswordHash, NSalt} = case maps:get(password, UserInfo, undefined) of undefined -> {PasswordHash, Salt}; @@ -234,7 +234,7 @@ update_user(UserID, UserInfo, hash(Password, State) end, insert_user(UserGroup, UserID, NPasswordHash, NSalt, NSuperuser), - {ok, #{user_id => UserID, superuser => NSuperuser}} + {ok, #{user_id => UserID, is_superuser => NSuperuser}} end end). @@ -290,7 +290,7 @@ import(UserGroup, [#{<<"user_id">> := UserID, <<"password_hash">> := PasswordHash} = UserInfo | More]) when is_binary(UserID) andalso is_binary(PasswordHash) -> Salt = maps:get(<<"salt">>, UserInfo, <<>>), - Superuser = maps:get(<<"superuser">>, UserInfo, false), + Superuser = maps:get(<<"is_superuser">>, UserInfo, false), insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser), import(UserGroup, More); import(_UserGroup, [_ | _More]) -> @@ -305,7 +305,7 @@ import(UserGroup, File, Seq) -> {ok, #{user_id := UserID, password_hash := PasswordHash} = UserInfo} -> Salt = maps:get(salt, UserInfo, <<>>), - Superuser = maps:get(superuser, UserInfo, false), + Superuser = maps:get(is_superuser, UserInfo, false), insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser), import(UserGroup, File, Seq); {error, Reason} -> @@ -341,10 +341,10 @@ get_user_info_by_seq([PasswordHash | More1], [<<"password_hash">> | More2], Acc) get_user_info_by_seq(More1, More2, Acc#{password_hash => PasswordHash}); get_user_info_by_seq([Salt | More1], [<<"salt">> | More2], Acc) -> get_user_info_by_seq(More1, More2, Acc#{salt => Salt}); -get_user_info_by_seq([<<"true">> | More1], [<<"superuser">> | More2], Acc) -> - get_user_info_by_seq(More1, More2, Acc#{superuser => true}); -get_user_info_by_seq([<<"false">> | More1], [<<"superuser">> | More2], Acc) -> - get_user_info_by_seq(More1, More2, Acc#{superuser => false}); +get_user_info_by_seq([<<"true">> | More1], [<<"is_superuser">> | More2], Acc) -> + get_user_info_by_seq(More1, More2, Acc#{is_superuser => true}); +get_user_info_by_seq([<<"false">> | More1], [<<"is_superuser">> | More2], Acc) -> + get_user_info_by_seq(More1, More2, Acc#{is_superuser => false}); get_user_info_by_seq(_, _, _) -> {error, bad_format}. @@ -372,7 +372,7 @@ insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser) -> UserInfo = #user_info{user_id = {UserGroup, UserID}, password_hash = PasswordHash, salt = Salt, - superuser = Superuser}, + is_superuser = Superuser}, mnesia:write(?TAB, UserInfo, write). delete_user2(UserInfo) -> @@ -400,5 +400,5 @@ to_binary(B) when is_binary(B) -> to_binary(L) when is_list(L) -> iolist_to_binary(L). -serialize_user_info(#user_info{user_id = {_, UserID}, superuser = Superuser}) -> - #{user_id => UserID, superuser => Superuser}. +serialize_user_info(#user_info{user_id = {_, UserID}, is_superuser = Superuser}) -> + #{user_id => UserID, is_superuser => Superuser}. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl index f35be985a..9c2ec935c 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl @@ -149,7 +149,7 @@ authenticate(#{password := Password} = Credential, Doc -> case check_password(Password, Doc, State) of ok -> - {ok, #{superuser => superuser(Doc, State)}}; + {ok, #{is_superuser => is_superuser(Doc, State)}}; {error, {cannot_find_password_hash_field, PasswordHashField}} -> ?LOG(error, "['~s'] Can't find password hash field: ~s", [Unique, PasswordHashField]), {error, bad_username_or_password}; @@ -230,9 +230,9 @@ check_password(Password, end end. -superuser(Doc, #{superuser_field := SuperuserField}) -> +is_superuser(Doc, #{superuser_field := SuperuserField}) -> maps:get(SuperuserField, Doc, false); -superuser(_, _) -> +is_superuser(_, _) -> false. hash(Algorithm, Password, Salt, prefix) -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl index 67ccbf7ae..991bb6aee 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl @@ -123,7 +123,7 @@ authenticate(#{password := Password} = Credential, Selected = maps:from_list(lists:zip(Columns, Rows)), case check_password(Password, Selected, State) of ok -> - {ok, #{superuser => maps:get(<<"superuser">>, Selected, false)}}; + {ok, #{is_superuser => maps:get(<<"is_superuser">>, Selected, false)}}; {error, Reason} -> {error, Reason} end; diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl index 7676f338d..c497074de 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl @@ -113,7 +113,7 @@ authenticate(#{password := Password} = Credential, Selected = maps:from_list(lists:zip(NColumns, Rows)), case check_password(Password, Selected, State) of ok -> - {ok, #{superuser => maps:get(<<"superuser">>, Selected, false)}}; + {ok, #{is_superuser => maps:get(<<"is_superuser">>, Selected, false)}}; {error, Reason} -> {error, Reason} end; diff --git a/apps/emqx_authn/test/data/user-credentials.csv b/apps/emqx_authn/test/data/user-credentials.csv index 0548308b7..cbadaefbc 100644 --- a/apps/emqx_authn/test/data/user-credentials.csv +++ b/apps/emqx_authn/test/data/user-credentials.csv @@ -1,3 +1,3 @@ -user_id,password_hash,salt,superuser +user_id,password_hash,salt,is_superuser myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235,true myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139,false diff --git a/apps/emqx_authn/test/data/user-credentials.json b/apps/emqx_authn/test/data/user-credentials.json index e54501233..94375df22 100644 --- a/apps/emqx_authn/test/data/user-credentials.json +++ b/apps/emqx_authn/test/data/user-credentials.json @@ -3,12 +3,12 @@ "user_id":"myuser1", "password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242", "salt": "e378187547bf2d6f0545a3f441aa4d8a", - "superuser": true + "is_superuser": true }, { "user_id":"myuser2", "password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b", "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f", - "superuser": false + "is_superuser": false } ] diff --git a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl index 9d8b1d9fc..16c04771d 100644 --- a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl @@ -52,13 +52,13 @@ all() -> % JWS = generate_jws('hmac-based', Payload, <<"abcdef">>), % ClientInfo = #{username => <<"myuser">>, % password => JWS}, -% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), -% Payload1 = #{<<"username">> => <<"myuser">>, <<"superuser">> => true}, +% Payload1 = #{<<"username">> => <<"myuser">>, <<"is_superuser">> => true}, % JWS1 = generate_jws('hmac-based', Payload1, <<"abcdef">>), % ClientInfo1 = #{username => <<"myuser">>, % password => JWS1}, -% ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)), % BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>), % ClientInfo2 = ClientInfo#{password => BadJWS}, @@ -68,11 +68,11 @@ all() -> % Config2 = Config#{secret => base64:encode(<<"abcdef">>), % secret_base64_encoded => true}, % ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config2)), -% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), % Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]}, % ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config3)), -% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), % ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>}, ok)), % %% Expiration @@ -86,14 +86,14 @@ all() -> % , <<"exp">> => erlang:system_time(second) + 60}, % JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>), % ClientInfo4 = ClientInfo#{password => JWS4}, -% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)), % %% Issued At % Payload5 = #{ <<"username">> => <<"myuser">> % , <<"iat">> => erlang:system_time(second) - 60}, % JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>), % ClientInfo5 = ClientInfo#{password => JWS5}, -% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo5, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo5, ignored)), % Payload6 = #{ <<"username">> => <<"myuser">> % , <<"iat">> => erlang:system_time(second) + 60}, @@ -106,7 +106,7 @@ all() -> % , <<"nbf">> => erlang:system_time(second) - 60}, % JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>), % ClientInfo7 = ClientInfo#{password => JWS7}, -% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo7, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo7, ignored)), % Payload8 = #{ <<"username">> => <<"myuser">> % , <<"nbf">> => erlang:system_time(second) + 60}, @@ -134,7 +134,7 @@ all() -> % JWS = generate_jws('public-key', Payload, PrivateKey), % ClientInfo = #{username => <<"myuser">>, % password => JWS}, -% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), % ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ignored)), % ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), diff --git a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl index 4bc6961dd..959cf0323 100644 --- a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl @@ -56,9 +56,9 @@ all() -> % ClientInfo = #{zone => external, % username => <<"myuser">>, % password => <<"mypass">>}, -% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), % ?AUTH:enable(), -% ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)), +% ?assertEqual({ok, #{is_superuser => false}}, emqx_access_control:authenticate(ClientInfo)), % ClientInfo2 = ClientInfo#{username => <<"baduser">>}, % ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)), @@ -71,10 +71,10 @@ all() -> % UserInfo2 = UserInfo#{password => <<"mypass2">>}, % ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, UserInfo2)), % ClientInfo4 = ClientInfo#{password => <<"mypass2">>}, -% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)), -% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, #{superuser => true})), -% ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo4, ignored)), +% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, #{is_superuser => true})), +% ?assertEqual({stop, {ok, #{is_superuser => true}}}, ?AUTH:authenticate(ClientInfo4, ignored)), % ?assertEqual(ok, ?AUTH:delete_user(?CHAIN, ID, <<"myuser">>)), % ?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), @@ -107,15 +107,15 @@ all() -> % ClientInfo1 = #{username => <<"myuser1">>, % password => <<"mypassword1">>}, -% ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)), % ClientInfo2 = ClientInfo1#{username => <<"myuser2">>, % password => <<"mypassword2">>}, -% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)), % ClientInfo3 = ClientInfo1#{username => <<"myuser3">>, % password => <<"mypassword3">>}, -% ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo3, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => true}}}, ?AUTH:authenticate(ClientInfo3, ignored)), % ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), % ok. @@ -152,12 +152,12 @@ all() -> % ClientInfo1 = #{username => <<"myuser">>, % clientid => <<"myclient">>, % password => <<"mypass1">>}, -% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo1, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo1, ignored)), % ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, top)), % ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ignored)), % ClientInfo2 = ClientInfo1#{password => <<"mypass2">>}, -% ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)), +% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)), % ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), % ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)), From ce851e5b0f8da27edebdb48c179a53fb018ce618 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 8 Sep 2021 10:32:54 +0800 Subject: [PATCH 080/109] chore(authn): miss redis --- apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl index 18840fdea..949aeeaea 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl @@ -135,7 +135,7 @@ authenticate(#{password := Password} = Credential, Selected = merge(Fields, Values), case check_password(Password, Selected, State) of ok -> - {ok, #{superuser => maps:get("superuser", Selected, false)}}; + {ok, #{is_superuser => maps:get("is_superuser", Selected, false)}}; {error, Reason} -> {error, Reason} end; @@ -180,7 +180,7 @@ check_fields(["password_hash" | More], false) -> check_fields(More, true); check_fields(["salt" | More], HasPassHash) -> check_fields(More, HasPassHash); -check_fields(["superuser" | More], HasPassHash) -> +check_fields(["is_superuser" | More], HasPassHash) -> check_fields(More, HasPassHash); check_fields([Field | _], _) -> error({unsupported_field, Field}). From b5ded1ece0ebbe90719810bff78c1022dafab5eb Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 8 Sep 2021 10:46:18 +0800 Subject: [PATCH 081/109] chore(authn): add the serialization of more errors --- apps/emqx_authn/src/emqx_authn_api.erl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index c306e102b..a4d322712 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -1962,9 +1962,11 @@ serialize_error({not_found, {authenticator, ID}}) -> serialize_error({not_found, {listener, ID}}) -> {404, #{code => <<"NOT_FOUND">>, message => list_to_binary(io_lib:format("Listener '~s' does not exist", [ID]))}}; -serialize_error(name_has_be_used) -> +serialize_error({already_exists, {authenticator, ID}}) -> {409, #{code => <<"ALREADY_EXISTS">>, - message => <<"Name has be used">>}}; + message => list_to_binary( + io_lib:format("Authenticator '~s' already exist", [ID]) + )}}; serialize_error({missing_parameter, Name}) -> {400, #{code => <<"MISSING_PARAMETER">>, message => list_to_binary( @@ -1977,7 +1979,7 @@ serialize_error({invalid_parameter, Name}) -> )}}; serialize_error(Reason) -> {400, #{code => <<"BAD_REQUEST">>, - message => list_to_binary(io_lib:format("Todo: ~p", [Reason]))}}. + message => list_to_binary(io_lib:format("~p", [Reason]))}}. to_list(M) when is_map(M) -> [M]; From 29cad91a471526af5540bd42c5763acb28a2519a Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 8 Sep 2021 10:58:00 +0800 Subject: [PATCH 082/109] fix(authn): fix superuser in mongodb authn --- apps/emqx/src/emqx_channel.erl | 4 +- apps/emqx_authn/src/emqx_authn_api.erl | 9 ++++- .../emqx_enhanced_authn_scram_mnesia.erl | 40 +++++++++---------- .../src/simple_authn/emqx_authn_mnesia.erl | 30 +++++++------- .../src/simple_authn/emqx_authn_mongodb.erl | 10 ++++- 5 files changed, 52 insertions(+), 41 deletions(-) diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 26342d8aa..0b1ff7e25 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -1320,8 +1320,8 @@ do_authenticate(#{auth_method := AuthMethod} = Credential, #channel{clientinfo = do_authenticate(Credential, #channel{clientinfo = ClientInfo} = Channel) -> case emqx_access_control:authenticate(Credential) of - {ok, #{is_superuser := Superuser}} -> - {ok, #{}, Channel#channel{clientinfo = ClientInfo#{is_superuser => Superuser}}}; + {ok, #{is_superuser := IsSuperuser}} -> + {ok, #{}, Channel#channel{clientinfo = ClientInfo#{is_superuser => IsSuperuser}}}; {error, Reason} -> {error, emqx_reason_codes:connack_error(Reason)} end. diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index a4d322712..5ba1419f0 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -73,6 +73,7 @@ }, password_hash_field => <<"password_hash">>, salt_field => <<"salt">>, + is_superuser_field => <<"is_superuser">>, password_hash_algorithm => <<"sha256">>, salt_position => <<"prefix">> }). @@ -1398,6 +1399,10 @@ definitions() -> type => string, example => <<"salt">> }, + is_superuser_field => #{ + type => string, + example => <<"is_superuser">> + }, password_hash_algorithm => #{ type => string, enum => [<<"plain">>, <<"md5">>, <<"sha">>, <<"sha256">>, <<"sha512">>, <<"bcrypt">>], @@ -1882,10 +1887,10 @@ move_authenitcator(ConfKeyPath, ChainName0, AuthenticatorID, Position) -> add_user(ChainName0, AuthenticatorID, #{<<"user_id">> := UserID, <<"password">> := Password} = UserInfo) -> ChainName = to_atom(ChainName0), - Superuser = maps:get(<<"is_superuser">>, UserInfo, false), + IsSuperuser = maps:get(<<"is_superuser">>, UserInfo, false), case ?AUTHN:add_user(ChainName, AuthenticatorID, #{ user_id => UserID , password => Password - , is_superuser => Superuser}) of + , is_superuser => IsSuperuser}) of {ok, User} -> {201, User}; {error, Reason} -> diff --git a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl index 4aac21bb2..e0f37a50d 100644 --- a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl +++ b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl @@ -147,9 +147,9 @@ add_user(#{user_id := UserID, fun() -> case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> - Superuser = maps:get(is_superuser, UserInfo, false), - add_user(UserID, Password, Superuser, State), - {ok, #{user_id => UserID, is_superuser => Superuser}}; + IsSuperuser = maps:get(is_superuser, UserInfo, false), + add_user(UserID, Password, IsSuperuser, State), + {ok, #{user_id => UserID, is_superuser => IsSuperuser}}; [_] -> {error, already_exist} end @@ -173,8 +173,8 @@ update_user(UserID, User, case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> {error, not_found}; - [#user_info{is_superuser = Superuser} = UserInfo] -> - UserInfo1 = UserInfo#user_info{is_superuser = maps:get(is_superuser, User, Superuser)}, + [#user_info{is_superuser = IsSuperuser} = UserInfo] -> + UserInfo1 = UserInfo#user_info{is_superuser = maps:get(is_superuser, User, IsSuperuser)}, UserInfo2 = case maps:get(password, User, undefined) of undefined -> UserInfo1; @@ -229,36 +229,36 @@ check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = S {error, not_authorized} end. -check_client_final_message(Bin, #{is_superuser := Superuser} = Cache, #{algorithm := Alg}) -> +check_client_final_message(Bin, #{is_superuser := IsSuperuser} = Cache, #{algorithm := Alg}) -> case esasl_scram:check_client_final_message( Bin, Cache#{algorithm => Alg} ) of {ok, ServerFinalMessage} -> - {ok, #{is_superuser => Superuser}, ServerFinalMessage}; + {ok, #{is_superuser => IsSuperuser}, ServerFinalMessage}; {error, _Reason} -> {error, not_authorized} end. -add_user(UserID, Password, Superuser, State) -> +add_user(UserID, Password, IsSuperuser, State) -> {StoredKey, ServerKey, Salt} = esasl_scram:generate_authentication_info(Password, State), - UserInfo = #user_info{user_id = UserID, - stored_key = StoredKey, - server_key = ServerKey, - salt = Salt, - is_superuser = Superuser}, + UserInfo = #user_info{user_id = UserID, + stored_key = StoredKey, + server_key = ServerKey, + salt = Salt, + is_superuser = IsSuperuser}, mnesia:write(?TAB, UserInfo, write). retrieve(UserID, #{user_group := UserGroup}) -> case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of - [#user_info{stored_key = StoredKey, - server_key = ServerKey, - salt = Salt, - is_superuser = Superuser}] -> + [#user_info{stored_key = StoredKey, + server_key = ServerKey, + salt = Salt, + is_superuser = IsSuperuser}] -> {ok, #{stored_key => StoredKey, server_key => ServerKey, salt => Salt, - is_superuser => Superuser}}; + is_superuser => IsSuperuser}}; [] -> {error, not_found} end. @@ -273,5 +273,5 @@ trans(Fun, Args) -> {aborted, Reason} -> {error, Reason} end. -serialize_user_info(#user_info{user_id = {_, UserID}, is_superuser = Superuser}) -> - #{user_id => UserID, is_superuser => Superuser}. +serialize_user_info(#user_info{user_id = {_, UserID}, is_superuser = IsSuperuser}) -> + #{user_id => UserID, is_superuser => IsSuperuser}. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl index 563a255f0..b69d613f8 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl @@ -158,13 +158,13 @@ authenticate(#{password := Password} = Credential, case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of [] -> ignore; - [#user_info{password_hash = PasswordHash, salt = Salt0, is_superuser = Superuser}] -> + [#user_info{password_hash = PasswordHash, salt = Salt0, is_superuser = IsSuperuser}] -> Salt = case Algorithm of bcrypt -> PasswordHash; _ -> Salt0 end, case PasswordHash =:= hash(Algorithm, Password, Salt) of - true -> {ok, #{is_superuser => Superuser}}; + true -> {ok, #{is_superuser => IsSuperuser}}; false -> {error, bad_username_or_password} end end. @@ -197,9 +197,9 @@ add_user(#{user_id := UserID, case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> {PasswordHash, Salt} = hash(Password, State), - Superuser = maps:get(is_superuser, UserInfo, false), - insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser), - {ok, #{user_id => UserID, is_superuser => Superuser}}; + IsSuperuser = maps:get(is_superuser, UserInfo, false), + insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser), + {ok, #{user_id => UserID, is_superuser => IsSuperuser}}; [_] -> {error, already_exist} end @@ -225,8 +225,8 @@ update_user(UserID, UserInfo, {error, not_found}; [#user_info{ password_hash = PasswordHash , salt = Salt - , is_superuser = Superuser}] -> - NSuperuser = maps:get(is_superuser, UserInfo, Superuser), + , is_superuser = IsSuperuser}] -> + NSuperuser = maps:get(is_superuser, UserInfo, IsSuperuser), {NPasswordHash, NSalt} = case maps:get(password, UserInfo, undefined) of undefined -> {PasswordHash, Salt}; @@ -290,8 +290,8 @@ import(UserGroup, [#{<<"user_id">> := UserID, <<"password_hash">> := PasswordHash} = UserInfo | More]) when is_binary(UserID) andalso is_binary(PasswordHash) -> Salt = maps:get(<<"salt">>, UserInfo, <<>>), - Superuser = maps:get(<<"is_superuser">>, UserInfo, false), - insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser), + IsSuperuser = maps:get(<<"is_superuser">>, UserInfo, false), + insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser), import(UserGroup, More); import(_UserGroup, [_ | _More]) -> {error, bad_format}. @@ -305,8 +305,8 @@ import(UserGroup, File, Seq) -> {ok, #{user_id := UserID, password_hash := PasswordHash} = UserInfo} -> Salt = maps:get(salt, UserInfo, <<>>), - Superuser = maps:get(is_superuser, UserInfo, false), - insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser), + IsSuperuser = maps:get(is_superuser, UserInfo, false), + insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser), import(UserGroup, File, Seq); {error, Reason} -> {error, Reason} @@ -368,11 +368,11 @@ hash(Password, #{password_hash_algorithm := Algorithm} = State) -> PasswordHash = hash(Algorithm, Password, Salt), {PasswordHash, Salt}. -insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser) -> +insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser) -> UserInfo = #user_info{user_id = {UserGroup, UserID}, password_hash = PasswordHash, salt = Salt, - is_superuser = Superuser}, + is_superuser = IsSuperuser}, mnesia:write(?TAB, UserInfo, write). delete_user2(UserInfo) -> @@ -400,5 +400,5 @@ to_binary(B) when is_binary(B) -> to_binary(L) when is_list(L) -> iolist_to_binary(L). -serialize_user_info(#user_info{user_id = {_, UserID}, is_superuser = Superuser}) -> - #{user_id => UserID, is_superuser => Superuser}. +serialize_user_info(#user_info{user_id = {_, UserID}, is_superuser = IsSuperuser}) -> + #{user_id => UserID, is_superuser => IsSuperuser}. diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl index 9c2ec935c..9d77c673c 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl @@ -64,6 +64,7 @@ common_fields() -> , {selector, fun selector/1} , {password_hash_field, fun password_hash_field/1} , {salt_field, fun salt_field/1} + , {is_superuser_field, fun is_superuser_field/1} , {password_hash_algorithm, fun password_hash_algorithm/1} , {salt_position, fun salt_position/1} ] ++ emqx_authn_schema:common_fields(). @@ -84,6 +85,10 @@ salt_field(type) -> binary(); salt_field(nullable) -> true; salt_field(_) -> undefined. +is_superuser_field(type) -> binary(); +is_superuser_field(nullable) -> true; +is_superuser_field(_) -> undefined. + password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]}; password_hash_algorithm(default) -> sha256; password_hash_algorithm(_) -> undefined. @@ -109,6 +114,7 @@ create(#{ selector := Selector State = maps:with([ collection , password_hash_field , salt_field + , is_superuser_field , password_hash_algorithm , salt_position , '_unique'], Config), @@ -230,8 +236,8 @@ check_password(Password, end end. -is_superuser(Doc, #{superuser_field := SuperuserField}) -> - maps:get(SuperuserField, Doc, false); +is_superuser(Doc, #{is_superuser_field := IsSuperuserField}) -> + maps:get(IsSuperuserField, Doc, false); is_superuser(_, _) -> false. From 287d315ed506a3addd4b1b560e1ad770307b2151 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Wed, 8 Sep 2021 11:24:59 +0800 Subject: [PATCH 083/109] fix(listener): updating authentication no longer causes the listener to restart --- apps/emqx/src/emqx_listeners.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqx/src/emqx_listeners.erl b/apps/emqx/src/emqx_listeners.erl index 7b1d6b0dd..06d900ed5 100644 --- a/apps/emqx/src/emqx_listeners.erl +++ b/apps/emqx/src/emqx_listeners.erl @@ -280,7 +280,7 @@ flatten_listeners(Conf0) -> || {Type, Conf} <- maps:to_list(Conf0)])). do_flatten_listeners(Type, Conf0) -> - [{listener_id(Type, Name), Conf} || {Name, Conf} <- maps:to_list(Conf0)]. + [{listener_id(Type, Name), maps:remove(authentication, Conf)} || {Name, Conf} <- maps:to_list(Conf0)]. esockd_opts(Type, Opts0) -> Opts1 = maps:with([acceptors, max_connections, proxy_protocol, proxy_protocol_timeout], Opts0), From bcebe1de2470b6813645d11d582628ad4e796833 Mon Sep 17 00:00:00 2001 From: Jim Moen Date: Tue, 7 Sep 2021 16:54:05 +0800 Subject: [PATCH 084/109] refactor(emqx_cm_sup): Internal functions to create workers. --- apps/emqx/src/emqx_cm_sup.erl | 61 +++++++++++--------------- apps/emqx/test/emqx_client_SUITE.erl | 4 +- apps/emqx/test/emqx_flapping_SUITE.erl | 6 +-- 3 files changed, 30 insertions(+), 41 deletions(-) diff --git a/apps/emqx/src/emqx_cm_sup.erl b/apps/emqx/src/emqx_cm_sup.erl index f332a0868..cddd8aa5e 100644 --- a/apps/emqx/src/emqx_cm_sup.erl +++ b/apps/emqx/src/emqx_cm_sup.erl @@ -22,49 +22,38 @@ -export([init/1]). +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). +%%-------------------------------------------------------------------- +%% Supervisor callbacks +%%-------------------------------------------------------------------- + init([]) -> - Banned = #{id => banned, - start => {emqx_banned, start_link, []}, - restart => permanent, - shutdown => 1000, - type => worker, - modules => [emqx_banned]}, - Flapping = #{id => flapping, - start => {emqx_flapping, start_link, []}, - restart => permanent, - shutdown => 1000, - type => worker, - modules => [emqx_flapping]}, - %% Channel locker - Locker = #{id => locker, - start => {emqx_cm_locker, start_link, []}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_cm_locker] - }, - %% Channel registry - Registry = #{id => registry, - start => {emqx_cm_registry, start_link, []}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_cm_registry] - }, - %% Channel Manager - Manager = #{id => manager, - start => {emqx_cm, start_link, []}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [emqx_cm] - }, SupFlags = #{strategy => one_for_one, intensity => 100, period => 10 }, + Banned = child_spec(emqx_banned, 1000, worker), + Flapping = child_spec(emqx_flapping, 1000, worker), + Locker = child_spec(emqx_cm_locker, 5000, worker), + Registry = child_spec(emqx_cm_registry, 5000, worker), + Manager = child_spec(emqx_cm, 5000, worker), {ok, {SupFlags, [Banned, Flapping, Locker, Registry, Manager]}}. +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +child_spec(Mod, Shutdown, Type) -> + #{id => Mod, + start => {Mod, start_link, []}, + restart => permanent, + shutdown => Shutdown, + type => Type, + modules => [Mod] + }. diff --git a/apps/emqx/test/emqx_client_SUITE.erl b/apps/emqx/test/emqx_client_SUITE.erl index 117a0f5b9..0a3a050ac 100644 --- a/apps/emqx/test/emqx_client_SUITE.erl +++ b/apps/emqx/test/emqx_client_SUITE.erl @@ -114,8 +114,8 @@ t_cm(_) -> emqx_config:put_zone_conf(default, [mqtt, idle_timeout], 15000). t_cm_registry(_) -> - Info = supervisor:which_children(emqx_cm_sup), - {_, Pid, _, _} = lists:keyfind(registry, 1, Info), + Children = supervisor:which_children(emqx_cm_sup), + {_, Pid, _, _} = lists:keyfind(emqx_cm_registry, 1, Children), ignored = gen_server:call(Pid, <<"Unexpected call">>), gen_server:cast(Pid, <<"Unexpected cast">>), Pid ! <<"Unexpected info">>. diff --git a/apps/emqx/test/emqx_flapping_SUITE.erl b/apps/emqx/test/emqx_flapping_SUITE.erl index 5ac6b9cdf..a8e783c49 100644 --- a/apps/emqx/test/emqx_flapping_SUITE.erl +++ b/apps/emqx/test/emqx_flapping_SUITE.erl @@ -55,8 +55,8 @@ t_detect_check(_) -> true = emqx_banned:check(ClientInfo), timer:sleep(3000), false = emqx_banned:check(ClientInfo), - Childrens = supervisor:which_children(emqx_cm_sup), - {flapping, Pid, _, _} = lists:keyfind(flapping, 1, Childrens), + Children = supervisor:which_children(emqx_cm_sup), + {emqx_flapping, Pid, _, _} = lists:keyfind(emqx_flapping, 1, Children), gen_server:call(Pid, unexpected_msg), gen_server:cast(Pid, unexpected_msg), Pid ! test, @@ -72,4 +72,4 @@ t_expired_detecting(_) -> (_) -> false end, ets:tab2list(emqx_flapping))), timer:sleep(200), ?assertEqual(true, lists:all(fun({flapping, <<"client008">>, _, _, _}) -> false; - (_) -> true end, ets:tab2list(emqx_flapping))). \ No newline at end of file + (_) -> true end, ets:tab2list(emqx_flapping))). From 4e5d781d21b5178c07d628817d77bab9b3d641b6 Mon Sep 17 00:00:00 2001 From: Jim Moen Date: Tue, 7 Sep 2021 18:54:02 +0800 Subject: [PATCH 085/109] fix: Words spelling fix. --- apps/emqx/etc/emqx.conf | 8 ++++---- apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl | 2 +- apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/emqx/etc/emqx.conf b/apps/emqx/etc/emqx.conf index 7c404ff2d..6834d2a6e 100644 --- a/apps/emqx/etc/emqx.conf +++ b/apps/emqx/etc/emqx.conf @@ -64,7 +64,7 @@ listeners.tcp.default { proxy_protocol = false ## Sets the timeout for proxy protocol. EMQ X will close the TCP connection - ## if no proxy protocol packet recevied within the timeout. + ## if no proxy protocol packet received within the timeout. ## ## @doc listeners.tcp..proxy_protocol_timeout ## ValueType: Duration @@ -163,7 +163,7 @@ listeners.ssl.default { proxy_protocol = false ## Sets the timeout for proxy protocol. EMQ X will close the TCP connection - ## if no proxy protocol packet recevied within the timeout. + ## if no proxy protocol packet received within the timeout. ## ## @doc listeners.ssl..proxy_protocol_timeout ## ValueType: Duration @@ -345,7 +345,7 @@ listeners.ws.default { proxy_protocol = false ## Sets the timeout for proxy protocol. EMQ X will close the TCP connection - ## if no proxy protocol packet recevied within the timeout. + ## if no proxy protocol packet received within the timeout. ## ## @doc listeners.ws..proxy_protocol_timeout ## ValueType: Duration @@ -448,7 +448,7 @@ listeners.wss.default { proxy_protocol = false ## Sets the timeout for proxy protocol. EMQ X will close the TCP connection - ## if no proxy protocol packet recevied within the timeout. + ## if no proxy protocol packet received within the timeout. ## ## @doc listeners.wss..proxy_protocol_timeout ## ValueType: Duration diff --git a/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl b/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl index 51bcbd358..543b2e169 100644 --- a/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl +++ b/apps/emqx_gateway/src/bhvrs/emqx_gateway_conn.erl @@ -835,7 +835,7 @@ inc_incoming_stats(Ctx, FrameMod, Packet) -> ok end, Name = list_to_atom( - lists:concat(["packets.", FrameMod:type(Packet), ".recevied"])), + lists:concat(["packets.", FrameMod:type(Packet), ".received"])), emqx_gateway_ctx:metrics_inc(Ctx, Name). inc_outgoing_stats(Ctx, FrameMod, Packet) -> diff --git a/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl b/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl index e4b3d0095..23fb691d9 100644 --- a/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl @@ -994,7 +994,7 @@ t_will_case06(_) -> receive {deliver, WillTopic, #message{payload = WillMsg}} -> ok; - Msg -> ct:print("recevived --- unex: ~p", [Msg]) + Msg -> ct:print("received --- unex: ~p", [Msg]) after 1000 -> ct:fail(wait_willmsg_timeout) end, From e2d9d9bfcb46b781eda2851b487a5349a5d96a7a Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Wed, 8 Sep 2021 17:53:52 +0800 Subject: [PATCH 086/109] fix: banned api rfc time & login hidden error type (#5681) --- apps/emqx/src/emqx_banned.erl | 37 +++++++++++++++++++ .../emqx_dashboard/src/emqx_dashboard_api.erl | 8 ++-- .../src/emqx_mgmt_api_banned.erl | 30 ++------------- 3 files changed, 45 insertions(+), 30 deletions(-) diff --git a/apps/emqx/src/emqx_banned.erl b/apps/emqx/src/emqx_banned.erl index 715548d41..608734363 100644 --- a/apps/emqx/src/emqx_banned.erl +++ b/apps/emqx/src/emqx_banned.erl @@ -37,6 +37,7 @@ , delete/1 , info/1 , format/1 + , parse/1 ]). %% gen_server callbacks @@ -107,6 +108,33 @@ format(#banned{who = Who0, until => to_rfc3339(Until) }. +parse(Params) -> + Who = pares_who(Params), + By = maps:get(<<"by">>, Params, <<"mgmt_api">>), + Reason = maps:get(<<"reason">>, Params, <<"">>), + At = pares_time(maps:get(<<"at">>, Params, undefined), erlang:system_time(second)), + Until = pares_time(maps:get(<<"until">>, Params, undefined), At + 5 * 60), + #banned{ + who = Who, + by = By, + reason = Reason, + at = At, + until = Until + }. + +pares_who(#{as := As, who := Who}) -> + pares_who(#{<<"as">> => As, <<"who">> => Who}); +pares_who(#{<<"as">> := <<"peerhost">>, <<"who">> := Peerhost0}) -> + {ok, Peerhost} = inet:parse_address(binary_to_list(Peerhost0)), + {peerhost, Peerhost}; +pares_who(#{<<"as">> := As, <<"who">> := Who}) -> + {binary_to_atom(As, utf8), Who}. + +pares_time(undefined, Default) -> + Default; +pares_time(Rfc3339, _Default) -> + to_timestamp(Rfc3339). + maybe_format_host({peerhost, Host}) -> AddrBinary = list_to_binary(inet:ntoa(Host)), {peerhost, AddrBinary}; @@ -116,6 +144,11 @@ maybe_format_host({As, Who}) -> to_rfc3339(Timestamp) -> list_to_binary(calendar:system_time_to_rfc3339(Timestamp, [{unit, second}])). +to_timestamp(Rfc3339) when is_binary(Rfc3339) -> + to_timestamp(binary_to_list(Rfc3339)); +to_timestamp(Rfc3339) -> + calendar:rfc3339_to_system_time(Rfc3339, [{unit, second}]). + -spec(create(emqx_types:banned() | map()) -> ok). create(#{who := Who, by := By, @@ -130,12 +163,16 @@ create(#{who := Who, create(Banned) when is_record(Banned, banned) -> ekka_mnesia:dirty_write(?BANNED_TAB, Banned). +look_up(Who) when is_map(Who) -> + look_up(pares_who(Who)); look_up(Who) -> mnesia:dirty_read(?BANNED_TAB, Who). -spec(delete({clientid, emqx_types:clientid()} | {username, emqx_types:username()} | {peerhost, emqx_types:peerhost()}) -> ok). +delete(Who) when is_map(Who)-> + delete(pares_who(Who)); delete(Who) -> ekka_mnesia:dirty_delete(?BANNED_TAB, Who). diff --git a/apps/emqx_dashboard/src/emqx_dashboard_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_api.erl index 88ae85d9d..4761432fb 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_api.erl @@ -49,6 +49,8 @@ -define(EMPTY(V), (V == undefined orelse V == <<>>)). +-define(ERROR_USERNAME_OR_PWD, 'ERROR_USERNAME_OR_PWD'). + api_spec() -> {[ login_api() , logout_api() @@ -164,8 +166,8 @@ login(post, #{body := Params}) -> {ok, Token} -> Version = iolist_to_binary(proplists:get_value(version, emqx_sys:info())), {200, #{token => Token, version => Version, license => #{edition => ?RELEASE}}}; - {error, Code} -> - {401, #{code => Code, message => <<"Auth filed">>}} + {error, _} -> + {401, #{code => ?ERROR_USERNAME_OR_PWD, message => <<"Auth filed">>}} end. logout(_, #{body := Params}) -> @@ -233,7 +235,7 @@ parameters() -> unauthorized_request() -> object_schema( properties([{message, string}, - {code, string, <<"Resp Code">>, ['PASSWORD_ERROR','USERNAME_ERROR']} + {code, string, <<"Resp Code">>, [?ERROR_USERNAME_OR_PWD]} ]), <<"Unauthorized">> ). diff --git a/apps/emqx_management/src/emqx_mgmt_api_banned.erl b/apps/emqx_management/src/emqx_mgmt_api_banned.erl index 18abbd7e1..42addb4c7 100644 --- a/apps/emqx_management/src/emqx_mgmt_api_banned.erl +++ b/apps/emqx_management/src/emqx_mgmt_api_banned.erl @@ -101,44 +101,20 @@ banned(get, #{query_string := Params}) -> Response = emqx_mgmt_api:paginate(?TAB, Params, fun format/1), {200, Response}; banned(post, #{body := Body}) -> - Banned = trans_param(Body), - _ = emqx_banned:create(Banned), + _ = emqx_banned:create(emqx_banned:parse(Body)), {200}. delete_banned(delete, #{bindings := Params}) -> - Who = trans_who(Params), - case emqx_banned:look_up(Who) of + case emqx_banned:look_up(Params) of [] -> As0 = maps:get(as, Params), Who0 = maps:get(who, Params), Message = list_to_binary(io_lib:format("~p: ~p not found", [As0, Who0])), {404, #{code => 'RESOURCE_NOT_FOUND', message => Message}}; _ -> - ok = emqx_banned:delete(Who), + ok = emqx_banned:delete(Params), {200} end. -trans_param(Params) -> - Who = trans_who(Params), - By = maps:get(<<"by">>, Params, <<"mgmt_api">>), - Reason = maps:get(<<"reason">>, Params, <<"">>), - At = maps:get(<<"at">>, Params, erlang:system_time(second)), - Until = maps:get(<<"until">>, Params, At + 5 * 60), - #banned{ - who = Who, - by = By, - reason = Reason, - at = At, - until = Until - }. - -trans_who(#{as := As, who := Who}) -> - trans_who(#{<<"as">> => As, <<"who">> => Who}); -trans_who(#{<<"as">> := <<"peerhost">>, <<"who">> := Peerhost0}) -> - {ok, Peerhost} = inet:parse_address(binary_to_list(Peerhost0)), - {peerhost, Peerhost}; -trans_who(#{<<"as">> := As, <<"who">> := Who}) -> - {binary_to_atom(As, utf8), Who}. - format(Banned) -> emqx_banned:format(Banned). From f87a41a54ffdb643628042091ab3fab8e738036c Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Wed, 8 Sep 2021 19:58:04 +0800 Subject: [PATCH 087/109] fix: api support basic auth (#5687) --- apps/emqx_dashboard/src/emqx_dashboard.erl | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl index 603d8009b..d109dd445 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard.erl @@ -127,6 +127,16 @@ listener_name(Protocol, Port) -> authorize_appid(Req) -> case cowboy_req:parse_header(<<"authorization">>, Req) of + {basic, Username, Password} -> + case emqx_dashboard_admin:check(Username, Password) of + ok -> + ok; + {error, _} -> + {401, #{<<"WWW-Authenticate">> => + <<"Basic Realm=\"minirest-server\"">>}, + #{code => <<"ERROR_USERNAME_OR_PWD">>, + message => <<"Check your username and password">>}} + end; {bearer, Token} -> case emqx_dashboard_admin:verify_token(Token) of ok -> @@ -135,8 +145,7 @@ authorize_appid(Req) -> {401, #{<<"WWW-Authenticate">> => <<"Bearer Realm=\"minirest-server\"">>}, #{code => <<"TOKEN_TIME_OUT">>, - message => <<"POST '/login', get your new token">>} - }; + message => <<"POST '/login', get your new token">>}}; {error, not_found} -> {401, #{<<"WWW-Authenticate">> => <<"Bearer Realm=\"minirest-server\"">>}, @@ -145,7 +154,7 @@ authorize_appid(Req) -> end; _ -> {401, #{<<"WWW-Authenticate">> => - <<"Bearer Realm=\"minirest-server\"">>}, - #{code => <<"UNAUTHORIZED">>, - message => <<"POST '/login'">>}} + <<"Basic Realm=\"minirest-server\"">>}, + #{code => <<"ERROR_USERNAME_OR_PWD">>, + message => <<"Check your username and password">>}} end. From 5602cfc22341b28cf4be221dff5549e99d6c24dc Mon Sep 17 00:00:00 2001 From: Zaiming Shi Date: Wed, 8 Sep 2021 21:04:56 +0200 Subject: [PATCH 088/109] chore(build): ensure no gpb in release --- build | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build b/build index 727c3a1a2..27626387e 100755 --- a/build +++ b/build @@ -65,6 +65,10 @@ log() { make_rel() { # shellcheck disable=SC1010 ./rebar3 as "$PROFILE" do release,tar + if [ "$(find "_build/$PROFILE/rel/emqx/lib/" -name 'gpb-*' -type d)" != "" ]; then + echo "gpb should not be included in the release" + exit 1 + fi } ## unzip previous version .zip files to _build/$PROFILE/rel/emqx/releases before making relup From 3be6374f91e8f2bf8cb655e2b8023c5e538c0a35 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 9 Sep 2021 10:53:59 +0800 Subject: [PATCH 089/109] fix(auth mnesia api): fix get file type error --- apps/emqx_authz/src/emqx_authz_api_sources.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz_api_sources.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl index 4be631560..df62a418a 100644 --- a/apps/emqx_authz/src/emqx_authz_api_sources.erl +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -304,7 +304,7 @@ sources(get, _) -> {ok, Rules} = file:consult(Path), lists:append(AccIn, [#{type => file, enable => true, - rules => [ io_lib:format("~p", [R])|| R <- Rules], + rules => [ iolist_to_binary(io_lib:format("~p", [R])) || R <- Rules], annotations => #{status => healthy} }]); (#{type := _Type, annotations := #{id := Id}} = Source, AccIn) -> @@ -372,7 +372,7 @@ source(get, #{bindings := #{type := Type}}) -> {ok, Rules} = file:consult(Path), {200, #{type => file, enable => true, - rules => Rules, + rules => [ iolist_to_binary(io_lib:format("~p", [R])) || R <- Rules], annotations => #{status => healthy} } }; From 0813a81517ac73831eff100957e75dd47eaf5711 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 9 Sep 2021 11:51:46 +0800 Subject: [PATCH 090/109] fix(auth mnesia api): fix put file type error --- apps/emqx_authz/src/emqx_authz.erl | 1 + apps/emqx_authz/src/emqx_authz_api_sources.erl | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index af77390d5..e0e584806 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -198,6 +198,7 @@ post_config_update({{replace_once, Type}, #{type := Type} = Source}, _NewSources {Index, OldSource} = find_source_by_type(Type, OldInitedSources), case maps:get(type, OldSource, undefined) of undefined -> ok; + file -> ok; _ -> #{annotations := #{id := Id}} = OldSource, ok = emqx_resource:remove(Id) diff --git a/apps/emqx_authz/src/emqx_authz_api_sources.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl index df62a418a..00d0a5b7a 100644 --- a/apps/emqx_authz/src/emqx_authz_api_sources.erl +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -304,7 +304,7 @@ sources(get, _) -> {ok, Rules} = file:consult(Path), lists:append(AccIn, [#{type => file, enable => true, - rules => [ iolist_to_binary(io_lib:format("~p", [R])) || R <- Rules], + rules => [ iolist_to_binary(io_lib:format("~p.", [R])) || R <- Rules], annotations => #{status => healthy} }]); (#{type := _Type, annotations := #{id := Id}} = Source, AccIn) -> @@ -372,7 +372,7 @@ source(get, #{bindings := #{type := Type}}) -> {ok, Rules} = file:consult(Path), {200, #{type => file, enable => true, - rules => [ iolist_to_binary(io_lib:format("~p", [R])) || R <- Rules], + rules => [ iolist_to_binary(io_lib:format("~p.", [R])) || R <- Rules], annotations => #{status => healthy} } }; @@ -395,7 +395,7 @@ source(get, #{bindings := #{type := Type}}) -> end, {200, read_cert(NSource2)} end; -source(put, #{bindings := #{type := file}, body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable}}) -> +source(put, #{bindings := #{type := <<"file">>}, body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable}}) -> {ok, Filename} = write_file(maps:get(path, emqx_authz:lookup(file), ""), erlang:list_to_bitstring([<> || Rule <- Rules]) ), From 718dd80b481e69ffb1638daf66f2051a41d67213 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 9 Sep 2021 14:19:37 +0800 Subject: [PATCH 091/109] fix(auth mnesia api): fix write file error --- apps/emqx_authz/src/emqx_authz_api_sources.erl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz_api_sources.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl index 00d0a5b7a..209bbc01f 100644 --- a/apps/emqx_authz/src/emqx_authz_api_sources.erl +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -298,15 +298,15 @@ move_source_api() -> {"/authorization/sources/:type/move", Metadata, move_source}. sources(get, _) -> - Sources = lists:foldl(fun (#{enable := false} = Source, AccIn) -> - lists:append(AccIn, [Source#{annotations => #{status => unhealthy}}]); - (#{type := file, path := Path}, AccIn) -> + Sources = lists:foldl(fun (#{type := file, enable := Enable, path := Path}, AccIn) -> {ok, Rules} = file:consult(Path), lists:append(AccIn, [#{type => file, - enable => true, + enable => Enable, rules => [ iolist_to_binary(io_lib:format("~p.", [R])) || R <- Rules], annotations => #{status => healthy} }]); + (#{enable := false} = Source, AccIn) -> + lists:append(AccIn, [Source#{annotations => #{status => unhealthy}}]); (#{type := _Type, annotations := #{id := Id}} = Source, AccIn) -> NSource0 = case maps:get(server, Source, undefined) of undefined -> Source; @@ -367,15 +367,15 @@ sources(put, #{body := Body}) when is_list(Body) -> source(get, #{bindings := #{type := Type}}) -> case emqx_authz:lookup(Type) of {error, Reason} -> {404, #{messgae => atom_to_binary(Reason)}}; - #{enable := false} = Source -> {200, Source#{annotations => #{status => unhealthy}}}; - #{type := file, path := Path}-> + #{type := file, enable := Enable, path := Path}-> {ok, Rules} = file:consult(Path), {200, #{type => file, - enable => true, + enable => Enable, rules => [ iolist_to_binary(io_lib:format("~p.", [R])) || R <- Rules], annotations => #{status => healthy} } }; + #{enable := false} = Source -> {200, Source#{annotations => #{status => unhealthy}}}; #{annotations := #{id := Id}} = Source -> NSource0 = case maps:get(server, Source, undefined) of undefined -> Source; @@ -486,7 +486,7 @@ write_cert(Source) -> Source. write_file(Filename, Bytes) -> ok = filelib:ensure_dir(Filename), case file:write_file(Filename, Bytes) of - ok -> {ok, Filename}; + ok -> {ok, iolist_to_binary(Filename)}; {error, Reason} -> ?LOG(error, "Write File ~p Error: ~p", [Filename, Reason]), error(Reason) From 60914697b2be6983e9b6b83abb58fc661ac56190 Mon Sep 17 00:00:00 2001 From: lafirest Date: Wed, 8 Sep 2021 17:59:53 +0800 Subject: [PATCH 092/109] refactor(emqx_lwm2m): refactor lwm2m api use new rest framework --- apps/emqx_gateway/src/coap/emqx_coap_api.erl | 8 +- .../emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl | 232 ++++++------- .../src/lwm2m/emqx_lwm2m_channel.erl | 10 +- .../emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl | 4 +- .../src/lwm2m/emqx_lwm2m_session.erl | 56 +++- .../emqx_gateway/test/emqx_coap_api_SUITE.erl | 24 -- .../test/emqx_lwm2m_api_SUITE.erl | 317 ++++++++++++++++++ 7 files changed, 485 insertions(+), 166 deletions(-) create mode 100644 apps/emqx_gateway/test/emqx_lwm2m_api_SUITE.erl diff --git a/apps/emqx_gateway/src/coap/emqx_coap_api.erl b/apps/emqx_gateway/src/coap/emqx_coap_api.erl index 428e99ac5..4d0e8aff8 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_api.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_api.erl @@ -64,9 +64,9 @@ request(post, #{body := Body, bindings := Bindings}) -> case call_client(ClientId, Msg2, timer:seconds(WaitTime)) of timeout -> - {504}; + {504, #{code => 'CLIENT_NOT_RESPONSE'}}; not_found -> - {404}; + {404, #{code => 'CLIENT_NOT_FOUND'}}; Response -> {200, format_to_response(CT, Response)} end. @@ -101,8 +101,8 @@ request_method_meta() -> <<"request payload, binary must encode by base64">>), responses => #{ <<"200">> => object_schema(coap_message_properties()), - <<"404">> => schema(<<"NotFound">>), - <<"504">> => schema(<<"Timeout">>) + <<"404">> => error_schema("client not found error", ['CLIENT_NOT_FOUND']), + <<"504">> => error_schema("timeout", ['CLIENT_NOT_RESPONSE']) }}. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl index 03c3a6bc2..98c9fabe8 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_api.erl @@ -16,143 +16,119 @@ -module(emqx_lwm2m_api). --rest_api(#{name => list, - method => 'GET', - path => "/lwm2m_channels/", - func => list, - descr => "A list of all lwm2m channel" - }). +-behaviour(minirest_api). --rest_api(#{name => list, - method => 'GET', - path => "/nodes/:atom:node/lwm2m_channels/", - func => list, - descr => "A list of lwm2m channel of a node" - }). +-export([api_spec/0]). --rest_api(#{name => lookup_cmd, - method => 'GET', - path => "/lookup_cmd/:bin:ep/", - func => lookup_cmd, - descr => "Send a lwm2m downlink command" - }). +-export([lookup_cmd/2]). --rest_api(#{name => lookup_cmd, - method => 'GET', - path => "/nodes/:atom:node/lookup_cmd/:bin:ep/", - func => lookup_cmd, - descr => "Send a lwm2m downlink command of a node" - }). +-define(PREFIX, "/gateway/lwm2m/:clientid"). --export([ list/2 - , lookup_cmd/2 - ]). +-import(emqx_mgmt_util, [ object_schema/1 + , error_schema/2 + , properties/1]). -list(#{node := Node }, Params) -> - case Node = node() of - true -> list(#{}, Params); - _ -> rpc_call(Node, list, [#{}, Params]) - end; +api_spec() -> + {[lookup_cmd_api()], []}. -list(#{}, _Params) -> - %% Channels = emqx_lwm2m_cm:all_channels(), - Channels = [], - return({ok, format(Channels)}). +lookup_cmd_paramters() -> + [ make_paramter(clientid, path, true, "string") + , make_paramter(path, query, true, "string") + , make_paramter(action, query, true, "string")]. -lookup_cmd(#{ep := Ep, node := Node}, Params) -> - case Node = node() of - true -> lookup_cmd(#{ep => Ep}, Params); - _ -> rpc_call(Node, lookup_cmd, [#{ep => Ep}, Params]) - end; +lookup_cmd_properties() -> + properties([ {clientid, string} + , {path, string} + , {action, string} + , {code, string} + , {codeMsg, string} + , {content, {array, object}, lookup_cmd_content_props()}]). -lookup_cmd(#{ep := _Ep}, Params) -> - _MsgType = proplists:get_value(<<"msgType">>, Params), - _Path0 = proplists:get_value(<<"path">>, Params), - %% case emqx_lwm2m_cm:lookup_cmd(Ep, Path0, MsgType) of - %% [] -> return({ok, []}); - %% [{_, undefined} | _] -> return({ok, []}); - %% [{{IMEI, Path, MsgType}, undefined}] -> - %% return({ok, [{imei, IMEI}, - %% {'msgType', IMEI}, - %% {'code', <<"6.01">>}, - %% {'codeMsg', <<"reply_not_received">>}, - %% {'path', Path}]}); - %% [{{IMEI, Path, MsgType}, {Code, CodeMsg, Content}}] -> - %% Payload1 = format_cmd_content(Content, MsgType), - %% return({ok, [{imei, IMEI}, - %% {'msgType', IMEI}, - %% {'code', Code}, - %% {'codeMsg', CodeMsg}, - %% {'path', Path}] ++ Payload1}) - %% end. - return({ok, []}). +lookup_cmd_content_props() -> + [ {operations, string, <<"Resource Operations">>} + , {dataType, string, <<"Resource Type">>} + , {path, string, <<"Resource Path">>} + , {name, string, <<"Resource Name">>}]. -rpc_call(Node, Fun, Args) -> - case rpc:call(Node, ?MODULE, Fun, Args) of - {badrpc, Reason} -> {error, Reason}; - Res -> Res +lookup_cmd_api() -> + Metadata = #{get => + #{description => <<"look up resource">>, + parameters => lookup_cmd_paramters(), + responses => + #{<<"200">> => object_schema(lookup_cmd_properties()), + <<"404">> => error_schema("client not found error", ['CLIENT_NOT_FOUND']) + } + }}, + {?PREFIX ++ "/lookup_cmd", Metadata, lookup_cmd}. + + +lookup_cmd(get, #{bindings := Bindings, query_string := QS}) -> + ClientId = maps:get(clientid, Bindings), + case emqx_gateway_cm_registry:lookup_channels(lwm2m, ClientId) of + [Channel | _] -> + #{<<"path">> := Path, + <<"action">> := Action} = QS, + {ok, Result} = emqx_lwm2m_channel:lookup_cmd(Channel, Path, Action), + lookup_cmd_return(Result, ClientId, Action, Path); + _ -> + {404, #{code => 'CLIENT_NOT_FOUND'}} end. -format(Channels) -> - lists:map(fun({IMEI, #{lifetime := LifeTime, - peername := Peername, - version := Version, - reg_info := RegInfo}}) -> - ObjectList = lists:map(fun(Path) -> - [ObjId | _] = path_list(Path), - case emqx_lwm2m_xml_object:get_obj_def(binary_to_integer(ObjId), true) of - {error, _} -> - {Path, Path}; - ObjDefinition -> - ObjectName = emqx_lwm2m_xml_object:get_object_name(ObjDefinition), - {Path, list_to_binary(ObjectName)} - end - end, maps:get(<<"objectList">>, RegInfo)), - {IpAddr, Port} = Peername, - [{imei, IMEI}, - {lifetime, LifeTime}, - {ip_address, iolist_to_binary(ntoa(IpAddr))}, - {port, Port}, - {version, Version}, - {'objectList', ObjectList}] - end, Channels). +lookup_cmd_return(undefined, ClientId, Action, Path) -> + {200, + #{clientid => ClientId, + action => Action, + code => <<"6.01">>, + codeMsg => <<"reply_not_received">>, + path => Path}}; -%% format_cmd_content(undefined, _MsgType) -> []; -%% format_cmd_content(_Content, <<"discover">>) -> -%% %% [H | Content1] = Content, -%% %% {_, [HObjId]} = emqx_lwm2m_coap_resource:parse_object_list(H), -%% %% [ObjId | _]= path_list(HObjId), -%% %% ObjectList = case Content1 of -%% %% [Content2 | _] -> -%% %% {_, ObjL} = emqx_lwm2m_coap_resource:parse_object_list(Content2), -%% %% ObjL; -%% %% [] -> [] -%% %% end, -%% %% R = case emqx_lwm2m_xml_object:get_obj_def(binary_to_integer(ObjId), true) of -%% %% {error, _} -> -%% %% lists:map(fun(Object) -> {Object, Object} end, ObjectList); -%% %% ObjDefinition -> -%% %% lists:map(fun(Object) -> -%% %% [_, _, ResId| _] = path_list(Object), -%% %% Operations = case emqx_lwm2m_xml_object:get_resource_operations(binary_to_integer(ResId), ObjDefinition) of -%% %% "E" -> [{operations, list_to_binary("E")}]; -%% %% Oper -> [{'dataType', list_to_binary(emqx_lwm2m_xml_object:get_resource_type(binary_to_integer(ResId), ObjDefinition))}, -%% %% {operations, list_to_binary(Oper)}] -%% %% end, -%% %% [{path, Object}, -%% %% {name, list_to_binary(emqx_lwm2m_xml_object:get_resource_name(binary_to_integer(ResId), ObjDefinition))} -%% %% ] ++ Operations -%% %% end, ObjectList) -%% %% end, -%% %% [{content, R}]; -%% []; -%% format_cmd_content(Content, _) -> -%% [{content, Content}]. +lookup_cmd_return({Code, CodeMsg, Content}, ClientId, Action, Path) -> + {200, + format_cmd_content(Content, + Action, + #{clientid => ClientId, + action => Action, + code => Code, + codeMsg => CodeMsg, + path => Path})}. -ntoa({0,0,0,0,0,16#ffff,AB,CD}) -> - inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256}); -ntoa(IP) -> - inet_parse:ntoa(IP). +format_cmd_content(undefined, _MsgType, Result) -> + Result; + +format_cmd_content(Content, <<"discover">>, Result) -> + [H | Content1] = Content, + {_, [HObjId]} = emqx_lwm2m_session:parse_object_list(H), + [ObjId | _]= path_list(HObjId), + ObjectList = case Content1 of + [Content2 | _] -> + {_, ObjL} = emqx_lwm2m_session:parse_object_list(Content2), + ObjL; + [] -> [] + end, + + R = case emqx_lwm2m_xml_object:get_obj_def(binary_to_integer(ObjId), true) of + {error, _} -> + lists:map(fun(Object) -> #{Object => Object} end, ObjectList); + ObjDefinition -> + lists:map( + fun(Object) -> + [_, _, RawResId| _] = path_list(Object), + ResId = binary_to_integer(RawResId), + Operations = case emqx_lwm2m_xml_object:get_resource_operations(ResId, ObjDefinition) of + "E" -> + #{operations => list_to_binary("E")}; + Oper -> + #{'dataType' => list_to_binary(emqx_lwm2m_xml_object:get_resource_type(ResId, ObjDefinition)), + operations => list_to_binary(Oper)} + end, + Operations#{path => Object, + name => list_to_binary(emqx_lwm2m_xml_object:get_resource_name(ResId, ObjDefinition))} + end, ObjectList) + end, + Result#{content => R}; + +format_cmd_content(Content, _, Result) -> + Result#{content => Content}. path_list(Path) -> case binary:split(binary_util:trim(Path, $/), [<<$/>>], [global]) of @@ -162,6 +138,8 @@ path_list(Path) -> [ObjId] -> [ObjId] end. -return(_) -> -%% TODO: V5 API - ok. +make_paramter(Name, In, IsRequired, Type) -> + #{name => Name, + in => In, + required => IsRequired, + schema => #{type => Type}}. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl index d0647897b..6ad78742f 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_channel.erl @@ -25,7 +25,8 @@ , info/2 , stats/1 , with_context/2 - , do_takeover/3]). + , do_takeover/3 + , lookup_cmd/3]). -export([ init/2 , handle_in/2 @@ -116,6 +117,9 @@ with_context(Ctx, ClientInfo) -> with_context(Type, Topic, Ctx, ClientInfo) end. +lookup_cmd(Channel, Path, Action) -> + gen_server:call(Channel, {?FUNCTION_NAME, Path, Action}). + %%-------------------------------------------------------------------- %% Handle incoming packet %%-------------------------------------------------------------------- @@ -150,6 +154,10 @@ handle_timeout(_, _, Channel) -> %%-------------------------------------------------------------------- %% Handle call %%-------------------------------------------------------------------- +handle_call({lookup_cmd, Path, Type}, _From, #channel{session = Session} = Channel) -> + Result = emqx_lwm2m_session:find_cmd_record(Path, Type, Session), + {reply, {ok, Result}, Channel}; + handle_call(Req, _From, Channel) -> ?LOG(error, "Unexpected call: ~p", [Req]), {reply, ignored, Channel}. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl index 7c0cc95cd..e17a83195 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_cmd.erl @@ -341,8 +341,8 @@ extract_path(Ref = #{}) -> drop_query( case Ref of #{<<"data">> := Data} -> - case maps:get(<<"path">>, Data, nil) of - nil -> maps:get(<<"basePath">>, Data, undefined); + case maps:get(<<"path">>, Data, undefined) of + undefined -> maps:get(<<"basePath">>, Data, undefined); Path -> Path end; #{<<"path">> := Path} -> diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl index a1d03e04f..ab27dfbca 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_session.erl @@ -22,7 +22,8 @@ -include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl"). %% API --export([new/0, init/4, update/3, reregister/3, on_close/1]). +-export([ new/0, init/4, update/3, parse_object_list/1 + , reregister/3, on_close/1, find_cmd_record/3]). -export([ info/1 , info/2 @@ -42,6 +43,15 @@ -type timestamp() :: non_neg_integer(). -type queued_request() :: {timestamp(), request_context(), emqx_coap_message()}. +-type cmd_path() :: binary(). +-type cmd_type() :: binary(). +-type cmd_record_key() :: {cmd_path(), cmd_type()}. +-type cmd_code() :: binary(). +-type cmd_code_msg() :: binary(). +-type cmd_code_content() :: list(map()). +-type cmd_result() :: undefined | {cmd_code(), cmd_code_msg(), cmd_code_content()}. +-type cmd_record() :: #{cmd_record_key() => cmd_result()}. + -record(session, { coap :: emqx_coap_tm:manager() , queue :: queue:queue(queued_request()) , wait_ack :: request_context() | undefined @@ -52,6 +62,7 @@ , is_cache_mode :: boolean() , mountpoint :: binary() , last_active_at :: non_neg_integer() + , cmd_record :: cmd_record() }). -type session() :: #session{}. @@ -61,6 +72,8 @@ -define(IGNORE_OBJECT, [<<"0">>, <<"1">>, <<"2">>, <<"4">>, <<"5">>, <<"6">>, <<"7">>, <<"9">>, <<"15">>]). +-define(CMD_KEY(Path, Type), {Path, Type}). + %% uplink and downlink topic configuration -define(lwm2m_up_dm_topic, {<<"/v1/up/dm">>, 0}). @@ -98,6 +111,7 @@ new() -> , last_active_at = ?NOW , is_cache_mode = false , mountpoint = <<>> + , cmd_record = #{} , lifetime = emqx:get_config([gateway, lwm2m, lifetime_max])}. -spec init(emqx_coap_message(), binary(), function(), session()) -> map(). @@ -135,6 +149,10 @@ on_close(Session) -> emqx:unsubscribe(MountedTopic), MountedTopic. +-spec find_cmd_record(cmd_path(), cmd_type(), session()) -> cmd_result(). +find_cmd_record(Path, Type, #session{cmd_record = Record}) -> + maps:get(?CMD_KEY(Path, Type), Record, undefined). + %%-------------------------------------------------------------------- %% Info, Stats %%-------------------------------------------------------------------- @@ -271,7 +289,7 @@ parse_object_list(FullObjLinkList) -> (<>) when Prefix =:= AlterPath -> trim(Link); (Link) -> Link - end, ObjLinkList), + end, ObjLinkList), {AlterPath, WithOutPrefix} end. @@ -443,19 +461,20 @@ handle_coap_response({Ctx = #{<<"msgType">> := EventType}, Session) -> MqttPayload = emqx_lwm2m_cmd:coap_to_mqtt(CoapMsgMethod, CoapMsgPayload, CoapMsgOpts, Ctx), {ReqPath, _} = emqx_lwm2m_cmd:path_list(emqx_lwm2m_cmd:extract_path(Ctx)), - Session2 = + Session2 = record_response(EventType, MqttPayload, Session), + Session3 = case {ReqPath, MqttPayload, EventType, CoapMsgType} of {[<<"5">>| _], _, <<"observe">>, CoapMsgType} when CoapMsgType =/= ack -> %% this is a notification for status update during NB firmware upgrade. %% need to reply to DM http callbacks - send_to_mqtt(Ctx, <<"notify">>, MqttPayload, ?lwm2m_up_dm_topic, WithContext, Session); + send_to_mqtt(Ctx, <<"notify">>, MqttPayload, ?lwm2m_up_dm_topic, WithContext, Session2); {_ReqPath, _, <<"observe">>, CoapMsgType} when CoapMsgType =/= ack -> %% this is actually a notification, correct the msgType - send_to_mqtt(Ctx, <<"notify">>, MqttPayload, WithContext, Session); + send_to_mqtt(Ctx, <<"notify">>, MqttPayload, WithContext, Session2); _ -> - send_to_mqtt(Ctx, EventType, MqttPayload, WithContext, Session) + send_to_mqtt(Ctx, EventType, MqttPayload, WithContext, Session2) end, - send_dl_msg(Ctx, Session2). + send_dl_msg(Ctx, Session3). %%-------------------------------------------------------------------- %% Ack @@ -624,7 +643,8 @@ deliver_to_coap(AlternatePath, TermData, MQTT, CacheMode, WithContext, Session) WithContext(metrics, 'messages.delivered'), {Req, Ctx} = emqx_lwm2m_cmd:mqtt_to_coap(AlternatePath, TermData), ExpiryTime = get_expiry_time(MQTT), - maybe_do_deliver_to_coap(Ctx, Req, ExpiryTime, CacheMode, Session). + Session2 = record_request(Ctx, Session), + maybe_do_deliver_to_coap(Ctx, Req, ExpiryTime, CacheMode, Session2). maybe_do_deliver_to_coap(Ctx, Req, ExpiryTime, CacheMode, #session{wait_ack = WaitAck, @@ -692,3 +712,23 @@ do_out([{Ctx, Out} | T], TM, Msgs) -> do_out(_, TM, Msgs) -> {ok, TM, Msgs}. + + +%%-------------------------------------------------------------------- +%% CMD Record +%%-------------------------------------------------------------------- +-spec record_request(request_context(), session()) -> session(). +record_request(#{<<"msgType">> := Type} = Context, Session) -> + Path = emqx_lwm2m_cmd:extract_path(Context), + record_cmd(Path, Type, undefined, Session). + +record_response(EventType, #{<<"data">> := Data}, Session) -> + ReqPath = maps:get(<<"reqPath">>, Data, undefined), + Code = maps:get(<<"code">>, Data, undefined), + CodeMsg = maps:get(<<"codeMsg">>, Data, undefined), + Content = maps:get(<<"content">>, Data, undefined), + record_cmd(ReqPath, EventType, {Code, CodeMsg, Content}, Session). + +record_cmd(Path, Type, Result, #session{cmd_record = Record} = Session) -> + Record2 = Record#{?CMD_KEY(Path, Type) => Result}, + Session#session{cmd_record = Record2}. diff --git a/apps/emqx_gateway/test/emqx_coap_api_SUITE.erl b/apps/emqx_gateway/test/emqx_coap_api_SUITE.erl index 74b0cadc8..83521f5cd 100644 --- a/apps/emqx_gateway/test/emqx_coap_api_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_coap_api_SUITE.erl @@ -62,12 +62,6 @@ end_per_suite(Config) -> emqx_mgmt_api_test_util:end_suite([emqx_gateway]), Config. -set_special_configs(emqx_gatewway) -> - ok = emqx_config:init_load(emqx_gateway_schema, ?CONF_DEFAULT); - -set_special_configs(_) -> - ok. - %%-------------------------------------------------------------------- %% Cases %%-------------------------------------------------------------------- @@ -187,17 +181,6 @@ split_segments(Path, Char, Acc) -> make_segment(Seg) -> list_to_binary(emqx_http_lib:uri_decode(Seg)). - -get_coap_path(Options) -> - get_path(Options, <<>>). - -get_coap_query(Options) -> - proplists:get_value(uri_query, Options, []). - -get_coap_observe(Options) -> - get_observe(Options). - - get_path([], Acc) -> %?LOGT("get_path Acc=~p", [Acc]), Acc; @@ -207,13 +190,6 @@ get_path([{uri_path, Path1}|T], Acc) -> get_path([{_, _}|T], Acc) -> get_path(T, Acc). -get_observe([]) -> - undefined; -get_observe([{observe, V}|_T]) -> - V; -get_observe([{_, _}|T]) -> - get_observe(T). - join_path([], Acc) -> Acc; join_path([<<"/">>|T], Acc) -> join_path(T, Acc); diff --git a/apps/emqx_gateway/test/emqx_lwm2m_api_SUITE.erl b/apps/emqx_gateway/test/emqx_lwm2m_api_SUITE.erl new file mode 100644 index 000000000..cb2ccf3f8 --- /dev/null +++ b/apps/emqx_gateway/test/emqx_lwm2m_api_SUITE.erl @@ -0,0 +1,317 @@ +%%-------------------------------------------------------------------- +%% Copyright (C) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_lwm2m_api_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-define(PORT, 5783). + +-define(LOGT(Format, Args), ct:pal("TEST_SUITE: " ++ Format, Args)). + +-include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl"). +-include_lib("lwm2m_coap/include/coap.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-define(CONF_DEFAULT, <<" +gateway.lwm2m { + xml_dir = \"../../lib/emqx_gateway/src/lwm2m/lwm2m_xml\" + lifetime_min = 1s + lifetime_max = 86400s + qmode_time_windonw = 22 + auto_observe = false + mountpoint = \"lwm2m/%u\" + update_msg_publish_condition = contains_object_list + translators { + command = {topic = \"/dn/#\", qos = 0} + response = {topic = \"/up/resp\", qos = 0} + notify = {topic = \"/up/notify\", qos = 0} + register = {topic = \"/up/resp\", qos = 0} + update = {topic = \"/up/resp\", qos = 0} + } + listeners.udp.default { + bind = 5783 + } +} +">>). + +-define(assertExists(Map, Key), + ?assertNotEqual(maps:get(Key, Map, undefined), undefined)). + +%%-------------------------------------------------------------------- +%% Setups +%%-------------------------------------------------------------------- + +all() -> + emqx_ct:all(?MODULE). + +init_per_suite(Config) -> + ok = emqx_config:init_load(emqx_gateway_schema, ?CONF_DEFAULT), + emqx_mgmt_api_test_util:init_suite([emqx_gateway]), + Config. + +end_per_suite(Config) -> + timer:sleep(300), + emqx_mgmt_api_test_util:end_suite([emqx_gateway]), + Config. + +init_per_testcase(_AllTestCase, Config) -> + ok = emqx_config:init_load(emqx_gateway_schema, ?CONF_DEFAULT), + {ok, _} = application:ensure_all_started(emqx_gateway), + {ok, ClientUdpSock} = gen_udp:open(0, [binary, {active, false}]), + + {ok, C} = emqtt:start_link([{host, "localhost"},{port, 1883},{clientid, <<"c1">>}]), + {ok, _} = emqtt:connect(C), + timer:sleep(100), + + [{sock, ClientUdpSock}, {emqx_c, C} | Config]. + +end_per_testcase(_AllTestCase, Config) -> + timer:sleep(300), + gen_udp:close(?config(sock, Config)), + emqtt:disconnect(?config(emqx_c, Config)), + ok = application:stop(emqx_gateway). + +%%-------------------------------------------------------------------- +%% Cases +%%-------------------------------------------------------------------- +t_lookup_cmd_read(Config) -> + UdpSock = ?config(sock, Config), + Epn = "urn:oma:lwm2m:oma:3", + MsgId1 = 15, + RespTopic = list_to_binary("lwm2m/"++Epn++"/up/resp"), + emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), + timer:sleep(200), + %% step 1, device register ... + test_send_coap_request( UdpSock, + post, + sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1", [?PORT, Epn]), + #coap_content{content_format = <<"text/plain">>, + payload = <<";rt=\"oma.lwm2m\";ct=11543,,,">>}, + [], + MsgId1), + #coap_message{method = Method1} = test_recv_coap_response(UdpSock), + ?assertEqual({ok,created}, Method1), + test_recv_mqtt_response(RespTopic), + + %% step2, send a READ command to device + CmdId = 206, + CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, + Command = #{ + <<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"read">>, + <<"data">> => #{ + <<"path">> => <<"/3/0/0">> + } + }, + CommandJson = emqx_json:encode(Command), + ?LOGT("CommandJson=~p", [CommandJson]), + test_mqtt_broker:publish(CommandTopic, CommandJson, 0), + timer:sleep(50), + + no_received_request(Epn, <<"/3/0/0">>, <<"read">>), + + Request2 = test_recv_coap_request(UdpSock), + ?LOGT("LwM2M client got ~p", [Request2]), + timer:sleep(50), + + test_send_coap_response(UdpSock, "127.0.0.1", ?PORT, {ok, content}, #coap_content{content_format = <<"text/plain">>, payload = <<"EMQ">>}, Request2, true), + timer:sleep(100), + + normal_received_request(Epn, <<"/3/0/0">>, <<"read">>). + +t_lookup_cmd_discover(Config) -> + %% step 1, device register ... + Epn = "urn:oma:lwm2m:oma:3", + MsgId1 = 15, + UdpSock = ?config(sock, Config), + ObjectList = <<", , , , ">>, + RespTopic = list_to_binary("lwm2m/"++Epn++"/up/resp"), + emqtt:subscribe(?config(emqx_c, Config), RespTopic, qos0), + timer:sleep(200), + + std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic), + + %% step2, send a WRITE command to device + CommandTopic = <<"lwm2m/", (list_to_binary(Epn))/binary, "/dn/dm">>, + CmdId = 307, + Command = #{<<"requestID">> => CmdId, <<"cacheID">> => CmdId, + <<"msgType">> => <<"discover">>, + <<"data">> => #{ + <<"path">> => <<"/3/0/7">> + } }, + CommandJson = emqx_json:encode(Command), + test_mqtt_broker:publish(CommandTopic, CommandJson, 0), + + no_received_request(Epn, <<"/3/0/7">>, <<"discover">>), + + timer:sleep(50), + Request2 = test_recv_coap_request(UdpSock), + timer:sleep(50), + + PayloadDiscover = <<";dim=8;pmin=10;pmax=60;gt=50;lt=42.2,">>, + test_send_coap_response(UdpSock, + "127.0.0.1", + ?PORT, + {ok, content}, + #coap_content{content_format = <<"application/link-format">>, payload = PayloadDiscover}, + Request2, + true), + timer:sleep(100), + discover_received_request(Epn, <<"/3/0/7">>, <<"discover">>). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Internal Functions +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +send_request(ClientId, Path, Action) -> + ApiPath = emqx_mgmt_api_test_util:api_path(["gateway/lwm2m", ClientId, "lookup_cmd"]), + Auth = emqx_mgmt_api_test_util:auth_header_(), + Query = io_lib:format("path=~s&action=~s", [Path, Action]), + {ok, Response} = emqx_mgmt_api_test_util:request_api(get, ApiPath, Query, Auth), + ?LOGT("rest api response:~s~n", [Response]), + Response. + +no_received_request(ClientId, Path, Action) -> + Response = send_request(ClientId, Path, Action), + NotReceived = #{<<"clientid">> => list_to_binary(ClientId), + <<"action">> => Action, + <<"code">> => <<"6.01">>, + <<"codeMsg">> => <<"reply_not_received">>, + <<"path">> => Path}, + ?assertEqual(NotReceived, emqx_json:decode(Response, [return_maps])). +normal_received_request(ClientId, Path, Action) -> + Response = send_request(ClientId, Path, Action), + RCont = emqx_json:decode(Response, [return_maps]), + ?assertEqual(list_to_binary(ClientId), maps:get(<<"clientid">>, RCont, undefined)), + ?assertEqual(Path, maps:get(<<"path">>, RCont, undefined)), + ?assertEqual(Action, maps:get(<<"action">>, RCont, undefined)), + ?assertExists(RCont, <<"code">>), + ?assertExists(RCont, <<"codeMsg">>), + ?assertExists(RCont, <<"content">>), + RCont. + +discover_received_request(ClientId, Path, Action) -> + RCont = normal_received_request(ClientId, Path, Action), + [Res | _] = maps:get(<<"content">>, RCont), + ?assertExists(Res, <<"path">>), + ?assertExists(Res, <<"name">>), + ?assertExists(Res, <<"operations">>). + +test_recv_mqtt_response(RespTopic) -> + receive + {publish, #{topic := RespTopic, payload := RM}} -> + ?LOGT("test_recv_mqtt_response Response=~p", [RM]), + RM + after 1000 -> timeout_test_recv_mqtt_response + end. + +test_send_coap_request(UdpSock, Method, Uri, Content, Options, MsgId) -> + is_record(Content, coap_content) orelse error("Content must be a #coap_content!"), + is_list(Options) orelse error("Options must be a list"), + case resolve_uri(Uri) of + {coap, {IpAddr, Port}, Path, Query} -> + Request0 = lwm2m_coap_message:request(con, Method, Content, [{uri_path, Path}, {uri_query, Query} | Options]), + Request = Request0#coap_message{id = MsgId}, + ?LOGT("send_coap_request Request=~p", [Request]), + RequestBinary = lwm2m_coap_message_parser:encode(Request), + ?LOGT("test udp socket send to ~p:~p, data=~p", [IpAddr, Port, RequestBinary]), + ok = gen_udp:send(UdpSock, IpAddr, Port, RequestBinary); + {SchemeDiff, ChIdDiff, _, _} -> + error(lists:flatten(io_lib:format("scheme ~s or ChId ~s does not match with socket", [SchemeDiff, ChIdDiff]))) + end. + +test_recv_coap_response(UdpSock) -> + {ok, {Address, Port, Packet}} = gen_udp:recv(UdpSock, 0, 2000), + Response = lwm2m_coap_message_parser:decode(Packet), + ?LOGT("test udp receive from ~p:~p, data1=~p, Response=~p", [Address, Port, Packet, Response]), + #coap_message{type = ack, method = Method, id=Id, token = Token, options = Options, payload = Payload} = Response, + ?LOGT("receive coap response Method=~p, Id=~p, Token=~p, Options=~p, Payload=~p", [Method, Id, Token, Options, Payload]), + Response. + +test_recv_coap_request(UdpSock) -> + case gen_udp:recv(UdpSock, 0, 2000) of + {ok, {_Address, _Port, Packet}} -> + Request = lwm2m_coap_message_parser:decode(Packet), + #coap_message{type = con, method = Method, id=Id, token = Token, payload = Payload, options = Options} = Request, + ?LOGT("receive coap request Method=~p, Id=~p, Token=~p, Options=~p, Payload=~p", [Method, Id, Token, Options, Payload]), + Request; + {error, Reason} -> + ?LOGT("test_recv_coap_request failed, Reason=~p", [Reason]), + timeout_test_recv_coap_request + end. + +test_send_coap_response(UdpSock, Host, Port, Code, Content, Request, Ack) -> + is_record(Content, coap_content) orelse error("Content must be a #coap_content!"), + is_list(Host) orelse error("Host is not a string"), + + {ok, IpAddr} = inet:getaddr(Host, inet), + Response = lwm2m_coap_message:response(Code, Content, Request), + Response2 = case Ack of + true -> Response#coap_message{type = ack}; + false -> Response + end, + ?LOGT("test_send_coap_response Response=~p", [Response2]), + ok = gen_udp:send(UdpSock, IpAddr, Port, lwm2m_coap_message_parser:encode(Response2)). + +std_register(UdpSock, Epn, ObjectList, MsgId1, RespTopic) -> + test_send_coap_request( UdpSock, + post, + sprintf("coap://127.0.0.1:~b/rd?ep=~s<=345&lwm2m=1", [?PORT, Epn]), + #coap_content{content_format = <<"text/plain">>, payload = ObjectList}, + [], + MsgId1), + #coap_message{method = {ok,created}} = test_recv_coap_response(UdpSock), + test_recv_mqtt_response(RespTopic), + timer:sleep(100). + +resolve_uri(Uri) -> + {ok, #{scheme := Scheme, + host := Host, + port := PortNo, + path := Path} = URIMap} = emqx_http_lib:uri_parse(Uri), + Query = maps:get(query, URIMap, ""), + {ok, PeerIP} = inet:getaddr(Host, inet), + {Scheme, {PeerIP, PortNo}, split_path(Path), split_query(Query)}. + +split_path([]) -> []; +split_path([$/]) -> []; +split_path([$/ | Path]) -> split_segments(Path, $/, []). + +split_query([]) -> []; +split_query(Path) -> split_segments(Path, $&, []). + +split_segments(Path, Char, Acc) -> + case string:rchr(Path, Char) of + 0 -> + [make_segment(Path) | Acc]; + N when N > 0 -> + split_segments(string:substr(Path, 1, N-1), Char, + [make_segment(string:substr(Path, N+1)) | Acc]) + end. + +make_segment(Seg) -> + list_to_binary(emqx_http_lib:uri_decode(Seg)). + +join_path([], Acc) -> Acc; +join_path([<<"/">>|T], Acc) -> + join_path(T, Acc); +join_path([H|T], Acc) -> + join_path(T, <>). + +sprintf(Format, Args) -> + lists:flatten(io_lib:format(Format, Args)). From e94e09075c2069f1cea6dea6cdf8afa808be123b Mon Sep 17 00:00:00 2001 From: JianBo He Date: Tue, 7 Sep 2021 16:17:58 +0800 Subject: [PATCH 093/109] refactor(gw): keep listeners conf tree compliance with core --- apps/emqx_gateway/etc/emqx_gateway.conf | 50 +++++- apps/emqx_gateway/src/emqx_gateway_schema.erl | 166 +++++++++--------- apps/emqx_gateway/src/emqx_gateway_utils.erl | 37 +++- apps/emqx_gateway/test/emqx_exproto_SUITE.erl | 67 +++---- 4 files changed, 198 insertions(+), 122 deletions(-) diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index 5212d319f..01fb9f316 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -42,7 +42,32 @@ gateway.stomp { acceptors = 16 max_connections = 1024000 max_conn_rate = 1000 - active_n = 100 + + ## TCP options + ## See ${example_common_tcp_options} for more information + tcp.active_n = 100 + tcp.backlog = 1024 + tcp.buffer = 4KB + } + + listeners.ssl.default { + bind = 61614 + acceptors = 16 + max_connections = 1024000 + max_conn_rate = 1000 + + ## TCP options + ## See ${example_common_tcp_options} for more information + tcp.active_n = 100 + tcp.backlog = 1024 + tcp.buffer = 4KB + + ## SSL options + ## See ${example_common_ssl_options} for more information + ssl.versions = ["tlsv1.3", "tlsv1.2", "tlsv1.1", "tlsv1"] + ssl.keyfile = "{{ platform_etc_dir }}/certs/key.pem" + ssl.certfile = "{{ platform_etc_dir }}/certs/cert.pem" + ssl.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" } } @@ -68,6 +93,29 @@ gateway.coap { listeners.udp.default { bind = 5683 + acceptors = 4 + max_connections = 102400 + max_conn_rate = 1000 + + ## UDP Options + ## See ${example_common_udp_options} for more information + udp.active_n = 100 + udp.buffer = 16KB + } + listeners.dtls.default { + bind = 5684 + acceptors = 4 + max_connections = 102400 + max_conn_rate = 1000 + + ## UDP Options + ## See ${example_common_udp_options} for more information + udp.active_n = 100 + udp.buffer = 16KB + + ## DTLS Options + ## See #{example_common_dtls_options} for more information + dtls.versions = ["dtlsv1"] } } diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index c3e241389..8c3663358 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -63,9 +63,9 @@ fields(stomp_structs) -> ] ++ gateway_common_options(); fields(stomp_frame) -> - [ {max_headers, sc(integer(), undefined, 10)} - , {max_headers_length, sc(integer(), undefined, 1024)} - , {max_body_length, sc(integer(), undefined, 8192)} + [ {max_headers, sc(integer(), 10)} + , {max_headers_length, sc(integer(), 1024)} + , {max_body_length, sc(integer(), 8192)} ]; fields(mqttsn_structs) -> @@ -82,11 +82,11 @@ fields(mqttsn_predefined) -> ]; fields(coap_structs) -> - [ {heartbeat, sc(duration(), undefined, <<"30s">>)} - , {connection_required, sc(boolean(), undefined, false)} - , {notify_type, sc(union([non, con, qos]), undefined, qos)} - , {subscribe_qos, sc(union([qos0, qos1, qos2, coap]), undefined, coap)} - , {publish_qos, sc(union([qos0, qos1, qos2, coap]), undefined, coap)} + [ {heartbeat, sc(duration(), <<"30s">>)} + , {connection_required, sc(boolean(), false)} + , {notify_type, sc(union([non, con, qos]), qos)} + , {subscribe_qos, sc(union([qos0, qos1, qos2, coap]), coap)} + , {publish_qos, sc(union([qos0, qos1, qos2, coap]), coap)} , {listeners, sc(ref(udp_listener_group))} ] ++ gateway_common_options(); @@ -166,58 +166,53 @@ fields(udp_listener) -> fields(dtls_listener) -> [ {"$name", sc(ref(dtls_listener_settings))}]; -fields(listener_settings) -> - [ {enable, sc(boolean(), undefined, true)} - , {bind, sc(union(ip_port(), integer()))} - , {acceptors, sc(integer(), undefined, 8)} - , {max_connections, sc(integer(), undefined, 1024)} - , {max_conn_rate, sc(integer())} - , {active_n, sc(integer(), undefined, 100)} - %, {rate_limit, sc(comma_separated_list())} - , {access, sc(ref(access))} - , {proxy_protocol, sc(boolean())} - , {proxy_protocol_timeout, sc(duration())} - , {backlog, sc(integer(), undefined, 1024)} - , {send_timeout, sc(duration(), undefined, <<"15s">>)} - , {send_timeout_close, sc(boolean(), undefined, true)} - , {recbuf, sc(bytesize())} - , {sndbuf, sc(bytesize())} - , {buffer, sc(bytesize())} - , {high_watermark, sc(bytesize(), undefined, <<"1MB">>)} - , {tune_buffer, sc(boolean())} - , {nodelay, sc(boolean())} - , {reuseaddr, sc(boolean())} - ]; - fields(tcp_listener_settings) -> [ %% some special confs for tcp listener - ] ++ fields(listener_settings); + ] ++ tcp_opts() + ++ proxy_protocol_opts() + ++ common_listener_opts(); fields(ssl_listener_settings) -> [ %% some special confs for ssl listener - ] ++ - ssl(undefined, #{handshake_timeout => <<"15s">> - , depth => 10 - , reuse_sessions => true}) ++ fields(listener_settings); + ] ++ tcp_opts() + ++ ssl_opts() + ++ proxy_protocol_opts() + ++ common_listener_opts(); fields(udp_listener_settings) -> [ %% some special confs for udp listener - ] ++ fields(listener_settings); + ] ++ udp_opts() + ++ common_listener_opts(); fields(dtls_listener_settings) -> [ %% some special confs for dtls listener - ] ++ - ssl(undefined, #{handshake_timeout => <<"15s">> - , depth => 10 - , reuse_sessions => true}) ++ fields(listener_settings); + ] ++ udp_opts() + ++ dtls_opts() + ++ common_listener_opts(); -fields(access) -> - [ {"$id", #{type => binary(), - nullable => true}}]; +fields(udp_opts) -> + [ {active_n, sc(integer(), 100)} + , {recbuf, sc(bytesize())} + , {sndbuf, sc(bytesize())} + , {buffer, sc(bytesize())} + , {reuseaddr, sc(boolean(), true)} + ]; + +fields(dtls_listener_ssl_opts) -> + Base = emqx_schema:fields("listener_ssl_opts"), + %% XXX: ciphers ??? + DtlsVers = hoconsc:mk( + typerefl:alias("string", list(atom())), + #{ default => default_dtls_vsns(), + converter => fun (Vsns) -> + [dtls_vsn(iolist_to_binary(V)) || V <- Vsns] + end + }), + lists:keyreplace("versions", 1, Base, {"versions", DtlsVers}); fields(ExtraField) -> Mod = list_to_atom(ExtraField++"_schema"), @@ -244,14 +239,47 @@ fields(ExtraField) -> % ]). gateway_common_options() -> - [ {enable, sc(boolean(), undefined, true)} - , {enable_stats, sc(boolean(), undefined, true)} - , {idle_timeout, sc(duration(), undefined, <<"30s">>)} + [ {enable, sc(boolean(), true)} + , {enable_stats, sc(boolean(), true)} + , {idle_timeout, sc(duration(), <<"30s">>)} , {mountpoint, sc(binary())} , {clientinfo_override, sc(ref(clientinfo_override))} , {authentication, sc(hoconsc:lazy(map()))} ]. +common_listener_opts() -> + [ {enable, sc(boolean(), true)} + , {bind, sc(union(ip_port(), integer()))} + , {acceptors, sc(integer(), 16)} + , {max_connections, sc(integer(), 1024)} + , {max_conn_rate, sc(integer())} + %, {rate_limit, sc(comma_separated_list())} + , {access_rules, sc(hoconsc:array(string()), [])} + ]. + +tcp_opts() -> + [{tcp, sc(ref(emqx_schema, "tcp_opts"), #{})}]. + +udp_opts() -> + [{udp, sc(ref(udp_opts), #{})}]. + +ssl_opts() -> + [{ssl, sc(ref(emqx_schema, "listener_ssl_opts"), #{})}]. + +dtls_opts() -> + [{dtls, sc(ref(dtls_listener_ssl_opts), #{})}]. + +proxy_protocol_opts() -> + [ {proxy_protocol, sc(boolean())} + , {proxy_protocol_timeout, sc(duration())} + ]. + +default_dtls_vsns() -> + [<<"dtlsv1.2">>, <<"dtlsv1">>]. + +dtls_vsn(<<"dtlsv1.2">>) -> 'dtlsv1.2'; +dtls_vsn(<<"dtlsv1">>) -> 'dtlsv1'. + %%-------------------------------------------------------------------- %% Helpers @@ -259,47 +287,11 @@ gateway_common_options() -> sc(Type) -> #{type => Type}. -sc(Type, Mapping, Default) -> - hoconsc:mk(Type, #{mapping => Mapping, default => Default}). +sc(Type, Default) -> + hoconsc:mk(Type, #{default => Default}). ref(Field) -> hoconsc:ref(?MODULE, Field). -%% utils - -%% generate a ssl field. -%% ssl("emqx", #{"verify" => verify_peer}) will return -%% [ {"cacertfile", sc(string(), "emqx.cacertfile", undefined)} -%% , {"certfile", sc(string(), "emqx.certfile", undefined)} -%% , {"keyfile", sc(string(), "emqx.keyfile", undefined)} -%% , {"verify", sc(union(verify_peer, verify_none), "emqx.verify", verify_peer)} -%% , {"server_name_indication", "emqx.server_name_indication", undefined)} -%% ... -ssl(Mapping, Defaults) -> - M = fun (Field) -> - case (Mapping) of - undefined -> undefined; - _ -> Mapping ++ "." ++ Field - end end, - D = fun (Field) -> maps:get(list_to_atom(Field), Defaults, undefined) end, - [ {"enable", sc(boolean(), M("enable"), D("enable"))} - , {"cacertfile", sc(binary(), M("cacertfile"), D("cacertfile"))} - , {"certfile", sc(binary(), M("certfile"), D("certfile"))} - , {"keyfile", sc(binary(), M("keyfile"), D("keyfile"))} - , {"verify", sc(union(verify_peer, verify_none), M("verify"), D("verify"))} - , {"fail_if_no_peer_cert", sc(boolean(), M("fail_if_no_peer_cert"), D("fail_if_no_peer_cert"))} - , {"secure_renegotiate", sc(boolean(), M("secure_renegotiate"), D("secure_renegotiate"))} - , {"reuse_sessions", sc(boolean(), M("reuse_sessions"), D("reuse_sessions"))} - , {"honor_cipher_order", sc(boolean(), M("honor_cipher_order"), D("honor_cipher_order"))} - , {"handshake_timeout", sc(duration(), M("handshake_timeout"), D("handshake_timeout"))} - , {"depth", sc(integer(), M("depth"), D("depth"))} - , {"password", hoconsc:mk(binary(), #{ mapping => M("key_password") - , default => D("key_password") - , sensitive => true - })} - , {"dhfile", sc(binary(), M("dhfile"), D("dhfile"))} - , {"server_name_indication", sc(union(disable, binary()), M("server_name_indication"), - D("server_name_indication"))} - , {"tls_versions", sc(comma_separated_list(), M("tls_versions"), D("tls_versions"))} - , {"ciphers", sc(comma_separated_list(), M("ciphers"), D("ciphers"))} - , {"psk_ciphers", sc(comma_separated_list(), M("ciphers"), D("ciphers"))}]. +ref(Mod, Field) -> + hoconsc:ref(Mod, Field). diff --git a/apps/emqx_gateway/src/emqx_gateway_utils.erl b/apps/emqx_gateway/src/emqx_gateway_utils.erl index 2b4e9f0a2..27e64bed3 100644 --- a/apps/emqx_gateway/src/emqx_gateway_utils.erl +++ b/apps/emqx_gateway/src/emqx_gateway_utils.erl @@ -143,16 +143,47 @@ normalize_config(RawConf) -> Listeners = maps:fold(fun(Name, Confs, AccIn2) -> ListenOn = maps:get(bind, Confs), - SocketOpts = esockd:parse_opt(maps:to_list(Confs)), + SocketOpts = esockd_opts(Type, Confs), RemainCfgs = maps:without( - [bind] ++ proplists:get_keys(SocketOpts), - Confs), + [bind, tcp, ssl, udp, dtls] + ++ proplists:get_keys(SocketOpts), Confs), Cfg = maps:merge(Cfg0, RemainCfgs), [{Type, Name, ListenOn, SocketOpts, Cfg}|AccIn2] end, [], Liss), [Listeners|AccIn1] end, [], LisMap)). +esockd_opts(Type, Opts0) -> + Opts1 = maps:with([acceptors, max_connections, max_conn_rate, + proxy_protocol, proxy_protocol_timeout], Opts0), + Opts2 = Opts1#{access_rules => esockd_access_rules(maps:get(access_rules, Opts0, []))}, + maps:to_list(case Type of + tcp -> Opts2#{tcp_options => sock_opts(tcp, Opts0)}; + ssl -> Opts2#{tcp_options => sock_opts(tcp, Opts0), + ssl_options => ssl_opts(ssl, Opts0)}; + udp -> Opts2#{udp_options => sock_opts(udp, Opts0)}; + dtls -> Opts2#{udp_options => sock_opts(udp, Opts0), + dtls_options => ssl_opts(dtls, Opts0)} + end). + +esockd_access_rules(StrRules) -> + Access = fun(S) -> + [A, CIDR] = string:tokens(S, " "), + {list_to_atom(A), case CIDR of "all" -> all; _ -> CIDR end} + end, + [Access(R) || R <- StrRules]. + +ssl_opts(Name, Opts) -> + maps:to_list( + emqx_tls_lib:drop_tls13_for_old_otp( + maps:without([enable], + maps:get(Name, Opts, #{})))). + +sock_opts(Name, Opts) -> + maps:to_list( + maps:without([active_n], + maps:get(Name, Opts, #{}))). + %%-------------------------------------------------------------------- %% Envs diff --git a/apps/emqx_gateway/test/emqx_exproto_SUITE.erl b/apps/emqx_gateway/test/emqx_exproto_SUITE.erl index 4ab91da5d..b91cd03b9 100644 --- a/apps/emqx_gateway/test/emqx_exproto_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_exproto_SUITE.erl @@ -76,7 +76,7 @@ set_special_cfg(_App) -> listener_confs(Type) -> Default = #{bind => 7993, acceptors => 8}, - #{Type => #{'1' => maps:merge(Default, maps:from_list(socketopts(Type)))}}. + #{Type => #{'default' => maps:merge(Default, socketopts(Type))}}. %%-------------------------------------------------------------------- %% Tests cases @@ -360,11 +360,11 @@ open(udp) -> {ok, Sock} = gen_udp:open(0, ?TCPOPTS), {udp, Sock}; open(ssl) -> - SslOpts = client_ssl_opts(), + SslOpts = maps:to_list(client_ssl_opts()), {ok, SslSock} = ssl:connect("127.0.0.1", 7993, ?TCPOPTS ++ SslOpts), {ssl, SslSock}; open(dtls) -> - SslOpts = client_ssl_opts(), + SslOpts = maps:to_list(client_ssl_opts()), {ok, SslSock} = ssl:connect("127.0.0.1", 7993, ?DTLSOPTS ++ SslOpts), {dtls, SslSock}. @@ -400,51 +400,56 @@ close({dtls, Sock}) -> %% Server-Opts socketopts(tcp) -> - [{tcp_options, tcp_opts()}]; + #{tcp => tcp_opts()}; socketopts(ssl) -> - [{tcp_options, tcp_opts()}, - {ssl_options, ssl_opts()}]; + #{tcp => tcp_opts(), + ssl => ssl_opts()}; socketopts(udp) -> - [{udp_options, udp_opts()}]; + #{udp => udp_opts()}; socketopts(dtls) -> - [{udp_options, udp_opts()}, - {dtls_options, dtls_opts()}]. + #{udp => udp_opts(), + dtls => dtls_opts()}. tcp_opts() -> - [{send_timeout, 15000}, - {send_timeout_close, true}, - {backlog, 100}, - {nodelay, true} | udp_opts()]. + maps:merge( + udp_opts(), + #{send_timeout => 15000, + send_timeout_close => true, + backlog => 100, + nodelay => true} + ). udp_opts() -> - [{recbuf, 1024}, - {sndbuf, 1024}, - {buffer, 1024}, - {reuseaddr, true}]. + #{recbuf => 1024, + sndbuf => 1024, + buffer => 1024, + reuseaddr => true}. ssl_opts() -> Certs = certs("key.pem", "cert.pem", "cacert.pem"), - [{versions, emqx_tls_lib:default_versions()}, - {ciphers, emqx_tls_lib:default_ciphers()}, - {verify, verify_peer}, - {fail_if_no_peer_cert, true}, - {secure_renegotiate, false}, - {reuse_sessions, true}, - {honor_cipher_order, true}]++Certs. + maps:merge( + Certs, + #{versions => emqx_tls_lib:default_versions(), + ciphers => emqx_tls_lib:default_ciphers(), + verify => verify_peer, + fail_if_no_peer_cert => true, + secure_renegotiate => false, + reuse_sessions => true, + honor_cipher_order => true} + ). dtls_opts() -> - Opts = ssl_opts(), - lists:keyreplace(versions, 1, Opts, {versions, ['dtlsv1.2', 'dtlsv1']}). + maps:merge(ssl_opts(), #{versions => ['dtlsv1.2', 'dtlsv1']}). %%-------------------------------------------------------------------- %% Client-Opts client_ssl_opts() -> - certs( "client-key.pem", "client-cert.pem", "cacert.pem" ). + certs("client-key.pem", "client-cert.pem", "cacert.pem"). -certs( Key, Cert, CACert ) -> +certs(Key, Cert, CACert) -> CertsPath = emqx_ct_helpers:deps_path(emqx, "etc/certs"), - [ { keyfile, filename:join([ CertsPath, Key ]) }, - { certfile, filename:join([ CertsPath, Cert ]) }, - { cacertfile, filename:join([ CertsPath, CACert ]) } ]. + #{keyfile => filename:join([ CertsPath, Key ]), + certfile => filename:join([ CertsPath, Cert ]), + cacertfile => filename:join([ CertsPath, CACert])}. From 0453702ce5bd5f88e306925ba0c4461cbc405d10 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Wed, 8 Sep 2021 18:59:59 +0800 Subject: [PATCH 094/109] refactor(gw): improve http-api return structure --- apps/emqx_gateway/etc/emqx_gateway.conf | 17 ++++ apps/emqx_gateway/src/coap/emqx_coap_impl.erl | 7 +- apps/emqx_gateway/src/emqx_gateway_api.erl | 94 ++++++++++++------- apps/emqx_gateway/src/emqx_gateway_http.erl | 86 ++++++++++++++--- apps/emqx_gateway/src/emqx_gateway_schema.erl | 35 ++++++- apps/emqx_gateway/src/emqx_gateway_utils.erl | 35 +++++++ .../src/exproto/emqx_exproto_impl.erl | 7 +- .../src/lwm2m/emqx_lwm2m_impl.erl | 8 +- apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl | 7 +- .../src/stomp/emqx_stomp_impl.erl | 7 +- 10 files changed, 228 insertions(+), 75 deletions(-) diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index 01fb9f316..9f558b761 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -43,6 +43,10 @@ gateway.stomp { max_connections = 1024000 max_conn_rate = 1000 + access_rules = [ + "allow all" + ] + ## TCP options ## See ${example_common_tcp_options} for more information tcp.active_n = 100 @@ -68,6 +72,16 @@ gateway.stomp { ssl.keyfile = "{{ platform_etc_dir }}/certs/key.pem" ssl.certfile = "{{ platform_etc_dir }}/certs/cert.pem" ssl.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" + #ssl.verify = verify_none + #ssl.fail_if_no_peer_cert = false + #ssl.server_name_indication = disable + #ssl.secure_renegotiate = false + #ssl.reuse_sessions = false + #ssl.honor_cipher_order = false + #ssl.handshake_timeout = 15s + #ssl.depth = 10 + #ssl.password = foo + #ssl.dhfile = path-to-your-file } } @@ -116,6 +130,9 @@ gateway.coap { ## DTLS Options ## See #{example_common_dtls_options} for more information dtls.versions = ["dtlsv1"] + dtls.keyfile = "{{ platform_etc_dir }}/certs/key.pem" + dtls.certfile = "{{ platform_etc_dir }}/certs/cert.pem" + dtls.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" } } diff --git a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl index b0714f5e9..69015788a 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl @@ -99,7 +99,7 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> end. start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> - Name = name(GwName, LisName, Type), + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), NCfg = Cfg#{ ctx => Ctx, frame_mod => emqx_coap_frame, @@ -114,9 +114,6 @@ do_start_listener(udp, Name, ListenOn, SocketOpts, MFA) -> do_start_listener(dtls, Name, ListenOn, SocketOpts, MFA) -> esockd:open_dtls(Name, ListenOn, SocketOpts, MFA). -name(GwName, LisName, Type) -> - list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). - stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), @@ -130,5 +127,5 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet. stop_listener(GwName, Type, LisName, ListenOn, _SocketOpts, _Cfg) -> - Name = name(GwName, LisName, Type), + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/src/emqx_gateway_api.erl b/apps/emqx_gateway/src/emqx_gateway_api.erl index 9c9398945..f264339a4 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api.erl @@ -66,18 +66,21 @@ gateway_insta(get, #{bindings := #{name := Name0}}) -> Name = binary_to_existing_atom(Name0), case emqx_gateway:lookup(Name) of #{config := _Config} -> - %% FIXME: Got the parsed config, but we should return rawconfig to - %% frontend - RawConf = emqx_config:fill_defaults( - emqx_config:get_root_raw([<<"gateway">>]) - ), - {200, emqx_map_lib:deep_get([<<"gateway">>, Name0], RawConf)}; + GwCfs = filled_raw_confs([<<"gateway">>, Name0]), + NGwCfs = GwCfs#{<<"listeners">> => + emqx_gateway_http:mapping_listener_m2l( + Name0, maps:get(<<"listeners">>, GwCfs, #{}) + ) + }, + {200, NGwCfs}; undefined -> return_http_error(404, <<"Gateway not found">>) end; -gateway_insta(put, #{body := RawConfsIn, +gateway_insta(put, #{body := RawConfsIn0, bindings := #{name := Name} }) -> + RawConfsIn = maps:without([<<"authentication">>, + <<"listeners">>], RawConfsIn0), %% FIXME: Cluster Consistence ?? case emqx_gateway:update_rawconf(Name, RawConfsIn) of ok -> @@ -91,6 +94,12 @@ gateway_insta(put, #{body := RawConfsIn, gateway_insta_stats(get, _Req) -> return_http_error(401, <<"Implement it later (maybe 5.1)">>). +filled_raw_confs(Path) -> + RawConf = emqx_config:fill_defaults( + emqx_config:get_root_raw(Path) + ), + Confs = emqx_map_lib:deep_get(Path, RawConf), + emqx_map_lib:jsonable_map(Confs). %%-------------------------------------------------------------------- %% Swagger defines @@ -199,8 +208,13 @@ schema_gateway_overview_list() -> <<"enable">> => true, <<"enable_stats">> => true,<<"heartbeat">> => <<"30s">>, <<"idle_timeout">> => <<"30s">>, - <<"listeners">> => - #{<<"udp">> => #{<<"default">> => #{<<"bind">> => 5683}}}, + <<"listeners">> => [ + #{<<"id">> => <<"coap:udp:default">>, + <<"type">> => <<"udp">>, + <<"running">> => true, + <<"acceptors">> => 8,<<"bind">> => 5683, + <<"max_conn_rate">> => 1000, + <<"max_connections">> => 10240}], <<"mountpoint">> => <<>>,<<"notify_type">> => <<"qos">>, <<"publish_qos">> => <<"qos1">>, <<"subscribe_qos">> => <<"qos0">>} @@ -212,12 +226,13 @@ schema_gateway_overview_list() -> <<"handler">> => #{<<"address">> => <<"http://127.0.0.1:9001">>}, <<"idle_timeout">> => <<"30s">>, - <<"listeners">> => - #{<<"tcp">> => - #{<<"default">> => - #{<<"acceptors">> => 8,<<"bind">> => 7993, - <<"max_conn_rate">> => 1000, - <<"max_connections">> => 10240}}}, + <<"listeners">> => [ + #{<<"id">> => <<"exproto:tcp:default">>, + <<"type">> => <<"tcp">>, + <<"running">> => true, + <<"acceptors">> => 8,<<"bind">> => 7993, + <<"max_conn_rate">> => 1000, + <<"max_connections">> => 10240}], <<"mountpoint">> => <<>>, <<"server">> => #{<<"bind">> => 9100}} ). @@ -229,8 +244,11 @@ schema_gateway_overview_list() -> <<"idle_timeout">> => <<"30s">>, <<"lifetime_max">> => <<"86400s">>, <<"lifetime_min">> => <<"1s">>, - <<"listeners">> => - #{<<"udp">> => #{<<"default">> => #{<<"bind">> => 5783}}}, + <<"listeners">> => [ + #{<<"id">> => <<"lwm2m:udp:default">>, + <<"type">> => <<"udp">>, + <<"running">> => true, + <<"bind">> => 5783}], <<"mountpoint">> => <<"lwm2m/%e/">>, <<"qmode_time_windonw">> => 22, <<"translators">> => @@ -251,11 +269,12 @@ schema_gateway_overview_list() -> <<"enable">> => true, <<"enable_qos3">> => true,<<"enable_stats">> => true, <<"gateway_id">> => 1,<<"idle_timeout">> => <<"30s">>, - <<"listeners">> => - #{<<"udp">> => - #{<<"default">> => - #{<<"bind">> => 1884,<<"max_conn_rate">> => 1000, - <<"max_connections">> => 10240000}}}, + <<"listeners">> => [ + #{<<"id">> => <<"mqttsn:udp:default">>, + <<"type">> => <<"udp">>, + <<"running">> => true, + <<"bind">> => 1884,<<"max_conn_rate">> => 1000, + <<"max_connections">> => 10240000}], <<"mountpoint">> => <<>>, <<"predefined">> => [#{<<"id">> => 1, @@ -279,12 +298,13 @@ schema_gateway_overview_list() -> #{<<"max_body_length">> => 8192,<<"max_headers">> => 10, <<"max_headers_length">> => 1024}, <<"idle_timeout">> => <<"30s">>, - <<"listeners">> => - #{<<"tcp">> => - #{<<"default">> => - #{<<"acceptors">> => 16,<<"active_n">> => 100, - <<"bind">> => 61613,<<"max_conn_rate">> => 1000, - <<"max_connections">> => 1024000}}}, + <<"listeners">> => [ + #{<<"id">> => <<"stomp:tcp:default">>, + <<"type">> => <<"tcp">>, + <<"running">> => true, + <<"acceptors">> => 16,<<"active_n">> => 100, + <<"bind">> => 61613,<<"max_conn_rate">> => 1000, + <<"max_connections">> => 1024000}], <<"mountpoint">> => <<>>} ). @@ -312,10 +332,12 @@ schema_gateway_stats() -> properties_gateway_overview() -> ListenerProps = - [ {name, string, - <<"Listener Name">>} - , {status, string, - <<"Listener Status">>, [<<"activing">>, <<"inactived">>]} + [ {id, string, + <<"Listener ID">>} + , {running, boolean, + <<"Listener Running status">>} + , {type, string, + <<"Listener Type">>, [<<"tcp">>, <<"ssl">>, <<"udp">>, <<"dtls">>]} ], emqx_mgmt_util:properties( [ {name, string, @@ -323,9 +345,13 @@ properties_gateway_overview() -> , {status, string, <<"Gateway Status">>, [<<"running">>, <<"stopped">>, <<"unloaded">>]} + , {created_at, string, + <<>>} , {started_at, string, <<>>} - , {max_connection, integer, <<>>} - , {current_connection, integer, <<>>} + , {stopped_at, string, + <<>>} + , {max_connections, integer, <<>>} + , {current_connections, integer, <<>>} , {listeners, {array, object}, ListenerProps} ]). diff --git a/apps/emqx_gateway/src/emqx_gateway_http.erl b/apps/emqx_gateway/src/emqx_gateway_http.erl index 2aa6b4b3d..f233a6151 100644 --- a/apps/emqx_gateway/src/emqx_gateway_http.erl +++ b/apps/emqx_gateway/src/emqx_gateway_http.erl @@ -24,6 +24,12 @@ -export([ gateways/1 ]). +%% Mgmt APIs - listeners +-export([ listeners/1 + , listener/2 + , mapping_listener_m2l/2 + ]). + %% Mgmt APIs - clients -export([ lookup_client/3 , lookup_client/4 @@ -42,8 +48,8 @@ #{ name := binary() , status := running | stopped | unloaded , started_at => binary() - , max_connection => integer() - , current_connect => integer() + , max_connections => integer() + , current_connections => integer() , listeners => [] }. @@ -68,8 +74,10 @@ gateways(Status) -> created_at, started_at, stopped_at], GwInfo0), - GwInfo1#{listeners => get_listeners_status(GwName, Config)} - + GwInfo1#{ + max_connections => max_connections_count(Config), + current_connections => current_connections_count(GwName), + listeners => get_listeners_status(GwName, Config)} end end, emqx_gateway_registry:list()), case Status of @@ -78,24 +86,78 @@ gateways(Status) -> [Gw || Gw = #{status := S} <- Gateways, S == Status] end. +%% @private +max_connections_count(Config) -> + Listeners = emqx_gateway_utils:normalize_config(Config), + lists:foldl(fun({_, _, _, SocketOpts, _}, Acc) -> + Acc + proplists:get_value(max_connections, SocketOpts, 0) + end, 0, Listeners). + +%% @private +current_connections_count(GwName) -> + try + InfoTab = emqx_gateway_cm:tabname(info, GwName), + ets:info(InfoTab, size) + catch _ : _ -> + 0 + end. + %% @private get_listeners_status(GwName, Config) -> Listeners = emqx_gateway_utils:normalize_config(Config), lists:map(fun({Type, LisName, ListenOn, _, _}) -> - Name0 = listener_name(GwName, Type, LisName), + Name0 = emqx_gateway_utils:listener_id(GwName, Type, LisName), Name = {Name0, ListenOn}, + LisO = #{id => Name0, type => Type}, case catch esockd:listener(Name) of _Pid when is_pid(_Pid) -> - #{Name0 => <<"activing">>}; + LisO#{running => true}; _ -> - #{Name0 => <<"inactived">>} - + LisO#{running => false} end end, Listeners). -%% @private -listener_name(GwName, Type, LisName) -> - list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). +%%-------------------------------------------------------------------- +%% Mgmt APIs - listeners +%%-------------------------------------------------------------------- + +listeners(GwName) when is_atom (GwName) -> + listeners(atom_to_binary(GwName)); +listeners(GwName) -> + RawConf = emqx_config:fill_defaults( + emqx_config:get_root_raw([<<"gateway">>])), + Listeners = emqx_map_lib:jsonable_map( + emqx_map_lib:deep_get( + [<<"gateway">>, GwName, <<"listeners">>], RawConf)), + mapping_listener_m2l(GwName, Listeners). + +listener(_GwName, _ListenerId) -> + ok. + +mapping_listener_m2l(GwName, Listeners0) -> + Listeners = maps:to_list(Listeners0), + lists:append([listener(GwName, Type, maps:to_list(Conf)) + || {Type, Conf} <- Listeners]). + +listener(GwName, Type, Conf) -> + [begin + ListenerId = emqx_gateway_utils:listener_id(GwName, Type, LName), + Running = is_running(ListenerId, LConf), + LConf#{ + id => ListenerId, + type => Type, + running => Running + } + end || {LName, LConf} <- Conf, is_map(LConf)]. + +is_running(ListenerId, #{<<"bind">> := ListenOn0}) -> + ListenOn = emqx_gateway_utils:parse_listenon(ListenOn0), + try esockd:listener({ListenerId, ListenOn}) of + Pid when is_pid(Pid)-> + true + catch _:_ -> + false + end. %%-------------------------------------------------------------------- %% Mgmt APIs - clients @@ -145,7 +207,7 @@ list_client_subscriptions(GwName, ClientId) -> with_channel(GwName, ClientId, fun(Pid) -> Subs = emqx_gateway_conn:call( - Pid, + Pid, subscriptions, ?DEFAULT_CALL_TIMEOUT), {ok, lists:map(fun({Topic, SubOpts}) -> SubOpts#{topic => Topic} diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index 8c3663358..41840c7f7 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -204,7 +204,6 @@ fields(udp_opts) -> fields(dtls_listener_ssl_opts) -> Base = emqx_schema:fields("listener_ssl_opts"), - %% XXX: ciphers ??? DtlsVers = hoconsc:mk( typerefl:alias("string", list(atom())), #{ default => default_dtls_vsns(), @@ -212,12 +211,41 @@ fields(dtls_listener_ssl_opts) -> [dtls_vsn(iolist_to_binary(V)) || V <- Vsns] end }), - lists:keyreplace("versions", 1, Base, {"versions", DtlsVers}); + Ciphers = sc(hoconsc:array(string()), default_ciphers()), + lists:keydelete( + "handshake_timeout", 1, + lists:keyreplace( + "ciphers", 1, + lists:keyreplace("versions", 1, Base, {"versions", DtlsVers}), + {"ciphers", Ciphers} + ) + ); fields(ExtraField) -> Mod = list_to_atom(ExtraField++"_schema"), Mod:fields(ExtraField). +default_ciphers() -> + ["ECDHE-ECDSA-AES256-GCM-SHA384", + "ECDHE-RSA-AES256-GCM-SHA384", "ECDHE-ECDSA-AES256-SHA384", "ECDHE-RSA-AES256-SHA384", + "ECDHE-ECDSA-DES-CBC3-SHA", "ECDH-ECDSA-AES256-GCM-SHA384", "ECDH-RSA-AES256-GCM-SHA384", + "ECDH-ECDSA-AES256-SHA384", "ECDH-RSA-AES256-SHA384", "DHE-DSS-AES256-GCM-SHA384", + "DHE-DSS-AES256-SHA256", "AES256-GCM-SHA384", "AES256-SHA256", + "ECDHE-ECDSA-AES128-GCM-SHA256", "ECDHE-RSA-AES128-GCM-SHA256", + "ECDHE-ECDSA-AES128-SHA256", "ECDHE-RSA-AES128-SHA256", "ECDH-ECDSA-AES128-GCM-SHA256", + "ECDH-RSA-AES128-GCM-SHA256", "ECDH-ECDSA-AES128-SHA256", "ECDH-RSA-AES128-SHA256", + "DHE-DSS-AES128-GCM-SHA256", "DHE-DSS-AES128-SHA256", "AES128-GCM-SHA256", "AES128-SHA256", + "ECDHE-ECDSA-AES256-SHA", "ECDHE-RSA-AES256-SHA", "DHE-DSS-AES256-SHA", + "ECDH-ECDSA-AES256-SHA", "ECDH-RSA-AES256-SHA", "AES256-SHA", "ECDHE-ECDSA-AES128-SHA", + "ECDHE-RSA-AES128-SHA", "DHE-DSS-AES128-SHA", "ECDH-ECDSA-AES128-SHA", + "ECDH-RSA-AES128-SHA", "AES128-SHA" + ] ++ psk_ciphers(). + +psk_ciphers() -> + ["PSK-AES128-CBC-SHA", "PSK-AES256-CBC-SHA", + "PSK-3DES-EDE-CBC-SHA", "PSK-RC4-SHA" + ]. + % authentication() -> % hoconsc:union( % [ undefined @@ -242,7 +270,7 @@ gateway_common_options() -> [ {enable, sc(boolean(), true)} , {enable_stats, sc(boolean(), true)} , {idle_timeout, sc(duration(), <<"30s">>)} - , {mountpoint, sc(binary())} + , {mountpoint, sc(binary(), undefined)} , {clientinfo_override, sc(ref(clientinfo_override))} , {authentication, sc(hoconsc:lazy(map()))} ]. @@ -254,6 +282,7 @@ common_listener_opts() -> , {max_connections, sc(integer(), 1024)} , {max_conn_rate, sc(integer())} %, {rate_limit, sc(comma_separated_list())} + , {mountpoint, sc(binary(), undefined)} , {access_rules, sc(hoconsc:array(string()), [])} ]. diff --git a/apps/emqx_gateway/src/emqx_gateway_utils.erl b/apps/emqx_gateway/src/emqx_gateway_utils.erl index 27e64bed3..4f19db23b 100644 --- a/apps/emqx_gateway/src/emqx_gateway_utils.erl +++ b/apps/emqx_gateway/src/emqx_gateway_utils.erl @@ -28,8 +28,11 @@ -export([ apply/2 , format_listenon/1 + , parse_listenon/1 , unix_ts_to_rfc3339/1 , unix_ts_to_rfc3339/2 + , listener_id/3 + , parse_listener_id/1 ]). -export([ stringfy/1 @@ -112,6 +115,38 @@ format_listenon({Addr, Port}) when is_list(Addr) -> format_listenon({Addr, Port}) when is_tuple(Addr) -> io_lib:format("~s:~w", [inet:ntoa(Addr), Port]). +parse_listenon(Port) when is_integer(Port) -> + Port; +parse_listenon(Str) when is_binary(Str) -> + parse_listenon(binary_to_list(Str)); +parse_listenon(Str) when is_list(Str) -> + case emqx_schema:to_ip_port(Str) of + {ok, R} -> R; + {error, _} -> + error({invalid_listenon_name, Str}) + end. + +listener_id(GwName, Type, LisName) -> + binary_to_atom( + <<(bin(GwName))/binary, ":", + (bin(Type))/binary, ":", + (bin(LisName))/binary + >>). + +parse_listener_id(Id) -> + try + [GwName, Type, Name] = binary:split(bin(Id), <<":">>, [global]), + {binary_to_existing_atom(GwName), binary_to_existing_atom(Type), + binary_to_atom(Name)} + catch + _ : _ -> error({invalid_listener_id, Id}) + end. + +bin(A) when is_atom(A) -> + atom_to_binary(A); +bin(L) when is_list(L); is_binary(L) -> + iolist_to_binary(L). + unix_ts_to_rfc3339(Keys, Map) when is_list(Keys) -> lists:foldl(fun(K, Acc) -> unix_ts_to_rfc3339(K, Acc) end, Map, Keys); unix_ts_to_rfc3339(Key, Map) -> diff --git a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl index 3b62ecd20..a9c0ee5fd 100644 --- a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl +++ b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl @@ -153,7 +153,7 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> end. start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> - Name = name(GwName, LisName, Type), + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), NCfg = Cfg#{ ctx => Ctx, frame_mod => emqx_exproto_frame, @@ -172,9 +172,6 @@ do_start_listener(udp, Name, ListenOn, Opts, MFA) -> do_start_listener(dtls, Name, ListenOn, Opts, MFA) -> esockd:open_dtls(Name, ListenOn, Opts, MFA). -name(GwName, LisName, Type) -> - list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). - merge_default_by_type(Type, Options) when Type =:= tcp; Type =:= ssl -> Default = emqx_gateway_utils:default_tcp_options(), @@ -209,5 +206,5 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet. stop_listener(GwName, Type, LisName, ListenOn, _SocketOpts, _Cfg) -> - Name = name(GwName, LisName, Type), + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl index e6720905b..5e1e9a70d 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl @@ -100,7 +100,7 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> end. start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> - Name = name(GwName, LisName, udp), + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), NCfg = Cfg#{ ctx => Ctx , frame_mod => emqx_coap_frame , chann_mod => emqx_lwm2m_channel @@ -119,16 +119,12 @@ merge_default(Options) -> [{udp_options, Default} | Options] end. -name(GwName, LisName, Type) -> - list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). - do_start_listener(udp, Name, ListenOn, SocketOpts, MFA) -> esockd:open_udp(Name, ListenOn, SocketOpts, MFA); do_start_listener(dtls, Name, ListenOn, SocketOpts, MFA) -> esockd:open_dtls(Name, ListenOn, SocketOpts, MFA). - stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), @@ -142,5 +138,5 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet. stop_listener(GwName, Type, LisName, ListenOn, _SocketOpts, _Cfg) -> - Name = name(GwName, LisName, Type), + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl index f510afdf9..8e64a41d1 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl @@ -118,7 +118,7 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> end. start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> - Name = name(GwName, LisName, Type), + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), NCfg = Cfg#{ ctx => Ctx, frame_mod => emqx_sn_frame, @@ -127,9 +127,6 @@ start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> esockd:open_udp(Name, ListenOn, merge_default(SocketOpts), {emqx_gateway_conn, start_link, [NCfg]}). -name(GwName, LisName, Type) -> - list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). - merge_default(Options) -> Default = emqx_gateway_utils:default_udp_options(), case lists:keytake(udp_options, 1, Options) of @@ -153,5 +150,5 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet. stop_listener(GwName, Type, LisName, ListenOn, _SocketOpts, _Cfg) -> - Name = name(GwName, LisName, Type), + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), esockd:close(Name, ListenOn). diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl index 2175b767b..96c7b7a3b 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl @@ -103,7 +103,7 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> end. start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> - Name = name(GwName, LisName, Type), + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), NCfg = Cfg#{ ctx => Ctx, frame_mod => emqx_stomp_frame, @@ -112,9 +112,6 @@ start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) -> esockd:open(Name, ListenOn, merge_default(SocketOpts), {emqx_gateway_conn, start_link, [NCfg]}). -name(GwName, LisName, Type) -> - list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])). - merge_default(Options) -> Default = emqx_gateway_utils:default_tcp_options(), case lists:keytake(tcp_options, 1, Options) of @@ -138,5 +135,5 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet. stop_listener(GwName, Type, LisName, ListenOn, _SocketOpts, _Cfg) -> - Name = name(GwName, LisName, Type), + Name = emqx_gateway_utils:listener_id(GwName, Type, LisName), esockd:close(Name, ListenOn). From 020e04e5cf4747d46023370278c5168aa16a2c49 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Thu, 9 Sep 2021 09:57:48 +0800 Subject: [PATCH 095/109] chore(gw): improve the default confs --- apps/emqx_gateway/etc/emqx_gateway.conf | 22 ++++++++++++++++++- apps/emqx_gateway/src/emqx_gateway_schema.erl | 3 +-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/emqx_gateway/etc/emqx_gateway.conf b/apps/emqx_gateway/etc/emqx_gateway.conf index 9f558b761..2ce48bf75 100644 --- a/apps/emqx_gateway/etc/emqx_gateway.conf +++ b/apps/emqx_gateway/etc/emqx_gateway.conf @@ -129,7 +129,7 @@ gateway.coap { ## DTLS Options ## See #{example_common_dtls_options} for more information - dtls.versions = ["dtlsv1"] + dtls.versions = ["dtlsv1.2", "dtlsv1"] dtls.keyfile = "{{ platform_etc_dir }}/certs/key.pem" dtls.certfile = "{{ platform_etc_dir }}/certs/cert.pem" dtls.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" @@ -182,6 +182,26 @@ gateway.mqttsn { max_connections = 10240000 max_conn_rate = 1000 } + + listeners.dtls.default { + bind = 1885 + acceptors = 4 + max_connections = 102400 + max_conn_rate = 1000 + + ## UDP Options + ## See ${example_common_udp_options} for more information + udp.active_n = 100 + udp.buffer = 16KB + + ## DTLS Options + ## See #{example_common_dtls_options} for more information + dtls.versions = ["dtlsv1.2", "dtlsv1"] + dtls.keyfile = "{{ platform_etc_dir }}/certs/key.pem" + dtls.certfile = "{{ platform_etc_dir }}/certs/cert.pem" + dtls.cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" + } + } gateway.lwm2m { diff --git a/apps/emqx_gateway/src/emqx_gateway_schema.erl b/apps/emqx_gateway/src/emqx_gateway_schema.erl index 41840c7f7..7fb945ba0 100644 --- a/apps/emqx_gateway/src/emqx_gateway_schema.erl +++ b/apps/emqx_gateway/src/emqx_gateway_schema.erl @@ -96,7 +96,6 @@ fields(lwm2m_structs) -> , {lifetime_max, sc(duration())} , {qmode_time_windonw, sc(integer())} , {auto_observe, sc(boolean())} - , {mountpoint, sc(string())} , {update_msg_publish_condition, sc(union([always, contains_object_list]))} , {translators, sc(ref(translators))} , {listeners, sc(ref(udp_listener_group))} @@ -270,7 +269,7 @@ gateway_common_options() -> [ {enable, sc(boolean(), true)} , {enable_stats, sc(boolean(), true)} , {idle_timeout, sc(duration(), <<"30s">>)} - , {mountpoint, sc(binary(), undefined)} + , {mountpoint, sc(binary(), <<>>)} , {clientinfo_override, sc(ref(clientinfo_override))} , {authentication, sc(hoconsc:lazy(map()))} ]. From 5da085baccf9f7aa3f425963030b691e102bbfb6 Mon Sep 17 00:00:00 2001 From: JianBo He Date: Thu, 9 Sep 2021 09:58:05 +0800 Subject: [PATCH 096/109] chore(gw): improve the listener started banner --- apps/emqx_gateway/src/coap/emqx_coap_impl.erl | 10 +++++----- apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl | 10 +++++----- apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl | 8 ++++---- apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl | 8 ++++---- apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl | 8 ++++---- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl index 69015788a..055eab759 100644 --- a/apps/emqx_gateway/src/coap/emqx_coap_impl.erl +++ b/apps/emqx_gateway/src/coap/emqx_coap_impl.erl @@ -89,11 +89,11 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start listener ~s:~s:~s on ~s successfully.~n", + ?ULOG("Gateway ~s:~s:~s on ~s started.~n", [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start listener ~s:~s:~s on ~s: ~0p~n", + ?ELOG("Failed to start gateway ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. @@ -118,10 +118,10 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop listener ~s:~s:~s on ~s successfully.~n", - [GwName, Type, LisName, ListenOnStr]); + ok -> ?ULOG("Gateway ~s:~s:~s on ~s stopped.~n", + [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop listener ~s:~s:~s on ~s: ~0p~n", + ?ELOG("Failed to stop gateway ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. diff --git a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl index a9c0ee5fd..3e142f3dc 100644 --- a/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl +++ b/apps/emqx_gateway/src/exproto/emqx_exproto_impl.erl @@ -143,11 +143,11 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start listener ~s:~s:~s on ~s successfully.~n", - [GwName, Type, LisName, ListenOnStr]), + ?ULOG("Gateway ~s:~s:~s on ~s started.~n", + [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start listener ~s:~s:~s on ~s: ~0p~n", + ?ELOG("Failed to start gateway ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. @@ -197,10 +197,10 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop listener ~s:~s:~s on ~s successfully.~n", + ok -> ?ULOG("Gateway ~s:~s:~s on ~s stopped.~n", [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop listener ~s:~s:~s on ~s: ~0p~n", + ?ELOG("Failed to stop gateway ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. diff --git a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl index 5e1e9a70d..649a14643 100644 --- a/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl +++ b/apps/emqx_gateway/src/lwm2m/emqx_lwm2m_impl.erl @@ -90,11 +90,11 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start listener ~s:~s:~s on ~s successfully.~n", + ?ULOG("Gateway ~s:~s:~s on ~s started.~n", [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start listener ~s:~s:~s on ~s: ~0p~n", + ?ELOG("Failed to start gateway ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. @@ -129,10 +129,10 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop listener ~s:~s:~s on ~s successfully.~n", + ok -> ?ULOG("Gateway ~s:~s:~s on ~s stopped.~n", [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop listener ~s:~s:~s on ~s: ~0p~n", + ?ELOG("Failed to stop gateway ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. diff --git a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl index 8e64a41d1..a79173cff 100644 --- a/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl +++ b/apps/emqx_gateway/src/mqttsn/emqx_sn_impl.erl @@ -108,11 +108,11 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start listener ~s:~s:~s on ~s successfully.~n", + ?ULOG("Gateway ~s:~s:~s on ~s started.~n", [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start listener ~s:~s:~s on ~s: ~0p~n", + ?ELOG("Failed to start gateway ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. @@ -141,10 +141,10 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, LisName, Type, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop listener ~s:~s:~s on ~s successfully.~n", + ok -> ?ULOG("Gateway ~s:~s:~s on ~s stopped.~n", [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop listener ~s:~s:~s on ~s: ~0p~n", + ?ELOG("Failed to stop gatewat ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. diff --git a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl index 96c7b7a3b..9599ef6e3 100644 --- a/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl +++ b/apps/emqx_gateway/src/stomp/emqx_stomp_impl.erl @@ -93,11 +93,11 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) of {ok, Pid} -> - ?ULOG("Start listener ~s:~s:~s on ~s successfully.~n", + ?ULOG("Gateway ~s:~s:~s on ~s started.~n", [GwName, Type, LisName, ListenOnStr]), Pid; {error, Reason} -> - ?ELOG("Failed to start listener ~s:~s:~s on ~s: ~0p~n", + ?ELOG("Failed to start gateway ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]), throw({badconf, Reason}) end. @@ -126,10 +126,10 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) -> StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg), ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn), case StopRet of - ok -> ?ULOG("Stop listener ~s:~s:~s on ~s successfully.~n", + ok -> ?ULOG("Gateway ~s:~s:~s on ~s stopped.~n", [GwName, Type, LisName, ListenOnStr]); {error, Reason} -> - ?ELOG("Failed to stop listener ~s:~s:~s on ~s: ~0p~n", + ?ELOG("Failed to stop gateway ~s:~s:~s on ~s: ~0p~n", [GwName, Type, LisName, ListenOnStr, Reason]) end, StopRet. From 9a09bf796420e50c703fe13413eaabe44bca362d Mon Sep 17 00:00:00 2001 From: DDDHuang <44492639+DDDHuang@users.noreply.github.com> Date: Fri, 10 Sep 2021 09:20:16 +0800 Subject: [PATCH 097/109] fix: logout api delete token (#5686) --- apps/emqx_dashboard/src/emqx_dashboard_admin.erl | 11 ++++++++--- apps/emqx_dashboard/src/emqx_dashboard_api.erl | 12 ++++++++---- apps/emqx_dashboard/src/emqx_dashboard_token.erl | 6 ++++-- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl index 8a1306e94..b477bd779 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl @@ -40,7 +40,7 @@ -export([ sign_token/2 , verify_token/1 - , destroy_token_by_username/1 + , destroy_token_by_username/2 ]). -export([add_default_user/0]). @@ -177,8 +177,13 @@ sign_token(Username, Password) -> verify_token(Token) -> emqx_dashboard_token:verify(Token). -destroy_token_by_username(Username) -> - emqx_dashboard_token:destroy_by_username(Username). +destroy_token_by_username(Username, Token) -> + case emqx_dashboard_token:lookup(Token) of + {ok, #mqtt_admin_jwt{username = Username}} -> + emqx_dashboard_token:destroy(Token); + _ -> + {error, not_found} + end. %%-------------------------------------------------------------------- %% Internal functions diff --git a/apps/emqx_dashboard/src/emqx_dashboard_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_api.erl index 4761432fb..68c737488 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_api.erl @@ -170,10 +170,14 @@ login(post, #{body := Params}) -> {401, #{code => ?ERROR_USERNAME_OR_PWD, message => <<"Auth filed">>}} end. -logout(_, #{body := Params}) -> - Username = maps:get(<<"username">>, Params), - emqx_dashboard_admin:destroy_token_by_username(Username), - {200}. +logout(_, #{body := #{<<"username">> := Username}, + headers := #{<<"authorization">> := <<"Bearer ", Token/binary>>}}) -> + case emqx_dashboard_admin:destroy_token_by_username(Username, Token) of + ok -> + 200; + _R -> + {401, 'BAD_TOKEN_OR_USERNAME', <<"Ensure your token & username">>} + end. users(get, _Request) -> {200, [row(User) || User <- emqx_dashboard_admin:all_users()]}; diff --git a/apps/emqx_dashboard/src/emqx_dashboard_token.erl b/apps/emqx_dashboard/src/emqx_dashboard_token.erl index 9086b4c2e..2acf00f13 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_token.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_token.erl @@ -22,6 +22,7 @@ -export([ sign/2 , verify/1 + , lookup/1 , destroy/1 , destroy_by_username/1 ]). @@ -121,14 +122,15 @@ do_verify(Token)-> do_destroy(Token) -> Fun = fun mnesia:delete/1, - ekka_mnesia:transaction(?DASHBOARD_SHARD, Fun, [{?TAB, Token}]). + {atomic, ok} = ekka_mnesia:transaction(?DASHBOARD_SHARD, Fun, [{?TAB, Token}]), + ok. do_destroy_by_username(Username) -> gen_server:cast(?MODULE, {destroy, Username}). %%-------------------------------------------------------------------- %% jwt internal util function - +-spec(lookup(Token :: binary()) -> {ok, #mqtt_admin_jwt{}} | {error, not_found}). lookup(Token) -> case mnesia:dirty_read(?TAB, Token) of [JWT] -> {ok, JWT}; From bfb2df37ce7cf7f03b955817432fe4e206ef480a Mon Sep 17 00:00:00 2001 From: Shawn <506895667@qq.com> Date: Mon, 6 Sep 2021 17:32:46 +0800 Subject: [PATCH 098/109] refactor(bridge): rename emqx_data_bridge to emqx_bridge --- .../.gitignore | 0 .../README.md | 2 +- .../etc/emqx_bridge.conf} | 49 ++++++++++++++++--- .../rebar.config | 2 +- .../src/emqx_bridge.app.src} | 4 +- .../src/emqx_bridge.erl} | 12 +++-- .../src/emqx_bridge_api.erl} | 28 +++++------ .../src/emqx_bridge_app.erl} | 8 +-- .../src/emqx_bridge_monitor.erl} | 14 ++++-- apps/emqx_bridge/src/emqx_bridge_schema.erl | 17 +++++++ .../src/emqx_bridge_sup.erl} | 8 +-- apps/emqx_machine/src/emqx_machine.erl | 2 +- apps/emqx_machine/src/emqx_machine_schema.erl | 2 +- apps/emqx_resource/README.md | 2 +- apps/emqx_resource/examples/demo.md | 6 +-- rebar.config.erl | 2 +- 16 files changed, 107 insertions(+), 51 deletions(-) rename apps/{emqx_data_bridge => emqx_bridge}/.gitignore (100%) rename apps/{emqx_data_bridge => emqx_bridge}/README.md (95%) rename apps/{emqx_data_bridge/etc/emqx_data_bridge.conf => emqx_bridge/etc/emqx_bridge.conf} (78%) rename apps/{emqx_data_bridge => emqx_bridge}/rebar.config (73%) rename apps/{emqx_data_bridge/src/emqx_data_bridge.app.src => emqx_bridge/src/emqx_bridge.app.src} (75%) rename apps/{emqx_data_bridge/src/emqx_data_bridge.erl => emqx_bridge/src/emqx_bridge.erl} (86%) rename apps/{emqx_data_bridge/src/emqx_data_bridge_api.erl => emqx_bridge/src/emqx_bridge_api.erl} (83%) rename apps/{emqx_data_bridge/src/emqx_data_bridge_app.erl => emqx_bridge/src/emqx_bridge_app.erl} (87%) rename apps/{emqx_data_bridge/src/emqx_data_bridge_monitor.erl => emqx_bridge/src/emqx_bridge_monitor.erl} (84%) create mode 100644 apps/emqx_bridge/src/emqx_bridge_schema.erl rename apps/{emqx_data_bridge/src/emqx_data_bridge_sup.erl => emqx_bridge/src/emqx_bridge_sup.erl} (86%) diff --git a/apps/emqx_data_bridge/.gitignore b/apps/emqx_bridge/.gitignore similarity index 100% rename from apps/emqx_data_bridge/.gitignore rename to apps/emqx_bridge/.gitignore diff --git a/apps/emqx_data_bridge/README.md b/apps/emqx_bridge/README.md similarity index 95% rename from apps/emqx_data_bridge/README.md rename to apps/emqx_bridge/README.md index 8f76f17a5..0f274eea1 100644 --- a/apps/emqx_data_bridge/README.md +++ b/apps/emqx_bridge/README.md @@ -1,4 +1,4 @@ -# emqx_data_bridge +# emqx_bridge EMQ X Data Bridge is an application that managing the resources (see emqx_resource) used by emqx rule engine. diff --git a/apps/emqx_data_bridge/etc/emqx_data_bridge.conf b/apps/emqx_bridge/etc/emqx_bridge.conf similarity index 78% rename from apps/emqx_data_bridge/etc/emqx_data_bridge.conf rename to apps/emqx_bridge/etc/emqx_bridge.conf index 99a49dba3..663ae6586 100644 --- a/apps/emqx_data_bridge/etc/emqx_data_bridge.conf +++ b/apps/emqx_bridge/etc/emqx_bridge.conf @@ -1,10 +1,46 @@ ##-------------------------------------------------------------------- -## EMQ X Bridge Plugin +## EMQ X Bridge ##-------------------------------------------------------------------- -emqx_data_bridge { - bridges:[ - # {name: "mysql_bridge_1" +bridges.mqtt.my_mqtt_bridge { + server = "127.0.0.1:1883" + proto_ver = "v4" + clientid = "client1" + username = "username1" + password = "" + clean_start = true + keepalive = 300 + retry_interval = "30s" + max_inflight = 32 + reconnect_interval = "30s" + bridge_mode = true + replayq { + dir = "{{ platform_data_dir }}/replayq/bridge_mqtt/" + seg_bytes = "100MB" + offload = false + max_total_bytes = "1GB" + } + ssl { + enable = false + keyfile = "{{ platform_etc_dir }}/certs/client-key.pem" + certfile = "{{ platform_etc_dir }}/certs/client-cert.pem" + cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem" + } + in [{ + from_remote_topic = "msg/#" + to_local_topic = "from_aws/${topic}" + payload_template = "${message}" + qos = 1 + }] + out [{ + from_local_topic = "msg/#" + to_remote_topic = "from_emqx/${topic}" + payload_template = "${message}" + }] +} + + +# {name: "mysql_bridge_1" # type: mysql # config: { # server: "192.168.0.172:3306" @@ -123,7 +159,4 @@ emqx_data_bridge { # pool_size: 1 # ssl: false # } - # } - - ] -} + # } \ No newline at end of file diff --git a/apps/emqx_data_bridge/rebar.config b/apps/emqx_bridge/rebar.config similarity index 73% rename from apps/emqx_data_bridge/rebar.config rename to apps/emqx_bridge/rebar.config index cf4cfcf1b..3fd6b41e0 100644 --- a/apps/emqx_data_bridge/rebar.config +++ b/apps/emqx_bridge/rebar.config @@ -3,5 +3,5 @@ {shell, [ % {config, "config/sys.config"}, - {apps, [emqx_data_bridge]} + {apps, [emqx_bridge]} ]}. diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge.app.src b/apps/emqx_bridge/src/emqx_bridge.app.src similarity index 75% rename from apps/emqx_data_bridge/src/emqx_data_bridge.app.src rename to apps/emqx_bridge/src/emqx_bridge.app.src index 84486da19..42fc245f5 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge.app.src +++ b/apps/emqx_bridge/src/emqx_bridge.app.src @@ -1,8 +1,8 @@ -{application, emqx_data_bridge, +{application, emqx_bridge, [{description, "An OTP application"}, {vsn, "0.1.0"}, {registered, []}, - {mod, {emqx_data_bridge_app, []}}, + {mod, {emqx_bridge_app, []}}, {applications, [kernel, stdlib, diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge.erl b/apps/emqx_bridge/src/emqx_bridge.erl similarity index 86% rename from apps/emqx_data_bridge/src/emqx_data_bridge.erl rename to apps/emqx_bridge/src/emqx_bridge.erl index 52cea80fb..4e05f8e96 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge.erl +++ b/apps/emqx_bridge/src/emqx_bridge.erl @@ -13,7 +13,7 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_data_bridge). +-module(emqx_bridge). -export([ load_bridges/0 , resource_type/1 @@ -27,15 +27,17 @@ ]). load_bridges() -> - Bridges = emqx:get_config([emqx_data_bridge, bridges], []), - emqx_data_bridge_monitor:ensure_all_started(Bridges). + Bridges = emqx:get_config([bridges], #{}), + emqx_bridge_monitor:ensure_all_started(Bridges). +resource_type(mqtt) -> emqx_connector_mqtt; resource_type(mysql) -> emqx_connector_mysql; resource_type(pgsql) -> emqx_connector_pgsql; resource_type(mongo) -> emqx_connector_mongo; resource_type(redis) -> emqx_connector_redis; resource_type(ldap) -> emqx_connector_ldap. +bridge_type(emqx_connector_mqtt) -> mqtt; bridge_type(emqx_connector_mysql) -> mysql; bridge_type(emqx_connector_pgsql) -> pgsql; bridge_type(emqx_connector_mongo) -> mongo; @@ -49,7 +51,7 @@ resource_id_to_name(<<"bridge:", BridgeName/binary>> = _ResourceId) -> BridgeName. list_bridges() -> - emqx_resource_api:list_instances(fun emqx_data_bridge:is_bridge/1). + emqx_resource_api:list_instances(fun emqx_bridge:is_bridge/1). is_bridge(#{id := <<"bridge:", _/binary>>}) -> true; @@ -57,7 +59,7 @@ is_bridge(_Data) -> false. config_key_path() -> - [emqx_data_bridge, bridges]. + [emqx_bridge, bridges]. update_config(ConfigReq) -> emqx:update_config(config_key_path(), ConfigReq). diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge_api.erl b/apps/emqx_bridge/src/emqx_bridge_api.erl similarity index 83% rename from apps/emqx_data_bridge/src/emqx_data_bridge_api.erl rename to apps/emqx_bridge/src/emqx_bridge_api.erl index 6fe75e4ce..c10875e55 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_api.erl +++ b/apps/emqx_bridge/src/emqx_bridge_api.erl @@ -13,7 +13,7 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_data_bridge_api). +-module(emqx_bridge_api). -rest_api(#{ name => list_data_bridges , method => 'GET' @@ -61,10 +61,10 @@ list_bridges(_Binding, _Params) -> {200, #{code => 0, data => [format_api_reply(Data) || - Data <- emqx_data_bridge:list_bridges()]}}. + Data <- emqx_bridge:list_bridges()]}}. get_bridge(#{name := Name}, _Params) -> - case emqx_resource:get_instance(emqx_data_bridge:name_to_resource_id(Name)) of + case emqx_resource:get_instance(emqx_bridge:name_to_resource_id(Name)) of {ok, Data} -> {200, #{code => 0, data => format_api_reply(emqx_resource_api:format_data(Data))}}; {error, not_found} -> @@ -75,8 +75,8 @@ create_bridge(#{name := Name}, Params) -> Config = proplists:get_value(<<"config">>, Params), BridgeType = proplists:get_value(<<"type">>, Params), case emqx_resource:check_and_create( - emqx_data_bridge:name_to_resource_id(Name), - emqx_data_bridge:resource_type(atom(BridgeType)), maps:from_list(Config)) of + emqx_bridge:name_to_resource_id(Name), + emqx_bridge:resource_type(atom(BridgeType)), maps:from_list(Config)) of {ok, already_created} -> {400, #{code => 102, message => <<"bridge already created: ", Name/binary>>}}; {ok, Data} -> @@ -91,8 +91,8 @@ update_bridge(#{name := Name}, Params) -> Config = proplists:get_value(<<"config">>, Params), BridgeType = proplists:get_value(<<"type">>, Params), case emqx_resource:check_and_update( - emqx_data_bridge:name_to_resource_id(Name), - emqx_data_bridge:resource_type(atom(BridgeType)), maps:from_list(Config), []) of + emqx_bridge:name_to_resource_id(Name), + emqx_bridge:resource_type(atom(BridgeType)), maps:from_list(Config), []) of {ok, Data} -> update_config_and_reply(Name, BridgeType, Config, Data); {error, not_found} -> @@ -104,26 +104,26 @@ update_bridge(#{name := Name}, Params) -> end. delete_bridge(#{name := Name}, _Params) -> - case emqx_resource:remove(emqx_data_bridge:name_to_resource_id(Name)) of + case emqx_resource:remove(emqx_bridge:name_to_resource_id(Name)) of ok -> delete_config_and_reply(Name); {error, Reason} -> {500, #{code => 102, message => emqx_resource_api:stringnify(Reason)}} end. format_api_reply(#{resource_type := Type, id := Id, config := Conf, status := Status}) -> - #{type => emqx_data_bridge:bridge_type(Type), - name => emqx_data_bridge:resource_id_to_name(Id), + #{type => emqx_bridge:bridge_type(Type), + name => emqx_bridge:resource_id_to_name(Id), config => Conf, status => Status}. % format_conf(#{resource_type := Type, id := Id, config := Conf}) -> -% #{type => Type, name => emqx_data_bridge:resource_id_to_name(Id), +% #{type => Type, name => emqx_bridge:resource_id_to_name(Id), % config => Conf}. % get_all_configs() -> -% [format_conf(Data) || Data <- emqx_data_bridge:list_bridges()]. +% [format_conf(Data) || Data <- emqx_bridge:list_bridges()]. update_config_and_reply(Name, BridgeType, Config, Data) -> - case emqx_data_bridge:update_config({update, ?BRIDGE(Name, BridgeType, Config)}) of + case emqx_bridge:update_config({update, ?BRIDGE(Name, BridgeType, Config)}) of {ok, _} -> {200, #{code => 0, data => format_api_reply( emqx_resource_api:format_data(Data))}}; @@ -132,7 +132,7 @@ update_config_and_reply(Name, BridgeType, Config, Data) -> end. delete_config_and_reply(Name) -> - case emqx_data_bridge:update_config({delete, Name}) of + case emqx_bridge:update_config({delete, Name}) of {ok, _} -> {200, #{code => 0, data => #{}}}; {error, Reason} -> {500, #{code => 102, message => emqx_resource_api:stringnify(Reason)}} diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge_app.erl b/apps/emqx_bridge/src/emqx_bridge_app.erl similarity index 87% rename from apps/emqx_data_bridge/src/emqx_data_bridge_app.erl rename to apps/emqx_bridge/src/emqx_bridge_app.erl index 859952480..cfefe118f 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_app.erl +++ b/apps/emqx_bridge/src/emqx_bridge_app.erl @@ -13,7 +13,7 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_data_bridge_app). +-module(emqx_bridge_app). -behaviour(application). @@ -22,9 +22,9 @@ -export([start/2, stop/1, pre_config_update/2]). start(_StartType, _StartArgs) -> - {ok, Sup} = emqx_data_bridge_sup:start_link(), - ok = emqx_data_bridge:load_bridges(), - emqx_config_handler:add_handler(emqx_data_bridge:config_key_path(), ?MODULE), + {ok, Sup} = emqx_bridge_sup:start_link(), + ok = emqx_bridge:load_bridges(), + emqx_config_handler:add_handler(emqx_bridge:config_key_path(), ?MODULE), {ok, Sup}. stop(_State) -> diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge_monitor.erl b/apps/emqx_bridge/src/emqx_bridge_monitor.erl similarity index 84% rename from apps/emqx_data_bridge/src/emqx_data_bridge_monitor.erl rename to apps/emqx_bridge/src/emqx_bridge_monitor.erl index 4917833ec..4b3695615 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_monitor.erl +++ b/apps/emqx_bridge/src/emqx_bridge_monitor.erl @@ -15,7 +15,7 @@ %%-------------------------------------------------------------------- %% This process monitors all the data bridges, and try to restart a bridge %% when one of it stopped. --module(emqx_data_bridge_monitor). +-module(emqx_bridge_monitor). -behaviour(gen_server). @@ -65,14 +65,18 @@ code_change(_OldVsn, State, _Extra) -> %%============================================================================ load_bridges(Configs) -> - lists:foreach(fun load_bridge/1, Configs). + lists:foreach(fun(Type, NamedConf) -> + lists:foreach(fun(Name, Conf) -> + load_bridge(Name, Type, Conf) + end, maps:to_list(NamedConf)) + end, maps:to_list(Configs)). %% TODO: move this monitor into emqx_resource %% emqx_resource:check_and_create_local(ResourceId, ResourceType, Config, #{keep_retry => true}). -load_bridge(#{name := Name, type := Type, config := Config}) -> +load_bridge(Name, Type, Config) -> case emqx_resource:create_local( - emqx_data_bridge:name_to_resource_id(Name), - emqx_data_bridge:resource_type(Type), Config) of + emqx_bridge:name_to_resource_id(Name), + emqx_bridge:resource_type(Type), Config) of {ok, already_created} -> ok; {ok, _} -> ok; {error, Reason} -> diff --git a/apps/emqx_bridge/src/emqx_bridge_schema.erl b/apps/emqx_bridge/src/emqx_bridge_schema.erl new file mode 100644 index 000000000..f651ce189 --- /dev/null +++ b/apps/emqx_bridge/src/emqx_bridge_schema.erl @@ -0,0 +1,17 @@ +-module(emqx_bridge_schema). + +-export([roots/0, fields/1]). + +%%====================================================================================== +%% Hocon Schema Definitions + +roots() -> ["bridges"]. + +fields("bridges") -> + [{mqtt, hoconsc:ref("mqtt")}]; + +fields("mqtt") -> + [{"?name"}, hoconsc:ref("mqtt_briage")]; + +fields("mqtt_briage") -> + emqx_connector_mqtt:fields("config"). diff --git a/apps/emqx_data_bridge/src/emqx_data_bridge_sup.erl b/apps/emqx_bridge/src/emqx_bridge_sup.erl similarity index 86% rename from apps/emqx_data_bridge/src/emqx_data_bridge_sup.erl rename to apps/emqx_bridge/src/emqx_bridge_sup.erl index a699a72a0..fd12b1a99 100644 --- a/apps/emqx_data_bridge/src/emqx_data_bridge_sup.erl +++ b/apps/emqx_bridge/src/emqx_bridge_sup.erl @@ -13,7 +13,7 @@ %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- --module(emqx_data_bridge_sup). +-module(emqx_bridge_sup). -behaviour(supervisor). @@ -31,11 +31,11 @@ init([]) -> intensity => 10, period => 10}, ChildSpecs = [ - #{id => emqx_data_bridge_monitor, - start => {emqx_data_bridge_monitor, start_link, []}, + #{id => emqx_bridge_monitor, + start => {emqx_bridge_monitor, start_link, []}, restart => permanent, type => worker, - modules => [emqx_data_bridge_monitor]} + modules => [emqx_bridge_monitor]} ], {ok, {SupFlags, ChildSpecs}}. diff --git a/apps/emqx_machine/src/emqx_machine.erl b/apps/emqx_machine/src/emqx_machine.erl index 3e5772b4a..97125d79f 100644 --- a/apps/emqx_machine/src/emqx_machine.erl +++ b/apps/emqx_machine/src/emqx_machine.erl @@ -140,7 +140,7 @@ reboot_apps() -> , emqx_statsd , emqx_resource , emqx_rule_engine - , emqx_data_bridge + , emqx_bridge , emqx_bridge_mqtt , emqx_plugin_libs , emqx_management diff --git a/apps/emqx_machine/src/emqx_machine_schema.erl b/apps/emqx_machine/src/emqx_machine_schema.erl index 4894bda98..1f5d639cd 100644 --- a/apps/emqx_machine/src/emqx_machine_schema.erl +++ b/apps/emqx_machine/src/emqx_machine_schema.erl @@ -43,7 +43,7 @@ %% by nodetool to generate app.)Xr}=W0 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