Entendiendo Erlang y Lua / Luerl para VerneMQ MongoDB auth_on_register hook

Mi objetivo / TLDR

Mi objetivo con esta entrada de blog es explicar cómo establecer puntos de montaje personalizados para VerneMQ modificando el script Lua de autentificación de MongoDB (lua/auth/mongodb.lua).

Establecer un punto de montaje personalizado es posible con VerneMQ no sólo estableciendo puntos de montaje manualmente para oyentes específicos (por ejemplo, puertos), sino también programáticamente durante la autorización en sus scripts.

Me costó mucho entenderlo, ya que no hay ejemplos, y no había programado antes en un lenguaje funcional (como Erlang). Además, no había tocado Lua antes - pero Lua es más fácil de entender que Erlang IMHO.

¿por qué puntos de montaje personalizados?

La idea es aislar a los diferentes usuarios entre sí (multi-tenancy). Cada usuario tendrá su propio árbol de temas, no habrá necesidad de comprobar si hay colisiones o exigir a los usuarios que añadan un prefijo adicional.

El aislamiento total también aumenta la seguridad por la configuración accidental de las ACLs de forma incorrecta.

Por lo tanto, desde mi punto de vista, es imprescindible.

modificaciones de script necesarias

La modificación necesaria del script es la siguiente (pegado como código y de nuevo como captura de pantalla, para que puedas ver dónde WordPress estropea el formato, etc.):

function auth_on_register(reg)
     si reg.username ~= nil y reg.password ~= nil entonces
         doc = mongodb.find_one(pool, "vmq_acl_auth",
                                 {client_id = reg.client_id,
                                  nombre de usuario = reg.nombre de usuario})
         si doc ~= false entonces
             si doc.active entonces
                 si doc.passhash == bcrypt.hashpw(reg.password, doc.passhash) entonces
                     cache_insert(
                         doc.mountpoint,
                         reg.client_id,
                         reg.username,
                         doc.publicar_acl,
                         doc.subscribe_acl
                         )
                     reg.mountpoint = doc.mountpoint
                     - alternativamente devuelve sólo true, pero entonces no se pueden establecer modificadores
                     devolver {
                         subscriber_id = {
                                 mountpoint = doc.mountpoint,
                                 client_id = reg.client_id
                             }
                         }

                 fin
             fin
         fin
     fin
     devolver falso
fin

imagen

Por supuesto, también puede devolver otros modificadores. Aquí hay una lista más exhaustiva para auth_on_register del Documentación de VerneMQ:

imagen

Nota: es importante proporcionar los tipos correctos:

imagen

el subscriber_id es un tipo más complejo que consiste en una tupla (desde el punto de vista de Erlang) de mountpoint y client_id.

Es decir, por lo que paso en una tabla de una tabla (en la terminología de Lua):

                    devolver {
                         subscriber_id = {
                                 mountpoint = doc.mountpoint,
                                 client_id = reg.client_id
                             }
                         }

Nota: el formato es bastante poco importante para Lua, sólo lo he puesto así para que sea más legible.

Se pueden modificar varios parámetros a la vez. Por ejemplo, se puede acelerar la tasa de mensajes, cambiar la bandera clean_session, etc.

Elaboración del código Lua

Tenga en cuenta que he actualizado las comprobaciones iniciales para que se lean:

doc = mongodb.find_one(pool, "vmq_acl_auth",
                         {client_id = reg.client_id,
                          nombre de usuario = reg.nombre de usuario})

Omitiendo así la comprobación del punto de montaje. Como vamos a establecer el punto de montaje desde la base de datos, no nos importa el punto de montaje inicial del cliente (que será "" una cadena vacía) muy probablemente.

Devuelvo el punto de montaje como leído de la base de datos, y establezco el client_id como el original que se nos pasó durante la solicitud de autenticación. En este punto el usuario ya está autenticado contra la base de datos.

Recarga de la secuencia de comandos

