OpenResty é o NGINX aprimorado com requisições e respostas dinâmicas

API7.ai

October 23, 2022

OpenResty (NGINX + Lua)

Após a introdução anterior, você deve ter entendido o conceito de OpenResty e como aprendê-lo. Este artigo nos guiará sobre como o OpenResty lida com as solicitações e respostas do cliente.

Embora o OpenResty seja um servidor web baseado no NGINX, ele é fundamentalmente diferente do NGINX: o NGINX é impulsionado por arquivos de configuração estáticos, enquanto o OpenResty é impulsionado pela API Lua, oferecendo mais flexibilidade e programabilidade.

Vou mostrar os benefícios da API Lua.

Categorias de API

Primeiro, precisamos saber que a API do OpenResty é dividida nas seguintes categorias amplas.

  • Processamento de solicitações e respostas.
  • Relacionado a SSL.
  • Dicionário compartilhado.
  • Cosocket.
  • Manipulação de tráfego de quatro camadas.
  • Processo e worker.
  • Acesso a variáveis e configurações do NGINX.
  • Funções gerais como strings, tempo, codec, etc.

Aqui, sugiro que você também abra a documentação da API Lua do OpenResty e verifique a lista de APIs para ver se consegue relacionar com essa categoria.

As APIs do OpenResty existem não apenas no projeto lua-nginx-module, mas também no projeto 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 e outras APIs.

Para APIs que não estão no projeto lua-nginx-module, você precisa requerê-las separadamente para usá-las. Por exemplo, se você quiser usar a função split, precisa chamá-la da seguinte forma.

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

Claro, isso pode confundir você: no projeto lua-nginx-module, existem várias APIs começando com ngx.re.sub, ngx.re.find, etc. Por que a API ngx.re.split é a única que precisa ser requerida antes de ser usada?

Como mencionamos no capítulo anterior sobre lua-resty-core, as novas APIs do OpenResty são implementadas no repositório lua-rety-core por meio de FFI, então há inevitavelmente uma sensação de fragmentação. Estou ansioso para resolver esse problema no futuro, unindo os projetos lua-nginx-module e lua-resty-core.

Solicitação

A seguir, vamos ver como o OpenResty lida com as solicitações e respostas do cliente. Primeiro, vamos ver a API para lidar com solicitações, mas há mais de 20 APIs começando com ngx.req, então por onde começamos?

Sabemos que as mensagens de solicitação HTTP consistem em três partes: a linha de solicitação, o cabeçalho de solicitação e o corpo da solicitação, então vou apresentar a API nessas três partes.

Linha de Solicitação

A primeira é a linha de solicitação, que contém o método de solicitação, URI e a versão do protocolo HTTP. No NGINX, você pode obter esse valor usando uma variável embutida, enquanto no OpenResty, ele corresponde à API ngx.var.*. Vamos ver dois exemplos.

  • A variável embutida $scheme, que representa o nome do protocolo no NGINX, é http ou https; no OpenResty, você pode usar ngx.var.scheme para retornar o mesmo valor.
  • $request_method representa o método de solicitação como GET, POST, etc.; no OpenResty, você pode retornar o mesmo valor via ngx.var.request_method.

Você pode visitar a documentação oficial do NGINX para obter uma lista completa das variáveis embutidas do NGINX: http://nginx.org/en/docs/http/ngx_http_core_module.html#variables.

Então surge a pergunta: por que o OpenResty fornece uma API separada para a linha de solicitação quando você pode obter os dados na linha de solicitação retornando o valor de uma variável como ngx.var.*?

O resultado contém muitos fatores:

  • Primeiro, não é recomendado ler repetidamente ngx.var devido à sua ineficiência de desempenho.
  • Segundo, por consideração ao aspecto amigável do programa, ngx.var retorna uma string, não um objeto Lua. É difícil de lidar ao obter args, que pode retornar vários valores.
  • Terceiro, do ponto de vista da flexibilidade, a maioria de ngx.var é somente leitura, e apenas algumas variáveis são graváveis, como $args e limit_rate. No entanto, frequentemente precisamos modificar o método, URI e args.

Portanto, o OpenResty fornece várias APIs dedicadas a manipular a linha de solicitação, que podem reescrever a linha de solicitação para operações subsequentes, como redirecionamento.

