-- ************************************************************************
--
-- Common Gateway Interface (CGI)
-- Copyright 2020 by Sean Conner. All Rights Reserved.
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <
http://www.gnu.org/licenses/>.
--
-- Comments, questions and criticisms can be sent to:
[email protected]
--
-- ************************************************************************
-- luacheck: ignore 611
-- RFC-3875, with some deviations for gopher
local syslog = require "org.conman.syslog"
local errno = require "org.conman.errno"
local fsys = require "org.conman.fsys"
local process = require "org.conman.process"
local exit = require "org.conman.const.exit"
local mkios = require "org.conman.net.ios"
local nfl = require "org.conman.nfl"
local mklink = require "port70.mklink"
local io = require "io"
local coroutine = require "coroutine"
local require = require
local pairs = pairs
local ipairs = ipairs
local DEVNULI = io.open("/dev/null","r")
local DEVNULO = io.open("/dev/null","w")
-- ************************************************************************
local function fdtoios(fd)
local newfd = mkios()
newfd.__fd = fd
newfd.__co = coroutine.running()
newfd.close = function(self)
nfl.SOCKETS:remove(fd)
self.__fd:close()
return true
end
newfd._refill = function()
return coroutine.yield()
end
nfl.SOCKETS:insert(fd,'r',function(event)
if event.read then
local data,err = fd:read(8192)
if data then
if #data == 0 then
nfl.SOCKETS:remove(fd)
newfd._eof = true
end
nfl.schedule(newfd.__co,data)
else
if err ~= errno.EAGAIN then
syslog('error',"fd:read() = %s",errno[err])
end
end
else
newfd._eof = true
nfl.SOCKETS:remove(fd)
nfl.schedule(newfd.__co)
end
end)
return newfd
end
-- ************************************************************************
return function(program,cinfo,request,ios)
local conf = require "port70.CONF"
if not conf.cgi then
syslog('error',"CGI script %q called, but CGI not configured!",program)
ios:write(mklink { type = 'error' , display = "Selector not found" , selector = request.selector })
return false
end
local pipe,err1 = fsys.pipe()
if not pipe then
syslog('error',"CGI pipe: %s",errno[err1])
ios:write(mklink { type = 'error' , display = "Selector not found" , selector = request.selector })
return false
end
pipe.read:setvbuf('no') -- buffering kills the event loop
local child,err2 = process.fork()
if not child then
syslog('error',"process.fork() = %s",errno[err2])
ios:write(mklink { type = 'error' , display = "Selector not found" , selector = request.selector })
return false
end
-- =========================================================
-- The child runs off to do its own thang ...
-- =========================================================
if child == 0 then
fsys.redirect(DEVNULI,io.stdin);
fsys.redirect(pipe.write,io.stdout);
fsys.redirect(DEVNULO,io.stderr);
-- -----------------------------------------------------------------
-- Close file descriptors that aren't stdin, stdout or stderr. Most
-- Unix systems have dirfd(), right? Right? And /proc/self/fd,
-- right? Um ... erm ...
-- -----------------------------------------------------------------
local dir = fsys.opendir("/proc/self/fd")
if dir and dir._tofd then
local dirfh = dir:_tofd()
for file in dir.next,dir do
local fh = tonumber(file)
if fh > 2 and fh ~= dirfh then
fsys._close(fh)
end
end
-- ----------------------------------------------------------
-- if all else fails, at least close these to make this work
-- ----------------------------------------------------------
else
DEVNULI:close()
DEVNULO:close()
pipe.write:close()
pipe.read:close()
end
local cwd = conf.cgi.cwd
local no_slash = conf.cgi.no_slash
local args = {}
local env = {}
if conf.cgi.env then
for var,val in pairs(conf.cgi.env) do
env[var] = val
end
end
if conf.cgi.instance then
for name,info in pairs(conf.cgi.instance) do
if request.rest:match(name) then
if info.cwd then cwd = info.cwd end
-- ---------------------------------------------------------------
-- We want an instance no_slash to override a global no_slash, but
-- if we can't just simply assign no_slash to the instance
-- no_slash because if it doesn't exist, then it will set no_slash
-- to false, possibly overriding the global var, which is NOT what
-- is wanted here. We need to check if the instance no_slash
-- exists to properly override it.
-- ---------------------------------------------------------------
if type(info.no_slash) == 'boolean' then
no_slash = info.no_slash
end
if info.arg then
for i,arg in ipairs(info.arg) do
args[i] = arg
end
end
if info.env then
for var,val in pairs(info.env) do
env[var] = val
end
end
end
end
end
local _,e = program:find(cinfo.directory,1,true)
local script = e and program:sub(e+1,-1) or program
if no_slash and script:match("^/") then
script = script:sub(2,-1)
end
env.GOPHER_DOCUMENT_ROOT = cinfo.directory
env.GOPHER_SCRIPT_FILENAME = program
env.GOPHER_SELECTOR = request.selector .. request.rest
env.GATEWAY_INTERFACE = "CGI/1.1"
env.QUERY_STRING = request.search or ""
env.REMOTE_ADDR = request.remote.addr
env.REMOTE_HOST = request.remote.addr
env.REQUEST_METHOD = ""
env.SCRIPT_NAME = request.selector .. "/" .. script
env.SERVER_NAME = conf.network.host
env.SERVER_PORT = conf.network.port
env.SERVER_PROTOCOL = "GOPHER"
env.SERVER_SOFTWARE = "port70"
_,e = request.rest:find(fsys.basename(program),1,true)
local pathinfo = e and request.rest:sub(e+1,-1) or request.selector
if pathinfo ~= "" then
env.PATH_TRANSLATED = env.GOPHER_DOCUMENT_ROOT .. request.rest
pathinfo = pathinfo .. request.rest
if no_slash and pathinfo:match("^/") then
pathinfo = pathinfo:sub(2,-1)
end
env.PATH_INFO = pathinfo
end
if cwd then
local okay,err3 = fsys.chdir(cwd)
if not okay then
syslog('error',"CGI cwd(%q) = %s",cwd,errno[err3])
process.exit(exit.CONFIG)
end
end
process.exec(program,args,env)
process.exit(exit.OSERR)
end
-- =========================================================
-- Meanwhile, back at the parent's place ...
--
-- NOTE: the CGI script is reponsible for sending the final '.' if the
-- output is text.
-- =========================================================
pipe.write:close()
local inp = fdtoios(pipe.read)
repeat
local data = inp:read(1024)
if data then ios:write(data) end
until not data
inp:close()
local info,err4 = process.wait(child)
if not info then
syslog('error',"process.wait() = %s",errno[err4])
return true,true
end
if info.status == 'normal' then
if info.rc == 0 then
return true,true
else
syslog('warning',"program=%q status=%d",program,info.rc)
return true,true
end
else
syslog('error',"program=%q status=%s description=%s",program,info.status,info.description)
return true,true
end
end