httpv2.lua 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. --- 模块功能:HTTP客户端
  2. -- @module httpv2
  3. -- @author 稀饭放姜
  4. -- @license MIT
  5. -- @copyright OpenLuat.com
  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. --- HTTP客户端
  22. -- @string method,提交方式"GET" or "POST"
  23. -- @string url,HTTP请求超链接
  24. -- @number timeout,超时时间
  25. -- @param params,table类型,请求发送的查询字符串,通常为键值对表
  26. -- @param data,table类型,正文提交的body,通常为键值对、json或文件对象类似的表
  27. -- @number ctype,Content-Type的类型(可选1,2,3),默认1:"urlencode",2:"json",3:"octet-stream"
  28. -- @string basic,HTTP客户端的authorization basic验证的"username:password"
  29. -- @param headers,table类型,HTTP headers部分
  30. -- @param cert,table类型,此参数可选,默认值为: nil,ssl连接需要的证书配置,只有ssl参数为true时,才参数才有意义,cert格式如下:
  31. -- {
  32. -- caCert = "ca.crt", --CA证书文件(Base64编码 X.509格式),如果存在此参数,则表示客户端会对服务器的证书进行校验;不存在则不校验
  33. -- clientCert = "client.crt", --客户端证书文件(Base64编码 X.509格式),服务器对客户端的证书进行校验时会用到此参数
  34. -- clientKey = "client.key", --客户端私钥文件(Base64编码 X.509格式) clientPassword = "123456", --客户端证书文件密码[可选]
  35. -- }
  36. -- @return string,table,string,正常返回response_code, response_header, response_body
  37. -- @return string,string,错误返回 response_code, error_message
  38. -- @usage local c, h, b = httpv2.request(url, method, headers, body)
  39. -- @usage local r, e = httpv2.request("http://wrong.url/ ")
  40. function request(method, url, timeout, params, data, ctype, basic, headers, cert, fnc)
  41. local response_header, response_code, response_body = {}
  42. local _, idx, offset, ssl, auth, https, host, port, path
  43. if type(headers) == "string" then
  44. local tmp = {}
  45. for k, v in string.gmatch(headers, "(.-):%s*(.-)\r\n") do tmp[k] = v end
  46. headers = tmp
  47. elseif type(headers) ~= "table" then
  48. headers = {
  49. ['User-Agent'] = 'Mozilla/4.0',
  50. ['Accept'] = '*/*',
  51. ['Accept-Language'] = 'zh-CN,zh,cn',
  52. ['Content-Type'] = 'application/x-www-form-urlencoded',
  53. ['Content-Length'] = '0',
  54. ['Connection'] = 'Keep-alive',
  55. ["Keep-Alive"] = 'timeout=20',
  56. }
  57. end
  58. ssl = string.find(rtos.get_version(), 'SSL') or string.find(rtos.get_version(), '8955F')
  59. -- 处理url的协议头和鉴权
  60. _, offset, https = url:find("^(%a+)://")
  61. _, idx, auth = url:find("(.-:.-)@", (offset or 0) + 1)
  62. offset = idx or offset
  63. -- 判断SSL支持是否满足
  64. if not ssl and https and https:lower() == "https" then return '401', 'SOCKET_SSL_ERROR' end
  65. -- 对host:port整形
  66. if url:match("^[^/]+:(%d+)", (offset or 0) + 1) then
  67. _, offset, host, port = url:find("^([^/]+):(%d+)", (offset or 0) + 1)
  68. elseif url:find("(.-)/", (offset or 0) + 1) then
  69. _, offset, host = url:find("(.-)/", (offset or 0) + 1)
  70. offset = offset - 1
  71. else
  72. offset, host = #url, url:sub((offset or 0) + 1, -1)
  73. end
  74. if not host then return '105', 'ERR_NAME_NOT_RESOLVED' end
  75. if not headers.Host then headers["Host"] = host end
  76. port = port or (https == "https" and 443 or 80)
  77. path = url:sub(offset + 1, -1)
  78. path = path == "" and "/" or path
  79. -- 处理查询字符串
  80. if params then path = path .. '?' .. (type(params) == 'table' and urlencodeTab(params) or params) end
  81. -- 处理HTTP协议body部分的数据
  82. ctype = ctype or 2
  83. headers['Content-Type'] = Content_type[ctype]
  84. if ctype == 1 and type(data) == 'table' then
  85. data = urlencodeTab(data)
  86. headers['Content-Length'] = #data or 0
  87. elseif ctype == 2 and data ~= nil then
  88. data = type(data) == 'string' and data or (type(data) == 'table' and json.encode(data)) or ""
  89. headers['Content-Length'] = #data or 0
  90. elseif ctype == 3 and type(data) == 'string' then
  91. headers['Content-Length'] = io.fileSize(data) or 0
  92. elseif data and type(data) == "string" then
  93. headers['Content-Length'] = #data or 0
  94. end
  95. -- 处理HTTP Basic Authorization 验证
  96. if auth then
  97. headers['Authorization'] = 'Basic ' .. crypto.base64_encode(auth, #auth)
  98. elseif type(basic) == 'string' then
  99. headers['Authorization'] = 'Basic ' .. crypto.base64_encode(basic, #basic)
  100. end
  101. -- 处理headers部分
  102. local str = ""
  103. for k, v in pairs(headers) do str = str .. k .. ": " .. v .. "\r\n" end
  104. -- 发送请求报文
  105. while not socket.isReady() do sys.wait(1000) end
  106. local c = socket.tcp(https == "https", cert)
  107. if not c:connect(host, port) then
  108. c:close()
  109. return '502', 'SOCKET_CONN_ERROR'
  110. end
  111. if ctype ~= 3 then
  112. str = method .. ' ' .. path .. ' HTTP/1.1\r\n' .. str .. '\r\n' .. (data and data .. "\r\n" or "")
  113. -- log.info("发送的http报文:", str)
  114. if not c:send(str) then
  115. c:close()
  116. return '426', 'SOCKET_SEND_ERROR'
  117. end
  118. else
  119. str = method .. ' ' .. path .. ' HTTP/1.1\r\n' .. str .. '\r\n'
  120. if not c:send(str) then
  121. c:close()
  122. return '426', 'SOCKET_SEND_ERROR'
  123. end
  124. local file = io.open(data, 'r')
  125. if file then
  126. while true do
  127. local dat = file:read(8192)
  128. if dat == nil then
  129. io.close(file)
  130. break
  131. end
  132. if not c:send(dat) then
  133. io.close(file)
  134. c:close()
  135. return '426', 'SOCKET_SEND_ERROR'
  136. end
  137. end
  138. end
  139. if not c:send('\r\n') then
  140. c:close()
  141. return '426', 'SOCKET_SEND_ERROR'
  142. end
  143. end
  144. ------------------------------------ 接收服务器返回消息部分 ------------------------------------
  145. local msg, str = {}, ""
  146. local r, s = c:recv(timeout)
  147. if not r then
  148. c:close()
  149. return '503', 'SOCKET_RECV_TIMOUT'
  150. end
  151. -- 处理状态代码
  152. _, idx, response_code = s:find("%s(%d+)%s.-\r\n")
  153. _, offset = s:find('\r\n\r\n')
  154. if not idx or not offset then return '501', 'SERVER_NOT_RESPONSE' end
  155. log.info('httpv2.response code:', response_code)
  156. -- 处理headers代码
  157. for k, v in string.gmatch(s:sub(idx + 1, offset), "(.-):%s*(.-)\r\n") do response_header[k] = v end
  158. local len = response_header["Content-Range"] and tonumber(response_header["Content-Range"]:match("/(%d+)")) or tonumber(response_header["Content-Length"]) or 2147483648
  159. s = s:sub((offset or 0) + 1, -1)
  160. local cnt = #s
  161. if tonumber(response_code) == 200 or tonumber(response_code) == 206 then
  162. -- 处理body
  163. while true do
  164. if type(fnc) == "function" then
  165. fnc(s, len)
  166. else
  167. table.insert(msg, s)
  168. end
  169. if cnt >= len then break end
  170. r, s = c:recv(timeout)
  171. if not r then break end
  172. cnt = cnt + #s
  173. end
  174. s = table.concat(msg) or ""
  175. end
  176. c:close()
  177. local gzip = response_header["Content-Encoding"] == "gzip"
  178. return response_code, response_header, gzip and ((zlib.inflate(s)):read()) or s
  179. end