Comprendere Erlang & Lua / Luerl per VerneMQ MongoDB auth_on_register hook
Il mio obiettivo / TLDR
Il mio obiettivo con questo post è di spiegare come impostare punti di montaggio personalizzati per VerneMQ modificando lo script MongoDB auth Lua (lua/auth/mongodb.lua).
L'impostazione di un mountpoint personalizzato è possibile con VerneMQ non solo impostando manualmente i mountpoint per specifici ascoltatori (ad esempio le porte), ma anche programmaticamente durante l'autorizzazione nei vostri script.
Ho avuto difficoltà a capire, perché non ci sono esempi, e non avevo programmato in un linguaggio funzionale (come Erlang) prima. Inoltre, non avevo toccato Lua prima - ma Lua è più facile da capire di Erlang IMHO.
perché i punti di montaggio personalizzati?
L'idea è quella di isolare diversi utenti uno contro l'altro (multi-tenancy). Ogni utente avrà il proprio albero degli argomenti, non ci sarà bisogno di controllare le collisioni o di chiedere agli utenti di aggiungere un ulteriore prefisso.
L'isolamento totale aumenta anche la sicurezza in caso di impostazione accidentale delle ACL in modo errato.
Dal mio punto di vista quindi un must!
modifiche necessarie allo script
La modifica necessaria dello script è la seguente (incollata come codice e di nuovo come screenshot, così puoi vedere dove WordPress incasina la formattazione, ecc:)
funzione auth_on_register(reg)
se reg.username ~= nil e reg.password ~= nil allora
doc = mongodb.find_one(pool, "vmq_acl_auth",
{client_id = reg.client_id,
username = reg.username})
se doc ~= falso allora
se doc.active allora
se doc.passhash == bcrypt.hashpw(reg.password, doc.passhash) allora
cache_insert(
doc.mountpoint,
reg.client_id,
reg.username,
doc.publish_acl,
doc.subscribe_acl
)
reg.mountpoint = doc.mountpoint
- in alternativa restituisce solo true, ma allora nessun modificatore può essere impostato
ritorno {
subscriber_id = {
mountpoint = doc.mountpoint,
client_id = reg.client_id
}
}
fine
fine
fine
fine
restituire falso
fine
Naturalmente, potete anche restituire altri modificatori. Ecco una lista più esaustiva per auth_on_register dal file Documentazione su VerneMQ:
Nota: è importante fornire i tipi corretti:
il subscriber_id è un tipo più complesso che consiste in una tupla (dal punto di vista di Erlang) di mountpoint e client_id.
Ecco perché passo in una tabella di una tabella (nella terminologia di Lua):
ritorno {
subscriber_id = {
mountpoint = doc.mountpoint,
client_id = reg.client_id
}
}
Nota: la formattazione non è molto importante per Lua, l'ho solo impostata in questo modo per una migliore leggibilità.
Diversi parametri possono essere modificati contemporaneamente. Per esempio, si può accelerare la frequenza dei messaggi, cambiare il flag clean_session, ecc.
Elaborazione del codice Lua
Notate che ho aggiornato i controlli iniziali per leggere:
doc = mongodb.find_one(pool, "vmq_acl_auth",
{client_id = reg.client_id,
username = reg.username})
Omettendo così il controllo del punto di montaggio. Poiché stiamo per impostare il punto di montaggio dal database, non ci interessa il punto di montaggio iniziale del client (che sarà "" una stringa vuota) molto probabilmente.
Restituisco il mountpoint come letto dal database, e imposto il client_id come quello originale passato durante la richiesta di autenticazione. A questo punto l'utente è già autenticato con il database.
Ricaricare lo script
Potete semplicemente ricaricare lo script dopo averlo aggiornato run-time, usando la seguente linea di comando:
vmq-admin script reload path=./share/lua/auth/mongodb.lua
VerneMQ non ha bisogno di essere spento e riavviato per questo. Questo è un bene, perché accelera immensamente lo sviluppo e i test!
Debug (mostrando le sessioni)
Basta usare
vmq-admin mostra la sessione
Come potete vedere, vengono visualizzati anche i punti di montaggio.
Questo corrisponde alle informazioni attese del database che ho:
Il client_id shahrukh dovrebbe avere un mountpoint vuoto, e il client_id goat dovrebbe avere il mountpoint beardedgoat.
Notate che le ACL a questo punto sono molto permissive per permettere un debug più facile:
Bonus: comandi mosquitto_sub per i test
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
Informazioni su Erlang
Erlang è un linguaggio che è stato sviluppato alla Ericsson specificamente per sistemi di telecomunicazione ad alta disponibilità e tolleranza ai guasti.
Ha alcune caratteristiche interessanti, se siete interessati, leggere di più su Wikipedia.
La sfida principale per capire il codice Erlang è che è abbastanza diverso da tutto ciò che ho incontrato finora.
Erlang è un linguaggio funzionale. Questo significa che non si scrive codice che dice "fai questo, fai quello, guarda la variabile, poi fai questo, fai quello", ma che tutto è chiamato come una funzione con un valore di ritorno.
Ad esempio, al posto dei loop avrete funzioni che si chiamano a vicenda in modo ricorsivo.
Inoltre, il run-time di Erlang abbina la funzione corretta che deve chiamare a seconda dei parametri.
Ad esempio, per una funzione/ciclo ricorsivo, continuerete a chiamare la funzione fino a quando non viene raggiunto un certo punto. Per esempio, avete elaborato l'ultimo elemento della lista, e la lista è vuota - qui, potete rispondere in modo diverso, invece di continuare a ricorreggere, restituire il risultato finale.
Comprendere il codice Erlang di VerneMQ
Nota: ho riprodotto il codice solo per lo scopo esplicito di spiegare cosa fa, tutto il codice è copyright di Octavo Labs AG.
Capendo un po' il codice erl, per vmq_diversity_plugin.erl:
-modulo(vmq_diversity_plugin).
il nome del modulo qui deve corrispondere al nome del file del modulo
-export([auth_on_register/5, ... etc]).
quali funzioni possono essere chiamate in questo modulo, e il numero di parametri che si aspettano.
%%%===================================================================
%%% Funzioni del gancio
%%%===================================================================
%% chiamato come un gancio 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).
tutti_fino_all_ok chiamerà tutti i "backend" di autenticazione disponibili (hooks) fino a quando uno non restituirà ok a sua volta, per dare a ciascuno la possibilità di autenticare l'utente.
auth_on_publish(UserName, SubscriberId, QoS, Topic, Payload, IsRetain) ->
{MP, ClientId} = subscriber_id(SubscriberId),
caso vmq_diversity_cache:match_publish_acl(MP, ClientId, QoS, Topic, Payload, IsRetain) di
vero ->
%% Trovata una voce della cache valida che concede questa pubblicazione
Ok;
Modificatori quando is_list(Modificatori) ->
%% Trovata una voce della cache valida contenente modificatori
{ok, Modificatori};
falso ->
%% Trovata una voce della cache valida che rifiuta questa pubblicazione
{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)
fine.
nota:
SubscriberId contiene sia mountpoint che client_id:
viene scompattato in mountpoint e client_id nella prima dichiarazione:
{MP, ClientId} = subscriber_id(SubscriberId),
Si noti che le variabili iniziano con una lettera maiuscola in Erlang. MP e ClientId sono quindi variabili.
caso vmq_diversity_cache:match_publish_acl(MP, ClientId, QoS, Topic, Payload, IsRetain) di
vero ->
%% Trovata una voce della cache valida che concede questa pubblicazione
Ok;
il modulo vmq_diversity_cache è chiamato con la funzione match_publish_acl.
il punto di montaggio (MP), ClientId, Quality of Service (QoS), Topic, Payload e IsRetain sono passati in esso.
Se questa funzione restituisce veroil valore di ritorno della funzione Erlang auth_on_publish è ok.
Si noti che in Erlang, poiché "ok" inizia con una lettera minuscola, è solo un nome - non una variabile (è, in particolare, un'istanza del datatype Atom). L'equivalente in Crystal Lang sarebbe probabilmente Symbols.
Ok;
Si noti che il ";" non è una terminazione della dichiarazione, ma dovrebbe essere letto come un "else".
Modificatori quando is_list(Modificatori) ->
%% Trovata una voce della cache valida contenente modificatori
{ok, Modificatori};
quando il valore restituito è una lista, viene passato come valore di ritorno con Modifiers - in questo caso come una tupla Erlang {ok, Modifiers} (raggruppando l'Atom "ok" e la variabile Modifiers insieme e restituendoli).
Notate che is_list è una funzione integrata (BIF) di Erlang, e non qualcosa di specifico di Lua/Luerl.
falso ->
%% Trovata una voce della cache valida che rifiuta questa pubblicazione
{error, not_authorized};
qui invece di "ok" viene passato "error", insieme a "not_authorized". Questi sono tutti Atomi, non variabili - come 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)
infine, se la cache restituisce "no_cache", chiamiamo la funzione all_till_ok, con "auth_on_publish", passando un array di tuple, cercando se qualche hook può autenticare la pubblicazione di questo messaggio.
all_till_ok([Pid|Rest], HookName, Args) ->
caso vmq_diversity_script:call_function(Pid, HookName, Args) di
vero ->
Ok;
Mods0 quando is_list(Mods0) ->
Mods1 = convert_modifiers(HookName, Mods0),
caso vmq_plugin_util:check_modifiers(HookName, Mods1) di
errore ->
{errore, {invalid_modifiers, Mods1}};
Modificatori controllati ->
{ok, CheckedModifiers}
fine;
falso ->
{errore, lua_script_returned_false};
errore ->
{errore, lua_script_error};
{errore, Motivo} ->
{error, Reason};
_ ->
all_till_ok(Rest, HookName, Args)
fine;
all_till_ok([], _, _) -> prossimo.
qui la funzione all_till_ok chiama la funzione vmq_diversity_script:call_function, passando anche l'HookName (che è impostato ad es. su pubblicazione o auth_on_register), e gli argomenti del Gancio.
Se il gancio restituisce "true", allora il valore da restituire è "ok".
Altrimenti se Hook restituisce una lista di modificatori,
i modificatori vengono eseguiti attraverso convertire_modificatori
Poiché le variabili sono immutabili in Erlang - cioè, una volta che si assegna qualcosa a una variabile, non si può riassegnare alla variabile, usiamo una nuova variabile per i modificatori convertiti, Mods1.
convertire_modificatori(Hook, Mods0) ->
vmq_diversity_utils:convert_modifiers(Hook, Mods0).
questo avvolge semplicemente la funzione vmq_diversity_utils:convert_modifiers.
È definito qui:
convert_modifiers(auth_on_subscribe, Mods0) ->
normalize_subscribe_topics(convert(Mods0));
convert_modifiers(on_unsubscribe, Mods0) ->
convertire(Mods0);
convertire_modificatori(Hook, Mods0) ->
Mods1 = atomize_keys(Mods0),
Convertito = liste:mappa(
fun(Mod) ->
convert_modifier(Hook, Mod)
fine,
Mods1),
caso lists:member(Hook, [auth_on_register_m5,
auth_on_subscribe_m5,
auth_on_unsubscribe_m5,
on_unsubscribe_m5,
auth_on_publish_m5,
su_deliver_m5,
on_auth_m5]) di
vero ->
maps:from_list(Converted);
_ ->
Convertito
fine.
Questo mostra le capacità di Erlang di abbinamento. A seconda di quale atomo come prima parte viene chiamata la funzione, viene eseguita una funzione diversa.
Se chiamato con auth_on_subscribechiamerà normalize_subscribe_topics, passando una versione convertita di Mods0.
normalize_subscribe_topics(convert(Mods0));
convert è definito e spiegato più in basso nello stesso file:
%% @doc converte ricorsivamente un valore restituito da lua in un erlang
Struttura dati %%.
convert(Val) quando is_list(Val) ->
convert_list(Val, []);
convert(Val) quando è_numero(Val) ->
caso round(Val) di
RVal quando RVal == Val -> RVal;
_ -> Val
fine;
convert(Val) quando is_binary(Val) -> Val;
convert(Val) quando is_boolean(Val) -> Val;
convertire(nil) -> non definito.
se Val (Mods0 nel nostro caso) è una lista, viene chiamato 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}) quando is_integer(Idx) ->
%% array lua
convertire (Val);
convert_list_item({BinKey, Val}) quando is_binary(BinKey) ->
prova list_to_existing_atom(binary_to_list(BinKey)) di
Key -> {Key, convert(Val)}
catturare
_:_ ->
{BinKey, convert(Val)}
fine.
convert_list([ListItem|Rest], Acc) ->
convert_list(Rest, [convert_list_item(ListItem)|Acc]);
convert_list([], Acc) -> lists:reverse(Acc).
Questo usa la ricorsione di Erlang. La lista viene convertita un elemento alla volta, chiamando ricorsivamente se stessa (ed elaborando ogni elemento della lista a turno, usando convert_list_item). L'elemento della lista viene trasferito da sinistra a destra quando è stato processato, in modo che la variabile di sinistra finisca come una lista vuota. Una volta fatto ciò, la seconda parte corrisponderà:
convert_list([], Acc) -> lists:reverse(Acc).
e il risultato della funzione sarà lists:reverse(Acc) (la parte destra).
convert_list_item sta usando alcune funzioni Erlang, attualmente non capisco completamente quella parte. Capisco però la prima parte:
convert_list_item({Idx, Val}) quando is_integer(Idx) ->
%% array lua
convertire (Val);
Per un array Lua (tabella), l'array viene scompattato e l'indice Array dell'elemento corrispondente viene eliminato.
Si noti che in Lua la funzione tabella è l'unico tipo di array associativo / hash / .... Non c'è un tipo di array specifico.
Torniamo alla funzione all_till_ok:
caso vmq_plugin_util:check_modifiers(HookName, Mods1) di
errore ->
{errore, {invalid_modifiers, Mods1}};
Modificatori controllati ->
{ok, CheckedModifiers}
fine;
I modificatori convertiti sono passati in vmq_plugin_util:check_modifiers (cioè la funzione check_modifiers nel modulo vmq_plugin_util).
Se questa funzione restituisce un errore, il valore di ritorno è una tupla di {error, {invalid_modifiers, Mods1}};
error e invalid_modifiers sono, ricordate, solo nomi. Inoltre i modificatori vengono passati per la nostra ispezione. (Di nuovo, notate il punto e virgola alla fine della prima parte della dichiarazione, che indica un "altro")
se invece la funzione restituisce una variabile, restituiamo una tupla di {ok, CheckedModifiers}.
Questa funzione check_modifiers è implementata qui:
-spec check_modifiers(atom(), list() | map()) -> list() | map() | error.
Questa linea ci dice che la funzione check_modifiers si aspetta un atomo (cioè auth_on_subscribe) come primo parametro, e una lista() o una mappa() come secondo parametro. Restituisce una lista(), o una mappa(), o un errore (un atomo()).
Una funzione intimidatoria, lo ammetto onestamente. Passiamo attraverso di essa:
AllowedModifiers = modificatori(Hook),
AllowedModifiers è una variabile. Chiama i modificatori di funzione, con la variabile Hook che viene passata.
Per esempio, per auth_on_register, corrisponderà alla seguente funzione:
modificatori(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}];
quindi restituirà un array di tuple. In queste tuple vengono definiti i nomi consentiti (che possono essere modificati), ad esempio il subscriber_id. (ricordate, il subscriber_id contiene sia il mountpoint che il client_id!), e la funzione con cui controllare il valore. es. fun val_subscriber_id/1 significa che la funzione val_subscriber_id deve essere controllata, con 1 parametro da passarle.
Per capire le prossime affermazioni, dobbiamo guardare un po' di documentazione di Erlang:
liste:foldl(fun (_, error) -> error;
http://erlang.org/doc/man/lists.html
foldl chiamerà una funzione su elementi successivi di una lista
Questo è il modo in cui è definito.
foldl(Fun, Acc0, List) -> Acc1
quindi passiamo una funzione, una lista vuota e i nostri modificatori.
spiegazione per la lista vuota: "Acc0 viene restituito se la lista è vuota" - cioè se la nostra lista iniziale di modificatori è vuota, restituiamo una lista vuota.
Il "_" indica una variabile anonima. Ciò significa che la variabile è richiesta, ma il suo valore può essere ignorato.
Vedere http://erlang.org/doc/reference_manual/expressions.html per i dettagli.
così, se la funzione viene chiamata con qualcosa e l'errore come seconda variabile passata, il risultato è errore.
altrimenti, se chiamato con una tupla {ModKey, ModVal} e Acc, il valore del risultato dipende dal fatto che la chiave sia trovata nella lista di AllowedModifiers. Se non viene trovata (false), allora il risultato è errore.
keyfind restituisce una tupla se la chiave viene trovata (inclusa la chiave), altrimenti false.
Dato che abbiamo già determinato che conosciamo la chiave, è nella lista, possiamo ignorarla usando la variabile anonima "_", e concentrarci sulla ValidatorFun (funzione del validatore).
Qui, poi ModVal viene eseguito attraverso la funzione di validazione (che è definita nella funzione appropriata dei modificatori che abbiamo abbinato).
Se la funzione restituisce true, allora la tupla di ModKey e ModVal viene restituita (è ok ed è stata controllata) insieme al resto di Acc.
Se è falso, verrà registrato un errore (non può convalidare il modificatore) e verrà restituito error.
Se è una tupla con ok e NewModVal, allora ModKey e NewModVal saranno usati.
Diamo un'occhiata a val_subscriber_id, che ci permette di modificare il sottoscrittore, e quindi di cambiare il punto di montaggio:
se passiamo una lista, allora vengono fatti ulteriori controlli. Altrimenti, viene restituito false.
La lista deve contenere sia "client_id" che "mountpoint". Il resto del codice è poco comprensibile per me al momento.
Se questa prima affermazione non corrisponde, restituiamo anche false.
Il risultato
Vedere il codice Lua nell'introduzione, ciò che ci siamo prefissati di ottenere è stato raggiunto, il punto di montaggio è ora un punto personalizzato per ogni client:
Riferimenti:
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 (questo numero mi ha indicato la direzione giusta, grazie mille!)