| rev | line source | 
| jbe/bsw@0 | 1 #!/usr/bin/env lua | 
| jbe/bsw@0 | 2 | 
| jbe/bsw@2 | 3 local assert         = assert | 
| jbe/bsw@2 | 4 local collectgarbage = collectgarbage | 
| jbe/bsw@2 | 5 local error          = error | 
| jbe/bsw@2 | 6 local getfenv        = getfenv | 
| jbe/bsw@2 | 7 local getmetatable   = getmetatable | 
| jbe/bsw@2 | 8 local ipairs         = ipairs | 
| jbe/bsw@2 | 9 local next           = next | 
| jbe/bsw@2 | 10 local pairs          = pairs | 
| jbe/bsw@2 | 11 local pcall          = pcall | 
| jbe/bsw@2 | 12 local print          = print | 
| jbe/bsw@2 | 13 local rawequal       = rawequal | 
| jbe/bsw@2 | 14 local rawget         = rawget | 
| jbe/bsw@2 | 15 local rawset         = rawset | 
| jbe/bsw@2 | 16 local select         = select | 
| jbe/bsw@2 | 17 local setfenv        = setfenv | 
| jbe/bsw@2 | 18 local setmetatable   = setmetatable | 
| jbe/bsw@2 | 19 local tonumber       = tonumber | 
| jbe/bsw@2 | 20 local tostring       = tostring | 
| jbe/bsw@2 | 21 local type           = type | 
| jbe/bsw@2 | 22 local unpack         = unpack | 
| jbe/bsw@2 | 23 local xpcall         = xpcall | 
| jbe/bsw@0 | 24 | 
| jbe/bsw@0 | 25 local io     = io | 
| jbe/bsw@0 | 26 local math   = math | 
| jbe/bsw@0 | 27 local os     = os | 
| jbe/bsw@0 | 28 local string = string | 
| jbe/bsw@0 | 29 local table  = table | 
| jbe/bsw@0 | 30 | 
| jbe/bsw@0 | 31 module(...) | 
| jbe/bsw@0 | 32 | 
| jbe/bsw@0 | 33 data_sent = false | 
| jbe/bsw@0 | 34 | 
| jbe/bsw@16 | 35 --[[-- | 
| jbe/bsw@16 | 36 rocketcgi.add_header( | 
| jbe/bsw@16 | 37   string_part1,        -- string | 
| jbe/bsw@16 | 38   string_part2,        -- optional second part of string to be concatted | 
| jbe/bsw@16 | 39   ... | 
| jbe/bsw@16 | 40 ) | 
| jbe/bsw@16 | 41 | 
| jbe/bsw@16 | 42 Sends a header line to the browser. Multiple arguments are concatted to form a single string. | 
| jbe/bsw@16 | 43 | 
| jbe/bsw@16 | 44 --]]-- | 
| jbe/bsw@0 | 45 function add_header(...) | 
| jbe/bsw@0 | 46   if data_sent then | 
| jbe/bsw@0 | 47     error("Can not add header after data has been sent.", 2) | 
| jbe/bsw@0 | 48   end | 
| jbe/bsw@0 | 49   io.stdout:write(...) | 
| jbe/bsw@0 | 50   io.stdout:write("\r\n") | 
| jbe/bsw@0 | 51 end | 
| jbe/bsw@16 | 52 --//-- | 
| jbe/bsw@0 | 53 | 
| jbe/bsw@16 | 54 --[[-- | 
| jbe/bsw@16 | 55 rocketcgi.send_data( | 
| jbe/bsw@16 | 56   string_part1,       -- string | 
| jbe/bsw@16 | 57   string_part2,       -- optional second part of string to be concatted | 
| jbe/bsw@16 | 58   ... | 
| jbe/bsw@16 | 59 ) | 
| jbe/bsw@16 | 60 | 
| jbe/bsw@16 | 61 Sends document data to the browser. Multiple arguments are concatted to form a single string. | 
| jbe/bsw@16 | 62 | 
| jbe/bsw@16 | 63 --]]-- | 
| jbe/bsw@0 | 64 function send_data(...) | 
| jbe/bsw@0 | 65   if not data_sent then | 
| jbe/bsw@0 | 66     io.stdout:write("\r\n") | 
| jbe/bsw@0 | 67     data_sent = true | 
| jbe/bsw@0 | 68   end | 
| jbe/bsw@0 | 69   io.stdout:write(...) | 
| jbe/bsw@0 | 70 end | 
| jbe/bsw@16 | 71 --//-- | 
| jbe/bsw@0 | 72 | 
| jbe/bsw@16 | 73 --[[-- | 
| jbe/bsw@16 | 74 rocketcgi.set_status( | 
| jbe/bsw@16 | 75   status               -- Status code and description, e.g. "404 Not Found" | 
| jbe/bsw@16 | 76 ) | 
| jbe/bsw@16 | 77 | 
| jbe/bsw@16 | 78 Sends a header line to the browser, indicating a given HTTP status. | 
| jbe/bsw@16 | 79 | 
| jbe/bsw@16 | 80 --]]-- | 
| jbe/bsw@0 | 81 function set_status(status) | 
| jbe/bsw@0 | 82   add_header("Status: ", status) | 
| jbe/bsw@0 | 83 end | 
| jbe/bsw@16 | 84 --//-- | 
| jbe/bsw@0 | 85 | 
| jbe/bsw@16 | 86 --[[-- | 
| jbe/bsw@16 | 87 rocketcgi.redirect( | 
| jbe/bsw@16 | 88   status             -- Absolute URL to redirect the browser to | 
| jbe/bsw@16 | 89 ) | 
| jbe/bsw@16 | 90 | 
| jbe/bsw@16 | 91 Redirects the browser to the given absolute URL, using a 303 Redirect. | 
| jbe/bsw@16 | 92 | 
| jbe/bsw@16 | 93 --]]-- | 
| jbe/bsw@0 | 94 function redirect(location) | 
| jbe/bsw@0 | 95   set_status("303 See Other") | 
| jbe/bsw@0 | 96   add_header("Location: ", location) | 
| jbe/bsw@0 | 97 end | 
| jbe/bsw@16 | 98 --//-- | 
| jbe/bsw@0 | 99 | 
| jbe/bsw@16 | 100 --[[-- | 
| bsw@18 | 101 rocketcgi.set_content_type( | 
| bsw@18 | 102   content_type               -- MIME content type | 
| jbe/bsw@16 | 103 ) | 
| jbe/bsw@16 | 104 | 
| jbe/bsw@16 | 105 Sends a header line specifying the content-type to the browser. | 
| jbe/bsw@16 | 106 | 
| jbe/bsw@16 | 107 --]]-- | 
| jbe/bsw@0 | 108 function set_content_type(content_type) | 
| jbe/bsw@0 | 109   add_header("Content-Type: ", content_type) | 
| jbe/bsw@0 | 110 end | 
| jbe/bsw@16 | 111 --//-- | 
| jbe/bsw@16 | 112 | 
| jbe/bsw@16 | 113 --[[-- | 
| jbe/bsw@16 | 114 rocketcgi.set_cookie{ | 
| jbe/bsw@16 | 115   name   = name,       -- name of cookie | 
| jbe/bsw@16 | 116   value  = value,      -- value of cookie | 
| jbe/bsw@16 | 117   domain = domain,     -- domain where cookie is transmitted | 
| jbe/bsw@16 | 118   path   = path,       -- path where cookie is transmitted | 
| jbe/bsw@16 | 119   secure = secure      -- boolean, indicating if cookie should only be transmitted over HTTPS | 
| jbe/bsw@16 | 120 } | 
| jbe/bsw@16 | 121 | 
| jbe/bsw@16 | 122 Sends a header line setting a cookie. NOTE: Currently only session cookies are supported. | 
| jbe/bsw@16 | 123 | 
| jbe/bsw@16 | 124 --]]-- | 
| jbe/bsw@16 | 125 function set_cookie(args) | 
| jbe/bsw@16 | 126   assert(string.find(args.name, "^[0-9A-Za-z%%._~-]+$"), "Illegal cookie name") | 
| jbe/bsw@16 | 127   assert(string.find(args.value, "^[0-9A-Za-z%%._~-]+$"), "Illegal cookie value") | 
| jbe/bsw@16 | 128   local parts = {"Set-Cookie: " .. args.name .. "=" .. args.value} | 
| jbe/bsw@16 | 129   if args.domain then | 
| jbe/bsw@16 | 130     assert( | 
| jbe/bsw@16 | 131       string.find(args.path, "^[0-9A-Za-z%%/._~-]+$"), | 
| jbe/bsw@16 | 132       "Illegal cookie domain" | 
| jbe/bsw@16 | 133     ) | 
| jbe/bsw@16 | 134     parts[#parts+1] = "domain=" .. args.domain | 
| jbe/bsw@16 | 135   end | 
| jbe/bsw@16 | 136   if args.path then | 
| jbe/bsw@16 | 137     assert( | 
| jbe/bsw@16 | 138       string.find(args.path, "^[0-9A-Za-z%%/._~-]+$"), | 
| jbe/bsw@16 | 139       "Illegal cookie path" | 
| jbe/bsw@16 | 140     ) | 
| jbe/bsw@16 | 141     parts[#parts+1] = "path=" .. args.path | 
| jbe/bsw@16 | 142   end | 
| jbe/bsw@16 | 143   if args.secure then | 
| jbe/bsw@16 | 144     parts[#parts+1] = "secure" | 
| jbe/bsw@16 | 145   end | 
| jbe/bsw@16 | 146   add_header(table.concat(parts, "; ")) | 
| jbe/bsw@16 | 147 end | 
| jbe/bsw@16 | 148 --//-- | 
| jbe/bsw@0 | 149 | 
| jbe/bsw@0 | 150 method = os.getenv("REQUEST_METHOD") or false | 
| jbe/bsw@0 | 151 query = os.getenv("QUERY_STRING") or false | 
| jbe/bsw@0 | 152 cookie_data = os.getenv("HTTP_COOKIE") or false | 
| jbe/bsw@0 | 153 post_data = io.stdin:read("*a") or false | 
| jbe/bsw@0 | 154 post_contenttype = os.getenv("CONTENT_TYPE") or false | 
| jbe/bsw@0 | 155 params = {} | 
| jbe/bsw@0 | 156 get_params = {} | 
| jbe/bsw@0 | 157 cookies = {} | 
| jbe/bsw@0 | 158 post_params = {} | 
| jbe/bsw@0 | 159 post_filenames = {} | 
| jbe/bsw@0 | 160 post_types = {} | 
| jbe/bsw@0 | 161 | 
| jbe/bsw@0 | 162 local urldecode | 
| jbe/bsw@0 | 163 do | 
| jbe/bsw@0 | 164   local b0 = string.byte("0") | 
| jbe/bsw@0 | 165   local b9 = string.byte("9") | 
| jbe/bsw@0 | 166   local bA = string.byte("A") | 
| jbe/bsw@0 | 167   local bF = string.byte("F") | 
| jbe/bsw@0 | 168   local ba = string.byte("a") | 
| jbe/bsw@0 | 169   local bf = string.byte("f") | 
| jbe/bsw@0 | 170   function urldecode(str) | 
| jbe/bsw@0 | 171     return ( | 
| jbe/bsw@0 | 172       string.gsub( | 
| jbe/bsw@0 | 173         string.gsub(str, "%+", " "), | 
| jbe/bsw@0 | 174         "%%([0-9A-Fa-f][0-9A-Fa-f])", | 
| jbe/bsw@0 | 175         function(hex) | 
| jbe/bsw@0 | 176           local n1, n2 = string.byte(hex, 1, 2) | 
| jbe/bsw@0 | 177           if n1 >= b0 and n1 <= b9 then n1 = n1 - b0 | 
| jbe/bsw@0 | 178           elseif n1 >= bA and n1 <= bF then n1 = n1 - bA + 10 | 
| jbe/bsw@0 | 179           elseif n1 >= ba and n1 <= bf then n1 = n1 - ba + 10 | 
| jbe/bsw@0 | 180           else return end | 
| jbe/bsw@0 | 181           if n2 >= b0 and n2 <= b9 then n2 = n2 - b0 | 
| jbe/bsw@0 | 182           elseif n2 >= bA and n2 <= bF then n2 = n2 - bA + 10 | 
| jbe/bsw@0 | 183           elseif n2 >= ba and n2 <= bf then n2 = n2 - ba + 10 | 
| jbe/bsw@0 | 184           else return end | 
| jbe/bsw@0 | 185           return string.char(n1 * 16 + n2) | 
| jbe/bsw@0 | 186         end | 
| jbe/bsw@0 | 187       ) | 
| jbe/bsw@0 | 188     ) | 
| jbe/bsw@0 | 189   end | 
| jbe/bsw@0 | 190 end | 
| jbe/bsw@0 | 191 | 
| jbe/bsw@0 | 192 local function proc_param(tbl, key, value) | 
| jbe/bsw@0 | 193   if string.find(key, "%[%]$") then | 
| jbe/bsw@0 | 194     if tbl[key] then | 
| jbe/bsw@0 | 195       table.insert(tbl[key], value) | 
| jbe/bsw@0 | 196     else | 
| jbe/bsw@0 | 197       local list = { value } | 
| jbe/bsw@0 | 198       params[key] = list | 
| jbe/bsw@0 | 199       tbl[key] = list | 
| jbe/bsw@0 | 200     end | 
| jbe/bsw@0 | 201   else | 
| jbe/bsw@0 | 202     params[key] = value | 
| jbe/bsw@0 | 203     tbl[key] = value | 
| jbe/bsw@0 | 204   end | 
| jbe/bsw@0 | 205 end | 
| jbe/bsw@0 | 206 | 
| jbe/bsw@0 | 207 local function read_urlencoded_form(tbl, data) | 
| jbe/bsw@0 | 208   for rawkey, rawvalue in string.gmatch(data, "([^=&]*)=([^=&]*)") do | 
| jbe/bsw@0 | 209     proc_param(tbl, urldecode(rawkey), urldecode(rawvalue)) | 
| jbe/bsw@0 | 210   end | 
| jbe/bsw@0 | 211 end | 
| jbe/bsw@0 | 212 | 
| jbe/bsw@0 | 213 if query then | 
| jbe/bsw@0 | 214   read_urlencoded_form(get_params, query) | 
| jbe/bsw@0 | 215 end | 
| jbe/bsw@0 | 216 | 
| jbe/bsw@0 | 217 if cookie_data then | 
| jbe/bsw@0 | 218   for rawkey, rawvalue in string.gmatch(cookie_data, "([^=; ]*)=([^=; ]*)") do | 
| jbe/bsw@0 | 219     cookies[urldecode(rawkey)] = urldecode(rawvalue) | 
| jbe/bsw@0 | 220   end | 
| jbe/bsw@0 | 221 end | 
| jbe/bsw@0 | 222 | 
| jbe/bsw@11 | 223 if post_contenttype and ( | 
| jbe/bsw@11 | 224   post_contenttype == "application/x-www-form-urlencoded" or | 
| jbe/bsw@11 | 225   string.match(post_contenttype, "^application/x%-www%-form%-urlencoded[ ;]") | 
| jbe/bsw@11 | 226 ) then | 
| jbe/bsw@0 | 227   read_urlencoded_form(post_params, post_data) | 
| jbe/bsw@0 | 228 elseif post_contenttype then | 
| jbe/bsw@0 | 229   local boundary = string.match( | 
| jbe/bsw@0 | 230     post_contenttype, | 
| jbe/bsw@0 | 231     '^multipart/form%-data[ \t]*;[ \t]*boundary="([^"]+)"' | 
| jbe/bsw@0 | 232   ) or string.match( | 
| jbe/bsw@0 | 233       post_contenttype, | 
| jbe/bsw@0 | 234       '^multipart/form%-data[ \t]*;[ \t]*boundary=([^"; \t]+)' | 
| jbe/bsw@0 | 235   ) | 
| jbe/bsw@0 | 236   if boundary then | 
| jbe/bsw@0 | 237     local parts = {} | 
| jbe/bsw@0 | 238     do | 
| jbe/bsw@0 | 239       local boundary = "\r\n--" .. boundary | 
| jbe/bsw@0 | 240       local post_data = "\r\n" .. post_data | 
| jbe/bsw@0 | 241       local pos1, pos2 = string.find(post_data, boundary, 1, true) | 
| jbe/bsw@0 | 242       while true do | 
| jbe/bsw@0 | 243         local ind = string.sub(post_data, pos2 + 1, pos2 + 2) | 
| jbe/bsw@0 | 244         if ind == "\r\n" then | 
| jbe/bsw@0 | 245           local pos3, pos4 = string.find(post_data, boundary, pos2 + 1, true) | 
| jbe/bsw@0 | 246           if pos3 then | 
| jbe/bsw@0 | 247             parts[#parts + 1] = string.sub(post_data, pos2 + 3, pos3 - 1) | 
| jbe/bsw@0 | 248           else | 
| jbe/bsw@0 | 249             error("Illegal POST data.") | 
| jbe/bsw@0 | 250           end | 
| jbe/bsw@0 | 251           pos1, pos2 = pos3, pos4 | 
| jbe/bsw@0 | 252         elseif ind == "--" then | 
| jbe/bsw@0 | 253           break | 
| jbe/bsw@0 | 254         else | 
| jbe/bsw@0 | 255           error("Illegal POST data.") | 
| jbe/bsw@0 | 256         end | 
| jbe/bsw@0 | 257       end | 
| jbe/bsw@0 | 258     end | 
| jbe/bsw@0 | 259     for i, part in ipairs(parts) do | 
| jbe/bsw@0 | 260       local pos = 1 | 
| jbe/bsw@0 | 261       local name, filename, contenttype | 
| jbe/bsw@0 | 262       while true do | 
| jbe/bsw@0 | 263         local header | 
| jbe/bsw@0 | 264         do | 
| jbe/bsw@0 | 265           local oldpos = pos | 
| jbe/bsw@0 | 266           pos = string.find(part, "\r\n", oldpos, true) | 
| jbe/bsw@0 | 267           if not pos then | 
| jbe/bsw@0 | 268             error("Illegal POST data.") | 
| jbe/bsw@0 | 269           end | 
| jbe/bsw@0 | 270           if pos == oldpos then break end | 
| jbe/bsw@0 | 271           header = string.sub(part, oldpos, pos - 1) | 
| jbe/bsw@0 | 272           pos = pos + 2 | 
| jbe/bsw@0 | 273         end | 
| jbe/bsw@0 | 274         if string.find( | 
| jbe/bsw@0 | 275           string.lower(header), | 
| jbe/bsw@0 | 276           "^content%-disposition:[ \t]*form%-data[ \t]*;" | 
| jbe/bsw@0 | 277         ) then | 
| jbe/bsw@0 | 278           -- TODO: handle all cases correctly | 
| jbe/bsw@0 | 279           name = string.match(header, ';[ \t]*name="([^"]*)"') or | 
| jbe/bsw@0 | 280             string.match(header, ';[ \t]*name=([^"; \t]+)') | 
| jbe/bsw@0 | 281           filename = string.match(header, ';[ \t]*filename="([^"]*)"') or | 
| jbe/bsw@0 | 282             string.match(header, ';[ \t]*filename=([^"; \t]+)') | 
| jbe/bsw@0 | 283         else | 
| jbe/bsw@0 | 284           local dummy, subpos = string.find( | 
| jbe/bsw@0 | 285             string.lower(header), | 
| jbe/bsw@0 | 286             "^content%-type:[ \t]*" | 
| jbe/bsw@0 | 287           ) | 
| jbe/bsw@0 | 288           if subpos then | 
| jbe/bsw@0 | 289             contenttype = string.sub(header, subpos + 1, #header) | 
| jbe/bsw@0 | 290           end | 
| jbe/bsw@0 | 291         end | 
| jbe/bsw@0 | 292       end | 
| jbe/bsw@0 | 293       local content = string.sub(part, pos + 2, #part) | 
| jbe/bsw@0 | 294       if not name then | 
| jbe/bsw@0 | 295         error("Illegal POST data.") | 
| jbe/bsw@0 | 296       end | 
| jbe/bsw@0 | 297       proc_param(post_params, name, content) | 
| jbe/bsw@0 | 298       post_filenames[name] = filename | 
| jbe/bsw@0 | 299       post_types[name] = contenttype | 
| jbe/bsw@0 | 300     end | 
| jbe/bsw@0 | 301   end | 
| jbe/bsw@0 | 302 end | 
| jbe/bsw@0 | 303 | 
| jbe/bsw@0 | 304 if post_data and #post_data > 262144 then | 
| jbe/bsw@0 | 305   post_data = nil | 
| jbe/bsw@0 | 306   collectgarbage("collect") | 
| jbe/bsw@0 | 307 else | 
| jbe/bsw@0 | 308   post_data = nil | 
| jbe/bsw@0 | 309 end |