Files
2026-07-13 12:32:21 +08:00

270 lines
7.2 KiB
Lua

local lrucache = require "resty.lrucache"
local url = require "socket.url"
local kong_meta = require "kong.meta"
local kong = kong
local re_find = ngx.re.find
local find = string.find
local concat = table.concat
local tostring = tostring
local ipairs = ipairs
local HTTP_OK = 200
local CorsHandler = {}
CorsHandler.PRIORITY = 2000
CorsHandler.VERSION = kong_meta.version
-- per-plugin cache of normalized origins for runtime comparison
local mt_cache = { __mode = "k" }
local config_cache = setmetatable({}, mt_cache)
-- per-worker cache of parsed requests origins with 1000 slots
local normalized_req_domains = lrucache.new(10e3)
local function is_origin_provided(origin)
return origin and origin ~= ""
end
local function normalize_origin(domain)
local parsed_obj = assert(url.parse(domain))
if not parsed_obj.host then
return {
domain = domain,
host = domain,
}
end
local port = parsed_obj.port
if (parsed_obj.scheme == "http" and port == "80")
or (parsed_obj.scheme == "https" and port == "443") then
port = nil
end
return {
domain = (parsed_obj.scheme and parsed_obj.scheme .. "://" or "") ..
parsed_obj.host ..
(port and ":" .. port or ""),
host = parsed_obj.host,
}
end
local function add_vary_header(header_filter)
if header_filter then
kong.response.add_header("vary", "Origin")
else
kong.response.set_header("vary", "Origin")
end
end
local function configure_origin(conf, header_filter, req_origin)
local n_origins = conf.origins ~= nil and #conf.origins or 0
local set_header = kong.response.set_header
if n_origins == 0 or (n_origins == 1 and conf.origins[1] == "*") then
set_header("Access-Control-Allow-Origin", "*")
return true
end
-- always set Vary header (it can be used for calculating cache key)
-- https://github.com/rs/cors/issues/10
add_vary_header(header_filter)
if n_origins == 1 then
-- if this doesnt look like a regex, set the ACAO header directly
-- otherwise, we'll fall through to an iterative search and
-- set the ACAO header based on the client Origin
local from, _, err = re_find(conf.origins[1], "^[A-Za-z0-9.:/-]+$", "jo")
if err then
kong.log.err("could not inspect origin for type: ", err)
end
if from then
set_header("Access-Control-Allow-Origin", conf.origins[1])
return false
end
end
if is_origin_provided(req_origin) then
local cached_domains = config_cache[conf]
if not cached_domains then
cached_domains = {}
for _, entry in ipairs(conf.origins) do
if entry == "*" then
set_header("Access-Control-Allow-Origin", "*")
return true
end
local domain
local maybe_regex, _, err = re_find(entry, "[^A-Za-z0-9.:/-]", "jo")
if err then
kong.log.err("could not inspect origin for type: ", err)
end
if maybe_regex then
-- Kong 0.x did not anchor regexes:
-- Perform adjustments to support regexes
-- explicitly anchored by the user.
if entry:sub(-1) ~= "$" then
entry = entry .. "$"
end
if entry:sub(1, 1) == "^" then
entry = entry:sub(2)
end
domain = { regex = entry }
else
domain = normalize_origin(entry)
end
domain.by_host = not find(entry, ":", 1, true)
table.insert(cached_domains, domain)
end
config_cache[conf] = cached_domains
end
local normalized_req_origin = normalized_req_domains:get(req_origin)
if not normalized_req_origin then
normalized_req_origin = normalize_origin(req_origin)
normalized_req_domains:set(req_origin, normalized_req_origin)
end
for _, cached_domain in ipairs(cached_domains) do
local found, _, err
if cached_domain.regex then
local subject = cached_domain.by_host
and normalized_req_origin.host
or normalized_req_origin.domain
found, _, err = re_find(subject, cached_domain.regex, "ajo")
if err then
kong.log.err("could not search for domain: ", err)
end
else
found = (normalized_req_origin.domain == cached_domain.domain)
end
if found then
set_header("Access-Control-Allow-Origin", normalized_req_origin.domain)
return false
end
end
end
kong.response.clear_header("Access-Control-Allow-Origin")
return false
end
local function configure_credentials(conf, allow_all, header_filter, req_origin)
local set_header = kong.response.set_header
if not conf.credentials then
return
end
if not allow_all then
set_header("Access-Control-Allow-Credentials", true)
return
end
-- Access-Control-Allow-Origin is '*', must change it because ACAC cannot
-- be 'true' if ACAO is '*'.
if is_origin_provided(req_origin) then
add_vary_header(header_filter)
set_header("Access-Control-Allow-Origin", req_origin)
set_header("Access-Control-Allow-Credentials", true)
end
end
function CorsHandler:access(conf)
local req_origin = kong.request.get_header("Origin")
if kong.request.get_method() ~= "OPTIONS"
or not is_origin_provided(req_origin)
or not kong.request.get_header("Access-Control-Request-Method")
then
return
end
-- don't add any response header because we are delegating the preflight to
-- the upstream API (conf.preflight_continue=true), or because we already
-- added them all
kong.ctx.plugin.skip_response_headers = true
if conf.preflight_continue then
return
end
local allow_all = configure_origin(conf, false, req_origin)
configure_credentials(conf, allow_all, false, req_origin)
local set_header = kong.response.set_header
if conf.headers and #conf.headers > 0 then
set_header("Access-Control-Allow-Headers", concat(conf.headers, ","))
else
local acrh = kong.request.get_header("Access-Control-Request-Headers")
if acrh then
set_header("Access-Control-Allow-Headers", acrh)
else
kong.response.clear_header("Access-Control-Allow-Headers")
end
end
local methods = conf.methods and concat(conf.methods, ",")
or "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS,TRACE,CONNECT"
set_header("Access-Control-Allow-Methods", methods)
if conf.max_age then
set_header("Access-Control-Max-Age", tostring(conf.max_age))
end
if conf.private_network and
kong.request.get_header("Access-Control-Request-Private-Network") == 'true' then
set_header("Access-Control-Allow-Private-Network", 'true')
end
return kong.response.exit(HTTP_OK)
end
function CorsHandler:header_filter(conf)
if kong.ctx.plugin.skip_response_headers then
return
end
local req_origin = kong.request.get_header("origin")
if not is_origin_provided(req_origin) and not conf.allow_origin_absent then
return
end
local allow_all = configure_origin(conf, true, req_origin)
configure_credentials(conf, allow_all, true, req_origin)
if conf.exposed_headers and #conf.exposed_headers > 0 then
kong.response.set_header("Access-Control-Expose-Headers",
concat(conf.exposed_headers, ","))
end
end
return CorsHandler