Introducción a las APIs comunes en OpenResty

API7.ai

November 4, 2022

OpenResty (NGINX + Lua)

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.