OpenResty est la version améliorée de NGINX avec des requêtes et réponses dynamiques

API7.ai

October 23, 2022

OpenResty (NGINX + Lua)

Après l'introduction précédente, vous devez comprendre le concept d'OpenResty et comment l'apprendre. Cet article nous guidera sur la manière dont OpenResty gère les requêtes et les réponses des clients.

Bien qu'OpenResty soit un serveur web basé sur NGINX, il est fondamentalement différent de NGINX : NGINX est piloté par des fichiers de configuration statiques, tandis qu'OpenResty est piloté par l'API Lua, offrant ainsi plus de flexibilité et de programmabilité.

Permettez-moi de vous expliquer les avantages de l'API Lua.

Catégories d'API

Tout d'abord, nous devons savoir que l'API OpenResty est divisée en grandes catégories suivantes.

  • Traitement des requêtes et des réponses.
  • SSL.
  • Dictionnaire partagé.
  • Cosocket.
  • Gestion du trafic de couche 4.
  • Processus et worker.
  • Accès aux variables et configurations de NGINX.
  • Fonctions générales telles que les chaînes de caractères, le temps, le codage, etc.

Ici, je vous suggère également d'ouvrir la documentation de l'API Lua d'OpenResty et de la consulter par rapport à la liste des API pour voir si vous pouvez relier cette catégorie.

Les API OpenResty existent non seulement dans le projet lua-nginx-module, mais aussi dans le projet lua-resty-core, comme ngx.ssl, ngx.base64, ngx.errlog, ngx.process, ngx.re.split, ngx.resp.add_header, ngx.balancer, ngx.semaphore, ngx.ocsp, et d'autres API.

Pour les API qui ne sont pas dans le projet lua-nginx-module, vous devez les inclure séparément pour les utiliser. Par exemple, si vous souhaitez utiliser la fonction split, vous devez l'appeler comme suit.

$ resty -e 'local ngx_re = require "ngx.re"
 local res, err = ngx_re.split("a,b,c,d", ",", nil, {pos = 5})
 print(res)
 '

Bien sûr, cela peut vous embrouiller : dans le projet lua-nginx-module, il y a plusieurs API commençant par ngx.re.sub, ngx.re.find, etc. Pourquoi est-ce que l'API ngx.re.split est la seule qui nécessite d'être incluse avant d'être utilisée ?

Comme nous l'avons mentionné dans le chapitre précédent sur lua-resty-core, les nouvelles API OpenResty sont implémentées dans le dépôt lua-rety-core via FFI, ce qui entraîne inévitablement un sentiment de fragmentation. J'espère que ce problème sera résolu à l'avenir en fusionnant les projets lua-nginx-module et lua-resty-core.

Requête

Ensuite, voyons comment OpenResty gère les requêtes et les réponses des clients. Tout d'abord, regardons l'API pour gérer les requêtes, mais il y a plus de 20 API commençant par ngx.req, alors par où commencer ?

Nous savons que les messages de requête HTTP sont composés de trois parties : la ligne de requête, l'en-tête de requête et le corps de la requête, donc je vais présenter l'API en fonction de ces trois parties.

Ligne de requête

La première est la ligne de requête, qui contient la méthode de requête, l'URI et la version du protocole HTTP. Dans NGINX, vous pouvez obtenir cette valeur en utilisant une variable intégrée, tandis que dans OpenResty, cela correspond à l'API ngx.var.*. Regardons deux exemples.

  • La variable intégrée $scheme, qui représente le nom du protocole dans NGINX, est soit http soit https ; dans OpenResty, vous pouvez utiliser ngx.var.scheme pour obtenir la même valeur.
  • $request_method représente la méthode de requête comme GET, POST, etc. ; dans OpenResty, vous pouvez obtenir la même valeur via ngx.var.request_method.

Vous pouvez consulter la documentation officielle de NGINX pour obtenir une liste complète des variables intégrées de NGINX : http://nginx.org/en/docs/http/ngx_http_core_module.html#variables.

Alors la question se pose : pourquoi OpenResty fournit-il une API séparée pour la ligne de requête alors que vous pouvez obtenir les données de la ligne de requête en retournant la valeur d'une variable comme ngx.var.* ?

Le résultat contient de nombreux facteurs :

  • Tout d'abord, il n'est pas recommandé de lire ngx.var de manière répétée en raison de ses performances inefficaces.
  • Deuxièmement, par souci de convivialité pour le programme, ngx.var retourne une chaîne de caractères, pas un objet Lua. Il est difficile de gérer lorsque vous obtenez args, qui peut retourner plusieurs valeurs.
  • Troisièmement, du point de vue de la flexibilité, la plupart des ngx.var sont en lecture seule, et seules quelques variables sont modifiables, comme $args et limit_rate. Cependant, nous avons souvent besoin de modifier la méthode, l'URI et les args.

Par conséquent, OpenResty fournit plusieurs API dédiées à la manipulation de la ligne de requête, qui peuvent réécrire la ligne de requête pour des opérations ultérieures telles que la redirection.

