add: 添加新的阿里云物联网库-aliyun2库,及搭配的demo, 逐步完善中
[LuatOS.git] / script / libs / httpplus.lua
blob379186cbd8a87ce128ba0cef88aca461f3503bf8
1 --[[
2 @module httpplus
3 @summary http库的补充
4 @version 1.0
5 @date 2023.11.23
6 @author wendal
7 @demo httpplus
8 @tag LUAT_USE_NETWORK
9 @usage
10 -- 本库支持的功能有:
11 -- 1. 大文件上传的问题,不限大小
12 -- 2. 任意长度的header设置
13 -- 3. 任意长度的body设置
14 -- 4. 鉴权URL自动识别
15 -- 5. body使用zbuff返回,可直接传输给uart等库
17 -- 与http库的差异
18 -- 1. 不支持文件下载
19 -- 2. 不支持fota
23 local httpplus = {}
24 local TAG = "httpplus"
26 local function http_opts_parse(opts)
27 if not opts then
28 log.error(TAG, "opts不能为nil")
29 return -100, "opts不能为nil"
30 end
31 if not opts.url or #opts.url < 5 then
32 log.error(TAG, "URL不存在或者太短了", url)
33 return -100, "URL不存在或者太短了"
34 end
35 if not opts.headers then
36 opts.headers = {}
37 end
39 if opts.debug or httpplus.debug then
40 if not opts.log then
41 opts.log = log.debug
42 end
43 else
44 opts.log = function()
45 -- log.info(TAG, "无日志")
46 end
47 end
49 -- 解析url
50 -- 先判断协议是否加密
51 local is_ssl = false
52 local tmp = ""
53 if opts.url:startsWith("https://") then
54 is_ssl = true
55 tmp = opts.url:sub(9)
56 elseif opts.url:startsWith("http://") then
57 tmp = opts.url:sub(8)
58 else
59 tmp = opts.url
60 end
61 -- log.info("http分解阶段1", is_ssl, tmp)
62 -- 然后判断host段
63 local uri = ""
64 local host = ""
65 local port = 0
66 if tmp:find("/") then
67 uri = tmp:sub((tmp:find("/"))) -- 注意find会返回多个值
68 tmp = tmp:sub(1, tmp:find("/") - 1)
69 else
70 uri = "/"
71 end
72 -- log.info("http分解阶段2", is_ssl, tmp, uri)
73 if tmp == nil or #tmp == 0 then
74 log.error(TAG, "非法的URL", url)
75 return -101, "非法的URL"
76 end
77 -- 有无鉴权信息
78 if tmp:find("@") then
79 local auth = tmp:sub(1, tmp:find("@") - 1)
80 if not opts.headers["Authorization"] then
81 opts.headers["Authorization"] = "Basic " .. auth:toBase64()
82 end
83 -- log.info("http鉴权信息", auth, opts.headers["Authorization"])
84 tmp = tmp:sub(tmp:find("@") + 1)
85 end
86 -- 解析端口
87 if tmp:find(":") then
88 host = tmp:sub(1, tmp:find(":") - 1)
89 port = tmp:sub(tmp:find(":") + 1)
90 port = tonumber(port)
91 else
92 host = tmp
93 end
94 if not port or port < 1 then
95 if is_ssl then
96 port = 443
97 else
98 port = 80
99 end
101 -- 收尾工作
102 if not opts.headers["Host"] then
103 opts.headers["Host"] = string.format("%s:%d", host, port)
105 -- Connection 必须关闭
106 opts.headers["Connection"] = "Close"
108 -- 复位一些变量,免得判断出错
109 opts.is_closed = nil
110 opts.body_len = 0
112 -- multipart需要boundary
113 local boundary = "------------------------16ef6e68ef" .. tostring(os.time())
114 opts.boundary = boundary
115 opts.mp = {}
117 if opts.files then
118 -- 强制设置为true
119 opts.multipart = true
120 local contentType =
122 txt = "text/plain", -- 文本
123 jpg = "image/jpeg", -- JPG 格式图片
124 jpeg = "image/jpeg", -- JPEG 格式图片
125 png = "image/png", -- PNG 格式图片
126 gif = "image/gif", -- GIF 格式图片
127 html = "image/html", -- HTML
128 json = "application/json" -- JSON
130 for kk, vv in pairs(opts.files) do
131 local ct = contentType[vv:match("%.(%w+)$")] or "application/octet-stream"
132 local fname = vv:match("[^%/]+%w$")
133 local tmp = string.format("--%s\r\nContent-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\nContent-Type: %s\r\n\r\n", boundary, kk, fname, ct)
134 -- log.info("文件传输头", tmp)
135 table.insert(opts.mp, {vv, tmp, "file"})
136 opts.body_len = opts.body_len + #tmp + io.fileSize(vv) + 2
137 -- log.info("当前body长度", opts.body_len, "文件长度", io.fileSize(vv), fname, ct)
141 -- 表单数据
142 if opts.forms then
143 if opts.multipart then
144 for kk, vv in pairs(opts.forms) do
145 local tmp = string.format("--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n", boundary, kk)
146 table.insert(opts.mp, {vv, tmp, "form"})
147 opts.body_len = opts.body_len + #tmp + #vv + 2
148 -- log.info("当前body长度", opts.body_len, "数据长度", #vv)
150 else
151 if not opts.headers["Content-Type"] then
152 opts.headers["Content-Type"] = "application/x-www-form-urlencoded;charset=UTF-8"
154 local buff = zbuff.create(120)
155 for kk, vv in pairs(opts.forms) do
156 buff:copy(nil, kk)
157 buff:copy(nil, "=")
158 buff:copy(nil, string.urlEncode(tostring(vv)))
159 buff:copy(nil, "&")
161 if buff:used() > 0 then
162 buff:del(-1, 1)
163 opts.body = buff
164 opts.body_len = buff:used()
165 opts.log(TAG, "普通表单", opts.body)
170 -- 如果multipart模式
171 if opts.multipart then
172 -- 如果没主动设置body, 那么补个结尾
173 if not opts.body then
174 opts.body_len = opts.body_len + #boundary + 2 + 2 + 2
176 -- Content-Type没设置? 那就设置一下
177 if not opts.headers["Content-Type"] then
178 opts.headers["Content-Type"] = "multipart/form-data; boundary="..boundary
182 -- 直接设置bodyfile
183 if opts.bodyfile then
184 local fd = io.open(opts.bodyfile, "rb")
185 if not fd then
186 log.error("httpplus", "bodyfile失败,文件不存在", opts.bodyfile)
187 return -104, "bodyfile失败,文件不存在"
189 fd:close()
190 opts.body_len = io.fileSize(opts.bodyfile)
193 -- 有设置body, 而且没设置长度
194 if opts.body and (not opts.body_len or opts.body_len == 0) then
195 -- body是zbuff的情况
196 if type(opts.body) == "userdata" then
197 opts.body_len = opts.body:used()
198 -- body是json的情况
199 elseif type(opts.body) == "table" then
200 opts.body = json.encode(opts.body, "7f")
201 if opts.body then
202 opts.body_len = #opts.body
203 if not opts.headers["Content-Type"] then
204 opts.headers["Content-Type"] = "application/json;charset=UTF-8"
205 opts.log(TAG, "JSON", opts.body)
208 -- 其他情况就只能当文本了
209 else
210 opts.body = tostring(opts.body)
211 opts.body_len = #opts.body
214 -- 一定要设置Content-Length,而且强制覆盖客户自定义的值
215 -- opts.body_len = opts.body_len or 0
216 opts.headers["Content-Length"] = tostring(opts.body_len or 0)
218 -- 如果没设置method, 自动补齐
219 if not opts.method or #opts.method == 0 then
220 if opts.body_len > 0 then
221 opts.method = "POST"
222 else
223 opts.method = "GET"
225 else
226 -- 确保一定是大写字母
227 opts.method = opts.method:upper()
230 if opts.debug then
231 opts.log(TAG, is_ssl, host, port, uri, json.encode(opts.headers))
235 -- 把剩余的属性设置好
236 opts.host = host
237 opts.port = port
238 opts.uri = uri
239 opts.is_ssl = is_ssl
241 if not opts.timeout or opts.timeout == 0 then
242 opts.timeout = 30
245 return -- 成功完成,不需要返回值
250 local function zbuff_find(buff, str)
251 -- log.info("zbuff查找", buff:used(), #str)
252 if buff:used() < #str then
253 return
255 local maxoff = buff:used()
256 maxoff = maxoff - #str
257 local tmp = zbuff.create(#str)
258 tmp:write(str)
259 -- log.info("tmp数据", tmp:query():toHex())
260 for i = 0, maxoff, 1 do
261 local flag = true
262 for j = 0, #str - 1, 1 do
263 -- log.info("对比", i, j, string.char(buff[i+j]):toHex(), string.char(tmp[j]):toHex(), buff[i+j] ~= tmp[j])
264 if buff[i+j] ~= tmp[j] then
265 flag = false
266 break
269 if flag then
270 return i
275 local function resp_parse(opts)
276 -- log.info("这里--------")
277 local header_offset = zbuff_find(opts.rx_buff, "\r\n\r\n")
278 -- log.info("头部偏移量", header_offset)
279 if not header_offset then
280 log.warn(TAG, "没有检测到http响应头部,非法响应")
281 opts.resp_code = -198
282 return
284 local state_line_offset = zbuff_find(opts.rx_buff, "\r\n")
285 local state_line = opts.rx_buff:query(0, state_line_offset)
286 local tmp = state_line:split(" ")
287 if not tmp or #tmp < 2 then
288 log.warn(TAG, "非法的响应行", state_line)
289 opts.resp_code = -197
290 return
292 local code = tonumber(tmp[2])
293 if not code then
294 log.warn(TAG, "非法的响应码", tmp[2])
295 opts.resp_code = -196
296 return
298 opts.resp_code = code
299 opts.resp = {
300 headers = {}
302 opts.log(TAG, "state code", code)
303 -- TODO 解析header和body
305 opts.rx_buff:del(0, state_line_offset + 2)
306 -- opts.log(TAG, "剩余的响应体", opts.rx_buff:query())
308 -- 解析headers
309 while 1 do
310 local offset = zbuff_find(opts.rx_buff, "\r\n")
311 if not offset then
312 log.warn(TAG, "不合法的剩余headers", opts.rx_buff:query())
313 break
315 if offset == 0 then
316 -- header的最后一个空行
317 opts.rx_buff:del(0, 2)
318 break
320 local line = opts.rx_buff:query(0, offset)
321 opts.rx_buff:del(0, offset + 2)
322 local tmp2 = line:split(":")
323 opts.log(TAG, tmp2[1]:trim(), tmp2[2]:trim())
324 opts.resp.headers[tmp2[1]:trim()] = tmp2[2]:trim()
327 -- if opts.resp_code < 299 then
328 -- 解析body
329 -- 有Content-Length就好办
330 if opts.resp.headers["Content-Length"] then
331 opts.log(TAG, "有长度, 标准的咯")
332 opts.resp.body = opts.rx_buff
333 elseif opts.resp.headers["Transfer-Encoding"] == "chunked" then
334 -- log.info(TAG, "数据是chunked编码", opts.rx_buff[0], opts.rx_buff[1])
335 -- log.info(TAG, "数据是chunked编码", opts.rx_buff:query(0, 4):toHex())
336 local coffset = 0
337 local crun = true
338 while crun and coffset < opts.rx_buff:used() do
339 -- 从当前offset读取长度, 长度总不会超过8字节吧?
340 local flag = true
341 -- local coffset = zbuff_find(opts.rx_buff, "\r\n")
342 -- if not coffset then
344 -- end
345 for i = 1, 8, 1 do
346 if opts.rx_buff[coffset+i] == 0x0D and opts.rx_buff[coffset+i+1] == 0x0A then
347 local ctmp = opts.rx_buff:query(coffset, i)
348 -- opts.log(TAG, "chunked分片长度", ctmp, ctmp:toHex())
349 local clen = tonumber(ctmp, 16)
350 -- opts.log(TAG, "chunked分片长度2", clen)
351 if clen == 0 then
352 -- 末尾了
353 opts.rx_buff:resize(coffset)
354 crun = false
355 else
356 -- 先删除chunked块
357 opts.rx_buff:del(coffset, i+2)
358 coffset = coffset + clen
360 flag = false
361 break
364 -- 肯定能搜到chunked
365 if flag then
366 log.error("非法的chunked块")
367 break
370 opts.resp.body = opts.rx_buff
372 -- end
374 -- 清空rx_buff
375 opts.rx_buff = nil
377 -- 完结散花
380 -- socket 回调函数
381 local function http_socket_cb(opts, event)
382 opts.log(TAG, "tcp.event", event)
383 if event == socket.ON_LINE then
384 -- TCP链接已建立, 那就可以上行了
385 -- opts.state = "ON_LINE"
386 sys.publish(opts.topic)
387 elseif event == socket.TX_OK then
388 -- 数据传输完成, 如果是文件上传就需要这个消息
389 -- opts.state = "TX_OK"
390 sys.publish(opts.topic)
391 elseif event == socket.EVENT then
392 -- 收到数据或者链接断开了, 这里总需要读取一次才知道
393 local succ, data_len = socket.rx(opts.netc, opts.rx_buff)
394 if succ and data_len > 0 then
395 opts.log(TAG, "收到数据", data_len, "总长", #opts.rx_buff)
396 -- opts.log(TAG, "数据", opts.rx_buff:query())
397 else
398 if not opts.is_closed then
399 opts.log(TAG, "服务器已经断开了连接或接收出错")
400 opts.is_closed = true
401 sys.publish(opts.topic)
404 elseif event == socket.CLOSED then
405 log.info(TAG, "连接已关闭")
406 opts.is_closed = true
407 sys.publish(opts.topic)
411 local function http_exec(opts)
412 local netc = socket.create(opts.adapter, function(sc, event)
413 if opts.netc then
414 return http_socket_cb(opts, event)
416 end)
417 if not netc then
418 log.error(TAG, "创建socket失败了!!")
419 return -102
421 opts.netc = netc
422 opts.rx_buff = zbuff.create(1024)
423 opts.topic = tostring(netc)
424 socket.config(netc, nil,nil, opts.is_ssl)
425 if opts.debug or httpplus.debug then
426 socket.debug(netc)
428 if not socket.connect(netc, opts.host, opts.port, opts.try_ipv6) then
429 log.warn(TAG, "调用socket.connect返回错误了")
430 return -103, "调用socket.connect返回错误了"
432 local ret = sys.waitUntil(opts.topic, 5000)
433 if ret == false then
434 log.warn(TAG, "建立连接超时了!!!")
435 return -104, "建立连接超时了!!!"
438 -- 首先是头部
439 local line = string.format("%s %s HTTP/1.1\r\n", opts.method:upper(), opts.uri)
440 -- opts.log(TAG, line)
441 socket.tx(netc, line)
442 for k, v in pairs(opts.headers) do
443 line = string.format("%s: %s\r\n", k, v)
444 socket.tx(netc, line)
446 line = "\r\n"
447 socket.tx(netc, line)
449 -- 然后是body
450 local rbody = ""
451 local write_counter = 0
452 if opts.mp and #opts.mp > 0 then
453 opts.log(TAG, "执行mulitpart上传模式")
454 for k, v in pairs(opts.mp) do
455 socket.tx(netc, v[2])
456 write_counter = write_counter + #v[2]
457 if v[3] == "file" then
458 -- log.info("写入文件数据头", v[2])
459 local fd = io.open(v[1], "rb")
460 -- log.info("写入文件数据", v[1])
461 if fd then
462 while not opts.is_closed do
463 local fdata = fd:read(1400)
464 if not fdata or #fdata == 0 then
465 break
467 -- log.info("写入文件数据", "长度", #fdata)
468 socket.tx(netc, fdata)
469 write_counter = write_counter + #fdata
470 -- 注意, 这里要等待TX_OK事件
471 sys.waitUntil(opts.topic, 3000)
473 fd:close()
475 else
476 socket.tx(netc, v[1])
477 write_counter = write_counter + #v[1]
479 socket.tx(netc, "\r\n")
480 write_counter = write_counter + 2
482 -- rbody = rbody .. "--" .. opts.boundary .. "--\r\n"
483 socket.tx(netc, "--")
484 socket.tx(netc, opts.boundary)
485 socket.tx(netc, "--\r\n")
486 write_counter = write_counter + #opts.boundary + 2 + 2 + 2
487 elseif opts.bodyfile then
488 local fd = io.open(opts.bodyfile, "rb")
489 -- log.info("写入文件数据", v[1])
490 if fd then
491 while not opts.is_closed do
492 local fdata = fd:read(1400)
493 if not fdata or #fdata == 0 then
494 break
496 -- log.info("写入文件数据", "长度", #fdata)
497 socket.tx(netc, fdata)
498 write_counter = write_counter + #fdata
499 -- 注意, 这里要等待TX_OK事件
500 sys.waitUntil(opts.topic, 300)
502 fd:close()
504 elseif opts.body then
505 if type(opts.body) == "string" and #opts.body > 0 then
506 socket.tx(netc, opts.body)
507 write_counter = write_counter + #opts.body
508 elseif type(opts.body) == "userdata" then
509 write_counter = write_counter + opts.body:used()
510 if opts.body:used() < 4*1024 then
511 socket.tx(netc, opts.body)
512 else
513 local offset = 0
514 local tmpbuff = opts.body
515 local tsize = tmpbuff:used()
516 while offset < tsize do
517 opts.log(TAG, "body(zbuff)分段写入", offset, tsize)
518 if tsize - offset > 4096 then
519 socket.tx(netc, tmpbuff:toStr(offset, 4096))
520 offset = offset + 4096
521 sys.waitUntil(opts.topic, 300)
522 else
523 socket.tx(netc, tmpbuff:toStr(offset, tsize - offset))
524 break
530 -- log.info("写入长度", "期望", opts.body_len, "实际", write_counter)
531 -- log.info("hex", rbody)
533 -- 处理响应信息
534 while not opts.is_closed and opts.timeout > 0 do
535 log.info(TAG, "等待服务器完成响应")
536 sys.waitUntil(opts.topic, 1000)
537 opts.timeout = opts.timeout - 1
539 log.info(TAG, "服务器已完成响应,开始解析响应")
540 resp_parse(opts)
541 -- log.info("执行完成", "返回结果")
544 --[[
545 执行HTTP请求
546 @api httpplus.request(opts)
547 @table 请求参数,是一个table,最起码得有url属性
548 @return int 响应码,服务器返回的状态码>=100, 若本地检测到错误,会返回<0的值
549 @return 服务器正常响应时返回结果, 否则是错误信息或者nil
550 @usage
551 -- 请求参数介绍
552 local opts = {
553 url = "https://httpbin.air32.cn/abc", -- 必选, 目标URL
554 method = "POST", -- 可选,默认GET, 如果有body,files,forms参数,会设置成POST
555 headers = {}, -- 可选,自定义的额外header
556 files = {}, -- 可选,文件上传,若存在本参数,会强制以multipart/form-data形式上传
557 forms = {}, -- 可选,表单参数,若存在本参数,如果不存在files,按application/x-www-form-urlencoded上传
558 body = "abc=123",-- 可选,自定义body参数, 字符串/zbuff/table均可, 但不能与files和forms同时存在
559 debug = false, -- 可选,打开调试日志,默认false
560 try_ipv6 = false, -- 可选,是否优先尝试ipv6地址,默认是false
561 adapter = nil, -- 可选,网络适配器编号, 默认是自动选
562 timeout = 30, -- 可选,读取服务器响应的超时时间,单位秒,默认30
563 bodyfile = "xxx" -- 可选,直接把文件内容作为body上传, 优先级高于body参数
566 local code, resp = httpplus.request({url="https://httpbin.air32.cn/get"})
567 log.info("http", code)
568 -- 返回值resp的说明
569 -- 情况1, code >= 100 时, resp会是个table, 包含2个元素
570 if code >= 100 then
571 -- headers, 是个table
572 log.info("http", "headers", json.encode(resp.headers))
573 -- body, 是个zbuff
574 -- 通过query函数可以转为lua的string
575 log.info("http", "headers", resp.body:query())
576 -- 也可以通过uart.tx等支持zbuff的函数转发出去
577 -- uart.tx(1, resp.body)
580 function httpplus.request(opts)
581 -- 参数解析
582 local ret = http_opts_parse(opts)
583 if ret then
584 return ret
587 -- 执行请求
588 local ret, msg = pcall(http_exec, opts)
589 if opts.netc then
590 -- 清理连接
591 if not opts.is_closed then
592 socket.close(opts.netc)
594 socket.release(opts.netc)
595 opts.netc = nil
597 -- 处理响应或错误
598 if not ret then
599 log.error(TAG, msg)
600 return -199, msg
602 return opts.resp_code, opts.resp
605 return httpplus