Saltar a contenido

http: Controlador de cliente HTTP cosocket de Lua para nginx-module-lua

Instalación

Si no has configurado la suscripción al repositorio RPM, regístrate. Luego puedes proceder con los siguientes pasos.

CentOS/RHEL 7 o Amazon Linux 2

yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install lua-resty-http

CentOS/RHEL 8+, Fedora Linux, Amazon Linux 2023

dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install lua5.1-resty-http

Para usar esta biblioteca de Lua con NGINX, asegúrate de que nginx-module-lua esté instalado.

Este documento describe lua-resty-http v0.17.2 lanzado el 29 de febrero de 2024.


Controlador de cliente HTTP cosocket para OpenResty / ngx_lua.

Características

  • HTTP 1.0 y 1.1
  • SSL
  • Interfaz de transmisión para el cuerpo de la respuesta, para un uso predecible de la memoria
  • Interfaz alternativa simple para solicitudes de un solo uso sin un paso de conexión manual
  • Codificaciones de transferencia en trozos y no en trozos
  • Conexiones keepalives
  • Pipelining de solicitudes
  • Trailers
  • Conexiones proxy HTTP
  • mTLS (requiere ngx_lua_http_module >= v0.10.23)

API

Obsoleto

Estos métodos pueden ser eliminados en versiones futuras.

Uso

Hay dos modos básicos de operación:

  1. Solicitudes simples de un solo uso que no requieren gestión manual de conexiones, pero que almacenan en búfer toda la respuesta y dejan la conexión cerrada o de vuelta en el grupo de conexiones.

  2. Solicitudes transmitidas donde la conexión se establece por separado, luego se envía la solicitud, se lee el cuerpo en trozos y, finalmente, la conexión se cierra manualmente o se mantiene viva. Esta técnica requiere un poco más de código, pero proporciona la capacidad de descartar cuerpos de respuesta potencialmente grandes en el lado de Lua, así como de hacer pipelining de múltiples solicitudes a través de una sola conexión.

Solicitud de un solo uso

local httpc = require("resty.http").new()

-- Las solicitudes de un solo uso utilizan la interfaz `request_uri`.
local res, err = httpc:request_uri("http://example.com/helloworld", {
    method = "POST",
    body = "a=1&b=2",
    headers = {
        ["Content-Type"] = "application/x-www-form-urlencoded",
    },
})
if not res then
    ngx.log(ngx.ERR, "la solicitud falló: ", err)
    return
end

-- En este punto, toda la solicitud / respuesta está completa y la conexión
-- se cerrará o volverá al grupo de conexiones.

-- La tabla `res` contiene los campos esperados `status`, `headers` y `body`.
local status = res.status
local length = res.headers["Content-Length"]
local body   = res.body

Solicitud transmitida

local httpc = require("resty.http").new()

-- Primero establece una conexión
local ok, err, ssl_session = httpc:connect({
    scheme = "https",
    host = "127.0.0.1",
    port = 8080,
})
if not ok then
    ngx.log(ngx.ERR, "la conexión falló: ", err)
    return
end

-- Luego envía usando `request`, proporcionando una ruta y un encabezado `Host` en lugar de un
-- URI completo.
local res, err = httpc:request({
    path = "/helloworld",
    headers = {
        ["Host"] = "example.com",
    },
})
if not res then
    ngx.log(ngx.ERR, "la solicitud falló: ", err)
    return
end

-- En este punto, el estado y los encabezados estarán disponibles para usar en la tabla `res`,
-- pero el cuerpo y cualquier trailer aún estarán en la red.

-- Podemos usar el iterador `body_reader` para transmitir el cuerpo según nuestro
-- tamaño de búfer deseado.
local reader = res.body_reader
local buffer_size = 8192

repeat
    local buffer, err = reader(buffer_size)
    if err then
        ngx.log(ngx.ERR, err)
        break
    end

    if buffer then
        -- procesar
    end
until not buffer

local ok, err = httpc:set_keepalive()
if not ok then
    ngx.say("falló al establecer keepalive: ", err)
    return
end

-- En este punto, la conexión estará de vuelta en el grupo de forma segura o cerrada.
````

