moonbridge
diff moonbridge_http.lua @ 50:0dd15d642124
Proper handling of I/O errors; Added property "request.faulty"; Removed "io_error_handler" hook; Added documentation for global function "timeout"
author | jbe |
---|---|
date | Fri Mar 20 02:27:28 2015 +0100 (2015-03-20) |
parents | 649df11b1f5a |
children | 2be59069a184 |
line diff
1.1 --- a/moonbridge_http.lua Thu Mar 19 22:49:28 2015 +0100 1.2 +++ b/moonbridge_http.lua Fri Mar 20 02:27:28 2015 +0100 1.3 @@ -183,22 +183,6 @@ 1.4 local output_chunk_size = options.minimum_output_chunk_size or options.chunk_size or 1024 1.5 -- return connect handler: 1.6 return function(socket) 1.7 - -- handing I/O errors: 1.8 - local socket_closed = false 1.9 - local function assert_io(retval, errmsg) 1.10 - if retval then 1.11 - return retval 1.12 - end 1.13 - if not socket_closed then 1.14 - socket_closed = true 1.15 - socket:cancel() 1.16 - end 1.17 - if options.io_error_handler then 1.18 - options.io_error_handler(errmsg) 1.19 - error(errmsg) 1.20 - end 1.21 - error(errmsg, 2) 1.22 - end 1.23 local survive = true -- set to false if process shall be terminated later 1.24 repeat 1.25 -- process named arguments "request_header_size_limit" and "request_body_size_limit": 1.26 @@ -229,6 +213,57 @@ 1.27 local streamed_post_param_patterns = {} -- list of POST field pattern and stream function pairs 1.28 -- object passed to handler (with methods, GET/POST params, etc.): 1.29 local request 1.30 + -- handling I/O errors (including protocol violations): 1.31 + local socket_closed = false 1.32 + local function assert_output(retval, errmsg) 1.33 + if retval then 1.34 + return retval 1.35 + end 1.36 + request.faulty = true 1.37 + errmsg = "Could not send data to client: " .. errmsg 1.38 + io.stderr:write(errmsg, "\n") 1.39 + if not socket_closed then 1.40 + socket_closed = true 1.41 + socket:cancel() 1.42 + end 1.43 + error("Could not send data to client: " .. errmsg) 1.44 + end 1.45 + local function request_error(throw_error, status, text) 1.46 + local errmsg = "Error while reading request from client. Error response: " .. status 1.47 + if text then 1.48 + errmsg = errmsg .. " (" .. text .. ")" 1.49 + end 1.50 + io.stderr:write(errmsg, "\n") -- needs to be written now, because of possible timeout error later 1.51 + if 1.52 + output_state == "no_status_sent" or 1.53 + output_state == "info_status_sent" 1.54 + then 1.55 + local error_response_status, errmsg2 = pcall(function() 1.56 + request:defer_reading() -- don't read request body (because of possibly invalid state) 1.57 + request:send_status(status) 1.58 + request:send_header("Content-Type", "text/plain") 1.59 + request:send_header("Connection", "close") 1.60 + request:send_data(status, "\n") 1.61 + if text then 1.62 + request:send_data("\n", text, "\n") 1.63 + end 1.64 + request:finish() 1.65 + end) 1.66 + if not error_response_status and not request.faulty then 1.67 + request.faulty = true 1.68 + error("Unexpected error while sending error response: " .. errmsg2) 1.69 + end 1.70 + end 1.71 + if throw_error then 1.72 + request.faulty = true 1.73 + error(errmsg) 1.74 + else 1.75 + return survive 1.76 + end 1.77 + end 1.78 + local function assert_not_faulty() 1.79 + assert(not request.faulty, "Tried to use faulty request handle") 1.80 + end 1.81 -- reads a number of bytes from the socket, 1.82 -- optionally feeding these bytes chunk-wise 1.83 -- into a callback function: 1.84 @@ -242,7 +277,7 @@ 1.85 end 1.86 local chunk = socket:read(limit) 1.87 if not chunk or #chunk ~= limit then 1.88 - assert_io(false, "Unexpected EOF or read error while reading chunk of request body") 1.89 + request_error(true, "400 Bad Request", "Unexpected EOF or read error while reading chunk of request body") 1.90 end 1.91 remaining = remaining - limit 1.92 if callback then 1.93 @@ -255,29 +290,29 @@ 1.94 local function finish_response() 1.95 if connection_close_responded then 1.96 -- close output stream: 1.97 - assert_io(socket.output:close()) 1.98 + assert_output(socket.output:close()) 1.99 -- wait for EOF of peer to avoid immediate TCP RST condition: 1.100 timeout(2, function() 1.101 while socket.input:read(input_chunk_size) do end 1.102 end) 1.103 -- fully close socket: 1.104 socket_closed = true -- avoid double close on error 1.105 - assert_io(socket:close()) 1.106 + assert_output(socket:close()) 1.107 else 1.108 - assert_io(socket:flush()) 1.109 + assert_output(socket:flush()) 1.110 request:stream_request_body() 1.111 end 1.112 end 1.113 -- writes out buffered chunks (without flushing the socket): 1.114 local function send_chunk() 1.115 if chunk_bytes > 0 then 1.116 - assert_io(socket:write(string.format("%x\r\n", chunk_bytes))) 1.117 + assert_output(socket:write(string.format("%x\r\n", chunk_bytes))) 1.118 for i = 1, #chunk_parts do 1.119 - assert_io(socket:write(chunk_parts[i])) 1.120 + assert_output(socket:write(chunk_parts[i])) 1.121 end 1.122 chunk_parts = {} 1.123 chunk_bytes = 0 1.124 - assert_io(socket:write("\r\n")) 1.125 + assert_output(socket:write("\r\n")) 1.126 end 1.127 end 1.128 -- terminate header section in response, optionally flushing: 1.129 @@ -288,28 +323,28 @@ 1.130 elseif output_state == "finished" then 1.131 error("Response has already been finished") 1.132 elseif output_state == "info_status_sent" then 1.133 - assert_io(socket:write("\r\n")) 1.134 - assert_io(socket:flush()) 1.135 + assert_output(socket:write("\r\n")) 1.136 + assert_output(socket:flush()) 1.137 output_state = "no_status_sent" 1.138 elseif output_state == "bodyless_status_sent" then 1.139 if connection_close_requested and not connection_close_responded then 1.140 request:send_header("Connection", "close") 1.141 end 1.142 - assert_io(socket:write("\r\n")) 1.143 + assert_output(socket:write("\r\n")) 1.144 finish_response() 1.145 output_state = "finished" 1.146 elseif output_state == "status_sent" then 1.147 if not content_length then 1.148 - assert_io(socket:write("Transfer-Encoding: chunked\r\n")) 1.149 + assert_output(socket:write("Transfer-Encoding: chunked\r\n")) 1.150 end 1.151 if connection_close_requested and not connection_close_responded then 1.152 request:send_header("Connection", "close") 1.153 end 1.154 - assert_io(socket:write("\r\n")) 1.155 + assert_output(socket:write("\r\n")) 1.156 if request.method == "HEAD" then 1.157 finish_response() 1.158 elseif flush then 1.159 - assert_io(socket:flush()) 1.160 + assert_output(socket:flush()) 1.161 end 1.162 output_state = "headers_sent" 1.163 elseif output_state ~= "headers_sent" then 1.164 @@ -318,24 +353,27 @@ 1.165 end 1.166 -- create request object and set several functions and values: 1.167 request = { 1.168 + -- error state: 1.169 + faulty = false, 1.170 -- allow raw socket access: 1.171 socket = socket, 1.172 -- parsed cookies: 1.173 cookies = {}, 1.174 -- send a HTTP response status (e.g. "200 OK"): 1.175 send_status = function(self, value) 1.176 + assert_not_faulty() 1.177 if input_state == "pending" then 1.178 request:process_request_body() 1.179 end 1.180 if output_state == "info_status_sent" then 1.181 - assert_io(socket:write("\r\n")) 1.182 - assert_io(socket:flush()) 1.183 + assert_output(socket:write("\r\n")) 1.184 + assert_output(socket:flush()) 1.185 elseif output_state ~= "no_status_sent" then 1.186 error("HTTP status has already been sent") 1.187 end 1.188 local status1 = string.sub(value, 1, 1) 1.189 local status3 = string.sub(value, 1, 3) 1.190 - assert_io(socket:write("HTTP/1.1 ", value, "\r\n", preamble)) 1.191 + assert_output(socket:write("HTTP/1.1 ", value, "\r\n", preamble)) 1.192 local without_response_body = status_without_response_body[status3] 1.193 if without_response_body then 1.194 output_state = "bodyless_status_sent" 1.195 @@ -351,6 +389,7 @@ 1.196 -- send a HTTP response header 1.197 -- (key and value as separate args): 1.198 send_header = function(self, key, value) 1.199 + assert_not_faulty() 1.200 if output_state == "no_status_sent" then 1.201 error("HTTP status has not been sent yet") 1.202 elseif 1.203 @@ -388,14 +427,16 @@ 1.204 end 1.205 end 1.206 end 1.207 - assert_io(socket:write(key, ": ", value, "\r\n")) 1.208 + assert_output(socket:write(key, ": ", value, "\r\n")) 1.209 end, 1.210 -- method to finish and flush headers: 1.211 finish_headers = function() 1.212 + assert_not_faulty() 1.213 finish_headers(true) 1.214 end, 1.215 -- send data for response body: 1.216 send_data = function(self, ...) 1.217 + assert_not_faulty() 1.218 if output_state == "info_status_sent" then 1.219 error("No (non-informational) HTTP status has been sent yet") 1.220 elseif output_state == "bodyless_status_sent" then 1.221 @@ -414,11 +455,11 @@ 1.222 if content_length then 1.223 local bytes_to_send = #str 1.224 if bytes_sent + bytes_to_send > content_length then 1.225 - assert_io(socket:write(string.sub(str, 1, content_length - bytes_sent))) 1.226 + assert_output(socket:write(string.sub(str, 1, content_length - bytes_sent))) 1.227 bytes_sent = content_length 1.228 error("Content length exceeded") 1.229 else 1.230 - assert_io(socket:write(str)) 1.231 + assert_output(socket:write(str)) 1.232 bytes_sent = bytes_sent + bytes_to_send 1.233 end 1.234 else 1.235 @@ -433,11 +474,13 @@ 1.236 end, 1.237 -- flush output buffer: 1.238 flush = function(self) 1.239 + assert_not_faulty() 1.240 send_chunk() 1.241 - assert_io(socket:flush()) 1.242 + assert_output(socket:flush()) 1.243 end, 1.244 -- finish response: 1.245 finish = function(self) 1.246 + assert_not_faulty() 1.247 if output_state == "finished" then 1.248 return 1.249 elseif output_state == "info_status_sent" then 1.250 @@ -452,7 +495,7 @@ 1.251 end 1.252 else 1.253 send_chunk() 1.254 - assert_io(socket:write("0\r\n\r\n")) 1.255 + assert_output(socket:write("0\r\n\r\n")) 1.256 end 1.257 finish_response() 1.258 end 1.259 @@ -550,6 +593,7 @@ 1.260 }), 1.261 -- register POST param stream handler for a single field name: 1.262 stream_post_param = function(self, field_name, callback) 1.263 + assert_not_faulty() 1.264 if input_state == "inprogress" or input_state == "finished" then 1.265 error("Cannot register POST param streaming function if request body is already processed") 1.266 end 1.267 @@ -557,6 +601,7 @@ 1.268 end, 1.269 -- register POST param stream handler for a field name pattern: 1.270 stream_post_params = function(self, pattern, callback) 1.271 + assert_not_faulty() 1.272 if input_state == "inprogress" or input_state == "finished" then 1.273 error("Cannot register POST param streaming function if request body is already processed") 1.274 end 1.275 @@ -565,6 +610,7 @@ 1.276 -- disables automatic request body processing on write 1.277 -- (use with caution): 1.278 defer_reading = function(self) 1.279 + assert_not_faulty() 1.280 if input_state == "pending" then 1.281 input_state = "deferred" 1.282 end 1.283 @@ -574,6 +620,7 @@ 1.284 -- request.meta_post_params_list values (can be called manually or 1.285 -- automatically if post_params are accessed or data is written out) 1.286 process_request_body = function(self) 1.287 + assert_not_faulty() 1.288 if input_state == "finished" then 1.289 return 1.290 end 1.291 @@ -652,7 +699,7 @@ 1.292 headerdata = remaining 1.293 local header_key, header_value = string.match(line, "^([^:]*):[ \t]*(.-)[ \t]*$") 1.294 if not header_key then 1.295 - assert_io(false, "Invalid header in multipart/form-data part") 1.296 + request_error(true, "400 Bad Request", "Invalid header in multipart/form-data part") 1.297 end 1.298 header_key = string.lower(header_key) 1.299 if header_key == "content-disposition" then 1.300 @@ -674,7 +721,7 @@ 1.301 elseif header_key == "content-type" then 1.302 metadata.content_type = header_value 1.303 elseif header_key == "content-transfer-encoding" then 1.304 - assert_io(false, "Content-transfer-encoding not supported by multipart/form-data parser") 1.305 + request_error(true, "400 Bad Request", "Content-transfer-encoding not supported by multipart/form-data parser") 1.306 end 1.307 end 1.308 end 1.309 @@ -702,7 +749,7 @@ 1.310 bigchunk = nil 1.311 return 1.312 else 1.313 - assert_io(false, "Error while parsing multipart body (expected CRLF or double minus)") 1.314 + request_error(true, "400 Bad Request", "Error while parsing multipart body (expected CRLF or double minus)") 1.315 end 1.316 end 1.317 local pos1, pos2 = string.find(bigchunk, boundary, 1, true) 1.318 @@ -728,11 +775,11 @@ 1.319 end 1.320 end) 1.321 if not terminated then 1.322 - assert_io(false, "Premature end of multipart/form-data request body") 1.323 + request_error(true, "400 Bad Request", "Premature end of multipart/form-data request body") 1.324 end 1.325 request.post_metadata_list, request.post_metadata = post_metadata_list, post_metadata 1.326 else 1.327 - assert_io(false, "Unknown Content-Type of request body") 1.328 + request_error(true, "415 Unsupported Media Type", "Unknown Content-Type of request body") 1.329 end 1.330 end 1.331 end 1.332 @@ -741,6 +788,7 @@ 1.333 -- stream request body to an (optional) callback function 1.334 -- without processing it otherwise: 1.335 stream_request_body = function(self, callback) 1.336 + assert_not_faulty() 1.337 if input_state ~= "pending" and input_state ~= "deferred" then 1.338 if callback then 1.339 if input_state == "inprogress" then 1.340 @@ -760,7 +808,7 @@ 1.341 while true do 1.342 local line = socket:readuntil("\n", 32 + remaining_body_size_limit) 1.343 if not line then 1.344 - assert_io(false, "Unexpected EOF while reading next chunk of request body") 1.345 + request_error(true, "400 Bad Request", "Unexpected EOF while reading next chunk of request body") 1.346 end 1.347 local zeros, lenstr = string.match(line, "^(0*)([1-9A-Fa-f]+[0-9A-Fa-f]*)\r?\n$") 1.348 local chunkext 1.349 @@ -770,18 +818,18 @@ 1.350 zeros, lenstr, chunkext = string.match(line, "^(0*)([1-9A-Fa-f]+[0-9A-Fa-f]*)([ \t;].-)\r?\n$") 1.351 end 1.352 if not lenstr or #lenstr > 13 then 1.353 - assert_io(false, "Encoding error or unexpected EOF or read error while reading chunk of request body") 1.354 + request_error(true, "400 Bad Request", "Encoding error or unexpected EOF or read error while reading chunk of request body") 1.355 end 1.356 local len = tonumber("0x" .. lenstr) 1.357 remaining_body_size_limit = remaining_body_size_limit - (#zeros + #chunkext + len) 1.358 if remaining_body_size_limit < 0 then 1.359 - assert_io(false, "Request body size limit exceeded") 1.360 + request_error(true, "413 Request Entity Too Large", "Request body size limit exceeded") 1.361 end 1.362 if len == 0 then break end 1.363 read_body_bytes(len, callback) 1.364 local term = socket:readuntil("\n", 2) 1.365 if term ~= "\r\n" and term ~= "\n" then 1.366 - assert_io(false, "Encoding error while reading chunk of request body") 1.367 + request_error(true, "400 Bad Request", "Encoding error while reading chunk of request body") 1.368 end 1.369 end 1.370 while true do 1.371 @@ -789,7 +837,7 @@ 1.372 if line == "\r\n" or line == "\n" then break end 1.373 remaining_body_size_limit = remaining_body_size_limit - #line 1.374 if remaining_body_size_limit < 0 then 1.375 - assert_io(false, "Request body size limit exceeded while reading trailer section of chunked request body") 1.376 + request_error(true, "413 Request Entity Too Large", "Request body size limit exceeded while reading trailer section of chunked request body") 1.377 end 1.378 end 1.379 elseif request_body_content_length then 1.380 @@ -819,51 +867,37 @@ 1.381 end 1.382 end 1.383 }) 1.384 - -- sends a minimalistic error response and enforces closing of the 1.385 - -- connection and returns the boolean value "survive" 1.386 - local function error_response(status, text) 1.387 - request:defer_reading() -- don't read request body (because of possibly invalid state) 1.388 - request:send_status(status) 1.389 - request:send_header("Content-Type", "text/plain") 1.390 - request:send_header("Connection", "close") 1.391 - request:send_data(status, "\n") 1.392 - if text then 1.393 - request:send_data("\n", text, "\n") 1.394 - end 1.395 - request:finish() 1.396 - return survive 1.397 - end 1.398 -- read and parse request line: 1.399 local line = socket:readuntil("\n", remaining_header_size_limit) 1.400 if not line then return survive end 1.401 remaining_header_size_limit = remaining_header_size_limit - #line 1.402 if remaining_header_size_limit == 0 then 1.403 - return error_response("413 Request Entity Too Large", "Request line too long") 1.404 + return request_error(false, "414 Request-URI Too Long") 1.405 end 1.406 local target, proto 1.407 request.method, target, proto = 1.408 line:match("^([^ \t\r]+)[ \t]+([^ \t\r]+)[ \t]*([^ \t\r]*)[ \t]*\r?\n$") 1.409 if not request.method then 1.410 - return error_response("400 Bad Request") 1.411 + return request_error(false, "400 Bad Request") 1.412 elseif proto ~= "HTTP/1.1" then 1.413 - return error_response("505 HTTP Version Not Supported") 1.414 + return request_error(false, "505 HTTP Version Not Supported") 1.415 end 1.416 -- read and parse headers: 1.417 while true do 1.418 local line = socket:readuntil("\n", remaining_header_size_limit); 1.419 remaining_header_size_limit = remaining_header_size_limit - #line 1.420 if not line then 1.421 - return error_response("400 Bad Request") 1.422 + return request_error(false, "400 Bad Request") 1.423 end 1.424 if line == "\r\n" or line == "\n" then 1.425 break 1.426 end 1.427 if remaining_header_size_limit == 0 then 1.428 - return error_response("413 Request Entity Too Large", "Headers too long") 1.429 + return request_error(false, "431 Request Header Fields Too Large") 1.430 end 1.431 local key, value = string.match(line, "^([^ \t\r]+):[ \t]*(.-)[ \t]*\r?\n$") 1.432 if not key then 1.433 - return error_response("400 Bad Request") 1.434 + return request_error(false, "400 Bad Request") 1.435 end 1.436 local values = request.headers[key] 1.437 values[#values+1] = value 1.438 @@ -879,11 +913,11 @@ 1.439 for i, value in ipairs(values) do 1.440 value = string.match(value, "^0*(.*)") 1.441 if value ~= proper_value then 1.442 - return error_response("400 Bad Request", "Content-Length header(s) invalid") 1.443 + return request_error(false, "400 Bad Request", "Content-Length header(s) invalid") 1.444 end 1.445 end 1.446 if request_body_content_length > remaining_body_size_limit then 1.447 - return error_response("413 Request Entity Too Large", "Request body too big") 1.448 + return request_error(false, "413 Request Entity Too Large", "Announced request body size is too big") 1.449 end 1.450 end 1.451 end 1.452 @@ -892,19 +926,19 @@ 1.453 local flag = request.headers_flags["Transfer-Encoding"]["chunked"] 1.454 local list = request.headers_csv_table["Transfer-Encoding"] 1.455 if (flag and #list ~= 1) or (not flag and #list ~= 0) then 1.456 - return error_response("400 Bad Request", "Unexpected Transfer-Encoding") 1.457 + return request_error(false, "400 Bad Request", "Unexpected Transfer-Encoding") 1.458 end 1.459 end 1.460 -- process "Expect" header if existent: 1.461 for i, value in ipairs(request.headers_csv_table["Expect"]) do 1.462 if string.lower(value) ~= "100-continue" then 1.463 - return error_response("417 Expectation Failed", "Unexpected Expect header") 1.464 + return request_error(false, "417 Expectation Failed", "Unexpected Expect header") 1.465 end 1.466 end 1.467 -- get mandatory Host header according to RFC 7230: 1.468 request.host = request.headers_value["Host"] 1.469 if not request.host then 1.470 - return error_response("400 Bad Request", "No valid host header") 1.471 + return request_error(false, "400 Bad Request", "No valid host header") 1.472 end 1.473 -- parse request target: 1.474 request.path, request.query = string.match(target, "^/([^?]*)(.*)$") 1.475 @@ -913,10 +947,10 @@ 1.476 host2, request.path, request.query = string.match(target, "^[Hh][Tt][Tt][Pp]://([^/?]+)/?([^?]*)(.*)$") 1.477 if host2 then 1.478 if request.host ~= host2 then 1.479 - return error_response("400 Bad Request", "No valid host header") 1.480 + return request_error(false, "400 Bad Request", "No valid host header") 1.481 end 1.482 elseif not (target == "*" and request.method == "OPTIONS") then 1.483 - return error_response("400 Bad Request", "Invalid request target") 1.484 + return request_error(false, "400 Bad Request", "Invalid request target") 1.485 end 1.486 end 1.487 -- parse GET params: