jbe/bsw@0: #!/usr/bin/env lua jbe/bsw@0: jbe/bsw@2: local assert = assert jbe/bsw@2: local collectgarbage = collectgarbage jbe/bsw@2: local error = error jbe/bsw@2: local getmetatable = getmetatable jbe/bsw@2: local ipairs = ipairs jbe/bsw@2: local next = next jbe/bsw@2: local pairs = pairs jbe/bsw@2: local print = print jbe/bsw@2: local rawequal = rawequal jbe/bsw@2: local rawget = rawget jbe@64: local rawlen = rawlen jbe/bsw@2: local rawset = rawset jbe/bsw@2: local select = select jbe/bsw@2: local setmetatable = setmetatable jbe/bsw@2: local tonumber = tonumber jbe/bsw@2: local tostring = tostring jbe/bsw@2: local type = type jbe/bsw@0: jbe/bsw@0: local io = io jbe/bsw@0: local math = math jbe/bsw@0: local os = os jbe/bsw@0: local string = string jbe/bsw@0: local table = table jbe/bsw@0: jbe@64: local _M = {} jbe@64: if _ENV then jbe@64: _ENV = _M jbe@64: else jbe@64: _G[...] = _M jbe@64: setfenv(1, _M) jbe@64: end jbe/bsw@0: jbe/bsw@0: data_sent = false jbe/bsw@0: jbe/bsw@16: --[[-- jbe/bsw@16: rocketcgi.add_header( jbe/bsw@16: string_part1, -- string jbe/bsw@16: string_part2, -- optional second part of string to be concatted jbe/bsw@16: ... jbe/bsw@16: ) jbe/bsw@16: jbe/bsw@16: Sends a header line to the browser. Multiple arguments are concatted to form a single string. jbe/bsw@16: jbe/bsw@16: --]]-- jbe/bsw@0: function add_header(...) jbe/bsw@0: if data_sent then jbe/bsw@0: error("Can not add header after data has been sent.", 2) jbe/bsw@0: end jbe/bsw@0: io.stdout:write(...) jbe/bsw@0: io.stdout:write("\r\n") jbe/bsw@0: end jbe/bsw@16: --//-- jbe/bsw@0: jbe/bsw@16: --[[-- jbe/bsw@16: rocketcgi.send_data( jbe/bsw@16: string_part1, -- string jbe/bsw@16: string_part2, -- optional second part of string to be concatted jbe/bsw@16: ... jbe/bsw@16: ) jbe/bsw@16: jbe/bsw@16: Sends document data to the browser. Multiple arguments are concatted to form a single string. jbe/bsw@16: jbe/bsw@16: --]]-- jbe/bsw@0: function send_data(...) jbe/bsw@0: if not data_sent then jbe/bsw@0: io.stdout:write("\r\n") jbe/bsw@0: data_sent = true jbe/bsw@0: end jbe/bsw@0: io.stdout:write(...) jbe/bsw@0: end jbe/bsw@16: --//-- jbe/bsw@0: jbe/bsw@16: --[[-- jbe/bsw@16: rocketcgi.set_status( jbe/bsw@16: status -- Status code and description, e.g. "404 Not Found" jbe/bsw@16: ) jbe/bsw@16: jbe/bsw@16: Sends a header line to the browser, indicating a given HTTP status. jbe/bsw@16: jbe/bsw@16: --]]-- jbe/bsw@0: function set_status(status) jbe/bsw@0: add_header("Status: ", status) jbe/bsw@0: end jbe/bsw@16: --//-- jbe/bsw@0: jbe/bsw@16: --[[-- jbe/bsw@16: rocketcgi.redirect( jbe/bsw@16: status -- Absolute URL to redirect the browser to jbe/bsw@16: ) jbe/bsw@16: jbe/bsw@16: Redirects the browser to the given absolute URL, using a 303 Redirect. jbe/bsw@16: jbe/bsw@16: --]]-- jbe/bsw@0: function redirect(location) jbe/bsw@0: set_status("303 See Other") jbe/bsw@0: add_header("Location: ", location) jbe/bsw@0: end jbe/bsw@16: --//-- jbe/bsw@0: jbe/bsw@16: --[[-- bsw@18: rocketcgi.set_content_type( bsw@18: content_type -- MIME content type jbe/bsw@16: ) jbe/bsw@16: jbe/bsw@16: Sends a header line specifying the content-type to the browser. jbe/bsw@16: jbe/bsw@16: --]]-- jbe/bsw@0: function set_content_type(content_type) jbe/bsw@0: add_header("Content-Type: ", content_type) jbe/bsw@0: end jbe/bsw@16: --//-- jbe/bsw@16: jbe/bsw@16: --[[-- jbe/bsw@16: rocketcgi.set_cookie{ jbe/bsw@16: name = name, -- name of cookie jbe/bsw@16: value = value, -- value of cookie jbe/bsw@16: domain = domain, -- domain where cookie is transmitted jbe/bsw@16: path = path, -- path where cookie is transmitted jbe/bsw@16: secure = secure -- boolean, indicating if cookie should only be transmitted over HTTPS jbe/bsw@16: } jbe/bsw@16: jbe/bsw@16: Sends a header line setting a cookie. NOTE: Currently only session cookies are supported. jbe/bsw@16: jbe/bsw@16: --]]-- jbe/bsw@16: function set_cookie(args) jbe/bsw@16: assert(string.find(args.name, "^[0-9A-Za-z%%._~-]+$"), "Illegal cookie name") jbe/bsw@16: assert(string.find(args.value, "^[0-9A-Za-z%%._~-]+$"), "Illegal cookie value") jbe/bsw@16: local parts = {"Set-Cookie: " .. args.name .. "=" .. args.value} jbe/bsw@16: if args.domain then jbe/bsw@16: assert( jbe/bsw@16: string.find(args.path, "^[0-9A-Za-z%%/._~-]+$"), jbe/bsw@16: "Illegal cookie domain" jbe/bsw@16: ) jbe/bsw@16: parts[#parts+1] = "domain=" .. args.domain jbe/bsw@16: end jbe/bsw@16: if args.path then jbe/bsw@16: assert( jbe/bsw@16: string.find(args.path, "^[0-9A-Za-z%%/._~-]+$"), jbe/bsw@16: "Illegal cookie path" jbe/bsw@16: ) jbe/bsw@16: parts[#parts+1] = "path=" .. args.path jbe/bsw@16: end jbe/bsw@16: if args.secure then jbe/bsw@16: parts[#parts+1] = "secure" jbe/bsw@16: end jbe/bsw@16: add_header(table.concat(parts, "; ")) jbe/bsw@16: end jbe/bsw@16: --//-- jbe/bsw@0: jbe/bsw@0: method = os.getenv("REQUEST_METHOD") or false jbe/bsw@0: query = os.getenv("QUERY_STRING") or false jbe/bsw@0: cookie_data = os.getenv("HTTP_COOKIE") or false jbe/bsw@0: post_data = io.stdin:read("*a") or false jbe/bsw@0: post_contenttype = os.getenv("CONTENT_TYPE") or false jbe/bsw@0: params = {} jbe/bsw@0: get_params = {} jbe/bsw@0: cookies = {} jbe/bsw@0: post_params = {} jbe/bsw@0: post_filenames = {} jbe/bsw@0: post_types = {} jbe/bsw@0: jbe/bsw@0: local urldecode jbe/bsw@0: do jbe/bsw@0: local b0 = string.byte("0") jbe/bsw@0: local b9 = string.byte("9") jbe/bsw@0: local bA = string.byte("A") jbe/bsw@0: local bF = string.byte("F") jbe/bsw@0: local ba = string.byte("a") jbe/bsw@0: local bf = string.byte("f") jbe/bsw@0: function urldecode(str) jbe/bsw@0: return ( jbe/bsw@0: string.gsub( jbe/bsw@0: string.gsub(str, "%+", " "), jbe/bsw@0: "%%([0-9A-Fa-f][0-9A-Fa-f])", jbe/bsw@0: function(hex) jbe/bsw@0: local n1, n2 = string.byte(hex, 1, 2) jbe/bsw@0: if n1 >= b0 and n1 <= b9 then n1 = n1 - b0 jbe/bsw@0: elseif n1 >= bA and n1 <= bF then n1 = n1 - bA + 10 jbe/bsw@0: elseif n1 >= ba and n1 <= bf then n1 = n1 - ba + 10 jbe/bsw@0: else return end jbe/bsw@0: if n2 >= b0 and n2 <= b9 then n2 = n2 - b0 jbe/bsw@0: elseif n2 >= bA and n2 <= bF then n2 = n2 - bA + 10 jbe/bsw@0: elseif n2 >= ba and n2 <= bf then n2 = n2 - ba + 10 jbe/bsw@0: else return end jbe/bsw@0: return string.char(n1 * 16 + n2) jbe/bsw@0: end jbe/bsw@0: ) jbe/bsw@0: ) jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: local function proc_param(tbl, key, value) jbe/bsw@0: if string.find(key, "%[%]$") then jbe/bsw@0: if tbl[key] then jbe/bsw@0: table.insert(tbl[key], value) jbe/bsw@0: else jbe/bsw@0: local list = { value } jbe/bsw@0: params[key] = list jbe/bsw@0: tbl[key] = list jbe/bsw@0: end jbe/bsw@0: else jbe/bsw@0: params[key] = value jbe/bsw@0: tbl[key] = value jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: local function read_urlencoded_form(tbl, data) jbe/bsw@0: for rawkey, rawvalue in string.gmatch(data, "([^=&]*)=([^=&]*)") do jbe/bsw@0: proc_param(tbl, urldecode(rawkey), urldecode(rawvalue)) jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: if query then jbe/bsw@0: read_urlencoded_form(get_params, query) jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: if cookie_data then jbe/bsw@0: for rawkey, rawvalue in string.gmatch(cookie_data, "([^=; ]*)=([^=; ]*)") do jbe/bsw@0: cookies[urldecode(rawkey)] = urldecode(rawvalue) jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: jbe/bsw@11: if post_contenttype and ( jbe/bsw@11: post_contenttype == "application/x-www-form-urlencoded" or jbe/bsw@11: string.match(post_contenttype, "^application/x%-www%-form%-urlencoded[ ;]") jbe/bsw@11: ) then jbe/bsw@0: read_urlencoded_form(post_params, post_data) jbe/bsw@0: elseif post_contenttype then jbe/bsw@0: local boundary = string.match( jbe/bsw@0: post_contenttype, jbe/bsw@0: '^multipart/form%-data[ \t]*;[ \t]*boundary="([^"]+)"' jbe/bsw@0: ) or string.match( jbe/bsw@0: post_contenttype, jbe/bsw@0: '^multipart/form%-data[ \t]*;[ \t]*boundary=([^"; \t]+)' jbe/bsw@0: ) jbe/bsw@0: if boundary then jbe/bsw@0: local parts = {} jbe/bsw@0: do jbe/bsw@0: local boundary = "\r\n--" .. boundary jbe/bsw@0: local post_data = "\r\n" .. post_data jbe/bsw@0: local pos1, pos2 = string.find(post_data, boundary, 1, true) jbe/bsw@0: while true do jbe/bsw@0: local ind = string.sub(post_data, pos2 + 1, pos2 + 2) jbe/bsw@0: if ind == "\r\n" then jbe/bsw@0: local pos3, pos4 = string.find(post_data, boundary, pos2 + 1, true) jbe/bsw@0: if pos3 then jbe/bsw@0: parts[#parts + 1] = string.sub(post_data, pos2 + 3, pos3 - 1) jbe/bsw@0: else jbe/bsw@0: error("Illegal POST data.") jbe/bsw@0: end jbe/bsw@0: pos1, pos2 = pos3, pos4 jbe/bsw@0: elseif ind == "--" then jbe/bsw@0: break jbe/bsw@0: else jbe/bsw@0: error("Illegal POST data.") jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: for i, part in ipairs(parts) do jbe/bsw@0: local pos = 1 jbe/bsw@0: local name, filename, contenttype jbe/bsw@0: while true do jbe/bsw@0: local header jbe/bsw@0: do jbe/bsw@0: local oldpos = pos jbe/bsw@0: pos = string.find(part, "\r\n", oldpos, true) jbe/bsw@0: if not pos then jbe/bsw@0: error("Illegal POST data.") jbe/bsw@0: end jbe/bsw@0: if pos == oldpos then break end jbe/bsw@0: header = string.sub(part, oldpos, pos - 1) jbe/bsw@0: pos = pos + 2 jbe/bsw@0: end jbe/bsw@0: if string.find( jbe/bsw@0: string.lower(header), jbe/bsw@0: "^content%-disposition:[ \t]*form%-data[ \t]*;" jbe/bsw@0: ) then jbe/bsw@0: -- TODO: handle all cases correctly jbe/bsw@0: name = string.match(header, ';[ \t]*name="([^"]*)"') or jbe/bsw@0: string.match(header, ';[ \t]*name=([^"; \t]+)') jbe/bsw@0: filename = string.match(header, ';[ \t]*filename="([^"]*)"') or jbe/bsw@0: string.match(header, ';[ \t]*filename=([^"; \t]+)') jbe/bsw@0: else jbe/bsw@0: local dummy, subpos = string.find( jbe/bsw@0: string.lower(header), jbe/bsw@0: "^content%-type:[ \t]*" jbe/bsw@0: ) jbe/bsw@0: if subpos then jbe/bsw@0: contenttype = string.sub(header, subpos + 1, #header) jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: local content = string.sub(part, pos + 2, #part) jbe/bsw@0: if not name then jbe/bsw@0: error("Illegal POST data.") jbe/bsw@0: end jbe/bsw@0: proc_param(post_params, name, content) jbe/bsw@0: post_filenames[name] = filename jbe/bsw@0: post_types[name] = contenttype jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: jbe/bsw@0: if post_data and #post_data > 262144 then jbe/bsw@0: post_data = nil jbe/bsw@0: collectgarbage("collect") jbe/bsw@0: else jbe/bsw@0: post_data = nil jbe/bsw@0: end jbe@64: jbe@64: return _M