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
Claro, você também pode devolver outros modificadores. Aqui está uma lista mais exaustiva para auth_on_register do Documentação VerneMQ:
Nota: é importante fornecer os tipos correctos:
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
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:
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:
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
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:
é 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:
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:
-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()).
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:
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:
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
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!)