-- Copyright (C) Lice Pan (aCayF) local md5 = ngx.md5 local base64 = ngx.encode_base64 local http_time = ngx.http_time local time_sec = ngx.time local tcp = ngx.socket.tcp local read_body = ngx.req.read_body local get_body_data = ngx.req.get_body_data local ngx_match = ngx.re.match local ngx_gmatch = ngx.re.gmatch local ngx_print = ngx.print local str_sub = string.sub local lower = string.lower local byte = string.byte local concat = table.concat local insert = table.insert local tostring = tostring local tonumber = tonumber local setmetatable = setmetatable local type = type local pairs = pairs local HTTP_1_1 = " HTTP/1.1\r\n" local _M = { _VERSION = '0.0.1' } local mt = { __index = _M } local host_list = { "v0.api.upyun.com", "v1.api.upyun.com", "v2.api.upyun.com", "v3.api.upyun.com" } local gmkerl_format = { type = { type = { "required", ["fix_width"] = "allowed", ["fix_height"] = "allowed", ["fix_width_or_height"] = "allowed", ["fix_both"] = "allowed", ["fix_max"] = "allowed", ["fix_min"] = "allowed", ["fix_scale"] = "allowed" }, value = { "required", "([1-9][0-9]*)|([1-9][0-9]*x[1-9][0-9]*)" }, quality = { "optional", "[1-9][0-9]*" }, unsharp = { "optional", ["true"] = "allowed", ["false"] = "allowed" }, ["exif-switch"] = { "optional", ["true"] = "allowed", ["false"] = "allowed" } }, thumbnail = { thumbnail = { "optional", "[A-Za-z0-9.]+" }, ["exif-switch"] = { "optional", ["true"] = "allowed", ["false"] = "allowed" } }, rotate = { rotate = { "required", ["auto"] = "allowed", ["90"] = "allowed", ["180"] = "allowed", ["270"] = "allowed" } }, crop = { crop = { "required", "[0-9]+,[0-9]+,[1-9][0-9]*,[1-9][0-9]*" }, ["exif-switch"] = { "optional", ["true"] = "allowed", ["false"] = "allowed" } } } local function _rev_headers(sock) -- return headers, err local headers = {} while true do local line = sock:receive() local m, err = ngx_match(line, [[^([\w-]+)\s*:\s*(.+)$|(^\s*$)]]) if err then return nil, "failed to parse received header " .. err end if not m then return nil, "invalid received header : " .. line end if m[3] then break end headers[m[1]] = m[2] end return headers end local function _receive_length(sock, length) -- return chunk, err local chunk, err = sock:receive(length) if not chunk then return nil, err end return chunk end local function _receive_chunked(sock, maxsize) -- return chunks, err local chunks = {} local size = 0 local done = false repeat local str, err = sock:receive() --receive until \r\n if not str then return nil, err end local length = tonumber(str, 16) if not length then return nil, "unable to read chunksize" end size = size + length if maxsize and size > maxsize then return nil, 'exceeds maxsize' end if length > 0 then local str, err = sock:receive(length) if not str then return nil, err end --print(str) insert(chunks, str) else done = true end -- read the \r\n sock:receive(2) until done return concat(chunks) end local function _receive(sock) -- return {}, err local line, err = sock:receive() if not line then return nil, err end local status = tonumber(str_sub(line, 10, 12)) local headers, err = _rev_headers(sock) if not headers then return nil, err end local length = tonumber(headers["Content-Length"]) local body, err --TODO local maxsize = 8096 local keepalive = true if length then body, err = _receive_length(sock, length) else local encoding = headers["Transfer-Encoding"] if encoding and lower(encoding) == "chunked" then body, err = _receive_chunked(sock, maxsize) end end if err then return nil, err end local connection = headers["Connection"] connection = connection and lower(connection) or nil if connection == "close" then keepalive = false end if keepalive then sock:setkeepalive() else sock:close() end if status ~= 200 then local info = body if not info then info = str_sub(line, 14, -1) end return nil, info end return { status = status, headers = headers, body = body } end local function _req_header(method, path, headers, extra) -- return req local req = { method, " " } -- Append path insert(req, path) -- Append version insert(req, HTTP_1_1) -- Append headers for key, value in pairs(headers) do insert(req, key .. ": " .. value .. "\r\n") end -- Append extra if extra ~= {} and extra ~= nil then for key, value in pairs(extra) do insert(req, key .. ": " .. value .. "\r\n") end end -- Close headers insert(req, "\r\n") return concat(req) end local function _request(sock, method, path, headers, body, extra) -- return {}, err local host = headers.Host if not sock then ngx.log(ngx.ERR, "sock not initialized yet") return nil, "not initialized yet" else ngx.log(ngx.INFO, "Upyun module request connect host : ", host) end sock:settimeout(20000) local ok, err = sock:connect(host, 80) if not ok then ngx.log(ngx.ERR, "sock:connect ERROR -> ", err) return nil, err end -- Build and send request header ngx.log(ngx.INFO, "Debug upyun module request header method : ", method) ngx.log(ngx.INFO, "Debug upyun module request header path : ", path) --print("Debug upyun module request header headers : ", headers) --print("Debug upyun module request header extra : ", extra) local header = _req_header(method, path, headers, extra) local bytes, err = sock:send(header) if not bytes then ngx.log(ngx.ERR, "sock:send ERR -> ", err) return nil, err end -- Send the body if there is one if body and body.content then local bytes, err = sock:send(body.content) if not bytes then ngx.log(ngx.ERR, "sock:send ERR -> ", err) return nil, err end end return _receive(sock) end local function _upyun_request(self, method, path, headers, body, extra) local sock = self.sock local author_mode = self.author_mode local length = headers["Content-Length"] local signature if author_mode == "U" then signature = md5(method .. "&" .. path .. "&" .. headers.Date .. "&" .. length .. "&" .. md5(self.passwd)) headers.Authorization = headers.Authorization .. signature end return _request(sock, method, path, headers, body, extra) end local function _is_dir(path) -- return true or false return str_sub(path, -1, -1) == "/" end local function _parse_gmkerl(gmkerl, extra) -- return ok, err local expect_format = true local format for k, v in pairs(gmkerl_format) do -- find the format if gmkerl[k] and gmkerl[k] ~= "" then if not expect_format then return nil, "duplicated format" end format = v expect_format = false end end if expect_format then return nil, "invalid gmkerl" end for k, v in pairs(format) do local value = tostring(gmkerl[k]) if value == "nil" or value == "" then if v[1] == "required" then return nil, "missing required arg : " .. k end --do nothing to optinal arg else if v[value] ~= "allowed" then if not v[2] then return nil, 'invalid value "' .. value .. '" to ' .. k end -- regex is stored in the v[2] local m, err = ngx_match(value, v[2]) if err then return nil, "error occurs when matching " .. value .. " with " .. v[2] end if not m then return nil, 'invalid value "' .. value .. '" to ' .. k end value = m[0] end extra["x-gmkerl-" .. k] = value end end return true end local function _format_path(path, legal_path) -- return path, err if not path or type(path) ~= "string" or path == "" then return nil, "invalid path : " .. path end if legal_path == "dir" and not _is_dir(path) then return nil, path .. " is not a legal directory name" end if legal_path == "file" and _is_dir(path) then return nil, path .. " is not a legal file name" end -- checking is not needed when legal_path is "dir_or_file" -- pre insert a "/" if needed path = str_sub(path, 1, 1) == "/" and path or "/" .. path return path end local function _parse_upyun_option(option, extra, content) -- return modified extra local mkdir = tostring(option.mkdir) local omd5 = tostring(option.md5) local secret = option.secret local otype = option.type if mkdir == "true" or mkdir == "false" then extra["Mkdir"] = mkdir end if omd5 == "true" then extra["Content-MD5"] = md5(content) end if type(secret) == "string" then extra["Content-Secret"] = secret end if type(otype) == "string" then extra["Content-Type"] = otype end end local function _parse_upyun_headers(headers, regex) -- return info, err if not headers or type(headers) ~= "table" then return nil, "missing recieved headers" end local info = {} for k, v in pairs(headers) do local m, err = ngx_match(k, regex) if err then return nil, "failed to parse upyun headers " .. err end if m then info[m[1]] = v end end return info end local function _parse_upyun_body(body) -- return info, err if not body or type(body) ~= "string" then return nil, "missing recieved body" end local iterator, err = ngx_gmatch(body, [[([^\n\t]+)(\n|\t|$)]]) if not iterator then return nil, err end local i = 1 local j = 1 local file local info = {} local key = { "name", "type", "size", "lastmodified" } while true do local m, err = iterator() if err then return nil, err end if not m then break end if j == 1 then info[i] = {} file = info[i] end file[key[j]] = m[1] j = j + 1 if m[2] == "\n" then if j ~= 5 then return nil, "invalid upyun body " .. body end i = i + 1 j = 1 end end return info end function _M.new(self, config) local user = config.user local passwd = config.passwd local endpoint = config.endpoint and tonumber(config.endpoint) + 1 or 1 local author = config.author and lower(config.author) or nil local localFilePath = config.localFilePath or nil -- add config para for upload file directly if not user or type(user) ~= "string" or user == "" then return nil, "invalid user" end if not passwd or type(passwd) ~= "string" or passwd == "" then return nil, "invalid passwd" end if endpoint > 4 then return nil, "invalid endpoint" end -- explicit "basic" sets author_mode to Basic local author_mode = "U" if author == "basic" then author_mode = "B" author = "Basic " .. base64(user .. ":" .. passwd) else author = "UpYun " .. user .. ":" end -- file to be uploaded is stored in the request body print("Upyun module new config para local file path : ", localFilePath) local file = nil local content = nil if localFilePath == nil then read_body() content = get_body_data() file = ngx.req.get_body_file() else file = localFilePath; end print("Upyun module new config file : ", file) if file then local f, err = io.open(file, "r") if not f then return nil, err end content = f:read("*a") f:close() end --if not content then -- return nil, "request body is expected" --end --TODO ngx.updatetime? local date = http_time(time_sec()) if not date then return nil, "failed to get current time" end local sock = tcp() if not sock then return nil, "failed to create a TCP socket" end return setmetatable({ sock = sock, user = user, passwd = passwd, author_mode = author_mode, headers = { Authorization = author, Host = host_list[endpoint], Date = date, ["Content-Length"] = "0" }, body = { content = content }, }, mt) end function _M.upload_file(self, path, gmkerl, option) -- return info, err local headers = self.headers local author = headers.Authorization local body = self.body local content = body.content local legal_path = "file" local extra = {} local ret, err path, err = _format_path(path, legal_path) if not path then return nil, err end if gmkerl and type(gmkerl) == "table" and gmkerl ~= {} then local ok, err = _parse_gmkerl(gmkerl, extra) if not ok then return nil, err end end -- file to be uploaded is stored in the request body if not content or content == "" then return nil, "request body is expected" end if option and type(option) == "table" then _parse_upyun_option(option, extra, content) end headers["Content-Length"] = tostring(#content) ret, err = _upyun_request(self, "PUT", path, headers, body, extra) if not ret then return nil, err end --print("Upyun module upload file request ret : ", ret) --print("Upyun module upload file parse response headers : ", ret) ret, err = _parse_upyun_headers(ret.headers, [[^x-upyun-([\w-]+)$]]) if not ret then return nil, err end --print("Upyun module upload file parse response headers ret : ", ret) -- write the original author back as header.Authorization -- may be changed in the _upyun_request() headers.Authorization = author headers["Content-Length"] = "0" --print("Upyun module upload file ret : ", ret) return ret end function _M.download_file(self, path) -- return file, err local headers = self.headers local author = headers.Authorization local legal_path = "file" local ret, err path, err = _format_path(path, legal_path) if not path then return nil, err end ret, err = _upyun_request(self, "GET", path, headers) if not ret then return nil, err end headers.Authorization = author return ret.body end function _M.get_fileinfo(self, path) -- return info, err local headers = self.headers local author = headers.Authorization local legal_path = "file" local ret, err path, err = _format_path(path, legal_path) if not path then return nil, err end ret, err = _upyun_request(self, "HEAD", path, headers) if not ret then return nil, err end ret, err = _parse_upyun_headers(ret.headers, [[^x-upyun-file-([\w-]+)$]]) if not ret then return nil, err end headers.Authorization = author return ret end function _M.remove_file(self, path) -- return ok, err local headers = self.headers local author = headers.Authorization local legal_path = "dir_or_file" local ret, err path, err = _format_path(path, legal_path) if not path then return nil, err end ret, err = _upyun_request(self, "DELETE", path, headers) if not ret then return nil, err end headers.Authorization = author return true end function _M.make_dir(self, path, option) -- return ok, err local headers = self.headers local author = headers.Authorization local legal_path = "dir" local ret, err path, err = _format_path(path, legal_path) if not path then return nil, err end local extra = { Folder = "true" } if option and type(option) == "table" then _parse_upyun_option(option, extra) end ret, err = _upyun_request(self, "POST", path, headers, nil, extra) if not ret then return nil, err end headers.Authorization = author return true end function _M.read_dir(self, path) -- return items, err local headers = self.headers local author = headers.Authorization local legal_path = "dir" local ret, err path, err = _format_path(path, legal_path) if not path then return nil, err end ret, err = _upyun_request(self, "GET", path, headers) if not ret then return nil, err end ret, err = _parse_upyun_body(ret.body) if not ret then return nil, err end headers.Authorization = author return ret end function _M.get_usage(self, path) -- return usage, err local headers = self.headers local author = headers.Authorization local legal_path = "dir_or_file" local ret, err path, err = _format_path(path, legal_path) if not path then return nil, err end path = path .. "?usage" ret, err = _upyun_request(self, "GET", path, headers) if not ret then return nil, err end headers.Authorization = author return ret.body end return _M