http.lua 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  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 function response(client,cbFnc,result,prompt,head,body)
  11. if not result then log.error("http.response",result,prompt) end
  12. if cbFnc then cbFnc(result,prompt,head,body) end
  13. if client then client:close() end
  14. end
  15. local function receive(client,timeout,cbFnc,result,prompt,head,body)
  16. local res,data = client:recv(timeout)
  17. if not res then
  18. response(client,cbFnc,result,prompt or "receive timeout",head,body)
  19. end
  20. return res,data
  21. end
  22. local function getFileBase64Len(s)
  23. if s then return (io.fileSize(s)+2)/3*4 end
  24. end
  25. local function taskClient(method,protocal,auth,host,port,path,cert,head,body,timeout,cbFnc,rcvFilePath,tCoreExtPara)
  26. log.info("http path",path)
  27. while not socket.isReady() do
  28. if not sys.waitUntil("IP_READY_IND",timeout) then return response(nil,cbFnc,false,"network not ready") end
  29. end
  30. --计算body长度
  31. local bodyLen = 0
  32. if body then
  33. if type(body)=="string" then
  34. bodyLen = body:len()
  35. elseif type(body)=="table" then
  36. for i=1,#body do
  37. bodyLen = bodyLen + (type(body[i])=="string" and string.len(body[i]) or getFileBase64Len(body[i].file_base64) or io.fileSize(body[i].file))
  38. end
  39. end
  40. end
  41. --重构head
  42. local heads = head or {}
  43. if not heads.Host then heads["Host"] = (port ~= 80 and port ~= 443) and (host..":"..port) or host end
  44. if not heads.Connection then heads["Connection"] = "short" end
  45. if bodyLen>0 and bodyLen~=tonumber(heads["Content-Length"] or "0") then heads["Content-Length"] = bodyLen end
  46. if auth~="" and not heads.Authorization then heads["Authorization"] = ("Basic "..crypto.base64_encode(auth,#auth)) end
  47. local headStr = ""
  48. for k,v in pairs(heads) do
  49. headStr = headStr..k..": "..v.."\r\n"
  50. end
  51. headStr = headStr.."\r\n"
  52. local client = socket.tcp(protocal=="https",cert,tCoreExtPara)
  53. if not client then return response(nil,cbFnc,false,"create socket error") end
  54. if not client:connect(host,port,timeout/1000) then
  55. return response(client,cbFnc,false,"connect fail")
  56. end
  57. --发送请求行+请求头+string类型的body
  58. if not client:send(method.." "..path.." HTTP/1.1".."\r\n"..headStr..(type(body)=="string" and body or "")) then
  59. return response(client,cbFnc,false,"send head fail")
  60. end
  61. --发送table类型的body
  62. if type(body)=="table" then
  63. for i=1,#body do
  64. if type(body[i])=="string" then
  65. if not client:send(body[i]) then
  66. return response(client,cbFnc,false,"send body fail")
  67. end
  68. else
  69. local file = io.open(body[i].file or body[i].file_base64,"rb")
  70. if file then
  71. while true do
  72. local dat = file:read(body[i].file and 11200 or 8400)
  73. if not dat then
  74. io.close(file)
  75. break
  76. end
  77. if body[i].file_base64 then dat=crypto.base64_encode(dat,#dat) end
  78. if not client:send(dat) then
  79. io.close(file)
  80. return response(client,cbFnc,false,"send file fail")
  81. end
  82. end
  83. else
  84. return response(client,cbFnc,false,"send file open fail")
  85. end
  86. end
  87. end
  88. end
  89. local rcvCache,rspHead,rspBody,d1,d2,result,data,statusCode,rcvChunked,contentLen = "",{},{}
  90. --接收数据,解析状态行和头
  91. while true do
  92. result,data = receive(client,timeout,cbFnc,false,nil,rspHead,rcvFilePath or table.concat(rspBody))
  93. if not result then return end
  94. rcvCache = rcvCache..data
  95. d1,d2 = rcvCache:find("\r\n\r\n")
  96. if d2 then
  97. --状态行
  98. _,d1,statusCode = rcvCache:find("%s(%d+)%s.-\r\n")
  99. if not statusCode then
  100. return response(client,cbFnc,false,"parse received status error",rspHead,rcvFilePath or table.concat(rspBody))
  101. end
  102. --应答头
  103. for k,v in string.gmatch(rcvCache:sub(d1+1,d2-2),"(.-):%s*(.-)\r\n") do
  104. rspHead[k] = v
  105. if (string.upper(k)==string.upper("Transfer-Encoding")) and (string.upper(v)==string.upper("chunked")) then rcvChunked = true end
  106. end
  107. if not rcvChunked then
  108. contentLen = tonumber(rspHead["Content-Length"] or "2147483647")
  109. end
  110. if method == "HEAD" then
  111. contentLen = 0
  112. end
  113. --未处理的body数据
  114. rcvCache = rcvCache:sub(d2+1,-1)
  115. break
  116. end
  117. end
  118. --解析body
  119. if rcvChunked then
  120. local chunkSize
  121. --循环处理每个chunk
  122. while true do
  123. --解析chunk size
  124. if not chunkSize then
  125. d1,d2,chunkSize = rcvCache:find("(%x+)\r\n")
  126. if chunkSize then
  127. chunkSize = tonumber(chunkSize,16)
  128. rcvCache = rcvCache:sub(d2+1,-1)
  129. else
  130. result,data = receive(client,timeout,cbFnc,false,nil,rspHead,rcvFilePath or table.concat(rspBody))
  131. if not result then return end
  132. rcvCache = rcvCache..data
  133. end
  134. end
  135. --log.info("http.taskClient chunkSize",chunkSize)
  136. --解析chunk data
  137. if chunkSize then
  138. if rcvCache:len()<chunkSize+2 then
  139. result,data = receive(client,timeout,cbFnc,false,nil,rspHead,rcvFilePath or table.concat(rspBody))
  140. if not result then return end
  141. rcvCache = rcvCache..data
  142. else
  143. if chunkSize>0 then
  144. local chunkData = rcvCache:sub(1,chunkSize)
  145. --保存到文件中
  146. if type(rcvFilePath)=="string" then
  147. local file = io.open(rcvFilePath,"a+")
  148. if not file then return response(client,cbFnc,false,"receive: open file error",rspHead,rcvFilePath or table.concat(rspBody)) end
  149. if not file:write(chunkData) then response(client,cbFnc,false,"receive: write file error",rspHead,rcvFilePath or table.concat(rspBody)) end
  150. file:close()
  151. elseif type(rcvFilePath)=="function" then --保存到缓冲区中
  152. local userResult = rcvFilePath(data,rspHead["Content-Range"] and tonumber((rspHead["Content-Range"]):match("/(%d+)")) or contentLen,statusCode)
  153. if userResult~=nil then
  154. return response(client,cbFnc,userResult,userResult and statusCode or "receive: user process error",rspHead)
  155. end
  156. else
  157. table.insert(rspBody,chunkData)
  158. end
  159. rcvCache = rcvCache:sub(chunkSize+3,-1)
  160. chunkSize = nil
  161. elseif chunkSize==0 then
  162. return response(client,cbFnc,true,statusCode,rspHead,rcvFilePath or table.concat(rspBody))
  163. end
  164. end
  165. end
  166. end
  167. else
  168. local rmnLen = contentLen
  169. while true do
  170. data = rcvCache:len()<=rmnLen and rcvCache or rcvCache:sub(1,rmnLen)
  171. if type(rcvFilePath)=="string" then
  172. if data:len()>0 then
  173. local file = io.open(rcvFilePath,"a+")
  174. if not file then return response(client,cbFnc,false,"receive: open file error",rspHead,rcvFilePath or table.concat(rspBody)) end
  175. if not file:write(data) then response(client,cbFnc,false,"receive: write file error",rspHead,rcvFilePath or table.concat(rspBody)) end
  176. file:close()
  177. end
  178. elseif type(rcvFilePath)=="function" then
  179. local userResult = rcvFilePath(data,rspHead["Content-Range"] and tonumber((rspHead["Content-Range"]):match("/(%d+)")) or contentLen,statusCode)
  180. if userResult~=nil then
  181. return response(client,cbFnc,userResult,userResult and statusCode or "receive: user process error",rspHead)
  182. end
  183. else
  184. table.insert(rspBody,data)
  185. end
  186. rmnLen = rmnLen-data:len()
  187. if rmnLen==0 then break end
  188. result,rcvCache = receive(client,timeout,cbFnc,contentLen==0x7FFFFFFF,contentLen==0x7FFFFFFF and statusCode or nil,rspHead,rcvFilePath or table.concat(rspBody))
  189. if not result then return end
  190. end
  191. return response(client,cbFnc,true,statusCode,rspHead,rcvFilePath or table.concat(rspBody))
  192. end
  193. end
  194. --- 发送HTTP请求
  195. -- @string method HTTP请求方法
  196. -- 支持"GET","HEAD","POST","OPTIONS","PUT","DELETE","TRACE","CONNECT"
  197. -- @string url HTTP请求url
  198. -- url格式(除hostname外,其余字段可选;目前的实现不支持hash),url中如果包含UTF8编码中文,则需要调用string.rawurlEncode转换成RFC3986编码。
  199. -- |------------------------------------------------------------------------------|
  200. -- | protocol ||| auth | host | path | hash |
  201. -- |----------|||-----------|-----------------|---------------------------|-------|
  202. -- | ||| | hostname | port | pathname | search | |
  203. -- | ||| |----------|------|----------|----------------| |
  204. -- " http[s] :// user:pass @ host.com : 8080 /p/a/t/h ? query=string # hash "
  205. -- | ||| | | | | | |
  206. -- |------------------------------------------------------------------------------|
  207. -- @table[opt=nil] cert table或者nil类型,ssl证书,当url为https类型时,此参数才有意义。cert格式如下:
  208. -- {
  209. -- caCert = "ca.crt", --CA证书文件(Base64编码 X.509格式),如果存在此参数,则表示客户端会对服务器的证书进行校验;不存在则不校验
  210. -- clientCert = "client.crt", --客户端证书文件(Base64编码 X.509格式),服务器对客户端的证书进行校验时会用到此参数
  211. -- clientKey = "client.key", --客户端私钥文件(Base64编码 X.509格式)
  212. -- clientPassword = "123456", --客户端证书文件密码[可选]
  213. -- }
  214. -- @table[opt=nil] head nil或者table类型,自定义请求头
  215. -- http.lua会自动添加Host: XXX、Connection: short、Content-Length: XXX三个请求头
  216. -- 如果这三个请求头满足不了需求,head参数传入自定义请求头,如果自定义请求头中存在Host、Connection、Content-Length三个请求头,将覆盖http.lua中自动添加的同名请求头
  217. -- head格式如下:
  218. -- 如果没有自定义请求头,传入nil或者{};否则传入{head1="value1", head2="value2", head3="value3"},value中不能有\r\n
  219. -- @param[opt=nil] body nil、string或者table类型,请求实体
  220. -- 如果body仅仅是一串数据,可以直接传入一个string类型的body即可
  221. --
  222. -- 如果body的数据比较复杂,包括字符串数据和文件,则传入table类型的数据,格式如下:
  223. -- {
  224. -- [1] = "string1",
  225. -- [2] = {file="/ldata/test.jpg"},
  226. -- [3] = "string2"
  227. -- }
  228. -- 例如上面的这个body,索引必须为连续的数字(从1开始),实际传输时,先发送字符串"string1",再发送文件/ldata/test.jpg的内容,最后发送字符串"string2"
  229. --
  230. -- 如果传输的文件内容需要进行base64编码再上传,请把file改成file_base64,格式如下:
  231. -- {
  232. -- [1] = "string1",
  233. -- [2] = {file_base64="/ldata/test.jpg"},
  234. -- [3] = "string2"
  235. -- }
  236. -- 例如上面的这个body,索引必须为连续的数字(从1开始),实际传输时,先发送字符串"string1",再发送文件/ldata/test.jpg经过base64编码后的内容,最后发送字符串"string2"
  237. -- @number[opt=30000] timeout http请求应答整个过程中,每个子过程的超时时间,单位毫秒,默认为30秒,子过程包括如下两种:
  238. -- 1、pdp数据网络激活的超时时间
  239. -- 2、http请求发送成功后,分段接收服务器的应答数据,每段数据接收的超时时间
  240. -- @function[opt=nil] cbFnc 执行HTTP请求的回调函数(请求发送结果以及应答数据接收结果都通过此函数通知用户),回调函数的调用形式为:
  241. -- cbFnc(result,prompt,head,body)
  242. -- result:true或者false,true表示成功收到了服务器的应答,false表示请求发送失败或者接收服务器应答失败
  243. -- prompt:string类型,result为true时,表示服务器的应答码;result为false时,表示错误信息
  244. -- head:table或者nil类型,表示服务器的应答头;result为true时,此参数为{head1="value1", head2="value2", head3="value3"},value中不包含\r\n;result为false时,此参数为nil
  245. -- body:string类型,如果调用request接口时传入了rcvFileName,此参数表示下载文件的完整路径;否则表示接收到的应答实体数据
  246. -- @string[opt=nil] rcvFileName string类型时,保存“服务器应答实体数据”的文件名,可以传入完整的文件路径,也可以传入单独的文件名,如果是文件名,http.lua会自动生成一个完整路径,通过cbFnc的参数body传出
  247. -- function类型时,rcvFileName(stepData,totalLen,statusCode)
  248. -- stepData: 本次服务器应答实体数据
  249. -- totalLen: 实体数据的总长度
  250. -- statusCode:服务器的应答码
  251. -- @table[opt=nil] tCoreExtPara table类型{rcvBufferSize=0}修改缓冲空间大小,解决窗口满连接超时问题,单位:字节
  252. -- @return string rcvFilePath,如果传入了rcvFileName,则返回对应的完整路径;其余情况都返回nil
  253. -- @usage
  254. -- http.request("GET","www.lua.org",nil,nil,nil,30000,cbFnc)
  255. -- http.request("GET","http://www.lua.org",nil,nil,nil,30000,cbFnc)
  256. -- http.request("GET","http://www.lua.org:80",nil,nil,nil,30000,cbFnc,"download.bin")
  257. -- http.request("GET","www.lua.org/about.html",nil,nil,nil,30000,cbFnc)
  258. -- http.request("GET","www.lua.org:80/about.html",nil,nil,nil,30000,cbFnc)
  259. -- http.request("GET","http://wiki.openluat.com/search.html?q=123",nil,nil,nil,30000,cbFnc)
  260. -- http.request("POST","www.test.com/report.html",nil,{Head1="ValueData1"},"BodyData",30000,cbFnc)
  261. -- http.request("POST","www.test.com/report.html",nil,{Head1="ValueData1",Head2="ValueData2"},{[1]="string1",[2] ={file="/ldata/test.jpg"},[3]="string2"},30000,cbFnc)
  262. -- http.request("GET","https://www.baidu.com",{caCert="ca.crt"})
  263. -- http.request("GET","https://www.baidu.com",{caCert="ca.crt",clientCert = "client.crt",clientKey = "client.key"})
  264. -- http.request("GET","https://www.baidu.com",{caCert="ca.crt",clientCert = "client.crt",clientKey = "client.key",clientPassword = "123456"})
  265. function request(method,url,cert,head,body,timeout,cbFnc,rcvFileName,tCoreExtPara)
  266. local protocal,auth,hostName,port,path,d1,d2,offset,rcvFilePath
  267. d1,d2,protocal = url:find("^(%a+)://")
  268. if not protocal then protocal = "http" end
  269. offset = d2 or 0
  270. d1,d2,auth = url:find("(.-:.-)@",offset+1)
  271. offset = d2 or offset
  272. if url:match("^[^/]+:(%d+)",offset+1) then
  273. d1,d2,hostName,port = url:find("^([^/]+):(%d+)",offset+1)
  274. else
  275. d1,d2,hostName = url:find("(.-)/",offset+1)
  276. if hostName then
  277. d2 = d2-1
  278. else
  279. hostName = url:sub(offset+1,-1)
  280. offset = url:len()
  281. end
  282. end
  283. if not hostName then return response(nil,cbFnc,false,"Invalid url, can't get host") end
  284. if port=="" or not port then port = (protocal=="https" and 443 or 80) end
  285. offset = d2 or offset
  286. path = url:sub(offset+1,-1)
  287. sys.taskInit(taskClient,method,protocal,auth or "",hostName,port,path=="" and "/" or path,cert,head,body or "",timeout or 30000,cbFnc,rcvFileName,tCoreExtPara)
  288. if type(rcvFileName) == "string" then
  289. return rcvFileName
  290. end
  291. end