| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458 |
- --- 模块功能:HTTP客户端
- -- @module http
- -- @author openLuat
- -- @license MIT
- -- @copyright openLuat
- -- @release 2017.10.23
- require"socket"
- require"utils"
- module(..., package.seeall)
- local Content_type = {'application/x-www-form-urlencoded', 'application/json', 'application/octet-stream'}
- -- 处理表的url编码
- function urlencodeTab(params)
- local msg = {}
- for k, v in pairs(params) do
- table.insert(msg, string.urlencode(k) .. '=' .. string.urlencode(v))
- table.insert(msg, '&')
- end
- table.remove(msg)
- return table.concat(msg)
- end
- 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)
- 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"] = 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)
- if not client then return response(nil,cbFnc,false,"create socket error") end
- if not client:connect(host,port) 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 1460 or 1095)
- 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 (k=="Transfer-Encoding") and (v=="chunked") then rcvChunked = true end
- end
- if not rcvChunked then
- contentLen = tonumber(rspHead["Content-Length"] or "2147483647")
- 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()<chunkSize+2 then
- result,data = receive(client,timeout,cbFnc,false,nil,rspHead,rcvFilePath or table.concat(rspBody))
- if not result then return end
- rcvCache = rcvCache..data
- else
- if chunkSize>0 then
- local chunkData = rcvCache:sub(1,chunkSize)
- --保存到文件中
- if rcvFilePath 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()
- --保存到缓冲区中
- 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 rcvFilePath 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
- 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)
- -- |------------------------------------------------------------------------------|
- -- | 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,请求发送成功后,接收服务器返回应答数据的超时时间,单位毫秒,默认为30秒
- -- @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,保存“服务器应答实体数据”的文件名,可以传入完整的文件路径,也可以传入单独的文件名,如果是文件名,http.lua会自动生成一个完整路径,通过cbFnc的参数body传出
- -- @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)
- 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)
- if rcvFileName and rcvFileName:sub(1,1)~="/" and rtos.make_dir and rtos.make_dir("/http_down") then
- rcvFilePath = "/http_down/"..rcvFileName
- end
-
- sys.taskInit(taskClient,method,protocal,auth or "",hostName,port,path=="" and "/" or path,cert,head,body or "",timeout or 30000,cbFnc,rcvFilePath or rcvFileName)
-
- return rcvFilePath or rcvFileName
- end
- --- HTTP客户端
- -- @string method,提交方式"GET" or "POST"
- -- @string url,HTTP请求超链接
- -- @number timeout,超时时间
- -- @param params,table类型,请求发送的查询字符串,通常为键值对表
- -- @param data,table类型,正文提交的body,通常为键值对、json或文件对象类似的表
- -- @number ctype,Content-Type的类型(可选1,2,3),默认1:"urlencode",2:"json",3:"octet-stream"
- -- @string basic,HTTP客户端的authorization basic验证的"username:password"
- -- @param headers,table类型,HTTP headers部分
- -- @return string,table,string,正常返回response_code, response_header, response_body
- -- @return string,string,错误返回 response_code, error_message
- -- @usage local c, h, b = http.request(url, method, headers, body)
- -- @usage local r, e = http.request("http://wrong.url/ ")
- function my_request(method, url, timeout, params, data, ctype, basic, headers)
- local response_header, response_code, response_message, response_body, host, port, path, str, sub, len = {}
- local headers =
- headers or
- {
- ['User-Agent'] = 'Mozilla/4.0',
- ['Accept'] = '*/*',
- ['Accept-Language'] = 'zh-CN,zh,cn',
- ['Content-Type'] = 'application/x-www-form-urlencoded',
- ['Content-Length'] = '0',
- ['Connection'] = 'close'
- }
- -- 判断SSL支持是否满足
- local ssl, https = string.find(rtos.get_version(), 'SSL'), url:find('https://')
- if ssl == nil and https then
- return '401', 'SOCKET_SSL_ERROR'
- end
- -- 对host:port整形
- if url:find('://') then
- url = url:sub(8)
- end
- sub = url:find('/')
- if not sub then
- url = url .. '/'
- sub = -1
- end
- str = url:match('([%w%.%-%:]+)/')
- port = str:match(':(%d+)') or 80
- host = str:match('[%w%.%-]+')
- path = url:sub(sub)
- sub = ''
- -- 处理查询字符串
- if params ~= nil and type(params) == 'table' then
- path = path .. '?' .. urlencodeTab(params)
- end
- -- 处理HTTP协议body部分的数据
- ctype = ctype or 2
- headers['Content-Type'] = Content_type[ctype]
- if ctype == 1 and data ~= nil then
- if type(data) == 'table' then
- data = table.concat(data)
- end
- sub = urlencodeTab(data)
- len = string.len(sub)
- headers['Content-Length'] = len or 0
- elseif ctype == 2 and data ~= nil then
- if type(data) == 'table' then
- sub = json.encode(data)
- elseif type(data) == 'string' then
- sub = data
- end
- len = string.len(sub)
- headers['Content-Length'] = len or 0
- elseif ctype == 3 and type(data) == 'string' then
- len = io.filesize(data)
- headers['Content-Length'] = len or 0
- end
- -- 处理HTTP Basic Authorization 验证
- if basic ~= nil and type(basic) == 'string' then
- headers['Authorization'] = 'Basic ' .. crypto.base64_encode(basic, #basic)
- end
- -- 处理headers部分
- local msg = {}
- for k, v in pairs(headers) do
- table.insert(msg, k .. ': ' .. v)
- end
- -- 合并request报文
- str = str .. '\r\n' .. table.concat(msg, '\r\n') .. '\r\n\r\n'
- -- log.info("http.request send:", str:tohex())
- -- 发送请求报文
- local c = socket.tcp()
- if not c:connect(host, port) then
- c:close()
- return '502', 'SOCKET_CONN_ERROR'
- end
- if ctype ~= 3 then
- str = method .. ' ' .. path .. ' HTTP/1.0\r\nHost: ' .. str .. sub .. '\r\n'
- if not c:send(str) then
- c:close()
- return '426', 'SOCKET_SEND_ERROR'
- end
- else
- str = method .. ' ' .. path .. ' HTTP/1.0\r\nHost: ' .. str
- if not c:send(str) then
- c:close()
- return '426', 'SOCKET_SEND_ERROR'
- end
- local file = io.open(data, 'r')
- if file then
- while true do
- local dat = file:read(1024)
- if dat == nil then
- io.close(file)
- break
- end
- log.info('http.request dat:', dat:tohex())
- if not c:send(dat) then
- io.close(file)
- c:close()
- return '426', 'SOCKET_SEND_ERROR'
- end
- end
- end
- if not c:send('\r\n') then
- c:close()
- return '426', 'SOCKET_SEND_ERROR'
- end
- end
- msg = {}
- r, s = c:recv(timeout)
- if not r then
- return '503', 'SOCKET_RECV_TIMOUT'
- end
- response_code = s:match(' (%d+) ')
- response_message = s:match(' (%a+)')
- log.info('http.response code and message:\t', response_code, response_message)
- for k, v in s:gmatch('([%a%-]+): (%C+)') do
- response_header[k] = v
- end
- gzip = s:match('%aontent%-%ancoding: (%a+)')
- while true do
- table.insert(msg, s)
- r, s = c:recv(timeout)
- if not r then
- break
- end
- end
- c:close()
- str = table.concat(msg)
- sub, len = str:find('\r?\n\r?\n')
- if gzip then
- return response_code, response_header, ((zlib.inflate(table.concat(msg))):read())
- end
- return response_code, response_header, str:sub(len + 1, -1)
- end
|