local pl_path = require "pl.path" local pl_file = require "pl.file" local type = type local pairs = pairs local ipairs = ipairs local tostring = tostring local tonumber = tonumber local setmetatable = setmetatable local sort = table.sort local concat = table.concat local fmt = string.format local re_match = ngx.re.match local join = require("kong.tools.string").join local isplitn = require("kong.tools.string").isplitn local splitn = require("kong.tools.string").splitn local split_once = require("kong.tools.string").split_once local strip = require("kong.tools.string").strip local parse_http_time = ngx.parse_http_time local time = ngx.time local ngx_re_gmatch = ngx.re.gmatch local ngx_re_match = ngx.re.match local lower = string.lower local max = math.max local tab_new = require("table.new") local EMPTY = require("kong.tools.table").EMPTY local _M = {} _M.CONTENT_TYPES = { CONTENT_TYPE = "Content-Type", CONTENT_TYPE_POST = "application/x-www-form-urlencoded", CONTENT_TYPE_JSON = "application/json", CONTENT_TYPE_FORM_DATA = "multipart/form-data", } do local url = require "socket.url" --- URL escape and format key and value -- values should be already decoded or the `raw` option should be passed to prevent double-encoding local function encode_args_value(key, value, raw) if not raw then key = url.escape(key) end if value ~= nil then if not raw then value = url.escape(value) end return fmt("%s=%s", key, value) else return key end end local function compare_keys(a, b) local ta = type(a) if ta == type(b) then return a < b end return ta == "number" -- numbers go first, then the rest of keys (usually strings) end -- Recursively URL escape and format key and value -- Handles nested arrays and tables local function recursive_encode_args(parent_key, value, raw, no_array_indexes, query) local sub_keys = {} for sk in pairs(value) do sub_keys[#sub_keys + 1] = sk end sort(sub_keys, compare_keys) local sub_value, next_sub_key for _, sub_key in ipairs(sub_keys) do sub_value = value[sub_key] if type(sub_key) == "number" then if no_array_indexes then next_sub_key = parent_key .. "[]" else next_sub_key = ("%s[%s]"):format(parent_key, tostring(sub_key)) end else next_sub_key = ("%s.%s"):format(parent_key, tostring(sub_key)) end if type(sub_value) == "table" then recursive_encode_args(next_sub_key, sub_value, raw, no_array_indexes, query) else query[#query+1] = encode_args_value(next_sub_key, sub_value, raw) end end end local ngx_null = ngx.null --- Encode a Lua table to a querystring -- Tries to mimic ngx_lua's `ngx.encode_args`, but has differences: -- * It percent-encodes querystring values. -- * It also supports encoding for bodies (only because it is used in http_client for specs. -- * It encodes arrays like Lapis instead of like ngx.encode_args to allow interacting with Lapis -- * It encodes ngx.null as empty strings -- * It encodes true and false as "true" and "false" -- * It is capable of encoding nested data structures: -- * An array access is encoded as `arr[1]` -- * A struct access is encoded as `struct.field` -- * Nested structures can use both: `arr[1].field[3]` -- @see https://github.com/Mashape/kong/issues/749 -- @param[type=table] args A key/value table containing the query args to encode. -- @param[type=boolean] raw If true, will not percent-encode any key/value and will ignore special boolean rules. -- @param[type=boolean] no_array_indexes If true, arrays/map elements will be -- encoded without an index: 'my_array[]='. By default, -- array elements will have an index: 'my_array[0]='. -- @treturn string A valid querystring (without the prefixing '?') function _M.encode_args(args, raw, no_array_indexes) local query = {} local keys = {} for k in pairs(args) do keys[#keys+1] = k end sort(keys, compare_keys) for _, key in ipairs(keys) do local value = args[key] if type(value) == "table" then recursive_encode_args(key, value, raw, no_array_indexes, query) elseif value == ngx_null then query[#query+1] = encode_args_value(key, "") elseif value ~= nil or raw then value = tostring(value) if value ~= "" then query[#query+1] = encode_args_value(key, value, raw) elseif raw or value == "" then query[#query+1] = key end end end return concat(query, "&") end -- { ["1"] = "a", ["2"] = "b" } becomes {"a", "b"} -- { "a", "b" } becomes { "a", "b" } local function decode_array(t) local keys = {} local len = 0 for k in pairs(t) do len = len + 1 local number = tonumber(k) if not number then return nil end keys[len] = number end sort(keys) local new_t = {} for i=1,len do if keys[i] ~= i then return nil end new_t[i] = t[tostring(i)] or t[i] end return new_t end -- Parses params in post requests -- Transforms "string-like numbers" inside "array-like" tables into numbers -- (needs a complete array with no holes starting on "1") -- { x = {["1"] = "a", ["2"] = "b" } } becomes { x = {"a", "b"} } -- Transforms empty strings into ngx.null: -- { x = "" } becomes { x = ngx.null } -- Transforms the strings "true" and "false" into booleans -- { x = "true" } becomes { x = true } function _M.decode_args(args) local new_args = {} for k, v in pairs(args) do if type(v) == "table" then v = decode_array(v) or v elseif v == "" then v = ngx_null elseif v == "true" then v = true elseif v == "false" then v = false end new_args[k] = v end return new_args end end --- Checks whether a request is https or was originally https (but already -- terminated). It will check in the current request (global `ngx` table). If -- the header `X-Forwarded-Proto` exists -- with value `https` then it will also -- be considered as an https connection. -- @param trusted_ip boolean indicating if the client is a trusted IP -- @param allow_terminated if truthy, the `X-Forwarded-Proto` header will be checked as well. -- @return boolean or nil+error in case the header exists multiple times _M.check_https = function(trusted_ip, allow_terminated) if ngx.var.scheme:lower() == "https" then return true end if not allow_terminated then return false end -- if we trust this IP, examine it's X-Forwarded-Proto header -- otherwise, we fall back to relying on the client scheme -- (which was either validated earlier, or we fall through this block) if trusted_ip then local scheme = ngx.var.http_x_forwarded_proto if not scheme then return false end -- we could use the first entry (lower security), or check the contents of -- each of them (slow). So for now defensive, and error -- out on multiple entries for the x-forwarded-proto header. if scheme:find(",", 1, true) then return nil, "Only one X-Forwarded-Proto header allowed" end return scheme:lower() == "https" end return false end local CONTROLS = [[\x00-\x1F\x7F]] local HIGHBIT = [[\x80-\xFF]] local SEPARATORS = [==[ \t()<>@,;:\\\"\/?={}\[\]]==] local HTTP_TOKEN_FORBID_PATTERN = "[".. CONTROLS .. HIGHBIT .. SEPARATORS .. "]" --- Validates a token defined by RFC 2616. -- @param token (string) the string to verify -- @return the valid token, or `nil+error` function _M.validate_http_token(token) if token == nil or token == "" then return nil, "no token provided" end if not re_match(token, HTTP_TOKEN_FORBID_PATTERN, "jo") then return token end return nil, "contains one or more invalid characters. ASCII " .. "control characters (0-31;127), space, tab and the " .. "characters ()<>@,;:\\\"/?={}[] are not allowed." end -- should we also use validate_http_token for this? --- Validates a header name. -- Checks characters used in a header name to be valid, as per nginx only -- a-z, A-Z, 0-9 and '-' are allowed. -- @param name (string) the header name to verify -- @return the valid header name, or `nil+error` function _M.validate_header_name(name) if name == nil or name == "" then return nil, "no header name provided" end if re_match(name, "^[a-zA-Z0-9-_]+$", "jo") then return name end return nil, "bad header name '" .. name .. "', allowed characters are A-Z, a-z, 0-9, '_', and '-'" end --- Validates a cookie name. -- @param name (string) the cookie name to verify -- @return the valid cookie name, or `nil+error` _M.validate_cookie_name = _M.validate_http_token --- -- Given an http status and an optional message, this function will -- return a body that could be used in `kong.response.exit`. -- -- * Status 204 will always return nil for the body -- * 405, 500 and 502 always return a predefined message -- * If there is a message, it will be used as a body -- * Otherwise, there's a default body for 401, 404 & 503 responses -- -- If after applying those rules there's a body, and that body isn't a -- table, it will be transformed into one of the form `{ message = ... }`, -- where `...` is the untransformed body. -- -- This function throws an error on invalid inputs. -- -- @tparam number status The status to be used -- @tparam[opt] table|string message The message to be used -- @tparam[opt] table headers The headers to be used -- @return table|nil a possible body which can be used in kong.response.exit -- @usage -- -- --- 204 always returns nil -- get_default_exit_body(204) --> nil -- get_default_exit_body(204, "foo") --> nil -- -- --- 405, 500 & 502 always return predefined values -- -- get_default_exit_body(502, "ignored") --> { message = "Bad gateway" } -- -- --- If message is a table, it is returned -- -- get_default_exit_body(200, { ok = true }) --> { ok = true } -- -- --- If message is not a table, it is transformed into one -- -- get_default_exit_body(200, "ok") --> { message = "ok" } -- -- --- 401, 404 and 503 provide default values if none is defined -- -- get_default_exit_body(404) --> { message = "Not found" } -- do local _overrides = { [405] = "Method not allowed", [500] = "An unexpected error occurred", [502] = "Bad gateway", } local _defaults = { [401] = "Unauthorized", [404] = "Not found", [503] = "Service unavailable", } local MIN_STATUS_CODE = 100 local MAX_STATUS_CODE = 599 function _M.get_default_exit_body(status, message) if type(status) ~= "number" then error("code must be a number", 2) elseif status < MIN_STATUS_CODE or status > MAX_STATUS_CODE then error(fmt("code must be a number between %u and %u", MIN_STATUS_CODE, MAX_STATUS_CODE), 2) end if status == 204 then return nil end local body = _overrides[status] or message or _defaults[status] if body ~= nil and type(body) ~= "table" then body = { message = body } end return body end end do local CONTENT_TYPE_JSON = "application/json" local CONTENT_TYPE_GRPC = "application/grpc" local CONTENT_TYPE_HTML = "text/html" local CONTENT_TYPE_XML = "application/xml" local CONTENT_TYPE_PLAIN = "text/plain" local CONTENT_TYPE_APP = "application" local CONTENT_TYPE_TEXT = "text" local CONTENT_TYPE_DEFAULT = "default" local CONTENT_TYPE_ANY = "*" local MIME_TYPES = { [CONTENT_TYPE_GRPC] = "", [CONTENT_TYPE_HTML] = "text/html; charset=utf-8", [CONTENT_TYPE_JSON] = "application/json; charset=utf-8", [CONTENT_TYPE_PLAIN] = "text/plain; charset=utf-8", [CONTENT_TYPE_XML] = "application/xml; charset=utf-8", [CONTENT_TYPE_APP] = "application/json; charset=utf-8", [CONTENT_TYPE_TEXT] = "text/plain; charset=utf-8", [CONTENT_TYPE_DEFAULT] = "application/json; charset=utf-8", } local ERROR_TEMPLATES = { [CONTENT_TYPE_GRPC] = "", [CONTENT_TYPE_HTML] = [[
%s.
request_id: %s
]], [CONTENT_TYPE_JSON] = [[ { "message":"%s", "request_id":"%s" }]], [CONTENT_TYPE_PLAIN] = "%s\nrequest_id: %s\n", [CONTENT_TYPE_XML] = [[