diff --git a/.gitignore b/.gitignore index 3ea23db6f..0d3edca80 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ test/ebin/*.beam .exrc plugins/*/ebin log/ +*.swp diff --git a/apps/emqttd/include/emqttd.hrl b/apps/emqttd/include/emqttd.hrl index a32732af1..ed74dff13 100644 --- a/apps/emqttd/include/emqttd.hrl +++ b/apps/emqttd/include/emqttd.hrl @@ -59,19 +59,12 @@ -type mqtt_session() :: #mqtt_session{}. -%%------------------------------------------------------------------------------ -%% MQTT Retained Message -%%------------------------------------------------------------------------------ --record(mqtt_retained, {topic, qos, payload}). - --type mqtt_retained() :: #mqtt_retained{}. - %%------------------------------------------------------------------------------ %% MQTT User Management %%------------------------------------------------------------------------------ -record(mqtt_user, { clientid :: binary(), - peername :: list(), + ipaddr :: inet:ip_address(), username :: binary(), password :: binary() }). @@ -93,3 +86,10 @@ -record(mqtt_plugin, {name, version, attrs, description}). +%%------------------------------------------------------------------------------ +%% MQTT Retained Message +%%------------------------------------------------------------------------------ +-record(mqtt_retained, {topic, qos, payload}). + +-type mqtt_retained() :: #mqtt_retained{}. + diff --git a/apps/emqttd/src/emqttd_access.erl b/apps/emqttd/src/emqttd_access.erl deleted file mode 100644 index 01a59d3c0..000000000 --- a/apps/emqttd/src/emqttd_access.erl +++ /dev/null @@ -1,70 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @Copyright (C) 2012-2015, Feng Lee -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% emqttd access rule match -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_access). - --include("emqttd.hrl"). - --export([match/3]). - --type who() :: all | - {clientid, binary()} | - {peername, string() | inet:ip_address()} | - {username, binary()}. - --type rule() :: {allow, all} | - {allow, who(), binary()} | - {deny, all} | - {deny, who(), binary()}. - --spec match(mqtt_user(), binary(), list(rule())) -> allow | deny | nomatch. -match(_User, _Topic, []) -> - nomatch; -match(_User, _Topic, [{AllowDeny, all}|_]) -> - AllowDeny; -match(User, Topic, [{AllowDeny, all, TopicFilter}|Rules]) -> - case emqttd_topic:match(Topic, TopicFilter) of - true -> AllowDeny; - false -> match(User, Topic, Rules) - end; -match(User = #mqtt_user{clientid = ClientId}, Topic, [{AllowDeny, ClientId, TopicFilter}|Rules]) when is_binary(ClientId) -> - case emqttd_topic:match(Topic, TopicFilter) of - true -> AllowDeny; - false -> match(User, Topic, Rules) - end; -match(User = #mqtt_user{peername = IpAddr}, Topic, [{AllowDeny, {peername, CIDR}, TopicFilter}|Rules]) -> - case {match_cidr(IpAddr, CIDR), emqttd_topic:match(Topic, TopicFilter)} of - {true, true} -> AllowDeny; - _ -> match(User, Topic, Rules) - end; -match(User = #mqtt_user{username = Username}, Topic, [{AllowDeny, {username, Username}, TopicFilter}|Rules]) -> - case emqttd_topic:match(Topic, TopicFilter) of - true -> AllowDeny; - false -> match(User, Topic, Rules) - end. - -match_cidr(IpAddr, CIDR) -> true. - diff --git a/apps/emqttd/src/emqttd_acl.erl b/apps/emqttd/src/emqttd_acl.erl index d1b5f235f..1b57ee239 100644 --- a/apps/emqttd/src/emqttd_acl.erl +++ b/apps/emqttd/src/emqttd_acl.erl @@ -27,8 +27,6 @@ %%% subscribe topic %%% publish to topic %%% -%%% TODO: Support regexp... -%%% %%% @end %%%----------------------------------------------------------------------------- -module(emqttd_acl). @@ -37,33 +35,26 @@ -include("emqttd.hrl"). +-callback check_acl(PubSub, User, Topic) -> {ok, allow | deny} | ignore | {error, any()} when + PubSub :: pubsub(), + User :: mqtt_user(), + Topic :: binary(). + +-callback reload_acl() -> ok | {error, any()}. + -behaviour(gen_server). -define(SERVER, ?MODULE). +%% API Function Exports +-export([start_link/1, check/3, reload/0, register_mod/1, unregister_mod/1]). + +%% ACL Callback +-export([check_acl/3, reload_acl/0]). + -define(ACL_TAB, mqtt_acl). -%% API Function Exports --export([start_link/0, check/3, allow/3, deny/3]). - -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - --type pubsub() :: publish | subscribe. - --type who() :: all | binary() | - {clientid, binary()} | - {peername, string() | inet:ip_address()} | - {username, binary()}. - --type rule() :: {allow, all} | - {allow, who(), binary()} | - {deny, all} | - {deny, who(), binary()}. - --record(mqtt_acl, {pubsub :: pubsub(), - rules :: list(rule())}). +-record(state, {acl_file, raw_rules = []}). %%%============================================================================= %%% API @@ -75,91 +66,104 @@ %% %% @end %%------------------------------------------------------------------------------ --spec start_link() -> {ok, pid()} | ignore | {error, any()}. -start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). +-spec start_link(AclOpts) -> {ok, pid()} | ignore | {error, any()} when + AclOpts :: [{file, list()}]. +start_link(AclOpts) -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [AclOpts], []). --spec check(PubSub, User, Topic) -> allowed | refused when +-spec check(PubSub, User, Topic) -> allow | deny when PubSub :: pubsub(), User :: mqtt_user(), Topic :: binary(). check(PubSub, User, Topic) -> - case match(User, Topic, lookup(PubSub)) of - nomatch -> allowed; - allow -> allow; - deny -> deny + case ets:lookup(?ACL_TAB, acl_mods) of + [] -> {error, "No ACL mod!"}; + [{_, Mods}] -> check(PubSub, User, Topic, Mods) + end. +check(_PubSub, _User, _Topic, []) -> + {error, "All ACL mods ignored!"}; +check(PubSub, User, Topic, [Mod|Mods]) -> + case Mod:check_acl(PubSub, User, Topic) of + {ok, AllowDeny} -> AllowDeny; + ignore -> check(PubSub, User, Topic, Mods) end. -lookup(PubSub) -> - case ets:lookup(?ACL_TAB, PubSub) of - [] -> []; - [#mqtt_acl{pubsub = PubSub, rules = Rules}] -> Rules +%%TODO: +reload() -> + case ets:lookup(?ACL_TAB, acl_mods) of + [] -> {error, "No ACL mod!"}; + [{_, Mods}] -> [M:reload() || M <- Mods] end. -match(_User, _Topic, []) -> - nomatch; -match(User, Topic, Rules) -> - %TODO:... - nomatch. +register_mod(Mod) -> + gen_server:call(?MODULE, {register_mod, Mod}). --spec allow(PubSub, Who, Topic) -> ok | {error, any()} when - PubSub :: pubsub(), - Who :: who(), - Topic :: binary(). -allow(PubSub, Who, Topic) -> - add_rule(PubSub, {allow, Who, Topic}). +unregister_mod(Mod) -> + gen_server:call(?MODULE, {unregister_mod, Mod}). --spec deny(PubSub, Who, Topic) -> ok | {error, any()} when - PubSub :: pubsub(), - Who :: who(), - Topic :: binary(). -deny(PubSub, Who, Topic) -> - add_rule(PubSub, {deny, Who, Topic}). - -add_rule(PubSub, RawRule) -> - case rule(RawRule) of - {error, Error} -> - {error, Error}; - Rule -> - F = fun() -> - case mnesia:wread(?ACL_TAB, PubSub) of - [] -> - mnesia:write(?ACL_TAB, #mqtt_acl{pubsub = PubSub, rules = [Rule]}); - [Rules] -> - mnesia:write(?ACL_TAB, #mqtt_acl{pubsub = PubSub, rules = [Rule|Rules]}) - end - end, - case mnesia:transaction(F) of - {atomic, _} -> ok; - {aborted, Reason} -> {error, {aborted, Reason}} - end - end. - -%% TODO: --spec rule(rule()) -> rule(). -rule({allow, all}) -> - {allow, all}; -rule({allow, Who, Topic}) -> - {allow, Who, Topic}; -rule({deny, Who, Topic}) -> - {deny, Who, Topic}; -rule({deny, all}) -> - {deny, all}. %% ------------------------------------------------------------------ %% gen_server Function Definitions %% ------------------------------------------------------------------ -init(Args) -> - mnesia:create_table(?ACL_TAB, [ - {type, set}, - {record_name, mqtt_acl}, - {ram_copies, [node()]}, - {attributes, record_info(fields, mqtt_acl)}]), - mnesia:add_table_copy(?ACL_TAB, node(), ram_copies), - {ok, Args}. +init([AclOpts]) -> + ets:new(?ACL_TAB, [set, protected, named_table]), + ets:insert(?ACL_TAB, {acl_mods, [?MODULE]}), + AclFile = proplists:get_value(file, AclOpts), + load_rules(#state{acl_file = AclFile}). -handle_call(_Request, _From, State) -> - {reply, error, State}. +load_rules(State = #state{acl_file = AclFile}) -> + {ok, Terms} = file:consult(AclFile), + Rules = [compile(Term) || Term <- Terms], + lists:foreach(fun(PubSub) -> + ets:insert(?ACL_TAB, {PubSub, + lists:filter(fun(Rule) -> filter(PubSub, Rule) end, Rules)}) + end, [publish, subscribe]), + {ok, State#state{raw_rules = Terms}}. + +filter(_PubSub, {allow, all}) -> + true; +filter(_PubSub, {deny, all}) -> + true; +filter(publish, {_AllowDeny, _Who, publish, _Topics}) -> + true; +filter(_PubSub, {_AllowDeny, _Who, pubsub, _Topics}) -> + true; +filter(subscribe, {_AllowDeny, _Who, subscribe, _Topics}) -> + true; +filter(_PubSub, {_AllowDeny, _Who, _, _Topics}) -> + false. + +handle_call(reload, _From, State) -> + case catch load_rules(State) of + {ok, NewState} -> + {reply, ok, NewState}; + {'EXIT', Error} -> + {reply, {error, Error}, State} + end; + +handle_call({register_mod, Mod}, _From, State) -> + [{_, Mods}] = ets:lookup(?ACL_TAB, acl_mods), + case lists:member(Mod, Mods) of + true -> + {reply, {error, registered}, State}; + false -> + ets:insert(?ACL_TAB, {acl_mods, [Mod|Mods]}), + {reply, ok, State} + end; + +handle_call({unregister_mod, Mod}, _From, State) -> + [{_, Mods}] = ets:lookup(?ACL_TAB, acl_mods), + case lists:member(Mod, Mods) of + true -> + ets:insert(?ACL_TAB, lists:delete(Mod, Mods)), + {reply, ok, State}; + false -> + {reply, {error, not_found}, State} + end; + +handle_call(Req, _From, State) -> + lager:error("Bad Request: ~p", [Req]), + {reply, {error, badreq}, State}. handle_cast(_Msg, State) -> {noreply, State}. @@ -173,9 +177,8 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%% ------------------------------------------------------------------ -%% Internal Function Definitions -%% ------------------------------------------------------------------ - +%%%============================================================================= +%%% Internal functions +%%%============================================================================= diff --git a/apps/emqttd/src/emqttd_acl_internal.erl b/apps/emqttd/src/emqttd_acl_internal.erl new file mode 100644 index 000000000..801663ec3 --- /dev/null +++ b/apps/emqttd/src/emqttd_acl_internal.erl @@ -0,0 +1,163 @@ +%%%----------------------------------------------------------------------------- +%%% @Copyright (C) 2012-2015, Feng Lee +%%% +%%% Permission is hereby granted, free of charge, to any person obtaining a copy +%%% of this software and associated documentation files (the "Software"), to deal +%%% in the Software without restriction, including without limitation the rights +%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%%% copies of the Software, and to permit persons to whom the Software is +%%% furnished to do so, subject to the following conditions: +%%% +%%% The above copyright notice and this permission notice shall be included in all +%%% copies or substantial portions of the Software. +%%% +%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%%% SOFTWARE. +%%%----------------------------------------------------------------------------- +%%% @doc +%%% Internal ACL that load rules from etc/acl.config +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(emqttd_acl_internal). + +-author('feng@emqtt.io'). + +-include("emqttd.hrl"). + +-define(SERVER, ?MODULE). + +-behaviour(gen_server). + +-export([start_link/1]). + +%% acl callbacks +-export([check_acl/3, reload_acl/0]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +%%%============================================================================= +%%% API +%%%============================================================================= + +%%------------------------------------------------------------------------------ +%% @doc +%% Start Internal ACL Server. +%% +%% @end +%%------------------------------------------------------------------------------ +-spec start_link(AclOpts) -> {ok, pid()} | ignore | {error, any()} when + AclOpts :: [{file, list()}]. +start_link(AclOpts) -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [AclOpts], []). + +-spec check_acl(PubSub, User, Topic) -> {ok, allow} | {ok, deny} | ignore | {error, any()} when + PubSub :: pubsub(), + User :: mqtt_user(), + Topic :: binary(). +check_acl(PubSub, User, Topic) -> + case match(User, Topic, lookup(PubSub)) of + {matched, allow} -> {ok, allow}; + {matched, deny} -> {ok, deny}; + nomatch -> {error, nomatch} + end. + +-spec reload_acl() -> ok. +reload_acl() -> + gen_server:call(?SERVER, reload). + +lookup(PubSub) -> + case ets:lookup(?ACL_TAB, PubSub) of + [] -> []; + [{PubSub, Rules}] -> Rules + end. + +match(_User, _Topic, []) -> + nomatch; + +match(User, Topic, [Rule|Rules]) -> + case match_rule(User, Topic, Rule) of + nomatch -> match(User, Topic, Rules); + {matched, AllowDeny} -> {matched, AllowDeny} + end. + + + +%% ------------------------------------------------------------------ +%% gen_server Function Definitions +%% ------------------------------------------------------------------ +init([AclOpts]) -> + ets:new(?ACL_TAB, [set, protected, named_table]), + ets:insert(?ACL_TAB, {acl_mods, [?MODULE]}), + AclFile = proplists:get_value(file, AclOpts), + load_rules(#state{acl_file = AclFile}). + +load_rules(State = #state{acl_file = AclFile}) -> + {ok, Terms} = file:consult(AclFile), + Rules = [compile(Term) || Term <- Terms], + lists:foreach(fun(PubSub) -> + ets:insert(?ACL_TAB, {PubSub, + lists:filter(fun(Rule) -> filter(PubSub, Rule) end, Rules)}) + end, [publish, subscribe]), + {ok, State#state{raw_rules = Terms}}. + +filter(_PubSub, {allow, all}) -> + true; +filter(_PubSub, {deny, all}) -> + true; +filter(publish, {_AllowDeny, _Who, publish, _Topics}) -> + true; +filter(_PubSub, {_AllowDeny, _Who, pubsub, _Topics}) -> + true; +filter(subscribe, {_AllowDeny, _Who, subscribe, _Topics}) -> + true; +filter(_PubSub, {_AllowDeny, _Who, _, _Topics}) -> + false. + +handle_call(reload, _From, State) -> + case catch load_rules(State) of + {ok, NewState} -> + {reply, ok, NewState}; + {'EXIT', Error} -> + {reply, {error, Error}, State} + end; + +handle_call({register_mod, Mod}, _From, State) -> + [{_, Mods}] = ets:lookup(?ACL_TAB, acl_mods), + case lists:member(Mod, Mods) of + true -> + {reply, {error, registered}, State}; + false -> + ets:insert(?ACL_TAB, {acl_mods, [Mod|Mods]}), + {reply, ok, State} + end; + +handle_call({unregister_mod, Mod}, _From, State) -> + [{_, Mods}] = ets:lookup(?ACL_TAB, acl_mods), + case lists:member(Mod, Mods) of + true -> + ets:insert(?ACL_TAB, lists:delete(Mod, Mods)), + {reply, ok, State}; + false -> + {reply, {error, not_found}, State} + end; + +handle_call(Req, _From, State) -> + lager:error("Bad Request: ~p", [Req]), + {reply, {error, badreq}, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. diff --git a/apps/emqttd/src/emqttd_acl_rule.erl b/apps/emqttd/src/emqttd_acl_rule.erl new file mode 100644 index 000000000..bbc119b2b --- /dev/null +++ b/apps/emqttd/src/emqttd_acl_rule.erl @@ -0,0 +1,152 @@ +%%%----------------------------------------------------------------------------- +%%% @Copyright (C) 2012-2015, Feng Lee +%%% +%%% Permission is hereby granted, free of charge, to any person obtaining a copy +%%% of this software and associated documentation files (the "Software"), to deal +%%% in the Software without restriction, including without limitation the rights +%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%%% copies of the Software, and to permit persons to whom the Software is +%%% furnished to do so, subject to the following conditions: +%%% +%%% The above copyright notice and this permission notice shall be included in all +%%% copies or substantial portions of the Software. +%%% +%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%%% SOFTWARE. +%%%----------------------------------------------------------------------------- +%%% @doc +%%% emqttd ACL rule. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(emqttd_acl_rule). + +-include("emqttd.hrl"). + +-type pubsub() :: subscribe | publish | pubsub. + +-type who() :: all | binary() | + {ipaddr, esockd_access:cidr()} | + {client, binary()} | + {user, binary()}. + +-type rule() :: {allow, all} | + {allow, who(), pubsub(), list(binary())} | + {deny, all} | + {deny, who(), pubsub(), list(binary())}. + +-export_type([pubsub/0]). + +-export([compile/1, match/3]). + +%%%----------------------------------------------------------------------------- +%% @doc +%% Compile rule. +%% +%% @end +%%%----------------------------------------------------------------------------- +compile({A, all}) when (A =:= allow) orelse (A =:= deny) -> + {A, all}; + +compile({A, Who, PubSub, TopicFilters}) when (A =:= allow) orelse (A =:= deny) -> + {A, compile(who, Who), PubSub, [compile(topic, bin(Topic)) || Topic <- TopicFilters]}. + +compile(who, all) -> + all; +compile(who, {ipaddr, CIDR}) -> + {Start, End} = esockd_access:range(CIDR), + {ipaddr, {CIDR, Start, End}}; +compile(who, {client, all}) -> + {client, all}; +compile(who, {client, ClientId}) -> + {client, bin(ClientId)}; +compile(who, {user, all}) -> + {user, all}; +compile(who, {user, Username}) -> + {user, bin(Username)}; + +compile(topic, Topic) -> + Words = emqttd_topic:words(Topic), + case pattern(Words) of + true -> {pattern, Words}; + false -> Words + end. + +pattern(Words) -> + lists:member(<<"$u">>, Words) + orelse lists:member(<<"$c">>, Words). + +bin(L) when is_list(L) -> + list_to_binary(L); +bin(B) when is_binary(B) -> + B. + +%%%----------------------------------------------------------------------------- +%% @doc +%% Match rule. +%% +%% @end +%%%----------------------------------------------------------------------------- +-spec match(mqtt_user(), binary(), rule()) -> {matched, allow} | {matched, deny} | nomatch. +match(_User, _Topic, {AllowDeny, all}) when (AllowDeny =:= allow) orelse (AllowDeny =:= deny) -> + {matched, AllowDeny}; +match(User, Topic, {AllowDeny, Who, _PubSub, TopicFilters}) + when (AllowDeny =:= allow) orelse (AllowDeny =:= deny) -> + case match_who(User, Who) andalso match_topics(User, Topic, TopicFilters) of + true -> {matched, AllowDeny}; + false -> nomatch + end. + +match_who(_User, all) -> + true; +match_who(_User, {user, all}) -> + true; +match_who(_User, {client, all}) -> + true; +match_who(#mqtt_user{clientid = ClientId}, {client, ClientId}) -> + true; +match_who(#mqtt_user{username = Username}, {user, Username}) -> + true; +match_who(#mqtt_user{ipaddr = IP}, {ipaddr, {_CDIR, Start, End}}) -> + I = esockd_access:atoi(IP), + I >= Start andalso I =< End; +match_who(_User, _Who) -> + false. + +match_topics(_User, _Topic, []) -> + false; +match_topics(User, Topic, [{pattern, PatternFilter}|Filters]) -> + TopicFilter = feed_var(User, PatternFilter), + case match_topic(emqttd_topic:words(Topic), TopicFilter) of + true -> true; + false -> match_topics(User, Topic, Filters) + end; +match_topics(User, Topic, [TopicFilter|Filters]) -> + case match_topic(emqttd_topic:words(Topic), TopicFilter) of + true -> true; + false -> match_topics(User, Topic, Filters) + end. + +match_topic(Topic, TopicFilter) -> + emqttd_topic:match(Topic, TopicFilter). + +feed_var(User, Pattern) -> + feed_var(User, Pattern, []). +feed_var(_User, [], Acc) -> + lists:reverse(Acc); +feed_var(User = #mqtt_user{clientid = undefined}, [<<"$c">>|Words], Acc) -> + feed_var(User, Words, [<<"$c">>|Acc]); +feed_var(User = #mqtt_user{clientid = ClientId}, [<<"$c">>|Words], Acc) -> + feed_var(User, Words, [ClientId |Acc]); +feed_var(User = #mqtt_user{username = undefined}, [<<"$u">>|Words], Acc) -> + feed_var(User, Words, [<<"$u">>|Acc]); +feed_var(User = #mqtt_user{username = Username}, [<<"$u">>|Words], Acc) -> + feed_var(User, Words, [Username|Acc]); +feed_var(User, [W|Words], Acc) -> + feed_var(User, Words, [W|Acc]). + diff --git a/apps/emqttd/src/emqttd_topic.erl b/apps/emqttd/src/emqttd_topic.erl index 3360d086f..6b1c3dc7a 100644 --- a/apps/emqttd/src/emqttd_topic.erl +++ b/apps/emqttd/src/emqttd_topic.erl @@ -1,82 +1,66 @@ -%%----------------------------------------------------------------------------- -%% Copyright (c) 2012-2015, Feng Lee -%% -%% Permission is hereby granted, free of charge, to any person obtaining a copy -%% of this software and associated documentation files (the "Software"), to deal -%% in the Software without restriction, including without limitation the rights -%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%% copies of the Software, and to permit persons to whom the Software is -%% furnished to do so, subject to the following conditions: -%% -%% The above copyright notice and this permission notice shall be included in all -%% copies or substantial portions of the Software. -%% -%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%% SOFTWARE. -%%------------------------------------------------------------------------------ - +%%%----------------------------------------------------------------------------- +%%% @Copyright (C) 2012-2015, Feng Lee +%%% +%%% Permission is hereby granted, free of charge, to any person obtaining a copy +%%% of this software and associated documentation files (the "Software"), to deal +%%% in the Software without restriction, including without limitation the rights +%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%%% copies of the Software, and to permit persons to whom the Software is +%%% furnished to do so, subject to the following conditions: +%%% +%%% The above copyright notice and this permission notice shall be included in all +%%% copies or substantial portions of the Software. +%%% +%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%%% SOFTWARE. +%%%----------------------------------------------------------------------------- +%%% @doc +%%% emqttd topic. +%%% +%%% @end +%%%----------------------------------------------------------------------------- -module(emqttd_topic). -author('feng@emqtt.io'). --import(lists, [reverse/1]). - -%% ------------------------------------------------------------------------ -%% Topic semantics and usage -%% ------------------------------------------------------------------------ -%% A topic must be at least one character long. -%% -%% Topic names are case sensitive. For example, ACCOUNTS and Accounts are two different topics. -%% -%% Topic names can include the space character. For example, Accounts payable is a valid topic. -%% -%% A leading "/" creates a distinct topic. For example, /finance is different from finance. /finance matches "+/+" and "/+", but not "+". -%% -%% Do not include the null character (Unicode \x0000) in any topic. -%% -%% The following principles apply to the construction and content of a topic tree: -%% -%% The length is limited to 64k but within that there are no limits to the number of levels in a topic tree. -%% -%% There can be any number of root nodes; that is, there can be any number of topic trees. -%% ------------------------------------------------------------------------ - -include("emqttd_topic.hrl"). + +-import(lists, [reverse/1]). -export([new/1, type/1, match/2, validate/1, triples/1, words/1]). -%%---------------------------------------------------------------------------- +-type word() :: '' | '+' | '#' | binary(). --ifdef(use_specs). +-type words() :: list(word()). --spec new( binary() ) -> topic(). +-type triple() :: {root | binary(), word(), binary()}. --spec type(topic() | binary()) -> direct | wildcard. - --spec match(binary(), binary()) -> boolean(). - --spec validate({name | filter, binary()}) -> boolean(). - --endif. - -%%---------------------------------------------------------------------------- +-export_type([word/0, triple/0]). -define(MAX_TOPIC_LEN, 65535). -%% ------------------------------------------------------------------------ +%%%----------------------------------------------------------------------------- +%% @doc %% New Topic -%% ------------------------------------------------------------------------ +%% +%% @end +%%%----------------------------------------------------------------------------- +-spec new(binary()) -> topic(). new(Name) when is_binary(Name) -> - #topic{ name = Name, node = node() }. + #topic{name = Name, node = node()}. -%% ------------------------------------------------------------------------ +%%%----------------------------------------------------------------------------- +%% @doc %% Topic Type: direct or wildcard -%% ------------------------------------------------------------------------ +%% +%% @end +%%%----------------------------------------------------------------------------- +-spec type(topic() | binary()) -> direct | wildcard. type(#topic{ name = Name }) when is_binary(Name) -> type(Name); type(Topic) when is_binary(Topic) -> @@ -91,9 +75,15 @@ type2(['+'|_]) -> type2([_H |T]) -> type2(T). -%% ------------------------------------------------------------------------ -%% Match Topic. B1 is Topic Name, B2 is Topic Filter. -%% ------------------------------------------------------------------------ +%%%----------------------------------------------------------------------------- +%% @doc +%% Match Topic name with filter. +%% +%% @end +%%%----------------------------------------------------------------------------- +-spec match(Name, Filter) -> boolean() when + Name :: binary() | words(), + Filter :: binary() | words(). match(Name, Filter) when is_binary(Name) and is_binary(Filter) -> match(words(Name), words(Filter)); match([], []) -> @@ -115,9 +105,13 @@ match([_H1|_], []) -> match([], [_H|_T2]) -> false. -%% ------------------------------------------------------------------------ -%% Validate Topic -%% ------------------------------------------------------------------------ +%%%----------------------------------------------------------------------------- +%% @doc +%% Validate Topic +%% +%% @end +%%%----------------------------------------------------------------------------- +-spec validate({name | filter, binary()}) -> boolean(). validate({_, <<>>}) -> false; validate({_, Topic}) when is_binary(Topic) and (size(Topic) > ?MAX_TOPIC_LEN) -> @@ -156,9 +150,13 @@ include_wildcard(['#'|_T]) -> true; include_wildcard(['+'|_T]) -> true; include_wildcard([ _ | T]) -> include_wildcard(T). -%% ------------------------------------------------------------------------ -%% Topic to Triples -%% ------------------------------------------------------------------------ +%%%----------------------------------------------------------------------------- +%% @doc +%% Topic to Triples. +%% +%% @end +%%%----------------------------------------------------------------------------- +-spec triples(binary()) -> list(triple()). triples(Topic) when is_binary(Topic) -> triples(words(Topic), root, []). @@ -177,11 +175,15 @@ join(Parent, W) -> bin('') -> <<>>; bin('+') -> <<"+">>; bin('#') -> <<"#">>; -bin( B ) when is_binary(B) -> B. +bin(B) when is_binary(B) -> B. -%% ------------------------------------------------------------------------ -%% Split Topic to Words -%% ------------------------------------------------------------------------ +%%%----------------------------------------------------------------------------- +%% @doc +%% Split Topic to Words. +%% +%% @end +%%%----------------------------------------------------------------------------- +-spec words(binary()) -> words(). words(Topic) when is_binary(Topic) -> [word(W) || W <- binary:split(Topic, <<"/">>, [global])]. diff --git a/apps/emqttd/test/emqttd_access_tests.erl b/apps/emqttd/test/emqttd_access_tests.erl deleted file mode 100644 index 3bb38caab..000000000 --- a/apps/emqttd/test/emqttd_access_tests.erl +++ /dev/null @@ -1,45 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @Copyright (C) 2012-2015, Feng Lee -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% emqttd_access rules tests. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_access_tests). - --include("emqttd.hrl"). - --ifdef(TEST). - --include_lib("eunit/include/eunit.hrl"). - --define(RULES1, [{allow, all}]). --define(RULES2, [{deny, all}]). - -match_test() -> - User = #mqtt_user{peername = {127,0,0,1}, clientid = <<"testClient">>, username = <<"TestUser">>}, - ?assertEqual(allow, emqttd_access:match(User, <<"Test/Topic">>, ?RULES1)), - ?assertEqual(deny, emqttd_access:match(User, <<"Test/Topic">>, ?RULES2)). - --endif. - - diff --git a/apps/emqttd/test/emqttd_acl_tests.erl b/apps/emqttd/test/emqttd_acl_tests.erl new file mode 100644 index 000000000..22619878c --- /dev/null +++ b/apps/emqttd/test/emqttd_acl_tests.erl @@ -0,0 +1,78 @@ +%%%----------------------------------------------------------------------------- +%%% @Copyright (C) 2012-2015, Feng Lee +%%% +%%% Permission is hereby granted, free of charge, to any person obtaining a copy +%%% of this software and associated documentation files (the "Software"), to deal +%%% in the Software without restriction, including without limitation the rights +%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%%% copies of the Software, and to permit persons to whom the Software is +%%% furnished to do so, subject to the following conditions: +%%% +%%% The above copyright notice and this permission notice shall be included in all +%%% copies or substantial portions of the Software. +%%% +%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%%% SOFTWARE. +%%%----------------------------------------------------------------------------- +%%% @doc +%%% emqttd_acl tests. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(emqttd_acl_tests). + +-include("emqttd.hrl"). + +-ifdef(TEST). + +-include_lib("eunit/include/eunit.hrl"). + +compile_test() -> + ?assertMatch({allow, {ipaddr, {"127.0.0.1", _I, _I}}, subscribe, [ [<<"$SYS">>, '#'], ['#'] ]}, + emqttd_acl:compile({allow, {ipaddr, "127.0.0.1"}, subscribe, ["$SYS/#", "#"]})), + ?assertMatch({allow, {user, <<"testuser">>}, subscribe, [ [<<"a">>, <<"b">>, <<"c">>], [<<"d">>, <<"e">>, <<"f">>, '#'] ]}, + emqttd_acl:compile({allow, {user, "testuser"}, subscribe, ["a/b/c", "d/e/f/#"]})), + ?assertEqual({allow, {user, <<"admin">>}, pubsub, [ [<<"d">>, <<"e">>, <<"f">>, '#'] ]}, + emqttd_acl:compile({allow, {user, "admin"}, pubsub, ["d/e/f/#"]})), + ?assertEqual({allow, {client, <<"testClient">>}, publish, [ [<<"testTopics">>, <<"testClient">>] ]}, + emqttd_acl:compile({allow, {client, "testClient"}, publish, ["testTopics/testClient"]})), + ?assertEqual({allow, all, pubsub, [{pattern, [<<"clients">>, <<"$c">>]}]}, + emqttd_acl:compile({allow, all, pubsub, ["clients/$c"]})), + ?assertEqual({allow, all, subscribe, [{pattern, [<<"users">>, <<"$u">>, '#']}]}, + emqttd_acl:compile({allow, all, subscribe, ["users/$u/#"]})), + ?assertEqual({deny, all, subscribe, [ [<<"$SYS">>, '#'], ['#'] ]}, + emqttd_acl:compile({deny, all, subscribe, ["$SYS/#", "#"]})), + ?assertEqual({allow, all}, emqttd_acl:compile({allow, all})), + ?assertEqual({deny, all}, emqttd_acl:compile({deny, all})). + +match_test() -> + User = #mqtt_user{ipaddr = {127,0,0,1}, clientid = <<"testClient">>, username = <<"TestUser">>}, + User2 = #mqtt_user{ipaddr = {192,168,0,10}, clientid = <<"testClient">>, username = <<"TestUser">>}, + + ?assertEqual({matched, allow}, emqttd_acl:match(User, <<"Test/Topic">>, [{allow, all}])), + ?assertEqual({matched, deny}, emqttd_acl:match(User, <<"Test/Topic">>, [{deny, all}])), + ?assertMatch({matched, allow}, emqttd_acl:match(User, <<"Test/Topic">>, + emqttd_acl:compile({allow, {ipaddr, "127.0.0.1"}, subscribe, ["$SYS/#", "#"]}))), + ?assertMatch({matched, allow}, emqttd_acl:match(User2, <<"Test/Topic">>, + emqttd_acl:compile({allow, {ipaddr, "192.168.0.1/24"}, subscribe, ["$SYS/#", "#"]}))), + ?assertMatch({matched, allow}, emqttd_acl:match(User, <<"d/e/f/x">>, + emqttd_acl:compile({allow, {user, "TestUser"}, subscribe, ["a/b/c", "d/e/f/#"]}))), + ?assertEqual(nomatch, emqttd_acl:match(User, <<"d/e/f/x">>, emqttd_access:compile({allow, {user, "admin"}, pubsub, ["d/e/f/#"]}))), + ?assertMatch({matched, allow}, emqttd_acl:match(User, <<"testTopics/testClient">>, + emqttd_acl:compile({allow, {client, "testClient"}, publish, ["testTopics/testClient"]}))), + ?assertMatch({matched, allow}, emqttd_acl:match(User, <<"clients/testClient">>, + emqttd_acl:compile({allow, all, pubsub, ["clients/$c"]}))), + ?assertMatch({matched, allow}, emqttd_acl:match(#mqtt_user{username = <<"user2">>}, <<"users/user2/abc/def">>, + emqttd_acl:compile({allow, all, subscribe, ["users/$u/#"]}))), + ?assertMatch({matched, deny}, + emqttd_acl:match(User, <<"d/e/f">>, + emqttd_acl:compile({deny, all, subscribe, ["$SYS/#", "#"]}))). + +-endif. + + diff --git a/apps/emqttd/test/esockd_access.erl b/apps/emqttd/test/esockd_access.erl new file mode 100644 index 000000000..1c29d5357 --- /dev/null +++ b/apps/emqttd/test/esockd_access.erl @@ -0,0 +1,152 @@ +%%%----------------------------------------------------------------------------- +%%% @Copyright (C) 2014-2015, Feng Lee +%%% +%%% Permission is hereby granted, free of charge, to any person obtaining a copy +%%% of this software and associated documentation files (the "Software"), to deal +%%% in the Software without restriction, including without limitation the rights +%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%%% copies of the Software, and to permit persons to whom the Software is +%%% furnished to do so, subject to the following conditions: +%%% +%%% The above copyright notice and this permission notice shall be included in all +%%% copies or substantial portions of the Software. +%%% +%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%%% SOFTWARE. +%%%----------------------------------------------------------------------------- +%%% @doc +%%% esockd access control. +%%% +%%% CIDR: Classless Inter-Domain Routing +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(esockd_access). + +-type cidr() :: string(). + +-type rule() :: {allow, all} | + {allow, cidr()} | + {deny, all} | + {deny, cidr()}. + +-type range() :: {cidr(), pos_integer(), pos_integer()}. + +-export_type([cidr/0, range/0, rule/0]). + +-type range_rule() :: {allow, all} | + {allow, range()} | + {deny, all} | + {deny, range()}. + +-export([rule/1, match/2, range/1, mask/1, atoi/1, itoa/1]). + +%%------------------------------------------------------------------------------ +%% @doc +%% Build CIDR, Make rule. +%% +%% @end +%%------------------------------------------------------------------------------ +-spec rule(rule()) -> range_rule(). +rule({allow, all}) -> + {allow, all}; +rule({allow, CIDR}) when is_list(CIDR) -> + rule(allow, CIDR); +rule({deny, CIDR}) when is_list(CIDR) -> + rule(deny, CIDR); +rule({deny, all}) -> + {deny, all}. + +rule(Type, CIDR) when is_list(CIDR) -> + {Start, End} = range(CIDR), {Type, {CIDR, Start, End}}. + +%%------------------------------------------------------------------------------ +%% @doc +%% Match Addr with Access Rules. +%% +%% @end +%%------------------------------------------------------------------------------ +-spec match(inet:ip_address(), [range_rule()]) -> {matched, allow} | {matched, deny} | nomatch. +match(Addr, Rules) when is_tuple(Addr) -> + match2(atoi(Addr), Rules). + +match2(_I, []) -> + nomatch; +match2(_I, [{allow, all}|_]) -> + {matched, allow}; +match2(I, [{allow, {_, Start, End}}|_]) when I >= Start, I =< End -> + {matched, allow}; +match2(I, [{allow, {_, _Start, _End}}|Rules]) -> + match2(I, Rules); +match2(I, [{deny, {_, Start, End}}|_]) when I >= Start, I =< End -> + {matched, deny}; +match2(I, [{deny, {_, _Start, _End}}|Rules]) -> + match2(I, Rules); +match2(_I, [{deny, all}|_]) -> + {matched, deny}. + +%%------------------------------------------------------------------------------ +%% @doc +%% CIDR range. +%% +%% @end +%%------------------------------------------------------------------------------ +-spec range(cidr()) -> {pos_integer(), pos_integer()}. +range(CIDR) -> + case string:tokens(CIDR, "/") of + [Addr] -> + {ok, IP} = inet:getaddr(Addr, inet), + {atoi(IP), atoi(IP)}; + [Addr, Mask] -> + {ok, IP} = inet:getaddr(Addr, inet), + {Start, End} = subnet(IP, mask(list_to_integer(Mask))), + {Start, End} + end. + +subnet(IP, Mask) -> + Start = atoi(IP) band Mask, + End = Start bor (Mask bxor 16#FFFFFFFF), + {Start, End}. + +%%------------------------------------------------------------------------------ +%% @doc +%% Mask Int +%% +%% @end +%%------------------------------------------------------------------------------ +-spec mask(0..32) -> 0..16#FFFFFFFF. +mask(0) -> + 16#00000000; +mask(32) -> + 16#FFFFFFFF; +mask(N) when N >= 1, N =< 31 -> + lists:foldl(fun(I, Mask) -> (1 bsl I) bor Mask end, 0, lists:seq(32 - N, 31)). + +%%------------------------------------------------------------------------------ +%% @doc +%% Addr to Integer. +%% +%% @end +%%------------------------------------------------------------------------------ +atoi({A, B, C, D}) -> + (A bsl 24) + (B bsl 16) + (C bsl 8) + D. + +%%------------------------------------------------------------------------------ +%% @doc +%% Integer to Addr. +%% +%% @end +%%------------------------------------------------------------------------------ +itoa(I) -> + A = (I bsr 24) band 16#FF, + B = (I bsr 16) band 16#FF, + C = (I bsr 8) band 16#FF, + D = I band 16#FF, + {A, B, C, D}. + + diff --git a/rel/files/acl.config b/rel/files/acl.config new file mode 100644 index 000000000..e408d1f68 --- /dev/null +++ b/rel/files/acl.config @@ -0,0 +1,16 @@ +{allow, {ipaddr, "127.0.0.1"}, subscribe, ["$SYS/#", "#"]}. + +{allow, {user, "testuser"}, subscribe, ["a/b/c", "d/e/f/#"]}. + +{allow, {user, "admin"}, pubsub, ["a/b/c", "d/e/f/#"]}. + +{allow, {client, "testClient"}, subscribe, ["testTopics/testClient"]}. + +{allow, all, subscribe, ["clients/$c"]}. + +{allow, all, subscribe, ["users/$u/#"]}. + +{deny, all, subscribe, ["$SYS/#", "#"]}. + +{allow, all}. + diff --git a/rel/files/app.config b/rel/files/app.config index 87299b84a..1ed23e875 100644 --- a/rel/files/app.config +++ b/rel/files/app.config @@ -38,9 +38,10 @@ {logger, {lager, info}} ]}, {emqttd, [ - %Authetication. Internal, Anonymous Default. - {auth, {anonymous, []}}, - {access, []}, + %Authetication. Internal, Anonymous Default + {auth, {anonymous, []}}, + %ACL config + {acl, [{file, "etc/acl.config"}]}, {packet, [ {max_clientid_len, 1024}, {max_packet_size, 16#ffff}