ikibooru/pegasus/request.lua

278 lines
6.5 KiB
Lua
Raw Normal View History

2024-06-01 17:40:11 +03:00
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