# HG changeset patch # User jbe # Date 1426814848 -3600 # Node ID 0dd15d642124a7131f2202b51762be8fdea03f5c # Parent 649df11b1f5a077ebe38c1f7da3a517e1182578c Proper handling of I/O errors; Added property "request.faulty"; Removed "io_error_handler" hook; Added documentation for global function "timeout" diff -r 649df11b1f5a -r 0dd15d642124 moonbridge_http.lua --- a/moonbridge_http.lua Thu Mar 19 22:49:28 2015 +0100 +++ b/moonbridge_http.lua Fri Mar 20 02:27:28 2015 +0100 @@ -183,22 +183,6 @@ local output_chunk_size = options.minimum_output_chunk_size or options.chunk_size or 1024 -- return connect handler: return function(socket) - -- handing I/O errors: - local socket_closed = false - local function assert_io(retval, errmsg) - if retval then - return retval - end - if not socket_closed then - socket_closed = true - socket:cancel() - end - if options.io_error_handler then - options.io_error_handler(errmsg) - error(errmsg) - end - error(errmsg, 2) - end local survive = true -- set to false if process shall be terminated later repeat -- process named arguments "request_header_size_limit" and "request_body_size_limit": @@ -229,6 +213,57 @@ 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 + -- handling I/O errors (including protocol violations): + local socket_closed = false + local function assert_output(retval, errmsg) + if retval then + return retval + end + request.faulty = true + errmsg = "Could not send data to client: " .. errmsg + io.stderr:write(errmsg, "\n") + if not socket_closed then + socket_closed = true + socket:cancel() + end + error("Could not send data to client: " .. errmsg) + end + local function request_error(throw_error, status, text) + local errmsg = "Error while reading request from client. Error response: " .. status + if text then + errmsg = errmsg .. " (" .. text .. ")" + end + io.stderr:write(errmsg, "\n") -- needs to be written now, because of possible timeout error later + if + output_state == "no_status_sent" or + output_state == "info_status_sent" + then + local error_response_status, errmsg2 = pcall(function() + request:defer_reading() -- don't read request body (because of possibly invalid state) + request:send_status(status) + request:send_header("Content-Type", "text/plain") + request:send_header("Connection", "close") + request:send_data(status, "\n") + if text then + request:send_data("\n", text, "\n") + end + request:finish() + end) + if not error_response_status and not request.faulty then + request.faulty = true + error("Unexpected error while sending error response: " .. errmsg2) + end + end + if throw_error then + request.faulty = true + error(errmsg) + else + return survive + end + end + local function assert_not_faulty() + assert(not request.faulty, "Tried to use faulty request handle") + end -- reads a number of bytes from the socket, -- optionally feeding these bytes chunk-wise -- into a callback function: @@ -242,7 +277,7 @@ end local chunk = socket:read(limit) if not chunk or #chunk ~= limit then - assert_io(false, "Unexpected EOF or read error while reading chunk of request body") + request_error(true, "400 Bad Request", "Unexpected EOF or read error while reading chunk of request body") end remaining = remaining - limit if callback then @@ -255,29 +290,29 @@ local function finish_response() if connection_close_responded then -- close output stream: - assert_io(socket.output:close()) + assert_output(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_closed = true -- avoid double close on error - assert_io(socket:close()) + assert_output(socket:close()) else - assert_io(socket:flush()) + assert_output(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 - assert_io(socket:write(string.format("%x\r\n", chunk_bytes))) + assert_output(socket:write(string.format("%x\r\n", chunk_bytes))) for i = 1, #chunk_parts do - assert_io(socket:write(chunk_parts[i])) + assert_output(socket:write(chunk_parts[i])) end chunk_parts = {} chunk_bytes = 0 - assert_io(socket:write("\r\n")) + assert_output(socket:write("\r\n")) end end -- terminate header section in response, optionally flushing: @@ -288,28 +323,28 @@ elseif output_state == "finished" then error("Response has already been finished") elseif output_state == "info_status_sent" then - assert_io(socket:write("\r\n")) - assert_io(socket:flush()) + assert_output(socket:write("\r\n")) + assert_output(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 - assert_io(socket:write("\r\n")) + assert_output(socket:write("\r\n")) finish_response() output_state = "finished" elseif output_state == "status_sent" then if not content_length then - assert_io(socket:write("Transfer-Encoding: chunked\r\n")) + assert_output(socket:write("Transfer-Encoding: chunked\r\n")) end if connection_close_requested and not connection_close_responded then request:send_header("Connection", "close") end - assert_io(socket:write("\r\n")) + assert_output(socket:write("\r\n")) if request.method == "HEAD" then finish_response() elseif flush then - assert_io(socket:flush()) + assert_output(socket:flush()) end output_state = "headers_sent" elseif output_state ~= "headers_sent" then @@ -318,24 +353,27 @@ end -- create request object and set several functions and values: request = { + -- error state: + faulty = false, -- allow raw socket access: socket = socket, -- parsed cookies: cookies = {}, -- send a HTTP response status (e.g. "200 OK"): send_status = function(self, value) + assert_not_faulty() if input_state == "pending" then request:process_request_body() end if output_state == "info_status_sent" then - assert_io(socket:write("\r\n")) - assert_io(socket:flush()) + assert_output(socket:write("\r\n")) + assert_output(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) - assert_io(socket:write("HTTP/1.1 ", value, "\r\n", preamble)) + assert_output(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" @@ -351,6 +389,7 @@ -- send a HTTP response header -- (key and value as separate args): send_header = function(self, key, value) + assert_not_faulty() if output_state == "no_status_sent" then error("HTTP status has not been sent yet") elseif @@ -388,14 +427,16 @@ end end end - assert_io(socket:write(key, ": ", value, "\r\n")) + assert_output(socket:write(key, ": ", value, "\r\n")) end, -- method to finish and flush headers: finish_headers = function() + assert_not_faulty() finish_headers(true) end, -- send data for response body: send_data = function(self, ...) + assert_not_faulty() if output_state == "info_status_sent" then error("No (non-informational) HTTP status has been sent yet") elseif output_state == "bodyless_status_sent" then @@ -414,11 +455,11 @@ if content_length then local bytes_to_send = #str if bytes_sent + bytes_to_send > content_length then - assert_io(socket:write(string.sub(str, 1, content_length - bytes_sent))) + assert_output(socket:write(string.sub(str, 1, content_length - bytes_sent))) bytes_sent = content_length error("Content length exceeded") else - assert_io(socket:write(str)) + assert_output(socket:write(str)) bytes_sent = bytes_sent + bytes_to_send end else @@ -433,11 +474,13 @@ end, -- flush output buffer: flush = function(self) + assert_not_faulty() send_chunk() - assert_io(socket:flush()) + assert_output(socket:flush()) end, -- finish response: finish = function(self) + assert_not_faulty() if output_state == "finished" then return elseif output_state == "info_status_sent" then @@ -452,7 +495,7 @@ end else send_chunk() - assert_io(socket:write("0\r\n\r\n")) + assert_output(socket:write("0\r\n\r\n")) end finish_response() end @@ -550,6 +593,7 @@ }), -- register POST param stream handler for a single field name: stream_post_param = function(self, field_name, callback) + assert_not_faulty() if input_state == "inprogress" or input_state == "finished" then error("Cannot register POST param streaming function if request body is already processed") end @@ -557,6 +601,7 @@ end, -- register POST param stream handler for a field name pattern: stream_post_params = function(self, pattern, callback) + assert_not_faulty() if input_state == "inprogress" or input_state == "finished" then error("Cannot register POST param streaming function if request body is already processed") end @@ -565,6 +610,7 @@ -- disables automatic request body processing on write -- (use with caution): defer_reading = function(self) + assert_not_faulty() if input_state == "pending" then input_state = "deferred" end @@ -574,6 +620,7 @@ -- 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) + assert_not_faulty() if input_state == "finished" then return end @@ -652,7 +699,7 @@ headerdata = remaining local header_key, header_value = string.match(line, "^([^:]*):[ \t]*(.-)[ \t]*$") if not header_key then - assert_io(false, "Invalid header in multipart/form-data part") + request_error(true, "400 Bad Request", "Invalid header in multipart/form-data part") end header_key = string.lower(header_key) if header_key == "content-disposition" then @@ -674,7 +721,7 @@ elseif header_key == "content-type" then metadata.content_type = header_value elseif header_key == "content-transfer-encoding" then - assert_io(false, "Content-transfer-encoding not supported by multipart/form-data parser") + request_error(true, "400 Bad Request", "Content-transfer-encoding not supported by multipart/form-data parser") end end end @@ -702,7 +749,7 @@ bigchunk = nil return else - assert_io(false, "Error while parsing multipart body (expected CRLF or double minus)") + request_error(true, "400 Bad Request", "Error while parsing multipart body (expected CRLF or double minus)") end end local pos1, pos2 = string.find(bigchunk, boundary, 1, true) @@ -728,11 +775,11 @@ end end) if not terminated then - assert_io(false, "Premature end of multipart/form-data request body") + request_error(true, "400 Bad Request", "Premature end of multipart/form-data request body") end request.post_metadata_list, request.post_metadata = post_metadata_list, post_metadata else - assert_io(false, "Unknown Content-Type of request body") + request_error(true, "415 Unsupported Media Type", "Unknown Content-Type of request body") end end end @@ -741,6 +788,7 @@ -- stream request body to an (optional) callback function -- without processing it otherwise: stream_request_body = function(self, callback) + assert_not_faulty() if input_state ~= "pending" and input_state ~= "deferred" then if callback then if input_state == "inprogress" then @@ -760,7 +808,7 @@ while true do local line = socket:readuntil("\n", 32 + remaining_body_size_limit) if not line then - assert_io(false, "Unexpected EOF while reading next chunk of request body") + request_error(true, "400 Bad Request", "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 @@ -770,18 +818,18 @@ 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 - assert_io(false, "Encoding error or unexpected EOF or read error while reading chunk of request body") + request_error(true, "400 Bad Request", "Encoding error or unexpected EOF or read error 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 - assert_io(false, "Request body size limit exceeded") + request_error(true, "413 Request Entity Too Large", "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 - assert_io(false, "Encoding error while reading chunk of request body") + request_error(true, "400 Bad Request", "Encoding error while reading chunk of request body") end end while true do @@ -789,7 +837,7 @@ 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 - assert_io(false, "Request body size limit exceeded while reading trailer section of chunked request body") + request_error(true, "413 Request Entity Too Large", "Request body size limit exceeded while reading trailer section of chunked request body") end end elseif request_body_content_length then @@ -819,51 +867,37 @@ end end }) - -- sends a minimalistic error response and enforces closing of the - -- connection and returns the boolean value "survive" - local function error_response(status, text) - request:defer_reading() -- don't read request body (because of possibly invalid state) - request:send_status(status) - request:send_header("Content-Type", "text/plain") - request:send_header("Connection", "close") - 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") + return request_error(false, "414 Request-URI Too Long") end local target, proto request.method, target, 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") + return request_error(false, "400 Bad Request") elseif proto ~= "HTTP/1.1" then - return error_response("505 HTTP Version Not Supported") + return request_error(false, "505 HTTP Version Not Supported") end -- 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") + return request_error(false, "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") + return request_error(false, "431 Request Header Fields Too Large") end local key, value = string.match(line, "^([^ \t\r]+):[ \t]*(.-)[ \t]*\r?\n$") if not key then - return error_response("400 Bad Request") + return request_error(false, "400 Bad Request") end local values = request.headers[key] values[#values+1] = value @@ -879,11 +913,11 @@ 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") + return request_error(false, "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") + return request_error(false, "413 Request Entity Too Large", "Announced request body size is too big") end end end @@ -892,19 +926,19 @@ 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") + return request_error(false, "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") + return request_error(false, "417 Expectation Failed", "Unexpected Expect header") end end -- get mandatory Host header according to RFC 7230: request.host = request.headers_value["Host"] if not request.host then - return error_response("400 Bad Request", "No valid host header") + return request_error(false, "400 Bad Request", "No valid host header") end -- parse request target: request.path, request.query = string.match(target, "^/([^?]*)(.*)$") @@ -913,10 +947,10 @@ host2, request.path, request.query = string.match(target, "^[Hh][Tt][Tt][Pp]://([^/?]+)/?([^?]*)(.*)$") if host2 then if request.host ~= host2 then - return error_response("400 Bad Request", "No valid host header") + return request_error(false, "400 Bad Request", "No valid host header") end elseif not (target == "*" and request.method == "OPTIONS") then - return error_response("400 Bad Request", "Invalid request target") + return request_error(false, "400 Bad Request", "Invalid request target") end end -- parse GET params: diff -r 649df11b1f5a -r 0dd15d642124 reference.txt --- a/reference.txt Thu Mar 19 22:49:28 2015 +0100 +++ b/reference.txt Fri Mar 20 02:27:28 2015 +0100 @@ -21,6 +21,26 @@ +Global function timeout(...) +---------------------------- + +Calling this function with a positive number (time in seconds) sets a timer +that kills the current process after the selected time runs out. The remaining +time can be queried by calling this function without arguments. + +Calling this function with a single argument that is the number zero will +disable the timeout. + +Another mode of operation is selected by passing two arguments: a time (in +seconds) as first argument and a function as second argument. In this case, a +sub-timer will be used to limit the execution time of the function. In case of +timeout, the process will be killed (and the timeout function does not return). +If the time for the sub-timer is longer than a previously set timeout (using +the timeout(...) function with one argument), the shorter timeout (of the +previous call of timeout(...)) will have precedence. + + + Socket object passed to "connect" handler ----------------------------------------- @@ -163,8 +183,6 @@ ignored when request:flush() is called) - static_headers: a set of headers to be included in every HTTP response (may be a string, a table or strings, or a table of key-value pairs) -- io_error_handler: a function to be called when an I/O operation with the - client fails (must not return or an error will be raised automatically) The callback function receives a single request object as argument, which is described below. @@ -200,6 +218,14 @@ are desired. +### request.faulty + +Normally set to false. In case of a read or write error on the client +connection, this value is set to true before a Lua error is raised. + +A faulty request handle must not be used, or another Lua error will be raised. + + ### request:finish() Finishes and flushes a HTTP response. May be called multiple times. An