# HG changeset patch # User jbe # Date 1434066950 -7200 # Node ID 80266ee1593f8822f78c248e683d8aa129ea8cf7 # Parent 6c1561547f79217bcb813828aabe4d2a504a680f Work on new HTTP module implementation diff -r 6c1561547f79 -r 80266ee1593f moonbridge_http.lua --- a/moonbridge_http.lua Thu Jun 11 21:17:29 2015 +0200 +++ b/moonbridge_http.lua Fri Jun 12 01:55:50 2015 +0200 @@ -163,11 +163,12 @@ local consume -- function that reads some input if possible -- function that drains some input if possible: local function drain() - local bytes, status = assert(socket:drain_nb(input_chunk_size)) - if status == "eof" then + local bytes, status = socket:drain_nb(input_chunk_size) + if not bytes or status == "eof" then consume = nil end end + -- function trying to unblock socket by reading: local function unblock() if consume then poll(socket_set, socket_set) @@ -176,11 +177,14 @@ poll(nil, socket_set) end end + -- function that enforces consumption of all input: local function consume_all() while consume do + poll(socket_set, nil) consume() end end + -- handle requests in a loop: repeat -- create a new request object: local request = { @@ -190,23 +194,56 @@ -- local variables to track the state: local state = "init" -- one of: -- "init" (initial state) + -- "prepare" (configureation in preparation) -- "no_status_sent" (configuration complete) -- "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) + -- "faulty" (I/O or protocaol error) local close_requested = false -- "Connection: close" requested local close_responded = false -- "Connection: close" sent local content_length = nil -- value of Content-Length header sent + -- functions to assert proper output/closing: + local function assert_output(...) + local retval, errmsg = ... + if retval then return ... end + state = "faulty" + socket:reset() + error("Could not send data to client: " .. errmsg) + end + local function assert_close(...) + local retval, errmsg = ... + if retval then return ... end + state = "faulty" + error("Could not finish sending data to client: " .. errmsg) + end -- functions to send data to the browser: local function send(...) - assert(socket:write_call(unblock, ...)) + assert_output(socket:write_call(unblock, ...)) end local function send_flush(...) - assert(socket:flush_call(unblock, ...)) + assert_output(socket:flush_call(unblock, ...)) end - -- TODO: + -- function to finish request: + local function finish() + if close_responded then + -- discard any input: + consume = drain + -- close output stream: + send_flush() + assert_close(socket:finish()) + -- wait for EOF of peer to avoid immediate TCP RST condition: + consume_all() + -- fully close socket: + assert_close(socket:close()) + else + send_flush() + consume_all() + end + end + -- function to prepare body processing: local function prepare() if state == "prepare" then error("Unexpected internal status in HTTP engine") @@ -217,24 +254,22 @@ -- TODO state = "no_status_sent" end - -- TODO: - local function finish_response() - if close_responded then - -- discard any input: - consume = drain - -- close output stream: - send_flush() - assert(socket:finish()) - -- wait for EOF of peer to avoid immediate TCP RST condition: - consume_all() - -- fully close socket: - --socket_closed = true -- avoid double close on error -- TODO - assert(socket:close()) - else - assert(socket:flush()) - consume_all() + -- method to ignore input and close connection after response: + function request:monologue() + prepare() + if + state == "headers_sent" or + state == "finished" + then + error("All HTTP headers have already been sent") + end + consume = drain + close_requested = true + if state == "init" or state == "prepare" then -- TODO: ok? + state = "no_status_sent" end end + -- -- method to send a HTTP response status (e.g. "200 OK"): function request:send_status(status) prepare() @@ -297,17 +332,6 @@ end assert_output(socket:write(key, ": ", value, "\r\n")) end - -- method to close connection after sending the response: - function request:close_after_finish() - prepare() - if - state == "headers_sent" or - state == "finished" - then - error("All HTTP headers have already been sent") - end - close_requested = true - end -- function to terminate header section in response, optionally flushing: -- (may be called multiple times unless response is finished) local function finish_headers(with_flush) @@ -321,7 +345,7 @@ request:send_header("Connection", "close") end send("\r\n") - --finish_response() -- TODO + finish() state = "finished" elseif state == "status_sent" then if not content_length then @@ -332,7 +356,7 @@ end send("\r\n") if request.method == "HEAD" then - --finish_response() -- TODO + finish() elseif with_flush then send_flush() end @@ -346,10 +370,43 @@ prepare() finish_headers(true) end + -- function to report an error: + 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 + if + state == "init" or + state == "prepare" or + state == "no_status_sent" or + state == "info_status_sent" + then + local error_response_status, errmsg2 = pcall(function() + request:monologue() + request:send_status(status) + request:send_header("Content-Type", "text/plain") + request:send_data(status, "\n") + if text then + request:send_data("\n", text, "\n") + end + request:finish() + end) + if not error_response_status then + error("Unexpected error while sending error response: " .. errmsg2) + end + elseif state ~= "faulty" then + assert_close(socket:reset()) + end + if throw_error then + error(errmsg) + else + return survive + end + end -- wait for input: if not poll(socket_set, nil, request_idle_timeout) then - -- TODO: send error - return survive + return request_error(false, "408 Request Timeout", "Idle connection timed out") end until close_responded return survive