| rev | line source | 
| jbe@0 | 1 #!/usr/bin/env lua | 
| jbe@0 | 2 | 
| jbe@0 | 3 -- module preamble | 
| jbe@0 | 4 local _G, _M = _ENV, {} | 
| jbe@0 | 5 _ENV = setmetatable({}, { | 
| jbe@0 | 6   __index = function(self, key) | 
| jbe@0 | 7     local value = _M[key]; if value ~= nil then return value end | 
| jbe@0 | 8     return _G[key] | 
| jbe@0 | 9   end, | 
| jbe@63 | 10   __newindex = _M | 
| jbe@0 | 11 }) | 
| jbe@0 | 12 | 
| jbe@0 | 13 -- function that encodes certain HTML entities: | 
| jbe@0 | 14 -- (not used by the library itself) | 
| jbe@0 | 15 function encode_html(text) | 
| jbe@0 | 16   return ( | 
| jbe@0 | 17     string.gsub( | 
| jbe@0 | 18       text, '[<>&"]', | 
| jbe@0 | 19       function(char) | 
| jbe@0 | 20         if char == '<' then | 
| jbe@0 | 21           return "<" | 
| jbe@0 | 22         elseif char == '>' then | 
| jbe@0 | 23           return ">" | 
| jbe@0 | 24         elseif char == '&' then | 
| jbe@0 | 25           return "&" | 
| jbe@0 | 26         elseif char == '"' then | 
| jbe@0 | 27           return """ | 
| jbe@0 | 28         end | 
| jbe@0 | 29       end | 
| jbe@0 | 30     ) | 
| jbe@0 | 31   ) | 
| jbe@0 | 32 | 
| jbe@0 | 33 end | 
| jbe@0 | 34 | 
| jbe@0 | 35 -- function that encodes special characters for URIs: | 
| jbe@0 | 36 -- (not used by the library itself) | 
| jbe@0 | 37 function encode_uri(text) | 
| jbe@0 | 38   return ( | 
| jbe@0 | 39     string.gsub(text, "[^0-9A-Za-z_%.~-]", | 
| jbe@0 | 40       function (char) | 
| jbe@0 | 41         return string.format("%%%02x", string.byte(char)) | 
| jbe@0 | 42       end | 
| jbe@0 | 43     ) | 
| jbe@0 | 44   ) | 
| jbe@0 | 45 end | 
| jbe@0 | 46 | 
| jbe@0 | 47 -- function undoing URL encoding: | 
| jbe@0 | 48 do | 
| jbe@0 | 49   local b0 = string.byte("0") | 
| jbe@0 | 50   local b9 = string.byte("9") | 
| jbe@0 | 51   local bA = string.byte("A") | 
| jbe@0 | 52   local bF = string.byte("F") | 
| jbe@0 | 53   local ba = string.byte("a") | 
| jbe@0 | 54   local bf = string.byte("f") | 
| jbe@0 | 55   function decode_uri(str) | 
| jbe@0 | 56     return ( | 
| jbe@0 | 57       string.gsub( | 
| jbe@0 | 58         string.gsub(str, "%+", " "), | 
| jbe@0 | 59         "%%([0-9A-Fa-f][0-9A-Fa-f])", | 
| jbe@0 | 60         function(hex) | 
| jbe@0 | 61           local n1, n2 = string.byte(hex, 1, 2) | 
| jbe@0 | 62           if n1 >= b0 and n1 <= b9 then n1 = n1 - b0 | 
| jbe@0 | 63           elseif n1 >= bA and n1 <= bF then n1 = n1 - bA + 10 | 
| jbe@0 | 64           elseif n1 >= ba and n1 <= bf then n1 = n1 - ba + 10 | 
| jbe@0 | 65           else error("Assertion failed") end | 
| jbe@0 | 66           if n2 >= b0 and n2 <= b9 then n2 = n2 - b0 | 
| jbe@0 | 67           elseif n2 >= bA and n2 <= bF then n2 = n2 - bA + 10 | 
| jbe@0 | 68           elseif n2 >= ba and n2 <= bf then n2 = n2 - ba + 10 | 
| jbe@0 | 69           else error("Assertion failed") end | 
| jbe@0 | 70           return string.char(n1 * 16 + n2) | 
| jbe@0 | 71         end | 
| jbe@0 | 72       ) | 
| jbe@0 | 73     ) | 
| jbe@0 | 74   end | 
| jbe@0 | 75 end | 
| jbe@0 | 76 | 
| jbe@0 | 77 -- status codes that carry no response body (in addition to 1xx): | 
| jbe@0 | 78 -- (set to "zero_content_length" if Content-Length header is required) | 
| jbe@0 | 79 status_without_response_body = { | 
| jbe@5 | 80   ["101"] = true,  -- list 101 to allow protocol switch | 
| jbe@0 | 81   ["204"] = true, | 
| jbe@0 | 82   ["205"] = "zero_content_length", | 
| jbe@0 | 83   ["304"] = true | 
| jbe@0 | 84 } | 
| jbe@0 | 85 | 
| jbe@167 | 86 -- handling of GET/POST param tables: | 
| jbe@167 | 87 local new_params_list  -- defined later | 
| jbe@167 | 88 do | 
| jbe@167 | 89   local params_list_mapping = setmetatable({}, {__mode="k"}) | 
| jbe@167 | 90   local function nextnonempty(tbl, key) | 
| jbe@167 | 91     while true do | 
| jbe@167 | 92       key = next(tbl, key) | 
| jbe@167 | 93       if key == nil then | 
| jbe@167 | 94         return nil | 
| jbe@167 | 95       end | 
| jbe@167 | 96       local value = tbl[key] | 
| jbe@167 | 97       if #value > 0 then | 
| jbe@167 | 98         return key, value | 
| jbe@167 | 99       end | 
| jbe@35 | 100     end | 
| jbe@35 | 101   end | 
| jbe@167 | 102   local function nextvalue(tbl, key) | 
| jbe@167 | 103     key = next(tbl, key) | 
| jbe@167 | 104     if key == nil then | 
| jbe@167 | 105       return nil | 
| jbe@167 | 106     end | 
| jbe@167 | 107     return key, tbl[key][1] | 
| jbe@167 | 108   end | 
| jbe@167 | 109   local params_list_metatable = { | 
| jbe@167 | 110     __index = function(self, key) | 
| jbe@167 | 111       local tbl = {} | 
| jbe@167 | 112       self[key] = tbl | 
| jbe@167 | 113       return tbl | 
| jbe@167 | 114     end, | 
| jbe@167 | 115     __pairs = function(self) | 
| jbe@167 | 116       return nextnonempty, self, nil | 
| jbe@167 | 117     end | 
| jbe@167 | 118   } | 
| jbe@167 | 119   local params_metatable = { | 
| jbe@167 | 120     __index = function(self, key) | 
| jbe@167 | 121       return params_list_mapping[self][key][1] | 
| jbe@167 | 122     end, | 
| jbe@167 | 123     __newindex = function(self, key, value) | 
| jbe@167 | 124       params_list_mapping[self][key] = {value} | 
| jbe@167 | 125     end, | 
| jbe@167 | 126     __pairs = function(self) | 
| jbe@167 | 127       return nextvalue, params_list_mapping[self], nil | 
| jbe@167 | 128     end | 
| jbe@167 | 129   } | 
| jbe@167 | 130   -- returns a table to store key value-list pairs (i.e. multiple values), | 
| jbe@167 | 131   -- and a second table automatically mapping keys to the first value | 
| jbe@167 | 132   -- using the key value-list pairs in the first table: | 
| jbe@167 | 133   new_params_list = function() | 
| jbe@167 | 134     local params_list = setmetatable({}, params_list_metatable) | 
| jbe@167 | 135     local params = setmetatable({}, params_metatable) | 
| jbe@167 | 136     params_list_mapping[params] = params_list | 
| jbe@167 | 137     return params_list, params | 
| jbe@167 | 138   end | 
| jbe@167 | 139 end | 
| jbe@167 | 140 | 
| jbe@167 | 141 -- parses URL encoded form data and stores it in | 
| jbe@167 | 142 -- a key value-list pairs structure that has to be | 
| jbe@167 | 143 -- previously obtained by calling by new_params_list(): | 
| jbe@167 | 144 local function read_urlencoded_form(tbl, data) | 
| jbe@167 | 145   for rawkey, rawvalue in string.gmatch(data, "([^?=&]*)=([^?=&]*)") do | 
| jbe@167 | 146     local subtbl = tbl[decode_uri(rawkey)] | 
| jbe@167 | 147     subtbl[#subtbl+1] = decode_uri(rawvalue) | 
| jbe@167 | 148   end | 
| jbe@0 | 149 end | 
| jbe@0 | 150 | 
| jbe@154 | 151 -- extracts first value from each subtable: | 
| jbe@154 | 152 local function get_first_values(tbl) | 
| jbe@154 | 153   local newtbl = {} | 
| jbe@154 | 154   for key, subtbl in pairs(tbl) do | 
| jbe@154 | 155     newtbl[key] = subtbl[1] | 
| jbe@0 | 156   end | 
| jbe@154 | 157   return newtbl | 
| jbe@154 | 158 end | 
| jbe@154 | 159 | 
| jbe@0 | 160 function generate_handler(handler, options) | 
| jbe@0 | 161   -- swap arguments if necessary (for convenience): | 
| jbe@0 | 162   if type(handler) ~= "function" and type(options) == "function" then | 
| jbe@0 | 163     handler, options = options, handler | 
| jbe@0 | 164   end | 
| jbe@160 | 165   -- helper function to process options: | 
| jbe@160 | 166   local function default(name, default_value) | 
| jbe@160 | 167     local value = options[name] | 
| jbe@160 | 168     if value == nil then | 
| jbe@160 | 169       return default_value | 
| jbe@160 | 170     else | 
| jbe@160 | 171       return value or nil | 
| jbe@159 | 172     end | 
| jbe@160 | 173   end | 
| jbe@0 | 174   -- process options: | 
| jbe@0 | 175   options = options or {} | 
| jbe@0 | 176   local preamble = ""  -- preamble sent with every(!) HTTP response | 
| jbe@0 | 177   do | 
| jbe@0 | 178     -- named arg "static_headers" is used to create the preamble: | 
| jbe@0 | 179     local s = options.static_headers | 
| jbe@0 | 180     local t = {} | 
| jbe@0 | 181     if s then | 
| jbe@0 | 182       if type(s) == "string" then | 
| jbe@0 | 183         for line in string.gmatch(s, "[^\r\n]+") do | 
| jbe@0 | 184           t[#t+1] = line | 
| jbe@0 | 185         end | 
| jbe@0 | 186       else | 
| jbe@0 | 187         for i, kv in ipairs(options.static_headers) do | 
| jbe@0 | 188           if type(kv) == "string" then | 
| jbe@0 | 189             t[#t+1] = kv | 
| jbe@0 | 190           else | 
| jbe@0 | 191             t[#t+1] = kv[1] .. ": " .. kv[2] | 
| jbe@0 | 192           end | 
| jbe@0 | 193         end | 
| jbe@0 | 194       end | 
| jbe@0 | 195     end | 
| jbe@0 | 196     t[#t+1] = "" | 
| jbe@0 | 197     preamble = table.concat(t, "\r\n") | 
| jbe@0 | 198   end | 
| jbe@160 | 199   local input_chunk_size  = options.maximum_input_chunk_size  or options.chunk_size or 16384 | 
| jbe@44 | 200   local output_chunk_size = options.minimum_output_chunk_size or options.chunk_size or 1024 | 
| jbe@160 | 201   local header_size_limit = options.header_size_limit or 1024*1024 | 
| jbe@160 | 202   local body_size_limit   = options.body_size_limit   or 64*1024*1024 | 
| jbe@160 | 203   local request_idle_timeout     = default("request_idle_timeout", 330) | 
| jbe@160 | 204   local request_header_timeout   = default("request_header_timeout", 30) | 
| jbe@160 | 205   local request_body_timeout     = default("request_body_timeout", 60) | 
| jbe@160 | 206   local request_response_timeout = default("request_response_timeout", 1800) | 
| jbe@160 | 207   local poll = options.poll_function or moonbridge_io.poll | 
| jbe@160 | 208   -- return socket handler: | 
| jbe@0 | 209   return function(socket) | 
| jbe@160 | 210     local socket_set = {[socket] = true}  -- used for poll function | 
| jbe@0 | 211     local survive = true  -- set to false if process shall be terminated later | 
| jbe@160 | 212     local consume  -- function that reads some input if possible | 
| jbe@160 | 213     -- function that drains some input if possible: | 
| jbe@160 | 214     local function drain() | 
| jbe@163 | 215       local bytes, status = socket:drain_nb(input_chunk_size) | 
| jbe@163 | 216       if not bytes or status == "eof" then | 
| jbe@160 | 217         consume = nil | 
| jbe@50 | 218       end | 
| jbe@159 | 219     end | 
| jbe@163 | 220     -- function trying to unblock socket by reading: | 
| jbe@160 | 221     local function unblock() | 
| jbe@160 | 222       if consume then | 
| jbe@160 | 223         poll(socket_set, socket_set) | 
| jbe@160 | 224         consume() | 
| jbe@160 | 225       else | 
| jbe@160 | 226         poll(nil, socket_set) | 
| jbe@0 | 227       end | 
| jbe@154 | 228     end | 
| jbe@163 | 229     -- function that enforces consumption of all input: | 
| jbe@162 | 230     local function consume_all() | 
| jbe@162 | 231       while consume do | 
| jbe@163 | 232         poll(socket_set, nil) | 
| jbe@162 | 233         consume() | 
| jbe@162 | 234       end | 
| jbe@162 | 235     end | 
| jbe@163 | 236     -- handle requests in a loop: | 
| jbe@160 | 237     repeat | 
| jbe@168 | 238       -- copy limits: | 
| jbe@168 | 239       local remaining_header_size_limit = header_size_limit | 
| jbe@168 | 240       local remaining_body_size_limit = body_size_limit | 
| jbe@166 | 241       -- table for caching nil values: | 
| jbe@166 | 242       local headers_value_nil = {} | 
| jbe@172 | 243       -- create a new request object with metatable: | 
| jbe@166 | 244       local request  -- allow references to local variable | 
| jbe@166 | 245       request = { | 
| jbe@165 | 246         -- allow access to underlying socket: | 
| jbe@0 | 247         socket = socket, | 
| jbe@165 | 248         -- cookies are simply stored in a table: | 
| jbe@165 | 249         cookies = {}, | 
| jbe@165 | 250         -- table mapping header field names to value-lists | 
| jbe@165 | 251         -- (raw access, but case-insensitive): | 
| jbe@165 | 252         headers = setmetatable({}, { | 
| jbe@165 | 253           __index = function(self, key) | 
| jbe@165 | 254             local lowerkey = string.lower(key) | 
| jbe@165 | 255             if lowerkey == key then | 
| jbe@165 | 256               return | 
| jbe@165 | 257             end | 
| jbe@165 | 258             local result = rawget(self, lowerkey) | 
| jbe@165 | 259             if result == nil then | 
| jbe@165 | 260               result = {} | 
| jbe@165 | 261             end | 
| jbe@165 | 262             self[lowerkey] = result | 
| jbe@165 | 263             self[key] = result | 
| jbe@165 | 264             return result | 
| jbe@165 | 265           end | 
| jbe@165 | 266         }), | 
| jbe@165 | 267         -- table mapping header field names to value-lists | 
| jbe@165 | 268         -- (for headers with comma separated values): | 
| jbe@165 | 269         headers_csv_table = setmetatable({}, { | 
| jbe@165 | 270           __index = function(self, key) | 
| jbe@165 | 271             local result = {} | 
| jbe@165 | 272             for i, line in ipairs(request.headers[key]) do | 
| jbe@165 | 273               for entry in string.gmatch(line, "[^,]+") do | 
| jbe@165 | 274                 local value = string.match(entry, "^[ \t]*(..-)[ \t]*$") | 
| jbe@165 | 275                 if value then | 
| jbe@165 | 276                   result[#result+1] = value | 
| jbe@165 | 277                 end | 
| jbe@165 | 278               end | 
| jbe@165 | 279             end | 
| jbe@165 | 280             self[key] = result | 
| jbe@165 | 281             return result | 
| jbe@165 | 282           end | 
| jbe@165 | 283         }), | 
| jbe@165 | 284         -- table mapping header field names to a comma separated string | 
| jbe@165 | 285         -- (for headers with comma separated values): | 
| jbe@165 | 286         headers_csv_string = setmetatable({}, { | 
| jbe@165 | 287           __index = function(self, key) | 
| jbe@165 | 288             local result = {} | 
| jbe@165 | 289             for i, line in ipairs(request.headers[key]) do | 
| jbe@165 | 290               result[#result+1] = line | 
| jbe@165 | 291             end | 
| jbe@172 | 292             result = table.concat(result, ", ") | 
| jbe@165 | 293             self[key] = result | 
| jbe@165 | 294             return result | 
| jbe@165 | 295           end | 
| jbe@165 | 296         }), | 
| jbe@165 | 297         -- table mapping header field names to a single string value | 
| jbe@165 | 298         -- (or false if header has been sent multiple times): | 
| jbe@165 | 299         headers_value = setmetatable({}, { | 
| jbe@165 | 300           __index = function(self, key) | 
| jbe@165 | 301             if headers_value_nil[key] then | 
| jbe@165 | 302               return nil | 
| jbe@165 | 303             end | 
| jbe@165 | 304             local result = nil | 
| jbe@165 | 305             local values = request.headers_csv_table[key] | 
| jbe@165 | 306             if #values == 0 then | 
| jbe@165 | 307               headers_value_nil[key] = true | 
| jbe@165 | 308             elseif #values == 1 then | 
| jbe@165 | 309               result = values[1] | 
| jbe@165 | 310             else | 
| jbe@165 | 311               result = false | 
| jbe@165 | 312             end | 
| jbe@165 | 313             self[key] = result | 
| jbe@165 | 314             return result | 
| jbe@165 | 315           end | 
| jbe@165 | 316         }), | 
| jbe@165 | 317         -- table mapping header field names to a flag table, | 
| jbe@165 | 318         -- indicating if the comma separated value contains certain entries: | 
| jbe@165 | 319         headers_flags = setmetatable({}, { | 
| jbe@165 | 320           __index = function(self, key) | 
| jbe@165 | 321             local result = setmetatable({}, { | 
| jbe@165 | 322               __index = function(self, key) | 
| jbe@165 | 323                 local lowerkey = string.lower(key) | 
| jbe@165 | 324                 local result = rawget(self, lowerkey) or false | 
| jbe@165 | 325                 self[lowerkey] = result | 
| jbe@165 | 326                 self[key] = result | 
| jbe@165 | 327                 return result | 
| jbe@165 | 328               end | 
| jbe@165 | 329             }) | 
| jbe@165 | 330             for i, value in ipairs(request.headers_csv_table[key]) do | 
| jbe@165 | 331               result[string.lower(value)] = true | 
| jbe@165 | 332             end | 
| jbe@165 | 333             self[key] = result | 
| jbe@165 | 334             return result | 
| jbe@165 | 335           end | 
| jbe@165 | 336         }) | 
| jbe@0 | 337       } | 
| jbe@172 | 338       -- create metatable for request object: | 
| jbe@172 | 339       local request_mt = {} | 
| jbe@172 | 340       setmetatable(request, request_mt) | 
| jbe@172 | 341       -- callback for request body streaming: | 
| jbe@172 | 342       local process_body_chunk | 
| jbe@162 | 343       -- local variables to track the state: | 
| jbe@162 | 344       local state = "init"        -- one of: | 
| jbe@162 | 345       --  "init"                  (initial state) | 
| jbe@162 | 346       --  "no_status_sent"        (configuration complete) | 
| jbe@162 | 347       --  "info_status_sent"      (1xx status code has been sent) | 
| jbe@162 | 348       --  "bodyless_status_sent"  (204/304 status code has been sent) | 
| jbe@162 | 349       --  "status_sent"           (regular status code has been sent) | 
| jbe@162 | 350       --  "headers_sent"          (headers have been terminated) | 
| jbe@162 | 351       --  "finished"              (request has been answered completely) | 
| jbe@163 | 352       --  "faulty"                (I/O or protocaol error) | 
| jbe@162 | 353       local close_requested = false  -- "Connection: close" requested | 
| jbe@162 | 354       local close_responded = false  -- "Connection: close" sent | 
| jbe@162 | 355       local content_length = nil  -- value of Content-Length header sent | 
| jbe@164 | 356       local chunk_parts = {}  -- list of chunks to send | 
| jbe@164 | 357       local chunk_bytes = 0   -- sum of lengths of chunks to send | 
| jbe@172 | 358       local streamed_post_params         = {}  -- mapping from POST field name to stream function | 
| jbe@172 | 359       local streamed_post_param_patterns = {}  -- list of POST field pattern and stream function pairs | 
| jbe@163 | 360       -- functions to assert proper output/closing: | 
| jbe@163 | 361       local function assert_output(...) | 
| jbe@163 | 362         local retval, errmsg = ... | 
| jbe@163 | 363         if retval then return ...  end | 
| jbe@163 | 364         state = "faulty" | 
| jbe@163 | 365         socket:reset() | 
| jbe@163 | 366         error("Could not send data to client: " .. errmsg) | 
| jbe@163 | 367       end | 
| jbe@163 | 368       local function assert_close(...) | 
| jbe@163 | 369         local retval, errmsg = ... | 
| jbe@163 | 370         if retval then return ...  end | 
| jbe@163 | 371         state = "faulty" | 
| jbe@163 | 372         error("Could not finish sending data to client: " .. errmsg) | 
| jbe@163 | 373       end | 
| jbe@164 | 374       -- function to assert non-faulty handle: | 
| jbe@164 | 375       local function assert_not_faulty() | 
| jbe@164 | 376         assert(state ~= "faulty", "Tried to use faulty request handle") | 
| jbe@164 | 377       end | 
| jbe@162 | 378       -- functions to send data to the browser: | 
| jbe@160 | 379       local function send(...) | 
| jbe@163 | 380         assert_output(socket:write_call(unblock, ...)) | 
| jbe@38 | 381       end | 
| jbe@162 | 382       local function send_flush(...) | 
| jbe@163 | 383         assert_output(socket:flush_call(unblock, ...)) | 
| jbe@162 | 384       end | 
| jbe@163 | 385       -- function to finish request: | 
| jbe@163 | 386       local function finish() | 
| jbe@163 | 387         if close_responded then | 
| jbe@163 | 388           -- discard any input: | 
| jbe@163 | 389           consume = drain | 
| jbe@163 | 390           -- close output stream: | 
| jbe@163 | 391           send_flush() | 
| jbe@163 | 392           assert_close(socket:finish()) | 
| jbe@163 | 393           -- wait for EOF of peer to avoid immediate TCP RST condition: | 
| jbe@163 | 394           consume_all() | 
| jbe@163 | 395           -- fully close socket: | 
| jbe@163 | 396           assert_close(socket:close()) | 
| jbe@163 | 397         else | 
| jbe@163 | 398           send_flush() | 
| jbe@172 | 399           process_body_chunk = nil | 
| jbe@163 | 400           consume_all() | 
| jbe@163 | 401         end | 
| jbe@163 | 402       end | 
| jbe@164 | 403       -- function that writes out buffered chunks (without flushing the socket): | 
| jbe@164 | 404       function send_chunk() | 
| jbe@164 | 405         if chunk_bytes > 0 then | 
| jbe@164 | 406           assert_output(socket:write(string.format("%x\r\n", chunk_bytes))) | 
| jbe@164 | 407           for i = 1, #chunk_parts do  -- TODO: evaluated only once? | 
| jbe@164 | 408             send(chunk_parts[i]) | 
| jbe@164 | 409             chunk_parts[i] = nil | 
| jbe@164 | 410           end | 
| jbe@164 | 411           chunk_bytes = 0 | 
| jbe@164 | 412           send("\r\n") | 
| jbe@164 | 413         end | 
| jbe@164 | 414       end | 
| jbe@168 | 415       -- function to report an error: | 
| jbe@168 | 416       local function request_error(throw_error, status, text) | 
| jbe@168 | 417         local errmsg = "Error while reading request from client. Error response: " .. status | 
| jbe@168 | 418         if text then | 
| jbe@168 | 419           errmsg = errmsg .. " (" .. text .. ")" | 
| jbe@168 | 420         end | 
| jbe@168 | 421         if | 
| jbe@168 | 422           state == "init" or | 
| jbe@168 | 423           state == "no_status_sent" or | 
| jbe@168 | 424           state == "info_status_sent" | 
| jbe@168 | 425         then | 
| jbe@168 | 426           local error_response_status, errmsg2 = pcall(function() | 
| jbe@168 | 427             request:monologue() | 
| jbe@168 | 428             request:send_status(status) | 
| jbe@168 | 429             request:send_header("Content-Type", "text/plain") | 
| jbe@168 | 430             request:send_data(status, "\n") | 
| jbe@168 | 431             if text then | 
| jbe@168 | 432               request:send_data("\n", text, "\n") | 
| jbe@168 | 433             end | 
| jbe@168 | 434             request:finish() | 
| jbe@168 | 435           end) | 
| jbe@168 | 436           if not error_response_status then | 
| jbe@168 | 437             error("Unexpected error while sending error response: " .. errmsg2) | 
| jbe@168 | 438           end | 
| jbe@168 | 439         elseif state ~= "faulty" then | 
| jbe@169 | 440           state = "faulty" | 
| jbe@168 | 441           assert_close(socket:reset()) | 
| jbe@168 | 442         end | 
| jbe@168 | 443         if throw_error then | 
| jbe@168 | 444           error(errmsg) | 
| jbe@168 | 445         else | 
| jbe@168 | 446           return survive | 
| jbe@168 | 447         end | 
| jbe@168 | 448       end | 
| jbe@170 | 449       -- read function | 
| jbe@170 | 450       local function read(...) | 
| jbe@170 | 451         local data, status = socket:read_yield(...) | 
| jbe@170 | 452         if data == nil then | 
| jbe@170 | 453           request_error(true, "400 Bad Request", "Read error") | 
| jbe@170 | 454         end | 
| jbe@170 | 455         if status == "eof" then | 
| jbe@170 | 456           request_error(true, "400 Bad Request", "Unexpected EOF") | 
| jbe@170 | 457         end | 
| jbe@170 | 458         return data | 
| jbe@170 | 459       end | 
| jbe@168 | 460       -- reads a number of bytes from the socket, | 
| jbe@168 | 461       -- optionally feeding these bytes chunk-wise | 
| jbe@168 | 462       -- into a callback function: | 
| jbe@168 | 463       local function read_body_bytes(remaining) | 
| jbe@168 | 464         while remaining > 0 do | 
| jbe@168 | 465           local limit | 
| jbe@168 | 466           if remaining > input_chunk_size then | 
| jbe@168 | 467             limit = input_chunk_size | 
| jbe@168 | 468           else | 
| jbe@168 | 469             limit = remaining | 
| jbe@168 | 470           end | 
| jbe@170 | 471           local chunk = read(limit) | 
| jbe@168 | 472           remaining = remaining - limit | 
| jbe@168 | 473           if process_body_chunk then | 
| jbe@168 | 474             process_body_chunk(chunk) | 
| jbe@168 | 475           end | 
| jbe@168 | 476         end | 
| jbe@168 | 477       end | 
| jbe@168 | 478       -- coroutine for request body processing: | 
| jbe@168 | 479       local function read_body() | 
| jbe@168 | 480         if request.headers_flags["Transfer-Encoding"]["chunked"] then | 
| jbe@168 | 481           while true do | 
| jbe@170 | 482             local line = read(32 + remaining_body_size_limit, "\n") | 
| jbe@168 | 483             local zeros, lenstr = string.match(line, "^(0*)([1-9A-Fa-f]+[0-9A-Fa-f]*)\r?\n$") | 
| jbe@168 | 484             local chunkext | 
| jbe@168 | 485             if lenstr then | 
| jbe@168 | 486               chunkext = "" | 
| jbe@168 | 487             else | 
| jbe@168 | 488               zeros, lenstr, chunkext = string.match(line, "^(0*)([1-9A-Fa-f]+[0-9A-Fa-f]*)([ \t;].-)\r?\n$") | 
| jbe@168 | 489             end | 
| jbe@168 | 490             if not lenstr or #lenstr > 13 then | 
| jbe@168 | 491               request_error(true, "400 Bad Request", "Encoding error while reading chunk of request body") | 
| jbe@168 | 492             end | 
| jbe@168 | 493             local len = tonumber("0x" .. lenstr) | 
| jbe@168 | 494             remaining_body_size_limit = remaining_body_size_limit - (#zeros + #chunkext + len) | 
| jbe@168 | 495             if remaining_body_size_limit < 0 then | 
| jbe@168 | 496               request_error(true, "413 Request Entity Too Large", "Request body size limit exceeded") | 
| jbe@168 | 497             end | 
| jbe@168 | 498             if len == 0 then break end | 
| jbe@168 | 499             read_body_bytes(len) | 
| jbe@170 | 500             local term = read(2, "\n") | 
| jbe@168 | 501             if term ~= "\r\n" and term ~= "\n" then | 
| jbe@168 | 502               request_error(true, "400 Bad Request", "Encoding error while reading chunk of request body") | 
| jbe@168 | 503             end | 
| jbe@168 | 504           end | 
| jbe@168 | 505           while true do | 
| jbe@170 | 506             local line = read(2 + remaining_body_size_limit, "\n") | 
| jbe@168 | 507             if line == "\r\n" or line == "\n" then break end | 
| jbe@168 | 508             remaining_body_size_limit = remaining_body_size_limit - #line | 
| jbe@168 | 509             if remaining_body_size_limit < 0 then | 
| jbe@168 | 510               request_error(true, "413 Request Entity Too Large", "Request body size limit exceeded while reading trailer section of chunked request body") | 
| jbe@168 | 511             end | 
| jbe@168 | 512           end | 
| jbe@168 | 513         elseif request_body_content_length then | 
| jbe@168 | 514           read_body_bytes(request_body_content_length) | 
| jbe@168 | 515         end | 
| jbe@168 | 516       end | 
| jbe@172 | 517       -- function to setup default request body handling: | 
| jbe@172 | 518       local function default_request_body_handling() | 
| jbe@172 | 519         local post_params_list, post_params = new_params_list() | 
| jbe@172 | 520         local content_type = request.headers_value["Content-Type"] | 
| jbe@172 | 521         if content_type then | 
| jbe@172 | 522           if | 
| jbe@172 | 523             content_type == "application/x-www-form-urlencoded" or | 
| jbe@172 | 524             string.match(content_type, "^application/x%-www%-form%-urlencoded *;") | 
| jbe@172 | 525           then | 
| jbe@172 | 526             read_urlencoded_form(post_params_list, request.body) | 
| jbe@172 | 527             request.post_params_list, request.post_params = post_params_list, post_params | 
| jbe@172 | 528           else | 
| jbe@172 | 529             local boundary = string.match( | 
| jbe@172 | 530               content_type, | 
| jbe@172 | 531               '^multipart/form%-data[ \t]*[;,][ \t]*boundary="([^"]+)"$' | 
| jbe@172 | 532             ) or string.match( | 
| jbe@172 | 533               content_type, | 
| jbe@172 | 534               '^multipart/form%-data[ \t]*[;,][ \t]*boundary=([^"; \t]+)$' | 
| jbe@172 | 535             ) | 
| jbe@172 | 536             if boundary then | 
| jbe@172 | 537               local post_metadata_list, post_metadata = new_params_list() | 
| jbe@172 | 538               boundary = "--" .. boundary | 
| jbe@172 | 539               local headerdata = "" | 
| jbe@172 | 540               local streamer | 
| jbe@172 | 541               local field_name | 
| jbe@172 | 542               local metadata = {} | 
| jbe@172 | 543               local value_parts | 
| jbe@172 | 544               local function default_streamer(chunk) | 
| jbe@172 | 545                 value_parts[#value_parts+1] = chunk | 
| jbe@172 | 546               end | 
| jbe@172 | 547               local function stream_part_finish() | 
| jbe@172 | 548                 if streamer == default_streamer then | 
| jbe@172 | 549                   local value = table.concat(value_parts) | 
| jbe@172 | 550                   value_parts = nil | 
| jbe@172 | 551                   if field_name then | 
| jbe@172 | 552                     local values = post_params_list[field_name] | 
| jbe@172 | 553                     values[#values+1] = value | 
| jbe@172 | 554                     local metadata_entries = post_metadata_list[field_name] | 
| jbe@172 | 555                     metadata_entries[#metadata_entries+1] = metadata | 
| jbe@172 | 556                   end | 
| jbe@172 | 557                 else | 
| jbe@172 | 558                   streamer() | 
| jbe@172 | 559                 end | 
| jbe@172 | 560                 headerdata   = "" | 
| jbe@172 | 561                 streamer     = nil | 
| jbe@172 | 562                 field_name   = nil | 
| jbe@172 | 563                 metadata     = {} | 
| jbe@172 | 564               end | 
| jbe@172 | 565               local function stream_part_chunk(chunk) | 
| jbe@172 | 566                 if streamer then | 
| jbe@172 | 567                   streamer(chunk) | 
| jbe@172 | 568                 else | 
| jbe@172 | 569                   headerdata = headerdata .. chunk | 
| jbe@172 | 570                   while true do | 
| jbe@172 | 571                     local line, remaining = string.match(headerdata, "^(.-)\r?\n(.*)$") | 
| jbe@172 | 572                     if not line then | 
| jbe@172 | 573                       break | 
| jbe@172 | 574                     end | 
| jbe@172 | 575                     if line == "" then | 
| jbe@172 | 576                       streamer = streamed_post_params[field_name] | 
| jbe@172 | 577                       if not streamer then | 
| jbe@172 | 578                         for i, rule in ipairs(streamed_post_param_patterns) do | 
| jbe@172 | 579                           if string.match(field_name, rule[1]) then | 
| jbe@172 | 580                             streamer = rule[2] | 
| jbe@172 | 581                             break | 
| jbe@172 | 582                           end | 
| jbe@172 | 583                         end | 
| jbe@172 | 584                       end | 
| jbe@172 | 585                       if not streamer then | 
| jbe@172 | 586                         value_parts = {} | 
| jbe@172 | 587                         streamer = default_streamer | 
| jbe@172 | 588                       end | 
| jbe@172 | 589                       streamer(remaining, field_name, metadata) | 
| jbe@172 | 590                       return | 
| jbe@172 | 591                     end | 
| jbe@172 | 592                     headerdata = remaining | 
| jbe@172 | 593                     local header_key, header_value = string.match(line, "^([^:]*):[ \t]*(.-)[ \t]*$") | 
| jbe@172 | 594                     if not header_key then | 
| jbe@172 | 595                       request_error(true, "400 Bad Request", "Invalid header in multipart/form-data part") | 
| jbe@172 | 596                     end | 
| jbe@172 | 597                     header_key = string.lower(header_key) | 
| jbe@172 | 598                     if header_key == "content-disposition" then | 
| jbe@172 | 599                       local escaped_header_value = string.gsub(header_value, '"[^"]*"', function(str) | 
| jbe@172 | 600                         return string.gsub(str, "=", "==") | 
| jbe@172 | 601                       end) | 
| jbe@172 | 602                       field_name = string.match(escaped_header_value, ';[ \t]*name="([^"]*)"') | 
| jbe@172 | 603                       if field_name then | 
| jbe@172 | 604                         field_name = string.gsub(field_name, "==", "=") | 
| jbe@172 | 605                       else | 
| jbe@172 | 606                         field_name = string.match(header_value, ';[ \t]*name=([^"; \t]+)') | 
| jbe@172 | 607                       end | 
| jbe@172 | 608                       metadata.file_name = string.match(escaped_header_value, ';[ \t]*filename="([^"]*)"') | 
| jbe@172 | 609                       if metadata.file_name then | 
| jbe@172 | 610                         metadata.file_name = string.gsub(metadata.file_name, "==", "=") | 
| jbe@172 | 611                       else | 
| jbe@172 | 612                         string.match(header_value, ';[ \t]*filename=([^"; \t]+)') | 
| jbe@172 | 613                       end | 
| jbe@172 | 614                     elseif header_key == "content-type" then | 
| jbe@172 | 615                       metadata.content_type = header_value | 
| jbe@172 | 616                     elseif header_key == "content-transfer-encoding" then | 
| jbe@172 | 617                       request_error(true, "400 Bad Request", "Content-transfer-encoding not supported by multipart/form-data parser") | 
| jbe@172 | 618                     end | 
| jbe@172 | 619                   end | 
| jbe@172 | 620                 end | 
| jbe@172 | 621               end | 
| jbe@172 | 622               local skippart   = true   -- ignore data until first boundary | 
| jbe@172 | 623               local afterbound = false  -- interpret 2 bytes after boundary ("\r\n" or "--") | 
| jbe@172 | 624               local terminated = false  -- final boundary read | 
| jbe@172 | 625               local bigchunk = "" | 
| jbe@172 | 626               request:set_request_body_streamer(function(chunk) | 
| jbe@172 | 627                 if chunk == nil then | 
| jbe@172 | 628                   if not terminated then | 
| jbe@172 | 629                     request_error(true, "400 Bad Request", "Premature end of multipart/form-data request body") | 
| jbe@172 | 630                   end | 
| jbe@172 | 631                   request.post_metadata_list, request.post_metadata = post_metadata_list, post_metadata | 
| jbe@172 | 632                 end | 
| jbe@172 | 633                 if terminated then | 
| jbe@172 | 634                   return | 
| jbe@172 | 635                 end | 
| jbe@172 | 636                 bigchunk = bigchunk .. chunk | 
| jbe@172 | 637                 while true do | 
| jbe@172 | 638                   if afterbound then | 
| jbe@172 | 639                     if #bigchunk <= 2 then | 
| jbe@172 | 640                       return | 
| jbe@172 | 641                     end | 
| jbe@172 | 642                     local terminator = string.sub(bigchunk, 1, 2) | 
| jbe@172 | 643                     if terminator == "\r\n" then | 
| jbe@172 | 644                       afterbound = false | 
| jbe@172 | 645                       bigchunk = string.sub(bigchunk, 3) | 
| jbe@172 | 646                     elseif terminator == "--" then | 
| jbe@172 | 647                       terminated = true | 
| jbe@172 | 648                       bigchunk = nil | 
| jbe@172 | 649                       return | 
| jbe@172 | 650                     else | 
| jbe@172 | 651                       request_error(true, "400 Bad Request", "Error while parsing multipart body (expected CRLF or double minus)") | 
| jbe@172 | 652                     end | 
| jbe@172 | 653                   end | 
| jbe@172 | 654                   local pos1, pos2 = string.find(bigchunk, boundary, 1, true) | 
| jbe@172 | 655                   if not pos1 then | 
| jbe@172 | 656                     if not skippart then | 
| jbe@172 | 657                       local safe = #bigchunk-#boundary | 
| jbe@172 | 658                       if safe > 0 then | 
| jbe@172 | 659                         stream_part_chunk(string.sub(bigchunk, 1, safe)) | 
| jbe@172 | 660                         bigchunk = string.sub(bigchunk, safe+1) | 
| jbe@172 | 661                       end | 
| jbe@172 | 662                     end | 
| jbe@172 | 663                     return | 
| jbe@172 | 664                   end | 
| jbe@172 | 665                   if not skippart then | 
| jbe@172 | 666                     stream_part_chunk(string.sub(bigchunk, 1, pos1 - 1)) | 
| jbe@172 | 667                     stream_part_finish() | 
| jbe@172 | 668                   else | 
| jbe@172 | 669                     boundary = "\r\n" .. boundary | 
| jbe@172 | 670                     skippart = false | 
| jbe@172 | 671                   end | 
| jbe@172 | 672                   bigchunk = string.sub(bigchunk, pos2 + 1) | 
| jbe@172 | 673                   afterbound = true | 
| jbe@172 | 674                 end | 
| jbe@172 | 675               end) | 
| jbe@172 | 676             else | 
| jbe@172 | 677               request_error(true, "415 Unsupported Media Type", "Unknown Content-Type of request body") | 
| jbe@172 | 678             end | 
| jbe@172 | 679           end | 
| jbe@172 | 680         end | 
| jbe@172 | 681       end | 
| jbe@172 | 682       -- function to prepare body processing: | 
| jbe@162 | 683       local function prepare() | 
| jbe@164 | 684         assert_not_faulty() | 
| jbe@172 | 685         if process_body_chunk == nil then | 
| jbe@172 | 686           default_request_body_handling() | 
| jbe@172 | 687         end | 
| jbe@171 | 688         if state ~= "init" then | 
| jbe@162 | 689           return | 
| jbe@162 | 690         end | 
| jbe@171 | 691         consume = coroutine.wrap(read_body) | 
| jbe@162 | 692         state = "no_status_sent" | 
| jbe@171 | 693         if request.headers_flags["Expect"]["100-continue"] then | 
| jbe@171 | 694           request:send_status("100 Continue") | 
| jbe@171 | 695           request:finish_headers() | 
| jbe@171 | 696         end | 
| jbe@162 | 697       end | 
| jbe@163 | 698       -- method to ignore input and close connection after response: | 
| jbe@163 | 699       function request:monologue() | 
| jbe@164 | 700         assert_not_faulty() | 
| jbe@163 | 701         if | 
| jbe@163 | 702           state == "headers_sent" or | 
| jbe@163 | 703           state == "finished" | 
| jbe@163 | 704         then | 
| jbe@163 | 705           error("All HTTP headers have already been sent") | 
| jbe@163 | 706         end | 
| jbe@164 | 707         local old_state = state | 
| jbe@164 | 708         state = "faulty" | 
| jbe@163 | 709         consume = drain | 
| jbe@163 | 710         close_requested = true | 
| jbe@171 | 711         if old_state == "init" then | 
| jbe@163 | 712           state = "no_status_sent" | 
| jbe@164 | 713         else | 
| jbe@164 | 714           state = old_state | 
| jbe@162 | 715         end | 
| jbe@162 | 716       end | 
| jbe@163 | 717       -- | 
| jbe@162 | 718       -- method to send a HTTP response status (e.g. "200 OK"): | 
| jbe@162 | 719       function request:send_status(status) | 
| jbe@162 | 720         prepare() | 
| jbe@164 | 721         local old_state = state | 
| jbe@164 | 722         state = "faulty" | 
| jbe@164 | 723         if old_state == "info_status_sent" then | 
| jbe@162 | 724           send_flush("\r\n") | 
| jbe@164 | 725         elseif old_state ~= "no_status_sent" then | 
| jbe@162 | 726           error("HTTP status has already been sent") | 
| jbe@162 | 727         end | 
| jbe@162 | 728         local status1 = string.sub(status, 1, 1) | 
| jbe@162 | 729         local status3 = string.sub(status, 1, 3) | 
| jbe@162 | 730         send("HTTP/1.1 ", status, "\r\n", preamble) | 
| jbe@162 | 731         local wrb = status_without_response_body[status3] | 
| jbe@162 | 732         if wrb then | 
| jbe@162 | 733           state = "bodyless_status_sent" | 
| jbe@162 | 734           if wrb == "zero_content_length" then | 
| jbe@162 | 735             request:send_header("Content-Length", 0) | 
| jbe@162 | 736           end | 
| jbe@162 | 737         elseif status1 == "1" then | 
| jbe@162 | 738           state = "info_status_sent" | 
| jbe@162 | 739         else | 
| jbe@162 | 740           state = "status_sent" | 
| jbe@162 | 741         end | 
| jbe@162 | 742       end | 
| jbe@162 | 743       -- method to send a HTTP response header: | 
| jbe@162 | 744       -- (key and value must be provided as separate args) | 
| jbe@162 | 745       function request:send_header(key, value) | 
| jbe@164 | 746         assert_not_faulty() | 
| jbe@171 | 747         if state == "init" or state == "no_status_sent" then | 
| jbe@162 | 748           error("HTTP status has not been sent yet") | 
| jbe@162 | 749         elseif | 
| jbe@164 | 750           state == "headers_sent" or | 
| jbe@164 | 751           state == "finished" | 
| jbe@162 | 752         then | 
| jbe@162 | 753           error("All HTTP headers have already been sent") | 
| jbe@162 | 754         end | 
| jbe@162 | 755         local key_lower = string.lower(key) | 
| jbe@162 | 756         if key_lower == "content-length" then | 
| jbe@162 | 757           if state == "info_status_sent" then | 
| jbe@162 | 758             error("Cannot set Content-Length for informational status response") | 
| jbe@162 | 759           end | 
| jbe@162 | 760           local cl = assert(tonumber(value), "Invalid content-length") | 
| jbe@162 | 761           if content_length == nil then | 
| jbe@162 | 762             content_length = cl | 
| jbe@162 | 763           elseif content_length == cl then | 
| jbe@162 | 764             return | 
| jbe@162 | 765           else | 
| jbe@162 | 766             error("Content-Length has been set multiple times with different values") | 
| jbe@162 | 767           end | 
| jbe@162 | 768         elseif key_lower == "connection" then | 
| jbe@162 | 769           for entry in string.gmatch(string.lower(value), "[^,]+") do | 
| jbe@162 | 770             if string.match(entry, "^[ \t]*close[ \t]*$") then | 
| jbe@162 | 771               if state == "info_status_sent" then | 
| jbe@162 | 772                 error("Cannot set \"Connection: close\" for informational status response") | 
| jbe@162 | 773               end | 
| jbe@162 | 774               close_responded = true | 
| jbe@162 | 775               break | 
| jbe@162 | 776             end | 
| jbe@162 | 777           end | 
| jbe@162 | 778         end | 
| jbe@162 | 779         assert_output(socket:write(key, ": ", value, "\r\n")) | 
| jbe@162 | 780       end | 
| jbe@162 | 781       -- function to terminate header section in response, optionally flushing: | 
| jbe@162 | 782       -- (may be called multiple times unless response is finished) | 
| jbe@162 | 783       local function finish_headers(with_flush) | 
| jbe@162 | 784         if state == "finished" then | 
| jbe@162 | 785           error("Response has already been finished") | 
| jbe@162 | 786         elseif state == "info_status_sent" then | 
| jbe@162 | 787           send_flush("\r\n") | 
| jbe@162 | 788           state = "no_status_sent" | 
| jbe@162 | 789         elseif state == "bodyless_status_sent" then | 
| jbe@162 | 790           if close_requested and not close_responded then | 
| jbe@162 | 791             request:send_header("Connection", "close") | 
| jbe@162 | 792           end | 
| jbe@162 | 793           send("\r\n") | 
| jbe@163 | 794           finish() | 
| jbe@162 | 795           state = "finished" | 
| jbe@162 | 796         elseif state == "status_sent" then | 
| jbe@162 | 797           if not content_length then | 
| jbe@162 | 798             request:send_header("Transfer-Encoding", "chunked") | 
| jbe@162 | 799           end | 
| jbe@162 | 800           if close_requested and not close_responded then | 
| jbe@162 | 801             request:send_header("Connection", "close") | 
| jbe@162 | 802           end | 
| jbe@162 | 803           send("\r\n") | 
| jbe@162 | 804           if request.method == "HEAD" then | 
| jbe@163 | 805             finish() | 
| jbe@162 | 806           elseif with_flush then | 
| jbe@162 | 807             send_flush() | 
| jbe@162 | 808           end | 
| jbe@162 | 809           state = "headers_sent" | 
| jbe@162 | 810         elseif state ~= "headers_sent" then | 
| jbe@162 | 811           error("HTTP status has not been sent yet") | 
| jbe@162 | 812         end | 
| jbe@162 | 813       end | 
| jbe@162 | 814       -- method to finish and flush headers: | 
| jbe@162 | 815       function request:finish_headers() | 
| jbe@164 | 816         assert_not_faulty() | 
| jbe@162 | 817         finish_headers(true) | 
| jbe@162 | 818       end | 
| jbe@164 | 819       -- method to send body data: | 
| jbe@164 | 820       function request:send_data(...) | 
| jbe@164 | 821         assert_not_faulty() | 
| jbe@164 | 822         if output_state == "info_status_sent" then | 
| jbe@164 | 823           error("No (non-informational) HTTP status has been sent yet") | 
| jbe@164 | 824         elseif output_state == "bodyless_status_sent" then | 
| jbe@164 | 825           error("Cannot send response data for body-less status message") | 
| jbe@164 | 826         end | 
| jbe@164 | 827         finish_headers(false) | 
| jbe@164 | 828         if output_state ~= "headers_sent" then | 
| jbe@164 | 829           error("Unexpected internal status in HTTP engine") | 
| jbe@164 | 830         end | 
| jbe@164 | 831         if request.method == "HEAD" then | 
| jbe@164 | 832           return | 
| jbe@164 | 833         end | 
| jbe@164 | 834         for i = 1, select("#", ...) do | 
| jbe@164 | 835           local str = tostring(select(i, ...)) | 
| jbe@164 | 836           if #str > 0 then | 
| jbe@164 | 837             if content_length then | 
| jbe@164 | 838               local bytes_to_send = #str | 
| jbe@164 | 839               if bytes_sent + bytes_to_send > content_length then | 
| jbe@164 | 840                 error("Content length exceeded") | 
| jbe@164 | 841               else | 
| jbe@164 | 842                 send(str) | 
| jbe@164 | 843                 bytes_sent = bytes_sent + bytes_to_send | 
| jbe@164 | 844               end | 
| jbe@164 | 845             else | 
| jbe@164 | 846               chunk_bytes = chunk_bytes + #str | 
| jbe@164 | 847               chunk_parts[#chunk_parts+1] = str | 
| jbe@164 | 848             end | 
| jbe@164 | 849           end | 
| jbe@164 | 850         end | 
| jbe@164 | 851         if chunk_bytes >= output_chunk_size then | 
| jbe@164 | 852           send_chunk() | 
| jbe@164 | 853         end | 
| jbe@164 | 854       end | 
| jbe@165 | 855       -- method to flush output buffer: | 
| jbe@165 | 856       function request:flush() | 
| jbe@165 | 857         assert_not_faulty() | 
| jbe@165 | 858         send_chunk() | 
| jbe@165 | 859         send_flush() | 
| jbe@165 | 860       end | 
| jbe@165 | 861       -- method to finish response: | 
| jbe@165 | 862       function request:finish() | 
| jbe@165 | 863         assert_not_faulty() | 
| jbe@165 | 864         if state == "finished" then | 
| jbe@165 | 865           return | 
| jbe@165 | 866         elseif state == "info_status_sent" then | 
| jbe@165 | 867           error("Informational HTTP response can be finished with :finish_headers() method") | 
| jbe@165 | 868         end | 
| jbe@165 | 869         finish_headers(false) | 
| jbe@165 | 870         if state == "headers_sent" then | 
| jbe@165 | 871           if request.method ~= "HEAD" then | 
| jbe@165 | 872             state = "faulty" | 
| jbe@165 | 873             if content_length then | 
| jbe@165 | 874               if bytes_sent ~= content_length then | 
| jbe@165 | 875                 error("Content length not used") | 
| jbe@165 | 876               end | 
| jbe@165 | 877             else | 
| jbe@165 | 878               send_chunk() | 
| jbe@165 | 879               send("0\r\n\r\n") | 
| jbe@165 | 880             end | 
| jbe@165 | 881             finish() | 
| jbe@165 | 882           end | 
| jbe@165 | 883           state = "finished" | 
| jbe@165 | 884         elseif state ~= "finished" then | 
| jbe@165 | 885           error("Unexpected internal status in HTTP engine") | 
| jbe@165 | 886         end | 
| jbe@165 | 887       end | 
| jbe@172 | 888       -- method to register POST param stream handler for a single field name: | 
| jbe@172 | 889       function request:stream_post_param(field_name, callback) | 
| jbe@172 | 890         if state ~= "init" then | 
| jbe@172 | 891           error("Cannot setup request body streamer at this stage") | 
| jbe@172 | 892         end | 
| jbe@172 | 893         streamed_post_params[field_name] = callback | 
| jbe@172 | 894       end | 
| jbe@172 | 895       -- method to register POST param stream handler for a field name pattern: | 
| jbe@172 | 896       function request:stream_post_params(pattern, callback) | 
| jbe@172 | 897         if state ~= "init" then | 
| jbe@172 | 898           error("Cannot setup request body streamer at this stage") | 
| jbe@172 | 899         end | 
| jbe@172 | 900         streamed_post_param_patterns[#streamed_post_param_patterns+1] = {pattern, callback} | 
| jbe@172 | 901       end | 
| jbe@172 | 902       -- method to register request body stream handler | 
| jbe@172 | 903       function request:set_request_body_streamer(callback) | 
| jbe@172 | 904         if state ~= "init" then | 
| jbe@172 | 905           error("Cannot setup request body streamer at this stage") | 
| jbe@172 | 906         end | 
| jbe@172 | 907         local inprogress = false | 
| jbe@172 | 908         local buffer = {} | 
| jbe@172 | 909         process_body_chunk = function(chunk) | 
| jbe@172 | 910           if inprogress then | 
| jbe@172 | 911             buffer[#buffer+1] = chunk | 
| jbe@172 | 912           else | 
| jbe@172 | 913             inprogress = true | 
| jbe@172 | 914             callback(chunk) | 
| jbe@172 | 915             while #buffer > 0 do | 
| jbe@172 | 916               chunk = table.concat(buffer) | 
| jbe@172 | 917               buffer = {} | 
| jbe@172 | 918               callback(chunk) | 
| jbe@172 | 919             end | 
| jbe@172 | 920             inprogress = false | 
| jbe@172 | 921           end | 
| jbe@172 | 922         end | 
| jbe@172 | 923       end | 
| jbe@172 | 924       -- method to start reading request body | 
| jbe@172 | 925       function request:consume_input() | 
| jbe@172 | 926         prepare() | 
| jbe@172 | 927         consume_all() | 
| jbe@172 | 928       end | 
| jbe@172 | 929       -- method to stream request body | 
| jbe@172 | 930       function request:stream_request_body(callback) | 
| jbe@172 | 931         request:set_request_body_streamer(function(chunk) | 
| jbe@172 | 932           if chunk ~= nil then | 
| jbe@172 | 933             callback(chunk) | 
| jbe@172 | 934           end | 
| jbe@172 | 935         end) | 
| jbe@172 | 936         request:consume_input() | 
| jbe@172 | 937       end | 
| jbe@172 | 938       -- metamethod to read special attibutes of request object: | 
| jbe@172 | 939       function request_mt:__index(key, value) | 
| jbe@172 | 940         if key == "body" then | 
| jbe@172 | 941           local chunks = {} | 
| jbe@172 | 942           request:stream_request_body(function(chunk) | 
| jbe@172 | 943             chunks[#chunks+1] = chunk | 
| jbe@172 | 944           end) | 
| jbe@172 | 945           self.body = table.concat(chunks) | 
| jbe@172 | 946           return self.body | 
| jbe@172 | 947         elseif | 
| jbe@172 | 948           key == "post_params_list" or key == "post_params" or | 
| jbe@172 | 949           key == "post_metadata_list" or key == "post_metadata" | 
| jbe@172 | 950         then | 
| jbe@172 | 951           prepare() | 
| jbe@172 | 952           consume_all() | 
| jbe@172 | 953           return self[key] | 
| jbe@172 | 954         end | 
| jbe@172 | 955       end | 
| jbe@160 | 956       -- wait for input: | 
| jbe@160 | 957       if not poll(socket_set, nil, request_idle_timeout) then | 
| jbe@163 | 958         return request_error(false, "408 Request Timeout", "Idle connection timed out") | 
| jbe@38 | 959       end | 
| jbe@162 | 960     until close_responded | 
| jbe@0 | 961     return survive | 
| jbe@0 | 962   end | 
| jbe@0 | 963 end | 
| jbe@0 | 964 | 
| jbe@0 | 965 return _M | 
| jbe@0 | 966 |