Verstehen von Erlang & Lua / Luerl für VerneMQ MongoDB auth_on_register hook

Mein Ziel / TLDR

In diesem Blogpost möchte ich erklären, wie man benutzerdefinierte Mountpoints für VerneMQ setzt, indem man das mitgelieferte MongoDB auth Lua Skript (lua/auth/mongodb.lua) modifiziert.

Das Setzen eines benutzerdefinierten Mountpoints ist mit VerneMQ nicht nur durch manuelles Setzen von Mountpoints für bestimmte Listener (z.B. Ports) möglich, sondern auch programmatisch während der Autorisierung in Ihren Skripten.

Es fiel mir schwer, mich damit zurechtzufinden, da es keine Beispiele gibt und ich noch nie in einer funktionalen Sprache (wie Erlang) programmiert hatte. Außerdem hatte ich vorher noch nie mit Lua zu tun gehabt - aber Lua ist IMHO leichter zu verstehen als Erlang.

warum benutzerdefinierte Einhängepunkte?

Die Idee ist, verschiedene Benutzer gegeneinander abzugrenzen (Multi-Tenancy). Jeder Benutzer wird seinen eigenen Themenbaum haben, es wird nicht nötig sein, auf Kollisionen zu prüfen oder von den Benutzern zu verlangen, ein zusätzliches Präfix hinzuzufügen.

Die vollständige Isolierung erhöht auch die Sicherheit vor versehentlich falsch gesetzten ACLs.

Aus meiner Sicht also ein Muss!

notwendige Skriptänderungen

Die notwendige Änderung des Skripts sieht wie folgt aus (als Code eingefügt und noch einmal als Screenshot, damit Sie sehen können, wo WordPress die Formatierung durcheinanderbringt usw.):

function auth_on_register(reg)
     wenn reg.username ~= nil und reg.password ~= nil dann
         doc = mongodb.find_one(pool, "vmq_acl_auth",
                                 {client_id = reg.client_id,
                                  benutzername = reg.benutzername})
         if doc ~= false then
             wenn doc.active dann
                 if doc.passhash == bcrypt.hashpw(reg.password, doc.passhash) then
                     cache_insert(
                         doc.mountpoint,
                         reg.client_id,
                         reg.username,
                         doc.publish_acl,
                         doc.subscribe_acl
                         )
                     reg.mountpoint = doc.mountpoint
                     - alternativ nur true zurückgeben, aber dann können keine Modifikatoren gesetzt werden
                     zurück {
                         subscriber_id = {
                                 Einhängepunkt = doc.mountpoint,
                                 client_id = reg.client_id
                             }
                         }

                 Ende
             Ende
         Ende
     Ende
     return false
Ende

Bild

Natürlich können Sie auch andere Modifikatoren zurückgeben. Hier ist eine ausführlichere Liste für auth_on_register aus der VerneMQ-Dokumentation:

Bild

Hinweis: Es ist wichtig, die richtigen Typen anzugeben:

Bild

die subscriber_id ist ein komplexerer Typ, der aus einem Tupel (aus der Sicht von Erlang) von mountpoint und client_id besteht.

Das ist der Grund, warum ich eine Tabelle einer Tabelle übergebe (in der Terminologie von Lua):

                    zurück {
                         subscriber_id = {
                                 Einhängepunkt = doc.mountpoint,
                                 client_id = reg.client_id
                             }
                         }

Hinweis: Die Formatierung ist für Lua ziemlich unwichtig, ich habe sie nur der besseren Lesbarkeit wegen so eingestellt.

Es können mehrere Parameter auf einmal geändert werden. Sie können zum Beispiel die Nachrichtenrate drosseln, das clean_session-Flag ändern usw.

Ausarbeitung des Lua-Codes

Beachten Sie, dass ich die anfänglichen Prüfungen aktualisiert habe:

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

Dadurch entfällt die Überprüfung des Einhängepunkts. Da wir den Einhängepunkt von der Datenbank aus setzen werden, ist der anfängliche Einhängepunkt des Clients (der höchstwahrscheinlich "", ein leerer String, sein wird) egal.

Ich gebe den Einhängepunkt als aus der Datenbank gelesen zurück und setze die client_id als die ursprüngliche, die uns bei der Authentifizierungsanfrage übergeben wurde. Zu diesem Zeitpunkt ist der Benutzer bereits gegenüber der Datenbank authentifiziert.

