Initial commit
This commit is contained in:
3
pegasus/compress.lua
Normal file
3
pegasus/compress.lua
Normal file
@@ -0,0 +1,3 @@
|
||||
-- compatibility shim for moving the plugin into a 'plugins' subfolder
|
||||
-- without breaking anything
|
||||
return require("pegasus.plugins.compress")
|
||||
151
pegasus/handler.lua
Normal file
151
pegasus/handler.lua
Normal file
@@ -0,0 +1,151 @@
|
||||
local Request = require 'pegasus.request'
|
||||
local Response = require 'pegasus.response'
|
||||
local mimetypes = require 'mimetypes'
|
||||
local lfs = require 'lfs'
|
||||
|
||||
local function ternary(condition, t, f)
|
||||
if condition then return t else return f end
|
||||
end
|
||||
|
||||
local Handler = {}
|
||||
Handler.__index = Handler
|
||||
|
||||
function Handler:new(callback, location, plugins)
|
||||
local handler = {}
|
||||
handler.callback = callback
|
||||
handler.location = location or ''
|
||||
handler.plugins = plugins or {}
|
||||
|
||||
local result = setmetatable(handler, self)
|
||||
result:pluginsAlterRequestResponseMetatable()
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
function Handler:pluginsAlterRequestResponseMetatable()
|
||||
for _, plugin in ipairs(self.plugins) do
|
||||
if plugin.alterRequestResponseMetaTable then
|
||||
local stop = plugin:alterRequestResponseMetaTable(Request, Response)
|
||||
if stop then
|
||||
return stop
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Handler:pluginsNewRequestResponse(request, response)
|
||||
for _, plugin in ipairs(self.plugins) do
|
||||
if plugin.newRequestResponse then
|
||||
local stop = plugin:newRequestResponse(request, response)
|
||||
if stop then
|
||||
return stop
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Handler:pluginsBeforeProcess(request, response)
|
||||
for _, plugin in ipairs(self.plugins) do
|
||||
if plugin.beforeProcess then
|
||||
local stop = plugin:beforeProcess(request, response)
|
||||
if stop then
|
||||
return stop
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Handler:pluginsAfterProcess(request, response)
|
||||
for _, plugin in ipairs(self.plugins) do
|
||||
if plugin.afterProcess then
|
||||
local stop = plugin:afterProcess(request, response)
|
||||
if stop then
|
||||
return stop
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Handler:pluginsProcessFile(request, response, filename)
|
||||
for _, plugin in ipairs(self.plugins) do
|
||||
if plugin.processFile then
|
||||
local stop = plugin:processFile(request, response, filename)
|
||||
|
||||
if stop then
|
||||
return stop
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Handler:processBodyData(data, stayOpen, response)
|
||||
local localData = data
|
||||
|
||||
for _, plugin in ipairs(self.plugins or {}) do
|
||||
if plugin.processBodyData then
|
||||
localData = plugin:processBodyData(
|
||||
localData,
|
||||
stayOpen,
|
||||
response.request,
|
||||
response
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
return localData
|
||||
end
|
||||
|
||||
function Handler:processRequest(port, client, server)
|
||||
local request = Request:new(port, client, server)
|
||||
|
||||
if not request:method() then
|
||||
client:close()
|
||||
return
|
||||
end
|
||||
|
||||
local response = Response:new(client, self)
|
||||
response.request = request
|
||||
local stop = self:pluginsNewRequestResponse(request, response)
|
||||
|
||||
if stop then
|
||||
return
|
||||
end
|
||||
|
||||
if request:path() and self.location ~= '' then
|
||||
local path = ternary(request:path() == '/' or request:path() == '', 'index.html', request:path())
|
||||
local filename = '.' .. self.location .. path
|
||||
|
||||
if not lfs.attributes(filename) then
|
||||
response:statusCode(404)
|
||||
end
|
||||
|
||||
stop = self:pluginsProcessFile(request, response, filename)
|
||||
|
||||
if stop then
|
||||
return
|
||||
end
|
||||
|
||||
local file = io.open(filename, 'rb')
|
||||
|
||||
if file then
|
||||
response:writeFile(file, mimetypes.guess(filename or '') or 'text/html')
|
||||
else
|
||||
response:statusCode(404)
|
||||
end
|
||||
end
|
||||
|
||||
if self.callback then
|
||||
response:statusCode(200)
|
||||
response.headers = {}
|
||||
response:addHeader('Content-Type', 'text/html')
|
||||
|
||||
self.callback(request, response)
|
||||
end
|
||||
|
||||
if response.status == 404 and not response._isClosed then
|
||||
response:writeDefaultErrorMessage(404)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
return Handler
|
||||
63
pegasus/init.lua
Normal file
63
pegasus/init.lua
Normal file
@@ -0,0 +1,63 @@
|
||||
local socket = require 'socket'
|
||||
local Handler = require 'pegasus.handler'
|
||||
|
||||
local FCNTL = require 'posix.fcntl'
|
||||
local time = require 'posix.time'
|
||||
|
||||
local Pegasus = {}
|
||||
Pegasus.__index = Pegasus
|
||||
|
||||
function Pegasus:new(params)
|
||||
params = params or {}
|
||||
local server = {}
|
||||
|
||||
server.host = params.host or '*'
|
||||
server.port = params.port or '9090'
|
||||
server.location = params.location or ''
|
||||
server.plugins = params.plugins or {}
|
||||
server.timeout = params.timeout or 1
|
||||
server.threads = {}
|
||||
|
||||
return setmetatable(server, self)
|
||||
end
|
||||
|
||||
function Pegasus:start(callback)
|
||||
local handler = Handler:new(callback, self.location, self.plugins)
|
||||
local server = assert(socket.bind(self.host, self.port))
|
||||
local ip, port = server:getsockname()
|
||||
print('Pegasus is up on ' .. ip .. ":".. port)
|
||||
|
||||
FCNTL.fcntl(server:getfd(), FCNTL.F_SETFD, FCNTL.FD_CLOEXEC)
|
||||
--server:settimeout(0)
|
||||
|
||||
local i = 1
|
||||
while 1 do
|
||||
local client, errmsg = server:accept()
|
||||
|
||||
if client then
|
||||
FCNTL.fcntl(client:getfd(), FCNTL.F_SETFD, FCNTL.FD_CLOEXEC)
|
||||
client:settimeout(self.timeout, 'b')
|
||||
--local coro = coroutine.create(handler.processRequest)
|
||||
--coroutine.resume(coro, handler, self.port, client, server)
|
||||
--if coroutine.status(coro) ~= "dead" then
|
||||
-- self.threads[#self.threads + 1] = coro
|
||||
--end
|
||||
handler:processRequest(self.port, client, server)
|
||||
elseif errmsg ~= 'timeout' then
|
||||
io.stderr:write('Failed to accept connection:' .. errmsg .. '\n')
|
||||
end
|
||||
|
||||
--[[if #self.threads > 0 then
|
||||
coroutine.resume(self.threads[i])
|
||||
if coroutine.status(self.threads[i]) == "dead" then
|
||||
table.remove(self.threads, i)
|
||||
else
|
||||
i = i % #self.threads + 1
|
||||
end
|
||||
else
|
||||
time.nanosleep({tv_sec = 0, tv_nsec = 500000})
|
||||
end]]
|
||||
end
|
||||
end
|
||||
|
||||
return Pegasus
|
||||
145
pegasus/plugins/compress.lua
Normal file
145
pegasus/plugins/compress.lua
Normal file
@@ -0,0 +1,145 @@
|
||||
local zlib = require "zlib"
|
||||
|
||||
local function zlib_name(lib)
|
||||
if lib._VERSION and string.find(lib._VERSION, 'lua-zlib', nil, true) then
|
||||
return 'lua-zlib'
|
||||
end
|
||||
|
||||
if lib._VERSION and string.find(lib._VERSION, 'lzlib', nil, true) then
|
||||
return 'lzlib'
|
||||
end
|
||||
end
|
||||
|
||||
local z_lib_name = assert(zlib_name(zlib), 'Unsupported zlib Lua binding')
|
||||
|
||||
local ZlibStream = {} do
|
||||
ZlibStream.__index = ZlibStream
|
||||
|
||||
ZlibStream.NO_COMPRESSION = zlib.NO_COMPRESSION or 0
|
||||
ZlibStream.BEST_SPEED = zlib.BEST_SPEED or 1
|
||||
ZlibStream.BEST_COMPRESSION = zlib.BEST_COMPRESSION or 9
|
||||
ZlibStream.DEFAULT_COMPRESSION = zlib.DEFAULT_COMPRESSION or -1
|
||||
ZlibStream.STORE = 0
|
||||
ZlibStream.DEFLATE = 8
|
||||
|
||||
if z_lib_name == 'lzlib' then
|
||||
function ZlibStream:new(writer, level, method, windowBits)
|
||||
level = level or ZlibStream.DEFAULT_COMPRESSION
|
||||
method = method or ZlibStream.DEFLATE
|
||||
|
||||
local o = setmetatable({
|
||||
zd = assert(zlib.deflate(writer, level, method, windowBits));
|
||||
}, self)
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
function ZlibStream:write(chunk)
|
||||
assert(self.zd:write(chunk))
|
||||
end
|
||||
|
||||
function ZlibStream:close()
|
||||
self.zd:close()
|
||||
end
|
||||
|
||||
elseif z_lib_name == 'lua-zlib' then
|
||||
function ZlibStream:new(writer, level, method, windowBits)
|
||||
level = level or ZlibStream.DEFAULT_COMPRESSION
|
||||
method = method or ZlibStream.DEFLATE
|
||||
|
||||
assert(method == ZlibStream.DEFLATE, 'lua-zlib support only deflated method')
|
||||
|
||||
local o = setmetatable({
|
||||
zd = assert(zlib.deflate(level, windowBits));
|
||||
writer = writer;
|
||||
}, self)
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
function ZlibStream:write(chunk)
|
||||
chunk = assert(self.zd(chunk))
|
||||
self.writer(chunk)
|
||||
end
|
||||
|
||||
function ZlibStream:close()
|
||||
local chunk = self.zd('', 'finish')
|
||||
if chunk and #chunk > 0 then self.writer(chunk) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local Compress = {} do
|
||||
Compress.__index = Compress
|
||||
|
||||
Compress.NO_COMPRESSION = ZlibStream.NO_COMPRESSION
|
||||
Compress.BEST_SPEED = ZlibStream.BEST_SPEED
|
||||
Compress.BEST_COMPRESSION = ZlibStream.BEST_COMPRESSION
|
||||
Compress.DEFAULT_COMPRESSION = ZlibStream.DEFAULT_COMPRESSION
|
||||
|
||||
function Compress:new(options)
|
||||
local compress = {}
|
||||
compress.options = options or {}
|
||||
|
||||
return setmetatable(compress, self)
|
||||
end
|
||||
|
||||
function Compress:processBodyData(data, stayOpen, request, response)
|
||||
local accept_encoding
|
||||
|
||||
if response.headersSended then
|
||||
accept_encoding = response.headers['Content-Encoding'] or ''
|
||||
else
|
||||
local headers = request:headers()
|
||||
accept_encoding = headers and headers['Accept-Encoding'] or ''
|
||||
end
|
||||
|
||||
local accept_gzip = not not accept_encoding:find('gzip', nil, true)
|
||||
|
||||
if accept_gzip and self.options.level ~= ZlibStream.NO_COMPRESSION then
|
||||
local stream = response.compress_stream
|
||||
local buffer = response.compress_buffer
|
||||
|
||||
if not stream then
|
||||
local writer = function (zdata) buffer[#buffer + 1] = zdata end
|
||||
stream, buffer = ZlibStream:new(writer, self.options.level, nil, 31), {}
|
||||
end
|
||||
|
||||
if stayOpen then
|
||||
if data == nil then
|
||||
stream:close()
|
||||
response.compress_stream = nil
|
||||
response.compress_buffer = nil
|
||||
else
|
||||
stream:write(data)
|
||||
response.compress_stream = stream
|
||||
response.compress_buffer = buffer
|
||||
end
|
||||
|
||||
local compressed = table.concat(buffer)
|
||||
for i = 1, #buffer do buffer[i] = nil end
|
||||
if not response.headersSended then
|
||||
response:addHeader('Content-Encoding', 'gzip')
|
||||
end
|
||||
|
||||
return compressed
|
||||
end
|
||||
|
||||
stream:write(data)
|
||||
stream:close()
|
||||
local compressed = table.concat(buffer)
|
||||
|
||||
if #compressed < #data then
|
||||
if not response.headersSended then
|
||||
response:addHeader('Content-Encoding', 'gzip')
|
||||
end
|
||||
|
||||
return compressed
|
||||
end
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
end
|
||||
|
||||
return Compress
|
||||
277
pegasus/request.lua
Normal file
277
pegasus/request.lua
Normal file
@@ -0,0 +1,277 @@
|
||||
local function normalizePath(path)
|
||||
local value = string.gsub(path ,'\\', '/')
|
||||
value = string.gsub(value, '^/*', '/')
|
||||
value = string.gsub(value, '(/%.%.?)$', '%1/')
|
||||
value = string.gsub(value, '/%./', '/')
|
||||
value = string.gsub(value, '/+', '/')
|
||||
|
||||
while true do
|
||||
local first, last = string.find(value, '/[^/]+/%.%./')
|
||||
if not first then break end
|
||||
value = string.sub(value, 1, first) .. string.sub(value, last + 1)
|
||||
end
|
||||
|
||||
while true do
|
||||
local n
|
||||
value, n = string.gsub(value, '^/%.%.?/', '/')
|
||||
if n == 0 then break end
|
||||
end
|
||||
|
||||
while true do
|
||||
local n
|
||||
value, n = string.gsub(value, '/%.%.?$', '/')
|
||||
if n == 0 then break end
|
||||
end
|
||||
|
||||
return value
|
||||
end
|
||||
|
||||
local Request = {}
|
||||
Request.__index = Request
|
||||
Request.PATTERN_METHOD = '^(.-)%s'
|
||||
Request.PATTERN_PATH = '(%S+)%s*'
|
||||
Request.PATTERN_PROTOCOL = '(HTTP%/%d%.%d)'
|
||||
Request.PATTERN_REQUEST = (Request.PATTERN_METHOD ..
|
||||
Request.PATTERN_PATH ..Request.PATTERN_PROTOCOL)
|
||||
Request.PATTERN_QUERY_STRING = '([^=]*)=([^&]*)&?'
|
||||
Request.PATTERN_HEADER = '([%w-]+): ([%w %p]+=?)'
|
||||
|
||||
function Request:new(port, client, server)
|
||||
local obj = {}
|
||||
obj.client = client
|
||||
obj.server = server
|
||||
obj.port = port
|
||||
obj.ip = client:getpeername()
|
||||
obj.querystring = {}
|
||||
obj._firstLine = nil
|
||||
obj._method = nil
|
||||
obj._path = nil
|
||||
obj._params = {}
|
||||
obj._headerParsed = false
|
||||
obj._headers = {}
|
||||
obj._contentDone = 0
|
||||
obj._contentLength = nil
|
||||
|
||||
return setmetatable(obj, self)
|
||||
end
|
||||
|
||||
function Request:parseFirstLine()
|
||||
if (self._firstLine ~= nil) then
|
||||
return
|
||||
end
|
||||
|
||||
local status, partial
|
||||
self._firstLine, status, partial = self.client:receive()
|
||||
|
||||
if (self._firstLine == nil or status == 'timeout' or partial == '' or status == 'closed') then
|
||||
return
|
||||
end
|
||||
|
||||
-- Parse firstline http: METHOD PATH
|
||||
-- GET Makefile HTTP/1.1
|
||||
local method, path = string.match(self._firstLine, Request.PATTERN_REQUEST)
|
||||
|
||||
if not method then
|
||||
self.client:close()
|
||||
return
|
||||
end
|
||||
|
||||
print(os.date() .. ";" .. self.client:getpeername() .. ';' .. method .. ";" .. path)
|
||||
|
||||
local filename = ''
|
||||
local querystring = ''
|
||||
|
||||
if #path then
|
||||
filename, querystring = string.match(path, '^([^#?]+)[#|?]?(.*)')
|
||||
filename = normalizePath(filename)
|
||||
end
|
||||
|
||||
self._path = filename
|
||||
self._method = method
|
||||
self.querystring = self:parseUrlEncoded(querystring)
|
||||
end
|
||||
|
||||
function Request:parseUrlEncoded(data)
|
||||
local output = {}
|
||||
|
||||
if data then
|
||||
for key, value in string.gmatch(data, Request.PATTERN_QUERY_STRING) do
|
||||
if key and value then
|
||||
local v = output[key]
|
||||
if not v then
|
||||
output[key] = value
|
||||
elseif type(v) == "string" then
|
||||
output[key] = { v, value }
|
||||
else -- v is a table
|
||||
v[#v + 1] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return output
|
||||
end
|
||||
|
||||
function Request:parseMultipartFormData(data)
|
||||
local boundary = self:headers()["content-type"]:match("multipart/form%-data;%s+boundary=([^,]+)")
|
||||
|
||||
local output = {}
|
||||
|
||||
while true do
|
||||
data = data:sub(data:find("--" .. boundary) + 2 + #boundary)
|
||||
|
||||
if data:match("^--%s+$") then
|
||||
break
|
||||
end
|
||||
|
||||
local name, filename, filetype
|
||||
|
||||
local metadataHeaders = data:sub(1, data:find("\r\n\r\n", 5) + 1)
|
||||
for line in metadataHeaders:gmatch("([^\r]+)\r\n") do
|
||||
local key, value = line:match(Request.PATTERN_HEADER)
|
||||
if not key then
|
||||
break
|
||||
elseif key:lower() == "content-disposition" then
|
||||
-- TODO: secure?
|
||||
name = value:match("; name=\"([^\"]+)\"")
|
||||
filename = value:match("; filename=\"([^\"]+)\"")
|
||||
elseif key:lower() == "content-type" then
|
||||
filetype = value
|
||||
end
|
||||
end
|
||||
|
||||
local itemdata = data:sub(data:find"\r\n\r\n" + 4, data:find("\r\n--" .. boundary) - 1)
|
||||
|
||||
if name then
|
||||
if filename then
|
||||
-- Files *must* have a type, else this ignores them
|
||||
if filetype then
|
||||
output[name] = {
|
||||
data = itemdata,
|
||||
filename = filename,
|
||||
type = filetype
|
||||
}
|
||||
end
|
||||
else
|
||||
output[name] = itemdata
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return output
|
||||
end
|
||||
|
||||
function Request:post()
|
||||
if self:method() ~= 'POST' then return nil end
|
||||
if self._postdata then return self._postdata end
|
||||
|
||||
local ct = self:headers()["content-type"]
|
||||
local data = self:receiveBody()
|
||||
if ct:sub(1, ct:find";") == "multipart/form-data;" then
|
||||
self._postdata = self:parseMultipartFormData(data)
|
||||
else
|
||||
self._postdata = self:parseUrlEncoded(data)
|
||||
end
|
||||
|
||||
return self._postdata
|
||||
end
|
||||
|
||||
function Request:path()
|
||||
self:parseFirstLine()
|
||||
return self._path
|
||||
end
|
||||
|
||||
function Request:method()
|
||||
self:parseFirstLine()
|
||||
return self._method
|
||||
end
|
||||
|
||||
function Request:headers()
|
||||
if self._headerParsed then
|
||||
return self._headers
|
||||
end
|
||||
|
||||
self:parseFirstLine()
|
||||
|
||||
local data = self.client:receive()
|
||||
|
||||
local headers = setmetatable({},{ -- add metatable to do case-insensitive lookup
|
||||
__index = function(self, key)
|
||||
if type(key) == "string" then
|
||||
key = key:lower()
|
||||
return rawget(self, key)
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
while (data ~= nil) and (data:len() > 0) do
|
||||
local key, value = string.match(data, Request.PATTERN_HEADER)
|
||||
|
||||
if key and value then
|
||||
key = key:lower()
|
||||
local v = headers[key]
|
||||
if not v then
|
||||
headers[key] = value
|
||||
elseif type(v) == "string" then
|
||||
headers[key] = { v, value }
|
||||
else -- t == "table", v is a table
|
||||
v[#v + 1] = value
|
||||
end
|
||||
end
|
||||
|
||||
data = self.client:receive()
|
||||
end
|
||||
|
||||
self._headerParsed = true
|
||||
self._contentLength = tonumber(headers["content-length"] or 0)
|
||||
self._headers = headers
|
||||
|
||||
return headers
|
||||
end
|
||||
|
||||
function Request:cookie(name)
|
||||
if not self:headers()["cookie"] then
|
||||
return nil
|
||||
end
|
||||
|
||||
return self:headers()["cookie"]:match(name:gsub("([^%w])", "%%%1") .. "=([^%;]*)")
|
||||
end
|
||||
|
||||
function Request:receiveBody(size)
|
||||
-- do we have content?
|
||||
if (self._contentLength == nil) or (self._contentDone >= self._contentLength) then
|
||||
return false
|
||||
end
|
||||
|
||||
--[[local leftToLoad = self._contentLength
|
||||
local ret = {}
|
||||
|
||||
local maxEmpties = 15
|
||||
|
||||
while leftToLoad > 0 do
|
||||
local chunkSz = math.min(512 * 1024, leftToLoad)
|
||||
local data, err, partial = self.client:receive(chunkSz)
|
||||
if err == 'timeout' then
|
||||
data = partial
|
||||
end
|
||||
table.insert(ret, data)
|
||||
leftToLoad = leftToLoad - #data
|
||||
|
||||
if leftToLoad > 0 then
|
||||
coroutine.yield()
|
||||
end
|
||||
end
|
||||
|
||||
return table.concat(ret)]]
|
||||
|
||||
local data, err, partial = self.client:receive(self._contentLength)
|
||||
if err == 'timeout' then
|
||||
data = partial
|
||||
end
|
||||
|
||||
self._contentDone = self._contentLength
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
return Request
|
||||
238
pegasus/response.lua
Normal file
238
pegasus/response.lua
Normal file
@@ -0,0 +1,238 @@
|
||||
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
|
||||
Reference in New Issue
Block a user