# HG changeset patch # User jbe # Date 1433526821 -7200 # Node ID bd7225b303913b585aaf4e6e64e804b3c404b003 # Parent 99a70d18e47c4f6dc231cc815e33acf18a0c1e45 Further work on new HTTP layer (not finished) diff -r 99a70d18e47c -r bd7225b30391 moonbridge_http.lua --- a/moonbridge_http.lua Wed May 27 01:51:04 2015 +0200 +++ b/moonbridge_http.lua Fri Jun 05 19:53:41 2015 +0200 @@ -108,6 +108,98 @@ return newtbl end +local headers_mt_self = setmetatable({}, {__mode="k"}) + +local headers_mts = { + headers_mt = { + __index = function(tbl, key) + local self = headers_mt_self[tbl] + local lowerkey = string.lower(key) + local result = self._headers[lowerkey] + if result == nil then + result = {} + end + tbl[lowerkey] = result + tbl[key] = result + return result + end + }, + -- table mapping header field names to value-lists + -- (for headers with comma separated values): + headers_csv_table = { + __index = function(tbl, key) + local self = headers_mt_self[tbl] + local result = {} + for i, line in ipairs(self.headers[key]) do + for entry in string.gmatch(line, "[^,]+") do + local value = string.match(entry, "^[ \t]*(..-)[ \t]*$") + if value then + result[#result+1] = value + end + end + end + tbl[key] = result + return result + end + }, + -- table mapping header field names to a comma separated string + -- (for headers with comma separated values): + headers_csv_string = { + __index = function(tbl, key) + local self = headers_mt_self[tbl] + local result = {} + for i, line in ipairs(self.headers[key]) do + result[#result+1] = line + end + result = string.concat(result, ", ") + tbl[key] = result + return result + end + }, + -- table mapping header field names to a single string value + -- (or false if header has been sent multiple times): + headers_value = { + __index = function(tbl, key) + local self = headers_mt_self[tbl] + if self._headers_value_nil[key] then + return nil + end + local result = nil + local values = self.headers_csv_table[key] + if #values == 0 then + self._headers_value_nil[key] = true + elseif #values == 1 then + result = values[1] + else + result = false + end + tbl[key] = result + return result + end + }, + -- table mapping header field names to a flag table, + -- indicating if the comma separated value contains certain entries: + headers_flags = { + __index = function(tbl, key) + local self = headers_mt_self[tbl] + local result = setmetatable({}, { + __index = function(tbl, key) + local lowerkey = string.lower(key) + local result = rawget(tbl, lowerkey) or false + tbl[lowerkey] = result + tbl[key] = result + return result + end + }) + for i, value in ipairs(self.headers_csv_table[key]) do + result[string.lower(value)] = true + end + tbl[key] = result + return result + end + } +} + request_pt = {} request_mt = { __index = request_pt } @@ -165,111 +257,22 @@ end end -function request_pt:_create_header_metatables() - -- table mapping header field names to value-lists: - self._headers_mt = { - __index = function(tbl, key) - local lowerkey = string.lower(key) - local result = self._headers[lowerkey] - if result == nil then - result = {} - end - tbl[lowerkey] = result - tbl[key] = result - return result - end - } - -- table mapping header field names to value-lists - -- (for headers with comma separated values): - self._headers_csv_table_mt = { - __index = function(tbl, key) - local result = {} - for i, line in ipairs(self.headers[key]) do - for entry in string.gmatch(line, "[^,]+") do - local value = string.match(entry, "^[ \t]*(..-)[ \t]*$") - if value then - result[#result+1] = value - end - end - end - tbl[key] = result - return result - end - } - -- table mapping header field names to a comma separated string - -- (for headers with comma separated values): - self._headers_csv_string_mt = { - __index = function(tbl, key) - local result = {} - for i, line in ipairs(self.headers[key]) do - result[#result+1] = line - end - result = string.concat(result, ", ") - tbl[key] = result - return result - end - } - -- table mapping header field names to a single string value - -- (or false if header has been sent multiple times): - self._headers_value_mt = { - __index = function(tbl, key) - if self._headers_value_nil[key] then - return nil - end - local result = nil - local values = self.headers_csv_table[key] - if #values == 0 then - self._headers_value_nil[key] = true - elseif #values == 1 then - result = values[1] - else - result = false - end - tbl[key] = result - return result - end - } - -- table mapping header field names to a flag table, - -- indicating if the comma separated value contains certain entries: - self._headers_flags_mt = { - __index = function(tbl, key) - local result = setmetatable({}, { - __index = function(tbl, key) - local lowerkey = string.lower(key) - local result = rawget(tbl, lowerkey) or false - tbl[lowerkey] = result - tbl[key] = result - return result - end - }) - for i, value in ipairs(self.headers_csv_table[key]) do - result[string.lower(value)] = true - end - tbl[key] = result - return result - end - } -end - -function request_pt:_create_magictable(name) - self[name] = setmetatable({}, self["_"..name.."_mt"]) -end - function request_pt:_handler(socket) self._socket = socket self._survive = true self._socket_set = {[socket] = true} self._faulty = false + self._state = "config" self._connection_close_requested = false self._connection_close_responded = false - self:_create_magictable("headers") - self:_create_magictable("headers_csv_table") - self:_create_magictable("headers_csv_string") - self:_create_magictable("headers_value") - self:_create_magictable("headers_flags") + for name, mt in pairs(headers_mts) do + local tbl = setmetatable({}, mt) + headers_mt_self[tbl] = self + self[name] = tbl + end repeat -- wait for input: - if not moonbridge_io.poll(self._socket_set, nil, self._request_idle_timeout) then + if not self._poll(self._socket_set, nil, self._request_idle_timeout) then self:_error("408 Request Timeout", "Idle connection timed out") return self._survive end @@ -299,7 +302,7 @@ end end -- prepare reading of body: - self._read_body_coro = coroutine.wrap(self._read_body) + self._read_body_coro = coroutine.wrap(self._read_body) --TODO? -- set timeout for application handler: timeout(self._response_timeout or 0) -- call application handler: @@ -314,6 +317,177 @@ return self._survive end +function request_pt:_prepare_body() + self:_assert_not_faulty() + if self._state == "prepare" then + error("Unexpected state in HTTP module") + elseif self._state ~= "config" then + return + end + self._state = "prepare" + local content_type = self.headers_value["Content-Type"] + if content_type then + if + content_type == "application/x-www-form-urlencoded" or + string.match(content_type, "^application/x%-www%-form%-urlencoded *;") + then + self._consume_all_input() + self.post_params_list = read_urlencoded_form(self.body) + else + local boundary = string.match( + content_type, + '^multipart/form%-data[ \t]*[;,][ \t]*boundary="([^"]+)"$' + ) or string.match( + content_type, + '^multipart/form%-data[ \t]*[;,][ \t]*boundary=([^"; \t]+)$' + ) + if boundary then + self.post_metadata_list = {} + boundary = "--" .. boundary + local headerdata = "" + local streamer + local field_name + local metadata = {} + local value_parts + local function default_streamer(chunk) + value_parts[#value_parts+1] = chunk + end + local function stream_part_finish() + if streamer == default_streamer then + local value = table.concat(value_parts) + value_parts = nil + if field_name then + local values = self.post_params_list[field_name] + values[#values+1] = value + local metadata_entries = post_metadata_list[field_name] + metadata_entries[#metadata_entries+1] = metadata + end + else + streamer() + end + headerdata = "" + streamer = nil + field_name = nil + metadata = {} + end + local function stream_part_chunk(chunk) + if streamer then + streamer(chunk) + else + headerdata = headerdata .. chunk + while true do + local line, remaining = string.match(headerdata, "^(.-)\r?\n(.*)$") + if not line then + break + end + if line == "" then + streamer = streamed_post_params[field_name] + if not streamer then + for i, rule in ipairs(streamed_post_param_patterns) do + if string.match(field_name, rule[1]) then + streamer = rule[2] + break + end + end + end + if not streamer then + value_parts = {} + streamer = default_streamer + end + streamer(remaining, field_name, metadata) + return + end + headerdata = remaining + local header_key, header_value = string.match(line, "^([^:]*):[ \t]*(.-)[ \t]*$") + if not header_key then + request_error(true, "400 Bad Request", "Invalid header in multipart/form-data part") + end + header_key = string.lower(header_key) + if header_key == "content-disposition" then + local escaped_header_value = string.gsub(header_value, '"[^"]*"', function(str) + return string.gsub(str, "=", "==") + end) + field_name = string.match(escaped_header_value, ';[ \t]*name="([^"]*)"') + if field_name then + field_name = string.gsub(field_name, "==", "=") + else + field_name = string.match(header_value, ';[ \t]*name=([^"; \t]+)') + end + metadata.file_name = string.match(escaped_header_value, ';[ \t]*filename="([^"]*)"') + if metadata.file_name then + metadata.file_name = string.gsub(metadata.file_name, "==", "=") + else + string.match(header_value, ';[ \t]*filename=([^"; \t]+)') + end + elseif header_key == "content-type" then + metadata.content_type = header_value + elseif header_key == "content-transfer-encoding" then + request_error(true, "400 Bad Request", "Content-transfer-encoding not supported by multipart/form-data parser") + end + end + end + end + local skippart = true -- ignore data until first boundary + local afterbound = false -- interpret 2 bytes after boundary ("\r\n" or "--") + local terminated = false -- final boundary read + local bigchunk = "" + request:stream_request_body(function(chunk) + if terminated then + return + end + bigchunk = bigchunk .. chunk + while true do + if afterbound then + if #bigchunk <= 2 then + return + end + local terminator = string.sub(bigchunk, 1, 2) + if terminator == "\r\n" then + afterbound = false + bigchunk = string.sub(bigchunk, 3) + elseif terminator == "--" then + terminated = true + bigchunk = nil + return + else + request_error(true, "400 Bad Request", "Error while parsing multipart body (expected CRLF or double minus)") + end + end + local pos1, pos2 = string.find(bigchunk, boundary, 1, true) + if not pos1 then + if not skippart then + local safe = #bigchunk-#boundary + if safe > 0 then + stream_part_chunk(string.sub(bigchunk, 1, safe)) + bigchunk = string.sub(bigchunk, safe+1) + end + end + return + end + if not skippart then + stream_part_chunk(string.sub(bigchunk, 1, pos1 - 1)) + stream_part_finish() + else + boundary = "\r\n" .. boundary + skippart = false + end + bigchunk = string.sub(bigchunk, pos2 + 1) + afterbound = true + end + end) + if not terminated then + request_error(true, "400 Bad Request", "Premature end of multipart/form-data request body") + end + request.post_metadata_list, request.post_metadata = post_metadata_list, post_metadata + else + request_error(true, "415 Unsupported Media Type", "Unknown Content-Type of request body") + end + end + end + self.post_params = get_first_values(self.post_params_list) + self._state = "no_status_sent" +end + function request_pt:_drain_input() self._read_body_coro = "drain" end