278 lines
6.5 KiB
Lua
278 lines
6.5 KiB
Lua
|
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
|