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 if name:sub(#name - 1) == "[]" then if output[name] then table.insert(output[name], itemdata) else output[name] = {itemdata} end else output[name] = itemdata end 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