270 lines
7.2 KiB
Lua
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
|