Просмотр исходного кода

update: httpplus,优化chunked解析逻辑,优化日志输出,修正mime表格错误,修正header解析逻辑,添加自测脚本

Wendal Chen 1 месяц назад
Родитель
Сommit
39f20ed99e
2 измененных файлов с 225 добавлено и 44 удалено
  1. 159 0
      bsp/pc/test/113.httpplus_test/main.lua
  2. 66 44
      script/libs/httpplus.lua

+ 159 - 0
bsp/pc/test/113.httpplus_test/main.lua

@@ -0,0 +1,159 @@
+-- httpplus 自测脚本(PC模拟器)
+-- 场景覆盖:GET、POST 表单、POST JSON、chunked 响应、multipart 文件上传、bodyfile 上传
+-- 运行环境:LuatOS PC 模拟器,用户自测
+
+-- local sys = require "sys"
+-- local json = require "json"
+local httpplus = require "httpplus"
+
+local TAG = "httpplus_test"
+local ok_count, fail_count = 0, 0
+
+local function equals(a, b)
+    return a == b
+end
+
+local function safe_json_decode(s)
+    local ok, obj = pcall(json.decode, s)
+    if ok then return obj end
+    return nil
+end
+
+local function run_case(name, fn)
+    log.info(TAG, "CASE", name)
+    local ok, err = pcall(fn)
+    if ok and err ~= false then
+        ok_count = ok_count + 1
+        log.info(TAG, "PASS", name)
+    else
+        fail_count = fail_count + 1
+        log.error(TAG, "FAIL", name, err or "")
+    end
+end
+
+sys.taskInit(function()
+    -- 用于上传测试的临时文件
+    local upload_file = "/httpplus_test_file.txt"
+    do
+        local f = io.open(upload_file, "wb")
+        if f then
+            f:write("hello_world_upload")
+            f:close()
+        else
+            log.warn(TAG, "无法创建测试文件", upload_file)
+        end
+    end
+
+    -- 1) GET 测试
+    run_case("GET", function()
+        local code, resp = httpplus.request({
+            url = "https://httpbin.air32.cn/get?foo=bar",
+            headers = { ["User-Agent"] = "LuatOS-httpplus-test" },
+            timeout = 20
+        })
+        if code ~= 200 or not resp or not resp.body then return false, "GET code/body 异常" end
+        local body = resp.body:query()
+        local obj = safe_json_decode(body)
+        if not obj then return false, "GET JSON 解析失败" end
+        if not equals(obj.args and obj.args.foo, "bar") then return false, "GET 参数回显不正确" end
+        return true
+    end)
+
+    -- 2) POST 表单测试(application/x-www-form-urlencoded)
+    run_case("POST forms", function()
+        local code, resp = httpplus.request({
+            url = "https://httpbin.air32.cn/post",
+            forms = { a = "1", b = "空 格", c = "c&=v" },
+            headers = { ["User-Agent"] = "LuatOS-httpplus-test" },
+            timeout = 20
+        })
+        if code ~= 200 or not resp or not resp.body then return false, "forms code/body 异常" end
+        local obj = safe_json_decode(resp.body:query())
+        if not obj then return false, "forms JSON 解析失败" end
+        if not (equals(obj.form and obj.form.a, "1") and obj.form.b and obj.form.c) then
+            return false, "forms 字段回显缺失"
+        end
+        return true
+    end)
+
+    -- 3) POST JSON 测试
+    run_case("POST json", function()
+        local code, resp = httpplus.request({
+            url = "https://httpbin.air32.cn/post",
+            body = { name = "abc", n = 123 },
+            headers = { ["User-Agent"] = "LuatOS-httpplus-test" },
+            timeout = 20
+        })
+        if code ~= 200 or not resp or not resp.body then return false, "json code/body 异常" end
+        local obj = safe_json_decode(resp.body:query())
+        if not obj then return false, "json JSON 解析失败" end
+        if not (obj.json and obj.json.name == "abc" and obj.json.n == 123) then
+            return false, "json 字段回显不正确"
+        end
+        return true
+    end)
+
+    -- 4) chunked 响应测试(httpbin 流式响应)
+    run_case("GET chunked", function()
+        local code, resp = httpplus.request({
+            url = "http://httpbin.air32.cn/stream/5",
+            headers = { ["User-Agent"] = "LuatOS-httpplus-test" },
+            timeout = 20,
+            debug = true
+        })
+        if code ~= 200 or not resp or not resp.body then return false, "chunked code/body 异常" end
+        local used = resp.body:used()
+        if not (used and used > 0) then return false, "chunked body 为空" end
+        -- 基础检验:应已去掉 chunked 框架,不包含明显的长度行
+        local s = resp.body:query()
+        if s:find("\r\n0\r\n") then return false, "chunked 末块残留" end
+        return true
+    end)
+
+    -- 5) multipart 文件上传
+    run_case("POST multipart", function()
+        local code, resp = httpplus.request({
+            url = "https://httpbin.air32.cn/post",
+            files = { file1 = upload_file },
+            forms = { note = "uploader" },
+            headers = { ["User-Agent"] = "LuatOS-httpplus-test" },
+            timeout = 20
+        })
+        if code ~= 200 or not resp or not resp.body then return false, "multipart code/body 异常" end
+        local obj = safe_json_decode(resp.body:query())
+        if not obj then return false, "multipart JSON 解析失败" end
+        if not (obj.files and obj.files.file1 and obj.form and obj.form.note == "uploader") then
+            return false, "multipart 回显缺失"
+        end
+        if not obj.files.file1:find("hello_world_upload") then
+            return false, "multipart 文件内容不匹配"
+        end
+        return true
+    end)
+
+    -- 6) bodyfile 上传(纯文本)
+    run_case("POST bodyfile", function()
+        local code, resp = httpplus.request({
+            url = "https://httpbin.air32.cn/post",
+            bodyfile = upload_file,
+            headers = {
+                ["User-Agent"] = "LuatOS-httpplus-test",
+                ["Content-Type"] = "text/plain"
+            },
+            timeout = 20
+        })
+        if code ~= 200 or not resp or not resp.body then return false, "bodyfile code/body 异常" end
+        local obj = safe_json_decode(resp.body:query())
+        if not obj then return false, "bodyfile JSON 解析失败" end
+        if not (obj.data and obj.data:find("hello_world_upload")) then
+            return false, "bodyfile 数据回显不匹配"
+        end
+        return true
+    end)
+
+    log.info(TAG, "RESULT", string.format("OK=%d FAIL=%d", ok_count, fail_count))
+    sys.wait(1000)
+    -- 测试结束
+end)
+
+sys.run()

