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