Puede simplemente recargar el script después de actualizarlo en tiempo de ejecución, utilizando la siguiente línea de comando:

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

VerneMQ no necesita ser apagado y reiniciado para esto. ¡Esto es bueno, ya que acelera el desarrollo y las pruebas inmensamente!

Depuración (mostrando las sesiones)

Simplemente utilice

vmq-admin session show

imagen

Como puede ver, también se muestran los puntos de montaje.

Esto corresponde a la información de la base de datos esperada que tengo:

imagen

El client_id shahrukh se supone que tiene un punto de montaje vacío, y el client_id goat se supone que tiene el punto de montaje beardedgoat.

Tenga en cuenta que las ACL en este punto son muy permisivas para permitir una depuración más fácil:

imagen

Bonus: comandos mosquitto_sub para probar

mosquitto_sub -t "#" -u "o2PTwBb" -P "Dis8gJ2yhdSYmkQBH1mCosxgJmAxCQm3698tg7Mh8mFAHXDf4" -host 192.168.1.2 -port 1883 -id "shahrukh" -q 2 -verbose

mosquitto_sub -t "#" -u "o2PTwBb" -P "Dis8gJ2yhdSYmkQBH1mCosxgJmAxCQm3698tg7Mh8mFAHXDf4" -host 192.168.1.2 -port 1883 -id "goat" -q 2 -verbose

imagen

Acerca de Erlang

Erlang es un lenguaje desarrollado en Ericsson específicamente para sistemas de telecomunicaciones de alta disponibilidad y tolerancia a fallos.

Tiene algunas características interesantes, por si te interesa, leer más en Wikipedia sobre el tema.

El principal reto para entender el código de Erlang es que es bastante diferente a todo lo que he encontrado hasta ahora.

Erlang es un lenguaje funcional. Esto significa que no se escribe código que diga "haz esto, haz aquello, mira la variable, luego haz esto, haz aquello", sino que todo se llama como una función con un valor de retorno.

Por ejemplo, en lugar de bucles, tendrá funciones que se llamen entre sí recursivamente.

Además, el tiempo de ejecución de Erlang hace coincidir la función correcta que necesita llamar dependiendo de los parámetros.

Por ejemplo, para una función / bucle recursivo, continuará llamando a la función hasta que se alcance un determinado punto. Por ejemplo, usted ha procesado el último elemento de la lista, y la lista está vacía - aquí, usted puede responder de manera diferente, en lugar de seguir recurriendo, devolver el resultado final.

Comprensión del código VerneMQ Erlang

Nota: He reproducido el código sólo con el propósito expreso de explicar lo que hace, todo el código es copyright de Octavo Labs AG.

Entendiendo un poco el código erl, para vmq_diversity_plugin.erl:

-modulo(vmq_diversity_plugin). 

el nombre del módulo aquí debe coincidir con el nombre del archivo del módulo

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

qué funciones se pueden llamar en este módulo, y el número de parámetros que esperan.

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

%%% Funciones del gancho

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

%% llamado como 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},

{puerto, Puerto},

{punto de montaje, MP},

{client_id, ClientId},

{nombre de usuario, nilify(UserName)},

{contraseña, nilify(Contraseña)},

{sesión_limpia, Sesión_limpia}]),

conv_res(auth_on_reg, Res).

all_till_ok llamará a todos los "backends" (ganchos) de autenticación disponibles hasta que uno de ellos devuelva el ok por turno, para dar a cada uno la oportunidad de autenticar al usuario.

auth_on_publish(UserName, SubscriberIdQoS, Topic, Payload, IsRetain) ->

{MP, ClientId} = subscriber_id(SubscriberId),

case vmq_diversity_cache:match_publish_acl(MP, ClientIdQoS, Topic, Payload, IsRetain) de

verdadero ->

%% Se ha encontrado una entrada válida en la caché que concede esta publicación

Bien;

Modificadores cuando is_list(Modificadores) ->

