593 lines
13 KiB
Lua
593 lines
13 KiB
Lua
local cjson = require "cjson.safe"
|
|
local upload = require "resty.upload"
|
|
local decoder = require "kong.api.arguments_decoder"
|
|
|
|
|
|
local setmetatable = setmetatable
|
|
local getmetatable = getmetatable
|
|
local rawget = rawget
|
|
local concat = table.concat
|
|
local insert = table.insert
|
|
local pairs = pairs
|
|
local lower = string.lower
|
|
local find = string.find
|
|
local sub = string.sub
|
|
local type = type
|
|
local ngx = ngx
|
|
local req = ngx.req
|
|
local log = ngx.log
|
|
local req_read_body = req.read_body
|
|
local get_uri_args = req.get_uri_args
|
|
local get_body_data = req.get_body_data
|
|
local get_post_args = req.get_post_args
|
|
local json_decode = cjson.decode
|
|
local kong = kong
|
|
|
|
|
|
local NOTICE = ngx.NOTICE
|
|
|
|
|
|
local arguments_mt = {}
|
|
|
|
|
|
function arguments_mt:__index(name)
|
|
return rawget(self, "post")[name] or
|
|
rawget(self, "uri")[name]
|
|
end
|
|
|
|
|
|
local defaults = {
|
|
decode = true,
|
|
multipart = true,
|
|
timeout = 1000,
|
|
chunk_size = 4096,
|
|
max_uri_args = 1000,
|
|
max_post_args = 1000,
|
|
max_line_size = nil,
|
|
max_part_size = nil,
|
|
}
|
|
|
|
|
|
defaults.__index = defaults
|
|
|
|
|
|
local function basename(path, separator)
|
|
if not path then
|
|
return nil
|
|
end
|
|
|
|
local sep = separator or "/"
|
|
|
|
local location = 1
|
|
|
|
local boundary = find(path, sep, 1, true)
|
|
while boundary do
|
|
location = boundary + 1
|
|
boundary = find(path, sep, location, true)
|
|
end
|
|
|
|
if location > 1 then
|
|
path = sub(path, location)
|
|
end
|
|
|
|
if not separator then
|
|
return basename(path, "\\")
|
|
end
|
|
|
|
return path
|
|
end
|
|
|
|
|
|
local function find_content_type_boundary(content_type)
|
|
if not content_type then
|
|
return nil
|
|
end
|
|
|
|
local boundary, e = find(content_type, "boundary=", 21, true)
|
|
if boundary then
|
|
local s = find(content_type, ";", e + 1, true)
|
|
|
|
if s then
|
|
boundary = sub(content_type, e + 1, s - 1)
|
|
|
|
else
|
|
boundary = sub(content_type, e + 1)
|
|
end
|
|
|
|
if (sub(boundary, 1, 1) == '"' and sub(boundary, -1) == '"') or
|
|
(sub(boundary, 1, 1) == "'" and sub(boundary, -1) == "'") then
|
|
boundary = sub(boundary, 2, -2)
|
|
end
|
|
end
|
|
|
|
if boundary ~= "" then
|
|
return boundary
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
|
|
local function combine_arg(to, arg)
|
|
if type(arg) ~= "table" or getmetatable(arg) == decoder.multipart_mt then
|
|
insert(to, #to + 1, arg)
|
|
|
|
else
|
|
for k, v in pairs(arg) do
|
|
local t = to[k]
|
|
|
|
if not t then
|
|
to[k] = v
|
|
|
|
else
|
|
if type(t) == "table" and getmetatable(t) ~= decoder.multipart_mt then
|
|
combine_arg(t, v)
|
|
|
|
else
|
|
to[k] = { t }
|
|
combine_arg(to[k], v)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
local function combine(args)
|
|
local to = {}
|
|
|
|
if type(args) ~= "table" then
|
|
return to
|
|
end
|
|
|
|
for _, arg in ipairs(args) do
|
|
combine_arg(to, arg)
|
|
end
|
|
|
|
return to
|
|
end
|
|
|
|
|
|
local infer
|
|
|
|
|
|
local function infer_value(value, field)
|
|
if not value or type(field) ~= "table" then
|
|
return value
|
|
end
|
|
|
|
if value == "" or value == ngx.null then
|
|
return ngx.null
|
|
end
|
|
|
|
if field.type == "number" or field.type == "integer" then
|
|
return tonumber(value) or value
|
|
|
|
elseif field.type == "boolean" then
|
|
if value == "true" then
|
|
return true
|
|
|
|
elseif value == "false" then
|
|
return false
|
|
end
|
|
|
|
elseif field.type == "array" or field.type == "set" then
|
|
if type(value) ~= "table" then
|
|
value = { value }
|
|
end
|
|
|
|
for i, item in ipairs(value) do
|
|
value[i] = infer_value(item, field.elements)
|
|
end
|
|
|
|
elseif field.type == "foreign" then
|
|
if type(value) == "table" then
|
|
return infer(value, field.schema)
|
|
end
|
|
|
|
elseif field.type == "map" then
|
|
if type(value) == "table" then
|
|
for k, v in pairs(value) do
|
|
value[k] = infer_value(v, field.values)
|
|
end
|
|
end
|
|
|
|
elseif field.type == "record" and not field.abstract then
|
|
if type(value) == "table" then
|
|
for k, v in pairs(value) do
|
|
for i in ipairs(field.fields) do
|
|
local item = field.fields[i]
|
|
if item then
|
|
local key = next(item)
|
|
local fld = item[key]
|
|
if k == key then
|
|
value[k] = infer_value(v, fld)
|
|
end
|
|
end
|
|
end
|
|
|
|
if field.shorthand_fields then
|
|
for _, item in ipairs(field.shorthand_fields) do
|
|
local key = next(item)
|
|
local fld = item[key]
|
|
if k == key then
|
|
value[k] = infer_value(v, fld)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return value
|
|
end
|
|
|
|
|
|
infer = function(args, schema)
|
|
if not args then
|
|
return
|
|
end
|
|
|
|
if not schema then
|
|
return args
|
|
end
|
|
|
|
for field_name, field in schema:each_field(args) do
|
|
local value = args[field_name]
|
|
if value then
|
|
args[field_name] = infer_value(value, field)
|
|
end
|
|
end
|
|
|
|
if schema.shorthand_fields then
|
|
for _, v in ipairs(schema.shorthand_fields) do
|
|
local field_name = next(v)
|
|
local field = v[field_name]
|
|
|
|
local value = args[field_name]
|
|
if value then
|
|
args[field_name] = infer_value(value, field)
|
|
end
|
|
end
|
|
end
|
|
|
|
if schema.ttl == true and args.ttl then
|
|
args.ttl = tonumber(args.ttl) or args.ttl
|
|
end
|
|
|
|
return args
|
|
end
|
|
|
|
|
|
local function parse_multipart_header(header, results)
|
|
local name
|
|
local value
|
|
|
|
local boundary = find(header, "=", 1, true)
|
|
if boundary then
|
|
name = sub(header, 2, boundary - 1)
|
|
value = sub(header, boundary + 2, -2)
|
|
|
|
if (sub(value, 1, 1) == '"' and sub(value, -1) == '"') or
|
|
(sub(value, 1, 1) == "'" and sub(value, -1) == "'") then
|
|
value = sub(value, 2, -2)
|
|
end
|
|
|
|
if sub(name, -1) == "*" and lower(sub(value, 1, 7)) == "utf-8''" then
|
|
name = sub(name, 1, -2)
|
|
value = sub(value, 8)
|
|
|
|
results[name] = value
|
|
|
|
else
|
|
if not results[name] then
|
|
results[name] = value
|
|
end
|
|
end
|
|
|
|
else
|
|
results[#results + 1] = header
|
|
end
|
|
end
|
|
|
|
|
|
local function parse_multipart_headers(headers)
|
|
if not headers then
|
|
return nil
|
|
end
|
|
|
|
local results = {}
|
|
local location = 1
|
|
|
|
local boundary = find(headers, ";", 1, true)
|
|
while boundary do
|
|
local header = sub(headers, location, boundary - 1)
|
|
parse_multipart_header(header, results)
|
|
location = boundary + 1
|
|
boundary = find(headers, ";", location, true)
|
|
end
|
|
|
|
local header = sub(headers, location)
|
|
if header ~= "" then
|
|
parse_multipart_header(header, results)
|
|
end
|
|
|
|
return results
|
|
end
|
|
|
|
|
|
local function parse_multipart_stream(options, boundary)
|
|
local part_args = {}
|
|
|
|
local max_part_size = options.max_part_size
|
|
local max_post_args = options.max_post_args
|
|
local chunk_size = options.chunk_size
|
|
|
|
local multipart, err = upload:new(chunk_size, options.max_line_size)
|
|
if not multipart then
|
|
return nil, err
|
|
end
|
|
|
|
multipart:set_timeout(options.timeout)
|
|
|
|
local parts_count = 0
|
|
|
|
local headers, headers_count, part
|
|
|
|
while true do
|
|
local chunk_type, chunk
|
|
|
|
chunk_type, chunk, err = multipart:read()
|
|
if not chunk_type then
|
|
return nil, err
|
|
end
|
|
|
|
if chunk_type == "header" then
|
|
if not headers then
|
|
headers = {}
|
|
headers_count = 0
|
|
end
|
|
|
|
if type(chunk) == "table" then
|
|
headers_count = headers_count + 1
|
|
headers[headers_count] = chunk[3]
|
|
|
|
local key, value = chunk[1], parse_multipart_headers(chunk[2])
|
|
if value then
|
|
headers[key] = value
|
|
end
|
|
end
|
|
|
|
elseif chunk_type == "body" then
|
|
if headers then
|
|
local content_disposition = headers["Content-Disposition"] or {}
|
|
local content_type = headers["Content-Type"]
|
|
|
|
if type(content_type) == "table" then
|
|
content_type = content_type[1]
|
|
end
|
|
|
|
part = setmetatable({
|
|
boundary = boundary,
|
|
headers = headers,
|
|
name = content_disposition.name,
|
|
type = content_type,
|
|
file = basename(content_disposition.filename),
|
|
size = 0,
|
|
data = {
|
|
n = 0,
|
|
}
|
|
}, decoder.multipart_mt)
|
|
|
|
headers = nil
|
|
end
|
|
|
|
if part then
|
|
part.size = #chunk + part.size
|
|
|
|
if max_part_size then
|
|
if max_part_size < part.size then
|
|
return nil, "maximum size of multipart parameter exceeded"
|
|
end
|
|
end
|
|
|
|
if max_post_args and max_post_args < parts_count + 1 then
|
|
return nil, "maximum number of multipart parameters exceeded"
|
|
end
|
|
|
|
local n = part.data.n + 1
|
|
part.data[n] = chunk
|
|
part.data.n = n
|
|
end
|
|
|
|
elseif chunk_type == "part_end" then
|
|
if part then
|
|
if max_post_args then
|
|
parts_count = parts_count + 1
|
|
end
|
|
|
|
if part.data.n > 0 then
|
|
part.data = concat(part.data, nil, 1, part.data.n)
|
|
|
|
else
|
|
part.data = nil
|
|
end
|
|
|
|
if part.type and sub(part.type, 1, 16) == "application/json" then
|
|
local json
|
|
json, err = json_decode(part.data)
|
|
if json then
|
|
part.json = json
|
|
|
|
else
|
|
log(NOTICE, err)
|
|
end
|
|
end
|
|
|
|
local part_name = part.name
|
|
if part_name then
|
|
local enclosure = part_args[part_name]
|
|
if enclosure then
|
|
if type(enclosure) == "table" and getmetatable(enclosure) ~= decoder.multipart_mt then
|
|
enclosure[#enclosure + 1] = part
|
|
|
|
else
|
|
enclosure = { enclosure, part }
|
|
end
|
|
|
|
part_args[part_name] = enclosure
|
|
|
|
else
|
|
part_args[part_name] = part
|
|
end
|
|
|
|
else
|
|
part_args[#part_args + 1] = part
|
|
end
|
|
|
|
part = nil
|
|
end
|
|
|
|
elseif chunk_type == "eof" then
|
|
break
|
|
end
|
|
end
|
|
|
|
multipart:read()
|
|
|
|
return part_args
|
|
end
|
|
|
|
|
|
local function parse_multipart(options, content_type)
|
|
local boundary
|
|
|
|
if content_type then
|
|
boundary = find_content_type_boundary(content_type)
|
|
end
|
|
|
|
return parse_multipart_stream(options, boundary)
|
|
end
|
|
|
|
|
|
-- decodes and infers the arguments
|
|
-- the name "decode" is kept for backwards compatibility
|
|
local function decode(args, schema)
|
|
local decoded = decoder.decode(args)
|
|
return infer(combine(decoded), schema)
|
|
end
|
|
|
|
|
|
local function load(opts)
|
|
local options = setmetatable(opts or {}, defaults)
|
|
|
|
local args = setmetatable({
|
|
uri = {},
|
|
post = {},
|
|
}, arguments_mt)
|
|
|
|
local uargs, err = get_uri_args(options.max_uri_args)
|
|
if err == "truncated" then
|
|
return kong.response.exit(400, { message = "Too many arguments" })
|
|
end
|
|
|
|
if options.decode then
|
|
args.uri = decode(uargs, options.schema)
|
|
|
|
else
|
|
args.uri = uargs
|
|
end
|
|
|
|
local content_type = ngx.var.content_type
|
|
if not content_type then
|
|
return args
|
|
end
|
|
|
|
local content_type_lower = lower(content_type)
|
|
|
|
if find(content_type_lower, "application/x-www-form-urlencoded", 1, true) == 1 then
|
|
req_read_body()
|
|
local pargs, err = get_post_args(options.max_post_args)
|
|
if err == "truncated" then
|
|
return kong.response.exit(400, { message = "Too many arguments" })
|
|
end
|
|
if pargs then
|
|
if options.decode then
|
|
args.post = decode(pargs, options.schema)
|
|
|
|
else
|
|
args.post = pargs
|
|
end
|
|
|
|
elseif err then
|
|
log(NOTICE, err)
|
|
end
|
|
|
|
elseif find(content_type_lower, "application/json", 1, true) == 1 then
|
|
req_read_body()
|
|
|
|
-- we don't support file i/o in case the body is
|
|
-- buffered to a file, and that is how we want it.
|
|
local body_data = get_body_data()
|
|
if body_data then
|
|
local pargs, err = json_decode(body_data)
|
|
if pargs then
|
|
args.post = pargs
|
|
|
|
elseif err then
|
|
log(NOTICE, err)
|
|
end
|
|
end
|
|
|
|
elseif options.multipart and find(content_type_lower, "multipart/form-data", 1, true) == 1 then
|
|
if options.request and options.request.params_post then
|
|
local pargs = {}
|
|
for k, v in pairs(options.request.params_post) do
|
|
if type(v) == "table" and v.name and v.content then
|
|
pargs[k] = v.content
|
|
else
|
|
pargs[k] = v
|
|
end
|
|
end
|
|
|
|
args.post = decode(pargs, options.schema)
|
|
|
|
else
|
|
local pargs, err = parse_multipart(options, content_type)
|
|
if pargs then
|
|
if options.decode then
|
|
args.post = decode(pargs, options.schema)
|
|
|
|
else
|
|
args.post = pargs
|
|
end
|
|
|
|
elseif err then
|
|
log(NOTICE, err)
|
|
end
|
|
end
|
|
|
|
else
|
|
req_read_body()
|
|
|
|
-- we don't support file i/o in case the body is
|
|
-- buffered to a file, and that is how we want it.
|
|
local body_data = get_body_data()
|
|
|
|
if body_data then
|
|
args.body = body_data
|
|
end
|
|
end
|
|
|
|
return args
|
|
end
|
|
|
|
|
|
return {
|
|
load = load,
|
|
infer_value = infer_value,
|
|
decode = decode,
|
|
_infer = infer,
|
|
_combine = combine,
|
|
}
|