本文描述 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