## Conexión

## new

`syntax: httpc, err = http.new()`

Crea el objeto de conexión HTTP. En caso de fallos, devuelve `nil` y una cadena que describe el error.

## connect

`syntax: ok, err, ssl_session = httpc:connect(options)`

Intenta conectarse al servidor web mientras incorpora las siguientes actividades:

- Conexión TCP
- Negociación SSL
- Configuración del proxy HTTP

Al hacerlo, creará un nombre de grupo de conexiones distinto que es seguro de usar con conexiones basadas en SSL y/o proxy, y como tal, esta sintaxis se recomienda encarecidamente sobre la original (ahora obsoleta) [sintaxis de conexión solo TCP](#TCP-only-connect).

La tabla de opciones tiene los siguientes campos:

* `scheme`: esquema a utilizar, o nil para socket de dominio unix
* `host`: host de destino, o ruta a un socket de dominio unix
* `port`: puerto en el host de destino, que por defecto será `80` o `443` según el esquema
* `pool`: nombre de grupo de conexión personalizado. Opción según [documentación de OpenResty](https://github.com/openresty/lua-nginx-module#tcpsockconnect), excepto que el valor predeterminado se convertirá en un nombre de grupo construido utilizando las propiedades de SSL/proxy, lo cual es importante para la reutilización segura de conexiones. ¡Cuando tengas dudas, déjalo en blanco!
* `pool_size`: opción según [documentación de OpenResty](https://github.com/openresty/lua-nginx-module#tcpsockconnect)
* `backlog`: opción según [documentación de OpenResty](https://github.com/openresty/lua-nginx-module#tcpsockconnect)
* `proxy_opts`: subtabla, predeterminado a las opciones de proxy globales establecidas, ver [set\_proxy\_options](#set_proxy_options).
* `ssl_reused_session`: opción según [documentación de OpenResty](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake)
* `ssl_verify`: opción según [documentación de OpenResty](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake), excepto que por defecto es `true`.
* `ssl_server_name`: opción según [documentación de OpenResty](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake)
* `ssl_send_status_req`: opción según [documentación de OpenResty](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake)
* `ssl_client_cert`: se pasará a `tcpsock:setclientcert`. Requiere `ngx_lua_http_module` >= v0.10.23.
* `ssl_client_priv_key`: como arriba.

## set\_timeout

`syntax: httpc:set_timeout(time)`

Establece el tiempo de espera del socket (en ms) para operaciones posteriores. Ver [set\_timeouts](#set_timeouts) a continuación para un enfoque más declarativo.

## set\_timeouts

`syntax: httpc:set_timeouts(connect_timeout, send_timeout, read_timeout)`

Establece el umbral de tiempo de espera de conexión, el umbral de tiempo de espera de envío y el umbral de tiempo de espera de lectura, respectivamente, en milisegundos, para operaciones de socket posteriores (conectar, enviar, recibir e iteradores devueltos de receiveuntil).

## set\_keepalive

`syntax: ok, err = httpc:set_keepalive(max_idle_timeout, pool_size)`

Coloca la conexión actual en el grupo para su reutilización futura, o cierra la conexión. Llamar a esto en lugar de [close](#close) es "seguro" en el sentido de que cerrará condicionalmente dependiendo del tipo de solicitud. Específicamente, una solicitud `1.0` sin `Connection: Keep-Alive` se cerrará, al igual que una solicitud `1.1` con `Connection: Close`.

En caso de éxito, devuelve `1`. En caso de errores, devuelve `nil, err`. En el caso en que la conexión se cierre condicionalmente como se describió anteriormente, devuelve `2` y la cadena de error `la conexión debe cerrarse`, para distinguirla de errores inesperados.

Ver [documentación de OpenResty](https://github.com/openresty/lua-nginx-module#tcpsocksetkeepalive) para la documentación de parámetros.

## set\_proxy\_options

`syntax: httpc:set_proxy_options(opts)`

Configura un proxy HTTP que se utilizará con esta instancia de cliente. La tabla `opts` espera los siguientes campos:

* `http_proxy`: un URI a un servidor proxy que se utilizará con solicitudes HTTP
* `http_proxy_authorization`: un valor de encabezado `Proxy-Authorization` predeterminado que se utilizará con `http_proxy`, por ejemplo, `Basic ZGVtbzp0ZXN0`, que será sobrescrito si el encabezado de solicitud `Proxy-Authorization` está presente.
* `https_proxy`: un URI a un servidor proxy que se utilizará con solicitudes HTTPS
* `https_proxy_authorization`: como `http_proxy_authorization` pero para usar con `https_proxy` (ya que con HTTPS la autorización se realiza al conectar, este no puede ser sobrescrito pasando el encabezado de solicitud `Proxy-Authorization`).
* `no_proxy`: una lista separada por comas de hosts que no deben ser proxied.

Ten en cuenta que este método no tiene efecto al usar la obsoleta [sintaxis de conexión solo TCP](#TCP-only-connect).

## get\_reused\_times

`syntax: times, err = httpc:get_reused_times()`

Ver [documentación de OpenResty](https://github.com/openresty/lua-nginx-module#tcpsockgetreusedtimes).

## close

`syntax: ok, err = httpc:close()`

Ver [documentación de OpenResty](https://github.com/openresty/lua-nginx-module#tcpsockclose).

## Solicitudes

## request

`syntax: res, err = httpc:request(params)`

Envía una solicitud HTTP a través de una conexión ya establecida. Devuelve una tabla `res` o `nil` y un mensaje de error.

La tabla `params` espera los siguientes campos:

* `version`: El número de versión HTTP. Por defecto es `1.1`.
* `method`: La cadena del método HTTP. Por defecto es `GET`.
* `path`: La cadena de la ruta. Por defecto es `/`.
* `query`: La cadena de consulta, presentada como una cadena literal o tabla de Lua.
* `headers`: Una tabla de encabezados de solicitud.
* `body`: El cuerpo de la solicitud como una cadena, una tabla de cadenas o una función iteradora que produce cadenas hasta nil cuando se agota. Ten en cuenta que debes especificar un `Content-Length` para el cuerpo de la solicitud, o especificar `Transfer-Encoding: chunked` y hacer que tu función implemente la codificación. Ver también: [get\_client\_body\_reader](#get_client_body_reader).

Cuando la solicitud es exitosa, `res` contendrá los siguientes campos:

* `status`: El código de estado.
* `reason`: La frase de razón del estado.
* `headers`: Una tabla de encabezados. Múltiples encabezados con el mismo nombre de campo se presentarán como una tabla de valores.
* `has_body`: Una bandera booleana que indica si hay un cuerpo que leer.
* `body_reader`: Una función iteradora para leer el cuerpo de manera continua.
* `read_body`: Un método para leer todo el cuerpo en una cadena.
* `read_trailers`: Un método para fusionar cualquier trailer debajo de la tabla de encabezados.

Si la respuesta tiene un cuerpo, entonces antes de que la misma conexión pueda ser utilizada para otra solicitud, debes leer el cuerpo usando `read_body` o `body_reader`.

## request\_uri

`syntax: res, err = httpc:request_uri(uri, params)`

La interfaz de un solo uso (ver [uso](#Uso)). Dado que este método realiza una solicitud completa de extremo a extremo, las opciones especificadas en `params` pueden incluir cualquier cosa que se encuentre tanto en [connect](#connect) como en [request](#request) documentadas anteriormente. Ten en cuenta también que los campos `path` y `query`, en `params` sobrescribirán los componentes relevantes de `uri` si se especifican (`scheme`, `host` y `port` siempre se tomarán de `uri`).

Hay 3 parámetros adicionales para controlar los keepalives:

* `keepalive`: Establecer en `false` para deshabilitar los keepalives y cerrar inmediatamente la conexión. Por defecto es `true`.
* `keepalive_timeout`: El tiempo de espera máximo inactivo (ms). Por defecto es `lua_socket_keepalive_timeout`.
* `keepalive_pool`: El número máximo de conexiones en el grupo. Por defecto es `lua_socket_pool_size`.

Si la solicitud es exitosa, `res` contendrá los siguientes campos:

* `status`: El código de estado.
* `headers`: Una tabla de encabezados.
* `body`: Todo el cuerpo de la respuesta como una cadena.

## request\_pipeline

`syntax: responses, err = httpc:request_pipeline(params)`

Este método funciona según el método [request](#request) anterior, pero `params` es en su lugar una tabla anidada de tablas de parámetros. Cada solicitud se envía en orden, y `responses` se devuelve como una tabla de manejadores de respuesta. Por ejemplo:

```lua
local responses = httpc:request_pipeline({
    { path = "/b" },
    { path = "/c" },
    { path = "/d" },
})

for _, r in ipairs(responses) do
    if not r.status then
        ngx.log(ngx.ERR, "error de lectura de socket")
        break
    end

    ngx.say(r.status)
    ngx.say(r:read_body())
end

Debido a la naturaleza del pipelining, no se leen realmente respuestas hasta que intentas usar los campos de respuesta (estado / encabezados, etc.). Y dado que las respuestas se leen en orden, debes leer todo el cuerpo (y cualquier trailer si los tienes) antes de intentar leer la siguiente respuesta.

Ten en cuenta que esto no excluye el uso del lector de cuerpo de respuesta transmitido. Las respuestas aún pueden ser transmitidas, siempre que todo el cuerpo se transmita antes de intentar acceder a la siguiente respuesta.

Asegúrate de probar al menos un campo (como el estado) antes de intentar usar los otros, en caso de que haya ocurrido un error de lectura de socket.

Respuesta

res.body_reader

El iterador body_reader se puede usar para transmitir el cuerpo de la respuesta en tamaños de trozo de tu elección, de la siguiente manera:

local reader = res.body_reader
local buffer_size = 8192

repeat
    local buffer, err = reader(buffer_size)
    if err then
        ngx.log(ngx.ERR, err)
        break
    end

    if buffer then
        -- procesar
    end
until not buffer

Si el lector se llama sin argumentos, el comportamiento depende del tipo de conexión. Si la respuesta está codificada como trozos, entonces el iterador devolverá los trozos a medida que lleguen. Si no, simplemente devolverá todo el cuerpo.

Ten en cuenta que el tamaño proporcionado es en realidad un tamaño máximo. Así que en el caso de la transferencia en trozos, puedes obtener búferes más pequeños que el tamaño que pides, como un remanente de los trozos codificados reales.

res:read_body

syntax: body, err = res:read_body()

Lee todo el cuerpo en una cadena local.

res:read_trailers

syntax: res:read_trailers()

Esto fusiona cualquier trailer debajo de la tabla res.headers misma. Debe ser llamado después de leer el cuerpo.

Utilidad

parse_uri

syntax: local scheme, host, port, path, query? = unpack(httpc:parse_uri(uri, query_in_path?))

Esta es una función de conveniencia que permite usar más fácilmente la interfaz genérica, cuando los datos de entrada son un URI.

A partir de la versión 0.10, se agregó el parámetro opcional query_in_path, que especifica si la cadena de consulta debe incluirse en el valor de retorno path, o por separado como su propio valor de retorno. Esto predetermina a true para mantener la compatibilidad hacia atrás. Cuando se establece en false, path solo incluirá la ruta, y query contendrá los argumentos del URI, sin incluir el delimitador ?.

get_client_body_reader

syntax: reader, err = httpc:get_client_body_reader(chunksize?, sock?)

Devuelve una función iteradora que se puede usar para leer el cuerpo de la solicitud del cliente en un formato de transmisión. También puedes especificar un tamaño de trozo predeterminado opcional (el predeterminado es 65536), o un socket ya establecido en lugar de la solicitud del cliente.

Ejemplo:

local req_reader = httpc:get_client_body_reader()
local buffer_size = 8192

repeat
    local buffer, err = req_reader(buffer_size)
    if err then
        ngx.log(ngx.ERR, err)
        break
    end

    if buffer then
        -- procesar
    end
until not buffer

Este iterador también se puede usar como el valor para el campo body en los parámetros de solicitud, permitiendo transmitir el cuerpo de la solicitud en una solicitud upstream proxied.

local client_body_reader, err = httpc:get_client_body_reader()

local res, err = httpc:request({
    path = "/helloworld",
    body = client_body_reader,
})

Obsoleto

Estas características permanecen por compatibilidad hacia atrás, pero pueden ser eliminadas en futuras versiones.

Conexión solo TCP

Las siguientes versiones de la firma del método connect están obsoletas en favor del único argumento de table documentado arriba.

syntax: ok, err = httpc:connect(host, port, options_table?)

syntax: ok, err = httpc:connect("unix:/path/to/unix.sock", options_table?)

NOTA: el nombre de grupo predeterminado solo incorporará información de IP y puerto, por lo que es inseguro usarlo en caso de conexiones SSL y/o Proxy. Especifica tu propio grupo o, mejor aún, no uses estas firmas.

connect_proxy

syntax: ok, err = httpc:connect_proxy(proxy_uri, scheme, host, port, proxy_authorization)

Llamar a este método manualmente ya no es necesario ya que está incorporado dentro de connect. Se mantiene por ahora para compatibilidad con usuarios de la antigua sintaxis de conexión solo TCP.

Intenta conectarse al servidor web a través del servidor proxy dado. El método acepta los siguientes argumentos:

  • proxy_uri - URI completo del servidor proxy a utilizar (por ejemplo, http://proxy.example.com:3128/). Nota: Solo se admite el protocolo http.
  • scheme - El protocolo a utilizar entre el servidor proxy y el host remoto (http o https). Si se especifica https como esquema, connect_proxy() realiza una solicitud CONNECT para establecer un túnel TCP al host remoto a través del servidor proxy.
  • host - El nombre del host del host remoto al que conectarse.
  • port - El puerto del host remoto al que conectarse.
  • proxy_authorization - El valor del encabezado Proxy-Authorization enviado al servidor proxy a través de CONNECT cuando el scheme es https.

Si ocurre un error durante el intento de conexión, este método devuelve nil con una cadena que describe el error. Si la conexión se establece correctamente, el método devuelve 1.

Hay algunos puntos clave a tener en cuenta al usar esta API:

  • Si el esquema es https, necesitas realizar la negociación TLS con el servidor remoto manualmente usando el método ssl_handshake() antes de enviar cualquier solicitud a través del túnel proxy.
  • Si el esquema es http, necesitas asegurarte de que las solicitudes que envíes a través de las conexiones cumplan con RFC 7230 y especialmente Sección 5.3.2. que establece que el objetivo de la solicitud debe estar en forma absoluta. En la práctica, esto significa que cuando uses send_request(), la path debe ser un URI absoluto al recurso (por ejemplo, http://example.com/index.html en lugar de solo /index.html).

ssl_handshake

syntax: session, err = httpc:ssl_handshake(session, host, verify)

Llamar a este método manualmente ya no es necesario ya que está incorporado dentro de connect. Se mantiene por ahora para compatibilidad con usuarios de la antigua sintaxis de conexión solo TCP.

Ver documentación de OpenResty.

proxy_request / proxy_response

Estos dos métodos de conveniencia estaban destinados simplemente a demostrar un caso de uso común de implementación de proxy inverso, y el autor lamenta su inclusión en el módulo. Se anima a los usuarios a crear los suyos en lugar de depender de estas funciones, que pueden ser eliminadas en una versión posterior.

proxy_request

syntax: local res, err = httpc:proxy_request(request_body_chunk_size?)

Realiza una solicitud utilizando los argumentos de solicitud del cliente actual, efectivamente haciendo proxy a la upstream conectada. El cuerpo de la solicitud se leerá de manera continua, según request_body_chunk_size (ver documentación sobre el lector de cuerpo del cliente a continuación).

proxy_response

syntax: httpc:proxy_response(res, chunksize?)

Establece la respuesta actual según el res dado. Asegura que los encabezados hop-by-hop no se envíen hacia abajo, y leerá la respuesta según chunksize (ver documentación sobre el lector de cuerpo arriba).

GitHub

Puedes encontrar consejos de configuración adicionales y documentación para este módulo en el repositorio de GitHub para nginx-module-http.