La souplesse de nginx, sa configuration aux antipodes de l'ancestral apache.conf, mais aussi et surtout ses incroyables performances en font un des « chouchous » des acteurs du Web aujourd'hui.
Nginx dispose d'une palette de modules officiels des plus utiles, mais il est souvent intéressant de s'enquérir des contributions tierces, car certaines d'entre elles amènent parfois de véritables révolutions. Nous avions déjà parcouru dans ces colonnes les capacités du pare-feu applicatif NAXSI [1], nous allons aujourd'hui plonger dans les fabuleuses possibilités amenées par le module lua.
1. Programmer le protocole
Imaginez une seconde : qui, dans la chaîne HTTP, est le mieux placé pour tout voir passer, interagir, modifier ? Le serveur lui-même, voire le proxy inverse [2], rôle que joue avec brio nginx. Aussi, à l'aide de directives nginx et une API Lua [3] très accessible, on pourra, à la volée :
- Transformer les URI,
- Manipuler les en-têtes,
- Générer du contenu,
- Interagir avec d'autres composants,
- Intervenir dans le corps des objets transférés.
Le tout avec un véritable langage de programmation dont l'empreinte est minuscule et les performances « proches du C natif », en particulier grâce au compilateur LuaJIT [4].
En effet, afin d'obtenir les meilleures performances, il est fortement recommandé par le site du projet d'utiliser LuaJIT, le compilateur Just In Time, plutôt que l'interpréteur Lua standard dès que cela est possible. Il est intéressant de noter que l'interpréteur de LuaJIT, à l'occasion de sa version 2.0, a été réécrit en assembleur. On est loin d'autres univers moins regardants...
2. Installation
Si vous êtes l'heureux possesseur d'un système d'exploitation utilisant pkgsrc [5] comme système de paquets, la tâche sera relativement aisée puisque votre serviteur a inclus le support LuaJIT dans les paquets www/nginx et www/nginx-devel en mars 2014. Aussi, il vous suffira d'ajouter au fichier /etc/mk.conf la directive de compilation suivante :
PKG_OPTIONS.nginx+= luajit
Ou encore d'installer le paquet binaire proposé par NetBSDfr [6], qui l'inclura au moment où vous lirez ces lignes.
Utilisateurs de Debian GNU/Linux, si le paquet nginx-extras [7] inclut bien le support Lua, il ne s'agit malheureusement que de l'interpréteur standard et non de la version Jit. Cela permettra tout de même de s'essayer aux exemples que nous allons dérouler dans cet article, mais en production, cela vous priverait de performances optimales.
La compilation manuelle n'est pas beaucoup plus fastidieuse grâce au projet OpenResty [8] :
$ VERSION=1.5.8.1 # à remplacer par la version courante d’OpenResty
$ wget http://openresty.org/download/ngx_openresty-${VERSION}.tar.gz
$ tar zxvf ngx_openresty-${VERSION}.tar.gz
$ cd ngx_openresty-${VERSION}/
$ ./configure --with-luajit
$ make
$ make install
Le préfixe utilisé sera /usr/local/openresty/nginx, ce qui évitera de semer le chaos dans votre installation.
À noter que les utilisateurs de Debian GNU/Linux et Ubuntu devront s'acquitter de l'ajout préalable des paquets suivants :
# apt-get install libreadline-dev libncurses5-dev libpcre3-dev
libssl-dev perl make
Et les utilisateurs de Fedora / Red Hat :
# yum install readline-devel pcre-devel openssl-devel
3. Bonjour, AK47
Nginx disposant désormais des super-pouvoirs que lui octroie LuaJIT, nous allons pouvoir exposer notre premier -minuscule- bout de code à l'aide du langage Lua. Nous allons pour cela déclarer une location nginx particulière, que nous agrémenterons au fil de nos expériences. Pour le moment, nous déclarons le type MIME text/plain. Voici l'extrait concerné d'un fichier nginx.conf :
location /luatest {
default_type ‘text/plain’;
content_by_lua ‘ngx.say("Bonjour, Camarade.")’;
}
Notre premier morceau de Lua / nginx. Nous utilisons ici la directive content_by_lua, qui exécutera le code placé entre simple ou double quotes. On pourrait également appeler un fichier contenant du code Lua grâce à la directive content_by_lua_file à qui on passerait en paramètre le chemin vers le script.
On utilise au sein de ce « programme » la fonction say de l'API nginx de Lua. Cette fonction affiche une chaîne de caractères et la suffixe d'un retour chariot (\n).
Nous avons bien évidemment accès, depuis un morceau de code Lua, aux différentes variables de nginx, par exemple :
content_by_lua ‘ngx.print("Document root: [" .. ngx.var["document_root"] .. "]\\n")’
Ou encore :
content_by_lua ‘ngx.print("Document root: [" .. ngx.var.document_root .. "]\\n")’
Résultat :
imil@tatooine:~$ curl -o- -s http://coruscant/luatest
Document root: [/home/imil/www]
Et puisque nous avons accès à toutes les variables internes de nginx, nous pouvons conditionner des comportements utiles à l'administrateur système en quête d'informations :
if tonumber(ngx.var.upstream_response_time) > 1 then
ngx.log(ngx.WARN, "[LENT!] Reponse de l’upstream <" .. ngx.var.upstream_addr ..
">: " .. ngx.var.upstream_response_time);
end
À ce propos, en cas d'erreur de programmation, cette dernière se retrouvera naturellement dans l'error.log défini dans la configuration du serveur.
Bien entendu, on a également accès aux variables que sait manipuler le serveur :
content_by_lua ‘ngx.print("glmf vaut: <" .. ngx.var["arg_glmf"] .. ">\\n")’
Résultat :
imil@tatooine:~$ curl -o- -s http://coruscant/luatest/?glmf=OVER9000
glmf vaut: <OVER9000>
Rentrons un peu plus dans les arcanes du protocole ; parmi les informations dont dispose le serveur web, on retrouve la requête émanant du client, que l'on peut afficher sans plus de soin :
content_by_lua ‘ngx.print(ngx.req.raw_header())’
Résultat :
imil@tatooine:~$ curl -o- -s http://coruscant/luatest
GET /luatest HTTP/1.1
User-Agent: curl/7.26.0
Host: coruscant
Accept: */*
Ou encore récupérer dans un tableau associatif bien plus aisé à manipuler :
content_by_lua ‘
local head = ngx.req.get_headers()
for k, v in pairs(head) do
ngx.say("header disponible: " .. k)
end
ngx.say("Host: ", head["Host"])
‘;
Résultat :
imil@tatooine:~$ curl -o- -s http://coruscant/luatest
header disponible: user-agent
header disponible: host
header disponible: accept
Host: coruscant
Avant de servir la ressource demandée, l'API lua-nginx permet également de s'interposer dans l'échange :
local res = ngx.location.capture("/foo")
if res.status ~= 200 then
ngx.print("GOULAG! (reponse: " .. res.status .. ")\\n")
else
ngx.print(res.body)
end
Résultat :
imil@tatooine:~$ curl -o- -s http://coruscant/luatest
GOULAG! (reponse: 404)
Quelques explications s'imposent ici. L'appel à ngx.location.capture, qui est non-bloquant, comme nginx lui-même, effectue une requête interne vers la location foo ; comprendre qu'il n'y a aucune transaction TCP, HTTP ou encore d'IPC. Cela permet une efficience optimale, puisque s'articulant sur les mécanismes internes à nginx. ngx.location.capture retourne dans la variable locale res le tableau suivant :
- res.status contient le code de retour de la réponse HTTP ;
- res.header est un tableau contenant l'ensemble des en-têtes de réponse ;
- res.body contient le corps du message, par exemple le HTML ;
- res.truncated est un drapeau booléen permettant de savoir si le contenu de res.body est tronqué.
Et puisque l'on dispose du contenu retourné dans res.body, on peut a priori le transformer comme bon nous semble :
location /luatest {
default_type ‘text/plain’;
content_by_lua ‘
local res = ngx.location.capture("/")
if res.status ~= 200 then
ngx.say("GOULAG!! (reponse: " .. res.status .. ")")
else
local c, n, err = ngx.re.sub(res.body, "[uU][pP]", "down")
if c then
ngx.print(c)
else
ngx.log(ngx.ERR, "erreur: ", err)
return
end
end
‘;
}
Oui, pour ne rien gâcher, l'API lua-nginx supporte les expressions régulières.
Avec cette portion de code, lorsqu'on interroge la racine de coruscant, on obtient :
imil@tatooine:~$ curl -o- -s http://coruscant/
up.
Et lorsqu'on interroge la location /luatest :
imil@tatooine:~$ curl -o- -s http://coruscant/luatest
down.
Effrayant. À utiliser avec discernement, évidemment, mais les possibilités offertes par les capacités de matching des fonctions ngx.re.* ouvrent la porte à de multiples possibilités de contrôle de validité ou d'interaction avec des modules Lua tiers, tels que resty.redis [9] ou resty.memcached [10].
4. In mother Russia, Lua controls you.
Nous nous sommes jetés tête la première sur la directive content_by_lua, mais ce n'est pas, loin s'en faut, la seule section manipulable en Lua. Voyons ensemble les possibilités offertes par quelques-unes d'entre elles.
- set_by_lua nous permettra de réaliser des opérations complexes en Lua, afin de retourner une valeur dans une variable à la disposition de nginx. Ceux d'entre vous qui ont buté sur les limitations de la manipulation des variables au sein du serveur HTTP verront ici une façon simple d'additionner, soustraire, concaténer ou encore importer des valeurs dépendant d'une infinité de paramètres.
L'utilisation est similaire à content_by_lua si ce n'est la présence d'une variable de retour.
set $tagada ‘’;
set_by_lua $ret ‘
ngx.var.tagada = "tsointsoin"
return "et hop"
‘;
Ici, la variable nginx $tagada vaut « tsointsoin » et $ret « et hop ».
- rewrite_by_lua agit dans la phase de réécriture ; on pourra par exemple utiliser cette fonctionnalité pour rediriger immédiatement vers une location interne à l'aide de la fonction ngx.exec :
rewrite_by_lua ‘
local m, err = ngx.re.match(ngx.var.remote_addr, "192\.168\.1\.")
if m then
ngx.exec("/")
end
‘;
- access_by_lua donnera une immense souplesse dans la gestion des autorisations d'accès à une ressource. Voyons un exemple basique d'authentification dépendant d'une ressource tierce :
access_by_lua ‘
local res = ngx.location.capture("/auth")
if res.status == ngx.HTTP_OK then
return -- tout s’est bien déroulé, on poursuit
end
if res.status == ngx.HTTP_FORBIDDEN then
ngx.exit(res.status) -- le backend a renvoyé un code 403,
on le relaye et on sort
end
‘;
- Terminons cette liste d'exemples par la directive header_filter_by_lua grâce à laquelle on pourra réécrire les contenus des en-têtes de façon triviale :
ngx.header["X-Powered-By"] = "nginx baby!"
On imagine facilement ici la surcharge d'en-têtes malencontreusement laissée par un administrateur peu soucieux de la sécurité de ses serveurs.
5. Bon bah j'deviendrais bien maître du monde moi ce soir
Ce n'est là qu'une poignée des quarante (à ce jour) directives à votre disposition pour ajuster à la volée le comportement souhaité entre l'utilisateur et la ressource visitée. À l'aide des nombreux modules tiers disponibles, d'autres horizons ingénieux s'offrent à vous ; on remarquera par exemple le site http://devblog.mixlr.com/2012/09/01/nginx-lua/ qui donne l'exemple d'une directive access_by_lua qui fait communiquer nginx avec Redis [11] pour lire rapidement si un visiteur fait partie des adresses IP bannies sur le serveur.
Du fait de la simplicité du langage Lua et des innombrables possibilités du combo avec le serveur nginx, gageons que les contributions et nouveautés vont pulluler d'ici peu.
Références
[1] Site du pare-feu NAXSI : https://github.com/nbs-system/naxsi
[2] Définition d'un proxy inverse : http://fr.wikipedia.org/wiki/Proxy_inverse
[3] Site officiel de Lua : http://www.lua.org/
[4] Comparaison des performances de Lua et LuaJIT sur différentes architectures : http://luajit.org/performance.html
[5] Site officiel du projet pkgsrc (portable package build system) : http://pkgsrc.org/
[6] Paquets NetBSDfr : http://packages.netbsdfr.org/
[7] Paquet nginx-extra GNU/Debian : https://packages.debian.org/fr/wheezy/nginx-extras
[8] Site officiel du projet OpenResty : http://openresty.org/
[9] Module Lua Redis : https://github.com/agentzh/lua-resty-redis
[10] Module Lua Memcached : https://github.com/agentzh/lua-resty-memcached
[11] Site officiel de Redis : http://redis.io/