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@167: -- handling of GET/POST param tables: jbe@167: local new_params_list -- defined later jbe@167: do jbe@167: local params_list_mapping = setmetatable({}, {__mode="k"}) jbe@167: local function nextnonempty(tbl, key) jbe@167: while true do jbe@167: key = next(tbl, key) jbe@167: if key == nil then jbe@167: return nil jbe@167: end jbe@167: local value = tbl[key] jbe@167: if #value > 0 then jbe@167: return key, value jbe@167: end jbe@35: end jbe@35: end jbe@167: local function nextvalue(tbl, key) jbe@167: key = next(tbl, key) jbe@167: if key == nil then jbe@167: return nil jbe@167: end jbe@167: return key, tbl[key][1] jbe@167: end jbe@167: local params_list_metatable = { jbe@167: __index = function(self, key) jbe@167: local tbl = {} jbe@167: self[key] = tbl jbe@167: return tbl jbe@167: end, jbe@167: __pairs = function(self) jbe@167: return nextnonempty, self, nil jbe@167: end jbe@167: } jbe@167: local params_metatable = { jbe@167: __index = function(self, key) jbe@167: return params_list_mapping[self][key][1] jbe@167: end, jbe@167: __newindex = function(self, key, value) jbe@167: params_list_mapping[self][key] = {value} jbe@167: end, jbe@167: __pairs = function(self) jbe@167: return nextvalue, params_list_mapping[self], nil jbe@167: end jbe@167: } jbe@167: -- returns a table to store key value-list pairs (i.e. multiple values), jbe@167: -- and a second table automatically mapping keys to the first value jbe@167: -- using the key value-list pairs in the first table: jbe@167: new_params_list = function() jbe@167: local params_list = setmetatable({}, params_list_metatable) jbe@167: local params = setmetatable({}, params_metatable) jbe@167: params_list_mapping[params] = params_list jbe@167: return params_list, params jbe@167: end jbe@167: end jbe@167: jbe@167: -- parses URL encoded form data and stores it in jbe@167: -- a key value-list pairs structure that has to be jbe@167: -- previously obtained by calling by new_params_list(): jbe@167: local function read_urlencoded_form(tbl, data) jbe@167: for rawkey, rawvalue in string.gmatch(data, "([^?=&]*)=([^?=&]*)") do jbe@167: local subtbl = tbl[decode_uri(rawkey)] jbe@167: subtbl[#subtbl+1] = decode_uri(rawvalue) jbe@167: end jbe@0: end jbe@0: jbe@154: -- extracts first value from each subtable: jbe@154: local function get_first_values(tbl) jbe@154: local newtbl = {} jbe@154: for key, subtbl in pairs(tbl) do jbe@154: newtbl[key] = subtbl[1] jbe@0: end jbe@154: return newtbl jbe@154: end jbe@154: 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@160: -- helper function to process options: jbe@160: local function default(name, default_value) jbe@160: local value = options[name] jbe@160: if value == nil then jbe@160: return default_value jbe@160: else jbe@160: return value or nil jbe@159: end jbe@160: 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@160: 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@160: local header_size_limit = options.header_size_limit or 1024*1024 jbe@160: local body_size_limit = options.body_size_limit or 64*1024*1024 jbe@160: local request_idle_timeout = default("request_idle_timeout", 330) jbe@160: local request_header_timeout = default("request_header_timeout", 30) jbe@160: local request_body_timeout = default("request_body_timeout", 60) jbe@160: local request_response_timeout = default("request_response_timeout", 1800) jbe@160: local poll = options.poll_function or moonbridge_io.poll jbe@160: -- return socket handler: jbe@0: return function(socket) jbe@160: local socket_set = {[socket] = true} -- used for poll function jbe@0: local survive = true -- set to false if process shall be terminated later jbe@160: local consume -- function that reads some input if possible jbe@160: -- function that drains some input if possible: jbe@160: local function drain() jbe@163: local bytes, status = socket:drain_nb(input_chunk_size) jbe@163: if not bytes or status == "eof" then jbe@160: consume = nil jbe@50: end jbe@159: end jbe@163: -- function trying to unblock socket by reading: jbe@160: local function unblock() jbe@160: if consume then jbe@160: poll(socket_set, socket_set) jbe@160: consume() jbe@160: else jbe@160: poll(nil, socket_set) jbe@0: end jbe@154: end jbe@163: -- function that enforces consumption of all input: jbe@162: local function consume_all() jbe@162: while consume do jbe@163: poll(socket_set, nil) jbe@162: consume() jbe@162: end jbe@162: end jbe@163: -- handle requests in a loop: jbe@160: repeat jbe@168: -- copy limits: jbe@168: local remaining_header_size_limit = header_size_limit jbe@168: local remaining_body_size_limit = body_size_limit jbe@166: -- table for caching nil values: jbe@166: local headers_value_nil = {} jbe@162: -- create a new request object: jbe@166: local request -- allow references to local variable jbe@166: request = { jbe@165: -- allow access to underlying socket: jbe@0: socket = socket, jbe@165: -- cookies are simply stored in a table: jbe@165: cookies = {}, jbe@165: -- table mapping header field names to value-lists jbe@165: -- (raw access, but case-insensitive): jbe@165: headers = setmetatable({}, { jbe@165: __index = function(self, key) jbe@165: local lowerkey = string.lower(key) jbe@165: if lowerkey == key then jbe@165: return jbe@165: end jbe@165: local result = rawget(self, lowerkey) jbe@165: if result == nil then jbe@165: result = {} jbe@165: end jbe@165: self[lowerkey] = result jbe@165: self[key] = result jbe@165: return result jbe@165: end jbe@165: }), jbe@165: -- table mapping header field names to value-lists jbe@165: -- (for headers with comma separated values): jbe@165: headers_csv_table = setmetatable({}, { jbe@165: __index = function(self, key) jbe@165: local result = {} jbe@165: for i, line in ipairs(request.headers[key]) do jbe@165: for entry in string.gmatch(line, "[^,]+") do jbe@165: local value = string.match(entry, "^[ \t]*(..-)[ \t]*$") jbe@165: if value then jbe@165: result[#result+1] = value jbe@165: end jbe@165: end jbe@165: end jbe@165: self[key] = result jbe@165: return result jbe@165: end jbe@165: }), jbe@165: -- table mapping header field names to a comma separated string jbe@165: -- (for headers with comma separated values): jbe@165: headers_csv_string = setmetatable({}, { jbe@165: __index = function(self, key) jbe@165: local result = {} jbe@165: for i, line in ipairs(request.headers[key]) do jbe@165: result[#result+1] = line jbe@165: end jbe@165: result = string.concat(result, ", ") jbe@165: self[key] = result jbe@165: return result jbe@165: end jbe@165: }), jbe@165: -- table mapping header field names to a single string value jbe@165: -- (or false if header has been sent multiple times): jbe@165: headers_value = setmetatable({}, { jbe@165: __index = function(self, key) jbe@165: if headers_value_nil[key] then jbe@165: return nil jbe@165: end jbe@165: local result = nil jbe@165: local values = request.headers_csv_table[key] jbe@165: if #values == 0 then jbe@165: headers_value_nil[key] = true jbe@165: elseif #values == 1 then jbe@165: result = values[1] jbe@165: else jbe@165: result = false jbe@165: end jbe@165: self[key] = result jbe@165: return result jbe@165: end jbe@165: }), jbe@165: -- table mapping header field names to a flag table, jbe@165: -- indicating if the comma separated value contains certain entries: jbe@165: headers_flags = setmetatable({}, { jbe@165: __index = function(self, key) jbe@165: local result = setmetatable({}, { jbe@165: __index = function(self, key) jbe@165: local lowerkey = string.lower(key) jbe@165: local result = rawget(self, lowerkey) or false jbe@165: self[lowerkey] = result jbe@165: self[key] = result jbe@165: return result jbe@165: end jbe@165: }) jbe@165: for i, value in ipairs(request.headers_csv_table[key]) do jbe@165: result[string.lower(value)] = true jbe@165: end jbe@165: self[key] = result jbe@165: return result jbe@165: end jbe@165: }) jbe@0: } jbe@162: -- local variables to track the state: jbe@162: local state = "init" -- one of: jbe@162: -- "init" (initial state) jbe@163: -- "prepare" (configureation in preparation) jbe@162: -- "no_status_sent" (configuration complete) jbe@162: -- "info_status_sent" (1xx status code has been sent) jbe@162: -- "bodyless_status_sent" (204/304 status code has been sent) jbe@162: -- "status_sent" (regular status code has been sent) jbe@162: -- "headers_sent" (headers have been terminated) jbe@162: -- "finished" (request has been answered completely) jbe@163: -- "faulty" (I/O or protocaol error) jbe@162: local close_requested = false -- "Connection: close" requested jbe@162: local close_responded = false -- "Connection: close" sent jbe@162: local content_length = nil -- value of Content-Length header sent jbe@164: local chunk_parts = {} -- list of chunks to send jbe@164: local chunk_bytes = 0 -- sum of lengths of chunks to send jbe@163: -- functions to assert proper output/closing: jbe@163: local function assert_output(...) jbe@163: local retval, errmsg = ... jbe@163: if retval then return ... end jbe@163: state = "faulty" jbe@163: socket:reset() jbe@163: error("Could not send data to client: " .. errmsg) jbe@163: end jbe@163: local function assert_close(...) jbe@163: local retval, errmsg = ... jbe@163: if retval then return ... end jbe@163: state = "faulty" jbe@163: error("Could not finish sending data to client: " .. errmsg) jbe@163: end jbe@164: -- function to assert non-faulty handle: jbe@164: local function assert_not_faulty() jbe@164: assert(state ~= "faulty", "Tried to use faulty request handle") jbe@164: end jbe@162: -- functions to send data to the browser: jbe@160: local function send(...) jbe@163: assert_output(socket:write_call(unblock, ...)) jbe@38: end jbe@162: local function send_flush(...) jbe@163: assert_output(socket:flush_call(unblock, ...)) jbe@162: end jbe@163: -- function to finish request: jbe@163: local function finish() jbe@163: if close_responded then jbe@163: -- discard any input: jbe@163: consume = drain jbe@163: -- close output stream: jbe@163: send_flush() jbe@163: assert_close(socket:finish()) jbe@163: -- wait for EOF of peer to avoid immediate TCP RST condition: jbe@163: consume_all() jbe@163: -- fully close socket: jbe@163: assert_close(socket:close()) jbe@163: else jbe@163: send_flush() jbe@163: consume_all() jbe@163: end jbe@163: end jbe@164: -- function that writes out buffered chunks (without flushing the socket): jbe@164: function send_chunk() jbe@164: if chunk_bytes > 0 then jbe@164: assert_output(socket:write(string.format("%x\r\n", chunk_bytes))) jbe@164: for i = 1, #chunk_parts do -- TODO: evaluated only once? jbe@164: send(chunk_parts[i]) jbe@164: chunk_parts[i] = nil jbe@164: end jbe@164: chunk_bytes = 0 jbe@164: send("\r\n") jbe@164: end jbe@164: end jbe@168: -- function to report an error: jbe@168: local function request_error(throw_error, status, text) jbe@168: local errmsg = "Error while reading request from client. Error response: " .. status jbe@168: if text then jbe@168: errmsg = errmsg .. " (" .. text .. ")" jbe@168: end jbe@168: if jbe@168: state == "init" or jbe@168: state == "prepare" or jbe@168: state == "no_status_sent" or jbe@168: state == "info_status_sent" jbe@168: then jbe@168: local error_response_status, errmsg2 = pcall(function() jbe@168: request:monologue() jbe@168: request:send_status(status) jbe@168: request:send_header("Content-Type", "text/plain") jbe@168: request:send_data(status, "\n") jbe@168: if text then jbe@168: request:send_data("\n", text, "\n") jbe@168: end jbe@168: request:finish() jbe@168: end) jbe@168: if not error_response_status then jbe@168: error("Unexpected error while sending error response: " .. errmsg2) jbe@168: end jbe@168: elseif state ~= "faulty" then jbe@169: state = "faulty" jbe@168: assert_close(socket:reset()) jbe@168: end jbe@168: if throw_error then jbe@168: error(errmsg) jbe@168: else jbe@168: return survive jbe@168: end jbe@168: end jbe@170: -- read function jbe@170: local function read(...) jbe@170: local data, status = socket:read_yield(...) jbe@170: if data == nil then jbe@170: request_error(true, "400 Bad Request", "Read error") jbe@170: end jbe@170: if status == "eof" then jbe@170: request_error(true, "400 Bad Request", "Unexpected EOF") jbe@170: end jbe@170: return data jbe@170: end jbe@168: -- callback for request body streaming: jbe@168: local process_body_chunk jbe@168: -- reads a number of bytes from the socket, jbe@168: -- optionally feeding these bytes chunk-wise jbe@168: -- into a callback function: jbe@168: local function read_body_bytes(remaining) jbe@168: while remaining > 0 do jbe@168: local limit jbe@168: if remaining > input_chunk_size then jbe@168: limit = input_chunk_size jbe@168: else jbe@168: limit = remaining jbe@168: end jbe@170: local chunk = read(limit) jbe@168: remaining = remaining - limit jbe@168: if process_body_chunk then jbe@168: process_body_chunk(chunk) jbe@168: end jbe@168: end jbe@168: end jbe@168: -- coroutine for request body processing: jbe@168: local function read_body() jbe@168: if request.headers_flags["Transfer-Encoding"]["chunked"] then jbe@168: while true do jbe@170: local line = read(32 + remaining_body_size_limit, "\n") jbe@168: local zeros, lenstr = string.match(line, "^(0*)([1-9A-Fa-f]+[0-9A-Fa-f]*)\r?\n$") jbe@168: local chunkext jbe@168: if lenstr then jbe@168: chunkext = "" jbe@168: else jbe@168: zeros, lenstr, chunkext = string.match(line, "^(0*)([1-9A-Fa-f]+[0-9A-Fa-f]*)([ \t;].-)\r?\n$") jbe@168: end jbe@168: if not lenstr or #lenstr > 13 then jbe@168: request_error(true, "400 Bad Request", "Encoding error while reading chunk of request body") jbe@168: end jbe@168: local len = tonumber("0x" .. lenstr) jbe@168: remaining_body_size_limit = remaining_body_size_limit - (#zeros + #chunkext + len) jbe@168: if remaining_body_size_limit < 0 then jbe@168: request_error(true, "413 Request Entity Too Large", "Request body size limit exceeded") jbe@168: end jbe@168: if len == 0 then break end jbe@168: read_body_bytes(len) jbe@170: local term = read(2, "\n") jbe@168: if term ~= "\r\n" and term ~= "\n" then jbe@168: request_error(true, "400 Bad Request", "Encoding error while reading chunk of request body") jbe@168: end jbe@168: end jbe@168: while true do jbe@170: local line = read(2 + remaining_body_size_limit, "\n") jbe@168: if line == "\r\n" or line == "\n" then break end jbe@168: remaining_body_size_limit = remaining_body_size_limit - #line jbe@168: if remaining_body_size_limit < 0 then jbe@168: request_error(true, "413 Request Entity Too Large", "Request body size limit exceeded while reading trailer section of chunked request body") jbe@168: end jbe@168: end jbe@168: elseif request_body_content_length then jbe@168: read_body_bytes(request_body_content_length) jbe@168: end jbe@168: end jbe@164: -- function to prepare (or skip) body processing: jbe@162: local function prepare() jbe@164: assert_not_faulty() jbe@162: if state == "prepare" then jbe@164: error("Unexpected internal status in HTTP engine (recursive prepare)") jbe@162: elseif state ~= "init" then jbe@162: return jbe@162: end jbe@162: state = "prepare" jbe@162: -- TODO jbe@162: state = "no_status_sent" jbe@162: end jbe@163: -- method to ignore input and close connection after response: jbe@163: function request:monologue() jbe@164: assert_not_faulty() jbe@163: if jbe@163: state == "headers_sent" or jbe@163: state == "finished" jbe@163: then jbe@163: error("All HTTP headers have already been sent") jbe@163: end jbe@164: local old_state = state jbe@164: state = "faulty" jbe@163: consume = drain jbe@163: close_requested = true jbe@164: if old_state == "init" or old_state == "prepare" then -- TODO: ok? jbe@163: state = "no_status_sent" jbe@164: else jbe@164: state = old_state jbe@162: end jbe@162: end jbe@163: -- jbe@162: -- method to send a HTTP response status (e.g. "200 OK"): jbe@162: function request:send_status(status) jbe@162: prepare() jbe@164: local old_state = state jbe@164: state = "faulty" jbe@164: if old_state == "info_status_sent" then jbe@162: send_flush("\r\n") jbe@164: elseif old_state ~= "no_status_sent" then jbe@162: error("HTTP status has already been sent") jbe@162: end jbe@162: local status1 = string.sub(status, 1, 1) jbe@162: local status3 = string.sub(status, 1, 3) jbe@162: send("HTTP/1.1 ", status, "\r\n", preamble) jbe@162: local wrb = status_without_response_body[status3] jbe@162: if wrb then jbe@162: state = "bodyless_status_sent" jbe@162: if wrb == "zero_content_length" then jbe@162: request:send_header("Content-Length", 0) jbe@162: end jbe@162: elseif status1 == "1" then jbe@162: state = "info_status_sent" jbe@162: else jbe@162: state = "status_sent" jbe@162: end jbe@162: end jbe@162: -- method to send a HTTP response header: jbe@162: -- (key and value must be provided as separate args) jbe@162: function request:send_header(key, value) jbe@164: assert_not_faulty() jbe@164: if jbe@164: state == "init" or jbe@164: state == "prepare" or jbe@164: state == "no_status_sent" jbe@164: then jbe@162: error("HTTP status has not been sent yet") jbe@162: elseif jbe@164: state == "headers_sent" or jbe@164: state == "finished" jbe@162: then jbe@162: error("All HTTP headers have already been sent") jbe@162: end jbe@162: local key_lower = string.lower(key) jbe@162: if key_lower == "content-length" then jbe@162: if state == "info_status_sent" then jbe@162: error("Cannot set Content-Length for informational status response") jbe@162: end jbe@162: local cl = assert(tonumber(value), "Invalid content-length") jbe@162: if content_length == nil then jbe@162: content_length = cl jbe@162: elseif content_length == cl then jbe@162: return jbe@162: else jbe@162: error("Content-Length has been set multiple times with different values") jbe@162: end jbe@162: elseif key_lower == "connection" then jbe@162: for entry in string.gmatch(string.lower(value), "[^,]+") do jbe@162: if string.match(entry, "^[ \t]*close[ \t]*$") then jbe@162: if state == "info_status_sent" then jbe@162: error("Cannot set \"Connection: close\" for informational status response") jbe@162: end jbe@162: close_responded = true jbe@162: break jbe@162: end jbe@162: end jbe@162: end jbe@162: assert_output(socket:write(key, ": ", value, "\r\n")) jbe@162: end jbe@162: -- function to terminate header section in response, optionally flushing: jbe@162: -- (may be called multiple times unless response is finished) jbe@162: local function finish_headers(with_flush) jbe@162: if state == "finished" then jbe@162: error("Response has already been finished") jbe@162: elseif state == "info_status_sent" then jbe@162: send_flush("\r\n") jbe@162: state = "no_status_sent" jbe@162: elseif state == "bodyless_status_sent" then jbe@162: if close_requested and not close_responded then jbe@162: request:send_header("Connection", "close") jbe@162: end jbe@162: send("\r\n") jbe@163: finish() jbe@162: state = "finished" jbe@162: elseif state == "status_sent" then jbe@162: if not content_length then jbe@162: request:send_header("Transfer-Encoding", "chunked") jbe@162: end jbe@162: if close_requested and not close_responded then jbe@162: request:send_header("Connection", "close") jbe@162: end jbe@162: send("\r\n") jbe@162: if request.method == "HEAD" then jbe@163: finish() jbe@162: elseif with_flush then jbe@162: send_flush() jbe@162: end jbe@162: state = "headers_sent" jbe@162: elseif state ~= "headers_sent" then jbe@162: error("HTTP status has not been sent yet") jbe@162: end jbe@162: end jbe@162: -- method to finish and flush headers: jbe@162: function request:finish_headers() jbe@164: assert_not_faulty() jbe@162: finish_headers(true) jbe@162: end jbe@164: -- method to send body data: jbe@164: function request:send_data(...) jbe@164: assert_not_faulty() jbe@164: if output_state == "info_status_sent" then jbe@164: error("No (non-informational) HTTP status has been sent yet") jbe@164: elseif output_state == "bodyless_status_sent" then jbe@164: error("Cannot send response data for body-less status message") jbe@164: end jbe@164: finish_headers(false) jbe@164: if output_state ~= "headers_sent" then jbe@164: error("Unexpected internal status in HTTP engine") jbe@164: end jbe@164: if request.method == "HEAD" then jbe@164: return jbe@164: end jbe@164: for i = 1, select("#", ...) do jbe@164: local str = tostring(select(i, ...)) jbe@164: if #str > 0 then jbe@164: if content_length then jbe@164: local bytes_to_send = #str jbe@164: if bytes_sent + bytes_to_send > content_length then jbe@164: error("Content length exceeded") jbe@164: else jbe@164: send(str) jbe@164: bytes_sent = bytes_sent + bytes_to_send jbe@164: end jbe@164: else jbe@164: chunk_bytes = chunk_bytes + #str jbe@164: chunk_parts[#chunk_parts+1] = str jbe@164: end jbe@164: end jbe@164: end jbe@164: if chunk_bytes >= output_chunk_size then jbe@164: send_chunk() jbe@164: end jbe@164: end jbe@165: -- method to flush output buffer: jbe@165: function request:flush() jbe@165: assert_not_faulty() jbe@165: send_chunk() jbe@165: send_flush() jbe@165: end jbe@165: -- method to finish response: jbe@165: function request:finish() jbe@165: assert_not_faulty() jbe@165: if state == "finished" then jbe@165: return jbe@165: elseif state == "info_status_sent" then jbe@165: error("Informational HTTP response can be finished with :finish_headers() method") jbe@165: end jbe@165: finish_headers(false) jbe@165: if state == "headers_sent" then jbe@165: if request.method ~= "HEAD" then jbe@165: state = "faulty" jbe@165: if content_length then jbe@165: if bytes_sent ~= content_length then jbe@165: error("Content length not used") jbe@165: end jbe@165: else jbe@165: send_chunk() jbe@165: send("0\r\n\r\n") jbe@165: end jbe@165: finish() jbe@165: end jbe@165: state = "finished" jbe@165: elseif state ~= "finished" then jbe@165: error("Unexpected internal status in HTTP engine") jbe@165: end jbe@165: end jbe@160: -- wait for input: jbe@160: if not poll(socket_set, nil, request_idle_timeout) then jbe@163: return request_error(false, "408 Request Timeout", "Idle connection timed out") jbe@38: end jbe@162: until close_responded jbe@0: return survive jbe@0: end jbe@0: end jbe@0: jbe@0: return _M jbe@0: