--- 模块功能:HTTP客户端 -- @module http -- @author openLuat -- @license MIT -- @copyright openLuat -- @release 2017.10.23 require"socket" require"utils" module(..., package.seeall) local function response(client,cbFnc,result,prompt,head,body) if not result then log.error("http.response",result,prompt) end if cbFnc then cbFnc(result,prompt,head,body) end if client then client:close() end end local function receive(client,timeout,cbFnc,result,prompt,head,body) local res,data = client:recv(timeout) if not res then response(client,cbFnc,result,prompt or "receive timeout",head,body) end return res,data end local function getFileBase64Len(s) if s then return (io.fileSize(s)+2)/3*4 end end local function taskClient(method,protocal,auth,host,port,path,cert,head,body,timeout,cbFnc,rcvFilePath,tCoreExtPara) log.info("http path",path) while not socket.isReady() do if not sys.waitUntil("IP_READY_IND",timeout) then return response(nil,cbFnc,false,"network not ready") end end --计算body长度 local bodyLen = 0 if body then if type(body)=="string" then bodyLen = body:len() elseif type(body)=="table" then for i=1,#body do bodyLen = bodyLen + (type(body[i])=="string" and string.len(body[i]) or getFileBase64Len(body[i].file_base64) or io.fileSize(body[i].file)) end end end --重构head local heads = head or {} if not heads.Host then heads["Host"] = (port ~= 80 and port ~= 443) and (host..":"..port) or host end if not heads.Connection then heads["Connection"] = "short" end if bodyLen>0 and bodyLen~=tonumber(heads["Content-Length"] or "0") then heads["Content-Length"] = bodyLen end if auth~="" and not heads.Authorization then heads["Authorization"] = ("Basic "..crypto.base64_encode(auth,#auth)) end local headStr = "" for k,v in pairs(heads) do headStr = headStr..k..": "..v.."\r\n" end headStr = headStr.."\r\n" local client = socket.tcp(protocal=="https",cert,tCoreExtPara) if not client then return response(nil,cbFnc,false,"create socket error") end if not client:connect(host,port,timeout/1000) then return response(client,cbFnc,false,"connect fail") end --发送请求行+请求头+string类型的body if not client:send(method.." "..path.." HTTP/1.1".."\r\n"..headStr..(type(body)=="string" and body or "")) then return response(client,cbFnc,false,"send head fail") end --发送table类型的body if type(body)=="table" then for i=1,#body do if type(body[i])=="string" then if not client:send(body[i]) then return response(client,cbFnc,false,"send body fail") end else local file = io.open(body[i].file or body[i].file_base64,"rb") if file then while true do local dat = file:read(body[i].file and 11200 or 8400) if not dat then io.close(file) break end if body[i].file_base64 then dat=crypto.base64_encode(dat,#dat) end if not client:send(dat) then io.close(file) return response(client,cbFnc,false,"send file fail") end end else return response(client,cbFnc,false,"send file open fail") end end end end local rcvCache,rspHead,rspBody,d1,d2,result,data,statusCode,rcvChunked,contentLen = "",{},{} --接收数据,解析状态行和头 while true do result,data = receive(client,timeout,cbFnc,false,nil,rspHead,rcvFilePath or table.concat(rspBody)) if not result then return end rcvCache = rcvCache..data d1,d2 = rcvCache:find("\r\n\r\n") if d2 then --状态行 _,d1,statusCode = rcvCache:find("%s(%d+)%s.-\r\n") if not statusCode then return response(client,cbFnc,false,"parse received status error",rspHead,rcvFilePath or table.concat(rspBody)) end --应答头 for k,v in string.gmatch(rcvCache:sub(d1+1,d2-2),"(.-):%s*(.-)\r\n") do rspHead[k] = v if (string.upper(k)==string.upper("Transfer-Encoding")) and (string.upper(v)==string.upper("chunked")) then rcvChunked = true end end if not rcvChunked then contentLen = tonumber(rspHead["Content-Length"] or "2147483647") end if method == "HEAD" then contentLen = 0 end --未处理的body数据 rcvCache = rcvCache:sub(d2+1,-1) break end end --解析body if rcvChunked then local chunkSize --循环处理每个chunk while true do --解析chunk size if not chunkSize then d1,d2,chunkSize = rcvCache:find("(%x+)\r\n") if chunkSize then chunkSize = tonumber(chunkSize,16) rcvCache = rcvCache:sub(d2+1,-1) else result,data = receive(client,timeout,cbFnc,false,nil,rspHead,rcvFilePath or table.concat(rspBody)) if not result then return end rcvCache = rcvCache..data end end --log.info("http.taskClient chunkSize",chunkSize) --解析chunk data if chunkSize then if rcvCache:len()0 then local chunkData = rcvCache:sub(1,chunkSize) --保存到文件中 if type(rcvFilePath)=="string" then local file = io.open(rcvFilePath,"a+") if not file then return response(client,cbFnc,false,"receive: open file error",rspHead,rcvFilePath or table.concat(rspBody)) end if not file:write(chunkData) then response(client,cbFnc,false,"receive: write file error",rspHead,rcvFilePath or table.concat(rspBody)) end file:close() elseif type(rcvFilePath)=="function" then --保存到缓冲区中 local userResult = rcvFilePath(data,rspHead["Content-Range"] and tonumber((rspHead["Content-Range"]):match("/(%d+)")) or contentLen,statusCode) if userResult~=nil then return response(client,cbFnc,userResult,userResult and statusCode or "receive: user process error",rspHead) end else table.insert(rspBody,chunkData) end rcvCache = rcvCache:sub(chunkSize+3,-1) chunkSize = nil elseif chunkSize==0 then return response(client,cbFnc,true,statusCode,rspHead,rcvFilePath or table.concat(rspBody)) end end end end else local rmnLen = contentLen while true do data = rcvCache:len()<=rmnLen and rcvCache or rcvCache:sub(1,rmnLen) if type(rcvFilePath)=="string" then if data:len()>0 then local file = io.open(rcvFilePath,"a+") if not file then return response(client,cbFnc,false,"receive: open file error",rspHead,rcvFilePath or table.concat(rspBody)) end if not file:write(data) then response(client,cbFnc,false,"receive: write file error",rspHead,rcvFilePath or table.concat(rspBody)) end file:close() end elseif type(rcvFilePath)=="function" then local userResult = rcvFilePath(data,rspHead["Content-Range"] and tonumber((rspHead["Content-Range"]):match("/(%d+)")) or contentLen,statusCode) if userResult~=nil then return response(client,cbFnc,userResult,userResult and statusCode or "receive: user process error",rspHead) end else table.insert(rspBody,data) end rmnLen = rmnLen-data:len() if rmnLen==0 then break end result,rcvCache = receive(client,timeout,cbFnc,contentLen==0x7FFFFFFF,contentLen==0x7FFFFFFF and statusCode or nil,rspHead,rcvFilePath or table.concat(rspBody)) if not result then return end end return response(client,cbFnc,true,statusCode,rspHead,rcvFilePath or table.concat(rspBody)) end end --- 发送HTTP请求 -- @string method HTTP请求方法 -- 支持"GET","HEAD","POST","OPTIONS","PUT","DELETE","TRACE","CONNECT" -- @string url HTTP请求url -- url格式(除hostname外,其余字段可选;目前的实现不支持hash),url中如果包含UTF8编码中文,则需要调用string.rawurlEncode转换成RFC3986编码。 -- |------------------------------------------------------------------------------| -- | protocol ||| auth | host | path | hash | -- |----------|||-----------|-----------------|---------------------------|-------| -- | ||| | hostname | port | pathname | search | | -- | ||| |----------|------|----------|----------------| | -- " http[s] :// user:pass @ host.com : 8080 /p/a/t/h ? query=string # hash " -- | ||| | | | | | | -- |------------------------------------------------------------------------------| -- @table[opt=nil] cert table或者nil类型,ssl证书,当url为https类型时,此参数才有意义。cert格式如下: -- { -- caCert = "ca.crt", --CA证书文件(Base64编码 X.509格式),如果存在此参数,则表示客户端会对服务器的证书进行校验;不存在则不校验 -- clientCert = "client.crt", --客户端证书文件(Base64编码 X.509格式),服务器对客户端的证书进行校验时会用到此参数 -- clientKey = "client.key", --客户端私钥文件(Base64编码 X.509格式) -- clientPassword = "123456", --客户端证书文件密码[可选] -- } -- @table[opt=nil] head nil或者table类型,自定义请求头 -- http.lua会自动添加Host: XXX、Connection: short、Content-Length: XXX三个请求头 -- 如果这三个请求头满足不了需求,head参数传入自定义请求头,如果自定义请求头中存在Host、Connection、Content-Length三个请求头,将覆盖http.lua中自动添加的同名请求头 -- head格式如下: -- 如果没有自定义请求头,传入nil或者{};否则传入{head1="value1", head2="value2", head3="value3"},value中不能有\r\n -- @param[opt=nil] body nil、string或者table类型,请求实体 -- 如果body仅仅是一串数据,可以直接传入一个string类型的body即可 -- -- 如果body的数据比较复杂,包括字符串数据和文件,则传入table类型的数据,格式如下: -- { -- [1] = "string1", -- [2] = {file="/ldata/test.jpg"}, -- [3] = "string2" -- } -- 例如上面的这个body,索引必须为连续的数字(从1开始),实际传输时,先发送字符串"string1",再发送文件/ldata/test.jpg的内容,最后发送字符串"string2" -- -- 如果传输的文件内容需要进行base64编码再上传,请把file改成file_base64,格式如下: -- { -- [1] = "string1", -- [2] = {file_base64="/ldata/test.jpg"}, -- [3] = "string2" -- } -- 例如上面的这个body,索引必须为连续的数字(从1开始),实际传输时,先发送字符串"string1",再发送文件/ldata/test.jpg经过base64编码后的内容,最后发送字符串"string2" -- @number[opt=30000] timeout http请求应答整个过程中,每个子过程的超时时间,单位毫秒,默认为30秒,子过程包括如下两种: -- 1、pdp数据网络激活的超时时间 -- 2、http请求发送成功后,分段接收服务器的应答数据,每段数据接收的超时时间 -- @function[opt=nil] cbFnc 执行HTTP请求的回调函数(请求发送结果以及应答数据接收结果都通过此函数通知用户),回调函数的调用形式为: -- cbFnc(result,prompt,head,body) -- result:true或者false,true表示成功收到了服务器的应答,false表示请求发送失败或者接收服务器应答失败 -- prompt:string类型,result为true时,表示服务器的应答码;result为false时,表示错误信息 -- head:table或者nil类型,表示服务器的应答头;result为true时,此参数为{head1="value1", head2="value2", head3="value3"},value中不包含\r\n;result为false时,此参数为nil -- body:string类型,如果调用request接口时传入了rcvFileName,此参数表示下载文件的完整路径;否则表示接收到的应答实体数据 -- @string[opt=nil] rcvFileName string类型时,保存“服务器应答实体数据”的文件名,可以传入完整的文件路径,也可以传入单独的文件名,如果是文件名,http.lua会自动生成一个完整路径,通过cbFnc的参数body传出 -- function类型时,rcvFileName(stepData,totalLen,statusCode) -- stepData: 本次服务器应答实体数据 -- totalLen: 实体数据的总长度 -- statusCode:服务器的应答码 -- @table[opt=nil] tCoreExtPara table类型{rcvBufferSize=0}修改缓冲空间大小,解决窗口满连接超时问题,单位:字节 -- @return string rcvFilePath,如果传入了rcvFileName,则返回对应的完整路径;其余情况都返回nil -- @usage -- http.request("GET","www.lua.org",nil,nil,nil,30000,cbFnc) -- http.request("GET","http://www.lua.org",nil,nil,nil,30000,cbFnc) -- http.request("GET","http://www.lua.org:80",nil,nil,nil,30000,cbFnc,"download.bin") -- http.request("GET","www.lua.org/about.html",nil,nil,nil,30000,cbFnc) -- http.request("GET","www.lua.org:80/about.html",nil,nil,nil,30000,cbFnc) -- http.request("GET","http://wiki.openluat.com/search.html?q=123",nil,nil,nil,30000,cbFnc) -- http.request("POST","www.test.com/report.html",nil,{Head1="ValueData1"},"BodyData",30000,cbFnc) -- http.request("POST","www.test.com/report.html",nil,{Head1="ValueData1",Head2="ValueData2"},{[1]="string1",[2] ={file="/ldata/test.jpg"},[3]="string2"},30000,cbFnc) -- http.request("GET","https://www.baidu.com",{caCert="ca.crt"}) -- http.request("GET","https://www.baidu.com",{caCert="ca.crt",clientCert = "client.crt",clientKey = "client.key"}) -- http.request("GET","https://www.baidu.com",{caCert="ca.crt",clientCert = "client.crt",clientKey = "client.key",clientPassword = "123456"}) function request(method,url,cert,head,body,timeout,cbFnc,rcvFileName,tCoreExtPara) local protocal,auth,hostName,port,path,d1,d2,offset,rcvFilePath d1,d2,protocal = url:find("^(%a+)://") if not protocal then protocal = "http" end offset = d2 or 0 d1,d2,auth = url:find("(.-:.-)@",offset+1) offset = d2 or offset if url:match("^[^/]+:(%d+)",offset+1) then d1,d2,hostName,port = url:find("^([^/]+):(%d+)",offset+1) else d1,d2,hostName = url:find("(.-)/",offset+1) if hostName then d2 = d2-1 else hostName = url:sub(offset+1,-1) offset = url:len() end end if not hostName then return response(nil,cbFnc,false,"Invalid url, can't get host") end if port=="" or not port then port = (protocal=="https" and 443 or 80) end offset = d2 or offset path = url:sub(offset+1,-1) sys.taskInit(taskClient,method,protocal,auth or "",hostName,port,path=="" and "/" or path,cert,head,body or "",timeout or 30000,cbFnc,rcvFileName,tCoreExtPara) if type(rcvFileName) == "string" then return rcvFileName end end