Vamos ver como obter o número da versão do protocolo HTTP através da API. A API do OpenResty ngx.req.http_version faz a mesma coisa que a variável $server_protocol do NGINX: retornar o número da versão do protocolo HTTP. No entanto, o valor de retorno dessa API não é uma string, mas em formato numérico, cujos valores possíveis são 2.0, 1.0, 1.1 e 0.9. Nil é retornado se o resultado estiver fora do intervalo desses valores.

Vamos ver o método de obter a solicitação na linha de solicitação. Como mencionado, a função e o valor de retorno de ngx.req.get_method e as variáveis $request_method do NGINX são os mesmos: em formato de string.

No entanto, o formato do parâmetro do método de solicitação HTTP atual ngx.req.set_method não é uma string, mas constantes numéricas embutidas. Por exemplo, o seguinte código reescreve o método de solicitação para POST.

ngx.req.set_method(ngx.HTTP_POST)

Para verificar que a constante embutida ngx.HTTP_POST é realmente um número e não uma string, você pode imprimir seu valor e ver se a saída é 8.

resty -e 'print(ngx.HTTP_POST)'

Dessa forma, o valor de retorno do método get é uma string, enquanto o valor de entrada do método set é um número. Está tudo bem quando o método set passa um valor confuso, porque a API pode falhar e relatar um erro 500. No entanto, na seguinte lógica de julgamento:

if (ngx.req.get_method() == ngx.HTTP_POST) then
    -- fazer algo
 end

Esse tipo de código funciona bem, não relata erros e é difícil de encontrar mesmo durante revisões de código. Eu cometi um erro semelhante antes e ainda me lembro: já havia passado por duas rodadas de revisões de código e casos de teste incompletos para tentar cobri-lo. No final, uma anomalia no ambiente online me levou ao problema.

Não há uma maneira prática de resolver esse tipo de problema, exceto ser mais cuidadoso ou adicionar outra camada de encapsulamento. Quando você projeta sua API de negócios, também pode considerar e manter o formato de parâmetro consistente dos métodos get e set, mesmo que precise sacrificar um pouco de desempenho.

Além disso, entre os métodos para reescrever a linha de solicitação, há duas APIs, ngx.req.set_uri e ngx.req.set_uri_args, que podem ser usadas para reescrever URI e args. Vamos ver esta configuração do NGINX.

rewrite ^ /foo?a=3? break;

Então, como podemos resolver isso com a API Lua equivalente? A resposta são as duas linhas de código a seguir.

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

Se você leu a documentação oficial, descobrirá que ngx.req.set_uri tem um segundo parâmetro: jump, que é "false" por padrão. Se você configurá-lo como "true", é igual a definir o sinalizador do comando rewrite para last em vez de break no exemplo acima.

No entanto, não sou muito fã da configuração de sinalizadores do comando rewrite, pois é ilegível e irreconhecível e muito menos intuitiva e sustentável do que o código.

Cabeçalho de Solicitação

Como sabemos, os cabeçalhos de solicitação HTTP estão no formato chave : valor, por exemplo:

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

No OpenResty, você pode usar ngx.req.get_headers para analisar e obter os cabeçalhos de solicitação, e o tipo de valor de retorno é table.

local h, err = ngx.req.get_headers()
  if err == "truncated" then
      -- pode-se escolher ignorar ou rejeitar a solicitação atual aqui
  end
  for k, v in pairs(h) do
      ...
  end

Por padrão, ele retorna os primeiros 100 cabeçalhos. Se o número exceder 100, ele relatará um erro truncated, deixando a decisão de como lidar com isso para o desenvolvedor. Você pode se perguntar por que fazer isso dessa forma, o que mencionarei mais tarde na seção sobre vulnerabilidades de segurança.

No entanto, devemos observar que o OpenResty não fornece uma API específica para obter um cabeçalho de solicitação especificado, o que significa que não há uma forma como ngx.req.header['host']. Se você tiver essa necessidade, deve confiar na variável do NGINX $http_xxx para alcançá-la. Então, no OpenResty, você pode obtê-la via ngx.var.http_xxx.

Agora vamos ver como devemos reescrever e excluir o cabeçalho de solicitação. As APIs para ambas as operações são bastante intuitivas:

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

