Entendendo Erlang & Lua / Luerl para VerneMQ MongoDB auth_on_register hook

Meu objetivo / TLDR

Meu objetivo com este post no blog é explicar como definir pontos de montagem personalizados para o VerneMQ modificando o script Lua do MongoDB auth enviado (lua/auth/mongodb.lua).

A definição de um ponto de montagem personalizado é possível com o VerneMQ não apenas definindo pontos de montagem manualmente para ouvintes específicos (por exemplo, portas), mas também programando durante a autorização em seus scripts.

Tive muita dificuldade em envolvê-la, pois não há exemplos, e não tinha programado em uma linguagem funcional (como Erlang) antes. Além disso, eu não tinha tocado em Lua antes - mas Lua é mais fácil de entender do que Erlang IMHO.

porquê pontos de montagem personalizados?

A idéia é isolar diferentes usuários uns contra os outros (multi-tenancy). Cada usuário terá sua própria árvore de tópicos, não haverá necessidade de verificar colisões ou demanda de usuários para adicionar um prefixo adicional.

O isolamento total também aumenta a segurança ao definir acidentalmente as ACLs de forma incorrecta.

Do meu ponto de vista, portanto, uma obrigação!

modificações de script necessárias

A modificação necessária do script é a seguinte (colado como código & novamente como captura de tela, para que você possa ver onde o WordPress mexe na formatação, etc.):

função auth_on_register(reg)
     se reg.username ~= nil e reg.password ~= nil então
         doc = mongodb.find_one(pool, "vmq_acl_auth",
                                 {client_id = reg.client_id,
                                  username = reg.username})
         se doc ~= falso então
             se doc.active então
                 se doc.passhash == bcrypt.hashpw(reg.password, doc.passhash) então
                     cache_insert(
                         doc.ponto de montagem,
                         reg.client_id,
                         reg.username,
                         doc.publish_acl,
                         doc.subscribe_acl
                         )
                     reg.ponto.de montagem = doc.ponto.de montagem
                     - alternativamente, retornar apenas verdadeiro, mas então nenhum modificador pode ser definido
                     retornar {
                         subscriber_id = {
                                 ponto de montagem = doc.ponto de montagem,
                                 client_id = reg.client_id
                             }
                         }

                 final
             final
         final
     final
     devolver falsas
final

imagem

Claro, você também pode devolver outros modificadores. Aqui está uma lista mais exaustiva para auth_on_register do Documentação VerneMQ:

imagem

Nota: é importante fornecer os tipos correctos:

imagem

o subscriber_id é um tipo mais complexo que consiste em um Tuple (do ponto de vista de Erlang) de ponto de montagem e client_id.

Isto é, porque passo numa mesa de uma mesa (na terminologia de Lua):

                    retornar {
                         subscriber_id = {
                                 ponto de montagem = doc.ponto de montagem,
                                 client_id = reg.client_id
                             }
                         }

Nota: a formatação não é muito importante para Lua, acabei de a configurar desta forma para uma melhor legibilidade.

Vários parâmetros podem ser modificados ao mesmo tempo. Por exemplo, você pode acelerar a taxa de mensagens, alterar a bandeira clean_session, etc.

Elaboração do código Lua

Note que eu atualizei as verificações iniciais para ler:

doc = mongodb.find_one(pool, "vmq_acl_auth",
                         {client_id = reg.client_id,
                          username = reg.username})

Omitindo, assim, a verificação contra o ponto de montagem. Como vamos definir o ponto de montagem a partir da base de dados, não nos preocupamos com o ponto de montagem inicial do cliente (que será "" uma string vazia) muito provavelmente.

Eu devolvo o ponto de montagem como lido do banco de dados, e defino o client_id como o original passado para nós durante o pedido de autenticação. Neste ponto o usuário já está autenticado contra o banco de dados.

Recarregando o roteiro

Você pode simplesmente recarregar o script após atualizar seu tempo de execução, usando a seguinte linha de comando:

vmq-admin script reload path=./share/lua/auth/mongodb.lua

