[總結] nginx+lua 請求body過大導致get_post_args()無法獲取到引數
阿新 • • 發佈:2019-02-06
本文描述 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¶m2=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