diff options
Diffstat (limited to 'src/luarocks/upload/api.tl')
-rw-r--r-- | src/luarocks/upload/api.tl | 265 |
1 files changed, 265 insertions, 0 deletions
diff --git a/src/luarocks/upload/api.tl b/src/luarocks/upload/api.tl new file mode 100644 index 00000000..e1413702 --- /dev/null +++ b/src/luarocks/upload/api.tl | |||
@@ -0,0 +1,265 @@ | |||
1 | |||
2 | local api = {} | ||
3 | |||
4 | local cfg = require("luarocks.core.cfg") | ||
5 | local fs = require("luarocks.fs") | ||
6 | local dir = require("luarocks.dir") | ||
7 | local util = require("luarocks.util") | ||
8 | local persist = require("luarocks.persist") | ||
9 | local multipart = require("luarocks.upload.multipart") | ||
10 | local json = require("luarocks.vendor.dkjson") | ||
11 | local dir_sep = package.config:sub(1, 1) | ||
12 | |||
13 | local Api = {} | ||
14 | |||
15 | local function upload_config_file() | ||
16 | if not cfg.config_files.user.file then | ||
17 | return nil | ||
18 | end | ||
19 | return (cfg.config_files.user.file:gsub("[\\/][^\\/]+$", dir_sep .. "upload_config.lua")) | ||
20 | end | ||
21 | |||
22 | function Api:load_config() | ||
23 | local upload_conf = upload_config_file() | ||
24 | if not upload_conf then return nil end | ||
25 | local config, err = persist.load_into_table(upload_conf) | ||
26 | return config | ||
27 | end | ||
28 | |||
29 | function Api:save_config() | ||
30 | -- Test configuration before saving it. | ||
31 | local res, err = self:raw_method("status") | ||
32 | if not res then | ||
33 | return nil, err | ||
34 | end | ||
35 | if res.errors then | ||
36 | util.printerr("Server says: " .. tostring(res.errors[1])) | ||
37 | return | ||
38 | end | ||
39 | local upload_conf = upload_config_file() | ||
40 | if not upload_conf then return nil end | ||
41 | local ok, err = fs.make_dir(dir.dir_name(upload_conf)) | ||
42 | if not ok then | ||
43 | return nil, err | ||
44 | end | ||
45 | persist.save_from_table(upload_conf, self.config) | ||
46 | fs.set_permissions(upload_conf, "read", "user") | ||
47 | end | ||
48 | |||
49 | function Api:check_version() | ||
50 | if not self._server_tool_version then | ||
51 | local tool_version = cfg.upload.tool_version | ||
52 | local res, err = self:request(tostring(self.config.server) .. "/api/tool_version", { | ||
53 | current = tool_version | ||
54 | }) | ||
55 | if not res then | ||
56 | return nil, err | ||
57 | end | ||
58 | if not res.version then | ||
59 | return nil, "failed to fetch tool version" | ||
60 | end | ||
61 | self._server_tool_version = res.version | ||
62 | if res.force_update then | ||
63 | return nil, "Your upload client is too out of date to continue, please upgrade LuaRocks." | ||
64 | end | ||
65 | if res.version ~= tool_version then | ||
66 | util.warning("your LuaRocks is out of date, consider upgrading.") | ||
67 | end | ||
68 | end | ||
69 | return true | ||
70 | end | ||
71 | |||
72 | function Api:method(...) | ||
73 | local res, err = self:raw_method(...) | ||
74 | if not res then | ||
75 | return nil, err | ||
76 | end | ||
77 | if res.errors then | ||
78 | if res.errors[1] == "Invalid key" then | ||
79 | return nil, res.errors[1] .. " (use the --api-key flag to change)" | ||
80 | end | ||
81 | local msg = table.concat(res.errors, ", ") | ||
82 | return nil, "API Failed: " .. msg | ||
83 | end | ||
84 | return res | ||
85 | end | ||
86 | |||
87 | function Api:raw_method(path, ...) | ||
88 | self:check_version() | ||
89 | local url = tostring(self.config.server) .. "/api/" .. tostring(cfg.upload.api_version) .. "/" .. tostring(self.config.key) .. "/" .. tostring(path) | ||
90 | return self:request(url, ...) | ||
91 | end | ||
92 | |||
93 | local function encode_query_string(t, sep) | ||
94 | if sep == nil then | ||
95 | sep = "&" | ||
96 | end | ||
97 | local i = 0 | ||
98 | local buf = { } | ||
99 | for k, v in pairs(t) do | ||
100 | if type(k) == "number" and type(v) == "table" then | ||
101 | k, v = v[1], v[2] | ||
102 | end | ||
103 | buf[i + 1] = multipart.url_escape(k) | ||
104 | buf[i + 2] = "=" | ||
105 | buf[i + 3] = multipart.url_escape(v) | ||
106 | buf[i + 4] = sep | ||
107 | i = i + 4 | ||
108 | end | ||
109 | buf[i] = nil | ||
110 | return table.concat(buf) | ||
111 | end | ||
112 | |||
113 | local function redact_api_url(url) | ||
114 | url = tostring(url) | ||
115 | return (url:gsub(".*/api/[^/]+/[^/]+", "")) or "" | ||
116 | end | ||
117 | |||
118 | local ltn12_ok, ltn12 = pcall(require, "ltn12") | ||
119 | if not ltn12_ok then -- If not using LuaSocket and/or LuaSec... | ||
120 | |||
121 | function Api:request(url, params, post_params) | ||
122 | local vars = cfg.variables | ||
123 | |||
124 | if fs.which_tool("downloader") == "wget" then | ||
125 | local curl_ok, err = fs.is_tool_available(vars.CURL, "curl") | ||
126 | if not curl_ok then | ||
127 | return nil, err | ||
128 | end | ||
129 | end | ||
130 | |||
131 | if not self.config.key then | ||
132 | return nil, "Must have API key before performing any actions." | ||
133 | end | ||
134 | if params and next(params) then | ||
135 | url = url .. ("?" .. encode_query_string(params)) | ||
136 | end | ||
137 | local method = "GET" | ||
138 | local out | ||
139 | local tmpfile = fs.tmpname() | ||
140 | if post_params then | ||
141 | method = "POST" | ||
142 | local curl_cmd = vars.CURL.." "..vars.CURLNOCERTFLAG.." -f -L --silent --user-agent \""..cfg.user_agent.." via curl\" " | ||
143 | for k,v in pairs(post_params) do | ||
144 | local var = v | ||
145 | if type(v) == "table" then | ||
146 | var = "@"..v.fname | ||
147 | end | ||
148 | curl_cmd = curl_cmd .. "--form \""..k.."="..var.."\" " | ||
149 | end | ||
150 | if cfg.connection_timeout and cfg.connection_timeout > 0 then | ||
151 | curl_cmd = curl_cmd .. "--connect-timeout "..tonumber(cfg.connection_timeout).." " | ||
152 | end | ||
153 | local ok = fs.execute_string(curl_cmd..fs.Q(url).." -o "..fs.Q(tmpfile)) | ||
154 | if not ok then | ||
155 | return nil, "API failure: " .. redact_api_url(url) | ||
156 | end | ||
157 | else | ||
158 | local ok, err = fs.download(url, tmpfile) | ||
159 | if not ok then | ||
160 | return nil, "API failure: " .. tostring(err) .. " - " .. redact_api_url(url) | ||
161 | end | ||
162 | end | ||
163 | |||
164 | local tmpfd = io.open(tmpfile) | ||
165 | if not tmpfd then | ||
166 | os.remove(tmpfile) | ||
167 | return nil, "API failure reading temporary file - " .. redact_api_url(url) | ||
168 | end | ||
169 | out = tmpfd:read("*a") | ||
170 | tmpfd:close() | ||
171 | os.remove(tmpfile) | ||
172 | |||
173 | if self.debug then | ||
174 | util.printout("[" .. tostring(method) .. " via curl] " .. redact_api_url(url) .. " ... ") | ||
175 | end | ||
176 | |||
177 | return json.decode(out) | ||
178 | end | ||
179 | |||
180 | else -- use LuaSocket and LuaSec | ||
181 | |||
182 | local warned_luasec = false | ||
183 | |||
184 | function Api:request(url, params, post_params) | ||
185 | local server = tostring(self.config.server) | ||
186 | local http_ok, http | ||
187 | local via = "luasocket" | ||
188 | if server:match("^https://") then | ||
189 | http_ok, http = pcall(require, "ssl.https") | ||
190 | if http_ok then | ||
191 | via = "luasec" | ||
192 | else | ||
193 | if not warned_luasec then | ||
194 | util.printerr("LuaSec is not available; using plain HTTP. Install 'luasec' to enable HTTPS.") | ||
195 | warned_luasec = true | ||
196 | end | ||
197 | http_ok, http = pcall(require, "socket.http") | ||
198 | url = url:gsub("^https", "http") | ||
199 | via = "luasocket" | ||
200 | end | ||
201 | else | ||
202 | http_ok, http = pcall(require, "socket.http") | ||
203 | end | ||
204 | if not http_ok then | ||
205 | return nil, "Failed loading socket library!" | ||
206 | end | ||
207 | |||
208 | if not self.config.key then | ||
209 | return nil, "Must have API key before performing any actions." | ||
210 | end | ||
211 | local body | ||
212 | local headers = {} | ||
213 | if params and next(params) then | ||
214 | url = url .. ("?" .. encode_query_string(params)) | ||
215 | end | ||
216 | if post_params then | ||
217 | local boundary | ||
218 | body, boundary = multipart.encode(post_params) | ||
219 | headers["Content-length"] = #body | ||
220 | headers["Content-type"] = "multipart/form-data; boundary=" .. tostring(boundary) | ||
221 | end | ||
222 | local method = post_params and "POST" or "GET" | ||
223 | if self.debug then | ||
224 | util.printout("[" .. tostring(method) .. " via "..via.."] " .. redact_api_url(url) .. " ... ") | ||
225 | end | ||
226 | local out = {} | ||
227 | local _, status = http.request({ | ||
228 | url = url, | ||
229 | headers = headers, | ||
230 | method = method, | ||
231 | sink = ltn12.sink.table(out), | ||
232 | source = body and ltn12.source.string(body) | ||
233 | }) | ||
234 | if self.debug then | ||
235 | util.printout(tostring(status)) | ||
236 | end | ||
237 | local pok, ret, err = pcall(json.decode, table.concat(out)) | ||
238 | if pok and ret then | ||
239 | return ret | ||
240 | end | ||
241 | return nil, "API returned " .. tostring(status) .. " - " .. redact_api_url(url) | ||
242 | end | ||
243 | |||
244 | end | ||
245 | |||
246 | function api.new(args) | ||
247 | local self = {} | ||
248 | setmetatable(self, { __index = Api }) | ||
249 | self.config = self:load_config() or {} | ||
250 | self.config.server = args.server or self.config.server or cfg.upload.server | ||
251 | self.config.version = self.config.version or cfg.upload.version | ||
252 | self.config.key = args.temp_key or args.api_key or self.config.key | ||
253 | self.debug = args.debug | ||
254 | if not self.config.key then | ||
255 | return nil, "You need an API key to upload rocks.\n" .. | ||
256 | "Navigate to "..self.config.server.."/settings to get a key\n" .. | ||
257 | "and then pass it through the --api-key=<key> flag." | ||
258 | end | ||
259 | if args.api_key then | ||
260 | self:save_config() | ||
261 | end | ||
262 | return self | ||
263 | end | ||
264 | |||
265 | return api | ||