local html = require 'html'
local md5 = require 'md5'
local M = {}
M.env = os.getenv
local header_written
local function header(h, v)
header_written = true
assert(not h:match "[: \n]")
assert(h:lower() == h)
assert(not (tostring(v)):match "\n")
print(("%s: %s"):format(h, v))
end
local function serve(headers, body)
assert(not header_written)
if type(headers) == 'string' then
headers = {content_type = headers}
end
headers.etag = ('"%s"'):format(md5.sumhexa(body))
headers.content_length = #body
for h, v in pairs(headers) do
if type(v) == 'table' then
for _, v in ipairs(v) do
header(h:gsub("_", "-"), v)
end
else
header(h:gsub("_", "-"), v)
end
end
print()
io.stdout:write(body)
end
function M.abort(...)
coroutine.yield(M.abort, ...)
end
function M.redirect(...)
coroutine.yield(M.redirect, ...)
end
function M.decode_path(path)
local p = {}
for location in path:gmatch '([^/]+)' do
table.insert(p, html.url_decode(location))
end
return p
end
function M.parse_qs(str,sep)
sep = sep or '&'
local values = {}
for key, val in str:gmatch(('([^%q=]+)(=*[^%q=]*)'):format(sep, sep)) do
local key = html.url_decode(key)
local keys = {}
key = key:gsub('%[([^%]]*)%]', function(v)
-- extract keys between balanced brackets
if string.find(v, "^-?%d+$") then
v = tonumber(v)
else
v = html.url_decode(v)
end
table.insert(keys, v)
return "="
end)
key = key:gsub('=+.*$', "")
key = key:gsub('%s', "_") -- remove spaces in parameter name
val = val:gsub('^=+', "")
if not values[key] then
values[key] = {}
end
if #keys > 0 and type(values[key]) ~= 'table' then
values[key] = {}
elseif #keys == 0 and type(values[key]) == 'table' then
values[key] = html.url_decode(val)
end
local t = values[key]
for i,k in ipairs(keys) do
if type(t) ~= 'table' then
t = {}
end
if k == "" then
k = #t+1
end
if not t[k] then
t[k] = {}
end
if i == #keys then
t[k] = html.url_decode(val)
end
t = t[k]
end
end
return values
end
local redir_statuses = {
[301] = "301 Moved Permanently",
[301] = "302 Found",
[302] = "302 See Other",
[307] = "307 Temporary Redirect",
[308] = "308 Permanent Redirect",
}
local function redirect_page(code, url)
return {
content = 'text/html', status = redir_statuses[code] or code,
location = url
}, html.render_doc(function()
html.h1 "redirect"
html.p(function()
html.a({href = url}, url)
end)
end)
end
function M.serve(methods, error_page)
local co = coroutine.create(function()
local path = M.env 'PATH_INFO'
local query = M.parse_qs(M.env 'QUERY_STRING')
local form
local content_type = M.env 'CONTENT_TYPE'
if content_type == 'application/x-www-form-urlencoded' then
form = M.parse_qs(io.read 'a')
end
local cookie = {}
if M.env 'HTTP_COOKIE' then
cookie = M.parse_qs(M.env 'HTTP_COOKIE', '; ')
end
if methods[M.env 'REQUEST_METHOD'] then
local info = {query = query, form = form, cookie = cookie}
serve(methods[M.env 'REQUEST_METHOD'](info, path))
else
M.abort(405)
end
end)
local ok, err, code, arg = coroutine.resume(co)
if ok and coroutine.status(co) ~= "dead" then
if err == M.abort then
serve(error_page(code, arg))
elseif err == M.redirect then
serve(redirect_page(code, arg))
else
ok = false err = "invalid yield"
end
end
if not ok then
io.stderr:write('\n'..debug.traceback(co, err)..'\n')
serve(error_page(500))
end
end
return M