%% Se ha encontrado una entrada de caché válida que contiene modificadores

{ok, Modificadores};

falso ->

%% Se ha encontrado una entrada válida en la caché que rechaza esta publicación

{error, not_authorized};

no_cache ->

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

{punto de montaje, MP},

{client_id, ClientId},

{qos, QoS},

{topic, unword(Topic)},

{carga de pago, carga de pago},

{retain, IsRetain}]),

conv_res(auth_on_pub, Res)

fin.

nota:

SubscriberId contiene tanto mountpoint como client_id:

clip_image002

se descompone en mountpoint y client_id en la primera declaración:

{MP, ClientId} = subscriber_id(SubscriberId),

Tenga en cuenta que las variables comienzan con una letra mayúscula en Erlang. Por lo tanto, MP y ClientId son variables.

case vmq_diversity_cache:match_publish_acl(MP, ClientIdQoS, Topic, Payload, IsRetain) de

verdadero ->

%% Se ha encontrado una entrada válida en la caché que concede esta publicación

Bien;

el módulo vmq_diversity_cache es llamado con la función match_publish_acl.

el punto de montaje (MP), el ClientId, la calidad del servicio (QoS), el tema, la carga útil y el IsRetain se pasan a él.

Si esta función devuelve verdaderoel valor de retorno de la función Erlang auth_on_publish es ok.

Tenga en cuenta que en Erlang, ya que "ok" comienza con una letra minúscula, es sólo un nombre - no una variable (es, específicamente, una instancia del tipo de datos Atom). El equivalente en Crystal Lang sería probablemente Symbols.

Bien;

Tenga en cuenta que el ";" no es una terminación de la declaración, sino que debe leerse como un "else".

Modificadores cuando is_list(Modificadores) ->

%% Se ha encontrado una entrada de caché válida que contiene modificadores

{ok, Modificadores};

cuando el valor devuelto es una lista , se pasa como un valor de retorno con Modificadores - en este caso como una tupla Erlang {ok, Modificadores} (agrupando el Atom "ok" y la variable Modificadores juntos y devolviéndolos).

Tenga en cuenta que is_list es una función incorporada (BIF) de Erlang, y no algo específico de Lua /Luerl.

falso ->

%% Se ha encontrado una entrada válida en la caché que rechaza esta publicación

{error, not_authorized};

aquí en lugar de "ok" se pasa "error", junto con "not_authorized". Todos estos son Átomos, no variables - como lo es false.

no_cache ->

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

{punto de montaje, MP},

{client_id, ClientId},

{qos, QoS},

{topic, unword(Topic)},

{carga de pago, carga de pago},

{retain, IsRetain}]),

conv_res(auth_on_pub, Res)

finalmente, si la caché devuelve "no_cache", llamamos a la función all_till_ok, con "auth_on_publish", pasando un array de tuplas, buscando si algún hook puede autentificar la publicación de este mensaje.

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

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

verdadero ->

Bien;

Mods0 cuando is_list(Mods0) ->

Mods1 = convert_modifiers(HookName, Mods0),

case vmq_plugin_util:check_modifiers(HookName, Mods1) of

error ->

{error, {modificadores_inválidos, Mods1}};

Modificadores de verificación ->

{ok, CheckedModifiers}

fin;

falso ->

{error, lua_script_returned_false};

error ->

{error, lua_script_error};

{error, Reason} ->

{error, Razón};

_ ->

all_till_ok(Rest, HookName, Args)

fin;

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

aquí la función all_till_ok llama a la función vmq_diversity_script:call_function, pasando también el HookName (que se establece, por ejemplo, en auth_on_publish o auth_on_register), y los argumentos del Gancho.

Si el gancho devuelve "true", entonces el valor a devolver es "ok".

En caso contrario, si el Gancho devuelve una lista de modificadores,

los modificadores se ejecutan a través de convertir_modificadores

Dado que las variables son inmutables en Erlang - es decir, una vez que se asigna algo a una variable, no se puede reasignar a la variable, utilizamos una nueva variable para los modificadores convertidos, Mods1.

