Files
kong--kong/kong/api/arguments.lua
T
2026-07-13 12:32:21 +08:00

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,
}