了解Erlang & Lua / Luerl for VerneMQ MongoDB auth_on_register挂钩
我的目标 / TLDR
我写这篇博文的目的是解释如何通过修改已交付的MongoDB auth Lua脚本(lua/auth/mongodb.lua)为VerneMQ设置自定义挂载点。
在VerneMQ中,设置自定义挂载点是可能的,不仅可以为特定的监听器(如端口)手动设置挂载点,还可以在你的脚本中授权时以编程方式设置。
我很难理解它,因为没有例子,而且我以前没有用功能语言(如Erlang)编程。此外,我以前也没有接触过Lua--但Lua比Erlang更容易理解,IMHO。
为什么要自定义挂载点?
我们的想法是将不同的用户相互隔离(多租户)。每个用户将有他们自己的主题树,不需要检查碰撞或要求用户增加一个额外的前缀。
完全隔离也增加了安全性,不小心设置错误的ACL。
从我的角度来看,因此是必须的。
对脚本进行必要的修改
对脚本的必要修改如下(以代码的形式粘贴,并再次以截图的形式粘贴,这样你就可以看到WordPress把格式弄乱的地方,等等)。
函数 auth_on_register(reg)
如果reg.username ~= nil and reg.password ~= nil then
doc = mongodb.find_one(pool, "vmq_acl_auth",
{client_id = reg.client_id,
username = reg.username})。
如果doc ~= false,那么
如果doc.active,那么
如果 doc.passhash == bcrypt.hashpw(reg.password, doc.passhash) 那么
cache_insert(
doc.mountpoint。
reg.client_id。
reg.username。
doc.publish_acl,
doc.subscribe_acl
)
reg.mountpoint = doc.mountpoint
- 或者只返回true,但这样就不能设置任何修饰语了。
返回 {
subscriber_id = {
mountpoint = doc.mountpoint。
client_id = reg.client_id
}
}
结束
结束
结束
结束
返回错误
结束
当然,你也可以返回其他修改器。这里有一个更详尽的列表,是关于auth_on_register的,从 VerneMQ文档:
注意:提供正确的类型很重要。
subscriber_id是一个更复杂的类型,由mountpoint和client_id的Tuple(从Erlang的角度)组成。
这就是为什么我传入一个表的表(用Lua的术语来说就是):
返回 {
subscriber_id = {
mountpoint = doc.mountpoint。
client_id = reg.client_id
}
}
注意:格式化对Lua来说相当不重要,我只是为了更好地阅读而这样设置的。
可以同时修改几个参数。例如,你可以节制消息率,改变clean_session标志,等等。
阐述Lua代码
请注意,我已将最初的检查更新为:。
doc = mongodb.find_one(pool, "vmq_acl_auth",
{client_id = reg.client_id,
username = reg.username})。
因此省略了对挂载点的检查。因为我们要从数据库中设置挂载点,所以我们并不关心客户端的初始挂载点(很可能是""一个空字符串)。
我返回从数据库读取的mountpoint,并将client_id设置为认证请求时传递给我们的原始数据。在这一点上,用户已经对数据库进行了认证。
重新加载脚本
你可以简单地在运行时更新脚本后重新加载,使用以下命令行。
vmq-admin脚本重新加载路径=./share/lua/auth/mongodb.lua
VerneMQ不需要为此关闭和重启。这很好,因为它极大地加快了开发和测试的速度。
调试(显示会话)。
只需使用
vmq-admin session show
正如你所看到的,挂载点也被显示出来。
这与我所拥有的预期数据库信息相一致。
client_id shahrukh应该有一个空的挂载点,而client_id goat应该有挂载点beardedgoat。
请注意,此时的ACL是非常允许的,以便于调试。
奖励:用于测试的mosquitto_sub命令
mosquitto_sub -t "#" -u "o2PTwBb" -P "Dis8gJ2yhdSYmkQBH1mCosxgJmAxCQm3698tg7Mh8mNFAHXDf4" -host 192.168.1.2 -port 1883 -id "shahrukh" -q 2 -verbose
mosquitto_sub -t "#" -u "o2PTwBb" -P "Dis8gJ2yhdSYmkQBH1mCosxgJmAxCQm3698tg7Mh8mNFAHXDf4" -host 192.168.1.2 -port 1883 -id "goat" -q 2 -verbose
关于Erlang
Erlang是爱立信公司专门为高可用性容错电信系统开发的语言。
它有一些有趣的功能,如果你感兴趣的话。 在维基百科上阅读更多关于它的信息.
理解Erlang代码的主要挑战是,它与我迄今为止遇到的任何东西都有很大不同。
埃兰是一种函数式语言。这意味着,你写的代码不是说 "做这个,做那个,看一下变量,然后做这个,做那个",而是把所有东西都作为一个有返回值的函数来调用。
例如,你可以用函数来代替循环,相互递归地调用对方。
同时,Erlang运行时根据参数匹配它需要调用的正确函数。
例如,对于一个递归函数/循环,你将继续调用该函数,直到达到某个点。例如,你已经处理了列表的最后一项,而列表是空的--在这里,你可以做出不同的反应,而不是继续递归,给出最后的结果。
了解VerneMQ的Erlang代码
注意:我复制代码的目的只是为了解释它的作用,所有代码的版权属于 Octavo Labs AG.
稍微了解一下erl代码,对于 vmq_diversity_plugin.erl:
-module(vmq_diversity_plugin)。
这里的模块名称必须与模块的文件名一致
-export([auth_on_register/5, ...etc])。
哪些函数可以在这个模块中被调用。 以及他们期望的参数数量。
%%%===================================================================
%%%挂钩功能
%%%===================================================================
%%作为一个all_till_ok钩子被调用。
auth_on_register(Peer, SubscriberId, UserName, Password, CleanSession) ->
{PPeer, Port} = peer(Peer),
{MP, ClientId} = subscriber_id(SubscriberId),
Res = all_till_ok(auth_on_register, [{addr, PPeer},
[port, Port],
{mountpoint, MP}。
{client_id, ClientId},
{username, nilify(UserName)}。
{password, nilify(Password)}。
{clean_session, CleanSession}])。
conv_res(auth_on_reg, Res)。
所有_直到_好 将调用所有可用的认证 "后端"(钩子),直到其中一个依次返回OK,以便给每个人一个机会来认证用户。
auth_on_publish(UserName, 订阅者身份, QoS, Topic, Payload, IsRetain) ->
{MP, ClientId} = subscriber_id(SubscriberId),
case vmq_diversity_cache: match_publish_acl(MP, ClientId, QoS, Topic, Payload, IsRetain) 的。
真实->
%% 找到一个有效的缓存条目,该条目授予这个发布。
好的。
修改器当is_list(修改器)->
%% 找到一个有效的包含修饰语的缓存条目
{pos(190,5)}{好的,修改者}。
假->
%% 找到了一个有效的缓存条目,拒绝了这个发布。
{error, not_authorized};
no_cache ->
Res = all_till_ok(auth_on_publish, [{username, nilify(UserName)},
{mountpoint, MP}。
{client_id, ClientId},
{qos, QoS},
{topic, unword(Topic)}。
{payload, Payload},
{retain, IsRetain}])。
conv_res(auth_on_pub, Res)
结束。
备注:
SubscriberId同时包含mountpoint和client_id。
它在第一条语句中被解包为mountpoint和client_id。
{MP, ClientId} = subscriber_id(SubscriberId),
注意,在Erlang中,变量以大写字母开头。因此,MP和ClientId是变量。
case vmq_diversity_cache: match_publish_acl(MP, ClientId, QoS, Topic, Payload, IsRetain) 的。
真实->
%% 找到一个有效的缓存条目,该条目授予这个发布。
好的。
vmq_diversity_cache模块被调用,函数为match_publish_acl。
在这个过程中,会传递给mountpoint(MP)、ClientId、Quality of Service(QoS)、Topic、Payload和IsRetain。
如果这个函数返回 真,Erlang函数auth_on_publish的返回值是 好的.
注意,在Erlang中,由于 "ok "是以一个小字母开头的,所以它只是一个名字--而不是一个变量(具体地说,它是一个数据类型Atom的实例)。 相当于水晶郎的可能是符号。
好的。
注意,";"不是语句的终止,而应该被理解为 "其他"。
修改器当is_list(修改器)->
%% 找到一个有效的包含修饰语的缓存条目
{pos(190,5)}{好的,修改者}。
当返回值是一个列表时,它被作为一个带有修饰符的返回值传递--在本例中是一个Erlang tuple {ok, Modifiers}(将Atom "ok "和变量Modifiers分组并返回)。
请注意,is_list是Erlang的一个内置函数(BIF),而不是Lua/Luerl特有的东西。
假->
%% 找到了一个有效的缓存条目,拒绝了这个发布。
{error, not_authorized};
这里传递的是 "ok "而不是 "error",还有 "not_authorized"。这些都是原子,而不是变量--正如false一样。
no_cache ->
Res = all_till_ok(auth_on_publish, [{username, nilify(UserName)},
{mountpoint, MP}。
{client_id, ClientId},
{qos, QoS},
{topic, unword(Topic)}。
{payload, Payload},
{retain, IsRetain}])。
conv_res(auth_on_pub, Res)
最后,如果缓存返回 "no_cache",我们调用all_till_ok函数,使用 "auth_on_publish",传入一个图元数组,寻找是否有钩子可以验证该消息的发布。
all_till_ok([Pid|Rest], HookName, Args) ->
case vmq_diversity_script:call_function(Pid, HookName, Args) of
真实->
好的。
Mods0当is_list(Mods0)->
Mods1 = convert_modifiers(HookName, Mods0)。
case vmq_plugin_util:check_modifiers(HookName, Mods1) of
错误->
{error, {invalid_modifiers, Mods1}}。
CheckedModifiers ->
{ok, CheckedModifiers}。
结束。
假->
{error, lua_script_returned_false};
错误->
[error, lua_script_error];
{error, Reason}->
{error, Reason};
_ ->
all_till_ok(Rest, HookName, Args)
结束。
all_till_ok([], _, _) -> next.
在这里,函数all_till_ok调用函数vmq_diversity_script:call_function,同时传入HookName(它被设置为例如 auth_on_publish 或 注册时的身份验证(auth_on_register),以及对钩子的论证。
如果钩子返回 "true",那么要返回的值就是 "ok"。
否则,如果Hook返回一个修改器的列表。
修饰语是通过 改造者
由于变量在Erlang中是不可改变的--也就是说,一旦你把某个东西赋值给一个变量,你就不能再赋值给这个变量,所以我们使用一个新的变量来转换修改器,即Modes1。
convert_modifiers(Hook, Mods0) ->
vmq_diversity_utils:convert_modifiers(Hook, Mods0)。
这只是包装了vmq_diversity_utils:convert_modifiers函数。
它的定义在这里。
convert_modifiers(auth_on_subscribe, Mods0) ->
normalize_subscribe_topics(convert(Mods0))。
convert_modifiers(on_unsubscribe, Mods0) ->
convert(Mods0)。
convert_modifiers(Hook, Mods0) ->
Mods1 = atomize_keys(Mods0)。
Converted = lists:map(
fun(Mod) ->
convert_modifier(Hook, Mod)。
结束。
Mods1)。
case lists:member(Hook, [auth_on_register_m5,
auth_on_subscribe_m5。
auth_on_unsubscribe_m5。
on_unsubscribe_m5。
auth_on_publish_m5。
on_deliver_m5,
on_auth_m5])中的
真实->
maps:from_list(Converted)。
_ ->
已转换
结束。
这显示了Erlang的匹配能力。根据哪个Atom作为函数的第一部分被调用,不同的函数被执行。
如果用 签名:Auth_on_subscribe,它将调用normalize_subscribe_topics,传入Mods0的转换版本。
normalize_subscribe_topics(convert(Mods0))。
转换的定义,并在同一文件中进一步解释。
%% @doc 递归地将一个从lua返回的值转换为erlang
%%数据结构。
convert(Val) when is_list(Val) ->
convert_list(Val, [])。
convert(Val) when is_number(Val) ->
的case round(Val)。
当RVal == Val -> RVal时,RVal。
_ -> Val
结束。
convert(Val) when is_binary(Val) -> Val;
convert(Val) when is_boolean(Val) -> Val;
convert(nil) -> 未定义。
如果Val(在我们的例子中是Mods0)是一个列表,就会调用convert_list。
convert_list([ListItem|Rest], Acc) ->
convert_list(Rest, [convert_list_item(ListItem)|Acc])。
convert_list([], Acc) -> lists:reverse(Acc)。
convert_list_item({Idx, Val}) when is_integer(Idx) ->
%% lua阵列
转换(Val)。
convert_list_item({BinKey, Val}) when is_binary(BinKey) ->
尝试list_to_existing_atom(binary_to_list(BinKey))of
钥匙 -> {钥匙,转换(Val)}
接住
_:_ ->
{BinKey, convert(Val)}。
结束。
convert_list([ListItem|Rest], Acc) ->
convert_list(Rest, [convert_list_item(ListItem)|Acc])。
convert_list([], Acc) -> lists:reverse(Acc)。
这使用了 Erlang 的递归功能。通过递归调用自身(并使用 convert_list_item 依次处理每个列表项),列表被逐一转换。列表项被处理后从左边转移到右边,这样左边的变量最后就变成了一个空列表。一旦如此,第二部分就会匹配。
convert_list([], Acc) -> lists:reverse(Acc)。
而该函数的结果将是lists:reverse(Acc)(右侧)。
convert_list_item使用了一些Erlang函数,目前我还没有完全理解这部分。不过,我确实理解了第一部分的内容。
convert_list_item({Idx, Val}) when is_integer(Idx) ->
%% lua阵列
转换(Val)。
对于一个Lua数组(表),该数组被解包,相应项目的Array索引被丢弃。
注意,在Lua中 桌子 是唯一的关联数组/哈希/...类型。没有特定的数组类型。
回到all_till_ok函数。
case vmq_plugin_util:check_modifiers(HookName, Mods1) of
错误->
{error, {invalid_modifiers, Mods1}}。
CheckedModifiers ->
{ok, CheckedModifiers}。
结束。
转换后的修改器被传入vmq_plugin_util:check_modifiers(也就是vmq_plugin_util模块中的check_modifiers函数)。
如果这个函数返回一个错误,返回值是一个{error, {invalid_modifiers, Mods1}}的元组。
error和invalid_modifiers,记住,只是名字。此外,这些修饰符被传递给我们检查。(再次注意语句第一部分末尾的分号,表示 "else")
如果该函数返回一个变量,我们将返回一个{ok, CheckedModifiers}的元组。
这个函数check_modifiers是在这里实现的。
-spec check_modifiers(atom(), list() | map()) -> list() | map() | error.
这一行告诉我们,check_modifiers函数希望将一个原子(即auth_on_subscribe)作为第一个参数,并将一个list()或map()作为第二个参数。它返回一个 list() 或 map() ,或错误(一个原子() )。
一个令人生畏的功能,我将诚实地承认这一点。让我们来看看它。
AllowedModifiers = modifiers(Hook)。
AllowedModifiers是一个变量。它调用函数修改器,Hook变量被传入。
例如,对于 注册时的身份验证(auth_on_register,它将与以下函数相匹配。
修改器(auth_on_register) ->
[{allow_register, fun val_bool/1}。
{allow_publish, fun val_bool/1}。
{allow_subscribe, fun val_bool/1}。
{allow_unsubscribe, fun val_bool/1}。
{max_message_size, fun val_int/1}。
{subscriber_id, fun val_subscriber_id/1}。
{clean_session, fun val_bool/1}。
{max_message_rate, fun val_int/1}。
{max_inflight_messages, fun val_int/1}。
{shared_subscription_policy, fun val_atom/1}。
{retry_interval, fun val_int/1}。
{upgrade_qos, fun val_bool/1}。
{allow_multiple_sessions, fun val_bool/1}。
{max_online_messages, fun val_int/1}。
{max_offline_messages, fun val_int/1}。
{queue_deliver_mode, fun val_atom/1}。
{queue_type, fun val_atom/1}。
{max_drain_time, fun val_int/1}。
{max_msgs_per_drain_step, fun val_int/1}】。]
所以它将返回一个图元数组。在这些图元中,定义了允许的名称(可以修改),例如subscriber_id。(记住,subscriber_id包含mountpoint和client_id!),以及检查该值的函数。例如,fun val_subscriber_id/1意味着要检查函数val_subscriber_id,并向其传递1个参数。
为了理解接下来的语句,我们必须看一下Erlang的文档。
lists:foldl(fun (_, error) -> error;
http://erlang.org/doc/man/lists.html
foldl会在一个列表的连续元素上调用一个函数
这就是它的定义。
foldl(Fun, Acc0, List) -> Acc1
所以我们传入一个函数,一个空的List,以及我们的修饰语。
对空列表的解释。"如果列表为空,则返回Acc0"--也就是说,如果我们的初始修改器列表为空,则返回一个空列表。
"_"表示一个匿名变量。这意味着,该变量是必需的,但它的值可以被忽略。.
见 http://erlang.org/doc/reference_manual/expressions.html 详情请见下文。
因此,如果在调用该函数时,将错误作为第二个变量传入,结果是 错误.
否则,如果用一个元组{ModKey, ModVal}和Acc来调用,其结果值取决于是否在AllowedModifiers列表中找到键。如果没有找到(false),那么结果是 错误.
keyfind如果找到键,将返回一个元组(包括键),否则为false。
由于我们已经确定我们知道这个键,它在列表中,我们可以使用匿名变量"_"忽略它,而把注意力放在ValidatorFun(验证器函数)上。
在这里,然后ModVal被运行在验证器函数中(它被定义在我们匹配的适当的修改器函数中)。
如果该函数返回 "true",那么ModKey和ModVal的元组将和Acc的其他部分一起返回(它是好的,并且已经被检查过)。
如果它是假的,将记录一个错误(不能验证修改器),并返回错误。
如果它是一个带有ok和NewModVal的元组,那么将使用ModKey和NewModVal。
让我们看看val_subscriber_id,它允许我们修改订阅者,从而改变装载点。
如果我们传入一个列表,那么将做进一步检查。否则,将返回false。
该列表必须包含 "client_id "和 "mountpoint"。其余的代码我目前还不太理解。
如果这第一条语句不匹配,我们也会返回错误。
结果
见介绍中的Lua代码,我们设定的目标已经实现,现在的挂载点是为每个客户端定制的。
参考文献。
https://github.com/vernemq/vernemq/issues/533
https://docs.vernemq.com/configuring-vernemq/db-auth
http://erlang.org/doc/getting_started/users_guide.html
https://docs.vernemq.com/plugin-development/luaplugins
https://docs.vernemq.com/plugin-development/sessionlifecycle
http://erlang.org/doc/man/erlang.html
https://en.wikipedia.org/wiki/Erlang_(programming_language)
https://github.com/vernemq/vernemq/issues/312 (这个问题给我指出了正确的方向,非常感谢!)