diff options
Diffstat (limited to 'cgi.lua')
-rw-r--r-- | cgi.lua | 162 |
1 files changed, 162 insertions, 0 deletions
@@ -0,0 +1,162 @@ +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 |