A VerneMQ não precisa de ser desligada e reiniciada para isto. Isto é bom, pois acelera imensamente o desenvolvimento e os testes!

Depuração (mostrando sessões)

Simplesmente use

vmq-admin sessão show

imagem

Como você pode ver, os pontos de montagem também são exibidos.

Isto corresponde à informação esperada da base de dados que eu tenho:

imagem

O client_id shahrukh deve ter um ponto de montagem vazio, e o client_id bode deve ter o ponto de montagem bode barbudo.

Note que os ACLs neste ponto são muito permissivos para permitir uma depuração mais fácil:

imagem

Bónus: comandos mosquitto_sub para testes

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 "bode" -q 2 -verbose

imagem

Sobre Erlang

Erlang é uma linguagem que foi desenvolvida na Ericsson especificamente para sistemas de telecomunicações tolerantes a falhas e de alta disponibilidade.

Tem algumas características interessantes, se você estiver interessado, leia mais na Wikipedia sobre isso.

O principal desafio para entender o código Erlang é que ele é bem diferente de qualquer coisa que eu tenha encontrado até agora.

Erlang é uma linguagem funcional. Isto significa, que você não escreve código que diz "faça isto, faça aquilo, olhe para a variável, depois faça isto, faça aquilo", mas que tudo é chamado como uma função com um valor de retorno.

Por exemplo, em vez de loops, vocês terão funções chamando uns aos outros recursivamente.

Além disso, o tempo de execução do Erlang corresponde à função correta que ele precisa chamar, dependendo dos parâmetros.

Por exemplo, para uma função / loop recursivo, você continuará chamando a função até que um certo ponto seja alcançado. Por exemplo, você processou o último item da lista, e a lista está vazia - aqui, você pode responder de forma diferente, em vez de continuar a repetir-se, devolva o resultado final.

Entendendo o Código Erlang da VerneMQ

Nota: Eu reproduzi o código apenas com o propósito expresso de explicar o que ele faz, todo o código é copyright por Octavo Labs AG.

Compreender um pouco o código erl, para vmq_diversity_plugin.erl:

-module(vmq_diversity_plugin). 

o nome do módulo aqui deve corresponder ao nome de arquivo do módulo

-export([auth_on_register/5, ... etc]).

que funções podem ser chamadas neste módulo, e o número de parâmetros que eles esperam.

%%%===================================================================

%%1T% Funções de gancho

%%%===================================================================

%% chamado como um gancho 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},

{ponto de montagem, deputado},

{client_id, ClientId},

{username, nilify(UserName)},

{password, nilify(Password)},

{clean_session, CleanSession}]),

conv_res(auth_on_reg, Res).

all_till_ok irá chamar todos os backends (ganchos) de autenticação disponíveis até que um retorne ok por sua vez, para dar a cada um a chance de autenticar o usuário.

auth_on_publish(Nome_de_utilizador, SubscritorIdQoS, QoS, Tópico, Payload, IsRetain) ->

{MP, ClientId} = subscriber_id(SubscriberId),

caso vmq_diversity_cache:match_publish_acl(DEPUTADO, ClienteIdQoS, QoS, Tópico, Payload, IsRetain) de

verdadeiro ->

%% Encontrei uma entrada de cache válida que garante esta publicação

Está bem;

Modifiers when is_list(Modifiers) ->

%% Encontrei uma entrada de cache válida contendo modificadores

{ok, Modifiers};

falso ->

%% Encontrou uma entrada de cache válida que rejeita esta publicação

{erro, não_autorizado};

no_cache ->

Res = all_till_ok(auth_on_publish, [{username, nilify(UserName)},

{ponto de montagem, deputado},

{client_id, ClientId},

QoS,

{tópico, não palavra(Tópico)},

{payload, Payload},

{retain, IsRetain}]),

conv_res(auth_on_pub, Res)

...fim.

nota:

O SubscriberId contém tanto o mountpoint como o client_id:

clip_image002

é desempacotado em mountpoint e client_id na primeira declaração:

{MP, ClientId} = subscriber_id(SubscriberId),

note que as variáveis começam com uma letra maiúscula em Erlang. MP e ClientId são, portanto, variáveis.

