# HG changeset patch # User jbe # Date 1434050249 -7200 # Node ID 6c1561547f79217bcb813828aabe4d2a504a680f # Parent d5b8295d035eb83aba97b8ccefe5a2f8ed610b42 Work on new HTTP module implementation diff -r d5b8295d035e -r 6c1561547f79 moonbridge_http.lua --- a/moonbridge_http.lua Thu Jun 11 00:54:18 2015 +0200 +++ b/moonbridge_http.lua Thu Jun 11 21:17:29 2015 +0200 @@ -176,20 +176,182 @@ poll(nil, socket_set) end end + local function consume_all() + while consume do + consume() + end + end repeat + -- create a new request object: local request = { socket = socket, cookies = {} } + -- local variables to track the state: + local state = "init" -- one of: + -- "init" (initial state) + -- "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) + 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 send data to the browser: local function send(...) assert(socket:write_call(unblock, ...)) end + local function send_flush(...) + assert(socket:flush_call(unblock, ...)) + end + -- TODO: + local function prepare() + if state == "prepare" then + error("Unexpected internal status in HTTP engine") + elseif state ~= "init" then + return + end + state = "prepare" + -- 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() + end + end + -- method to send a HTTP response status (e.g. "200 OK"): + function request:send_status(status) + prepare() + if state == "info_status_sent" then + send_flush("\r\n") + elseif state ~= "no_status_sent" then + error("HTTP status has already been sent") + end + local status1 = string.sub(status, 1, 1) + local status3 = string.sub(status, 1, 3) + send("HTTP/1.1 ", status, "\r\n", preamble) + local wrb = status_without_response_body[status3] + if wrb then + state = "bodyless_status_sent" + if wrb == "zero_content_length" then + request:send_header("Content-Length", 0) + end + elseif status1 == "1" then + state = "info_status_sent" + else + state = "status_sent" + end + end + -- method to send a HTTP response header: + -- (key and value must be provided as separate args) + function request:send_header(key, value) + prepare() + if state == "no_status_sent" then + error("HTTP status has not been sent yet") + elseif + state ~= "info_status_sent" and + state ~= "bodyless_status_sent" and + 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 state == "info_status_sent" then + error("Cannot set Content-Length for informational status response") + end + local cl = assert(tonumber(value), "Invalid content-length") + if content_length == nil then + content_length = cl + elseif content_length == cl 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 state == "info_status_sent" then + error("Cannot set \"Connection: close\" for informational status response") + end + close_responded = true + break + end + end + 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) + if state == "finished" then + error("Response has already been finished") + elseif state == "info_status_sent" then + send_flush("\r\n") + state = "no_status_sent" + elseif state == "bodyless_status_sent" then + if close_requested and not close_responded then + request:send_header("Connection", "close") + end + send("\r\n") + --finish_response() -- TODO + state = "finished" + elseif state == "status_sent" then + if not content_length then + request:send_header("Transfer-Encoding", "chunked") + end + if close_requested and not close_responded then + request:send_header("Connection", "close") + end + send("\r\n") + if request.method == "HEAD" then + --finish_response() -- TODO + elseif with_flush then + send_flush() + end + state = "headers_sent" + elseif state ~= "headers_sent" then + error("HTTP status has not been sent yet") + end + end + -- method to finish and flush headers: + function request:finish_headers() + prepare() + finish_headers(true) + end -- wait for input: if not poll(socket_set, nil, request_idle_timeout) then -- TODO: send error return survive end - until connection_close_responded + until close_responded return survive end end