Erneutes Laden des Skripts

Sie können das Skript nach einer Aktualisierung während der Laufzeit einfach mit der folgenden Befehlszeile neu laden:

vmq-admin-Skript neu laden path=./share/lua/auth/mongodb.lua

VerneMQ muss dafür nicht heruntergefahren und neu gestartet werden. Das ist gut, denn es beschleunigt die Entwicklung und das Testen immens!

Debugging (Sitzungen anzeigen)

Verwenden Sie einfach

vmq-admin Sitzung anzeigen

Bild

Wie Sie sehen können, werden auch die Einhängepunkte angezeigt.

Dies entspricht den erwarteten Datenbankinformationen, die ich habe:

Bild

Die client_id shahrukh soll einen leeren Einhängepunkt haben, und die client_id goat soll den Einhängepunkt beardedgoat haben.

Beachten Sie, dass die ACLs an dieser Stelle sehr freizügig sind, um eine leichtere Fehlersuche zu ermöglichen:

Bild

Bonus: mosquitto_sub-Befehle zum Testen

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

Bild

Über Erlang

Erlang ist eine Sprache, die bei Ericsson speziell für hochverfügbare, fehlertolerante Telekommunikationssysteme entwickelt wurde.

Es hat einige interessante Funktionen, falls Sie daran interessiert sind, Lesen Sie mehr auf Wikipedia darüber.

Die größte Herausforderung beim Verstehen von Erlang-Code besteht darin, dass er sich von allem unterscheidet, was ich bisher kennengelernt habe.

Erlang ist eine funktionale Sprache. Das bedeutet, dass man keinen Code schreibt, der sagt "tu dies, tu das, schau auf die Variable, dann tu dies, tu das", sondern dass alles als Funktion mit einem Rückgabewert aufgerufen wird.

z.B. anstelle von Schleifen werden Sie Funktionen haben, die sich gegenseitig rekursiv aufrufen.

Außerdem wählt die Erlang-Laufzeit abhängig von den Parametern die richtige Funktion aus, die sie aufrufen muss.

Z.B. bei einer rekursiven Funktion/Schleife rufen Sie die Funktion so lange auf, bis ein bestimmter Punkt erreicht ist. Zum Beispiel, Sie haben das letzte Element der Liste verarbeitet, und die Liste ist leer - hier können Sie anders reagieren, anstatt weiter zu rekursieren, geben Sie das Endergebnis zurück.

VerneMQ Erlang Code verstehen

Hinweis: Ich habe den Code nur reproduziert, um zu erklären, was er tut. Der gesamte Code unterliegt dem Copyright von Octavo Labs AG.

Ein wenig Verständnis für den Erl-Code, für vmq_diversity_plugin.erl:

-module(vmq_diversity_plugin). 

der Modulname muss hier mit dem Dateinamen des Moduls übereinstimmen

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

welche Funktionen in diesem Modul aufgerufen werden können, und die Anzahl der Parameter, die sie erwarten.

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

%%% Hakenfunktionen

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

%% wird als all_till_ok-Haken aufgerufen

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

{Bergpunkt, MP},

{client_id, ClientId},

{Benutzername, nilify(UserName)},

{Passwort, nilify(Passwort)},

{clean_session, CleanSession}]),

conv_res(auth_on_reg, Res).

all_till_ok ruft alle verfügbaren Authentifizierungs-"Backends" (Hooks) auf, bis einer nach dem anderen "ok" zurückgibt, um jedem eine Chance zu geben, den Benutzer zu authentifizieren.

auth_on_publish(UserName, SubscriberId, QoS, Topic, Payload, IsRetain) ->

{MP, ClientId} = subscriber_id(SubscriberId),

case vmq_diversity_cache:match_publish_acl(MP, ClientId, QoS, Topic, Payload, IsRetain) von

wahr ->

%% Es wurde ein gültiger Cache-Eintrag gefunden, der diese Veröffentlichung erlaubt

OK;

Modifikatoren wenn is_list(Modifikatoren) ->

%% Es wurde ein gültiger Cache-Eintrag mit Modifikatoren gefunden

{ok, Modifikatoren};

falsch ->

%% Es wurde ein gültiger Cache-Eintrag gefunden, der diese Veröffentlichung ablehnt

{error, not_authorized};

no_cache ->

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

{Bergpunkt, MP},

{client_id, ClientId},

{qos, QoS},

{topic, unword(Topic)},

{Payload, Payload},

{retain, IsRetain}]),

conv_res(auth_on_pub, Res)

Ende.

Anmerkung:

SubscriberId enthält sowohl mountpoint als auch client_id:

clip_image002

wird in der ersten Anweisung in mountpoint und client_id entpackt:

{MP, ClientId} = subscriber_id(SubscriberId),

Beachten Sie, dass Variablen in Erlang mit einem Großbuchstaben beginnen. MP und ClientId sind daher Variablen.

case vmq_diversity_cache:match_publish_acl(MP, ClientId, QoS, Topic, Payload, IsRetain) von

wahr ->

%% Es wurde ein gültiger Cache-Eintrag gefunden, der diese Veröffentlichung erlaubt

OK;

das Modul vmq_diversity_cache wird mit der Funktion match_publish_acl aufgerufen.

werden der Mountpoint (MP), ClientId, Quality of Service (QoS), Topic, Payload und IsRetain übergeben.

Wenn diese Funktion den Wert wahrist der Rückgabewert der Erlang-Funktion auth_on_publish ok.

Beachten Sie, dass in Erlang, da "ok" mit einem Kleinbuchstaben beginnt, es nur ein Name und keine Variable ist (es handelt sich um eine Instanz des Datentyps Atom). Das Äquivalent in Crystal Lang wäre wahrscheinlich "Symbole".

OK;

Beachten Sie, dass das ";" keine Beendigung der Anweisung ist, sondern als "else" gelesen werden sollte.

Modifikatoren wenn is_list(Modifikatoren) ->

%% Es wurde ein gültiger Cache-Eintrag mit Modifikatoren gefunden

{ok, Modifikatoren};

wenn der zurückgegebene Wert eine Liste ist, wird er als Rückgabewert mit Modifikatoren übergeben - in diesem Fall als Erlang-Tupel {ok, Modifikatoren} (wobei das Atom "ok" und die Variable Modifikatoren zusammen gruppiert und zurückgegeben werden).

Beachten Sie, dass is_list eine eingebaute Funktion (BIF) von Erlang ist, und nicht etwas Lua/Luerl-spezifisches.

falsch ->

%% Es wurde ein gültiger Cache-Eintrag gefunden, der diese Veröffentlichung ablehnt

{error, not_authorized};

hier wird anstelle von "ok" "error" übergeben, zusammen mit "not_authorized". Dies sind alles Atome, keine Variablen - wie auch false.

no_cache ->

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

{Bergpunkt, MP},

{client_id, ClientId},

{qos, QoS},

{topic, unword(Topic)},

{Payload, Payload},

{retain, IsRetain}]),

conv_res(auth_on_pub, Res)

Wenn der Cache schließlich "no_cache" zurückgibt, rufen wir die Funktion all_till_ok mit "auth_on_publish" auf, wobei wir ein Array von Tupeln übergeben und prüfen, ob irgendein Hook die Veröffentlichung dieser Nachricht authentifizieren kann.

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

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

wahr ->

OK;

Mods0 when is_list(Mods0) ->

Mods1 = convert_modifiers(HookName, Mods0),

case vmq_plugin_util:check_modifiers(HookName, Mods1) of

Fehler ->

{Fehler, {Ungültige_Modifikatoren, Mods1}};

GeprüfteModifikatoren ->

{ok, CheckedModifiers}

Ende;

falsch ->

{Fehler, lua_script_returned_false};

Fehler ->

{Fehler, lua_script_error};

{Fehler, Grund} ->

{Fehler, Grund};

_ ->

all_till_ok(Rest, Hakenname, Args)

Ende;

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

hier ruft die Funktion all_till_ok die Funktion vmq_diversity_script:call_function auf, wobei auch der HookName übergeben wird (der z.B. auf auth_on_publish oder auth_on_register), und die Argumente für den Hook.

Wenn der Hook "true" zurückgibt, ist der zurückzugebende Wert "ok".

Andernfalls, wenn der Hook eine Liste von Modifikatoren zurückgibt,

werden die Modifikatoren durchlaufen convert_modifiers

Da Variablen in Erlang unveränderlich sind - d. h., wenn man einer Variablen etwas zugewiesen hat, kann man der Variablen nichts mehr neu zuweisen -, verwenden wir eine neue Variable für die umgewandelten Modifikatoren, Mods1.