caso vmq_diversity_cache:match_publish_acl(DEPUTADO, ClienteIdQoS, QoS, Tópico, Payload, IsRetain) de

verdadeiro ->

%% Encontrei uma entrada de cache válida que garante esta publicação

Está bem;

o módulo vmq_diversity_cache é chamado com a função match_publish_acl.

o ponto de montagem (MP), ClientId, Qualidade de Serviço (QoS), Tópico, Payload e IsRetain são passados para ele.

Se esta função retornar verdadeiroo valor de retorno da função Erlang auth_on_publish é ok.

note que em Erlang, como "ok" começa com uma letra pequena, é apenas um nome - não uma variável (é, especificamente, uma instância do datatype Atom). O equivalente em Crystal Lang seria provavelmente Symbols.

Está bem;

note que o ";" não é um término da declaração, mas deve ser lido como um "outro".

Modifiers when is_list(Modifiers) ->

%% Encontrei uma entrada de cache válida contendo modificadores

{ok, Modifiers};

quando o valor retornado é uma lista , ele é passado como um valor de retorno com Modificadores - neste caso como um tuple Erlang {ok, Modificadores} (agrupando o Átomo "ok" e a variável Modificadores juntos e retornando-os).

Note que is_list é uma função integrada (BIF) de Erlang, e não algo específico de Lua /Luerl.

falso ->

%% Encontrou uma entrada de cache válida que rejeita esta publicação

{erro, não_autorizado};

aqui ao invés de "ok" "erro" é passado, juntamente com "não_autorizado". Estes são todos átomos, não variáveis - como é falso.

no_cache ->

Res = all_till_ok(auth_on_publish, [{username, nilify(UserName)},

{ponto de montagem, deputado},

{client_id, ClientId},

QoS,

{tópico, não palavra(Tópico)},

{payload, Payload},

{retain, IsRetain}]),

conv_res(auth_on_pub, Res)

finalmente, se o cache retornar "no_cache", nós chamamos a função all_till_ok, com "auth_on_publish", passando em um array de tuples, olhando se qualquer gancho pode autenticar a publicação desta mensagem.

all_till_ok([Pid|Rest], HookName, Args) ->

case vmq_diversity_script:call_function(Pid, HookName, Args) of

verdadeiro ->

Está bem;

Mods0 quando is_list(Mods0) ->

Mods1 = convert_modifiers(HookName, Mods0),

case vmq_plugin_util:check_modifiers(HookName, Mods1) of

erro ->

{error, {invalid_modifiers, Mods1}};

CheckedModifiers ->

{ok, CheckedModifiers}

...fim;

falso ->

{error, lua_script_returned_false};

erro ->

{error, lua_script_error};

{erro, razão} ->

{erro, razão};

_ ->

all_till_ok(Descanso, Nome_do_goleiro, Args)

...fim;

all_till_ok([], _, _) -> next.

aqui a função all_till_ok chama a função vmq_diversity_script:call_function, passando também no HookName (que está definido para, por exemplo auth_on_publish ou auth_on_register), e os argumentos para o Gancho.

Se o gancho retornar "verdadeiro", então o valor a retornar é "ok".

Caso contrário, se o Gancho devolver uma lista de modificadores,

os modificadores são executados através de convert_modifiers

Como as variáveis são imutáveis em Erlang - ou seja, uma vez que você atribui algo a uma variável, você não pode reatribuir à variável, nós usamos uma nova variável para os modificadores convertidos, Mods1.

convert_modifiers(Hook, Mods0) ->

vmq_diversity_utils:convert_modifiers(Hook, Mods0).

isto apenas envolve a função vmq_diversity_utils:convert_modifiers.

Está definido aqui:

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) ->

converter(Mods0);

convert_modifiers(Hook, Mods0) ->

Mods1 = atomize_keys(Mods0),

Convertido = listas:mapa(

fun(Mod) ->

convert_modifier(Gancho, Mod)

...fim,

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]) de

verdadeiro ->

maps:from_list(Converted);

_ ->

Convertido em

