# HG changeset patch # User jbe # Date 1420840285 -3600 # Node ID 583e2ad140dc21bb3df44e28a0fd66b8a28da60f # Parent 3b3f0ecc4ac42c8fc99f2314d43e9abc1152bb5d Renamed "http" module to "moonbridge_http" diff -r 3b3f0ecc4ac4 -r 583e2ad140dc example_application.lua --- a/example_application.lua Thu Jan 08 20:55:56 2015 +0100 +++ b/example_application.lua Fri Jan 09 22:51:25 2015 +0100 @@ -1,7 +1,7 @@ -- Moonbridge example application -- invoke with ./moonbridge example_application.lua -local http = require "http" +local http = require "moonbridge_http" local documents = {"example_webpage.html", "example_webpage.css"} diff -r 3b3f0ecc4ac4 -r 583e2ad140dc http.lua --- a/http.lua Thu Jan 08 20:55:56 2015 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,883 +0,0 @@ -#!/usr/bin/env lua - --- module preamble -local _G, _M = _ENV, {} -_ENV = setmetatable({}, { - __index = function(self, key) - local value = _M[key]; if value ~= nil then return value end - return _G[key] - end, - __newindex = function(self, key, value) _M[key] = value end -}) - --- function that encodes certain HTML entities: --- (not used by the library itself) -function encode_html(text) - return ( - string.gsub( - text, '[<>&"]', - function(char) - if char == '<' then - return "<" - elseif char == '>' then - return ">" - elseif char == '&' then - return "&" - elseif char == '"' then - return """ - end - end - ) - ) - -end - --- function that encodes special characters for URIs: --- (not used by the library itself) -function encode_uri(text) - return ( - string.gsub(text, "[^0-9A-Za-z_%.~-]", - function (char) - return string.format("%%%02x", string.byte(char)) - end - ) - ) -end - --- function undoing URL encoding: -do - local b0 = string.byte("0") - local b9 = string.byte("9") - local bA = string.byte("A") - local bF = string.byte("F") - local ba = string.byte("a") - local bf = string.byte("f") - function decode_uri(str) - return ( - string.gsub( - string.gsub(str, "%+", " "), - "%%([0-9A-Fa-f][0-9A-Fa-f])", - function(hex) - local n1, n2 = string.byte(hex, 1, 2) - if n1 >= b0 and n1 <= b9 then n1 = n1 - b0 - elseif n1 >= bA and n1 <= bF then n1 = n1 - bA + 10 - elseif n1 >= ba and n1 <= bf then n1 = n1 - ba + 10 - else error("Assertion failed") end - if n2 >= b0 and n2 <= b9 then n2 = n2 - b0 - elseif n2 >= bA and n2 <= bF then n2 = n2 - bA + 10 - elseif n2 >= ba and n2 <= bf then n2 = n2 - ba + 10 - else error("Assertion failed") end - return string.char(n1 * 16 + n2) - end - ) - ) - end -end - --- status codes that carry no response body (in addition to 1xx): --- (set to "zero_content_length" if Content-Length header is required) -status_without_response_body = { - ["101"] = true, - ["204"] = true, - ["205"] = "zero_content_length", - ["304"] = true -} - --- handling of GET/POST param tables: -local new_params_list -- defined later -do - local params_list_mapping = setmetatable({}, {__mode="k"}) - local params_list_metatable = { - __index = function(self, key) - local tbl = {} - self[key] = tbl - return tbl - end - } - local params_metatable = { - __index = function(self, key) - local value = params_list_mapping[self][key][1] - self[key] = value - return value - end - } - -- returns a table to store key value-list pairs (i.e. multiple values), - -- and a second table automatically mapping keys to the first value - -- using the key value-list pairs in the first table: - new_params_list = function() - local params_list = setmetatable({}, params_list_metatable) - local params = setmetatable({}, params_metatable) - params_list_mapping[params] = params_list - return params_list, params - end -end --- parses URL encoded form data and stores it in --- a key value-list pairs structure that has to be --- previously obtained by calling by new_params_list(): -local function read_urlencoded_form(tbl, data) - for rawkey, rawvalue in string.gmatch(data, "([^=&]*)=([^=&]*)") do - local subtbl = tbl[decode_uri(rawkey)] - subtbl[#subtbl+1] = decode_uri(rawvalue) - end -end - --- function creating a HTTP handler: -function generate_handler(handler, options) - -- swap arguments if necessary (for convenience): - if type(handler) ~= "function" and type(options) == "function" then - handler, options = options, handler - end - -- process options: - options = options or {} - local preamble = "" -- preamble sent with every(!) HTTP response - do - -- named arg "static_headers" is used to create the preamble: - local s = options.static_headers - local t = {} - if s then - if type(s) == "string" then - for line in string.gmatch(s, "[^\r\n]+") do - t[#t+1] = line - end - else - for i, kv in ipairs(options.static_headers) do - if type(kv) == "string" then - t[#t+1] = kv - else - t[#t+1] = kv[1] .. ": " .. kv[2] - end - end - end - end - t[#t+1] = "" - preamble = table.concat(t, "\r\n") - end - -- return connect handler: - return function(socket) - local survive = true -- set to false if process shall be terminated later - while true do - -- desired chunk sizes: - local input_chunk_size = options.maximum_input_chunk_size or options.chunk_size or 16384 - local output_chunk_size = options.minimum_output_chunk_size or options.chunk_size or 1024 - -- process named arguments "request_header_size_limit" and "request_body_size_limit": - local remaining_header_size_limit = options.request_header_size_limit or 1024*1024 - local remaining_body_size_limit = options.request_body_size_limit or 64*1024*1024 - -- state variables for request handling: - local output_state = "no_status_sent" -- one of: - -- "no_status_sent" (initial state) - -- "info_status_sent" (1xx status code has been sent) - -- "bodyless_status_sent" (204/304 status code has been sent) - -- "status_sent" (regular status code has been sent) - -- "headers_sent" (headers have been terminated) - -- "finished" (request has been answered completely) - local content_length -- value of Content-Length header sent - local bytes_sent = 0 -- number of bytes sent if Content-Length is set - local chunk_parts = {} -- list of chunks to send - local chunk_bytes = 0 -- sum of lengths of chunks to send - local connection_close_requested = false - local connection_close_responded = false - local headers_value_nil = {} -- header values that are nil - local request_body_content_length -- Content-Length of request body - local input_state = "pending" -- one of: - -- "pending" (request body has not been processed yet) - -- "deferred" (request body processing is deferred) - -- "reading" (request body is currently being read) - -- "finished" (request body has been read) - local streamed_post_params = {} -- mapping from POST field name to stream function - local streamed_post_param_patterns = {} -- list of POST field pattern and stream function pairs - -- object passed to handler (with methods, GET/POST params, etc.): - local request - -- reads a number of bytes from the socket, - -- optionally feeding these bytes chunk-wise - -- into a callback function: - local function read_body_bytes(remaining, callback) - while remaining > 0 do - local limit - if remaining > input_chunk_size then - limit = input_chunk_size - else - limit = remaining - end - local chunk = socket:read(limit) - if not chunk or #chunk ~= limit then - error("Unexpected EOF while reading chunk of request body") - end - remaining = remaining - limit - if callback then - callback(chunk) - end - end - end - -- flushes or closes the socket (depending on - -- whether "Connection: close" header was sent): - local function finish_response() - if connection_close_responded then - -- close output stream: - socket.output:close() - -- wait for EOF of peer to avoid immediate TCP RST condition: - timeout(2, function() - while socket.input:read(input_chunk_size) do end - end) - -- fully close socket: - socket:close() - else - socket:flush() - request:stream_request_body() - end - end - -- writes out buffered chunks (without flushing the socket): - local function send_chunk() - if chunk_bytes > 0 then - socket:write(string.format("%x\r\n", chunk_bytes)) - for i = 1, #chunk_parts do - socket:write(chunk_parts[i]) - end - chunk_parts = {} - chunk_bytes = 0 - socket:write("\r\n") - end - end - -- terminate header section in response, optionally flushing: - -- (may be called multiple times unless response is finished) - local function finish_headers(flush) - if output_state == "no_status_sent" then - error("HTTP status has not been sent yet") - elseif output_state == "finished" then - error("Response has already been finished") - elseif output_state == "info_status_sent" then - socket:write("\r\n") - socket:flush() - output_state = "no_status_sent" - elseif output_state == "bodyless_status_sent" then - if connection_close_requested and not connection_close_responded then - request:send_header("Connection", "close") - end - socket:write("\r\n") - finish_response() - output_state = "finished" - elseif output_state == "status_sent" then - if not content_length then - socket:write("Transfer-Encoding: chunked\r\n") - end - if connection_close_requested and not connection_close_responded then - request:send_header("Connection", "close") - end - socket:write("\r\n") - if request.method == "HEAD" then - finish_response() - elseif flush then - socket:flush() - end - output_state = "headers_sent" - elseif output_state ~= "headers_sent" then - error("Unexpected internal status in HTTP engine") - end - end - -- create request object and set several functions and values: - request = { - -- allow raw socket access: - socket = socket, - -- parsed cookies: - cookies = {}, - -- send a HTTP response status (e.g. "200 OK"): - send_status = function(self, value) - if input_state == "pending" then - request:process_request_body() - end - if output_state == "info_status_sent" then - socket:write("\r\n") - socket:flush() - elseif output_state ~= "no_status_sent" then - error("HTTP status has already been sent") - end - local status1 = string.sub(value, 1, 1) - local status3 = string.sub(value, 1, 3) - socket:write("HTTP/1.1 ", value, "\r\n", preamble) - local without_response_body = status_without_response_body[status3] - if without_response_body then - output_state = "bodyless_status_sent" - if without_response_body == "zero_content_length" then - request:send_header("Content-Length", 0) - end - elseif status1 == "1" then - output_state = "info_status_sent" - else - output_state = "status_sent" - end - end, - -- send a HTTP response header - -- (key and value as separate args): - send_header = function(self, key, value) - if output_state == "no_status_sent" then - error("HTTP status has not been sent yet") - elseif - output_state ~= "info_status_sent" and - output_state ~= "bodyless_status_sent" and - output_state ~= "status_sent" - then - error("All HTTP headers have already been sent") - end - local key_lower = string.lower(key) - if key_lower == "content-length" then - if output_state == "info_status_sent" then - error("Cannot set Content-Length for informational status response") - end - local new_content_length = assert(tonumber(value), "Invalid content-length") - if content_length == nil then - content_length = new_content_length - elseif content_length == new_content_length then - return - else - error("Content-Length has been set multiple times with different values") - end - elseif key_lower == "connection" then - for entry in string.gmatch(string.lower(value), "[^,]+") do - if string.match(entry, "^[ \t]*close[ \t]*$") then - if output_state == "info_status_sent" then - error("Cannot set \"Connection: close\" for informational status response") - end - if connection_close_responded then - return - else - connection_close_responded = true - break - end - end - end - end - socket:write(key, ": ", value, "\r\n") - end, - -- method to finish and flush headers: - finish_headers = function() - finish_headers(true) - end, - -- send data for response body: - send_data = function(self, ...) - if output_state == "info_status_sent" then - error("No (non-informational) HTTP status has been sent yet") - elseif output_state == "bodyless_status_sent" then - error("Cannot send response data for body-less status message") - end - finish_headers(false) - if output_state ~= "headers_sent" then - error("Unexpected internal status in HTTP engine") - end - if request.method == "HEAD" then - return - end - for i = 1, select("#", ...) do - local str = tostring(select(i, ...)) - if #str > 0 then - if content_length then - local bytes_to_send = #str - if bytes_sent + bytes_to_send > content_length then - socket:write(string.sub(str, 1, content_length - bytes_sent)) - bytes_sent = content_length - error("Content length exceeded") - else - socket:write(str) - bytes_sent = bytes_sent + bytes_to_send - end - else - chunk_bytes = chunk_bytes + #str - chunk_parts[#chunk_parts+1] = str - end - end - end - if chunk_bytes >= output_chunk_size then - send_chunk() - end - end, - -- flush output buffer: - flush = function(self) - send_chunk() - socket:flush() - end, - -- finish response: - finish = function(self) - if output_state == "finished" then - return - elseif output_state == "info_status_sent" then - error("Informational HTTP response can be finished with :finish_headers() method") - end - finish_headers(false) - if output_state == "headers_sent" then - if request.method ~= "HEAD" then - if content_length then - if bytes_sent ~= content_length then - error("Content length not used") - end - else - send_chunk() - socket:write("0\r\n\r\n") - end - finish_response() - end - output_state = "finished" - elseif output_state ~= finished then - error("Unexpected internal status in HTTP engine") - end - end, - -- table mapping header field names to value-lists - -- (raw access): - headers = setmetatable({}, { - __index = function(self, key) - local lowerkey = string.lower(key) - if lowerkey == key then - return - end - local result = rawget(self, lowerkey) - if result == nil then - result = {} - end - self[lowerkey] = result - self[key] = result - return result - end - }), - -- table mapping header field names to value-lists - -- (for headers with comma separated values): - headers_csv_table = setmetatable({}, { - __index = function(self, key) - local result = {} - for i, line in ipairs(request.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 - self[key] = result - return result - end - }), - -- table mapping header field names to a comma separated string - -- (for headers with comma separated values): - headers_csv_string = setmetatable({}, { - __index = function(self, key) - local result = {} - for i, line in ipairs(request.headers[key]) do - result[#result+1] = line - end - result = string.concat(result, ", ") - self[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 = setmetatable({}, { - __index = function(self, key) - if headers_value_nil[key] then - return nil - end - local result = nil - local values = request.headers_csv_table[key] - if #values == 0 then - headers_value_nil[key] = true - elseif #values == 1 then - result = values[1] - else - result = false - end - self[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 = setmetatable({}, { - __index = function(self, key) - local result = setmetatable({}, { - __index = function(self, key) - local lowerkey = string.lower(key) - local result = rawget(self, lowerkey) or false - self[lowerkey] = result - self[key] = result - return result - end - }) - for i, value in ipairs(request.headers_csv_table[key]) do - result[string.lower(value)] = true - end - self[key] = result - return result - end - }), - -- register POST param stream handler for a single field name: - stream_post_param = function(self, field_name, callback) - if input_state == "inprogress" or input_state == "finished" then - error("Cannot register POST param streaming function if request body is already processed") - end - streamed_post_params[field_name] = callback - end, - -- register POST param stream handler for a field name pattern: - stream_post_params = function(self, pattern, callback) - if input_state == "inprogress" or input_state == "finished" then - error("Cannot register POST param streaming function if request body is already processed") - end - streamed_post_param_patterns[#streamed_post_param_patterns+1] = {pattern, callback} - end, - -- disables automatic request body processing on write - -- (use with caution): - defer_reading = function(self) - if input_state == "pending" then - input_state = "deferred" - end - end, - -- processes the request body and sets the request.post_params, - -- request.post_params_list, request.meta_post_params, and - -- request.meta_post_params_list values (can be called manually or - -- automatically if post_params are accessed or data is written out) - process_request_body = function(self) - if input_state == "finished" then - return - end - local post_params_list, post_params = new_params_list() - local content_type = request.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 - read_urlencoded_form(post_params_list, request.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 - local post_metadata_list, post_metadata = new_params_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 = 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 - error("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 - error("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 - error("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 - error("Premature end of multipart/form-data request body") - end - request.post_metadata_list, request.post_metadata = post_metadata_list, post_metadata - else - error("Unknown Content-Type of request body") - end - end - end - request.post_params_list, request.post_params = post_params_list, post_params - end, - -- stream request body to an (optional) callback function - -- without processing it otherwise: - stream_request_body = function(self, callback) - if input_state ~= "pending" and input_state ~= "deferred" then - if callback then - if input_state == "inprogress" then - error("Request body is already being processed") - else - error("Request body has already been processed") - end - end - return - end - input_state = "inprogress" - if request.headers_flags["Expect"]["100-continue"] then - request:send_status("100 Continue") - request:finish_headers() - end - if request.headers_flags["Transfer-Encoding"]["chunked"] then - while true do - local line = socket:readuntil("\n", 32 + remaining_body_size_limit) - if not line then - error("Unexpected EOF while reading next chunk of request body") - end - local zeros, lenstr = string.match(line, "^(0*)([1-9A-Fa-f]+[0-9A-Fa-f]*)\r?\n$") - local chunkext - if lenstr then - chunkext = "" - else - zeros, lenstr, chunkext = string.match(line, "^(0*)([1-9A-Fa-f]+[0-9A-Fa-f]*)([ \t;].-)\r?\n$") - end - if not lenstr or #lenstr > 13 then - error("Encoding error or unexpected EOF while reading chunk of request body") - end - local len = tonumber("0x" .. lenstr) - remaining_body_size_limit = remaining_body_size_limit - (#zeros + #chunkext + len) - if remaining_body_size_limit < 0 then - error("Request body size limit exceeded") - end - if len == 0 then break end - read_body_bytes(len, callback) - local term = socket:readuntil("\n", 2) - if term ~= "\r\n" and term ~= "\n" then - error("Encoding error while reading chunk of request body") - end - end - while true do - local line = socket:readuntil("\n", 2 + remaining_body_size_limit) - if line == "\r\n" or line == "\n" then break end - remaining_body_size_limit = remaining_body_size_limit - #line - if remaining_body_size_limit < 0 then - error("Request body size limit exceeded while reading trailer section of chunked request body") - end - end - elseif request_body_content_length then - read_body_bytes(request_body_content_length, callback) - end - input_state = "finished" - end - } - -- initialize tables for GET params in request object: - request.get_params_list, request.get_params = new_params_list() - -- add meta table to request object to allow access to "body" and POST params: - setmetatable(request, { - __index = function(self, key) - if key == "body" then - local chunks = {} - request:stream_request_body(function(chunk) - chunks[#chunks+1] = chunk - end) - self.body = table.concat(chunks) - return self.body - elseif - key == "post_params_list" or key == "post_params" or - key == "post_metadata_list" or key == "post_metadata" - then - request:process_request_body() - return request[key] - end - end - }) - -- low level HTTP error response (for malformed requests, etc.): - local function error_response(status, text) - request:send_status(status) - request:send_header("Content-Type", "text/plain") - if not connection_close_responded then - request:send_header("Connection", "close") - end - request:send_data(status, "\n") - if text then - request:send_data("\n", text, "\n") - end - request:finish() - return survive - end - -- read and parse request line: - local line = socket:readuntil("\n", remaining_header_size_limit) - if not line then return survive end - remaining_header_size_limit = remaining_header_size_limit - #line - if remaining_header_size_limit == 0 then - return error_response("413 Request Entity Too Large", "Request line too long") - end - local proto - request.method, request.url, proto = - line:match("^([^ \t\r]+)[ \t]+([^ \t\r]+)[ \t]*([^ \t\r]*)[ \t]*\r?\n$") - if not request.method then - return error_response("400 Bad Request") - elseif proto ~= "HTTP/1.1" then - return error_response("505 HTTP Version Not Supported") - else - -- read and parse headers: - while true do - local line = socket:readuntil("\n", remaining_header_size_limit); - remaining_header_size_limit = remaining_header_size_limit - #line - if not line then - return error_response("400 Bad Request") - end - if line == "\r\n" or line == "\n" then - break - end - if remaining_header_size_limit == 0 then - return error_response("413 Request Entity Too Large", "Headers too long") - end - local key, value = string.match(line, "^([^ \t\r]+):[ \t]*(.-)[ \t]*\r?\n$") - if not key then - return error_response("400 Bad Request") - end - local values = request.headers[key] - values[#values+1] = value - end - -- process "Connection: close" header if existent: - connection_close_requested = request.headers_flags["Connection"]["close"] - -- process "Content-Length" header if existent: - do - local values = request.headers_csv_table["Content-Length"] - if #values > 0 then - request_body_content_length = tonumber(values[1]) - local proper_value = tostring(request_body_content_length) - for i, value in ipairs(values) do - value = string.match(value, "^0*(.*)") - if value ~= proper_value then - return error_response("400 Bad Request", "Content-Length header(s) invalid") - end - end - if request_body_content_length > remaining_body_size_limit then - return error_response("413 Request Entity Too Large", "Request body too big") - end - end - end - -- process "Transfer-Encoding" header if existent: - do - local flag = request.headers_flags["Transfer-Encoding"]["chunked"] - local list = request.headers_csv_table["Transfer-Encoding"] - if (flag and #list ~= 1) or (not flag and #list ~= 0) then - return error_response("400 Bad Request", "Unexpected Transfer-Encoding") - end - end - -- process "Expect" header if existent: - for i, value in ipairs(request.headers_csv_table["Expect"]) do - if string.lower(value) ~= "100-continue" then - return error_response("417 Expectation Failed", "Unexpected Expect header") - end - end - -- parse GET params: - request.path, request.query = string.match(request.url, "^([^?]*)%??(.*)$") - read_urlencoded_form(request.get_params_list, request.query) - -- parse cookies: - for i, line in ipairs(request.headers["Cookie"]) do - for rawkey, rawvalue in - string.gmatch(line, "([^=; ]*)=([^=; ]*)") - do - request.cookies[decode_uri(rawkey)] = decode_uri(rawvalue) - end - end - -- call underlying handler and remember boolean result: - if handler(request) ~= true then survive = false end - -- finish request (unless already done by underlying handler): - request:finish() - end - end - return survive - end -end - -return _M - diff -r 3b3f0ecc4ac4 -r 583e2ad140dc moonbridge_http.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/moonbridge_http.lua Fri Jan 09 22:51:25 2015 +0100 @@ -0,0 +1,883 @@ +#!/usr/bin/env lua + +-- module preamble +local _G, _M = _ENV, {} +_ENV = setmetatable({}, { + __index = function(self, key) + local value = _M[key]; if value ~= nil then return value end + return _G[key] + end, + __newindex = function(self, key, value) _M[key] = value end +}) + +-- function that encodes certain HTML entities: +-- (not used by the library itself) +function encode_html(text) + return ( + string.gsub( + text, '[<>&"]', + function(char) + if char == '<' then + return "<" + elseif char == '>' then + return ">" + elseif char == '&' then + return "&" + elseif char == '"' then + return """ + end + end + ) + ) + +end + +-- function that encodes special characters for URIs: +-- (not used by the library itself) +function encode_uri(text) + return ( + string.gsub(text, "[^0-9A-Za-z_%.~-]", + function (char) + return string.format("%%%02x", string.byte(char)) + end + ) + ) +end + +-- function undoing URL encoding: +do + local b0 = string.byte("0") + local b9 = string.byte("9") + local bA = string.byte("A") + local bF = string.byte("F") + local ba = string.byte("a") + local bf = string.byte("f") + function decode_uri(str) + return ( + string.gsub( + string.gsub(str, "%+", " "), + "%%([0-9A-Fa-f][0-9A-Fa-f])", + function(hex) + local n1, n2 = string.byte(hex, 1, 2) + if n1 >= b0 and n1 <= b9 then n1 = n1 - b0 + elseif n1 >= bA and n1 <= bF then n1 = n1 - bA + 10 + elseif n1 >= ba and n1 <= bf then n1 = n1 - ba + 10 + else error("Assertion failed") end + if n2 >= b0 and n2 <= b9 then n2 = n2 - b0 + elseif n2 >= bA and n2 <= bF then n2 = n2 - bA + 10 + elseif n2 >= ba and n2 <= bf then n2 = n2 - ba + 10 + else error("Assertion failed") end + return string.char(n1 * 16 + n2) + end + ) + ) + end +end + +-- status codes that carry no response body (in addition to 1xx): +-- (set to "zero_content_length" if Content-Length header is required) +status_without_response_body = { + ["101"] = true, + ["204"] = true, + ["205"] = "zero_content_length", + ["304"] = true +} + +-- handling of GET/POST param tables: +local new_params_list -- defined later +do + local params_list_mapping = setmetatable({}, {__mode="k"}) + local params_list_metatable = { + __index = function(self, key) + local tbl = {} + self[key] = tbl + return tbl + end + } + local params_metatable = { + __index = function(self, key) + local value = params_list_mapping[self][key][1] + self[key] = value + return value + end + } + -- returns a table to store key value-list pairs (i.e. multiple values), + -- and a second table automatically mapping keys to the first value + -- using the key value-list pairs in the first table: + new_params_list = function() + local params_list = setmetatable({}, params_list_metatable) + local params = setmetatable({}, params_metatable) + params_list_mapping[params] = params_list + return params_list, params + end +end +-- parses URL encoded form data and stores it in +-- a key value-list pairs structure that has to be +-- previously obtained by calling by new_params_list(): +local function read_urlencoded_form(tbl, data) + for rawkey, rawvalue in string.gmatch(data, "([^=&]*)=([^=&]*)") do + local subtbl = tbl[decode_uri(rawkey)] + subtbl[#subtbl+1] = decode_uri(rawvalue) + end +end + +-- function creating a HTTP handler: +function generate_handler(handler, options) + -- swap arguments if necessary (for convenience): + if type(handler) ~= "function" and type(options) == "function" then + handler, options = options, handler + end + -- process options: + options = options or {} + local preamble = "" -- preamble sent with every(!) HTTP response + do + -- named arg "static_headers" is used to create the preamble: + local s = options.static_headers + local t = {} + if s then + if type(s) == "string" then + for line in string.gmatch(s, "[^\r\n]+") do + t[#t+1] = line + end + else + for i, kv in ipairs(options.static_headers) do + if type(kv) == "string" then + t[#t+1] = kv + else + t[#t+1] = kv[1] .. ": " .. kv[2] + end + end + end + end + t[#t+1] = "" + preamble = table.concat(t, "\r\n") + end + -- return connect handler: + return function(socket) + local survive = true -- set to false if process shall be terminated later + while true do + -- desired chunk sizes: + local input_chunk_size = options.maximum_input_chunk_size or options.chunk_size or 16384 + local output_chunk_size = options.minimum_output_chunk_size or options.chunk_size or 1024 + -- process named arguments "request_header_size_limit" and "request_body_size_limit": + local remaining_header_size_limit = options.request_header_size_limit or 1024*1024 + local remaining_body_size_limit = options.request_body_size_limit or 64*1024*1024 + -- state variables for request handling: + local output_state = "no_status_sent" -- one of: + -- "no_status_sent" (initial state) + -- "info_status_sent" (1xx status code has been sent) + -- "bodyless_status_sent" (204/304 status code has been sent) + -- "status_sent" (regular status code has been sent) + -- "headers_sent" (headers have been terminated) + -- "finished" (request has been answered completely) + local content_length -- value of Content-Length header sent + local bytes_sent = 0 -- number of bytes sent if Content-Length is set + local chunk_parts = {} -- list of chunks to send + local chunk_bytes = 0 -- sum of lengths of chunks to send + local connection_close_requested = false + local connection_close_responded = false + local headers_value_nil = {} -- header values that are nil + local request_body_content_length -- Content-Length of request body + local input_state = "pending" -- one of: + -- "pending" (request body has not been processed yet) + -- "deferred" (request body processing is deferred) + -- "reading" (request body is currently being read) + -- "finished" (request body has been read) + local streamed_post_params = {} -- mapping from POST field name to stream function + local streamed_post_param_patterns = {} -- list of POST field pattern and stream function pairs + -- object passed to handler (with methods, GET/POST params, etc.): + local request + -- reads a number of bytes from the socket, + -- optionally feeding these bytes chunk-wise + -- into a callback function: + local function read_body_bytes(remaining, callback) + while remaining > 0 do + local limit + if remaining > input_chunk_size then + limit = input_chunk_size + else + limit = remaining + end + local chunk = socket:read(limit) + if not chunk or #chunk ~= limit then + error("Unexpected EOF while reading chunk of request body") + end + remaining = remaining - limit + if callback then + callback(chunk) + end + end + end + -- flushes or closes the socket (depending on + -- whether "Connection: close" header was sent): + local function finish_response() + if connection_close_responded then + -- close output stream: + socket.output:close() + -- wait for EOF of peer to avoid immediate TCP RST condition: + timeout(2, function() + while socket.input:read(input_chunk_size) do end + end) + -- fully close socket: + socket:close() + else + socket:flush() + request:stream_request_body() + end + end + -- writes out buffered chunks (without flushing the socket): + local function send_chunk() + if chunk_bytes > 0 then + socket:write(string.format("%x\r\n", chunk_bytes)) + for i = 1, #chunk_parts do + socket:write(chunk_parts[i]) + end + chunk_parts = {} + chunk_bytes = 0 + socket:write("\r\n") + end + end + -- terminate header section in response, optionally flushing: + -- (may be called multiple times unless response is finished) + local function finish_headers(flush) + if output_state == "no_status_sent" then + error("HTTP status has not been sent yet") + elseif output_state == "finished" then + error("Response has already been finished") + elseif output_state == "info_status_sent" then + socket:write("\r\n") + socket:flush() + output_state = "no_status_sent" + elseif output_state == "bodyless_status_sent" then + if connection_close_requested and not connection_close_responded then + request:send_header("Connection", "close") + end + socket:write("\r\n") + finish_response() + output_state = "finished" + elseif output_state == "status_sent" then + if not content_length then + socket:write("Transfer-Encoding: chunked\r\n") + end + if connection_close_requested and not connection_close_responded then + request:send_header("Connection", "close") + end + socket:write("\r\n") + if request.method == "HEAD" then + finish_response() + elseif flush then + socket:flush() + end + output_state = "headers_sent" + elseif output_state ~= "headers_sent" then + error("Unexpected internal status in HTTP engine") + end + end + -- create request object and set several functions and values: + request = { + -- allow raw socket access: + socket = socket, + -- parsed cookies: + cookies = {}, + -- send a HTTP response status (e.g. "200 OK"): + send_status = function(self, value) + if input_state == "pending" then + request:process_request_body() + end + if output_state == "info_status_sent" then + socket:write("\r\n") + socket:flush() + elseif output_state ~= "no_status_sent" then + error("HTTP status has already been sent") + end + local status1 = string.sub(value, 1, 1) + local status3 = string.sub(value, 1, 3) + socket:write("HTTP/1.1 ", value, "\r\n", preamble) + local without_response_body = status_without_response_body[status3] + if without_response_body then + output_state = "bodyless_status_sent" + if without_response_body == "zero_content_length" then + request:send_header("Content-Length", 0) + end + elseif status1 == "1" then + output_state = "info_status_sent" + else + output_state = "status_sent" + end + end, + -- send a HTTP response header + -- (key and value as separate args): + send_header = function(self, key, value) + if output_state == "no_status_sent" then + error("HTTP status has not been sent yet") + elseif + output_state ~= "info_status_sent" and + output_state ~= "bodyless_status_sent" and + output_state ~= "status_sent" + then + error("All HTTP headers have already been sent") + end + local key_lower = string.lower(key) + if key_lower == "content-length" then + if output_state == "info_status_sent" then + error("Cannot set Content-Length for informational status response") + end + local new_content_length = assert(tonumber(value), "Invalid content-length") + if content_length == nil then + content_length = new_content_length + elseif content_length == new_content_length then + return + else + error("Content-Length has been set multiple times with different values") + end + elseif key_lower == "connection" then + for entry in string.gmatch(string.lower(value), "[^,]+") do + if string.match(entry, "^[ \t]*close[ \t]*$") then + if output_state == "info_status_sent" then + error("Cannot set \"Connection: close\" for informational status response") + end + if connection_close_responded then + return + else + connection_close_responded = true + break + end + end + end + end + socket:write(key, ": ", value, "\r\n") + end, + -- method to finish and flush headers: + finish_headers = function() + finish_headers(true) + end, + -- send data for response body: + send_data = function(self, ...) + if output_state == "info_status_sent" then + error("No (non-informational) HTTP status has been sent yet") + elseif output_state == "bodyless_status_sent" then + error("Cannot send response data for body-less status message") + end + finish_headers(false) + if output_state ~= "headers_sent" then + error("Unexpected internal status in HTTP engine") + end + if request.method == "HEAD" then + return + end + for i = 1, select("#", ...) do + local str = tostring(select(i, ...)) + if #str > 0 then + if content_length then + local bytes_to_send = #str + if bytes_sent + bytes_to_send > content_length then + socket:write(string.sub(str, 1, content_length - bytes_sent)) + bytes_sent = content_length + error("Content length exceeded") + else + socket:write(str) + bytes_sent = bytes_sent + bytes_to_send + end + else + chunk_bytes = chunk_bytes + #str + chunk_parts[#chunk_parts+1] = str + end + end + end + if chunk_bytes >= output_chunk_size then + send_chunk() + end + end, + -- flush output buffer: + flush = function(self) + send_chunk() + socket:flush() + end, + -- finish response: + finish = function(self) + if output_state == "finished" then + return + elseif output_state == "info_status_sent" then + error("Informational HTTP response can be finished with :finish_headers() method") + end + finish_headers(false) + if output_state == "headers_sent" then + if request.method ~= "HEAD" then + if content_length then + if bytes_sent ~= content_length then + error("Content length not used") + end + else + send_chunk() + socket:write("0\r\n\r\n") + end + finish_response() + end + output_state = "finished" + elseif output_state ~= finished then + error("Unexpected internal status in HTTP engine") + end + end, + -- table mapping header field names to value-lists + -- (raw access): + headers = setmetatable({}, { + __index = function(self, key) + local lowerkey = string.lower(key) + if lowerkey == key then + return + end + local result = rawget(self, lowerkey) + if result == nil then + result = {} + end + self[lowerkey] = result + self[key] = result + return result + end + }), + -- table mapping header field names to value-lists + -- (for headers with comma separated values): + headers_csv_table = setmetatable({}, { + __index = function(self, key) + local result = {} + for i, line in ipairs(request.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 + self[key] = result + return result + end + }), + -- table mapping header field names to a comma separated string + -- (for headers with comma separated values): + headers_csv_string = setmetatable({}, { + __index = function(self, key) + local result = {} + for i, line in ipairs(request.headers[key]) do + result[#result+1] = line + end + result = string.concat(result, ", ") + self[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 = setmetatable({}, { + __index = function(self, key) + if headers_value_nil[key] then + return nil + end + local result = nil + local values = request.headers_csv_table[key] + if #values == 0 then + headers_value_nil[key] = true + elseif #values == 1 then + result = values[1] + else + result = false + end + self[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 = setmetatable({}, { + __index = function(self, key) + local result = setmetatable({}, { + __index = function(self, key) + local lowerkey = string.lower(key) + local result = rawget(self, lowerkey) or false + self[lowerkey] = result + self[key] = result + return result + end + }) + for i, value in ipairs(request.headers_csv_table[key]) do + result[string.lower(value)] = true + end + self[key] = result + return result + end + }), + -- register POST param stream handler for a single field name: + stream_post_param = function(self, field_name, callback) + if input_state == "inprogress" or input_state == "finished" then + error("Cannot register POST param streaming function if request body is already processed") + end + streamed_post_params[field_name] = callback + end, + -- register POST param stream handler for a field name pattern: + stream_post_params = function(self, pattern, callback) + if input_state == "inprogress" or input_state == "finished" then + error("Cannot register POST param streaming function if request body is already processed") + end + streamed_post_param_patterns[#streamed_post_param_patterns+1] = {pattern, callback} + end, + -- disables automatic request body processing on write + -- (use with caution): + defer_reading = function(self) + if input_state == "pending" then + input_state = "deferred" + end + end, + -- processes the request body and sets the request.post_params, + -- request.post_params_list, request.meta_post_params, and + -- request.meta_post_params_list values (can be called manually or + -- automatically if post_params are accessed or data is written out) + process_request_body = function(self) + if input_state == "finished" then + return + end + local post_params_list, post_params = new_params_list() + local content_type = request.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 + read_urlencoded_form(post_params_list, request.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 + local post_metadata_list, post_metadata = new_params_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 = 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 + error("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 + error("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 + error("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 + error("Premature end of multipart/form-data request body") + end + request.post_metadata_list, request.post_metadata = post_metadata_list, post_metadata + else + error("Unknown Content-Type of request body") + end + end + end + request.post_params_list, request.post_params = post_params_list, post_params + end, + -- stream request body to an (optional) callback function + -- without processing it otherwise: + stream_request_body = function(self, callback) + if input_state ~= "pending" and input_state ~= "deferred" then + if callback then + if input_state == "inprogress" then + error("Request body is already being processed") + else + error("Request body has already been processed") + end + end + return + end + input_state = "inprogress" + if request.headers_flags["Expect"]["100-continue"] then + request:send_status("100 Continue") + request:finish_headers() + end + if request.headers_flags["Transfer-Encoding"]["chunked"] then + while true do + local line = socket:readuntil("\n", 32 + remaining_body_size_limit) + if not line then + error("Unexpected EOF while reading next chunk of request body") + end + local zeros, lenstr = string.match(line, "^(0*)([1-9A-Fa-f]+[0-9A-Fa-f]*)\r?\n$") + local chunkext + if lenstr then + chunkext = "" + else + zeros, lenstr, chunkext = string.match(line, "^(0*)([1-9A-Fa-f]+[0-9A-Fa-f]*)([ \t;].-)\r?\n$") + end + if not lenstr or #lenstr > 13 then + error("Encoding error or unexpected EOF while reading chunk of request body") + end + local len = tonumber("0x" .. lenstr) + remaining_body_size_limit = remaining_body_size_limit - (#zeros + #chunkext + len) + if remaining_body_size_limit < 0 then + error("Request body size limit exceeded") + end + if len == 0 then break end + read_body_bytes(len, callback) + local term = socket:readuntil("\n", 2) + if term ~= "\r\n" and term ~= "\n" then + error("Encoding error while reading chunk of request body") + end + end + while true do + local line = socket:readuntil("\n", 2 + remaining_body_size_limit) + if line == "\r\n" or line == "\n" then break end + remaining_body_size_limit = remaining_body_size_limit - #line + if remaining_body_size_limit < 0 then + error("Request body size limit exceeded while reading trailer section of chunked request body") + end + end + elseif request_body_content_length then + read_body_bytes(request_body_content_length, callback) + end + input_state = "finished" + end + } + -- initialize tables for GET params in request object: + request.get_params_list, request.get_params = new_params_list() + -- add meta table to request object to allow access to "body" and POST params: + setmetatable(request, { + __index = function(self, key) + if key == "body" then + local chunks = {} + request:stream_request_body(function(chunk) + chunks[#chunks+1] = chunk + end) + self.body = table.concat(chunks) + return self.body + elseif + key == "post_params_list" or key == "post_params" or + key == "post_metadata_list" or key == "post_metadata" + then + request:process_request_body() + return request[key] + end + end + }) + -- low level HTTP error response (for malformed requests, etc.): + local function error_response(status, text) + request:send_status(status) + request:send_header("Content-Type", "text/plain") + if not connection_close_responded then + request:send_header("Connection", "close") + end + request:send_data(status, "\n") + if text then + request:send_data("\n", text, "\n") + end + request:finish() + return survive + end + -- read and parse request line: + local line = socket:readuntil("\n", remaining_header_size_limit) + if not line then return survive end + remaining_header_size_limit = remaining_header_size_limit - #line + if remaining_header_size_limit == 0 then + return error_response("413 Request Entity Too Large", "Request line too long") + end + local proto + request.method, request.url, proto = + line:match("^([^ \t\r]+)[ \t]+([^ \t\r]+)[ \t]*([^ \t\r]*)[ \t]*\r?\n$") + if not request.method then + return error_response("400 Bad Request") + elseif proto ~= "HTTP/1.1" then + return error_response("505 HTTP Version Not Supported") + else + -- read and parse headers: + while true do + local line = socket:readuntil("\n", remaining_header_size_limit); + remaining_header_size_limit = remaining_header_size_limit - #line + if not line then + return error_response("400 Bad Request") + end + if line == "\r\n" or line == "\n" then + break + end + if remaining_header_size_limit == 0 then + return error_response("413 Request Entity Too Large", "Headers too long") + end + local key, value = string.match(line, "^([^ \t\r]+):[ \t]*(.-)[ \t]*\r?\n$") + if not key then + return error_response("400 Bad Request") + end + local values = request.headers[key] + values[#values+1] = value + end + -- process "Connection: close" header if existent: + connection_close_requested = request.headers_flags["Connection"]["close"] + -- process "Content-Length" header if existent: + do + local values = request.headers_csv_table["Content-Length"] + if #values > 0 then + request_body_content_length = tonumber(values[1]) + local proper_value = tostring(request_body_content_length) + for i, value in ipairs(values) do + value = string.match(value, "^0*(.*)") + if value ~= proper_value then + return error_response("400 Bad Request", "Content-Length header(s) invalid") + end + end + if request_body_content_length > remaining_body_size_limit then + return error_response("413 Request Entity Too Large", "Request body too big") + end + end + end + -- process "Transfer-Encoding" header if existent: + do + local flag = request.headers_flags["Transfer-Encoding"]["chunked"] + local list = request.headers_csv_table["Transfer-Encoding"] + if (flag and #list ~= 1) or (not flag and #list ~= 0) then + return error_response("400 Bad Request", "Unexpected Transfer-Encoding") + end + end + -- process "Expect" header if existent: + for i, value in ipairs(request.headers_csv_table["Expect"]) do + if string.lower(value) ~= "100-continue" then + return error_response("417 Expectation Failed", "Unexpected Expect header") + end + end + -- parse GET params: + request.path, request.query = string.match(request.url, "^([^?]*)%??(.*)$") + read_urlencoded_form(request.get_params_list, request.query) + -- parse cookies: + for i, line in ipairs(request.headers["Cookie"]) do + for rawkey, rawvalue in + string.gmatch(line, "([^=; ]*)=([^=; ]*)") + do + request.cookies[decode_uri(rawkey)] = decode_uri(rawvalue) + end + end + -- call underlying handler and remember boolean result: + if handler(request) ~= true then survive = false end + -- finish request (unless already done by underlying handler): + request:finish() + end + end + return survive + end +end + +return _M +