239 lines
6.0 KiB
Lua
239 lines
6.0 KiB
Lua
|
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 = [[
|
||
|
<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01//EN'
|
||
|
'http://www.w3.org/TR/html4/strict.dtd'>
|
||
|
<html>
|
||
|
<head>
|
||
|
<meta http-equiv='Content-Type' content='text/html;charset=utf-8'>
|
||
|
<title>Error response</title>
|
||
|
</head>
|
||
|
<body>
|
||
|
<h1>Error response</h1>
|
||
|
<p>Error code: {{ STATUS_CODE }}</p>
|
||
|
<p>Message: {{ STATUS_TEXT }}</p>
|
||
|
</body>
|
||
|
</html>
|
||
|
]]
|
||
|
|
||
|
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
|