...fim.

Isto mostra as capacidades de correspondência do Erlang. Dependendo com que Átomo como primeira parte a função é chamada, uma função diferente é executada.

Se chamado com auth_on_subscribeele chamará normalize_subscribe_topics, passando em uma versão convertida de Mods0.

normalize_subscribe_topics(convert(Mods0));

convert é definido e explicado mais abaixo no mesmo arquivo:

%% @doc converte recursivamente um valor retornado de lua para um erlang

%% estrutura de dados.

convert(Val) quando is_list(Val) ->

convert_list(Val, []);

convert(Val) when is_number(Val) ->

caso em questão(Val) de

RVal quando RVal == Val -> RVal;

_ -> Val

...fim;

convert(Val) quando is_binary(Val) -> Val;

convert(Val) when is_boolean(Val) -> Val;

convert(nil) -> indefinido.

se Val (Mods0 no nosso caso) é uma lista, convert_list é chamado:

convert_list([ListItem|Rest], Acc) ->

convert_list(Restante, [converter_list_item(ListItem)|Acc]);

convert_list([], Acc) -> lists:reverse(Acc).

convert_list_item({Idx, Val}) when is_integer(Idx) ->

%% matriz lua

converter(Val);

convert_list_item({BinKey, Val}) quando is_binary(BinKey) ->

try list_to_existing_atom(binary_to_list(BinKey)) of

Key -> {Key, convert(Val)}

pegar

_:_ ->

{BinKey, convert(Val)}

...fim.

convert_list([ListItem|Rest], Acc) ->

convert_list(Restante, [converter_list_item(ListItem)|Acc]);

convert_list([], Acc) -> lists:reverse(Acc).

Isto usa a recorrência do Erlang. A lista é convertida um item de cada vez, chamando-se recursivamente a si mesma (e processando cada item da lista por sua vez, usando convert_list_item). O item da lista é transferido da esquerda para a direita quando processado, para que a variável esquerda acabe como uma lista vazia. Uma vez processada, a segunda parte será igualada:

convert_list([], Acc) -> lists:reverse(Acc).

e o resultado da função será listas:reverse(Acc) (o lado direito).

convert_list_item está usando algumas funções Erlang, atualmente eu não entendo essa parte completamente. No entanto, eu não entendo a primeira parte:

convert_list_item({Idx, Val}) when is_integer(Idx) ->

%% matriz lua

converter(Val);

Para uma matriz Lua (tabela), a matriz é desempacotada e o índice de matriz do item correspondente é descartado.

Note, que em Lua o quadro é a única matriz associativa / hash / ... tipo. Não há um tipo específico de array.

Voltar para a função all_till_ok:

case vmq_plugin_util:check_modifiers(HookName, Mods1) of

erro ->

{error, {invalid_modifiers, Mods1}};

CheckedModifiers ->

{ok, CheckedModifiers}

...fim;

Os modificadores convertidos são passados para vmq_plugin_util:check_modifiers (ou seja, a função check_modifiers no módulo vmq_plugin_util).

Se esta função retorna um erro, o valor de retorno é um tuple de {erro, {modificadores_invalidos, Mods1}}};

erro e modificadores_invalidos são, lembre-se, apenas nomes. Além disso, os modificadores são passados para a nossa inspecção. (Novamente, observe o ponto-e-vírgula no final da primeira parte da declaração, indicando um "outro")

se a função retorna uma variável em seu lugar, retornamos um tuple de {ok, CheckedModifiers}.

Esta função check_modifiers é implementada aqui:

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

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

Esta linha nos diz que a função check_modifiers espera um átomo (ou seja, auth_on_subscribe) como primeiro parâmetro, e uma lista() ou um mapa() como segundo parâmetro. Ele retorna uma list(), ou um map(), ou um erro (um átomo()).

clip_image004

Uma função intimidante, vou admiti-lo honestamente. Vamos ultrapassá-la:

AllowedModifiers = modificadores(Hook),

O AllowedModifiers é uma variável. Ele chama os modificadores de função, com a variável Hook sendo passada para dentro.

Por exemplo, para auth_on_registerele vai corresponder à seguinte função:

modifiers(auth_on_register) ->

{\an8}{\an8}{\an8}"allow_register, fun val_bool/1}

{\an8}{\an8}"allow_publish, fun val_bool/1},

{subscrição, divertido val_bool/1},

{\an8}{\an8}{\an8}"sebo_unsubscribe", divertido val_bool/1},

{max_message_size, fun val_int/1},

{subscriber_id, divertido val_subscriber_id/1},

{clean_session, fun val_bool/1},

{max_message_rate, fun val_int/1},

{max_inflight_messages, val_int/1 divertido},

{\an8}{\an8}{\an8}política de subscrição_partilhada, val_atom/1} divertida,

{retry_interval, divertimento val_int/1},

{upgrade_qos, divertido val_bool/1},

{sessões_múltiplas, diversão val_bool/1},

{max_online_messages, val_int/1 divertido},

{max_offline_messages, val_int/1 divertido},

{queue_deliver_mode, divertimento val_atom/1},

{queue_type, fun val_atom/1},

{max_drain_time, val_int/1}divertido,

{max_msgs_per_drain_step, fun val_int/1}];

por isso vai devolver um conjunto de tuplos. Nestes tuplos, os nomes permitidos (que podem ser modificados) são definidos, por exemplo, o subscriber_id. (lembre-se, o subscriber_id contém tanto o ponto de montagem quanto o client_id!) , e a função para verificar o valor em relação a ele. Por exemplo, fun val_subscriber_id/1 significa que a função val_subscriber_id deve ser verificada, com 1 parâmetro a ser passado para ele.

Para compreender as próximas declarações, temos de ver um pouco da documentação do Erlang:

lists:foldl(fun (_, error) -> error;

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

O foldl irá chamar uma função sobre os sucessivos elementos de uma lista

É assim que é definido.

foldl(Diversão, Acc0, Lista) -> Acc1

então nós passamos em uma função, uma Lista vazia, e nossos modificadores.

explicação para a lista vazia: "Acc0 é devolvido se a lista estiver vazia" - ou seja, se a nossa lista inicial de modificadores estiver vazia, nós devolvemos uma lista vazia.

O "_" significa uma variável anónima. Isso significa que a variável é necessária, mas o seu valor pode ser ignorado..

Ver http://erlang.org/doc/reference_manual/expressions.html para detalhes.

Assim, se a função é chamada com algo, e o erro como a segunda variável é passada, o resultado é erro.

caso contrário, se chamado com um tuple {ModKey, ModVal} e Acc, o valor do resultado depende se a chave é encontrada na lista de Modificadores Permitidos. Se não for encontrada (falsa) , então o resultado é erro.

keyfind devolverá um tuple se a chave for encontrada (incluindo a chave), caso contrário, será falsa.

Como já determinamos que conhecemos a chave, ela está na lista, podemos ignorá-la usando a variável anônima "_", e focar na função ValidatorFun (validador).

Aqui, então o ModVal é executado através da função validador (que é definida na função modificadora apropriada que nós combinamos).

Se a função retorna verdadeiro, então o tuple de ModKey e ModVal é retornado (está ok e foi verificado) juntamente com o resto do Acc.

Se for falso, um erro será registrado (não é possível validar o modificador), e o erro será devolvido.

Se for um tuple com ok e NewModVal, então ModKey e NewModVal serão usados.

Vamos dar uma olhada no val_subscriber_id, que nos permite modificar o assinante, e portanto mudar o ponto de montagem:

clip_image006

se passarmos em uma lista, então mais verificações são feitas. Caso contrário, o falso é devolvido.

A lista tem de conter tanto "client_id" como "mountpoint". O resto do código é mal entendido por mim no momento.

Se esta primeira afirmação não corresponder, também devolvemos falsos.

O resultado

Veja o código Lua na introdução, o que nos propusemos alcançar foi alcançado, o ponto de montagem é agora um ponto personalizado para cada cliente:

clip_image007

Referências:

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 (esta edição apontou-me na direcção certa, muito obrigado!)

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