nginx+lua-请求body过大导致get_post_args()无法获取到参数

本文描述 nginx + lua 解析 http 报文参数并计算文件md5的详细解决方法。

其中包括解析http 报文参数,计算上传文件md5,并解决了当请求body 大于client_body_buffer_size导致ngx.req.get_post_args()无法获取到参数的问题。

问题:request body 大于client_body_buffer_size,导致ngx.req.get_post_args()无法获取到参数。

原因分析:当post请求body size大于client_body_buffer_size 默认值8k或16k时,请求报文将会被nginx缓存到硬盘,此时ngx.req.get_post_args()无法获取到参数,此时post参数需要从ngx.req.get_body_data() 或者ngx.req.get_body_file()中获取,获取后的参数是进过unicode编码过的,我们如果要取得原始的值,还需要进行unicode解码。

解决方法:

1.修改nginx client_body_buffer_size 128k,或者更大。

2.当ngx.req.get_post_args()无法获取到参数时,从ngx.req.get_body_data() 或者ngx.req.get_body_file()中手动解析参数。

以下是核心代码:

local function init_form_args()  
    -- 返回args 和 文件md5
    local args = {}  
    local file_args = {}  
    local is_have_file_param = false  
    local receive_headers = ngx.req.get_headers()  
    local request_method = ngx.var.request_method  
    local error_code = 0
    local error_msg = "未初始化"
    local file_md5 = ""
  
    if "GET" == request_method then  
      -- 普通get请求
        args = ngx.req.get_uri_args()  
    elseif "POST" == request_method then  
        ngx.req.read_body()  
        if string.sub(receive_headers["content-type"],1,20) == "multipart/form-data;" then--判断是否是multipart/form-data类型的表单  
            is_have_file_param = true  
            local body_data = ngx.req.get_body_data()--body_data可是符合http协议的请求体,不是普通的字符串  
            --请求体的size大于nginx配置里的client_body_buffer_size,则会导致请求体被缓冲到磁盘临时文件里,client_body_buffer_size默认是8k或者16k  
            if not body_data then  
                local datafile = ngx.req.get_body_file()  
            
                if not datafile then  
                    error_code = 1  
                    error_msg = "no request body found"  
                else  
                    local fh, err = io.open(datafile, "r")  
                    if not fh then  
                        error_code = 2  
                        error_msg = "failed to open " .. tostring(datafile) .. "for reading: " .. tostring(err)  
                    else  
                        fh:seek("set")  
                        body_data = fh:read("*a") 
                        fh:close()  
                        if body_data == "" then  
                            error_code = 3  
                            error_msg = "request body is empty"  
                        end  
                    end  
                end  
            end  
            local new_body_data = {}  
            --确保取到请求体的数据  
            if error_code == 0 then  
                local boundary = get_boundary(body_data)  
                -- 兼容处理:当content-type中取不到boundary时,直接从body首行提取。
                local body_data_table = explode(tostring(body_data), boundary)  
                local first_string = table.remove(body_data_table,1)
                local last_string = table.remove(body_data_table)
                -- ngx.log(ngx.ERR, ">>>>>>>>>>>>>>>>>>>>>start\n", table.concat(body_data_table,"<<<<<<>>>>>>"), ">>>>>>>>>>>>>>>>>>>>>end\n")
                for i,v in ipairs(body_data_table) do  
                    local start_pos,end_pos,capture,capture2 = string.find(v,'Content%-Disposition: form%-data; name="(.+)"; filename="(.*)"')  
                    if not start_pos then
                        --普通参数  
                        local t = explode(v,"\r\n\r\n")  
                        --[[
                          按照双换行切分后得到的table
                          第一个元素,t[1]='
                          Content-Disposition: form-data; name="_data_"
                          Content-Type: text/plain; charset=UTF-8
                          Content-Transfer-Encoding: 8bit
                          '
                          第二个元素,t[2]='{"fileName":"redis-use.png","bizCode":"1099","description":"redis使用范例.png","type":"application/octet-stream"}'
              
                          从第一个元素中提取到参数名称,第二个元素就是参数的值。
                        ]]
                        local param_name_start_pos, param_name_end_pos, temp_param_name = string.find(t[1],'Content%-Disposition: form%-data; name="(.+)"')   
                        local temp_param_value = string.sub(t[2],1,-3)  
                        args[temp_param_name] = temp_param_value  
                    end  
                end  
 
                -- 找到第一行\r\n\r\n进行切割,将Contentype-*的内容去掉
                local file_pos_start, file_pos_end = string.find(last_string, "\r\n\r\n")
                local temps_table = explode(boundary, "\r\n") 
                -- local boundary_end = temps_table[1] .. "--"
                local boundary_end = temps_table[1]
                -- ngx.log(ngx.ERR, "boundary_end:", boundary_end)
                -- ngx.log(ngx.ERR, "文件报文原文:", last_string)
                local last_boundary_pos_start, last_boundary_pos_end = string.find(last_string, boundary_end)
                -- 减去3是为了:\r\n两个字符,加上sub对end标记的位置是闭合的截取策略,所以还要减去1。
                local file_string = string.sub(last_string, file_pos_end+1, last_boundary_pos_start - 3)
                -- 去掉最后的换行符
                -- file_string
                file_md5 = ngx.md5(file_string)
                -- ngx.log(ngx.ERR, "文件报文原文:", file_string)
                -- ngx.log(ngx.ERR, "\nfile_real_md5:", real_md5)
                -- local lua_md5 = md5.sumhexa(file_string)
                -- ngx.log(ngx.ERR, "字符串长度:", #file_string)
                -- 调试代码,当计算MD5不一致时,直接读取文件,计算长度.
                --[[
                local file = io.open("/var/tmp/readme.txt","r")
                local string_source = file:read("*a")
                file:close()
                local red_md5 = md5.sumhexa(string_source)
                ngx.log(ngx.ERR, "直接读取文件原文:",string_source)
                ngx.log(ngx.ERR, "字符串长度:", #string_source)
                ngx.log(ngx.ERR, "直接读取文件计算md5:", red_md5)
                ]]
            end  
        else  
          -- 普通post请求
          args = ngx.req.get_post_args()
          --[[
            请求体的size大于nginx配置里的client_body_buffer_size,则会导致请求体被缓冲到磁盘临时文件里
            此时,get_post_args 无法获取参数时,需要从缓冲区文件中读取http报文。http报文遵循param1=value1&param2=value2的格式。
          ]]
          if not args then
              args = {}
              -- body_data可是符合http协议的请求体,不是普通的字符串
              local body_data = ngx.req.get_body_data()
              -- client_body_buffer_size默认是8k或者16k
              if not body_data then
                  local datafile = ngx.req.get_body_file()
                  if not datafile then
                      error_code = 1
                      error_msg = "no request body found"
                  else
                      local fh, err = io.open(datafile, "r")
                      if not fh then
                          error_code = 2
                          error_msg = "failed to open " .. tostring(datafile) .. "for reading: " .. tostring(err)
                      else
                          fh:seek("set")
                          body_data = fh:read("*a")
                          fh:close()
                          if body_data == "" then
                              error_code = 3
                              error_msg = "request body is empty"
                          end
                      end
                  end
              end
              -- 解析body_data
              local post_param_table = explode(tostring(body_data), "&")
              for i,v in ipairs(post_param_table) do
                  local paramEntity = explode(v,"=")
                  local tempValue = paramEntity[2]
                  -- 对请求参数的value进行unicode解码
                  tempValue = unescape(tempValue)
                  args[paramEntity[1]] = tempValue
              end
          end
        end  
    end  
    return args, file_md5;
end

其他依赖函数:

-- 解析得到boundary
local function get_boundary(body_data)
  -- 解析body首行或者第二行,直到解析到--$boundary。若三行内都没解析到boundary,则视为异常。
  local first_position = string.find(body_data, "\n");  
  local boundary = string.sub(body_data, 1, first_position)
  if not boundary then
    -- Todo 取第二行作为boundary
  end
 
  return boundary
end
 
-- 字符串分隔得到数组
local function explode ( _str, seperator)  
    local pos, arr = 0, {}  
    for st, sp in function() return string.find( _str, seperator, pos, true ) end do  
      table.insert(arr, string.sub(_str, pos, st-1 ))  
      pos = sp + 1  
    end  
    table.insert(arr, string.sub( _str, pos))  
    return arr  
end 
-- unicode 解码
function unescape (s)
  s = string.gsub(s, "+", " ")
  s = string.gsub(s, "%%(%x%x)", function (h)
    return string.char(tonumber(h, 16))
  end)
  return s
end