了解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。

clip_image002

它在第一条语句中被解包为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函数。

它的定义在这里。

https://github.com/vernemq/vernemq/blob/c8b92f398e76d6ce4b8cca5e438e8ae1e717d71c/apps/vmq_diversity/src/vmq_diversity_utils.erl

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是在这里实现的。

https://github.com/vernemq/vernemq/blob/cd6666a2a57e16eb04011d0628359ad6a4883b34/apps/vmq_plugin/src/vmq_plugin_util.erl

-spec check_modifiers(atom(), list() | map()) -> list() | map() | error.

这一行告诉我们,check_modifiers函数希望将一个原子(即auth_on_subscribe)作为第一个参数,并将一个list()或map()作为第二个参数。它返回一个 list() 或 map() ,或错误(一个原子() )。

clip_image004

一个令人生畏的功能,我将诚实地承认这一点。让我们来看看它。

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,它允许我们修改订阅者,从而改变装载点。

clip_image006

如果我们传入一个列表,那么将做进一步检查。否则,将返回false。

该列表必须包含 "client_id "和 "mountpoint"。其余的代码我目前还不太理解。

如果这第一条语句不匹配,我们也会返回错误。

结果

见介绍中的Lua代码,我们设定的目标已经实现,现在的挂载点是为每个客户端定制的。

clip_image007

参考文献。

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

https://www.erlang.org/docs

http://erlang.org/doc/man/erlang.html

https://en.wikipedia.org/wiki/Erlang_(programming_language)

https://github.com/vernemq/vernemq/issues/312 (这个问题给我指出了正确的方向,非常感谢!)

https://github.com/vernemq/vernemq/blob/cd6666a2a57e16eb04011d0628359ad6a4883b34/apps/vmq_plugin/src/vmq_plugin_util.erl

https://github.com/vernemq/vernemq/blob/c8b92f398e76d6ce4b8cca5e438e8ae1e717d71c/apps/vmq_diversity/src/vmq_diversity_plugin.erl