Voyons comment obtenir le numéro de version du protocole HTTP via l'API. L'API OpenResty ngx.req.http_version fait la même chose que la variable NGINX $server_protocol : retourner le numéro de version du protocole HTTP. Cependant, la valeur de retour de cette API n'est pas une chaîne de caractères mais un format numérique, les valeurs possibles étant 2.0, 1.0, 1.1 et 0.9. Nil est retourné si le résultat est en dehors de cette plage de valeurs.

Voyons la méthode pour obtenir la requête dans la ligne de requête. Comme mentionné, le rôle et la valeur de retour de ngx.req.get_method et des variables $request_method de NGINX sont les mêmes : au format chaîne de caractères.

Cependant, le format du paramètre de la méthode de requête HTTP actuelle ngx.req.set_method n'est pas une chaîne de caractères mais des constantes numériques intégrées. Par exemple, le code suivant réécrit la méthode de requête en POST.

ngx.req.set_method(ngx.HTTP_POST)

Pour vérifier que la constante intégrée ngx.HTTP_POST est bien un nombre et non une chaîne de caractères, vous pouvez imprimer sa valeur et voir si la sortie est 8.

resty -e 'print(ngx.HTTP_POST)'

De cette manière, la valeur de retour de la méthode get est une chaîne de caractères, tandis que la valeur d'entrée de la méthode set est un nombre. Cela fonctionne lorsque la méthode set passe une valeur confuse car l'API peut planter et signaler une erreur 500. Cependant, dans la logique de jugement suivante :

if (ngx.req.get_method() == ngx.HTTP_POST) then
    -- faire quelque chose
 end

Ce type de code fonctionne bien, ne signale aucune erreur et est difficile à détecter même lors des revues de code. J'ai déjà fait une erreur similaire et je m'en souviens encore : j'avais déjà passé deux tours de revues de code et des cas de test incomplets pour essayer de la couvrir. Finalement, une anomalie dans l'environnement de production m'a conduit au problème.

Il n'y a pas de moyen pratique de résoudre un tel problème, sauf à être plus prudent ou à ajouter une autre couche d'encapsulation. Lorsque vous concevez votre API métier, vous pouvez également envisager de garder le format de paramètre cohérent des méthodes get et set, même si cela nécessite de sacrifier un peu de performance.

De plus, parmi les méthodes pour réécrire la ligne de requête, il y a deux API, ngx.req.set_uri et ngx.req.set_uri_args, qui peuvent être utilisées pour réécrire l'URI et les args. Regardons cette configuration NGINX.

rewrite ^ /foo?a=3? break;

Alors, comment pouvons-nous résoudre cela avec l'API Lua équivalente ? La réponse est les deux lignes de code suivantes.

ngx.req.set_uri_args("a=3")
ngx.req.set_uri("/foo")

Si vous avez lu la documentation officielle, vous constaterez que ngx.req.set_uri a un deuxième paramètre : jump, qui est "false" par défaut. Si vous le définissez comme "true", cela équivaut à définir le drapeau de la commande rewrite à last au lieu de break dans l'exemple ci-dessus.

Cependant, je n'aime pas trop la configuration des drapeaux de la commande rewrite car elle est illisible et difficile à reconnaître, et bien moins intuitive et maintenable que le code.

En-tête de requête

