Introducción a las APIs comunes en OpenResty
API7.ai
November 4, 2022
En los artículos anteriores, te has familiarizado con muchas API importantes de Lua en OpenResty. Hoy, aprenderemos sobre algunas otras API generales, principalmente relacionadas con expresiones regulares, tiempo, procesos, etc.
API relacionadas con Expresiones Regulares
Comencemos por ver las expresiones regulares más utilizadas y más importantes. En OpenResty, deberíamos usar el conjunto de API proporcionado por ngx.re.*
para manejar la lógica relacionada con expresiones regulares en lugar de usar la coincidencia de patrones de Lua. Esto no solo es por razones de rendimiento, sino también porque la regularidad de Lua es autónoma y no es una especificación PCRE
, lo que sería molesto para la mayoría de los desarrolladores.
En los artículos anteriores, ya te has encontrado con algunas de las API de ngx.re.*
, cuya documentación es muy detallada. Por lo tanto, no las enumeraré más. Aquí, presentaré las siguientes dos API por separado.
ngx.re.split
La primera es ngx.re.split
. El corte de cadenas es una función muy común, y OpenResty también proporciona una API correspondiente, pero muchos desarrolladores no pueden encontrar tal función y tienen que elegir implementarla ellos mismos.
¿Por qué? La API ngx.re.split
no está en lua-nginx-module
sino en lua-resty-core
; no está en la documentación de la página principal de lua-resty-core
sino en la documentación del directorio de tercer nivel lua-resty-core/lib/ngx/re.md
. Como resultado, muchos desarrolladores desconocen por completo la existencia de esta API.
De manera similar, las API difíciles de descubrir incluyen ngx_resp.add_header
, enable_privileged_agent
, etc., que mencionamos anteriormente. Entonces, ¿cómo resolvemos rápidamente este problema? Además de leer la documentación de la página principal de lua-resty-core
, también necesitas leer la documentación *.md
en el directorio lua-resty-core/lib/ngx/
.
lua_regex_match_limit
En segundo lugar, quiero presentar lua_regex_match_limit
. No hemos hablado antes sobre los comandos de NGINX proporcionados por OpenResty porque, en la mayoría de los casos, los valores predeterminados son suficientes y no es necesario modificarlos en tiempo de ejecución. La excepción a esto es el comando lua_regex_match_limit
, que está relacionado con expresiones regulares.
Sabemos que si usamos un motor de expresiones regulares que está implementado basado en NFA con retroceso, entonces existe el riesgo de Retroceso Catastrófico, donde la expresión regular retrocede demasiado al coincidir, causando que la CPU llegue al 100% y los servicios se bloqueen.
Una vez que ocurre un retroceso catastrófico, necesitamos usar gdb
para analizar el volcado o usar systemtap
para analizar el entorno en línea para localizarlo. Desafortunadamente, detectarlo de antemano no es fácil porque solo solicitudes especiales lo activarán. Esto permite que los atacantes aprovechen esto, y ReDoS
(Denegación de Servicio por Expresiones Regulares) se refiere a este tipo de ataque.
Aquí, principalmente te presento cómo usar la siguiente línea de código en OpenResty para evitar los problemas anteriores de manera simple y efectiva:
lua_regex_match_limit
se usa para limitar el número de retrocesos por el motor de expresiones regulares PCRE
. De esta manera, incluso si ocurre un retroceso catastrófico, las consecuencias se limitarán a un rango que no causará que tu CPU se sature.
lua_regex_match_limit 100000;
API relacionadas con el Tiempo
La API de tiempo más utilizada es ngx.now
, que imprime la marca de tiempo actual, como la siguiente línea de código:
resty -e 'ngx.say(ngx.now())'
Como puedes ver en los resultados impresos, ngx.now
incluye la parte fraccional, por lo que es más precisa. La API relacionada ngx.time
solo devuelve la parte entera del valor. Las otras, ngx.localtime
, ngx.utctime
, ngx.cookie_time
y ngx.http_time
, se utilizan principalmente para devolver y procesar el tiempo en diferentes formatos. Si deseas usarlas, puedes consultar la documentación, no son difíciles de entender, por lo que no necesito hablar de ellas.
Sin embargo, vale la pena mencionar que estas API que devuelven el tiempo actual, si no son activadas por una operación de IO de red no bloqueante, siempre devolverán el valor en caché en lugar del tiempo real actual como nos gustaría. Mira el siguiente código de ejemplo:
$ resty -e 'ngx.say(ngx.now())
os.execute("sleep 1")
ngx.say(ngx.now())'
Entre las dos llamadas a ngx.now
, usamos la función de bloqueo de Lua para dormir durante 1
segundo, pero la marca de tiempo devuelta es la misma en ambas ocasiones, como se muestra en los resultados impresos.
Entonces, ¿qué pasa si lo reemplazamos con una función de sueño no bloqueante? Por ejemplo, el siguiente código nuevo:
$ resty -e 'ngx.say(ngx.now())
ngx.sleep(1)
ngx.say(ngx.now())'
Imprimirá una marca de tiempo diferente. Esto nos lleva a ngx.sleep
, una función de sueño no bloqueante. Además de dormir durante una cantidad de tiempo especificada, esta función tiene otro propósito especial.
Por ejemplo, si tienes un fragmento de código que está haciendo cálculos intensivos, lo que lleva mucho tiempo, las solicitudes correspondientes a este fragmento de código seguirán ocupando recursos de worker y CPU durante este tiempo, causando que otras solicitudes se acumulen y no obtengan una respuesta oportuna. En este punto, podemos intercalar ngx.sleep(0)
para que este código ceda el control y otras solicitudes también puedan ser procesadas.
API de Worker y Proceso
OpenResty proporciona las API ngx.worker.*
y ngx.process.*
para obtener información sobre workers y procesos. El primero se relaciona con los procesos worker de Nginx, mientras que el segundo se refiere a todos los procesos de Nginx en general, no solo los procesos worker, sino también el proceso maestro, el proceso privilegiado, etc.
El problema de los valores true
y null
Finalmente, veamos el problema de los valores true
y null
. En OpenResty, la determinación de los valores true
y null
ha sido un punto muy problemático y confuso.
Veamos la definición de un valor true
en Lua: excepto nil
y false
, todos son valores true
.
Por lo tanto, los valores true
también incluirían 0
, cadena vacía, tabla vacía, etc.
Veamos nil
en Lua, que significa indefinido
. Por ejemplo, si declaras una variable pero no la has inicializado, su valor es nil
.
$ resty -e 'local a
ngx.say(type(a))'
Y nil
también es un tipo de dato en Lua. Habiendo entendido estos dos puntos, ahora veamos los otros problemas derivados de estas dos definiciones.
ngx.null
El primer problema es ngx.null
. Debido a que el nil
de Lua no puede usarse como el valor de una tabla
, OpenResty introduce ngx.null
como el valor null
en la tabla.
$ resty -e 'print(ngx.null)'
null
$ resty -e 'print(type(ngx.null))'
userdata
Como puedes ver en los dos fragmentos de código anteriores, ngx.null
se imprime como null
, y su tipo es userdata
, entonces, ¿puede tratarse como un valor false
? Por supuesto que no. El valor booleano de ngx.null
es true
.
$ resty -e 'if ngx.null then
ngx.say("true")
end'
Por lo tanto, recuerda que solo nil
y false
son valores false
. Si pasas por alto este punto, es fácil caer en trampas, por ejemplo, cuando usas lua-resty-redis
y haces el siguiente juicio:
local res, err = red:get("dog")
if not res then
res = res + "test"
end
Si el valor de retorno res
es nil
, la llamada a la función ha fallado; si res
es ngx.null
, la clave dog
no existe en redis, entonces el código falla si la clave dog
no existe.
cdata:NULL
El segundo problema es cdata:NULL
. Cuando llamas a una función C a través de la interfaz FFI de LuaJIT, y la función devuelve un puntero NULL
, entonces te encontrarás con otro tipo de valor null
, cdata:NULL
.
$ resty -e 'local ffi = require "ffi"
local cdata_null = ffi.new("void*", nil)
if cdata_null then
ngx.say("true")
end'
Al igual que ngx.null
, cdata:NULL
también es true
. Pero lo más desconcertante es que el siguiente código, que imprime true
, significa que cdata:NULL
es equivalente a nil
.
$ resty -e 'local ffi = require "ffi"
local cdata_null = ffi.new("void*", nil)
ngx.say(cdata_null == nil)'
Entonces, ¿cómo deberíamos manejar ngx.null
y cdata:NULL
? No es una buena solución dejar que la capa de aplicación se preocupe por estos problemas. Es mejor hacer un envoltorio de segundo nivel y no dejar que el llamador conozca estos detalles.
Es mejor hacer un envoltorio de segundo nivel y no dejar que el llamador conozca estos detalles.
cjson.null
Finalmente, veamos los valores null
que aparecen en cjson
. La biblioteca cjson
toma el NULL
en json, lo decodifica en lightuserdata
de Lua y usa cjson.null
para representarlo.
$ resty -e 'local cjson = require "cjson"
local data = cjson.encode(nil)
local decode_null = cjson.decode(data)
ngx.say(decode_null == cjson.null)'
El nil
de Lua se convierte en cjson.null
después de ser codificado y decodificado por JSON. Como puedes imaginar, se introduce por la misma razón que ngx.null
, porque nil
no puede usarse como un valor en una tabla
.
Hasta ahora, ¿te has confundido con tantos tipos de valores null
en OpenResty? No te preocupes. Lee esta parte varias veces y ordénala tú mismo, entonces no te confundirás. Por supuesto, necesitamos pensar más en el futuro sobre si funciona cuando escribimos algo como if not foo then
.
Resumen
El artículo de hoy te presenta las API de Lua comúnmente utilizadas en OpenResty.
Finalmente, te dejo una pregunta: En el ejemplo de ngx.now
, ¿por qué el valor de ngx.now
no se modifica cuando no hay una operación de yield
? Bienvenido a compartir tu opinión en los comentarios, y también te invito a compartir este artículo para que podamos comunicarnos y mejorar juntos.