convert_modifiers(Hook, Mods0) ->

vmq_diversity_utils:convert_modifiers(Hook, Mods0).

esto sólo envuelve la función vmq_diversity_utils:convert_modifiers.

Se define aquí:

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

Convertido = lists:map(

fun(Mod) ->

convert_modifier(Hook, Mod)

fin,

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

verdadero ->

maps:from_list(Convertido);

_ ->

Convertido

fin.

Esto muestra las capacidades de Erlang para hacer coincidir. Dependiendo de qué Átomo como primera parte se llame a la función, se ejecuta una función diferente.

Si se llama con auth_on_subscribellamará a normalize_subscribe_topics, pasando una versión convertida de Mods0.

normalize_subscribe_topics(convert(Mods0));

convert se define y explica más abajo en el mismo archivo:

%% @doc convierte recursivamente un valor devuelto desde lua a un erlang

Estructura de datos %%.

convert(Val) cuando is_list(Val) ->

convert_list(Val, []);

convert(Val) cuando is_number(Val) ->

caso round(Val) de

RVal cuando RVal == Val -> RVal;

_ -> Val

fin;

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

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

convert(nil) -> undefined.

si Val (Mods0 en nuestro caso) es una lista, se llama a 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 array

convertir(Val);

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

try lista_a_átomo_existente(binario_a_lista(BinKey)) de

Clave -> {Clave, convert(Val)}

atrapar

_:_ ->

{BinKey, convert(Val)}

fin.

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

convert_list(Rest, [convert_list_item(ListItem)|Acc]);

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

Esto utiliza la recursión de Erlang. La lista se convierte un elemento a la vez, llamándose recursivamente a sí misma (y procesando cada elemento de la lista a su vez, utilizando convert_list_item). El elemento de la lista se transfiere de la izquierda a la derecha cuando se ha procesado, de modo que la variable de la izquierda termina como una lista vacía. Una vez que lo haga, la segunda parte coincidirá:

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

y el resultado de la función será lists:reverse(Acc) (el lado derecho).

convert_list_item está usando algunas funciones de Erlang, actualmente no entiendo esa parte completamente. Sin embargo, entiendo la primera parte:

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

%% lua array

convertir(Val);

En el caso de un array (tabla) de Lua, se desempaqueta el array y se elimina el índice de Array del elemento correspondiente.

Tenga en cuenta que en Lua el tabla es el único tipo de array asociativo / hash / .... No hay ningún tipo de array específico.

Volver a la función all_till_ok:

case vmq_plugin_util:check_modifiers(HookName, Mods1) of

error ->

{error, {modificadores_inválidos, Mods1}};

Modificadores de verificación ->

{ok, CheckedModifiers}

fin;

Los modificadores convertidos se pasan a vmq_plugin_util:check_modifiers (es decir, la función check_modifiers del módulo vmq_plugin_util).

Si esta función devuelve un error, el valor de retorno es una tupla de {error, {modificadores_inválidos, Mods1}};

error y invalid_modifiers son, recordemos, sólo nombres. Además, los modificadores se pasan para nuestra inspección. (De nuevo, observe el punto y coma al final de la primera parte de la declaración, que indica un "else")

si la función devuelve una variable en su lugar, devolvemos una tupla de {ok, CheckedModifiers}.

Esta función check_modifiers se implementa aquí:

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 línea nos dice que la función check_modifiers espera un átomo (es decir, auth_on_subscribe) como primer parámetro, y una lista() o un mapa() como segundo parámetro. Devuelve una lista(), o un mapa(), o un error (un átomo()).

clip_image004

Una función intimidante, lo admito sinceramente. Vamos a recorrerla:

AllowedModifiers = modificadores(Hook),

AllowedModifiers es una variable. Llama a los modificadores de la función, pasando la variable Hook.

Por ejemplo, para auth_on_register, coincidirá con la siguiente función:

modificadores(auth_on_register) ->

[{permitir_registro, fun val_bool/1},

{permitir_publicar, fun val_bool/1},

{allow_subscribe, fun val_bool/1},

{allow_unsubscribe, fun val_bool/1},

{tamaño_del_mensaje_máximo, fun val_int/1},

{subscriber_id, fun val_subscriber_id/1},

{clean_session, fun val_bool/1},

{tasa_mensaje_máxima, fun val_int/1},

{máximo_de_vuelo_de_mensajes, fun val_int/1},

{política_suscripción_compartida, fun val_atom/1},

{intervalo_de_reintento, fun val_int/1},

{actualizar_qos, fun val_bool/1},

{allow_multiple_sessions, fun val_bool/1},

{máximo_de_mensajes_en_línea, fun val_int/1},

{máximo_de_mensajes_en_línea, fun val_int/1},

{modo_de_entrega_de_cola, fun val_atom/1},

{tipo_cola, fun val_atom/1},

{tiempo_de_drenaje_máximo, fun val_int/1},

{máx_msgs_por_paso_de_drenaje, fun val_int/1}];

por lo que devolverá una matriz de tuplas. En estas tuplas se definen los nombres permitidos (que pueden ser modificados), por ejemplo, el subscriber_id. (¡recuerda que el subscriber_id contiene tanto el punto de montaje como el client_id!) , y la función con la que se comprobará el valor. p.ej. fun val_subscriber_id/1 significa que se comprobará la función val_subscriber_id, con 1 parámetro que se le pasará.

Para entender las siguientes declaraciones, tenemos que mirar un poco la documentación de Erlang:

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

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

foldl llamará a una función sobre elementos sucesivos de una lista

Así es como se define.

foldl(Fun, Acc0, List) -> Acc1

así que pasamos una función, una lista vacía y nuestros modificadores.

explicación de la lista vacía: "Acc0 se devuelve si la lista está vacía" - es decir, si nuestra lista inicial de modificadores está vacía, devolvemos una lista vacía.

El "_" significa una variable anónima. Esto significa que la variable es necesaria, pero su valor puede ser ignorado.

Ver http://erlang.org/doc/reference_manual/expressions.html para más detalles.

por lo tanto, si la función se llama con algo, y el error como la segunda variable pasada, el resultado es error.

de lo contrario, si se llama con una tupla {ModKey, ModVal} y Acc, el valor del resultado depende de si la clave se encuentra en la lista de AllowedModifiers. Si no se encuentra (false), el resultado es error.

keyfind devolverá una tupla si se encuentra la clave (incluyendo la clave), en caso contrario, false.

Como ya hemos determinado que conocemos la clave, está en la lista, podemos ignorarla usando la variable anónima "_", y centrarnos en el ValidatorFun (función validadora).

Aquí, entonces ModVal se ejecuta a través de la función del validador (que se define en la función de modificadores apropiada que emparejamos).

Si la función devuelve true, entonces la tupla de ModKey y ModVal es devuelta (está bien y ha sido comprobada) junto con el resto de Acc.

Si es falso, se registrará un error (no se puede validar el modificador) y se devolverá un error.

Si se trata de una tupla con ok y NewModVal, se utilizará ModKey y NewModVal.

Echemos un vistazo a val_subscriber_id, que nos permite modificar el suscriptor, y por lo tanto cambiar el punto de montaje:

clip_image006

si pasamos una lista, se hacen más comprobaciones. En caso contrario, se devuelve false.

La lista tiene que contener tanto "client_id" como "mountpoint". El resto del código lo entiendo mal por el momento.

Si esta primera afirmación no coincide, también devolvemos false.

El resultado

Ver código Lua en la introducción, lo que nos propusimos lograr se ha logrado, el punto de montaje es ahora uno personalizado para cada cliente:

clip_image007

Referencias:

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 (este tema me indicó la dirección correcta, ¡muchas gracias!)

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