Comme nous le savons, les en-têtes de requête HTTP sont au format key : value, par exemple :

  Accept: text/css,*/*;q=0.1
  Accept-Encoding: gzip, deflate, br

Dans OpenResty, vous pouvez utiliser ngx.req.get_headers pour analyser et obtenir les en-têtes de requête, et le type de valeur de retour est une table.

local h, err = ngx.req.get_headers()
  if err == "truncated" then
      -- on peut choisir d'ignorer ou de rejeter la requête actuelle ici
  end
  for k, v in pairs(h) do
      ...
  end

Par défaut, il retourne les 100 premiers en-têtes. Si le nombre dépasse 100, il signalera une erreur truncated, laissant au développeur le soin de décider comment la gérer. Vous vous demandez peut-être pourquoi prendre cette voie, ce que je mentionnerai plus tard dans la section sur les vulnérabilités de sécurité.

Cependant, nous devons noter qu'OpenResty ne fournit pas d'API spécifique pour obtenir un en-tête de requête spécifié, ce qui signifie qu'il n'y a pas de forme comme ngx.req.header['host']. Si vous avez un tel besoin, vous devez vous appuyer sur la variable NGINX $http_xxx pour l'obtenir. Ensuite, dans OpenResty, vous pouvez l'obtenir via ngx.var.http_xxx.

Voyons maintenant comment nous devrions réécrire et supprimer l'en-tête de requête. Les API pour ces deux opérations sont assez intuitives :

ngx.req.set_header("Content-Type", "text/css")
ngx.req.clear_header("Content-Type")

Bien sûr, la documentation officielle mentionne également d'autres méthodes pour supprimer l'en-tête de requête, comme définir la valeur du titre à nil, etc. Cependant, je recommande toujours d'utiliser clear_header pour le faire uniformément pour la clarté du code.

Corps de la requête

Enfin, regardons le corps de la requête. Pour des raisons de performance, OpenResty ne lit pas activement le corps de la requête à moins que vous n'activiez la directive lua_need_request_body dans nginx.conf. De plus, pour les corps de requête plus volumineux, OpenResty enregistre le contenu dans un fichier temporaire sur le disque, donc le processus de lecture du corps de la requête ressemble à ceci.

ngx.req.read_body()
local data = ngx.req.get_body_data()
if not data then
    local tmp_file = ngx.req.get_body_file()
     -- io.open(tmp_file)
     -- ...
 end

Ce code a une opération de blocage IO pour lire le fichier disque. Vous devriez ajuster la configuration de client_body_buffer_size (16 Ko par défaut sur les systèmes 64 bits) pour minimiser les opérations de blocage ; vous pouvez également configurer client_body_buffer_size et client_max_body_size pour qu'ils soient identiques et les gérer entièrement en mémoire, en fonction de la taille de votre mémoire et du nombre de requêtes simultanées que vous prenez.

De plus, le corps de la requête peut être réécrit. Les deux API ngx.req.set_body_data et ngx.req.set_body_file acceptent une chaîne de caractères et un fichier disque local comme paramètres d'entrée pour accomplir la réécriture du corps de la requête. Cependant, ce type d'opération est rare, et vous pouvez consulter la documentation pour plus de détails.

Réponse

Après le traitement de la requête, nous devons envoyer une réponse au client. Comme le message de requête, le message de réponse est également composé de plusieurs parties : la ligne d'état, l'en-tête de réponse et le corps de la réponse. Je vais présenter les API correspondantes en fonction de ces trois parties.

Ligne d'état

La principale chose qui nous intéresse dans la ligne d'état est le code d'état. Par défaut, le code d'état HTTP retourné est 200, qui est la constante ngx.HTTP_OK intégrée dans OpenResty. Mais dans le monde du code, c'est toujours le code qui gère les cas les plus exceptionnels.

Si vous détectez le message de requête et constatez qu'il s'agit d'une requête malveillante, alors vous devez terminer la requête :

ngx.exit(ngx.HTTP_BAD_REQUEST)

Cependant, il y a une constante particulière dans les codes d'état HTTP d'OpenResty : ngx.OK. Dans le cas de ngx.exit(ngx.OK), la requête quitte la phase de traitement actuelle et passe à la phase suivante plutôt que de retourner directement au client.

Bien sûr, vous pouvez également choisir de ne pas quitter et simplement réécrire le code d'état en utilisant ngx.status, comme écrit dans la manière suivante.

ngx.status = ngx.HTTP_FORBIDDEN

Vous pouvez les consulter dans la documentation si vous souhaitez en savoir plus sur les constantes de code d'état.

En-tête de réponse

En ce qui concerne l'en-tête de réponse, il y a deux façons de le configurer. La première est la plus simple.

ngx.header.content_type = 'text/plain'
ngx.header["X-My-Header"] = 'blah blah'
ngx.header["X-My-Header"] = nil -- supprimer

Ici, ngx.header contient les informations de l'en-tête de réponse, qui peuvent être lues, modifiées et supprimées.

La deuxième façon de configurer l'en-tête de réponse est ngx_resp.add_header, du dépôt lua-resty-core, qui ajoute un message d'en-tête, appelé avec :

local ngx_resp = require "ngx.resp"
ngx_resp.add_header("Foo", "bar")

La différence avec la première méthode est que add_header ne remplace pas un champ existant du même nom.

Corps de la réponse

Enfin, regardons le corps de la réponse. Dans OpenResty, vous pouvez utiliser ngx.say et ngx.print pour afficher le corps de la réponse.

ngx.say('hello, world')

La fonctionnalité des deux API est identique, la seule différence étant que ngx.say a un saut de ligne à la fin.

Pour éviter l'inefficacité de la concaténation de chaînes de caractères, ngx.say / ngx.print supporte les formats de chaîne de caractères et de tableau comme paramètres.

$ resty -e 'ngx.say({"hello", ", ", "world"})'
 hello, world

Cette méthode saute la concaténation de chaînes de caractères au niveau Lua et laisse les fonctions C s'en occuper.

Résumé

Passons en revue le contenu d'aujourd'hui. Nous avons introduit les API OpenResty associées aux messages de requête et de réponse. Comme vous pouvez le voir, l'API OpenResty est plus flexible et puissante que la directive NGINX.

Par conséquent, l'API Lua fournie par OpenResty est-elle suffisante pour répondre à vos besoins lorsque vous traitez des requêtes HTTP ? Veuillez laisser vos commentaires et partager cet article avec vos collègues et amis afin que nous puissions communiquer et nous améliorer ensemble.