jbe@0: #!/usr/bin/env lua jbe@0: jbe@0: -- module preamble jbe@0: local _G, _M = _ENV, {} jbe@0: _ENV = setmetatable({}, { jbe@0: __index = function(self, key) jbe@0: local value = _M[key]; if value ~= nil then return value end jbe@0: return _G[key] jbe@0: end, jbe@63: __newindex = _M jbe@0: }) jbe@0: jbe@0: -- function that encodes certain HTML entities: jbe@0: -- (not used by the library itself) jbe@0: function encode_html(text) jbe@0: return ( jbe@0: string.gsub( jbe@0: text, '[<>&"]', jbe@0: function(char) jbe@0: if char == '<' then jbe@0: return "<" jbe@0: elseif char == '>' then jbe@0: return ">" jbe@0: elseif char == '&' then jbe@0: return "&" jbe@0: elseif char == '"' then jbe@0: return """ jbe@0: end jbe@0: end jbe@0: ) jbe@0: ) jbe@0: jbe@0: end jbe@0: jbe@0: -- function that encodes special characters for URIs: jbe@0: -- (not used by the library itself) jbe@0: function encode_uri(text) jbe@0: return ( jbe@0: string.gsub(text, "[^0-9A-Za-z_%.~-]", jbe@0: function (char) jbe@0: return string.format("%%%02x", string.byte(char)) jbe@0: end jbe@0: ) jbe@0: ) jbe@0: end jbe@0: jbe@0: -- function undoing URL encoding: jbe@0: do jbe@0: local b0 = string.byte("0") jbe@0: local b9 = string.byte("9") jbe@0: local bA = string.byte("A") jbe@0: local bF = string.byte("F") jbe@0: local ba = string.byte("a") jbe@0: local bf = string.byte("f") jbe@0: function decode_uri(str) jbe@0: return ( jbe@0: string.gsub( jbe@0: string.gsub(str, "%+", " "), jbe@0: "%%([0-9A-Fa-f][0-9A-Fa-f])", jbe@0: function(hex) jbe@0: local n1, n2 = string.byte(hex, 1, 2) jbe@0: if n1 >= b0 and n1 <= b9 then n1 = n1 - b0 jbe@0: elseif n1 >= bA and n1 <= bF then n1 = n1 - bA + 10 jbe@0: elseif n1 >= ba and n1 <= bf then n1 = n1 - ba + 10 jbe@0: else error("Assertion failed") end jbe@0: if n2 >= b0 and n2 <= b9 then n2 = n2 - b0 jbe@0: elseif n2 >= bA and n2 <= bF then n2 = n2 - bA + 10 jbe@0: elseif n2 >= ba and n2 <= bf then n2 = n2 - ba + 10 jbe@0: else error("Assertion failed") end jbe@0: return string.char(n1 * 16 + n2) jbe@0: end jbe@0: ) jbe@0: ) jbe@0: end jbe@0: end jbe@0: jbe@0: -- status codes that carry no response body (in addition to 1xx): jbe@0: -- (set to "zero_content_length" if Content-Length header is required) jbe@0: status_without_response_body = { jbe@5: ["101"] = true, -- list 101 to allow protocol switch jbe@0: ["204"] = true, jbe@0: ["205"] = "zero_content_length", jbe@0: ["304"] = true jbe@0: } jbe@0: jbe@154: -- parses URL encoded form data: jbe@154: local function read_urlencoded_form(data) jbe@154: local tbl = {} jbe@154: for rawkey, rawvalue in string.gmatch(data, "([^?=&]*)=([^?=&]*)") do jbe@154: local key = decode_uri(rawkey) jbe@154: local value = decode_uri(rawvalue) jbe@154: local subtbl = tbl[key] jbe@154: if subtbl then jbe@154: subtbl[#subtbl+1] = value jbe@154: else jbe@154: tbl[key] = {value} jbe@35: end jbe@35: end jbe@154: return tbl jbe@0: end jbe@0: jbe@154: -- extracts first value from each subtable: jbe@154: local function get_first_values(tbl) jbe@154: local newtbl = {} jbe@154: for key, subtbl in pairs(tbl) do jbe@154: newtbl[key] = subtbl[1] jbe@0: end jbe@154: return newtbl jbe@154: end jbe@154: jbe@159: local headers_mt_self = setmetatable({}, {__mode="k"}) jbe@159: jbe@159: local headers_mts = { jbe@159: headers_mt = { jbe@159: __index = function(tbl, key) jbe@159: local self = headers_mt_self[tbl] jbe@159: local lowerkey = string.lower(key) jbe@159: local result = self._headers[lowerkey] jbe@159: if result == nil then jbe@159: result = {} jbe@159: end jbe@159: tbl[lowerkey] = result jbe@159: tbl[key] = result jbe@159: return result jbe@159: end jbe@159: }, jbe@159: -- table mapping header field names to value-lists jbe@159: -- (for headers with comma separated values): jbe@159: headers_csv_table = { jbe@159: __index = function(tbl, key) jbe@159: local self = headers_mt_self[tbl] jbe@159: local result = {} jbe@159: for i, line in ipairs(self.headers[key]) do jbe@159: for entry in string.gmatch(line, "[^,]+") do jbe@159: local value = string.match(entry, "^[ \t]*(..-)[ \t]*$") jbe@159: if value then jbe@159: result[#result+1] = value jbe@159: end jbe@159: end jbe@159: end jbe@159: tbl[key] = result jbe@159: return result jbe@159: end jbe@159: }, jbe@159: -- table mapping header field names to a comma separated string jbe@159: -- (for headers with comma separated values): jbe@159: headers_csv_string = { jbe@159: __index = function(tbl, key) jbe@159: local self = headers_mt_self[tbl] jbe@159: local result = {} jbe@159: for i, line in ipairs(self.headers[key]) do jbe@159: result[#result+1] = line jbe@159: end jbe@159: result = string.concat(result, ", ") jbe@159: tbl[key] = result jbe@159: return result jbe@159: end jbe@159: }, jbe@159: -- table mapping header field names to a single string value jbe@159: -- (or false if header has been sent multiple times): jbe@159: headers_value = { jbe@159: __index = function(tbl, key) jbe@159: local self = headers_mt_self[tbl] jbe@159: if self._headers_value_nil[key] then jbe@159: return nil jbe@159: end jbe@159: local result = nil jbe@159: local values = self.headers_csv_table[key] jbe@159: if #values == 0 then jbe@159: self._headers_value_nil[key] = true jbe@159: elseif #values == 1 then jbe@159: result = values[1] jbe@159: else jbe@159: result = false jbe@159: end jbe@159: tbl[key] = result jbe@159: return result jbe@159: end jbe@159: }, jbe@159: -- table mapping header field names to a flag table, jbe@159: -- indicating if the comma separated value contains certain entries: jbe@159: headers_flags = { jbe@159: __index = function(tbl, key) jbe@159: local self = headers_mt_self[tbl] jbe@159: local result = setmetatable({}, { jbe@159: __index = function(tbl, key) jbe@159: local lowerkey = string.lower(key) jbe@159: local result = rawget(tbl, lowerkey) or false jbe@159: tbl[lowerkey] = result jbe@159: tbl[key] = result jbe@159: return result jbe@159: end jbe@159: }) jbe@159: for i, value in ipairs(self.headers_csv_table[key]) do jbe@159: result[string.lower(value)] = true jbe@159: end jbe@159: tbl[key] = result jbe@159: return result jbe@159: end jbe@159: } jbe@159: } jbe@159: jbe@154: request_pt = {} jbe@154: request_mt = { __index = request_pt } jbe@154: jbe@154: function request_pt:_init(handler, options) jbe@155: self._application_handler = handler jbe@0: -- process options: jbe@0: options = options or {} jbe@0: do jbe@0: -- named arg "static_headers" is used to create the preamble: jbe@0: local s = options.static_headers jbe@0: local t = {} jbe@0: if s then jbe@0: if type(s) == "string" then jbe@0: for line in string.gmatch(s, "[^\r\n]+") do jbe@0: t[#t+1] = line jbe@0: end jbe@0: else jbe@0: for i, kv in ipairs(options.static_headers) do jbe@0: if type(kv) == "string" then jbe@0: t[#t+1] = kv jbe@0: else jbe@0: t[#t+1] = kv[1] .. ": " .. kv[2] jbe@0: end jbe@0: end jbe@0: end jbe@0: end jbe@0: t[#t+1] = "" jbe@154: self._preamble = table.concat(t, "\r\n") -- preamble sent with every(!) HTTP response jbe@115: end jbe@154: self._input_chunk_size = options.maximum_input_chunk_size or options.chunk_size or 16384 jbe@154: self._output_chunk_size = options.minimum_output_chunk_size or options.chunk_size or 1024 jbe@154: self._header_size_limit = options.header_size_limit or 1024*1024 jbe@156: self._body_size_limit = options.body_size_limit or 64*1024*1024 jbe@154: local function init_timeout(name, default) jbe@154: local value = options[name] jbe@154: if value == nil then jbe@154: self["_"..name] = default jbe@154: else jbe@155: self["_"..name] = value jbe@154: end jbe@53: end jbe@154: init_timeout("request_idle_timeout", 330) jbe@154: init_timeout("request_header_timeout", 30) jbe@154: init_timeout("request_body_timeout", 1800) jbe@154: init_timeout("response_timeout", 1830) jbe@154: self._poll = options.poll_function or moonbridge_io.poll jbe@154: self:_create_closure("_write_yield") jbe@154: self:_create_closure("_handler") jbe@155: self:_create_header_metatables() jbe@155: end jbe@155: jbe@155: function request_pt:_create_closure(name) jbe@155: self[name.."_closure"] = function(...) jbe@155: return self[name](self, ...) jbe@155: end jbe@155: end jbe@155: jbe@154: function request_pt:_handler(socket) jbe@154: self._socket = socket jbe@154: self._survive = true jbe@154: self._socket_set = {[socket] = true} jbe@154: self._faulty = false jbe@159: self._state = "config" jbe@155: self._connection_close_requested = false jbe@155: self._connection_close_responded = false jbe@159: for name, mt in pairs(headers_mts) do jbe@159: local tbl = setmetatable({}, mt) jbe@159: headers_mt_self[tbl] = self jbe@159: self[name] = tbl jbe@159: end jbe@154: repeat jbe@154: -- wait for input: jbe@159: if not self._poll(self._socket_set, nil, self._request_idle_timeout) then jbe@154: self:_error("408 Request Timeout", "Idle connection timed out") jbe@154: return self._survive jbe@154: end jbe@154: -- read headers (with timeout): jbe@154: do jbe@154: local coro = coroutine.wrap(self._read_headers) jbe@154: local timeout = self._request_header_timeout jbe@154: local starttime = timeout and moonbridge_io.timeref() jbe@154: while true do jbe@154: local status = coro(self) jbe@154: if status == nil then jbe@154: local remaining jbe@154: if timeout then jbe@154: remaining = timeout - moonbridge_io.timeref(starttime) jbe@0: end jbe@154: if not self._poll(self._socket_set, nil, remaining) then jbe@154: self:_error("408 Request Timeout", "Timeout while receiving headers") jbe@154: return self._survive jbe@0: end jbe@154: elseif status == false then jbe@154: return self._survive jbe@154: elseif status == true then jbe@154: break jbe@154: else jbe@154: error("Unexpected yield value") jbe@0: end jbe@0: end jbe@154: end jbe@157: -- prepare reading of body: jbe@159: self._read_body_coro = coroutine.wrap(self._read_body) --TODO? jbe@157: -- set timeout for application handler: jbe@155: timeout(self._response_timeout or 0) jbe@157: -- call application handler: jbe@155: if self._application_handler(self) ~= true then jbe@155: self._survive = false jbe@155: end jbe@157: -- enforce request:finish() jbe@155: request:finish() jbe@157: -- reset timeout of application handler jbe@155: timeout(0) jbe@155: until self._connection_close_responded jbe@155: return self._survive jbe@154: end jbe@154: jbe@159: function request_pt:_prepare_body() jbe@159: self:_assert_not_faulty() jbe@159: if self._state == "prepare" then jbe@159: error("Unexpected state in HTTP module") jbe@159: elseif self._state ~= "config" then jbe@159: return jbe@159: end jbe@159: self._state = "prepare" jbe@159: local content_type = self.headers_value["Content-Type"] jbe@159: if content_type then jbe@159: if jbe@159: content_type == "application/x-www-form-urlencoded" or jbe@159: string.match(content_type, "^application/x%-www%-form%-urlencoded *;") jbe@159: then jbe@159: self._consume_all_input() jbe@159: self.post_params_list = read_urlencoded_form(self.body) jbe@159: else jbe@159: local boundary = string.match( jbe@159: content_type, jbe@159: '^multipart/form%-data[ \t]*[;,][ \t]*boundary="([^"]+)"$' jbe@159: ) or string.match( jbe@159: content_type, jbe@159: '^multipart/form%-data[ \t]*[;,][ \t]*boundary=([^"; \t]+)$' jbe@159: ) jbe@159: if boundary then jbe@159: self.post_metadata_list = {} jbe@159: boundary = "--" .. boundary jbe@159: local headerdata = "" jbe@159: local streamer jbe@159: local field_name jbe@159: local metadata = {} jbe@159: local value_parts jbe@159: local function default_streamer(chunk) jbe@159: value_parts[#value_parts+1] = chunk jbe@159: end jbe@159: local function stream_part_finish() jbe@159: if streamer == default_streamer then jbe@159: local value = table.concat(value_parts) jbe@159: value_parts = nil jbe@159: if field_name then jbe@159: local values = self.post_params_list[field_name] jbe@159: values[#values+1] = value jbe@159: local metadata_entries = post_metadata_list[field_name] jbe@159: metadata_entries[#metadata_entries+1] = metadata jbe@159: end jbe@159: else jbe@159: streamer() jbe@159: end jbe@159: headerdata = "" jbe@159: streamer = nil jbe@159: field_name = nil jbe@159: metadata = {} jbe@159: end jbe@159: local function stream_part_chunk(chunk) jbe@159: if streamer then jbe@159: streamer(chunk) jbe@159: else jbe@159: headerdata = headerdata .. chunk jbe@159: while true do jbe@159: local line, remaining = string.match(headerdata, "^(.-)\r?\n(.*)$") jbe@159: if not line then jbe@159: break jbe@159: end jbe@159: if line == "" then jbe@159: streamer = streamed_post_params[field_name] jbe@159: if not streamer then jbe@159: for i, rule in ipairs(streamed_post_param_patterns) do jbe@159: if string.match(field_name, rule[1]) then jbe@159: streamer = rule[2] jbe@159: break jbe@159: end jbe@159: end jbe@159: end jbe@159: if not streamer then jbe@159: value_parts = {} jbe@159: streamer = default_streamer jbe@159: end jbe@159: streamer(remaining, field_name, metadata) jbe@159: return jbe@159: end jbe@159: headerdata = remaining jbe@159: local header_key, header_value = string.match(line, "^([^:]*):[ \t]*(.-)[ \t]*$") jbe@159: if not header_key then jbe@159: request_error(true, "400 Bad Request", "Invalid header in multipart/form-data part") jbe@159: end jbe@159: header_key = string.lower(header_key) jbe@159: if header_key == "content-disposition" then jbe@159: local escaped_header_value = string.gsub(header_value, '"[^"]*"', function(str) jbe@159: return string.gsub(str, "=", "==") jbe@159: end) jbe@159: field_name = string.match(escaped_header_value, ';[ \t]*name="([^"]*)"') jbe@159: if field_name then jbe@159: field_name = string.gsub(field_name, "==", "=") jbe@159: else jbe@159: field_name = string.match(header_value, ';[ \t]*name=([^"; \t]+)') jbe@159: end jbe@159: metadata.file_name = string.match(escaped_header_value, ';[ \t]*filename="([^"]*)"') jbe@159: if metadata.file_name then jbe@159: metadata.file_name = string.gsub(metadata.file_name, "==", "=") jbe@159: else jbe@159: string.match(header_value, ';[ \t]*filename=([^"; \t]+)') jbe@159: end jbe@159: elseif header_key == "content-type" then jbe@159: metadata.content_type = header_value jbe@159: elseif header_key == "content-transfer-encoding" then jbe@159: request_error(true, "400 Bad Request", "Content-transfer-encoding not supported by multipart/form-data parser") jbe@159: end jbe@159: end jbe@159: end jbe@159: end jbe@159: local skippart = true -- ignore data until first boundary jbe@159: local afterbound = false -- interpret 2 bytes after boundary ("\r\n" or "--") jbe@159: local terminated = false -- final boundary read jbe@159: local bigchunk = "" jbe@159: request:stream_request_body(function(chunk) jbe@159: if terminated then jbe@159: return jbe@159: end jbe@159: bigchunk = bigchunk .. chunk jbe@159: while true do jbe@159: if afterbound then jbe@159: if #bigchunk <= 2 then jbe@159: return jbe@159: end jbe@159: local terminator = string.sub(bigchunk, 1, 2) jbe@159: if terminator == "\r\n" then jbe@159: afterbound = false jbe@159: bigchunk = string.sub(bigchunk, 3) jbe@159: elseif terminator == "--" then jbe@159: terminated = true jbe@159: bigchunk = nil jbe@159: return jbe@159: else jbe@159: request_error(true, "400 Bad Request", "Error while parsing multipart body (expected CRLF or double minus)") jbe@159: end jbe@159: end jbe@159: local pos1, pos2 = string.find(bigchunk, boundary, 1, true) jbe@159: if not pos1 then jbe@159: if not skippart then jbe@159: local safe = #bigchunk-#boundary jbe@159: if safe > 0 then jbe@159: stream_part_chunk(string.sub(bigchunk, 1, safe)) jbe@159: bigchunk = string.sub(bigchunk, safe+1) jbe@159: end jbe@159: end jbe@159: return jbe@159: end jbe@159: if not skippart then jbe@159: stream_part_chunk(string.sub(bigchunk, 1, pos1 - 1)) jbe@159: stream_part_finish() jbe@159: else jbe@159: boundary = "\r\n" .. boundary jbe@159: skippart = false jbe@159: end jbe@159: bigchunk = string.sub(bigchunk, pos2 + 1) jbe@159: afterbound = true jbe@159: end jbe@159: end) jbe@159: if not terminated then jbe@159: request_error(true, "400 Bad Request", "Premature end of multipart/form-data request body") jbe@159: end jbe@159: request.post_metadata_list, request.post_metadata = post_metadata_list, post_metadata jbe@159: else jbe@159: request_error(true, "415 Unsupported Media Type", "Unknown Content-Type of request body") jbe@159: end jbe@159: end jbe@159: end jbe@159: self.post_params = get_first_values(self.post_params_list) jbe@159: self._state = "no_status_sent" jbe@159: end jbe@159: jbe@157: function request_pt:_drain_input() jbe@157: self._read_body_coro = "drain" jbe@157: end jbe@157: jbe@157: function request_pt:_consume_some_input() jbe@157: local coro = self._read_body_coro jbe@157: if coro == "drain" then jbe@157: local bytes, status = self._socket:drain_nb(self._input_chunk_size) jbe@157: if status == "eof" then jbe@157: coro = nil jbe@157: end jbe@157: elseif coro then jbe@157: local retval = coro(self) jbe@157: if retval ~= nil then jbe@157: coro = nil -- can't consume more data jbe@157: end jbe@157: end jbe@157: end jbe@157: jbe@157: function request_pt:_consume_all_input() jbe@157: while self._read_body_coro do jbe@157: self._poll(socket_set) jbe@157: self:_consume_some_input() jbe@157: end jbe@157: end jbe@157: jbe@154: function request_pt:_error(status, explanation) jbe@154: end jbe@154: jbe@154: function request_pt:_read(...) jbe@154: local line, status = self._socket:read_yield(...) jbe@154: if line == nil then jbe@154: self._faulty = true jbe@154: error(status) jbe@154: else jbe@154: return line, status jbe@154: end jbe@154: end jbe@154: jbe@154: function request_pt:_read_headers() jbe@154: local remaining = self._header_size_limit jbe@154: -- read and parse request line: jbe@154: local target, proto jbe@154: do jbe@154: local line, status = self:_read(remaining-2, "\n") jbe@154: if status == "maxlen" then jbe@154: self:_error("414 Request-URI Too Long") jbe@154: return false jbe@154: elseif status == "eof" then jbe@154: if line ~= "" then jbe@154: self:_error("400 Bad Request", "Unexpected EOF in request-URI line") jbe@115: end jbe@154: return false jbe@154: end jbe@154: remaining = remaining - #line jbe@154: self.method, target, proto = jbe@154: line:match("^([^ \t\r]+)[ \t]+([^ \t\r]+)[ \t]*([^ \t\r]*)[ \t]*\r?\n$") jbe@154: if not request.method then jbe@154: self:_error("400 Bad Request", "Invalid request-URI line") jbe@154: return false jbe@154: elseif proto ~= "HTTP/1.1" then jbe@154: self:_error("505 HTTP Version Not Supported") jbe@154: return false jbe@154: end jbe@154: end jbe@154: -- read and parse headers: jbe@157: self._headers = {} jbe@157: self._headers_value_nil = {} jbe@154: while true do jbe@154: local line, status = self:_read(remaining, "\n"); jbe@154: if status == "maxlen" then jbe@154: self:_error("431 Request Header Fields Too Large") jbe@154: return false jbe@154: elseif status == "eof" then jbe@154: self:_error("400 Bad Request", "Unexpected EOF in request headers") jbe@154: return false jbe@154: end jbe@154: remaining = remaining - #line jbe@154: if line == "\r\n" or line == "\n" then jbe@154: break jbe@154: end jbe@154: local key, value = string.match(line, "^([^ \t\r]+):[ \t]*(.-)[ \t]*\r?\n$") jbe@154: if not key then jbe@154: self:_error("400 Bad Request", "Invalid header line") jbe@154: return false jbe@154: end jbe@154: local lowerkey = key:lower() jbe@154: local values = self._headers[lowerkey] jbe@154: if values then jbe@154: values[#values+1] = value jbe@154: else jbe@154: self._headers[lowerkey] = {value} jbe@154: end jbe@154: end jbe@154: -- process "Connection: close" header if existent: jbe@154: self._connection_close_requested = self.headers_flags["Connection"]["close"] jbe@154: -- process "Content-Length" header if existent: jbe@154: do jbe@154: local values = self.headers_csv_table["Content-Length"] jbe@154: if #values > 0 then jbe@154: self._request_body_content_length = tonumber(values[1]) jbe@154: local proper_value = tostring(request_body_content_length) jbe@154: for i, value in ipairs(values) do jbe@154: value = string.match(value, "^0*(.*)") jbe@154: if value ~= proper_value then jbe@154: self:_error("400 Bad Request", "Content-Length header(s) invalid") jbe@154: return false jbe@0: end jbe@38: end jbe@154: if request_body_content_length > self._body_size_limit then jbe@154: self:_error("413 Request Entity Too Large", "Announced request body size is too big") jbe@154: return false jbe@38: end jbe@154: end jbe@154: end jbe@154: -- process "Transfer-Encoding" header if existent: jbe@154: do jbe@154: local flag = self.headers_flags["Transfer-Encoding"]["chunked"] jbe@154: local list = self.headers_csv_table["Transfer-Encoding"] jbe@154: if (flag and #list ~= 1) or (not flag and #list ~= 0) then jbe@154: self:_error("400 Bad Request", "Unexpected Transfer-Encoding") jbe@154: return false jbe@154: end jbe@154: end jbe@154: -- process "Expect" header if existent: jbe@154: for i, value in ipairs(self.headers_csv_table["Expect"]) do jbe@154: if string.lower(value) ~= "100-continue" then jbe@154: self:_error("417 Expectation Failed", "Unexpected Expect header") jbe@154: return false jbe@154: end jbe@154: end jbe@154: -- get mandatory Host header according to RFC 7230: jbe@154: self.host = self.headers_value["Host"] jbe@154: if not self.host then jbe@154: self:_error("400 Bad Request", "No valid host header") jbe@154: return false jbe@154: end jbe@154: -- parse request target: jbe@154: self.path, self.query = string.match(target, "^/([^?]*)(.*)$") jbe@154: if not self.path then jbe@154: local host2 jbe@154: host2, self.path, self.query = string.match(target, "^[Hh][Tt][Tt][Pp]://([^/?]+)/?([^?]*)(.*)$") jbe@154: if host2 then jbe@154: if self.host ~= host2 then jbe@154: self:_error("400 Bad Request", "No valid host header") jbe@154: return false jbe@38: end jbe@154: elseif not (target == "*" and self.method == "OPTIONS") then jbe@154: self:_error("400 Bad Request", "Invalid request target") jbe@157: return false jbe@154: end jbe@154: end jbe@154: -- parse GET params: jbe@154: if self.query then jbe@154: self.get_params_list = read_urlencoded_form(request.query) jbe@154: self.get_params = get_first_values(self.get_params_list) jbe@154: end jbe@154: -- parse cookies: jbe@157: self.cookies = {} jbe@154: for i, line in ipairs(self.headers["Cookie"]) do jbe@154: for rawkey, rawvalue in jbe@154: string.gmatch(line, "([^=; ]*)=([^=; ]*)") jbe@154: do jbe@154: self.cookies[decode_uri(rawkey)] = decode_uri(rawvalue) jbe@154: end jbe@0: end jbe@157: -- indicate success: jbe@157: return true jbe@0: end jbe@0: jbe@156: function request_pt:_read_body() jbe@156: local remaining = self._body_size_limit jbe@156: if request.headers_flags["Transfer-Encoding"]["chunked"] then jbe@156: while true do jbe@156: local line, status = self:_read(32 + remaining, "\n") jbe@156: if status == "maxlen" then jbe@156: self:_error("400 Bad Request", "Request body size limit exceeded") jbe@156: return false jbe@156: elseif status == "eof" then jbe@156: self:_error("400 Bad Request", "Encoding error or unexpected EOF while reading next chunk of request body") jbe@156: return false jbe@156: end jbe@156: local zeros, lenstr = string.match(line, "^(0*)([1-9A-Fa-f]+[0-9A-Fa-f]*)\r?\n$") jbe@156: local chunkext jbe@156: if lenstr then jbe@156: chunkext = "" jbe@156: else jbe@156: zeros, lenstr, chunkext = string.match(line, "^(0*)([1-9A-Fa-f]+[0-9A-Fa-f]*)([ \t;].-)\r?\n$") jbe@156: end jbe@156: if not lenstr or #lenstr > 13 then jbe@156: self:_error("400 Bad Request", "Encoding error while reading chunk of request body") jbe@156: return false jbe@156: end jbe@156: local len = tonumber("0x" .. lenstr) jbe@156: remaining = remaining - (#zeros + #chunkext + len) jbe@156: if remaining < 0 then jbe@156: self:_error("400 Bad Request", "Request body size limit exceeded") jbe@156: return false jbe@156: end jbe@156: if len == 0 then break end jbe@156: if self:_read_body_bytes(len) == false then jbe@156: return false jbe@156: end jbe@156: local term, status = self:_read(2, "\n") jbe@156: if status == "eof" then jbe@156: self:_error("400 Bad Request", "Unexpected EOF while reading next chunk of request body") jbe@156: return false jbe@156: end jbe@156: if term ~= "\r\n" and term ~= "\n" then jbe@156: self:_error("400 Bad Request", "Encoding error while reading chunk of request body") jbe@156: return false jbe@156: end jbe@156: end jbe@156: while true do jbe@156: local line, status = self:_read(2 + remaining, "\n") jbe@156: if status == "eof" then jbe@156: self:_error("400 Bad Request", "Unexpected EOF while reading chunk of request body") jbe@156: return false jbe@156: end jbe@156: if line == "\r\n" or line == "\n" then break end jbe@156: remaining = remaining - #line jbe@156: if remaining < 0 then jbe@157: self:_error("413 Request Entity Too Large", "Request body size limit exceeded while reading trailer section of chunked request body") jbe@157: return false jbe@156: end jbe@156: end jbe@156: elseif request_body_content_length then jbe@156: if self._read_body_bytes(request_body_content_length) == false then jbe@156: return false jbe@156: end jbe@156: end jbe@157: -- indicate success: jbe@157: return true jbe@156: end jbe@156: jbe@156: function request_pt:_read_body_bytes(remaining, callback) jbe@156: while remaining > 0 do jbe@156: local limit jbe@156: if remaining > self._input_chunk_size then jbe@156: limit = self._input_chunk_size jbe@156: else jbe@156: limit = remaining jbe@156: end jbe@156: local chunk, status = self:_read(limit) jbe@156: if status == "eof" then jbe@156: self:_error("400 Bad Request", "Unexpected EOF while reading chunk of request body") jbe@156: return false jbe@156: end jbe@156: remaining = remaining - limit jbe@156: if self._body_streamer then jbe@156: self._body_streamer(chunk) jbe@156: end jbe@156: end jbe@157: return true jbe@156: end jbe@156: jbe@154: function request_pt:_assert_not_faulty() jbe@154: assert(not self._faulty, "Tried to use faulty request handle") jbe@154: end jbe@154: jbe@154: function request_pt:_write_yield() jbe@157: self:_consume_some_input() jbe@154: self._poll(self._socket_set, self._socket_set) jbe@154: end jbe@154: jbe@154: function request_pt:_write(...) jbe@154: assert(self._socket:write_call(self._write_yield_closure, ...)) jbe@154: end jbe@154: jbe@154: function request_pt:_flush(...) jbe@154: assert(self._socket:write_call(self._write_yield_closure, ...)) jbe@154: end jbe@154: jbe@154: -- function creating a HTTP handler: jbe@154: function generate_handler(handler, options) jbe@154: -- swap arguments if necessary (for convenience): jbe@154: if type(handler) ~= "function" and type(options) == "function" then jbe@154: handler, options = options, handler jbe@154: end jbe@154: local request = setmetatable({}, request_mt) jbe@154: request:_init(handler, options) jbe@154: return request._handler_closure jbe@154: end jbe@154: jbe@0: return _M jbe@0: