OpenResty es el NGINX mejorado con solicitudes y respuestas dinámicas
API7.ai
October 23, 2022
Después de la introducción anterior, debes entender el concepto de OpenResty y cómo aprenderlo. Este artículo nos guiará sobre cómo OpenResty maneja las solicitudes y respuestas del cliente.
Aunque OpenResty es un servidor web basado en NGINX, es fundamentalmente diferente de NGINX: NGINX está impulsado por archivos de configuración estáticos, mientras que OpenResty está impulsado por la API de Lua, ofreciendo más flexibilidad y programabilidad.
Permíteme mostrarte los beneficios de la API de Lua.
Categorías de API
Primero, necesitamos saber que la API de OpenResty se divide en las siguientes categorías principales.
- Procesamiento de solicitudes y respuestas.
- Relacionado con SSL.
- Diccionario compartido.
- Cosocket.
- Manejo de tráfico en cuatro capas.
- Proceso y worker.
- Acceso a variables y configuración de NGINX.
- Funciones generales como cadenas, tiempo, codificación, etc.
Aquí, te sugiero que también abras la documentación de la API de Lua de OpenResty y la compares con la lista de API para ver si puedes relacionarte con esta categoría.
Las APIs de OpenResty no solo existen en el proyecto lua-nginx-module
, sino también en el proyecto lua-resty-core
, como ngx.ssl
, ngx.base64
, ngx.errlog
, ngx.process
, ngx.re.split
, ngx.resp.add_header
, ngx.balancer
, ngx.semaphore
, ngx.ocsp
, y otras APIs.
Para las APIs que no están en el proyecto lua-nginx-module
, necesitas requerirlas por separado para usarlas. Por ejemplo, si deseas usar la función de división, debes llamarla de la siguiente manera.
$ resty -e 'local ngx_re = require "ngx.re"
local res, err = ngx_re.split("a,b,c,d", ",", nil, {pos = 5})
print(res)
'
Por supuesto, puede confundirte: en el proyecto lua-nginx-module
, hay varias APIs que comienzan con ngx.re.sub
, ngx.re.find
, etc. ¿Por qué es que la API ngx.re.split
es la única que necesita ser requerida antes de ser usada?
Como mencionamos en el capítulo anterior de lua-resty-core
, las nuevas APIs de OpenResty se implementan en el repositorio lua-resty-core
mediante FFI, por lo que inevitablemente hay una sensación de fragmentación. Espero que en el futuro se resuelva este problema fusionando los proyectos lua-nginx-module
y lua-resty-core
.
Solicitud
A continuación, veamos cómo OpenResty maneja las solicitudes y respuestas del cliente. Primero, veamos la API para manejar solicitudes, pero hay más de 20 APIs que comienzan con ngx.req
, ¿cómo empezamos?
Sabemos que los mensajes de solicitud HTTP consisten en tres partes: la línea de solicitud, el encabezado de solicitud y el cuerpo de solicitud, por lo que presentaré la API en estas tres partes.
Línea de Solicitud
La primera es la línea de solicitud, que contiene el método de solicitud, el URI y la versión del protocolo HTTP. En NGINX, puedes obtener este valor usando una variable incorporada, mientras que en OpenResty, corresponde a la API ngx.var.*
. Veamos dos ejemplos.
- La variable incorporada
$scheme
, que representa el nombre del protocolo en NGINX, eshttp
ohttps
; en OpenResty, puedes usarngx.var.scheme
para obtener el mismo valor. $request_method
representa el método de solicitud comoGET
,POST
, etc.; en OpenResty, puedes obtener el mismo valor mediantengx.var.request_method
.
Puedes visitar la documentación oficial de NGINX para obtener una lista completa de las variables incorporadas de NGINX: http://nginx.org/en/docs/http/ngx_http_core_module.html#variables.
Entonces, surge la pregunta: ¿por qué OpenResty proporciona una API separada para la línea de solicitud cuando puedes obtener los datos en la línea de solicitud devolviendo el valor de una variable como ngx.var.*
?
El resultado contiene muchos factores:
- En primer lugar, no se recomienda leer repetidamente
ngx.var
debido a su rendimiento ineficiente. - En segundo lugar, por consideración al aspecto amigable del programa,
ngx.var
devuelve una cadena, no un objeto Lua. Es difícil de manejar cuando se obtienenargs
, que pueden devolver múltiples valores. - En tercer lugar, desde el aspecto de la flexibilidad, la mayoría de
ngx.var
es de solo lectura, y solo unas pocas variables son escribibles, como$args
ylimit_rate
. Sin embargo, a menudo necesitamos modificar el método, el URI y los args.
Por lo tanto, OpenResty proporciona varias APIs dedicadas a manipular la línea de solicitud, que pueden reescribir la línea de solicitud para operaciones posteriores como la redirección.
Veamos cómo obtener el número de versión del protocolo HTTP a través de la API. La API de OpenResty ngx.req.http_version
hace lo mismo que la variable $server_protocol
de NGINX: devolver el número de versión del protocolo HTTP. Sin embargo, el valor de retorno de esta API no es una cadena, sino en formato numérico, cuyos valores posibles son 2.0
, 1.0
, 1.1
y 0.9
. Se devuelve nil
si el resultado está fuera del rango de estos valores.
Veamos el método de obtener la solicitud en la línea de solicitud. Como se mencionó, el papel y el valor de retorno de ngx.req.get_method
y las variables $request_method
de NGINX son los mismos: en formato de cadena.
Sin embargo, el formato de parámetro del método de solicitud HTTP actual ngx.req.set_method
no es una cadena, sino constantes numéricas incorporadas. Por ejemplo, el siguiente código reescribe el método de solicitud a POST.
ngx.req.set_method(ngx.HTTP_POST)
Para verificar que la constante incorporada ngx.HTTP_POST
es realmente un número y no una cadena, puedes imprimir su valor y ver si la salida es 8.
resty -e 'print(ngx.HTTP_POST)'
De esta manera, el valor de retorno del método get
es una cadena, mientras que el valor de entrada del método set
es un número. Está bien cuando el método set
pasa un valor confuso porque la API puede fallar y reportar un error 500
. Sin embargo, en la siguiente lógica de juicio:
if (ngx.req.get_method() == ngx.HTTP_POST) then
-- hacer algo
end
Este tipo de código funciona bien, no reporta errores y es difícil de encontrar incluso durante las revisiones de código. Yo cometí un error similar antes y todavía lo recuerdo: ya había pasado por dos rondas de revisiones de código y casos de prueba incompletos para intentar cubrirlo. Finalmente, una anomalía en el entorno en línea me llevó al problema.
No hay una forma práctica de resolver tal problema excepto ser más cuidadoso o agregar otra capa de encapsulación. Cuando diseñas tu API de negocio, también puedes considerar y mantener el formato de parámetro consistente de los métodos get
y set
, incluso si necesitas sacrificar algo de rendimiento.
Además, entre los métodos para reescribir la línea de solicitud, hay dos APIs, ngx.req.set_uri
y ngx.req.set_uri_args
, que se pueden usar para reescribir el URI y los args. Veamos esta configuración de NGINX.
rewrite ^ /foo?a=3? break;
Entonces, ¿cómo podemos resolverlo con la API equivalente de Lua? La respuesta son las siguientes dos líneas de código.
ngx.req.set_uri_args("a=3")
ngx.req.set_uri("/foo")
Si has leído la documentación oficial, encontrarás que ngx.req.set_uri
tiene un segundo parámetro: jump
, que es "false" por defecto. Si lo configuras como "true", es igual a configurar la bandera del comando rewrite
a last
en lugar de break
en el ejemplo anterior.
Sin embargo, no me gusta mucho la configuración de la bandera del comando rewrite
porque es ilegible y poco reconocible, y mucho menos intuitiva y mantenible que el código.
Encabezado de Solicitud
Como sabemos, los encabezados de solicitud HTTP están en formato key : value
, por ejemplo:
Accept: text/css,*/*;q=0.1
Accept-Encoding: gzip, deflate, br
En OpenResty, puedes usar ngx.req.get_headers
para analizar y obtener los encabezados de solicitud, y el tipo de valor de retorno es una tabla.
local h, err = ngx.req.get_headers()
if err == "truncated" then
-- uno puede elegir ignorar o rechazar la solicitud actual aquí
end
for k, v in pairs(h) do
...
end
Por defecto, devuelve los primeros 100 encabezados. Si el número excede 100, reportará un error truncated
, dejando que el desarrollador decida cómo manejarlo. Puedes preguntarte por qué se toma de esta manera, lo cual mencionaré más adelante en la sección sobre vulnerabilidades de seguridad.
Sin embargo, debemos tener en cuenta que OpenResty no proporciona una API específica para obtener un encabezado de solicitud especificado, lo que significa que no hay una forma como ngx.req.header['host']
. Si tienes tal necesidad, debes confiar en la variable de NGINX $http_xxx
para lograrlo. Luego, en OpenResty, puedes obtenerlo mediante ngx.var.http_xxx
.
Ahora veamos cómo deberíamos reescribir y eliminar el encabezado de solicitud. Las APIs para ambas operaciones son bastante intuitivas:
ngx.req.set_header("Content-Type", "text/css")
ngx.req.clear_header("Content-Type")
Por supuesto, la documentación oficial también menciona otros métodos para eliminar el encabezado de solicitud, como establecer el valor del título en nil
, etc. Sin embargo, todavía recomiendo usar clear_header
para hacerlo de manera uniforme para la claridad del código.
Cuerpo de Solicitud
Finalmente, veamos el cuerpo de la solicitud. Por razones de rendimiento, OpenResty no lee activamente el cuerpo de la solicitud a menos que habilites la directiva lua_need_request_body
en nginx.conf
. Además, para cuerpos de solicitud más grandes, OpenResty guarda el contenido en un archivo temporal en el disco, por lo que el proceso completo de leer el cuerpo de la solicitud se ve así.
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
Este código tiene una operación de bloqueo de IO para leer el archivo del disco. Debes ajustar la configuración de client_body_buffer_size
(16 KB por defecto en sistemas de 64 bits) para minimizar las operaciones de bloqueo; también puedes configurar client_body_buffer_size
y client_max_body_size
para que sean iguales y manejarlos completamente en memoria, dependiendo del tamaño de tu memoria y el número de solicitudes concurrentes que manejes.
Además, el cuerpo de la solicitud se puede reescribir. Las dos APIs ngx.req.set_body_data
y ngx.req.set_body_file
aceptan una cadena y un archivo local del disco como parámetros de entrada para realizar la reescritura del cuerpo de la solicitud. Sin embargo, este tipo de operación es poco común, y puedes consultar la documentación para obtener más detalles.
Respuesta
Después de procesar la solicitud, necesitamos enviar una respuesta al cliente. Al igual que el mensaje de solicitud, el mensaje de respuesta también consta de varias partes: la línea de estado, el encabezado de respuesta y el cuerpo de respuesta. Presentaré las APIs correspondientes según estas tres partes.
Línea de Estado
Lo principal que nos preocupa en la línea de estado es el código de estado. Por defecto, el código de estado HTTP devuelto es 200, que es la constante ngx.HTTP_OK
incorporada en OpenResty. Pero en el mundo del código, siempre es el código el que maneja los casos más excepcionales.
Si detectas el mensaje de solicitud y descubres que es una solicitud maliciosa, entonces necesitas terminar la solicitud:
ngx.exit(ngx.HTTP_BAD_REQUEST)
Sin embargo, hay una constante particular en los códigos de estado HTTP de OpenResty: ngx.OK
. En la situación de ngx.exit(ngx.OK)
, la solicitud sale de la fase de procesamiento actual y pasa a la siguiente etapa en lugar de regresar directamente al cliente.
Por supuesto, también puedes elegir no salir y simplemente reescribir el código de estado usando ngx.status
, como se escribe a continuación.
ngx.status = ngx.HTTP_FORBIDDEN
Puedes buscarlos en la documentación si deseas saber más sobre las constantes de códigos de estado.
Encabezado de Respuesta
En cuanto al encabezado de respuesta, hay dos formas en que puedes configurarlo. La primera es la más simple.
ngx.header.content_type = 'text/plain'
ngx.header["X-My-Header"] = 'blah blah'
ngx.header["X-My-Header"] = nil -- eliminar
Aquí, ngx.header
contiene la información del encabezado de respuesta, que se puede leer, modificar y eliminar.
La segunda forma de configurar el encabezado de respuesta es ngx_resp.add_header
, del repositorio lua-resty-core
, que agrega un mensaje de encabezado, llamado con:
local ngx_resp = require "ngx.resp"
ngx_resp.add_header("Foo", "bar")
La diferencia con el primer método es que add_header
no sobrescribe un campo existente del mismo nombre.
Cuerpo de Respuesta
Finalmente, veamos el cuerpo de la respuesta. En OpenResty, puedes usar ngx.say
y ngx.print
para enviar el cuerpo de la respuesta.
ngx.say('hello, world')
La funcionalidad de las dos APIs es idéntica, la única diferencia es que ngx.say
tiene un salto de línea al final.
Para evitar la ineficiencia de la concatenación de cadenas, ngx.say / ngx.print
admite cadenas y formatos de matriz como parámetros.
$ resty -e 'ngx.say({"hello", ", ", "world"})'
hello, world
Este método omite la concatenación de cadenas a nivel de Lua y deja que las funciones de C lo manejen.
Resumen
Repasemos el contenido de hoy. Presentamos las APIs de OpenResty asociadas con los mensajes de solicitud y respuesta. Como puedes ver, la API de OpenResty es más flexible y poderosa que la directiva de NGINX.
En consecuencia, ¿es suficiente la API de Lua proporcionada por OpenResty para satisfacer tus necesidades cuando manejas solicitudes HTTP? Por favor, deja tus comentarios y comparte este artículo con tus colegas y amigos para que podamos comunicarnos y mejorar juntos.