exec: Executar programas externos no nginx-module-lua sem criar um shell ou bloquear
Instalação
Se você ainda não configurou a assinatura do repositório RPM, inscreva-se. Em seguida, você pode prosseguir com os seguintes passos.
CentOS/RHEL 7 ou 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-exec
CentOS/RHEL 8+, Fedora Linux, Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install lua5.1-resty-exec
Para usar esta biblioteca Lua com o NGINX, certifique-se de que o nginx-module-lua esteja instalado.
Este documento descreve lua-resty-exec v3.0.3 lançado em 22 de agosto de 2017.
Um pequeno módulo Lua para executar processos. É principalmente destinado a ser usado com OpenResty, mas funcionará em aplicações Lua regulares também. Quando usado com OpenResty, é completamente não bloqueante (caso contrário, recai sobre o uso do LuaSocket e bloqueia).
É semelhante a (e inspirado por) lua-resty-shell, a principal diferença é que este módulo usa sockexec, que não cria um shell - em vez disso, você fornece um array de strings de argumento, o que significa que você não precisa se preocupar com regras de escape/citação/análise de shell.
Além disso, a partir da versão 2.0.0, você pode usar resty.exec.socket para acessar uma interface de nível inferior que permite comunicação bidirecional com programas. Você pode ler e escrever em aplicações em execução!
Isso requer que seu servidor web tenha uma instância ativa de sockexec em execução.
Changelog
3.0.0- novo campo retornado:
unknown- se isso acontecer, por favor, me envie um bug! 2.0.0- Novo módulo
resty.exec.socketpara usar uma conexão duplex resty.execnão usa mais o argumentobufsizeresty.execagora aceita um argumentotimeout, especificado em milissegundos, padrão de 60s- Esta é uma revisão importante, por favor, teste minuciosamente antes de atualizar!
- Sem changelog antes de
2.0.0
Uso do resty.exec
local exec = require'resty.exec'
local prog = exec.new('/tmp/exec.sock')
Cria um novo objeto prog, usando /tmp/exec.sock para sua conexão com sockexec.
A partir daí, você pode usar prog de algumas maneiras diferentes:
modo ez
local res, err = prog('uname')
-- res = { stdout = "Linux\n", stderr = nil, exitcode = 0, termsig = nil }
-- err = nil
ngx.print(res.stdout)
Isso executará uname, sem dados no stdin.
Retorna uma tabela de códigos de saída/erro, com err definido para quaisquer erros encontrados.
Configurar argv com antecedência
prog.argv = { 'uname', '-a' }
local res, err = prog()
-- res = { stdout = "Linux localhost 3.10.18 #1 SMP Tue Aug 2 21:08:34 PDT 2016 x86_64 GNU/Linux\n", stderr = nil, exitcode = 0, termsig = nil }
-- err = nil
ngx.print(res.stdout)
Configurar stdin com antecedência
prog.stdin = 'isso é legal!'
local res, err = prog('cat')
-- res = { stdout = "isso é legal!", stderr = nil, exitcode = 0, termsig = nil }
-- err = nil
ngx.print(res.stdout)
Chamar com argv explícito, dados de stdin, callbacks de stdout/stderr
local res, err = prog( {
argv = 'cat',
stdin = 'divertido!',
stdout = function(data) print(data) end,
stderr = function(data) print("erro:", data) end
} )
-- res = { stdout = nil, stderr = nil, exitcode = 0, termsig = nil }
-- err = nil
-- 'divertido!' é impresso
Nota: aqui argv é uma string, o que é aceitável se seu programa não precisar de argumentos.
Configurar callbacks de stdout/stderr
Se você definir prog.stdout ou prog.stderr como uma função, ela será chamada para cada pedaço de dados de stdout/stderr recebido.
Por favor, note que não há garantias de que stdout/stderr seja uma string completa, ou algo particularmente sensato a esse respeito!
prog.stdout = function(data)
ngx.print(data)
ngx.flush(true)
end
local res, err = prog('some-program')
Tratar timeouts como não erros
Por padrão, sockexec trata um timeout como um erro. Você pode desativar isso definindo a chave timeout_fatal do objeto como falsa. Exemplos:
-- definir timeout_fatal = false nos objetos prog
prog.timeout_fatal = false
-- ou, defina no momento da chamada:
local res, err = prog({argv = {'cat'}, timeout_fatal = false})
Mas eu realmente quero um shell!
Sem problemas! Você pode simplesmente fazer algo como:
local res, err = prog('bash','-c','echo $PATH')
Ou se você quiser executar um script inteiro:
prog.stdin = script_data
local res, err = prog('bash')
-- isso é aproximadamente equivalente a executar `bash < script` no CLI
Daemonizando processos
Geralmente, recomendo contra daemonizar processos - acho que é muito melhor usar algum tipo de fila de mensagens e/ou sistema de supervisão, para que você possa monitorar processos, tomar ações em caso de falha, e assim por diante.
Dito isso, se você quiser iniciar algum processo, você pode usar start-stop-daemon, ou seja:
local res, err = prog('start-stop-daemon','--pidfile','/dev/null','--background','--exec','/usr/bin/sleep', '--start','--','10')
isso iniciará sleep 10 como um processo em segundo plano destacado.
Se você não quiser lidar com start-stop-daemon, eu tenho uma pequena utilidade para iniciar um programa em segundo plano chamada idgaf, ou seja:
local res, err = prog('idgaf','sleep','10')
Isso basicamente alcançará a mesma coisa que start-stop-daemon faz sem exigir um bilhão de flags.
Uso do resty.exec.socket
local exec_socket = require'resty.exec.socket'
-- você pode especificar o timeout em milissegundos, opcional
local client = exec_socket:new({ timeout = 60000 })
-- cada nova instância de programa requer uma nova
-- chamada para conectar
local ok, err = client:connect('/tmp/exec.sock')
-- enviar argumentos do programa, aceita apenas uma tabela de
-- argumentos
client:send_args({'cat'})
-- enviar dados para stdin
client:send('olá')
-- receber dados
local data, typ, err = client:receive()
-- `typ` pode ser um dos:
-- `stdout` - dados da saída padrão do programa
-- `stderr` - dados da saída de erro do programa
-- `exitcode` - o código de saída do programa
-- `termsig` - se terminado via sinal, qual sinal foi usado
-- se `err` estiver definido, data e typ serão nil
-- valores comuns de `err` são `closed` e `timeout`
print(string.format('Recebido %s dados: %s',typ,data))
-- imprimirá 'Recebido stdout dados: olá'
client:send('ei, este processo cat ainda está em execução')
data, typ, err = client:receive()
print(string.format('Recebido %s dados: %s',typ,data))
-- imprimirá 'Recebido stdout dados: ei, este processo cat ainda está em execução'
client:send_close() -- fecha stdin
data, typ, err = client:receive()
print(string.format('Recebido %s dados: %s',typ,data))
-- imprimirá 'Recebido exitcode dados: 0'
data, typ, err = client:receive()
print(err) -- imprimirá 'closed'
Métodos do objeto client:
ok, err = client:connect(path)
Conecta via socket unix ao caminho dado. Se isso estiver sendo executado no nginx, a string unix: será adicionada automaticamente.
bytes, err = client:send_args(args)
Envia uma tabela de argumentos para sockexec e inicia o programa.
bytes, err = client:send_data(data)
Envia data para a entrada padrão do programa.
bytes, err = client:send(data)
Apenas um atalho para client:send_data(data).
bytes, err = client:send_close()
Fecha a entrada padrão do programa. Você também pode enviar uma string vazia, como client:send_data('').
data, typ, err = client:receive()
Recebe dados do processo em execução. typ indica o tipo de dados, que pode ser stdout, stderr, termsig, exitcode.
err é tipicamente closed ou timeout.
client:close()
Fecha forçosamente a conexão do cliente.
client:getfd()
Um método getfd, útil se você quiser monitorar a conexão de socket subjacente em um loop de seleção.
Alguns exemplos de configurações do nginx
Assumindo que você está executando sockexec em /tmp/exec.sock
$ sockexec /tmp/exec.sock
Então, na sua configuração do nginx:
location /uname-1 {
content_by_lua_block {
local prog = require'resty.exec'.new('/tmp/exec.sock')
local data,err = prog('uname')
if(err) then
ngx.say(err)
else
ngx.say(data.stdout)
end
}
}
location /uname-2 {
content_by_lua_block {
local prog = require'resty.exec'.new('/tmp/exec.sock')
prog.argv = { 'uname', '-a' }
local data,err = prog()
if(err) then
ngx.say(err)
else
ngx.say(data.stdout)
end
}
}
location /cat-1 {
content_by_lua_block {
local prog = require'resty.exec'.new('/tmp/exec.sock')
prog.stdin = 'isso é legal!'
local data,err = prog('cat')
if(err) then
ngx.say(err)
else
ngx.say(data.stdout)
end
}
}
location /cat-2 {
content_by_lua_block {
local prog = require'resty.exec'.new('/tmp/exec.sock')
local data,err = prog({argv = 'cat', stdin = 'incrível'})
if(err) then
ngx.say(err)
else
ngx.say(data.stdout)
end
}
}
location /slow-print {
content_by_lua_block {
local prog = require'resty.exec'.new('/tmp/exec.sock')
prog.stdout = function(v)
ngx.print(v)
ngx.flush(true)
end
prog('/usr/local/bin/slow-print')
}
# veja em `/misc` deste repositório por `slow-print`
}
location /shell {
content_by_lua_block {
local prog = require'resty.exec'.new('/tmp/exec.sock')
local data, err = prog('bash','-c','echo $PATH')
if(err) then
ngx.say(err)
else
ngx.say(data.stdout)
end
}
}
GitHub
Você pode encontrar dicas adicionais de configuração e documentação para este módulo no repositório do GitHub para nginx-module-exec.