1
0

httpclient.lua 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. -- Copyright 2009 Steven Barth <steven@midlink.org>
  2. -- Licensed to the public under the Apache License 2.0.
  3. require "nixio.util"
  4. local nixio = require "nixio"
  5. local ltn12 = require "luci.ltn12"
  6. local util = require "luci.util"
  7. local table = require "table"
  8. local http = require "luci.http"
  9. local date = require "luci.http.date"
  10. local ip = require "luci.ip"
  11. local type, pairs, ipairs, tonumber, tostring = type, pairs, ipairs, tonumber, tostring
  12. local unpack, string = unpack, string
  13. module "luci.httpclient"
  14. function chunksource(sock, buffer)
  15. buffer = buffer or ""
  16. return function()
  17. local output
  18. local _, endp, count = buffer:find("^([0-9a-fA-F]+);?.-\r\n")
  19. while not count and #buffer <= 1024 do
  20. local newblock, code = sock:recv(1024 - #buffer)
  21. if not newblock then
  22. return nil, code
  23. end
  24. buffer = buffer .. newblock
  25. _, endp, count = buffer:find("^([0-9a-fA-F]+);?.-\r\n")
  26. end
  27. count = tonumber(count, 16)
  28. if not count then
  29. return nil, -1, "invalid encoding"
  30. elseif count == 0 then
  31. return nil
  32. elseif count + 2 <= #buffer - endp then
  33. output = buffer:sub(endp+1, endp+count)
  34. buffer = buffer:sub(endp+count+3)
  35. return output
  36. else
  37. output = buffer:sub(endp+1, endp+count)
  38. buffer = ""
  39. if count - #output > 0 then
  40. local remain, code = sock:recvall(count-#output)
  41. if not remain then
  42. return nil, code
  43. end
  44. output = output .. remain
  45. count, code = sock:recvall(2)
  46. else
  47. count, code = sock:recvall(count+2-#buffer+endp)
  48. end
  49. if not count then
  50. return nil, code
  51. end
  52. return output
  53. end
  54. end
  55. end
  56. function request_to_buffer(uri, options)
  57. local source, code, msg = request_to_source(uri, options)
  58. local output = {}
  59. if not source then
  60. return nil, code, msg
  61. end
  62. source, code = ltn12.pump.all(source, (ltn12.sink.table(output)))
  63. if not source then
  64. return nil, code
  65. end
  66. return table.concat(output)
  67. end
  68. function request_to_source(uri, options)
  69. local status, response, buffer, sock = request_raw(uri, options)
  70. if not status then
  71. return status, response, buffer
  72. elseif status ~= 200 and status ~= 206 then
  73. return nil, status, buffer
  74. end
  75. if response.headers["Transfer-Encoding"] == "chunked" then
  76. return chunksource(sock, buffer)
  77. else
  78. return ltn12.source.cat(ltn12.source.string(buffer), sock:blocksource())
  79. end
  80. end
  81. function parse_url(uri)
  82. local url, rest, tmp = {}, nil, nil
  83. url.scheme, rest = uri:match("^(%w+)://(.+)$")
  84. if not (url.scheme and rest) then
  85. return nil
  86. end
  87. url.auth, tmp = rest:match("^([^@]+)@(.+)$")
  88. if url.auth and tmp then
  89. rest = tmp
  90. end
  91. url.host, tmp = rest:match("^%[(.+)%](.*)$")
  92. if url.host and tmp then
  93. url.ip6addr = ip.IPv6(url.host)
  94. if not url.ip6addr or url.ip6addr:prefix() < 128 then
  95. return nil
  96. end
  97. url.host = string.format("[%s]", url.ip6addr:string())
  98. rest = tmp
  99. else
  100. url.host, tmp = rest:match("^(%d+%.%d+%.%d+%.%d+)(.*)$")
  101. if url.host and tmp then
  102. url.ipaddr = ip.IPv4(url.host)
  103. if not url.ipaddr then
  104. return nil
  105. end
  106. url.host = url.ipaddr:string()
  107. rest = tmp
  108. else
  109. url.host, tmp = rest:match("^([0-9a-zA-Z%.%-]+)(.*)$")
  110. if url.host and tmp then
  111. rest = tmp
  112. else
  113. return nil
  114. end
  115. end
  116. end
  117. url.port, tmp = rest:match("^:(%d+)(.*)$")
  118. if url.port and tmp then
  119. url.port = tonumber(url.port)
  120. rest = tmp
  121. if url.port < 1 or url.port > 65535 then
  122. return nil
  123. end
  124. end
  125. if url.scheme == "http" then
  126. url.port = url.port or 80
  127. url.default_port = (url.port == 80)
  128. elseif url.scheme == "https" then
  129. url.port = url.port or 443
  130. url.default_port = (url.port == 443)
  131. end
  132. if rest == "" then
  133. url.path = "/"
  134. else
  135. url.path = rest
  136. end
  137. return url
  138. end
  139. --
  140. -- GET HTTP-resource
  141. --
  142. function request_raw(uri, options)
  143. options = options or {}
  144. if options.params then
  145. uri = uri .. '?' .. http.urlencode_params(options.params)
  146. end
  147. local url = parse_url(uri)
  148. if not url then
  149. return nil, -1, "unable to parse URI"
  150. end
  151. if url.scheme ~= "http" and url.scheme ~= "https" then
  152. return nil, -2, "protocol not supported"
  153. end
  154. options.depth = options.depth or 10
  155. local headers = options.headers or {}
  156. local protocol = options.protocol or "HTTP/1.1"
  157. headers["User-Agent"] = headers["User-Agent"] or "LuCI httpclient 0.1"
  158. if headers.Connection == nil then
  159. headers.Connection = "close"
  160. end
  161. if url.auth and not headers.Authorization then
  162. headers.Authorization = "Basic " .. nixio.bin.b64encode(url.auth)
  163. end
  164. local addr = tostring(url.ip6addr or url.ipaddr or url.host)
  165. local sock, code, msg = nixio.connect(addr, url.port)
  166. if not sock then
  167. return nil, code, msg
  168. end
  169. sock:setsockopt("socket", "sndtimeo", options.sndtimeo or 15)
  170. sock:setsockopt("socket", "rcvtimeo", options.rcvtimeo or 15)
  171. if url.scheme == "https" then
  172. local tls = options.tls_context or nixio.tls()
  173. sock = tls:create(sock)
  174. local stat, code, error = sock:connect()
  175. if not stat then
  176. return stat, code, error
  177. end
  178. end
  179. -- Pre assemble fixes
  180. if protocol == "HTTP/1.1" then
  181. headers.Host = headers.Host or
  182. (url.default_port and url.host or string.format("%s:%d", url.host, url.port))
  183. end
  184. if type(options.body) == "table" then
  185. options.body = http.urlencode_params(options.body)
  186. end
  187. if type(options.body) == "string" then
  188. headers["Content-Length"] = headers["Content-Length"] or #options.body
  189. headers["Content-Type"] = headers["Content-Type"] or
  190. "application/x-www-form-urlencoded"
  191. options.method = options.method or "POST"
  192. end
  193. if type(options.body) == "function" then
  194. options.method = options.method or "POST"
  195. end
  196. if options.cookies then
  197. local cookiedata = {}
  198. for _, c in ipairs(options.cookies) do
  199. local cdo = c.flags.domain
  200. local cpa = c.flags.path
  201. if (cdo == url.host or cdo == "."..url.host or url.host:sub(-#cdo) == cdo)
  202. and (cpa == url.path or cpa == "/" or cpa .. "/" == url.path:sub(#cpa+1))
  203. and (not c.flags.secure or url.scheme == "https")
  204. then
  205. cookiedata[#cookiedata+1] = c.key .. "=" .. c.value
  206. end
  207. end
  208. if headers["Cookie"] then
  209. headers["Cookie"] = headers["Cookie"] .. "; " .. table.concat(cookiedata, "; ")
  210. else
  211. headers["Cookie"] = table.concat(cookiedata, "; ")
  212. end
  213. end
  214. -- Assemble message
  215. local message = {(options.method or "GET") .. " " .. url.path .. " " .. protocol}
  216. for k, v in pairs(headers) do
  217. if type(v) == "string" or type(v) == "number" then
  218. message[#message+1] = k .. ": " .. v
  219. elseif type(v) == "table" then
  220. for i, j in ipairs(v) do
  221. message[#message+1] = k .. ": " .. j
  222. end
  223. end
  224. end
  225. message[#message+1] = ""
  226. message[#message+1] = ""
  227. -- Send request
  228. sock:sendall(table.concat(message, "\r\n"))
  229. if type(options.body) == "string" then
  230. sock:sendall(options.body)
  231. elseif type(options.body) == "function" then
  232. local res = {options.body(sock)}
  233. if not res[1] then
  234. sock:close()
  235. return unpack(res)
  236. end
  237. end
  238. -- Create source and fetch response
  239. local linesrc = sock:linesource()
  240. local line, code, error = linesrc()
  241. if not line then
  242. sock:close()
  243. return nil, code, error
  244. end
  245. local protocol, status, msg = line:match("^([%w./]+) ([0-9]+) (.*)")
  246. if not protocol then
  247. sock:close()
  248. return nil, -3, "invalid response magic: " .. line
  249. end
  250. local response = {
  251. status = line, headers = {}, code = 0, cookies = {}, uri = uri
  252. }
  253. line = linesrc()
  254. while line and line ~= "" do
  255. local key, val = line:match("^([%w-]+)%s?:%s?(.*)")
  256. if key and key ~= "Status" then
  257. if type(response.headers[key]) == "string" then
  258. response.headers[key] = {response.headers[key], val}
  259. elseif type(response.headers[key]) == "table" then
  260. response.headers[key][#response.headers[key]+1] = val
  261. else
  262. response.headers[key] = val
  263. end
  264. end
  265. line = linesrc()
  266. end
  267. if not line then
  268. sock:close()
  269. return nil, -4, "protocol error"
  270. end
  271. -- Parse cookies
  272. if response.headers["Set-Cookie"] then
  273. local cookies = response.headers["Set-Cookie"]
  274. for _, c in ipairs(type(cookies) == "table" and cookies or {cookies}) do
  275. local cobj = cookie_parse(c)
  276. cobj.flags.path = cobj.flags.path or url.path:match("(/.*)/?[^/]*")
  277. if not cobj.flags.domain or cobj.flags.domain == "" then
  278. cobj.flags.domain = url.host
  279. response.cookies[#response.cookies+1] = cobj
  280. else
  281. local hprt, cprt = {}, {}
  282. -- Split hostnames and save them in reverse order
  283. for part in url.host:gmatch("[^.]*") do
  284. table.insert(hprt, 1, part)
  285. end
  286. for part in cobj.flags.domain:gmatch("[^.]*") do
  287. table.insert(cprt, 1, part)
  288. end
  289. local valid = true
  290. for i, part in ipairs(cprt) do
  291. -- If parts are different and no wildcard
  292. if hprt[i] ~= part and #part ~= 0 then
  293. valid = false
  294. break
  295. -- Wildcard on invalid position
  296. elseif hprt[i] ~= part and #part == 0 then
  297. if i ~= #cprt or (#hprt ~= i and #hprt+1 ~= i) then
  298. valid = false
  299. break
  300. end
  301. end
  302. end
  303. -- No TLD cookies
  304. if valid and #cprt > 1 and #cprt[2] > 0 then
  305. response.cookies[#response.cookies+1] = cobj
  306. end
  307. end
  308. end
  309. end
  310. -- Follow
  311. response.code = tonumber(status)
  312. if response.code and options.depth > 0 then
  313. if (response.code == 301 or response.code == 302 or response.code == 307)
  314. and response.headers.Location then
  315. local nuri = response.headers.Location or response.headers.location
  316. if not nuri then
  317. return nil, -5, "invalid reference"
  318. end
  319. if not nuri:match("^%w+://") then
  320. nuri = url.default_port and string.format("%s://%s%s", url.scheme, url.host, nuri)
  321. or string.format("%s://%s:%d%s", url.scheme, url.host, url.port, nuri)
  322. end
  323. options.depth = options.depth - 1
  324. if options.headers then
  325. options.headers.Host = nil
  326. end
  327. sock:close()
  328. return request_raw(nuri, options)
  329. end
  330. end
  331. return response.code, response, linesrc(true)..sock:readall(), sock
  332. end
  333. function cookie_parse(cookiestr)
  334. local key, val, flags = cookiestr:match("%s?([^=;]+)=?([^;]*)(.*)")
  335. if not key then
  336. return nil
  337. end
  338. local cookie = {key = key, value = val, flags = {}}
  339. for fkey, fval in flags:gmatch(";%s?([^=;]+)=?([^;]*)") do
  340. fkey = fkey:lower()
  341. if fkey == "expires" then
  342. fval = date.to_unix(fval:gsub("%-", " "))
  343. end
  344. cookie.flags[fkey] = fval
  345. end
  346. return cookie
  347. end
  348. function cookie_create(cookie)
  349. local cookiedata = {cookie.key .. "=" .. cookie.value}
  350. for k, v in pairs(cookie.flags) do
  351. if k == "expires" then
  352. v = date.to_http(v):gsub(", (%w+) (%w+) (%w+) ", ", %1-%2-%3 ")
  353. end
  354. cookiedata[#cookiedata+1] = k .. ((#v > 0) and ("=" .. v) or "")
  355. end
  356. return table.concat(cookiedata, "; ")
  357. end