+ 66 - 44
script/libs/httpplus.lua

@@ -44,7 +44,7 @@ local function http_opts_parse(opts)
         return -100, "opts不能为nil"
     end
     if not opts.url or #opts.url < 5 then
-        log.error(TAG, "URL不存在或者太短了", url)
+        log.error(TAG, "URL不存在或者太短了", opts.url)
         return -100, "URL不存在或者太短了"
     end
     if not opts.headers then
@@ -86,7 +86,7 @@ local function http_opts_parse(opts)
     end
     -- log.info("http分解阶段2", is_ssl, tmp, uri)
     if tmp == nil or #tmp == 0 then
-        log.error(TAG, "非法的URL", url)
+        log.error(TAG, "非法的URL", opts.url)
         return -101, "非法的URL"
     end
     -- 有无鉴权信息
@@ -115,7 +115,11 @@ local function http_opts_parse(opts)
     end
     -- 收尾工作
     if not opts.headers["Host"] then
-        opts.headers["Host"] = string.format("%s:%d", host, port)
+        if (is_ssl and port == 443) or ((not is_ssl) and port == 80) then
+            opts.headers["Host"] = host
+        else
+            opts.headers["Host"] = string.format("%s:%d", host, port)
+        end
     end
     -- Connection 必须关闭
     opts.headers["Connection"] = "Close"
@@ -147,9 +151,9 @@ local function http_opts_parse(opts)
             if not opts.headers["Content-Type"] then
                 opts.headers["Content-Type"] = "application/x-www-form-urlencoded;charset=UTF-8"
             end
-            local buff = zbuff.create(120)
+            local buff = zbuff.create(256)
             for kk, vv in pairs(opts.forms) do
-                buff:copy(nil, kk)
+                buff:copy(nil, string.urlEncode(tostring(kk)))
                 buff:copy(nil, "=")
                 buff:copy(nil, string.urlEncode(tostring(vv)))
                 buff:copy(nil, "&")
@@ -173,7 +177,7 @@ local function http_opts_parse(opts)
             jpeg = "image/jpeg",            -- JPEG 格式图片
             png = "image/png",              -- PNG 格式图片   
             gif = "image/gif",              -- GIF 格式图片
-            html = "image/html",            -- HTML
+            html = "text/html",             -- HTML
             json = "application/json",      -- JSON
             mp4 = "video/mp4",              -- MP4 格式视频
             mp3 = "audio/mp3",              -- MP3 格式音频
@@ -181,7 +185,7 @@ local function http_opts_parse(opts)
         }
         for kk, vv in pairs(opts.files) do
             local ct = contentType[vv:match("%.(%w+)$")] or "application/octet-stream"
-            local fname = vv:match("[^%/]+%w$")
+            local fname = vv:match("([^/\\]+)$") or vv
             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)
             -- log.info("文件传输头", tmp)
             table.insert(opts.mp, {vv, tmp, "file"})
@@ -329,7 +333,7 @@ local function resp_parse(opts)
     opts.rx_buff:del(0, state_line_offset + 2)
     -- opts.log(TAG, "剩余的响应体", opts.rx_buff:query())
 
-    -- 解析headers
+    -- 解析headers(仅按首个冒号拆分,保留值中的冒号)
     while 1 do
         local offset = zbuff_find(opts.rx_buff, "\r\n")
         if not offset then
