http.lua 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. --- 模块功能:HTTP客户端
  2. -- @module http
  3. -- @author openLuat
  4. -- @license MIT
  5. -- @copyright openLuat
  6. -- @release 2017.10.23
  7. require"socket"
  8. require"utils"
  9. module(..., package.seeall)
  10. local Content_type = {'application/x-www-form-urlencoded', 'application/json', 'application/octet-stream'}
  11. -- 处理表的url编码
  12. function urlencodeTab(params)
  13. local msg = {}
  14. for k, v in pairs(params) do
  15. table.insert(msg, string.urlencode(k) .. '=' .. string.urlencode(v))
  16. table.insert(msg, '&')
  17. end
  18. table.remove(msg)
  19. return table.concat(msg)
  20. end
  21. local function response(client,cbFnc,result,prompt,head,body)
  22. if not result then log.error("http.response",result,prompt) end
  23. if cbFnc then cbFnc(result,prompt,head,body) end
  24. if client then client:close() end
  25. end
  26. local function receive(client,timeout,cbFnc,result,prompt,head,body)
  27. local res,data = client:recv(timeout)
  28. if not res then
  29. response(client,cbFnc,result,prompt or "receive timeout",head,body)
  30. end
  31. return res,data
  32. end
  33. local function getFileBase64Len(s)
  34. if s then return (io.fileSize(s)+2)/3*4 end
  35. end
  36. local function taskClient(method,protocal,auth,host,port,path,cert,head,body,timeout,cbFnc,rcvFilePath)
  37. while not socket.isReady() do
  38. if not sys.waitUntil("IP_READY_IND",timeout) then return response(nil,cbFnc,false,"network not ready") end
  39. end
  40. --计算body长度
  41. local bodyLen = 0
  42. if body then
  43. if type(body)=="string" then
  44. bodyLen = body:len()
  45. elseif type(body)=="table" then
  46. for i=1,#body do
  47. bodyLen = bodyLen + (type(body[i])=="string" and string.len(body[i]) or getFileBase64Len(body[i].file_base64) or io.fileSize(body[i].file))
  48. end
  49. end
  50. end
  51. --重构head
  52. local heads = head or {}
  53. if not heads.Host then heads["Host"] = host end
  54. if not heads.Connection then heads["Connection"] = "short" end
  55. if bodyLen>0 and bodyLen~=tonumber(heads["Content-Length"] or "0") then heads["Content-Length"] = bodyLen end
  56. if auth~="" and not heads.Authorization then heads["Authorization"] = ("Basic "..crypto.base64_encode(auth,#auth)) end
  57. local headStr = ""
  58. for k,v in pairs(heads) do
  59. headStr = headStr..k..": "..v.."\r\n"
  60. end
  61. headStr = headStr.."\r\n"
  62. local client = socket.tcp(protocal=="https",cert)
  63. if not client then return response(nil,cbFnc,false,"create socket error") end
  64. if not client:connect(host,port) then
  65. return response(client,cbFnc,false,"connect fail")
  66. end
  67. --发送请求行+请求头+string类型的body
  68. if not client:send(method.." "..path.." HTTP/1.1".."\r\n"..headStr..(type(body)=="string" and body or "")) then
  69. return response(client,cbFnc,false,"send head fail")
  70. end
  71. --发送table类型的body
  72. if type(body)=="table" then
  73. for i=1,#body do
  74. if type(body[i])=="string" then
  75. if not client:send(body[i]) then
  76. return response(client,cbFnc,false,"send body fail")
  77. end
  78. else
  79. local file = io.open(body[i].file or body[i].file_base64,"rb")
  80. if file then
  81. while true do
  82. local dat = file:read(body[i].file and 1460 or 1095)
  83. if not dat then
  84. io.close(file)
  85. break
  86. end
  87. if body[i].file_base64 then dat=crypto.base64_encode(dat,#dat) end
  88. if not client:send(dat) then
  89. io.close(file)
  90. return response(client,cbFnc,false,"send file fail")
  91. end
  92. end
  93. else
  94. return response(client,cbFnc,false,"send file open fail")
  95. end
  96. end
  97. end
  98. end
  99. local rcvCache,rspHead,rspBody,d1,d2,result,data,statusCode,rcvChunked,contentLen = "",{},{}
  100. --接收数据,解析状态行和头
  101. while true do
  102. result,data = receive(client,timeout,cbFnc,false,nil,rspHead,rcvFilePath or table.concat(rspBody))
  103. if not result then return end
  104. rcvCache = rcvCache..data
  105. d1,d2 = rcvCache:find("\r\n\r\n")
  106. if d2 then
  107. --状态行
  108. _,d1,statusCode = rcvCache:find("%s(%d+)%s.-\r\n")
  109. if not statusCode then
  110. return response(client,cbFnc,false,"parse received status error",rspHead,rcvFilePath or table.concat(rspBody))
  111. end
  112. --应答头
  113. for k,v in string.gmatch(rcvCache:sub(d1+1,d2-2),"(.-):%s*(.-)\r\n") do
  114. rspHead[k] = v
  115. if (k=="Transfer-Encoding") and (v=="chunked") then rcvChunked = true end
  116. end
  117. if not rcvChunked then
  118. contentLen = tonumber(rspHead["Content-Length"] or "2147483647")
  119. end
  120. --未处理的body数据
  121. rcvCache = rcvCache:sub(d2+1,-1)
  122. break
  123. end
  124. end
  125. --解析body
  126. if rcvChunked then
  127. local chunkSize
  128. --循环处理每个chunk
  129. while true do
  130. --解析chunk size
  131. if not chunkSize then
  132. d1,d2,chunkSize = rcvCache:find("(%x+)\r\n")
  133. if chunkSize then
  134. chunkSize = tonumber(chunkSize,16)
  135. rcvCache = rcvCache:sub(d2+1,-1)
  136. else
  137. result,data = receive(client,timeout,cbFnc,false,nil,rspHead,rcvFilePath or table.concat(rspBody))
  138. if not result then return end
  139. rcvCache = rcvCache..data
  140. end
  141. end
  142. --log.info("http.taskClient chunkSize",chunkSize)
  143. --解析chunk data
  144. if chunkSize then
  145. if rcvCache:len()<chunkSize+2 then
  146. result,data = receive(client,timeout,cbFnc,false,nil,rspHead,rcvFilePath or table.concat(rspBody))
  147. if not result then return end
  148. rcvCache = rcvCache..data
  149. else
  150. if chunkSize>0 then
  151. local chunkData = rcvCache:sub(1,chunkSize)
  152. --保存到文件中
  153. if rcvFilePath then
  154. local file = io.open(rcvFilePath,"a+")
  155. if not file then return response(client,cbFnc,false,"receive:open file error",rspHead,rcvFilePath or table.concat(rspBody)) end
  156. if not file:write(chunkData) then response(client,cbFnc,false,"receive:write file error",rspHead,rcvFilePath or table.concat(rspBody)) end
  157. file:close()
  158. --保存到缓冲区中
  159. else
  160. table.insert(rspBody,chunkData)
  161. end
  162. rcvCache = rcvCache:sub(chunkSize+3,-1)
  163. chunkSize = nil
  164. elseif chunkSize==0 then
  165. return response(client,cbFnc,true,statusCode,rspHead,rcvFilePath or table.concat(rspBody))
  166. end
  167. end
  168. end
  169. end
  170. else
  171. local rmnLen = contentLen
  172. while true do
  173. data = rcvCache:len()<=rmnLen and rcvCache or rcvCache:sub(1,rmnLen)
  174. --保存到文件中
  175. if rcvFilePath then
  176. if data:len()>0 then
  177. local file = io.open(rcvFilePath,"a+")
  178. if not file then return response(client,cbFnc,false,"receive:open file error",rspHead,rcvFilePath or table.concat(rspBody)) end
  179. if not file:write(data) then response(client,cbFnc,false,"receive:write file error",rspHead,rcvFilePath or table.concat(rspBody)) end
  180. file:close()
  181. end
  182. else
  183. table.insert(rspBody,data)
  184. end
  185. rmnLen = rmnLen-data:len()
  186. if rmnLen==0 then break end
  187. result,rcvCache = receive(client,timeout,cbFnc,contentLen==0x7FFFFFFF,contentLen==0x7FFFFFFF and statusCode or nil,rspHead,rcvFilePath or table.concat(rspBody))
  188. if not result then return end
  189. end
  190. return response(client,cbFnc,true,statusCode,rspHead,rcvFilePath or table.concat(rspBody))
  191. end
  192. end
  193. --- 发送HTTP请求
  194. -- @string method HTTP请求方法
  195. -- 支持"GET","HEAD","POST","OPTIONS","PUT","DELETE","TRACE","CONNECT"
  196. -- @string url HTTP请求url
  197. -- url格式(除hostname外,其余字段可选;目前的实现不支持hash)
  198. -- |------------------------------------------------------------------------------|
  199. -- | protocol ||| auth | host | path | hash |
  200. -- |----------|||-----------|-----------------|---------------------------|-------|
  201. -- | ||| | hostname | port | pathname | search | |
  202. -- | ||| |----------|------|----------|----------------| |
  203. -- " http[s] :// user:pass @ host.com : 8080 /p/a/t/h ? query=string # hash "
  204. -- | ||| | | | | | |
  205. -- |------------------------------------------------------------------------------|
  206. -- @table[opt=nil] cert,table或者nil类型,ssl证书,当url为https类型时,此参数才有意义。cert格式如下:
  207. -- {
  208. -- caCert = "ca.crt", --CA证书文件(Base64编码 X.509格式),如果存在此参数,则表示客户端会对服务器的证书进行校验;不存在则不校验
  209. -- clientCert = "client.crt", --客户端证书文件(Base64编码 X.509格式),服务器对客户端的证书进行校验时会用到此参数
  210. -- clientKey = "client.key", --客户端私钥文件(Base64编码 X.509格式)
  211. -- clientPassword = "123456", --客户端证书文件密码[可选]
  212. -- }
  213. -- @table[opt=nil] head,nil或者table类型,自定义请求头
  214. -- http.lua会自动添加Host: XXX、Connection: short、Content-Length: XXX三个请求头
  215. -- 如果这三个请求头满足不了需求,head参数传入自定义请求头,如果自定义请求头中存在Host、Connection、Content-Length三个请求头,将覆盖http.lua中自动添加的同名请求头
  216. -- head格式如下:
  217. -- 如果没有自定义请求头,传入nil或者{};否则传入{head1="value1", head2="value2", head3="value3"},value中不能有\r\n
  218. -- @param[opt=nil] body,nil、string或者table类型,请求实体
  219. -- 如果body仅仅是一串数据,可以直接传入一个string类型的body即可
  220. --
  221. -- 如果body的数据比较复杂,包括字符串数据和文件,则传入table类型的数据,格式如下:
  222. -- {
  223. -- [1] = "string1",
  224. -- [2] = {file="/ldata/test.jpg"},
  225. -- [3] = "string2"
  226. -- }
  227. -- 例如上面的这个body,索引必须为连续的数字(从1开始),实际传输时,先发送字符串"string1",再发送文件/ldata/test.jpg的内容,最后发送字符串"string2"
  228. --
  229. -- 如果传输的文件内容需要进行base64编码再上传,请把file改成file_base64,格式如下:
  230. -- {
  231. -- [1] = "string1",
  232. -- [2] = {file_base64="/ldata/test.jpg"},
  233. -- [3] = "string2"
  234. -- }
  235. -- 例如上面的这个body,索引必须为连续的数字(从1开始),实际传输时,先发送字符串"string1",再发送文件/ldata/test.jpg经过base64编码后的内容,最后发送字符串"string2"
  236. -- @number[opt=30000] timeout,请求发送成功后,接收服务器返回应答数据的超时时间,单位毫秒,默认为30秒
  237. -- @function[opt=nil] cbFnc,执行HTTP请求的回调函数(请求发送结果以及应答数据接收结果都通过此函数通知用户),回调函数的调用形式为:
  238. -- cbFnc(result,prompt,head,body)
  239. -- result:true或者false,true表示成功收到了服务器的应答,false表示请求发送失败或者接收服务器应答失败
  240. -- prompt:string类型,result为true时,表示服务器的应答码;result为false时,表示错误信息
  241. -- head:table或者nil类型,表示服务器的应答头;result为true时,此参数为{head1="value1", head2="value2", head3="value3"},value中不包含\r\n;result为false时,此参数为nil
  242. -- body:string类型,如果调用request接口时传入了rcvFileName,此参数表示下载文件的完整路径;否则表示接收到的应答实体数据
  243. -- @string[opt=nil] rcvFileName,保存“服务器应答实体数据”的文件名,可以传入完整的文件路径,也可以传入单独的文件名,如果是文件名,http.lua会自动生成一个完整路径,通过cbFnc的参数body传出
  244. -- @return string rcvFilePath,如果传入了rcvFileName,则返回对应的完整路径;其余情况都返回nil
  245. -- @usage
  246. -- http.request("GET","www.lua.org",nil,nil,nil,30000,cbFnc)
  247. -- http.request("GET","http://www.lua.org",nil,nil,nil,30000,cbFnc)
  248. -- http.request("GET","http://www.lua.org:80",nil,nil,nil,30000,cbFnc,"download.bin")
  249. -- http.request("GET","www.lua.org/about.html",nil,nil,nil,30000,cbFnc)
  250. -- http.request("GET","www.lua.org:80/about.html",nil,nil,nil,30000,cbFnc)
  251. -- http.request("GET","http://wiki.openluat.com/search.html?q=123",nil,nil,nil,30000,cbFnc)
  252. -- http.request("POST","www.test.com/report.html",nil,{Head1="ValueData1"},"BodyData",30000,cbFnc)
  253. -- http.request("POST","www.test.com/report.html",nil,{Head1="ValueData1",Head2="ValueData2"},{[1]="string1",[2] ={file="/ldata/test.jpg"},[3]="string2"},30000,cbFnc)
  254. -- http.request("GET","https://www.baidu.com",{caCert="ca.crt"})
  255. -- http.request("GET","https://www.baidu.com",{caCert="ca.crt",clientCert = "client.crt",clientKey = "client.key"})
  256. -- http.request("GET","https://www.baidu.com",{caCert="ca.crt",clientCert = "client.crt",clientKey = "client.key",clientPassword = "123456"})
  257. function request(method,url,cert,head,body,timeout,cbFnc,rcvFileName)
  258. local protocal,auth,hostName,port,path,d1,d2,offset,rcvFilePath
  259. d1,d2,protocal = url:find("^(%a+)://")
  260. if not protocal then protocal = "http" end
  261. offset = d2 or 0
  262. d1,d2,auth = url:find("(.-:.-)@",offset+1)
  263. offset = d2 or offset
  264. if url:match("^[^/]+:(%d+)",offset+1) then
  265. d1,d2,hostName,port = url:find("^([^/]+):(%d+)",offset+1)
  266. else
  267. d1,d2,hostName = url:find("(.-)/",offset+1)
  268. if hostName then
  269. d2 = d2-1
  270. else
  271. hostName = url:sub(offset+1,-1)
  272. offset = url:len()
  273. end
  274. end
  275. if not hostName then return response(nil,cbFnc,false,"Invalid url, can't get host") end
  276. if port=="" or not port then port = (protocal=="https" and 443 or 80) end
  277. offset = d2 or offset
  278. path = url:sub(offset+1,-1)
  279. if rcvFileName and rcvFileName:sub(1,1)~="/" and rtos.make_dir and rtos.make_dir("/http_down") then
  280. rcvFilePath = "/http_down/"..rcvFileName
  281. end
  282. sys.taskInit(taskClient,method,protocal,auth or "",hostName,port,path=="" and "/" or path,cert,head,body or "",timeout or 30000,cbFnc,rcvFilePath or rcvFileName)
  283. return rcvFilePath or rcvFileName
  284. end
  285. --- HTTP客户端
  286. -- @string method,提交方式"GET" or "POST"
  287. -- @string url,HTTP请求超链接
  288. -- @number timeout,超时时间
  289. -- @param params,table类型,请求发送的查询字符串,通常为键值对表
  290. -- @param data,table类型,正文提交的body,通常为键值对、json或文件对象类似的表
  291. -- @number ctype,Content-Type的类型(可选1,2,3),默认1:"urlencode",2:"json",3:"octet-stream"
  292. -- @string basic,HTTP客户端的authorization basic验证的"username:password"
  293. -- @param headers,table类型,HTTP headers部分
  294. -- @return string,table,string,正常返回response_code, response_header, response_body
  295. -- @return string,string,错误返回 response_code, error_message
  296. -- @usage local c, h, b = http.request(url, method, headers, body)
  297. -- @usage local r, e = http.request("http://wrong.url/ ")
  298. function my_request(method, url, timeout, params, data, ctype, basic, headers)
  299. local response_header, response_code, response_message, response_body, host, port, path, str, sub, len = {}
  300. local headers =
  301. headers or
  302. {
  303. ['User-Agent'] = 'Mozilla/4.0',
  304. ['Accept'] = '*/*',
  305. ['Accept-Language'] = 'zh-CN,zh,cn',
  306. ['Content-Type'] = 'application/x-www-form-urlencoded',
  307. ['Content-Length'] = '0',
  308. ['Connection'] = 'close'
  309. }
  310. -- 判断SSL支持是否满足
  311. local ssl, https = string.find(rtos.get_version(), 'SSL'), url:find('https://')
  312. if ssl == nil and https then
  313. return '401', 'SOCKET_SSL_ERROR'
  314. end
  315. -- 对host:port整形
  316. if url:find('://') then
  317. url = url:sub(8)
  318. end
  319. sub = url:find('/')
  320. if not sub then
  321. url = url .. '/'
  322. sub = -1
  323. end
  324. str = url:match('([%w%.%-%:]+)/')
  325. port = str:match(':(%d+)') or 80
  326. host = str:match('[%w%.%-]+')
  327. path = url:sub(sub)
  328. sub = ''
  329. -- 处理查询字符串
  330. if params ~= nil and type(params) == 'table' then
  331. path = path .. '?' .. urlencodeTab(params)
  332. end
  333. -- 处理HTTP协议body部分的数据
  334. ctype = ctype or 2
  335. headers['Content-Type'] = Content_type[ctype]
  336. if ctype == 1 and data ~= nil then
  337. if type(data) == 'table' then
  338. data = table.concat(data)
  339. end
  340. sub = urlencodeTab(data)
  341. len = string.len(sub)
  342. headers['Content-Length'] = len or 0
  343. elseif ctype == 2 and data ~= nil then
  344. if type(data) == 'table' then
  345. sub = json.encode(data)
  346. elseif type(data) == 'string' then
  347. sub = data
  348. end
  349. len = string.len(sub)
  350. headers['Content-Length'] = len or 0
  351. elseif ctype == 3 and type(data) == 'string' then
  352. len = io.filesize(data)
  353. headers['Content-Length'] = len or 0
  354. end
  355. -- 处理HTTP Basic Authorization 验证
  356. if basic ~= nil and type(basic) == 'string' then
  357. headers['Authorization'] = 'Basic ' .. crypto.base64_encode(basic, #basic)
  358. end
  359. -- 处理headers部分
  360. local msg = {}
  361. for k, v in pairs(headers) do
  362. table.insert(msg, k .. ': ' .. v)
  363. end
  364. -- 合并request报文
  365. str = str .. '\r\n' .. table.concat(msg, '\r\n') .. '\r\n\r\n'
  366. -- log.info("http.request send:", str:tohex())
  367. -- 发送请求报文
  368. local c = socket.tcp()
  369. if not c:connect(host, port) then
  370. c:close()
  371. return '502', 'SOCKET_CONN_ERROR'
  372. end
  373. if ctype ~= 3 then
  374. str = method .. ' ' .. path .. ' HTTP/1.0\r\nHost: ' .. str .. sub .. '\r\n'
  375. if not c:send(str) then
  376. c:close()
  377. return '426', 'SOCKET_SEND_ERROR'
  378. end
  379. else
  380. str = method .. ' ' .. path .. ' HTTP/1.0\r\nHost: ' .. str
  381. if not c:send(str) then
  382. c:close()
  383. return '426', 'SOCKET_SEND_ERROR'
  384. end
  385. local file = io.open(data, 'r')
  386. if file then
  387. while true do
  388. local dat = file:read(1024)
  389. if dat == nil then
  390. io.close(file)
  391. break
  392. end
  393. log.info('http.request dat:', dat:tohex())
  394. if not c:send(dat) then
  395. io.close(file)
  396. c:close()
  397. return '426', 'SOCKET_SEND_ERROR'
  398. end
  399. end
  400. end
  401. if not c:send('\r\n') then
  402. c:close()
  403. return '426', 'SOCKET_SEND_ERROR'
  404. end
  405. end
  406. msg = {}
  407. r, s = c:recv(timeout)
  408. if not r then
  409. return '503', 'SOCKET_RECV_TIMOUT'
  410. end
  411. response_code = s:match(' (%d+) ')
  412. response_message = s:match(' (%a+)')
  413. log.info('http.response code and message:\t', response_code, response_message)
  414. for k, v in s:gmatch('([%a%-]+): (%C+)') do
  415. response_header[k] = v
  416. end
  417. gzip = s:match('%aontent%-%ancoding: (%a+)')
  418. while true do
  419. table.insert(msg, s)
  420. r, s = c:recv(timeout)
  421. if not r then
  422. break
  423. end
  424. end
  425. c:close()
  426. str = table.concat(msg)
  427. sub, len = str:find('\r?\n\r?\n')
  428. if gzip then
  429. return response_code, response_header, ((zlib.inflate(table.concat(msg))):read())
  430. end
  431. return response_code, response_header, str:sub(len + 1, -1)
  432. end