summaryrefslogtreecommitdiff
path: root/src/luarocks/upload
diff options
context:
space:
mode:
Diffstat (limited to 'src/luarocks/upload')
-rw-r--r--src/luarocks/upload/api.lua265
-rw-r--r--src/luarocks/upload/multipart.lua109
2 files changed, 374 insertions, 0 deletions
diff --git a/src/luarocks/upload/api.lua b/src/luarocks/upload/api.lua
new file mode 100644
index 0000000..e141370
--- /dev/null
+++ b/src/luarocks/upload/api.lua
@@ -0,0 +1,265 @@
+
+local api = {}
+
+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 Api = {}
+
+local function upload_config_file()
+ 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()
+ local upload_conf = upload_config_file()
+ if not upload_conf then return nil end
+ local config, err = 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(path, ...)
+ 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(url, params, post_params)
+ 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=<key> flag."
+ end
+ if args.api_key then
+ self:save_config()
+ end
+ return self
+end
+
+return api
diff --git a/src/luarocks/upload/multipart.lua b/src/luarocks/upload/multipart.lua
new file mode 100644
index 0000000..56ae873
--- /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
+