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@0: -- handling of GET/POST param tables: jbe@0: local new_params_list -- defined later jbe@0: do jbe@0: local params_list_mapping = setmetatable({}, {__mode="k"}) jbe@35: local function nextnonempty(tbl, key) jbe@35: while true do jbe@35: key = next(tbl, key) jbe@35: if key == nil then jbe@35: return nil jbe@35: end jbe@35: local value = tbl[key] jbe@35: if #value > 0 then jbe@35: return key, value jbe@35: end jbe@35: end jbe@35: end jbe@35: local function nextvalue(tbl, key) jbe@35: key = next(tbl, key) jbe@35: if key == nil then jbe@35: return nil jbe@35: end jbe@35: return key, tbl[key][1] jbe@35: end jbe@0: local params_list_metatable = { jbe@0: __index = function(self, key) jbe@0: local tbl = {} jbe@0: self[key] = tbl jbe@0: return tbl jbe@35: end, jbe@35: __pairs = function(self) jbe@35: return nextnonempty, self, nil jbe@0: end jbe@0: } jbe@0: local params_metatable = { jbe@0: __index = function(self, key) jbe@36: return params_list_mapping[self][key][1] jbe@36: end, jbe@36: __newindex = function(self, key, value) jbe@36: params_list_mapping[self][key] = {value} jbe@35: end, jbe@35: __pairs = function(self) jbe@35: return nextvalue, params_list_mapping[self], nil jbe@0: end jbe@0: } jbe@0: -- returns a table to store key value-list pairs (i.e. multiple values), jbe@0: -- and a second table automatically mapping keys to the first value jbe@0: -- using the key value-list pairs in the first table: jbe@0: new_params_list = function() jbe@0: local params_list = setmetatable({}, params_list_metatable) jbe@0: local params = setmetatable({}, params_metatable) jbe@0: params_list_mapping[params] = params_list jbe@0: return params_list, params jbe@0: end jbe@0: end jbe@0: -- parses URL encoded form data and stores it in jbe@0: -- a key value-list pairs structure that has to be jbe@0: -- previously obtained by calling by new_params_list(): jbe@0: local function read_urlencoded_form(tbl, data) jbe@12: for rawkey, rawvalue in string.gmatch(data, "([^?=&]*)=([^?=&]*)") do jbe@0: local subtbl = tbl[decode_uri(rawkey)] jbe@0: subtbl[#subtbl+1] = decode_uri(rawvalue) jbe@0: end jbe@0: end jbe@0: jbe@0: -- function creating a HTTP handler: jbe@0: function generate_handler(handler, options) jbe@0: -- swap arguments if necessary (for convenience): jbe@0: if type(handler) ~= "function" and type(options) == "function" then jbe@0: handler, options = options, handler jbe@0: end jbe@0: -- process options: jbe@0: options = options or {} jbe@0: local preamble = "" -- preamble sent with every(!) HTTP response 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@0: preamble = table.concat(t, "\r\n") jbe@0: end jbe@44: local input_chunk_size = options.maximum_input_chunk_size or options.chunk_size or 16384 jbe@44: local output_chunk_size = options.minimum_output_chunk_size or options.chunk_size or 1024 jbe@54: local request_header_timeout, response_timeout jbe@54: if options.request_header_timeout ~= nil then jbe@54: request_header_timeout = options.request_header_timeout jbe@54: elseif options.timeout ~= nil then jbe@54: request_header_timeout = options.timeout or 0 jbe@54: else jbe@54: request_header_timeout = 360 jbe@53: end jbe@54: if options.timeout ~= nil then jbe@54: response_timeout = options.timeout or 0 jbe@54: else jbe@54: response_timeout = 1800 jbe@53: end jbe@0: -- return connect handler: jbe@0: return function(socket) jbe@0: local survive = true -- set to false if process shall be terminated later jbe@31: repeat jbe@54: -- (re)set timeout: jbe@54: timeout(request_header_timeout or 0) jbe@0: -- process named arguments "request_header_size_limit" and "request_body_size_limit": jbe@0: local remaining_header_size_limit = options.request_header_size_limit or 1024*1024 jbe@0: local remaining_body_size_limit = options.request_body_size_limit or 64*1024*1024 jbe@0: -- state variables for request handling: jbe@0: local output_state = "no_status_sent" -- one of: jbe@0: -- "no_status_sent" (initial state) jbe@0: -- "info_status_sent" (1xx status code has been sent) jbe@0: -- "bodyless_status_sent" (204/304 status code has been sent) jbe@0: -- "status_sent" (regular status code has been sent) jbe@0: -- "headers_sent" (headers have been terminated) jbe@0: -- "finished" (request has been answered completely) jbe@0: local content_length -- value of Content-Length header sent jbe@0: local bytes_sent = 0 -- number of bytes sent if Content-Length is set jbe@0: local chunk_parts = {} -- list of chunks to send jbe@0: local chunk_bytes = 0 -- sum of lengths of chunks to send jbe@0: local connection_close_requested = false jbe@0: local connection_close_responded = false jbe@0: local headers_value_nil = {} -- header values that are nil jbe@0: local request_body_content_length -- Content-Length of request body jbe@0: local input_state = "pending" -- one of: jbe@37: -- "pending" (request body has not been processed yet) jbe@37: -- "deferred" (request body processing is deferred) jbe@37: -- "inprogress" (request body is currently being read) jbe@37: -- "finished" (request body has been read) jbe@0: local streamed_post_params = {} -- mapping from POST field name to stream function jbe@0: local streamed_post_param_patterns = {} -- list of POST field pattern and stream function pairs jbe@0: -- object passed to handler (with methods, GET/POST params, etc.): jbe@0: local request jbe@50: -- handling I/O errors (including protocol violations): jbe@50: local socket_closed = false jbe@50: local function assert_output(retval, errmsg) jbe@50: if retval then jbe@50: return retval jbe@50: end jbe@50: request.faulty = true jbe@50: errmsg = "Could not send data to client: " .. errmsg jbe@50: io.stderr:write(errmsg, "\n") jbe@50: if not socket_closed then jbe@50: socket_closed = true jbe@50: socket:cancel() jbe@50: end jbe@50: error("Could not send data to client: " .. errmsg) jbe@50: end jbe@50: local function request_error(throw_error, status, text) jbe@50: local errmsg = "Error while reading request from client. Error response: " .. status jbe@50: if text then jbe@50: errmsg = errmsg .. " (" .. text .. ")" jbe@50: end jbe@50: io.stderr:write(errmsg, "\n") -- needs to be written now, because of possible timeout error later jbe@50: if jbe@50: output_state == "no_status_sent" or jbe@50: output_state == "info_status_sent" jbe@50: then jbe@50: local error_response_status, errmsg2 = pcall(function() jbe@50: request:defer_reading() -- don't read request body (because of possibly invalid state) jbe@60: request:close_after_finish() -- send "Connection: close" header jbe@50: request:send_status(status) jbe@50: request:send_header("Content-Type", "text/plain") jbe@50: request:send_data(status, "\n") jbe@50: if text then jbe@50: request:send_data("\n", text, "\n") jbe@50: end jbe@50: request:finish() jbe@50: end) jbe@50: if not error_response_status and not request.faulty then jbe@50: request.faulty = true jbe@50: error("Unexpected error while sending error response: " .. errmsg2) jbe@50: end jbe@51: else jbe@51: if not socket_closed then jbe@51: socket_closed = true jbe@51: socket:cancel() jbe@51: end jbe@50: end jbe@50: if throw_error then jbe@50: request.faulty = true jbe@50: error(errmsg) jbe@50: else jbe@50: return survive jbe@50: end jbe@50: end jbe@50: local function assert_not_faulty() jbe@50: assert(not request.faulty, "Tried to use faulty request handle") jbe@50: end jbe@0: -- reads a number of bytes from the socket, jbe@0: -- optionally feeding these bytes chunk-wise jbe@0: -- into a callback function: jbe@0: local function read_body_bytes(remaining, callback) jbe@0: while remaining > 0 do jbe@0: local limit jbe@0: if remaining > input_chunk_size then jbe@0: limit = input_chunk_size jbe@0: else jbe@0: limit = remaining jbe@0: end jbe@0: local chunk = socket:read(limit) jbe@0: if not chunk or #chunk ~= limit then jbe@50: request_error(true, "400 Bad Request", "Unexpected EOF or read error while reading chunk of request body") jbe@0: end jbe@0: remaining = remaining - limit jbe@0: if callback then jbe@0: callback(chunk) jbe@0: end jbe@0: end jbe@0: end jbe@0: -- flushes or closes the socket (depending on jbe@0: -- whether "Connection: close" header was sent): jbe@0: local function finish_response() jbe@0: if connection_close_responded then jbe@0: -- close output stream: jbe@50: assert_output(socket.output:close()) jbe@0: -- wait for EOF of peer to avoid immediate TCP RST condition: jbe@0: timeout(2, function() jbe@0: while socket.input:read(input_chunk_size) do end jbe@0: end) jbe@0: -- fully close socket: jbe@45: socket_closed = true -- avoid double close on error jbe@50: assert_output(socket:close()) jbe@0: else jbe@50: assert_output(socket:flush()) jbe@0: request:stream_request_body() jbe@0: end jbe@0: end jbe@0: -- writes out buffered chunks (without flushing the socket): jbe@0: local function send_chunk() jbe@0: if chunk_bytes > 0 then jbe@50: assert_output(socket:write(string.format("%x\r\n", chunk_bytes))) jbe@0: for i = 1, #chunk_parts do jbe@50: assert_output(socket:write(chunk_parts[i])) jbe@0: end jbe@0: chunk_parts = {} jbe@0: chunk_bytes = 0 jbe@50: assert_output(socket:write("\r\n")) jbe@0: end jbe@0: end jbe@0: -- terminate header section in response, optionally flushing: jbe@0: -- (may be called multiple times unless response is finished) jbe@0: local function finish_headers(flush) jbe@0: if output_state == "no_status_sent" then jbe@0: error("HTTP status has not been sent yet") jbe@0: elseif output_state == "finished" then jbe@0: error("Response has already been finished") jbe@0: elseif output_state == "info_status_sent" then jbe@50: assert_output(socket:write("\r\n")) jbe@50: assert_output(socket:flush()) jbe@0: output_state = "no_status_sent" jbe@0: elseif output_state == "bodyless_status_sent" then jbe@0: if connection_close_requested and not connection_close_responded then jbe@0: request:send_header("Connection", "close") jbe@0: end jbe@50: assert_output(socket:write("\r\n")) jbe@0: finish_response() jbe@0: output_state = "finished" jbe@0: elseif output_state == "status_sent" then jbe@0: if not content_length then jbe@50: assert_output(socket:write("Transfer-Encoding: chunked\r\n")) jbe@0: end jbe@0: if connection_close_requested and not connection_close_responded then jbe@0: request:send_header("Connection", "close") jbe@0: end jbe@50: assert_output(socket:write("\r\n")) jbe@0: if request.method == "HEAD" then jbe@0: finish_response() jbe@0: elseif flush then jbe@50: assert_output(socket:flush()) jbe@0: end jbe@0: output_state = "headers_sent" jbe@0: elseif output_state ~= "headers_sent" then jbe@0: error("Unexpected internal status in HTTP engine") jbe@0: end jbe@0: end jbe@0: -- create request object and set several functions and values: jbe@0: request = { jbe@50: -- error state: jbe@50: faulty = false, jbe@0: -- allow raw socket access: jbe@0: socket = socket, jbe@0: -- parsed cookies: jbe@0: cookies = {}, jbe@0: -- send a HTTP response status (e.g. "200 OK"): jbe@0: send_status = function(self, value) jbe@50: assert_not_faulty() jbe@0: if input_state == "pending" then jbe@0: request:process_request_body() jbe@0: end jbe@0: if output_state == "info_status_sent" then jbe@50: assert_output(socket:write("\r\n")) jbe@50: assert_output(socket:flush()) jbe@0: elseif output_state ~= "no_status_sent" then jbe@0: error("HTTP status has already been sent") jbe@0: end jbe@0: local status1 = string.sub(value, 1, 1) jbe@0: local status3 = string.sub(value, 1, 3) jbe@50: assert_output(socket:write("HTTP/1.1 ", value, "\r\n", preamble)) jbe@0: local without_response_body = status_without_response_body[status3] jbe@0: if without_response_body then jbe@0: output_state = "bodyless_status_sent" jbe@0: if without_response_body == "zero_content_length" then jbe@0: request:send_header("Content-Length", 0) jbe@0: end jbe@0: elseif status1 == "1" then jbe@0: output_state = "info_status_sent" jbe@0: else jbe@0: output_state = "status_sent" jbe@0: end jbe@0: end, jbe@0: -- send a HTTP response header jbe@0: -- (key and value as separate args): jbe@0: send_header = function(self, key, value) jbe@50: assert_not_faulty() jbe@0: if output_state == "no_status_sent" then jbe@0: error("HTTP status has not been sent yet") jbe@0: elseif jbe@0: output_state ~= "info_status_sent" and jbe@0: output_state ~= "bodyless_status_sent" and jbe@0: output_state ~= "status_sent" jbe@0: then jbe@0: error("All HTTP headers have already been sent") jbe@0: end jbe@0: local key_lower = string.lower(key) jbe@0: if key_lower == "content-length" then jbe@0: if output_state == "info_status_sent" then jbe@0: error("Cannot set Content-Length for informational status response") jbe@0: end jbe@0: local new_content_length = assert(tonumber(value), "Invalid content-length") jbe@0: if content_length == nil then jbe@0: content_length = new_content_length jbe@0: elseif content_length == new_content_length then jbe@0: return jbe@0: else jbe@0: error("Content-Length has been set multiple times with different values") jbe@0: end jbe@0: elseif key_lower == "connection" then jbe@0: for entry in string.gmatch(string.lower(value), "[^,]+") do jbe@0: if string.match(entry, "^[ \t]*close[ \t]*$") then jbe@0: if output_state == "info_status_sent" then jbe@0: error("Cannot set \"Connection: close\" for informational status response") jbe@0: end jbe@59: connection_close_responded = true jbe@59: break jbe@0: end jbe@0: end jbe@0: end jbe@50: assert_output(socket:write(key, ": ", value, "\r\n")) jbe@0: end, jbe@60: -- method to announce (and enforce) connection close after sending the response: jbe@60: close_after_finish = function() jbe@60: assert_not_faulty() jbe@60: if jbe@60: output_state == "headers_sent" or jbe@60: output_state == "finished" jbe@60: then jbe@60: error("All HTTP headers have already been sent") jbe@60: end jbe@60: connection_close_requested = true jbe@60: end, jbe@0: -- method to finish and flush headers: jbe@0: finish_headers = function() jbe@50: assert_not_faulty() jbe@0: finish_headers(true) jbe@0: end, jbe@0: -- send data for response body: jbe@0: send_data = function(self, ...) jbe@50: assert_not_faulty() jbe@0: if output_state == "info_status_sent" then jbe@0: error("No (non-informational) HTTP status has been sent yet") jbe@0: elseif output_state == "bodyless_status_sent" then jbe@0: error("Cannot send response data for body-less status message") jbe@0: end jbe@0: finish_headers(false) jbe@0: if output_state ~= "headers_sent" then jbe@0: error("Unexpected internal status in HTTP engine") jbe@0: end jbe@0: if request.method == "HEAD" then jbe@0: return jbe@0: end jbe@0: for i = 1, select("#", ...) do jbe@0: local str = tostring(select(i, ...)) jbe@0: if #str > 0 then jbe@0: if content_length then jbe@0: local bytes_to_send = #str jbe@0: if bytes_sent + bytes_to_send > content_length then jbe@50: assert_output(socket:write(string.sub(str, 1, content_length - bytes_sent))) jbe@0: bytes_sent = content_length jbe@0: error("Content length exceeded") jbe@0: else jbe@50: assert_output(socket:write(str)) jbe@0: bytes_sent = bytes_sent + bytes_to_send jbe@0: end jbe@0: else jbe@0: chunk_bytes = chunk_bytes + #str jbe@0: chunk_parts[#chunk_parts+1] = str jbe@0: end jbe@0: end jbe@0: end jbe@0: if chunk_bytes >= output_chunk_size then jbe@0: send_chunk() jbe@0: end jbe@0: end, jbe@0: -- flush output buffer: jbe@0: flush = function(self) jbe@50: assert_not_faulty() jbe@0: send_chunk() jbe@50: assert_output(socket:flush()) jbe@0: end, jbe@0: -- finish response: jbe@0: finish = function(self) jbe@50: assert_not_faulty() jbe@0: if output_state == "finished" then jbe@0: return jbe@0: elseif output_state == "info_status_sent" then jbe@0: error("Informational HTTP response can be finished with :finish_headers() method") jbe@0: end jbe@0: finish_headers(false) jbe@0: if output_state == "headers_sent" then jbe@0: if request.method ~= "HEAD" then jbe@0: if content_length then jbe@0: if bytes_sent ~= content_length then jbe@0: error("Content length not used") jbe@0: end jbe@0: else jbe@0: send_chunk() jbe@50: assert_output(socket:write("0\r\n\r\n")) jbe@0: end jbe@0: finish_response() jbe@0: end jbe@0: output_state = "finished" jbe@28: elseif output_state ~= "finished" then jbe@0: error("Unexpected internal status in HTTP engine") jbe@0: end jbe@0: end, jbe@0: -- table mapping header field names to value-lists jbe@0: -- (raw access): jbe@0: headers = setmetatable({}, { jbe@0: __index = function(self, key) jbe@0: local lowerkey = string.lower(key) jbe@0: if lowerkey == key then jbe@0: return jbe@0: end jbe@0: local result = rawget(self, lowerkey) jbe@0: if result == nil then jbe@0: result = {} jbe@0: end jbe@0: self[lowerkey] = result jbe@0: self[key] = result jbe@0: return result jbe@0: end jbe@0: }), jbe@0: -- table mapping header field names to value-lists jbe@0: -- (for headers with comma separated values): jbe@0: headers_csv_table = setmetatable({}, { jbe@0: __index = function(self, key) jbe@0: local result = {} jbe@0: for i, line in ipairs(request.headers[key]) do jbe@0: for entry in string.gmatch(line, "[^,]+") do jbe@0: local value = string.match(entry, "^[ \t]*(..-)[ \t]*$") jbe@0: if value then jbe@0: result[#result+1] = value jbe@0: end jbe@0: end jbe@0: end jbe@0: self[key] = result jbe@0: return result jbe@0: end jbe@0: }), jbe@0: -- table mapping header field names to a comma separated string jbe@0: -- (for headers with comma separated values): jbe@0: headers_csv_string = setmetatable({}, { jbe@0: __index = function(self, key) jbe@0: local result = {} jbe@0: for i, line in ipairs(request.headers[key]) do jbe@0: result[#result+1] = line jbe@0: end jbe@0: result = string.concat(result, ", ") jbe@0: self[key] = result jbe@0: return result jbe@0: end jbe@0: }), jbe@0: -- table mapping header field names to a single string value jbe@0: -- (or false if header has been sent multiple times): jbe@0: headers_value = setmetatable({}, { jbe@0: __index = function(self, key) jbe@0: if headers_value_nil[key] then jbe@0: return nil jbe@0: end jbe@0: local result = nil jbe@0: local values = request.headers_csv_table[key] jbe@0: if #values == 0 then jbe@0: headers_value_nil[key] = true jbe@0: elseif #values == 1 then jbe@0: result = values[1] jbe@0: else jbe@0: result = false jbe@0: end jbe@0: self[key] = result jbe@0: return result jbe@0: end jbe@0: }), jbe@0: -- table mapping header field names to a flag table, jbe@0: -- indicating if the comma separated value contains certain entries: jbe@0: headers_flags = setmetatable({}, { jbe@0: __index = function(self, key) jbe@0: local result = setmetatable({}, { jbe@0: __index = function(self, key) jbe@0: local lowerkey = string.lower(key) jbe@0: local result = rawget(self, lowerkey) or false jbe@0: self[lowerkey] = result jbe@0: self[key] = result jbe@0: return result jbe@0: end jbe@0: }) jbe@0: for i, value in ipairs(request.headers_csv_table[key]) do jbe@0: result[string.lower(value)] = true jbe@0: end jbe@0: self[key] = result jbe@0: return result jbe@0: end jbe@0: }), jbe@0: -- register POST param stream handler for a single field name: jbe@0: stream_post_param = function(self, field_name, callback) jbe@50: assert_not_faulty() jbe@0: if input_state == "inprogress" or input_state == "finished" then jbe@0: error("Cannot register POST param streaming function if request body is already processed") jbe@0: end jbe@0: streamed_post_params[field_name] = callback jbe@0: end, jbe@0: -- register POST param stream handler for a field name pattern: jbe@0: stream_post_params = function(self, pattern, callback) jbe@50: assert_not_faulty() jbe@0: if input_state == "inprogress" or input_state == "finished" then jbe@0: error("Cannot register POST param streaming function if request body is already processed") jbe@0: end jbe@0: streamed_post_param_patterns[#streamed_post_param_patterns+1] = {pattern, callback} jbe@0: end, jbe@0: -- disables automatic request body processing on write jbe@0: -- (use with caution): jbe@0: defer_reading = function(self) jbe@50: assert_not_faulty() jbe@0: if input_state == "pending" then jbe@0: input_state = "deferred" jbe@0: end jbe@0: end, jbe@0: -- processes the request body and sets the request.post_params, jbe@0: -- request.post_params_list, request.meta_post_params, and jbe@0: -- request.meta_post_params_list values (can be called manually or jbe@0: -- automatically if post_params are accessed or data is written out) jbe@0: process_request_body = function(self) jbe@50: assert_not_faulty() jbe@0: if input_state == "finished" then jbe@0: return jbe@0: end jbe@0: local post_params_list, post_params = new_params_list() jbe@0: local content_type = request.headers_value["Content-Type"] jbe@0: if content_type then jbe@0: if jbe@0: content_type == "application/x-www-form-urlencoded" or jbe@0: string.match(content_type, "^application/x%-www%-form%-urlencoded *;") jbe@0: then jbe@0: read_urlencoded_form(post_params_list, request.body) jbe@0: else jbe@0: local boundary = string.match( jbe@0: content_type, jbe@0: '^multipart/form%-data[ \t]*[;,][ \t]*boundary="([^"]+)"$' jbe@0: ) or string.match( jbe@0: content_type, jbe@0: '^multipart/form%-data[ \t]*[;,][ \t]*boundary=([^"; \t]+)$' jbe@0: ) jbe@0: if boundary then jbe@0: local post_metadata_list, post_metadata = new_params_list() jbe@0: boundary = "--" .. boundary jbe@0: local headerdata = "" jbe@0: local streamer jbe@0: local field_name jbe@0: local metadata = {} jbe@0: local value_parts jbe@0: local function default_streamer(chunk) jbe@0: value_parts[#value_parts+1] = chunk jbe@0: end jbe@0: local function stream_part_finish() jbe@0: if streamer == default_streamer then jbe@0: local value = table.concat(value_parts) jbe@0: value_parts = nil jbe@0: if field_name then jbe@0: local values = post_params_list[field_name] jbe@0: values[#values+1] = value jbe@0: local metadata_entries = post_metadata_list[field_name] jbe@0: metadata_entries[#metadata_entries+1] = metadata jbe@0: end jbe@0: else jbe@0: streamer() jbe@0: end jbe@0: headerdata = "" jbe@0: streamer = nil jbe@0: field_name = nil jbe@0: metadata = {} jbe@0: end jbe@0: local function stream_part_chunk(chunk) jbe@0: if streamer then jbe@0: streamer(chunk) jbe@0: else jbe@0: headerdata = headerdata .. chunk jbe@0: while true do jbe@0: local line, remaining = string.match(headerdata, "^(.-)\r?\n(.*)$") jbe@0: if not line then jbe@0: break jbe@0: end jbe@0: if line == "" then jbe@0: streamer = streamed_post_params[field_name] jbe@0: if not streamer then jbe@0: for i, rule in ipairs(streamed_post_param_patterns) do jbe@0: if string.match(field_name, rule[1]) then jbe@0: streamer = rule[2] jbe@0: break jbe@0: end jbe@0: end jbe@0: end jbe@0: if not streamer then jbe@0: value_parts = {} jbe@0: streamer = default_streamer jbe@0: end jbe@0: streamer(remaining, field_name, metadata) jbe@0: return jbe@0: end jbe@0: headerdata = remaining jbe@0: local header_key, header_value = string.match(line, "^([^:]*):[ \t]*(.-)[ \t]*$") jbe@0: if not header_key then jbe@50: request_error(true, "400 Bad Request", "Invalid header in multipart/form-data part") jbe@0: end jbe@0: header_key = string.lower(header_key) jbe@0: if header_key == "content-disposition" then jbe@0: local escaped_header_value = string.gsub(header_value, '"[^"]*"', function(str) jbe@0: return string.gsub(str, "=", "==") jbe@0: end) jbe@0: field_name = string.match(escaped_header_value, ';[ \t]*name="([^"]*)"') jbe@0: if field_name then jbe@0: field_name = string.gsub(field_name, "==", "=") jbe@0: else jbe@0: field_name = string.match(header_value, ';[ \t]*name=([^"; \t]+)') jbe@0: end jbe@0: metadata.file_name = string.match(escaped_header_value, ';[ \t]*filename="([^"]*)"') jbe@0: if metadata.file_name then jbe@0: metadata.file_name = string.gsub(metadata.file_name, "==", "=") jbe@0: else jbe@0: string.match(header_value, ';[ \t]*filename=([^"; \t]+)') jbe@0: end jbe@0: elseif header_key == "content-type" then jbe@0: metadata.content_type = header_value jbe@0: elseif header_key == "content-transfer-encoding" then jbe@50: request_error(true, "400 Bad Request", "Content-transfer-encoding not supported by multipart/form-data parser") jbe@0: end jbe@0: end jbe@0: end jbe@0: end jbe@0: local skippart = true -- ignore data until first boundary jbe@0: local afterbound = false -- interpret 2 bytes after boundary ("\r\n" or "--") jbe@0: local terminated = false -- final boundary read jbe@0: local bigchunk = "" jbe@0: request:stream_request_body(function(chunk) jbe@0: if terminated then jbe@0: return jbe@0: end jbe@0: bigchunk = bigchunk .. chunk jbe@0: while true do jbe@0: if afterbound then jbe@0: if #bigchunk <= 2 then jbe@0: return jbe@0: end jbe@0: local terminator = string.sub(bigchunk, 1, 2) jbe@0: if terminator == "\r\n" then jbe@0: afterbound = false jbe@0: bigchunk = string.sub(bigchunk, 3) jbe@0: elseif terminator == "--" then jbe@0: terminated = true jbe@0: bigchunk = nil jbe@0: return jbe@0: else jbe@50: request_error(true, "400 Bad Request", "Error while parsing multipart body (expected CRLF or double minus)") jbe@0: end jbe@0: end jbe@0: local pos1, pos2 = string.find(bigchunk, boundary, 1, true) jbe@0: if not pos1 then jbe@0: if not skippart then jbe@0: local safe = #bigchunk-#boundary jbe@0: if safe > 0 then jbe@0: stream_part_chunk(string.sub(bigchunk, 1, safe)) jbe@0: bigchunk = string.sub(bigchunk, safe+1) jbe@0: end jbe@0: end jbe@0: return jbe@0: end jbe@0: if not skippart then jbe@0: stream_part_chunk(string.sub(bigchunk, 1, pos1 - 1)) jbe@0: stream_part_finish() jbe@0: else jbe@0: boundary = "\r\n" .. boundary jbe@0: skippart = false jbe@0: end jbe@0: bigchunk = string.sub(bigchunk, pos2 + 1) jbe@0: afterbound = true jbe@0: end jbe@0: end) jbe@0: if not terminated then jbe@50: request_error(true, "400 Bad Request", "Premature end of multipart/form-data request body") jbe@0: end jbe@0: request.post_metadata_list, request.post_metadata = post_metadata_list, post_metadata jbe@0: else jbe@50: request_error(true, "415 Unsupported Media Type", "Unknown Content-Type of request body") jbe@0: end jbe@0: end jbe@0: end jbe@0: request.post_params_list, request.post_params = post_params_list, post_params jbe@0: end, jbe@0: -- stream request body to an (optional) callback function jbe@0: -- without processing it otherwise: jbe@0: stream_request_body = function(self, callback) jbe@50: assert_not_faulty() jbe@0: if input_state ~= "pending" and input_state ~= "deferred" then jbe@0: if callback then jbe@0: if input_state == "inprogress" then jbe@48: error("Request body is currently being processed") jbe@0: else jbe@0: error("Request body has already been processed") jbe@0: end jbe@0: end jbe@0: return jbe@0: end jbe@0: input_state = "inprogress" jbe@0: if request.headers_flags["Expect"]["100-continue"] then jbe@0: request:send_status("100 Continue") jbe@0: request:finish_headers() jbe@0: end jbe@0: if request.headers_flags["Transfer-Encoding"]["chunked"] then jbe@0: while true do jbe@0: local line = socket:readuntil("\n", 32 + remaining_body_size_limit) jbe@46: if not line then jbe@50: request_error(true, "400 Bad Request", "Unexpected EOF while reading next chunk of request body") jbe@0: end jbe@0: local zeros, lenstr = string.match(line, "^(0*)([1-9A-Fa-f]+[0-9A-Fa-f]*)\r?\n$") jbe@0: local chunkext jbe@0: if lenstr then jbe@0: chunkext = "" jbe@0: else jbe@0: zeros, lenstr, chunkext = string.match(line, "^(0*)([1-9A-Fa-f]+[0-9A-Fa-f]*)([ \t;].-)\r?\n$") jbe@0: end jbe@0: if not lenstr or #lenstr > 13 then jbe@50: request_error(true, "400 Bad Request", "Encoding error or unexpected EOF or read error while reading chunk of request body") jbe@0: end jbe@0: local len = tonumber("0x" .. lenstr) jbe@0: remaining_body_size_limit = remaining_body_size_limit - (#zeros + #chunkext + len) jbe@0: if remaining_body_size_limit < 0 then jbe@50: request_error(true, "413 Request Entity Too Large", "Request body size limit exceeded") jbe@0: end jbe@0: if len == 0 then break end jbe@0: read_body_bytes(len, callback) jbe@0: local term = socket:readuntil("\n", 2) jbe@0: if term ~= "\r\n" and term ~= "\n" then jbe@50: request_error(true, "400 Bad Request", "Encoding error while reading chunk of request body") jbe@0: end jbe@0: end jbe@0: while true do jbe@0: local line = socket:readuntil("\n", 2 + remaining_body_size_limit) jbe@0: if line == "\r\n" or line == "\n" then break end jbe@0: remaining_body_size_limit = remaining_body_size_limit - #line jbe@0: if remaining_body_size_limit < 0 then jbe@50: request_error(true, "413 Request Entity Too Large", "Request body size limit exceeded while reading trailer section of chunked request body") jbe@0: end jbe@0: end jbe@0: elseif request_body_content_length then jbe@0: read_body_bytes(request_body_content_length, callback) jbe@0: end jbe@0: input_state = "finished" jbe@0: end jbe@0: } jbe@0: -- initialize tables for GET params in request object: jbe@0: request.get_params_list, request.get_params = new_params_list() jbe@0: -- add meta table to request object to allow access to "body" and POST params: jbe@0: setmetatable(request, { jbe@0: __index = function(self, key) jbe@0: if key == "body" then jbe@0: local chunks = {} jbe@0: request:stream_request_body(function(chunk) jbe@0: chunks[#chunks+1] = chunk jbe@0: end) jbe@0: self.body = table.concat(chunks) jbe@0: return self.body jbe@0: elseif jbe@0: key == "post_params_list" or key == "post_params" or jbe@0: key == "post_metadata_list" or key == "post_metadata" jbe@0: then jbe@0: request:process_request_body() jbe@0: return request[key] jbe@0: end jbe@0: end jbe@0: }) jbe@0: -- read and parse request line: jbe@0: local line = socket:readuntil("\n", remaining_header_size_limit) jbe@0: if not line then return survive end jbe@0: remaining_header_size_limit = remaining_header_size_limit - #line jbe@0: if remaining_header_size_limit == 0 then jbe@50: return request_error(false, "414 Request-URI Too Long") jbe@0: end jbe@10: local target, proto jbe@10: request.method, target, proto = jbe@0: line:match("^([^ \t\r]+)[ \t]+([^ \t\r]+)[ \t]*([^ \t\r]*)[ \t]*\r?\n$") jbe@0: if not request.method then jbe@50: return request_error(false, "400 Bad Request") jbe@0: elseif proto ~= "HTTP/1.1" then jbe@50: return request_error(false, "505 HTTP Version Not Supported") jbe@38: end jbe@38: -- read and parse headers: jbe@38: while true do jbe@38: local line = socket:readuntil("\n", remaining_header_size_limit); jbe@38: remaining_header_size_limit = remaining_header_size_limit - #line jbe@38: if not line then jbe@50: return request_error(false, "400 Bad Request") jbe@38: end jbe@38: if line == "\r\n" or line == "\n" then jbe@38: break jbe@38: end jbe@38: if remaining_header_size_limit == 0 then jbe@50: return request_error(false, "431 Request Header Fields Too Large") jbe@38: end jbe@38: local key, value = string.match(line, "^([^ \t\r]+):[ \t]*(.-)[ \t]*\r?\n$") jbe@38: if not key then jbe@50: return request_error(false, "400 Bad Request") jbe@38: end jbe@38: local values = request.headers[key] jbe@38: values[#values+1] = value jbe@38: end jbe@38: -- process "Connection: close" header if existent: jbe@38: connection_close_requested = request.headers_flags["Connection"]["close"] jbe@38: -- process "Content-Length" header if existent: jbe@38: do jbe@38: local values = request.headers_csv_table["Content-Length"] jbe@38: if #values > 0 then jbe@38: request_body_content_length = tonumber(values[1]) jbe@38: local proper_value = tostring(request_body_content_length) jbe@38: for i, value in ipairs(values) do jbe@38: value = string.match(value, "^0*(.*)") jbe@38: if value ~= proper_value then jbe@50: return request_error(false, "400 Bad Request", "Content-Length header(s) invalid") jbe@38: end jbe@0: end jbe@38: if request_body_content_length > remaining_body_size_limit then jbe@50: return request_error(false, "413 Request Entity Too Large", "Announced request body size is too big") jbe@0: end jbe@0: end jbe@38: end jbe@38: -- process "Transfer-Encoding" header if existent: jbe@38: do jbe@38: local flag = request.headers_flags["Transfer-Encoding"]["chunked"] jbe@38: local list = request.headers_csv_table["Transfer-Encoding"] jbe@38: if (flag and #list ~= 1) or (not flag and #list ~= 0) then jbe@50: return request_error(false, "400 Bad Request", "Unexpected Transfer-Encoding") jbe@0: end jbe@38: end jbe@38: -- process "Expect" header if existent: jbe@38: for i, value in ipairs(request.headers_csv_table["Expect"]) do jbe@38: if string.lower(value) ~= "100-continue" then jbe@50: return request_error(false, "417 Expectation Failed", "Unexpected Expect header") jbe@10: end jbe@38: end jbe@38: -- get mandatory Host header according to RFC 7230: jbe@38: request.host = request.headers_value["Host"] jbe@38: if not request.host then jbe@50: return request_error(false, "400 Bad Request", "No valid host header") jbe@38: end jbe@38: -- parse request target: jbe@38: request.path, request.query = string.match(target, "^/([^?]*)(.*)$") jbe@38: if not request.path then jbe@38: local host2 jbe@38: host2, request.path, request.query = string.match(target, "^[Hh][Tt][Tt][Pp]://([^/?]+)/?([^?]*)(.*)$") jbe@38: if host2 then jbe@38: if request.host ~= host2 then jbe@50: return request_error(false, "400 Bad Request", "No valid host header") jbe@5: end jbe@38: elseif not (target == "*" and request.method == "OPTIONS") then jbe@50: return request_error(false, "400 Bad Request", "Invalid request target") jbe@5: end jbe@38: end jbe@38: -- parse GET params: jbe@38: if request.query then jbe@38: read_urlencoded_form(request.get_params_list, request.query) jbe@38: end jbe@38: -- parse cookies: jbe@38: for i, line in ipairs(request.headers["Cookie"]) do jbe@38: for rawkey, rawvalue in jbe@38: string.gmatch(line, "([^=; ]*)=([^=; ]*)") jbe@38: do jbe@38: request.cookies[decode_uri(rawkey)] = decode_uri(rawvalue) jbe@5: end jbe@0: end jbe@54: -- (re)set timeout: jbe@53: timeout(response_timeout or 0) jbe@38: -- call underlying handler and remember boolean result: jbe@38: if handler(request) ~= true then survive = false end jbe@38: -- finish request (unless already done by underlying handler): jbe@38: request:finish() jbe@31: until connection_close_responded jbe@0: return survive jbe@0: end jbe@0: end jbe@0: jbe@0: return _M jbe@0: