local function toHex(dec) local charset = { '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' } local tmp = {} repeat table.insert(tmp, 1, charset[dec % 16 + 1]) dec = math.floor(dec / 16) until dec == 0 return table.concat(tmp) end local STATUS_TEXT = setmetatable({ [100] = 'Continue', [101] = 'Switching Protocols', [200] = 'OK', [201] = 'Created', [202] = 'Accepted', [203] = 'Non-Authoritative Information', [204] = 'No Content', [205] = 'Reset Content', [206] = 'Partial Content', [300] = 'Multiple Choices', [301] = 'Moved Permanently', [302] = 'Found', [303] = 'See Other', [304] = 'Not Modified', [305] = 'Use Proxy', [307] = 'Temporary Redirect', [400] = 'Bad Request', [401] = 'Unauthorized', [402] = 'Payment Required', [403] = 'Forbidden', [404] = 'Not Found', [405] = 'Method Not Allowed', [406] = 'Not Acceptable', [407] = 'Proxy Authentication Required', [408] = 'Request Time-out', [409] = 'Conflict', [410] = 'Gone', [411] = 'Length Required', [412] = 'Precondition Failed', [413] = 'Request Entity Too Large', [414] = 'Request-URI Too Large', [415] = 'Unsupported Media Type', [416] = 'Requested range not satisfiable', [417] = 'Expectation Failed', [500] = 'Internal Server Error', [501] = 'Not Implemented', [502] = 'Bad Gateway', [503] = 'Service Unavailable', [504] = 'Gateway Time-out', [505] = 'HTTP Version not supported', }, { __index = function(self, statusCode) -- if the lookup failed, try coerce to a number and try again if type(statusCode) == "string" then local result = rawget(self, tonumber(statusCode) or -1) if result then return result end end error("http status code '"..tostring(statusCode).."' is unknown", 2) end, }) local DEFAULT_ERROR_MESSAGE = [[
Error code: {{ STATUS_CODE }}
Message: {{ STATUS_TEXT }}
]] local Response = {} Response.__index = Response function Response:new(client, writeHandler) local newObj = {} newObj._headersSended = false newObj._templateFirstLine = 'HTTP/1.1 {{ STATUS_CODE }} {{ STATUS_TEXT }}\r\n' newObj._headFirstLine = '' newObj._headers = {} newObj._isClosed = false newObj._client = client newObj._writeHandler = writeHandler newObj.status = 200 return setmetatable(newObj, self) end function Response:addHeader(key, value) assert(not self._headersSended, "can't add header, they were already sent") self._headers[key] = value return self end function Response:addHeaders(params) for key, value in pairs(params) do self:addHeader(key, value) end return self end function Response:contentType(value) return self:addHeader('Content-Type', value) end function Response:statusCode(statusCode, statusText) assert(not self._headersSended, "can't set status code, it was already sent") self.status = statusCode self._headFirstLine = string.gsub(self._templateFirstLine, '{{ STATUS_CODE }}', tostring(statusCode)) self._headFirstLine = string.gsub(self._headFirstLine, '{{ STATUS_TEXT }}', statusText or STATUS_TEXT[statusCode]) return self end function Response:_getHeaders() local headers = {} for header_name, header_value in pairs(self._headers) do if type(header_value) == "table" and #header_value > 0 then for _, sub_value in ipairs(header_value) do headers[#headers + 1] = header_name .. ': ' .. sub_value .. '\r\n' end else headers[#headers + 1] = header_name .. ': ' .. header_value .. '\r\n' end end return table.concat(headers) end function Response:writeDefaultErrorMessage(statusCode) self:statusCode(statusCode) local content = string.gsub(DEFAULT_ERROR_MESSAGE, '{{ STATUS_CODE }}', statusCode) self:write(string.gsub(content, '{{ STATUS_TEXT }}', STATUS_TEXT[statusCode]), false) return self end function Response:close() local body = self._writeHandler:processBodyData(nil, true, self) if body and #body > 0 then self._client:send(toHex(#body) .. '\r\n' .. body .. '\r\n') end self._client:send('0\r\n\r\n') self.close = true -- TODO: this seems unused?? return self end function Response:sendOnlyHeaders() self:sendHeaders(false, '') self:write('\r\n') return self end function Response:sendHeaders(stayOpen, body) if self._headersSended then return self end if stayOpen then self:addHeader('Transfer-Encoding', 'chunked') elseif type(body) == 'string' then self:addHeader('Content-Length', body:len()) end self:addHeader('Date', os.date('!%a, %d %b %Y %H:%M:%S GMT', os.time())) if not self._headers['Content-Type'] then self:addHeader('Content-Type', 'text/html') end self._headersSended = true self._client:send(self._headFirstLine .. self:_getHeaders() .. '\r\n') self._chunked = stayOpen return self end function Response:sendneck(data) --if #data <= 512*1024 then self._client:send(data) --[[else for i = 1, #data, 512*1024 do self._client:send(data:sub(i, math.min(i + 512*1024 - 1, #data))) coroutine.yield() end end]] end function Response:write(body, stayOpen) body = self._writeHandler:processBodyData(body or '', stayOpen, self) self:sendHeaders(stayOpen, body) self._isClosed = not stayOpen if self._isClosed then self:sendneck(body) elseif #body > 0 then self:sendneck(toHex(#body) .. '\r\n' .. body .. '\r\n') end if self._isClosed then self._client:close() -- TODO: remove this, a non-chunked body can also be sent in multiple pieces if self.status // 100 > 3 then print("Not OK: " .. self.status) end end return self end function Response:writeFile(file, contentType) self:contentType(contentType) self:statusCode(200) local value = file:read('*a') file:close() self:write(value) return self end return Response