Skip to content

Instantly share code, notes, and snippets.

@pkulchenko
Last active May 19, 2023 00:07
Show Gist options
  • Save pkulchenko/87b017e65a4f56ddb9b51545b9d37f1b to your computer and use it in GitHub Desktop.
Save pkulchenko/87b017e65a4f56ddb9b51545b9d37f1b to your computer and use it in GitHub Desktop.
CGI example for redbean web server
local MAXTIME = 5 -- sec
local function cgi(cmd, opts)
if not cmd or not cmd[1] then error('missing command') end
if not opts then opts = {} end
local nph = opts.nph
local maxtime = opts.maxtime or MAXTIME
local env = {}
local envu = opts.env or {}
local envd = #envu > 0 and envu or {
SERVER_ADDR = FormatIp(GetServerAddr()),
SERVER_PORT = select(2, GetServerAddr()),
SERVER_SOFTWARE = "redbean", -- fm.getBrand()
REMOTE_HOST = GetHost(),
REMOTE_ADDR = FormatIp(GetRemoteAddr()),
REMOTE_INDENT = ParseUrl(GetUrl()).user,
REQUEST_SCHEME = GetScheme(),
REQUEST_METHOD = GetMethod(),
GATEWAY_INTERFACE = "CGI/1.1",
SERVER_PROTOCOL = ("HTTP/%01.1f"):format(GetHttpVersion()/10),
QUERY_STRING = EncodeUrl({params = ParseUrl(GetUrl()).params}):sub(2),
REQUEST_URI = (function(u) return EncodeUrl({params = u.params, path = u.path})end)(ParseUrl(GetUrl())),
HTTPS = GetSslIdentity() and "on" or nil,
PATH_INFO = GetPath(),
PATH_TRANSLATED = GetEffectivePath(),
CONTENT_TYPE = GetHeader("Content-Type"),
CONTENT_LENGTH = GetHeader("Content-Length"),
}
if #envu == 0 then
-- provide all the headers
for k, v in pairs(GetHeaders()) do envd["HTTP_"..k:upper()] = v end
-- overwrite all defaults
for k, v in pairs(envu) do envd[k] = v end
-- convert to strings unless the value is false
for k, v in pairs(envd) do if v then table.insert(env, k.."="..v) end end
end
local rfd1, wfd1 = unix.pipe(unix.O_CLOEXEC)
local rfd2, wfd2 = unix.pipe(unix.O_CLOEXEC)
local pid = unix.fork()
if pid == 0 then -- forked child
assert(unix.close(GetClientFd())) -- close client fd to not send anything
assert(unix.dup(rfd2, 0)) -- redirect stdin
assert(unix.dup(wfd1, 1)) -- redirect stdout
assert(unix.dup(wfd1, 2)) -- redirect stderr
assert(unix.close(wfd1))
assert(unix.close(rfd1))
assert(unix.close(wfd2))
assert(unix.close(rfd2))
assert(unix.sigaction(unix.SIGQUIT, unix.exit))
unix.execve(cmd[1], cmd, env)
unix.exit(127)
else
unix.write(wfd2, GetBody())
local isactive = true
local header = true
local done = false
assert(unix.sigaction(unix.SIGALRM, function() isactive = false end))
assert(unix.setitimer(unix.ITIMER_REAL, 0, 0, maxtime, 0))
local cfd = GetClientFd()
while true do
-- block for a bit until there is output from the launched process
local se = (unix.poll({[rfd1] = unix.POLLIN}, maxtime*1000/10) or {})[rfd1] or 0
if se & (unix.POLLHUP + unix.POLLERR) > 0 then break end
-- check if the client is (still) writable
local re = (unix.poll({[cfd] = unix.POLLOUT}) or {})[cfd] or 0
if re & (unix.POLLHUP + unix.POLLERR) > 0 then break end
-- check if the process took too long to respond
if not isactive then break end
local data, errno
if se & unix.POLLIN > 0 then data, errno = unix.read(rfd1) end
-- check for end of file or any descriptor errors
if data == "" or errno and errno:errno() == unix.EBADF then break end
if data then
if header then
local pos = 1
while true do
-- allow both CRLF and LF to be handled
local spos, epos = data:find("\r?\n", pos, false)
if not spos then break end
-- found an empty string, which signals the end of headers
if spos == pos then header = false; pos = epos + 1; break end
local status, reason
if pos == 1 then
status, reason = data:sub(pos, spos-1):match("^HTTP/%d%.%d%s+(%d%d%d)%s+(.+)")
end
if status then
-- found the status line
SetStatus(status, reason)
else
-- found something that may be a header
local name, value = data:sub(pos, spos-1):match("^(%a[%w_-]+):%s*(.+)")
-- check if the header has a valid syntax
-- if not, skip it and handle it as part of the data;
-- this may happen if an error is thrown instead of a valid context
local ok, err = pcall(SetHeader, name, value)
if not name or not value or not ok then
header = false
break
end
end
pos = epos + 1
end
-- finished parsing headers, so remove them from data
if not header and pos > 1 then data = data:sub(pos) end
end
-- if the headers are set, write the response
if not header then
Write(data)
if nph then coroutine.yield() end
end
-- check if reading was interrupted or wasn't done
elseif not data and (not errno or errno:errno() == unix.EINTR) then
-- closing connection will fail on send
-- if it doesn't fail, then redo the interrupted read
else -- report an error and stop
-- TODO: log error
break
end
-- stop reading if the process is already done
done = not unix.wait(pid, unix.WNOHANG)
if done then break end
end
unix.close(rfd1)
unix.close(wfd1)
unix.close(rfd2)
unix.close(wfd2)
-- kill launched process if it's still running, as it's been running for too long
if not done then assert(unix.kill(pid, unix.SIGTERM)) end
end
end
OnHttpRequest = function()
cgi({'./redbean.com',
'-e', [[print('HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nEnvironment:') for _,v in ipairs(unix.environ()) do Sleep(0.1) print(_, v) end]], '-e', 'unix.exit()'},
{nph = true, env = {HTTP_DNT = false}})
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment