From 17f3ebdcb213e64ea9081991b29f0dfc94b1f910 Mon Sep 17 00:00:00 2001 From: V1K1NGbg Date: Thu, 1 Aug 2024 10:00:47 +0300 Subject: api + multipart rename to have a constant passing action --- src/luarocks/upload/api.tl | 271 +++++++++++++++++++++++++++++ src/luarocks/upload/multipart-original.lua | 109 ------------ src/luarocks/upload/multipart.lua | 109 ++++++++++++ 3 files changed, 380 insertions(+), 109 deletions(-) create mode 100644 src/luarocks/upload/api.tl delete mode 100644 src/luarocks/upload/multipart-original.lua create mode 100644 src/luarocks/upload/multipart.lua (limited to 'src') diff --git a/src/luarocks/upload/api.tl b/src/luarocks/upload/api.tl new file mode 100644 index 00000000..7c643c43 --- /dev/null +++ b/src/luarocks/upload/api.tl @@ -0,0 +1,271 @@ +local record api + +end + +local cfg = require("luarocks.core.cfg") +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local util = require("luarocks.util") +local persist = require("luarocks.persist") +local multipart = require("luarocks.upload.multipart") +local json = require("luarocks.vendor.dkjson") +local dir_sep = package.config:sub(1, 1) + +local record Api + raw_method: function(Api, string) + record config + server: any --! why to string + end +end + +local function upload_config_file(): string + if not cfg.config_files.user.file then + return nil + end + return (cfg.config_files.user.file:gsub("[\\/][^\\/]+$", dir_sep .. "upload_config.lua")) +end + +function Api:load_config(): {any: any} --? tighter bound? + local upload_conf = upload_config_file() + if not upload_conf then return nil end + local config = persist.load_into_table(upload_conf) + return config +end + +function Api:save_config() + -- Test configuration before saving it. + local res, err = self:raw_method("status") + if not res then + return nil, err + end + if res.errors then + util.printerr("Server says: " .. tostring(res.errors[1])) + return + end + local upload_conf = upload_config_file() + if not upload_conf then return nil end + local ok, err = fs.make_dir(dir.dir_name(upload_conf)) + if not ok then + return nil, err + end + persist.save_from_table(upload_conf, self.config) + fs.set_permissions(upload_conf, "read", "user") +end + +function Api:check_version() + if not self._server_tool_version then + local tool_version = cfg.upload.tool_version + local res, err = self:request(tostring(self.config.server) .. "/api/tool_version", { + current = tool_version + }) + if not res then + return nil, err + end + if not res.version then + return nil, "failed to fetch tool version" + end + self._server_tool_version = res.version + if res.force_update then + return nil, "Your upload client is too out of date to continue, please upgrade LuaRocks." + end + if res.version ~= tool_version then + util.warning("your LuaRocks is out of date, consider upgrading.") + end + end + return true +end + +function Api:method(...) + local res, err = self:raw_method(...) + if not res then + return nil, err + end + if res.errors then + if res.errors[1] == "Invalid key" then + return nil, res.errors[1] .. " (use the --api-key flag to change)" + end + local msg = table.concat(res.errors, ", ") + return nil, "API Failed: " .. msg + end + return res +end + +function Api.raw_method(self: Api, path, ...) --! path, ... type + self:check_version() + local url = tostring(self.config.server) .. "/api/" .. tostring(cfg.upload.api_version) .. "/" .. tostring(self.config.key) .. "/" .. tostring(path) + return self:request(url, ...) +end + +local function encode_query_string(t, sep) + if sep == nil then + sep = "&" + end + local i = 0 + local buf = { } + for k, v in pairs(t) do + if type(k) == "number" and type(v) == "table" then + k, v = v[1], v[2] + end + buf[i + 1] = multipart.url_escape(k) + buf[i + 2] = "=" + buf[i + 3] = multipart.url_escape(v) + buf[i + 4] = sep + i = i + 4 + end + buf[i] = nil + return table.concat(buf) +end + +local function redact_api_url(url) + url = tostring(url) + return (url:gsub(".*/api/[^/]+/[^/]+", "")) or "" +end + +local ltn12_ok, ltn12 = pcall(require, "ltn12") +if not ltn12_ok then -- If not using LuaSocket and/or LuaSec... + +function Api.request(self: Api, url: string, params?, post_params?): {string : any}, string + local vars = cfg.variables + + if fs.which_tool("downloader") == "wget" then + local curl_ok, err = fs.is_tool_available(vars.CURL, "curl") + if not curl_ok then + return nil, err + end + end + + if not self.config.key then + return nil, "Must have API key before performing any actions." + end + if params and next(params) then + url = url .. ("?" .. encode_query_string(params)) + end + local method = "GET" + local out + local tmpfile = fs.tmpname() + if post_params then + method = "POST" + local curl_cmd = vars.CURL.." "..vars.CURLNOCERTFLAG.." -f -L --silent --user-agent \""..cfg.user_agent.." via curl\" " + for k,v in pairs(post_params) do + local var = v + if type(v) == "table" then + var = "@"..v.fname + end + curl_cmd = curl_cmd .. "--form \""..k.."="..var.."\" " + end + if cfg.connection_timeout and cfg.connection_timeout > 0 then + curl_cmd = curl_cmd .. "--connect-timeout "..tonumber(cfg.connection_timeout).." " + end + local ok = fs.execute_string(curl_cmd..fs.Q(url).." -o "..fs.Q(tmpfile)) + if not ok then + return nil, "API failure: " .. redact_api_url(url) + end + else + local ok, err = fs.download(url, tmpfile) + if not ok then + return nil, "API failure: " .. tostring(err) .. " - " .. redact_api_url(url) + end + end + + local tmpfd = io.open(tmpfile) + if not tmpfd then + os.remove(tmpfile) + return nil, "API failure reading temporary file - " .. redact_api_url(url) + end + out = tmpfd:read("*a") + tmpfd:close() + os.remove(tmpfile) + + if self.debug then + util.printout("[" .. tostring(method) .. " via curl] " .. redact_api_url(url) .. " ... ") + end + + return json.decode(out) +end + +else -- use LuaSocket and LuaSec + +local warned_luasec = false + +function Api:request(url, params, post_params) + local server = tostring(self.config.server) + local http_ok, http + local via = "luasocket" + if server:match("^https://") then + http_ok, http = pcall(require, "ssl.https") + if http_ok then + via = "luasec" + else + if not warned_luasec then + util.printerr("LuaSec is not available; using plain HTTP. Install 'luasec' to enable HTTPS.") + warned_luasec = true + end + http_ok, http = pcall(require, "socket.http") + url = url:gsub("^https", "http") + via = "luasocket" + end + else + http_ok, http = pcall(require, "socket.http") + end + if not http_ok then + return nil, "Failed loading socket library!" + end + + if not self.config.key then + return nil, "Must have API key before performing any actions." + end + local body + local headers = {} + if params and next(params) then + url = url .. ("?" .. encode_query_string(params)) + end + if post_params then + local boundary + body, boundary = multipart.encode(post_params) + headers["Content-length"] = #body + headers["Content-type"] = "multipart/form-data; boundary=" .. tostring(boundary) + end + local method = post_params and "POST" or "GET" + if self.debug then + util.printout("[" .. tostring(method) .. " via "..via.."] " .. redact_api_url(url) .. " ... ") + end + local out = {} + local _, status = http.request({ + url = url, + headers = headers, + method = method, + sink = ltn12.sink.table(out), + source = body and ltn12.source.string(body) + }) + if self.debug then + util.printout(tostring(status)) + end + local pok, ret, err = pcall(json.decode, table.concat(out)) + if pok and ret then + return ret + end + return nil, "API returned " .. tostring(status) .. " - " .. redact_api_url(url) +end + +end + +function api.new(args) + local self = {} + setmetatable(self, { __index = Api }) + self.config = self:load_config() or {} + self.config.server = args.server or self.config.server or cfg.upload.server + self.config.version = self.config.version or cfg.upload.version + self.config.key = args.temp_key or args.api_key or self.config.key + self.debug = args.debug + if not self.config.key then + return nil, "You need an API key to upload rocks.\n" .. + "Navigate to "..self.config.server.."/settings to get a key\n" .. + "and then pass it through the --api-key= flag." + end + if args.api_key then + self:save_config() + end + return self +end + +return api diff --git a/src/luarocks/upload/multipart-original.lua b/src/luarocks/upload/multipart-original.lua deleted file mode 100644 index 790e368f..00000000 --- a/src/luarocks/upload/multipart-original.lua +++ /dev/null @@ -1,109 +0,0 @@ - -local multipart = {} - -local File = {} - -local unpack = unpack or table.unpack - --- socket.url.escape(s) from LuaSocket 3.0rc1 -function multipart.url_escape(s) - return (string.gsub(s, "([^A-Za-z0-9_])", function(c) - return string.format("%%%02x", string.byte(c)) - end)) -end - -function File:mime() - if not self.mimetype then - local mimetypes_ok, mimetypes = pcall(require, "mimetypes") - if mimetypes_ok then - self.mimetype = mimetypes.guess(self.fname) - end - self.mimetype = self.mimetype or "application/octet-stream" - end - return self.mimetype -end - -function File:content() - local fd = io.open(self.fname, "rb") - if not fd then - return nil, "Failed to open file: "..self.fname - end - local data = fd:read("*a") - fd:close() - return data -end - -local function rand_string(len) - local shuffled = {} - for i = 1, len do - local r = math.random(97, 122) - if math.random() >= 0.5 then - r = r - 32 - end - shuffled[i] = r - end - return string.char(unpack(shuffled)) -end - --- multipart encodes params --- returns encoded string,boundary --- params is an a table of tuple tables: --- params = { --- {key1, value2}, --- {key2, value2}, --- key3: value3 --- } -function multipart.encode(params) - local tuples = { } - for i = 1, #params do - tuples[i] = params[i] - end - for k,v in pairs(params) do - if type(k) == "string" then - table.insert(tuples, {k, v}) - end - end - local chunks = {} - for _, tuple in ipairs(tuples) do - local k,v = unpack(tuple) - k = multipart.url_escape(k) - local buffer = { 'Content-Disposition: form-data; name="' .. k .. '"' } - local content - if type(v) == "table" and v.__class == File then - buffer[1] = buffer[1] .. ('; filename="' .. v.fname:gsub(".*[/\\]", "") .. '"') - table.insert(buffer, "Content-type: " .. v:mime()) - content = v:content() - else - content = v - end - table.insert(buffer, "") - table.insert(buffer, content) - table.insert(chunks, table.concat(buffer, "\r\n")) - end - local boundary - while not boundary do - boundary = "Boundary" .. rand_string(16) - for _, chunk in ipairs(chunks) do - if chunk:find(boundary) then - boundary = nil - break - end - end - end - local inner = "\r\n--" .. boundary .. "\r\n" - return table.concat({ "--", boundary, "\r\n", - table.concat(chunks, inner), - "\r\n", "--", boundary, "--", "\r\n" }), boundary -end - -function multipart.new_file(fname, mime) - local self = {} - setmetatable(self, { __index = File }) - self.__class = File - self.fname = fname - self.mimetype = mime - return self -end - -return multipart - diff --git a/src/luarocks/upload/multipart.lua b/src/luarocks/upload/multipart.lua new file mode 100644 index 00000000..790e368f --- /dev/null +++ b/src/luarocks/upload/multipart.lua @@ -0,0 +1,109 @@ + +local multipart = {} + +local File = {} + +local unpack = unpack or table.unpack + +-- socket.url.escape(s) from LuaSocket 3.0rc1 +function multipart.url_escape(s) + return (string.gsub(s, "([^A-Za-z0-9_])", function(c) + return string.format("%%%02x", string.byte(c)) + end)) +end + +function File:mime() + if not self.mimetype then + local mimetypes_ok, mimetypes = pcall(require, "mimetypes") + if mimetypes_ok then + self.mimetype = mimetypes.guess(self.fname) + end + self.mimetype = self.mimetype or "application/octet-stream" + end + return self.mimetype +end + +function File:content() + local fd = io.open(self.fname, "rb") + if not fd then + return nil, "Failed to open file: "..self.fname + end + local data = fd:read("*a") + fd:close() + return data +end + +local function rand_string(len) + local shuffled = {} + for i = 1, len do + local r = math.random(97, 122) + if math.random() >= 0.5 then + r = r - 32 + end + shuffled[i] = r + end + return string.char(unpack(shuffled)) +end + +-- multipart encodes params +-- returns encoded string,boundary +-- params is an a table of tuple tables: +-- params = { +-- {key1, value2}, +-- {key2, value2}, +-- key3: value3 +-- } +function multipart.encode(params) + local tuples = { } + for i = 1, #params do + tuples[i] = params[i] + end + for k,v in pairs(params) do + if type(k) == "string" then + table.insert(tuples, {k, v}) + end + end + local chunks = {} + for _, tuple in ipairs(tuples) do + local k,v = unpack(tuple) + k = multipart.url_escape(k) + local buffer = { 'Content-Disposition: form-data; name="' .. k .. '"' } + local content + if type(v) == "table" and v.__class == File then + buffer[1] = buffer[1] .. ('; filename="' .. v.fname:gsub(".*[/\\]", "") .. '"') + table.insert(buffer, "Content-type: " .. v:mime()) + content = v:content() + else + content = v + end + table.insert(buffer, "") + table.insert(buffer, content) + table.insert(chunks, table.concat(buffer, "\r\n")) + end + local boundary + while not boundary do + boundary = "Boundary" .. rand_string(16) + for _, chunk in ipairs(chunks) do + if chunk:find(boundary) then + boundary = nil + break + end + end + end + local inner = "\r\n--" .. boundary .. "\r\n" + return table.concat({ "--", boundary, "\r\n", + table.concat(chunks, inner), + "\r\n", "--", boundary, "--", "\r\n" }), boundary +end + +function multipart.new_file(fname, mime) + local self = {} + setmetatable(self, { __index = File }) + self.__class = File + self.fname = fname + self.mimetype = mime + return self +end + +return multipart + -- cgit v1.2.3-55-g6feb