1. 程式人生 > >[總結] nginx+lua 請求body過大導致get_post_args()無法獲取到引數

[總結] 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