--[[------------------------------------------------------------------------
global table holding the cgi functions
--]]------------------------------------------------------------------------
local _cgi = {}
--[[------------------------------------------------------------------------
local functions/variables used in cgi member functions
--]]------------------------------------------------------------------------
local html_codes = {
[string.char(tonumber('0xE4', 16))] = 'ä',
[string.char(tonumber('0xC4', 16))] = 'Ä',
[string.char(tonumber('0xF6', 16))] = 'ö',
[string.char(tonumber('0xD6', 16))] = 'Ö',
[string.char(tonumber('0xFC', 16))] = 'ü',
[string.char(tonumber('0xDC', 16))] = 'Ü',
[string.char(tonumber('0xDF', 16))] = 'ß',
[string.char(tonumber('0xA4', 16))] = '€',
[string.char(tonumber('0x3C', 16))] = '<',
[string.char(tonumber('0x3E', 16))] = '>',
[string.char(tonumber('0x26', 16))] = '&',
[string.char(tonumber('0x22', 16))] = '"'
}
local html_codes_match = '['
for code, _ in pairs(html_codes) do
html_codes_match = html_codes_match .. code
end
html_codes_match = html_codes_match .. ']'
-- translate html special char to mapped string in 'html_codes'
local function html_map(str)
return html_codes[str]
end
-- convert hex format to character
local function hex_to_ch(str)
return string.char(tonumber(str,16))
end
-- convert character to hex format
local function ch_to_hex(str)
return string.format('%%%X', string.byte(str))
end
--[[------------------------------------------------------------------------
CGI related environment variables
--]]------------------------------------------------------------------------
local _cgi_vars = {
'AUTH_TYPE', 'CONTENT_LENGTH', 'CONTENT_TYPE', 'GATEWAY_INTERFACE',
'PATH_INFO', 'PATH_TRANSLATED', 'QUERY_STRING', 'REMOTE_ADDR',
'REMOTE_HOST', 'REMOTE_IDENT', 'REMOTE_USER', 'REQUEST_METHOD',
'SCRIPT_NAME', 'SERVER_NAME', 'SERVER_PORT', 'SERVER_PROTOCOL',
'SERVER_SOFTWARE'
}
--[[------------------------------------------------------------------------
send error message and discard all gathered content previously
--]]------------------------------------------------------------------------
function _cgi.send_error(title, message, terminate)
-- send an error message provided by the caller
io.write(
'Content-type: text/html\n\n',
'
',
(message or ''), ''
)
-- terminate program if 'terminate' is 'true'
if (terminate) then os.exit() end
end
--[[------------------------------------------------------------------------
set reply message content type
--]]------------------------------------------------------------------------
function _cgi.set_content_type(content_type)
_cgi.content_type = content_type
end
--[[------------------------------------------------------------------------
set reply message content type
--]]------------------------------------------------------------------------
function _cgi.set_status(status, message)
_cgi.status = status .. ' '.. message
end
--[[------------------------------------------------------------------------
append HTTP/CGI header to reply
header must not include newline character anywhere in the string
--]]------------------------------------------------------------------------
function _cgi.send_header(header)
if (_cgi.headers_sent) then
error('Headers were already sent')
end
table.insert(_cgi.headers, header)
end
function _cgi.redirect(location)
_cgi.send_header('Location: '..location)
end
--[[------------------------------------------------------------------------
name string
value string
max_age string [optional]
path string [optional]
domain string [optional]
secure boolean
--]]------------------------------------------------------------------------
function _cgi.add_cookie(name, value, max_age, path, domain, secure)
local header = 'Set-Cookie: '..name..'='..value
if (max_age) then header = header..'; Max-age='..max_age end
if (path) then header = header..'; Path='..path end
if (domain) then header = header..'; Domain='..domain end
if (secure) then header = header..'; Secure' end
_cgi.send_header(header)
end
--[[------------------------------------------------------------------------
send all gathered headers to standard output (called by cgi.done)
--]]------------------------------------------------------------------------
local function _cgi_send_headers()
if (not _cgi.headers_sent) then
-- status message
io.write('Status: ', _cgi.status, '\n')
-- write content type header
io.write('Content-type: ', _cgi.content_type, '\n')
-- write all gathered headers with a newline in the end
-- use ipairs as order may be important
for _, header in ipairs(_cgi.headers) do
io.write(header, '\n')
end
-- write empty line indicating end of headers
io.write('\n')
_cgi.headers_sent = true
end
end
--[[------------------------------------------------------------------------
append information to main content of cgi reply
--]]------------------------------------------------------------------------
function _cgi.send(...)
if (not _cgi.options.use_buffer) then
if (not _cgi.headers_sent) then
_cgi_send_headers()
end
io.write(table.concat(arg))
return
end
-- using a table to 'buffer' the contents instead of concatenating all
-- strings wich might degrade performance
table.insert(_cgi.content, table.concat(arg))
-- everytime we reach 1000 elements in the table, concatenate the
-- contents so we don't use too much memory by using this method to
-- concatenate strings
if (table.getn(_cgi.content) > 1000) then
-- create new list so existing strings may be garbage collected next
-- time a collection cycle takes place
_cgi.content = { table.concat(_cgi.content) }
end
end
--[[------------------------------------------------------------------------
send the gathered contents to standard output
--]]------------------------------------------------------------------------
function _cgi_send_content()
--table.foreachi(E.content, function(k,v) io.write(v) end)
io.write(table.concat(_cgi.content))
end
--[[------------------------------------------------------------------------
Decodes an encoded URL
--]]------------------------------------------------------------------------
function _cgi.url_decode(text)
-- convert '+' to spaces, and convert hex codes to characters
return string.gsub(string.gsub(text, '+', ' '), '%%(%x%x)', hex_to_ch)
end
--[[------------------------------------------------------------------------
Encodes an URL
--]]------------------------------------------------------------------------
function _cgi.url_encode(text)
-- convert spaces to '+' and any non-alphanumeric characters, underscore,
-- '-' and '+' to hex format
return string.gsub(string.gsub(text, ' ', '+'), '[^%w_%.%-+]', ch_to_hex)
end
--[[------------------------------------------------------------------------
replace special characters understood by html with encoded strings
--]]------------------------------------------------------------------------
function _cgi.html_encode(text)
return (string.gsub(text, html_codes_match, html_map))
end
--[[------------------------------------------------------------------------
decode multipart_post formatted string into a table
--]]------------------------------------------------------------------------
local function _cgi_process_multipart_post(content, t)
-- pointer inside content indicating next location to be parsed/read
local cpos = 1
local name = ''
local file = false
local delim, line, value
local first, last, token, ltoken, ptoken, _, state, size
-- get a line without the newline character(s) in the end
local function getline()
first, last = string.find(content, '\n', cpos, 1)
if (not first) then
return string.sub(content, cpos), string.len(content) + 1
else
if (first > 1 and string.sub(content, first - 1, first - 1) == '\r') then
first = first - 1
end
return string.sub(content, cpos, first - 1), last + 1
end
end
-- needed initializations
name = ''
file = false
delim, cpos = getline()
if (string.len(delim) == 0) then
return
end
while (cpos <= string.len(content)) do
line, cpos = getline()
if (string.len(line) == 0) then
-- get bounded content
first, last = string.find(content, '\n'..delim, cpos, true)
if (not first) then
value = string.sub(content, cpos)
cpos = string.len(content) + 1
else
if (first > 1 and string.sub(content, first-1, first-1) == '\r') then
first = first - 1
end
value = string.sub(content, cpos, first - 1)
cpos = last + 1
-- skip boundary
line, cpos = getline()
end
if (string.len(name) > 0) then
t[name] = value
if (file) then
t[name..':size'] = string.len(value)
end
end
name = ''
file = false
else
-- parse line tokens
ptoken = 1
-- state machine:
-- 0 - match token -> x
-- 1 - save name -> 0
-- 2 - save filename -> 0
-- 3 - save content-type -> 0
-- 9 - skip token -> 0
state = 0
while (ptoken <= string.len(line)) do
-- skip spaces and ';'
_, ptoken = string.find(line, '[^%s;]', ptoken)
-- no more processing to do -> break cycle
if (not ptoken) then
break
end
-- if token starts with '"' then scan the line untill a
-- matching '"' is found
if (string.sub(line, ptoken, ptoken) == '"') then
first = string.find(line, '"', ptoken + 1, true)
if (first) then
token = string.sub(line, ptoken + 1, first - 1)
ptoken = first + 1
else
token = string.sub(line, ptoken + 1)
ptoken = string.len(line) + 1
end
else
first = string.find(line, '[%s;"]', ptoken)
if (first) then
token = string.sub(line, ptoken, first - 1)
ptoken = first + 1
else
token = string.sub(line, ptoken)
ptoken = string.len(line) + 1
end
end
ltoken = string.lower(token)
if (state == 0) then
if (ltoken == 'content-disposition:') then
state = -1 -- skip next token
elseif (ltoken == 'name=') then
state = 1 -- next token sets name
elseif (ltoken == 'filename=') then
state = 2
elseif (ltoken == 'content-type:') then
state = 3
end
else
if (state == 1) then
name = token -- save name
elseif (state == 2 and name) then
t[name..':filename'] = token -- filename
file = true
elseif (state == 3 and name) then
t[name..':content-type'] = token -- content-type
end
state = 0
end -- if state
end -- while ptoken <= len(line)
end -- if line or contents
end -- while cpos <= len(content)
end
--[[------------------------------------------------------------------------
decode contents of string in the form:
name1=value1...
using terminator as a separator
ex: terminator = '&'
name1=value1&name2=value2&...&nameN=valueN
values will be 'url decoded' and stored in the given table 't'
--]]------------------------------------------------------------------------
local function _cgi_decode_pair(str, t, terminator)
local k, v, first, pos
local last = 0
local len = string.len(str)
while (last and last < len) do
pos = last + 1
first, last = string.find(str, '=', pos, true)
if (not first) then
k = string.sub(str, pos)
v = ''
else
k = string.sub(str, pos, first - 1)
pos = last + 1
first, last = string.find(str, terminator, pos, true)
if (not first) then
v = string.sub(str, pos)
else
v = string.sub(str, pos, first - 1)
end
end
t[k] = _cgi.url_decode(v)
end
end
--[[------------------------------------------------------------------------
initialize cgi information
options is a list of options to override default options
--]]------------------------------------------------------------------------
function _cgi.init(options)
options = options or {}
_cgi.options = {
use_temp = true, -- use temporary files for multipart/* messages :TODO
use_buffer = true, -- buffer content information
max_content = 4096, -- maximum size of content read from stdin
}
_cgi.env = {} -- environment variables (CGI related)
_cgi.vars = {} -- variables received (QUERY_STRING/FORM DATA)
_cgi.cookies = {} -- existing cookies
_cgi.headers = {} -- headers to be sent in cgi reply
_cgi.content = {} -- content to be sent in cgi reply
_cgi.status = '200 OK' -- default status
_cgi.content_type = 'text/html' -- default reply content type
_cgi.headers_sent = false -- signal if headers were already sent
for k, v in pairs(options) do
if (_cgi.options[k]) then
_cgi.options[k] = v
end
end
-- retrieve CGI related environment variables information
for _, v in pairs(_cgi_vars) do
_cgi.env[v] = os.getenv(v)
end
-- check used method or assume GET
_cgi.env.REQUEST_METHOD = _cgi.env.REQUEST_METHOD or 'GET'
-- POST information is read from standard input
if _cgi.env.REQUEST_METHOD == 'POST' then
-- standard input information has a limit given in CONTENT_LENGTH
_cgi.env.CONTENT_LENGTH = _cgi.env.CONTENT_LENGTH or '0'
_cgi.env.CONTENT_TYPE = _cgi.env.CONTENT_TYPE or ''
local ctype = _cgi.env.CONTENT_TYPE
local clen = tonumber(_cgi.env.CONTENT_LENGTH)
if (clen > _cgi.options.max_content) then
_cgi.error = 'CONTENT_LENGTH exceeds limit set ('.._cgi.options.max_content..')'
return false
end
if ctype == 'application/x-www-form-urlencoded' then
-- decode url encoded information
_cgi_decode_pair(io.read(clen), _cgi.vars, '&')
elseif string.sub(ctype, 1, 19) == 'multipart/form-data' then
-- decode multipart format information
_cgi_process_multipart_post(io.read(clen), _cgi.vars)
end
else
-- decode url encoded information given in QUERY_STRING
_cgi_decode_pair(_cgi.env.QUERY_STRING or '', _cgi.vars, '&')
end
-- decode cookies and save them in cgi.cookies
_cgi_decode_pair(os.getenv('HTTP_COOKIES') or '', _cgi.cookies, ';')
return true
end
--[[------------------------------------------------------------------------
reply to cgi request, seding all related information generated by the script
--]]------------------------------------------------------------------------
function _cgi.done(terminate)
_cgi_send_headers()
if (_cgi.options.use_buffer) then
_cgi_send_content()
end
-- terminate program if 'terminate' is 'true'
if (terminate) then os.exit() end
end
--[[------------------------------------------------------------------------
global cgi table visible to the user holding the cgi access functions/values
--]]------------------------------------------------------------------------
cgi = _cgi