init.lua 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. --[[
  2. LuaJIT-Request
  3. Lucien Greathouse
  4. Wrapper for LuaJIT-cURL for easy HTTP(S) requests.
  5. Copyright (c) 2016 Lucien Greathouse
  6. This software is provided 'as-is', without any express
  7. or implied warranty. In no event will the authors be held
  8. liable for any damages arising from the use of this software.
  9. Permission is granted to anyone to use this software for any purpose,
  10. including commercial applications, andto alter it and redistribute it
  11. freely, subject to the following restrictions:
  12. 1. The origin of this software must not be misrepresented; you must not
  13. claim that you wrote the original software. If you use this software
  14. in a product, an acknowledgment in the product documentation would be
  15. appreciated but is not required.
  16. 2. Altered source versions must be plainly marked as such, and must
  17. not be misrepresented as being the original software.
  18. 3. This notice may not be removed or altered from any source distribution.
  19. ]]
  20. local path = (...):gsub("%.init$", ""):match("%.?(.-)$") .. "."
  21. local ffi = require("ffi")
  22. local curl = require(path .. "luajit-curl")
  23. local request
  24. local function url_encode(str)
  25. if (str) then
  26. str = str:gsub("\n", "\r\n")
  27. str = str:gsub("([^%w %-%_%.%~])", function(c)
  28. return string.format ("%%%02X", string.byte(c))
  29. end)
  30. str = str:gsub(" ", "%%20")
  31. end
  32. return str
  33. end
  34. local function cookie_encode(str, name)
  35. str = str:gsub("[,;%s]", "")
  36. if (name) then
  37. str = str:gsub("=", "")
  38. end
  39. return str
  40. end
  41. local auth_map = {
  42. BASIC = ffi.cast("long", curl.CURLAUTH_BASIC),
  43. DIGEST = ffi.cast("long", curl.CURLAUTH_DIGEST),
  44. NEGOTIATE = ffi.cast("long", curl.CURLAUTH_NEGOTIATE)
  45. }
  46. local errors = {
  47. unknown = 0,
  48. timeout = 1,
  49. connect = 2,
  50. resolve_host = 3
  51. }
  52. local code_map = {
  53. [curl.CURLE_OPERATION_TIMEDOUT] = {
  54. errors.timeout, "Connection timed out"
  55. },
  56. [curl.CURLE_COULDNT_RESOLVE_HOST] = {
  57. errors.resolve_host, "Couldn't resolve host"
  58. },
  59. [curl.CURLE_COULDNT_CONNECT] = {
  60. errors.connect, "Couldn't connect to host"
  61. }
  62. }
  63. request = {
  64. error = errors,
  65. version = "2.4.0",
  66. version_major = 2,
  67. version_minor = 4,
  68. version_patch = 0,
  69. --[[
  70. Send an HTTP(S) request to the URL at 'url' using the HTTP method 'method'.
  71. Use the 'args' parameter to optionally configure the request:
  72. - method: HTTP method to use. Defaults to "GET", but can be any HTTP verb like "POST" or "PUT"
  73. - headers: Dictionary of additional HTTP headers to send with request
  74. - data: Dictionary or string to send as request body
  75. - cookies: Dictionary table of cookies to send
  76. - timeout: How long to wait for the connection to be made before giving up
  77. - allow_redirects: Whether or not to allow redirection. Defaults to true
  78. - body_stream_callback: A method to call with each piece of the response body.
  79. - header_stream_callback: A method to call with each piece of the resulting header.
  80. - transfer_info_callback: A method to call with transfer progress data.
  81. - auth_type: Authentication method to use. Defaults to "none", but can also be "basic", "digest" or "negotiate"
  82. - username: A username to use with authentication. 'auth_type' must also be specified.
  83. - password: A password to use with authentication. 'auth_type' must also be specified.
  84. - files: A dictionary of file names to their paths on disk to upload via stream.
  85. If both body_stream_callback and header_stream_callback are defined, a boolean true will be returned instead of the following object.
  86. The return object is a dictionary with the following members:
  87. - code: The HTTP status code the response gave. Will not exist if header_stream_callback is defined above.
  88. - body: The body of the response. Will not exist if body_stream_callback is defined above.
  89. - headers: A dictionary of headers and their values. Will not exist if header_stream_callback is defined above.
  90. - headers_raw: A raw string containing the actual headers the server sent back. Will not exist if header_stream_callback is defined above.
  91. - set_cookies: A dictionary of cookies given by the "Set-Cookie" header from the server. Will not exist if the server did not set any cookies.
  92. ]]
  93. send = function(url, args)
  94. local handle = curl.curl_easy_init()
  95. local header_chunk
  96. local out_buffer
  97. local headers_buffer
  98. args = args or {}
  99. local callbacks = {}
  100. local gc_handles = {}
  101. curl.curl_easy_setopt(handle, curl.CURLOPT_URL, url)
  102. curl.curl_easy_setopt(handle, curl.CURLOPT_SSL_VERIFYPEER, 1)
  103. curl.curl_easy_setopt(handle, curl.CURLOPT_SSL_VERIFYHOST, 2)
  104. if (args.method) then
  105. local method = string.upper(tostring(args.method))
  106. if (method == "GET") then
  107. curl.curl_easy_setopt(handle, curl.CURLOPT_HTTPGET, 1)
  108. elseif (method == "POST") then
  109. curl.curl_easy_setopt(handle, curl.CURLOPT_POST, 1)
  110. else
  111. curl.curl_easy_setopt(handle, curl.CURLOPT_CUSTOMREQUEST, method)
  112. end
  113. end
  114. if (args.headers) then
  115. for key, value in pairs(args.headers) do
  116. header_chunk = curl.curl_slist_append(header_chunk, tostring(key) .. ":" .. tostring(value))
  117. end
  118. curl.curl_easy_setopt(handle, curl.CURLOPT_HTTPHEADER, header_chunk)
  119. end
  120. if (args.auth_type) then
  121. local auth = string.upper(tostring(args.auth_type))
  122. if (auth_map[auth]) then
  123. curl.curl_easy_setopt(handle, curl.CURLOPT_HTTPAUTH, auth_map[auth])
  124. curl.curl_easy_setopt(handle, curl.CURLOPT_USERNAME, tostring(args.username))
  125. curl.curl_easy_setopt(handle, curl.CURLOPT_PASSWORD, tostring(args.password or ""))
  126. elseif (auth ~= "NONE") then
  127. error("Unsupported authentication type '" .. auth .. "'")
  128. end
  129. end
  130. if (args.body_stream_callback) then
  131. local callback = ffi.cast("curl_callback", function(data, size, nmeb, user)
  132. args.body_stream_callback(ffi.string(data, size * nmeb))
  133. return size * nmeb
  134. end)
  135. table.insert(callbacks, callback)
  136. curl.curl_easy_setopt(handle, curl.CURLOPT_WRITEFUNCTION, callback)
  137. else
  138. out_buffer = {}
  139. local callback = ffi.cast("curl_callback", function(data, size, nmeb, user)
  140. table.insert(out_buffer, ffi.string(data, size * nmeb))
  141. return size * nmeb
  142. end)
  143. table.insert(callbacks, callback)
  144. curl.curl_easy_setopt(handle, curl.CURLOPT_WRITEFUNCTION, callback)
  145. end
  146. if (args.header_stream_callback) then
  147. local callback = ffi.cast("curl_callback", function(data, size, nmeb, user)
  148. args.header_stream_callback(ffi.string(data, size * nmeb))
  149. return size * nmeb
  150. end)
  151. table.insert(callbacks, callback)
  152. curl.curl_easy_setopt(handle, curl.CURLOPT_HEADERFUNCTION, callback)
  153. else
  154. headers_buffer = {}
  155. local callback = ffi.cast("curl_callback", function(data, size, nmeb, user)
  156. table.insert(headers_buffer, ffi.string(data, size * nmeb))
  157. return size * nmeb
  158. end)
  159. table.insert(callbacks, callback)
  160. curl.curl_easy_setopt(handle, curl.CURLOPT_HEADERFUNCTION, callback)
  161. end
  162. if (args.transfer_info_callback) then
  163. local callback = ffi.cast("curl_xferinfo_callback", function(client, dltotal, dlnow, ultotal, ulnow)
  164. args.transfer_info_callback(tonumber(dltotal), tonumber(dlnow), tonumber(ultotal), tonumber(ulnow))
  165. return 0
  166. end)
  167. table.insert(callbacks, callback)
  168. curl.curl_easy_setopt(handle, curl.CURLOPT_NOPROGRESS, 0)
  169. curl.curl_easy_setopt(handle, curl.CURLOPT_XFERINFOFUNCTION, callback)
  170. end
  171. if (args.follow_redirects == nil) then
  172. curl.curl_easy_setopt(handle, curl.CURLOPT_FOLLOWLOCATION, true)
  173. else
  174. curl.curl_easy_setopt(handle, curl.CURLOPT_FOLLOWLOCATION, not not args.follow_redirects)
  175. end
  176. if (args.data) then
  177. if (type(args.data) == "table") then
  178. local buffer = {}
  179. for key, value in pairs(args.data) do
  180. table.insert(buffer, ("%s=%s"):format(url_encode(key), url_encode(value)))
  181. end
  182. curl.curl_easy_setopt(handle, curl.CURLOPT_POSTFIELDS, table.concat(buffer, "&"))
  183. else
  184. curl.curl_easy_setopt(handle, curl.CURLOPT_POSTFIELDS, tostring(args.data))
  185. end
  186. end
  187. local post
  188. if (args.files) then
  189. post = ffi.new("struct curl_httppost*[1]")
  190. local lastptr = ffi.new("struct curl_httppost*[1]")
  191. for key, value in pairs(args.files) do
  192. local file = ffi.new("char[?]", #value, value)
  193. table.insert(gc_handles, file)
  194. local res = curl.curl_formadd(
  195. post, lastptr,
  196. ffi.new("int", curl.CURLFORM_COPYNAME), key,
  197. ffi.new("int", curl.CURLFORM_FILE), file,
  198. ffi.new("int", curl.CURLFORM_END)
  199. )
  200. end
  201. curl.curl_easy_setopt(handle, curl.CURLOPT_HTTPPOST, post[0])
  202. end
  203. -- Enable the cookie engine
  204. curl.curl_easy_setopt(handle, curl.CURLOPT_COOKIEFILE, "")
  205. if (args.cookies) then
  206. local cookie_out
  207. if (type(args.cookies) == "table") then
  208. local buffer = {}
  209. for key, value in pairs(args.cookies) do
  210. table.insert(buffer, ("%s=%s"):format(cookie_encode(key, true), cookie_encode(value)))
  211. end
  212. cookie_out = table.concat(buffer, "; ")
  213. else
  214. cookie_out = tostring(args.cookies)
  215. end
  216. curl.curl_easy_setopt(handle, curl.CURLOPT_COOKIE, cookie_out)
  217. end
  218. if (tonumber(args.timeout)) then
  219. curl.curl_easy_setopt(handle, curl.CURLOPT_CONNECTTIMEOUT, tonumber(args.timeout))
  220. end
  221. local code = curl.curl_easy_perform(handle)
  222. if (code ~= curl.CURLE_OK) then
  223. local num = tonumber(code)
  224. if (code_map[num]) then
  225. return false, code_map[num][1], code_map[num][2]
  226. end
  227. return false, request.error.unknown, "Unknown error", num
  228. end
  229. local out
  230. if (out_buffer or headers_buffer) then
  231. local headers, status, parsed_headers, raw_cookies, set_cookies
  232. if (headers_buffer) then
  233. headers = table.concat(headers_buffer)
  234. status = headers:match("%s+(%d+)%s+")
  235. parsed_headers = {}
  236. for key, value in headers:gmatch("\n([^:]+): *([^\r\n]*)") do
  237. parsed_headers[key] = value
  238. end
  239. end
  240. local cookielist = ffi.new("struct curl_slist*[1]")
  241. curl.curl_easy_getinfo(handle, curl.CURLINFO_COOKIELIST, cookielist)
  242. if cookielist[0] ~= nil then
  243. raw_cookies, set_cookies = {}, {}
  244. local cookielist = ffi.gc(cookielist[0], curl.curl_slist_free_all)
  245. local cookie = cookielist
  246. repeat
  247. local raw = ffi.string(cookie[0].data)
  248. table.insert(raw_cookies, raw)
  249. local domain, subdomains, path, secure, expiration, name, value = raw:match("^(.-)\t(.-)\t(.-)\t(.-)\t(.-)\t(.-)\t(.*)$")
  250. set_cookies[name] = value
  251. cookie = cookie[0].next
  252. until cookie == nil
  253. end
  254. out = {
  255. body = table.concat(out_buffer),
  256. headers = parsed_headers,
  257. raw_cookies = raw_cookies,
  258. set_cookies = set_cookies,
  259. code = status,
  260. raw_headers = headers
  261. }
  262. else
  263. out = true
  264. end
  265. curl.curl_easy_cleanup(handle)
  266. curl.curl_slist_free_all(header_chunk)
  267. if (post) then
  268. curl.curl_formfree(post[0])
  269. end
  270. gc_handles = {}
  271. for i, v in ipairs(callbacks) do
  272. v:free()
  273. end
  274. return out
  275. end,
  276. init = function()
  277. curl.curl_global_init(curl.CURL_GLOBAL_ALL)
  278. end,
  279. close = function()
  280. curl.curl_global_cleanup()
  281. end
  282. }
  283. request.init()
  284. return request