convert_modifiers(Hook, Mods0) ->

vmq_diversity_utils:convert_modifiers(Hook, Mods0).

Dies umhüllt lediglich die Funktion vmq_diversity_utils:convert_modifiers.

Sie ist hier definiert:

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

Konvertiert = lists:map(

fun(Mod) ->

convert_modifier(Haken, Mod)

Ende,

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

wahr ->

maps:from_list(Umgewandelt);

_ ->

Umgewandelt

Ende.

Dies zeigt die Fähigkeiten von Erlang beim Matching. Je nachdem, mit welchem Atom als erstem Teil die Funktion aufgerufen wird, wird eine andere Funktion ausgeführt.

Beim Aufruf mit auth_on_subscribeauf, ruft es normalize_subscribe_topics auf und übergibt eine konvertierte Version von Mods0.

normalize_subscribe_topics(convert(Mods0));

convert wird weiter unten in derselben Datei definiert und erläutert:

%% @doc wandelt einen von lua zurückgegebenen Wert rekursiv in einen erlang

%% Datenstruktur.

convert(Val) when is_list(Val) ->

convert_list(Val, []);

convert(Val) when is_number(Val) ->

case round(Val) von

RVal wenn RVal == Val -> RVal;

_ -> Val

Ende;

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

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

convert(nil) -> undefiniert.

wenn Val (in unserem Fall Mods0) eine Liste ist, wird convert_list aufgerufen:

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

convert(Val);

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

try list_to_existing_atom(binary_to_list(BinKey)) of

Schlüssel -> {Schlüssel, convert(Val)}

fangen

_:_ ->

{BinKey, convert(Val)}

Ende.

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

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

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

Dabei wird die Rekursion von Erlang verwendet. Die Liste wird ein Element nach dem anderen umgewandelt, indem sie sich selbst rekursiv aufruft (und jedes Listenelement der Reihe nach mit convert_list_item verarbeitet). Das Listenelement wird von links nach rechts übertragen, wenn es abgearbeitet ist, so dass die linke Variable am Ende eine leere Liste ist. Sobald dies der Fall ist, wird der zweite Teil übereinstimmen:

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

und das Ergebnis der Funktion wird lists:reverse(Acc) sein (die rechte Seite).

convert_list_item verwendet einige Erlang-Funktionen, die ich derzeit nicht vollständig verstehe. Den ersten Teil verstehe ich jedoch:

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

%% lua-Array

convert(Val);

Bei einem Lua-Array (Tabelle) wird das Array ausgepackt und der Array-Index des entsprechenden Elements wird gelöscht.

Beachten Sie, dass in Lua die Tabelle ist der einzige assoziative Array / Hash / ... Typ. Es gibt keinen spezifischen Array-Typ.

Zurück zur Funktion all_till_ok:

case vmq_plugin_util:check_modifiers(HookName, Mods1) of

Fehler ->

{Fehler, {Ungültige_Modifikatoren, Mods1}};

GeprüfteModifikatoren ->

{ok, CheckedModifiers}

Ende;

Die umgewandelten Modifier werden an vmq_plugin_util:check_modifiers übergeben (d.h. an die Funktion check_modifiers im Modul vmq_plugin_util).

Wenn diese Funktion einen Fehler liefert, ist der Rückgabewert ein Tupel aus {error, {invalid_modifiers, Mods1}};

error und invalid_modifiers sind, wie gesagt, nur Namen. Zusätzlich werden die Modifikatoren zu unserer Überprüfung weitergeleitet. (Beachten Sie auch hier das Semikolon am Ende des ersten Teils der Anweisung, das auf ein "else" hinweist)

wenn die Funktion stattdessen eine Variable zurückgibt, geben wir ein Tupel aus {ok, CheckedModifiers} zurück.

Diese Funktion check_modifiers ist hier implementiert:

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

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

Diese Zeile sagt uns, dass die Funktion check_modifiers ein Atom (d.h. auth_on_subscribe) als ersten Parameter und eine list() oder eine map() als zweiten Parameter erwartet. Sie gibt eine list(), eine map() oder einen Fehler (ein atom()) zurück.

clip_image004

Eine einschüchternde Funktion, das muss ich ehrlich zugeben. Gehen wir sie einmal durch:

AllowedModifiers = modifiers(Hook),

AllowedModifiers ist eine Variable. Sie ruft die Funktion modifiers auf, wobei die Variable Hook übergeben wird.

Zum Beispiel für auth_on_registerwird sie mit der folgenden Funktion übereinstimmen:

modifiers(auth_on_register) ->

[{allow_register, fun val_bool/1},

{zulassen_veröffentlichen, fun val_bool/1},

{Abonnement zulassen, fun val_bool/1},

{allow_unsubscribe, fun val_bool/1},

{max_message_size, fun val_int/1},

{Abonnenten_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},

{Wiederholungsintervall, fun val_int/1},

{upgrade_qos, fun val_bool/1},

{Mehrere_Sitzungen zulassen, 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}];

so dass ein Array von Tupeln zurückgegeben wird. In diesen Tupeln werden die zulässigen Namen (die geändert werden können) definiert, z.B. die subscriber_id. (denken Sie daran, dass die subscriber_id sowohl den mountpoint als auch die client_id enthält!) , und die Funktion, gegen die der Wert geprüft werden soll. fun val_subscriber_id/1 bedeutet z.B., dass die Funktion val_subscriber_id geprüft werden soll, der 1 Parameter übergeben werden soll.

Um die nächsten Anweisungen zu verstehen, müssen wir ein wenig in der Erlang-Dokumentation nachschlagen:

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

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

foldl ruft eine Funktion für aufeinanderfolgende Elemente einer Liste auf

So wird sie definiert.

foldl(Fun, Acc0, Liste) -> Acc1

Wir geben also eine Funktion, eine leere Liste und unsere Modifikatoren ein.

Erklärung für die leere Liste: "Acc0 wird zurückgegeben, wenn die Liste leer ist" - das heißt, wenn unsere ursprüngliche Liste der Modifikatoren leer ist, geben wir eine leere Liste zurück.

Das "_" steht für eine anonyme Variable. Das bedeutet, dass die Variable benötigt wird, ihr Wert aber ignoriert werden kann.

Siehe http://erlang.org/doc/reference_manual/expressions.html für Einzelheiten.

Wenn die Funktion also mit "something" und "error" als zweiter übergebener Variable aufgerufen wird, ist das Ergebnis Fehler.

Andernfalls, wenn mit einem Tupel {ModKey, ModVal} und Acc aufgerufen, hängt der Ergebniswert davon ab, ob der Schlüssel in der Liste der zulässigen Modifikatoren gefunden wird. Wenn er nicht gefunden wird (false), ist das Ergebnis Fehler.

keyfind gibt ein Tupel zurück, wenn der Schlüssel gefunden wird (einschließlich des Schlüssels), andernfalls false.

Da wir bereits festgestellt haben, dass wir den Schlüssel kennen und er sich in der Liste befindet, können wir ihn mit Hilfe der anonymen Variablen "_" ignorieren und uns auf die ValidatorFun (Validatorfunktion) konzentrieren.

In diesem Fall wird ModVal durch die Validierungsfunktion (die in der entsprechenden Modifikatorfunktion definiert ist, die wir angepasst haben) geleitet.

Wenn die Funktion true zurückgibt, wird das Tupel aus ModKey und ModVal zusammen mit dem Rest von Acc zurückgegeben (es ist in Ordnung und wurde geprüft).

Wenn er falsch ist, wird ein Fehler protokolliert (Modifikator kann nicht validiert werden) und ein Fehler zurückgegeben.

Handelt es sich um ein Tupel mit ok und NewModVal, dann werden ModKey und NewModVal verwendet.

Werfen wir einen Blick auf val_subscriber_id, die es uns ermöglicht, den Abonnenten und damit den Einhängepunkt zu ändern:

clip_image006

wenn wir eine Liste übergeben, werden weitere Prüfungen durchgeführt. Andernfalls wird false zurückgegeben.

Die Liste muss sowohl "client_id" als auch "mountpoint" enthalten. Den Rest des Codes verstehe ich im Moment nur schlecht.

Wenn diese erste Aussage nicht zutrifft, geben wir ebenfalls false zurück.

Das Ergebnis

Siehe Lua-Code in der Einleitung, was wir erreichen wollten, wurde erreicht, der Einhängepunkt ist jetzt ein benutzerdefinierter für jeden Client:

clip_image007

Referenzen:

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 (diese Ausgabe hat mich auf den richtigen Weg gebracht, vielen Dank!)

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