Claro, a documentação oficial também menciona outros métodos para remover o cabeçalho de solicitação, como definir o valor do título como nil, etc. No entanto, ainda recomendo usar clear_header para fazer isso uniformemente para clareza do código.

Corpo da Solicitação

Finalmente, vamos ver o corpo da solicitação. Por razões de desempenho, o OpenResty não lê ativamente o corpo da solicitação, a menos que você force a diretiva lua_need_request_body a ser habilitada em nginx.conf. Além disso, para corpos de solicitação maiores, o OpenResty salva o conteúdo em um arquivo temporário no disco, então todo o processo de leitura do corpo da solicitação se parece com isso.

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 tem uma operação de bloqueio de IO para ler o arquivo do disco. Você deve ajustar a configuração de client_body_buffer_size (16 KB por padrão em sistemas de 64 bits) para minimizar as operações de bloqueio; você também pode configurar client_body_buffer_size e client_max_body_size para serem iguais e lidar com eles inteiramente na memória, dependendo do tamanho da sua memória e do número de solicitações simultâneas que você recebe.

Além disso, o corpo da solicitação pode ser reescrito. As duas APIs ngx.req.set_body_data e ngx.req.set_body_file aceitam uma string e um arquivo de disco local como parâmetros de entrada para realizar a reescrita do corpo da solicitação. No entanto, esse tipo de operação é incomum, e você pode verificar a documentação para mais detalhes.

Resposta

Após o processamento da solicitação, precisamos enviar uma resposta de volta ao cliente. Como a mensagem de solicitação, a mensagem de resposta também consiste em várias partes: a linha de status, o cabeçalho de resposta e o corpo da resposta. Vou apresentar as APIs correspondentes de acordo com essas três partes.

Linha de Status

O principal que nos preocupamos na linha de status é o código de status. Por padrão, o código de status HTTP retornado é 200, que é a constante ngx.HTTP_OK embutida no OpenResty. Mas no mundo do código, é sempre o código que lida com os casos mais excepcionais.

Se você detectar a mensagem de solicitação e descobrir que é uma solicitação maliciosa, então você precisa encerrar a solicitação:

ngx.exit(ngx.HTTP_BAD_REQUEST)

No entanto, há uma constante particular nos códigos de status HTTP do OpenResty: ngx.OK. Na situação de ngx.exit(ngx.OK), a solicitação sai da fase de processamento atual e passa para a próxima etapa, em vez de retornar diretamente ao cliente.

Claro, você também pode optar por não sair e apenas reescrever o código de status usando ngx.status, como escrito da seguinte forma.

ngx.status = ngx.HTTP_FORBIDDEN

Você pode consultá-los na documentação se quiser saber mais sobre as constantes de código de status.

Cabeçalho de Resposta

Em relação ao cabeçalho de resposta, há duas maneiras de configurá-lo. A primeira é a mais simples.

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

Aqui, o ngx.header contém as informações do cabeçalho de resposta, que podem ser lidas, modificadas e excluídas.

A segunda maneira de configurar o cabeçalho de resposta é ngx_resp.add_header, do repositório lua-resty-core, que adiciona uma mensagem de cabeçalho, chamada com:

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

A diferença com o primeiro método é que add_header não substitui um campo existente com o mesmo nome.

Corpo da Resposta

Finalmente, vamos ver o corpo da resposta. No OpenResty, você pode usar ngx.say e ngx.print para enviar o corpo da resposta.

ngx.say('hello, world')

A funcionalidade das duas APIs é idêntica, a única diferença é que ngx.say tem uma quebra de linha no final.

Para evitar a ineficiência da concatenação de strings, ngx.say / ngx.print suporta strings e formatos de array como parâmetros.

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

Este método ignora a concatenação de strings no nível Lua e deixa para as funções C lidarem com isso.

Resumo

Vamos revisar o conteúdo de hoje. Apresentamos as APIs do OpenResty associadas às mensagens de solicitação e resposta. Como você pode ver, a API do OpenResty é mais flexível e poderosa do que a diretiva do NGINX.

Consequentemente, a API Lua fornecida pelo OpenResty é suficiente para atender às suas necessidades ao lidar com solicitações HTTP? Deixe seus comentários e compartilhe este artigo com seus colegas e amigos para que possamos nos comunicar e melhorar juntos.