@@ -343,9 +347,15 @@ local function resp_parse(opts)
         end
         local line = opts.rx_buff:query(0, offset)
         opts.rx_buff:del(0, offset + 2)
-        local tmp2 = line:split(":")
-        opts.log(TAG, tmp2[1]:trim(), tmp2[2]:trim())
-        opts.resp.headers[tmp2[1]:trim()] = tmp2[2]:trim()
+        local name, value = line:match("^([^:]+):%s*(.*)$")
+        if name and value then
+            name = name:trim()
+            value = value:trim()
+            opts.log(TAG, name, value)
+            opts.resp.headers[name] = value
+        else
+            opts.log(TAG, "忽略非法header行", line)
+        end
     end
 
     -- if opts.resp_code < 299 then
@@ -353,45 +363,57 @@ local function resp_parse(opts)
         -- 有Content-Length就好办
         if opts.resp.headers["Content-Length"] then
             opts.log(TAG, "有Content-Length", opts.resp.headers["Content-Length"])
+            local declared = tonumber(opts.resp.headers["Content-Length"]) or 0
+            if declared > 0 and opts.rx_buff:used() >= declared then
+                opts.rx_buff:resize(declared)
+            end
             opts.resp.body = opts.rx_buff
         elseif opts.resp.headers["Transfer-Encoding"] == "chunked" then
-            -- log.info(TAG, "数据是chunked编码", opts.rx_buff[0], opts.rx_buff[1])
-            -- log.info(TAG, "数据是chunked编码", opts.rx_buff:query(0, 4):toHex())
-            local coffset = 0
-            local crun = true
-            while crun and coffset < opts.rx_buff:used() do
-                -- 从当前offset读取长度, 长度总不会超过8字节吧?
-                local flag = true
-                -- local coffset = zbuff_find(opts.rx_buff, "\r\n")
-                -- if not coffset then
-                    
-                -- end
-                for i = 1, 8, 1 do
-                    if opts.rx_buff[coffset+i] == 0x0D and opts.rx_buff[coffset+i+1] == 0x0A then
-                        local ctmp = opts.rx_buff:query(coffset, i)
-                        -- opts.log(TAG, "chunked分片长度", ctmp, ctmp:toHex())
-                        local clen = tonumber(ctmp, 16)
-                        -- opts.log(TAG, "chunked分片长度2", clen)
-                        if clen == nil or clen == 0 then
-                            -- 末尾了
-                            opts.rx_buff:resize(coffset)
-                            crun = false
-                        else
-                            -- 先删除chunked块
-                            opts.rx_buff:del(coffset, i+2)
-                            coffset = coffset + clen
-                        end
-                        flag = false
-                        break
+            -- 解析 chunked 编码:长度行(可含分号扩展)+ 数据 + CRLF,末块长度为0
+            local function zbuff_find_from(buff, str, start_off)
+                local used = buff:used()
+                if used - start_off < #str then return end
+                local maxoff = used - #str
+                local tmp2 = zbuff.create(#str)
+                tmp2:write(str)
+                for i = start_off, maxoff, 1 do
+                    local ok = true
+                    for j = 0, #str - 1, 1 do
+                        if buff[i+j] ~= tmp2[j] then ok = false; break end
                     end
+                    if ok then return i end
                 end
-                -- 肯定能搜到chunked
-                if flag then
-                    log.error("非法的chunked块")
+            end
+            local body = zbuff.create(opts.rx_buff:used())
+            local pos = 0
+            while true do
+                local line_end = zbuff_find_from(opts.rx_buff, "\r\n", pos)
+                if not line_end then
+                    log.error(TAG, "非法的chunk长度行")
                     break
                 end
+                local len_line = opts.rx_buff:query(pos, line_end - pos)
+                local semi = len_line:find(";")
+                local hex = semi and len_line:sub(1, semi - 1) or len_line
+                local clen = tonumber(hex, 16)
+                if not clen then
+                    log.error(TAG, "非法的chunk长度值", len_line)
+                    break
+                end
+                pos = line_end + 2
+                if clen == 0 then
+                    -- 末块:忽略后续 trailers
+                    break
+                end
+                if pos + clen > opts.rx_buff:used() then
+                    log.error(TAG, "chunk数据长度不足")
+                    break
+                end
+                local chunk = opts.rx_buff:query(pos, clen)
+                body:copy(nil, chunk)
+                pos = pos + clen + 2 -- 跳过数据及其后的CRLF
             end
-            opts.resp.body = opts.rx_buff
+            opts.resp.body = body
         end
     -- end
 
@@ -416,7 +438,7 @@ local function http_socket_cb(opts, event)
         -- 收到数据或者链接断开了, 这里总需要读取一次才知道
         local succ, data_len = socket.rx(opts.netc, opts.rx_buff)
         if succ and data_len > 0 then
-            opts.log(TAG, "收到数据", data_len, "总长", #opts.rx_buff)
+            opts.log(TAG, "收到数据", data_len, "总长", opts.rx_buff:used())
             -- opts.log(TAG, "数据", opts.rx_buff:query())
         else
             if not opts.is_closed then