1677 lines
51 KiB
Lua
1677 lines
51 KiB
Lua
-- Kong runloop
|
|
|
|
local meta = require "kong.meta"
|
|
local Router = require "kong.router"
|
|
local balancer = require "kong.runloop.balancer"
|
|
local events = require "kong.runloop.events"
|
|
local wasm = require "kong.runloop.wasm"
|
|
local upstream_ssl = require "kong.runloop.upstream_ssl"
|
|
local reports = require "kong.reports"
|
|
local constants = require "kong.constants"
|
|
local concurrency = require "kong.concurrency"
|
|
local lrucache = require "resty.lrucache"
|
|
local ktls = require "resty.kong.tls"
|
|
local request_id = require "kong.observability.tracing.request_id"
|
|
|
|
|
|
local PluginsIterator = require "kong.runloop.plugins_iterator"
|
|
local log_level = require "kong.runloop.log_level"
|
|
local instrumentation = require "kong.observability.tracing.instrumentation"
|
|
local req_dyn_hook = require "kong.dynamic_hook"
|
|
|
|
|
|
local kong = kong
|
|
local type = type
|
|
local ipairs = ipairs
|
|
local tostring = tostring
|
|
local tonumber = tonumber
|
|
local setmetatable = setmetatable
|
|
local max = math.max
|
|
local min = math.min
|
|
local ceil = math.ceil
|
|
local sub = string.sub
|
|
local byte = string.byte
|
|
local gsub = string.gsub
|
|
local find = string.find
|
|
local lower = string.lower
|
|
local fmt = string.format
|
|
|
|
local ngx = ngx
|
|
local var = ngx.var
|
|
local log = ngx.log
|
|
local exit = ngx.exit
|
|
local exec = ngx.exec
|
|
local header = ngx.header
|
|
local timer_at = ngx.timer.at
|
|
local get_phase = ngx.get_phase
|
|
local subsystem = ngx.config.subsystem
|
|
local clear_header = ngx.req.clear_header
|
|
local http_version = ngx.req.http_version
|
|
local request_id_get = request_id.get
|
|
local escape = require("kong.tools.uri").escape
|
|
local encode = require("string.buffer").encode
|
|
local uuid = require("kong.tools.uuid").uuid
|
|
local EMPTY = require("kong.tools.table").EMPTY
|
|
|
|
local req_dyn_hook_run_hook = req_dyn_hook.run_hook
|
|
|
|
local is_http_module = subsystem == "http"
|
|
local is_stream_module = subsystem == "stream"
|
|
|
|
local DEFAULT_MATCH_LRUCACHE_SIZE = Router.DEFAULT_MATCH_LRUCACHE_SIZE
|
|
|
|
|
|
local kong_shm = ngx.shared.kong
|
|
local PLUGINS_REBUILD_COUNTER_KEY =
|
|
constants.PLUGINS_REBUILD_COUNTER_KEY
|
|
local ROUTERS_REBUILD_COUNTER_KEY =
|
|
constants.ROUTERS_REBUILD_COUNTER_KEY
|
|
|
|
|
|
local ROUTER_CACHE_SIZE = DEFAULT_MATCH_LRUCACHE_SIZE
|
|
local ROUTER_CACHE = lrucache.new(ROUTER_CACHE_SIZE)
|
|
local ROUTER_CACHE_NEG = lrucache.new(ROUTER_CACHE_SIZE)
|
|
|
|
|
|
local DEFAULT_PROXY_HTTP_VERSION = "1.1"
|
|
|
|
|
|
local NOOP = function() end
|
|
|
|
|
|
local ERR = ngx.ERR
|
|
local NOTICE = ngx.NOTICE
|
|
local WARN = ngx.WARN
|
|
local INFO = ngx.INFO
|
|
local DEBUG = ngx.DEBUG
|
|
local COMMA = byte(",")
|
|
local SPACE = byte(" ")
|
|
local QUESTION_MARK = byte("?")
|
|
local ARRAY_MT = require("cjson.safe").array_mt
|
|
|
|
local HOST_PORTS = {}
|
|
|
|
|
|
local SUBSYSTEMS = constants.PROTOCOLS_WITH_SUBSYSTEM
|
|
local TTL_ZERO = { ttl = 0 }
|
|
|
|
|
|
local ROUTER
|
|
local ROUTER_VERSION
|
|
local ROUTER_SYNC_OPTS
|
|
|
|
local PLUGINS_ITERATOR
|
|
local PLUGINS_ITERATOR_SYNC_OPTS
|
|
|
|
local WASM_STATE_VERSION
|
|
local WASM_STATE_SYNC_OPTS
|
|
|
|
local RECONFIGURE_OPTS
|
|
local GLOBAL_QUERY_OPTS = { workspace = ngx.null, show_ws_id = true }
|
|
|
|
local SERVER_HEADER = meta._SERVER_TOKENS
|
|
|
|
|
|
local STREAM_TLS_TERMINATE_SOCK
|
|
local STREAM_TLS_PASSTHROUGH_SOCK
|
|
|
|
|
|
local get_header
|
|
local set_authority
|
|
local set_service_ssl = upstream_ssl.set_service_ssl
|
|
|
|
if is_http_module then
|
|
get_header = require("kong.tools.http").get_header
|
|
set_authority = require("resty.kong.grpc").set_authority
|
|
end
|
|
|
|
|
|
local disable_proxy_ssl
|
|
if is_stream_module then
|
|
disable_proxy_ssl = ktls.disable_proxy_ssl
|
|
end
|
|
|
|
|
|
local update_lua_mem
|
|
do
|
|
local pid = ngx.worker.pid
|
|
local ngx_time = ngx.time
|
|
local kong_shm = ngx.shared.kong
|
|
|
|
local LUA_MEM_SAMPLE_RATE = 10 -- seconds
|
|
local last = ngx_time()
|
|
|
|
local collectgarbage = collectgarbage
|
|
|
|
update_lua_mem = function(force)
|
|
local time = ngx_time()
|
|
|
|
if force or time - last >= LUA_MEM_SAMPLE_RATE then
|
|
local count = collectgarbage("count")
|
|
|
|
local ok, err = kong_shm:safe_set("kong:mem:" .. pid(), count)
|
|
if not ok then
|
|
log(ERR, "could not record Lua VM allocated memory: ", err)
|
|
end
|
|
|
|
last = time
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
local function csv_iterator(s, b)
|
|
if b == -1 then
|
|
return
|
|
end
|
|
|
|
local e = find(s, ",", b, true)
|
|
local v
|
|
local l
|
|
if e then
|
|
if e == b then
|
|
return csv_iterator(s, b + 1) -- empty string
|
|
end
|
|
v = sub(s, b, e - 1)
|
|
l = e - b
|
|
b = e + 1
|
|
|
|
else
|
|
if b > 1 then
|
|
v = sub(s, b)
|
|
else
|
|
v = s
|
|
end
|
|
|
|
l = #v
|
|
b = -1 -- end iteration
|
|
end
|
|
|
|
if l == 1 and (byte(v) == SPACE or byte(v) == COMMA) then
|
|
return csv_iterator(s, b)
|
|
end
|
|
|
|
if byte(v, 1, 1) == SPACE then
|
|
v = gsub(v, "^%s+", "")
|
|
end
|
|
|
|
if byte(v, -1) == SPACE then
|
|
v = gsub(v, "%s+$", "")
|
|
end
|
|
|
|
if v == "" then
|
|
return csv_iterator(s, b)
|
|
end
|
|
|
|
return b, v
|
|
end
|
|
|
|
|
|
local function csv(s)
|
|
if type(s) ~= "string" or s == "" then
|
|
return csv_iterator, s, -1
|
|
end
|
|
|
|
s = lower(s)
|
|
if s == "close" or s == "upgrade" or s == "keep-alive" then
|
|
return csv_iterator, s, -1
|
|
end
|
|
|
|
return csv_iterator, s, 1
|
|
end
|
|
|
|
|
|
-- @param name "router" or "plugins_iterator"
|
|
-- @param callback A function that will update the router or plugins_iterator
|
|
-- @param version target version
|
|
-- @param opts concurrency options, including lock name and timeout.
|
|
-- @returns true if callback was either successfully executed synchronously,
|
|
-- enqueued via async timer, or not needed (because current_version == target).
|
|
-- nil otherwise (callback was neither called successfully nor enqueued,
|
|
-- or an error happened).
|
|
-- @returns error message as a second return value in case of failure/error
|
|
local function rebuild(name, callback, version, opts)
|
|
local current_version, err = kong.core_cache:get(name .. ":version", TTL_ZERO, uuid)
|
|
if err then
|
|
return nil, "failed to retrieve " .. name .. " version: " .. err
|
|
end
|
|
|
|
if current_version == version then
|
|
return true
|
|
end
|
|
|
|
return concurrency.with_coroutine_mutex(opts, callback)
|
|
end
|
|
|
|
|
|
-- Given a protocol, return the subsystem that handles it
|
|
local function should_process_route(route)
|
|
for _, protocol in ipairs(route.protocols) do
|
|
if SUBSYSTEMS[protocol] == subsystem then
|
|
return true
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
|
|
local function load_service_from_db(service_pk, ws_id)
|
|
local options = ws_id and { workspace = ws_id, show_ws_id = true }
|
|
local service, err = kong.db.services:select(service_pk, options)
|
|
if service == nil then
|
|
-- the third value means "do not cache"
|
|
return nil, err, -1
|
|
end
|
|
return service
|
|
end
|
|
|
|
|
|
local function build_services_init_cache(db)
|
|
local services_init_cache = {}
|
|
local services = db.services
|
|
local page_size
|
|
if services.pagination then
|
|
page_size = services.pagination.max_page_size
|
|
end
|
|
|
|
for service, err in services:each(page_size, GLOBAL_QUERY_OPTS) do
|
|
if err then
|
|
return nil, err
|
|
end
|
|
|
|
services_init_cache[service.id] = service
|
|
end
|
|
|
|
return services_init_cache
|
|
end
|
|
|
|
|
|
local function get_service_for_route(db, route, services_init_cache)
|
|
local service_pk = route.service
|
|
if not service_pk then
|
|
return nil
|
|
end
|
|
|
|
local id = service_pk.id
|
|
local service = services_init_cache[id]
|
|
if service then
|
|
return service
|
|
end
|
|
|
|
local err
|
|
local ws_id = route.ws_id
|
|
|
|
-- kong.core_cache is available, not in init phase
|
|
if kong.core_cache and db.strategy ~= "off" then
|
|
local cache_key = db.services:cache_key(service_pk.id, nil, nil, nil, nil,
|
|
ws_id)
|
|
service, err = kong.core_cache:get(cache_key, TTL_ZERO,
|
|
load_service_from_db, service_pk, ws_id)
|
|
|
|
else -- dbless or init phase, kong.core_cache not needed/available
|
|
|
|
-- A new service/route has been inserted while the initial route
|
|
-- was being created, on init (perhaps by a different Kong node).
|
|
-- Load the service individually and update services_init_cache with it
|
|
service, err = load_service_from_db(service_pk, ws_id)
|
|
services_init_cache[id] = service
|
|
end
|
|
|
|
if err then
|
|
return nil, "error raised while finding service for route (" .. route.id .. "): " ..
|
|
err
|
|
|
|
elseif not service then
|
|
return nil, "could not find service for route (" .. route.id .. ")"
|
|
end
|
|
|
|
|
|
-- TODO: this should not be needed as the schema should check it already
|
|
if SUBSYSTEMS[service.protocol] ~= subsystem then
|
|
log(WARN, "service with protocol '", service.protocol,
|
|
"' cannot be used with '", subsystem, "' subsystem")
|
|
|
|
return nil
|
|
end
|
|
|
|
return service
|
|
end
|
|
|
|
|
|
local function get_router_version()
|
|
return kong.core_cache:get("router:version", TTL_ZERO, uuid)
|
|
end
|
|
|
|
|
|
local function new_router(version)
|
|
local db = kong.db
|
|
local routes, i = {}, 0
|
|
|
|
local err
|
|
-- The router is initially created on init phase, where kong.core_cache is
|
|
-- still not ready. For those cases, use a plain Lua table as a cache
|
|
-- instead
|
|
local services_init_cache = {}
|
|
if not kong.core_cache and db.strategy ~= "off" then
|
|
services_init_cache, err = build_services_init_cache(db)
|
|
if err then
|
|
services_init_cache = {}
|
|
log(WARN, "could not build services init cache: ", err)
|
|
end
|
|
end
|
|
|
|
-- We need to detect router changes if there is some one modifying the routers,
|
|
-- like rebuild_router_timer. And it relies on core_cache to detect changes.
|
|
--
|
|
-- 1. stratey off (dbless)
|
|
-- rpc_sync on:
|
|
-- non init worker: true(kong.core_cache)
|
|
-- init worker: false
|
|
-- rpc_sync off: false
|
|
-- 2. strategy on (non dbless): true(kong.core_cache)
|
|
local detect_changes = kong.core_cache and
|
|
(db.strategy ~= "off" or (kong.sync and get_phase() ~= "init_worker"))
|
|
|
|
local counter = 0
|
|
local page_size = db.routes.pagination.max_page_size
|
|
for route, err in db.routes:each(page_size, GLOBAL_QUERY_OPTS) do
|
|
if err then
|
|
return nil, "could not load routes: " .. err
|
|
end
|
|
|
|
if detect_changes then
|
|
if counter > 0 and counter % page_size == 0 then
|
|
local new_version, err = get_router_version()
|
|
if err then
|
|
return nil, "failed to retrieve router version: " .. err
|
|
end
|
|
|
|
if new_version ~= version then
|
|
log(INFO, "could not build router: router was changed while rebuilding it")
|
|
return nil, nil
|
|
end
|
|
end
|
|
counter = counter + 1
|
|
end
|
|
|
|
if should_process_route(route) then
|
|
local service, err = get_service_for_route(db, route, services_init_cache)
|
|
if err then
|
|
return nil, err
|
|
end
|
|
|
|
-- routes with no services are added to router
|
|
-- but routes where the services.enabled == false are not put in router
|
|
if service == nil or service.enabled ~= false then
|
|
local r = {
|
|
route = route,
|
|
service = service,
|
|
}
|
|
|
|
i = i + 1
|
|
routes[i] = r
|
|
end
|
|
end
|
|
end
|
|
|
|
local n = DEFAULT_MATCH_LRUCACHE_SIZE
|
|
local cache_size = min(ceil(max(i / n, 1)) * n, n * 20)
|
|
|
|
if cache_size ~= ROUTER_CACHE_SIZE then
|
|
ROUTER_CACHE = lrucache.new(cache_size)
|
|
ROUTER_CACHE_SIZE = cache_size
|
|
end
|
|
|
|
local new_router, err = Router.new(routes, ROUTER_CACHE, ROUTER_CACHE_NEG, ROUTER)
|
|
if not new_router then
|
|
return nil, "could not create router: " .. err
|
|
end
|
|
|
|
local _, err = kong_shm:incr(ROUTERS_REBUILD_COUNTER_KEY, 1, 0)
|
|
if err then
|
|
log(ERR, "failed to increase router rebuild counter: ", err)
|
|
end
|
|
|
|
return new_router
|
|
end
|
|
|
|
|
|
local function build_router(version)
|
|
-- as new_router may be interrupted, and after init_worker we assume
|
|
-- the ROUTE is never nil, we create an empty router which cannot be
|
|
-- interrupted and will be replaced by the new_router
|
|
if version == "init" then
|
|
local err
|
|
ROUTER, err = Router.new(EMPTY, ROUTER_CACHE, ROUTER_CACHE_NEG, ROUTER)
|
|
if not ROUTER then
|
|
log(ERR, "could not create an empty router: ", err)
|
|
end
|
|
end
|
|
|
|
local router, err = new_router(version)
|
|
if not router then
|
|
return nil, err
|
|
end
|
|
|
|
ROUTER = router
|
|
|
|
if version then
|
|
ROUTER_VERSION = version
|
|
end
|
|
|
|
ROUTER_CACHE:flush_all()
|
|
ROUTER_CACHE_NEG:flush_all()
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
local function update_router()
|
|
-- we might not need to rebuild the router (if we were not
|
|
-- the first request in this process to enter this code path)
|
|
-- check again and rebuild only if necessary
|
|
local version, err = get_router_version()
|
|
if err then
|
|
return nil, "failed to retrieve router version: " .. err
|
|
end
|
|
|
|
if version == ROUTER_VERSION then
|
|
return true
|
|
end
|
|
|
|
local ok, err = build_router(version)
|
|
if not ok then
|
|
return nil, --[[ 'err' fully formatted ]] err
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
local function rebuild_router(opts)
|
|
return rebuild("router", update_router, ROUTER_VERSION, opts)
|
|
end
|
|
|
|
|
|
local function get_updated_router()
|
|
if kong.db.strategy ~= "off" and kong.configuration.worker_consistency == "strict" then
|
|
local ok, err = rebuild_router(ROUTER_SYNC_OPTS)
|
|
if not ok and err then
|
|
-- If an error happens while updating, log it and return non-updated
|
|
-- version.
|
|
log(ERR, "could not rebuild router: ", err, " (stale router will be used)")
|
|
end
|
|
end
|
|
return ROUTER
|
|
end
|
|
|
|
|
|
-- for tests only
|
|
local function _set_update_router(f)
|
|
update_router = f
|
|
end
|
|
|
|
local function _set_build_router(f)
|
|
build_router = f
|
|
end
|
|
|
|
local function _set_router(r)
|
|
ROUTER = r
|
|
end
|
|
|
|
local function _set_router_version(v)
|
|
ROUTER_VERSION = v
|
|
end
|
|
|
|
|
|
local new_plugins_iterator
|
|
do
|
|
local PluginsIterator_new = PluginsIterator.new
|
|
new_plugins_iterator = function(version)
|
|
local plugin_iterator, err = PluginsIterator_new(version)
|
|
if not plugin_iterator then
|
|
return nil, err
|
|
end
|
|
|
|
local _, err = kong_shm:incr(PLUGINS_REBUILD_COUNTER_KEY, 1, 0)
|
|
if err then
|
|
log(ERR, "failed to increase plugins rebuild counter: ", err)
|
|
end
|
|
|
|
return plugin_iterator
|
|
end
|
|
end
|
|
|
|
|
|
local function build_plugins_iterator(version)
|
|
local plugins_iterator, err = new_plugins_iterator(version)
|
|
if not plugins_iterator then
|
|
return nil, err
|
|
end
|
|
|
|
local phase = get_phase()
|
|
-- skip calling plugins_iterator:configure on init/init_worker
|
|
-- as it is explicitly called on init_worker
|
|
if phase ~= "init" and phase ~= "init_worker" then
|
|
plugins_iterator:configure()
|
|
end
|
|
|
|
PLUGINS_ITERATOR = plugins_iterator
|
|
return true
|
|
end
|
|
|
|
|
|
local function update_plugins_iterator()
|
|
local version, err = kong.core_cache:get("plugins_iterator:version", TTL_ZERO, uuid)
|
|
if err then
|
|
return nil, "failed to retrieve plugins iterator version: " .. err
|
|
end
|
|
|
|
if PLUGINS_ITERATOR and PLUGINS_ITERATOR.version == version then
|
|
return true
|
|
end
|
|
|
|
local ok, err = build_plugins_iterator(version)
|
|
if not ok then
|
|
return nil, --[[ 'err' fully formatted ]] err
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
local function rebuild_plugins_iterator(opts)
|
|
local plugins_iterator_version = PLUGINS_ITERATOR and PLUGINS_ITERATOR.version
|
|
return rebuild("plugins_iterator", update_plugins_iterator, plugins_iterator_version, opts)
|
|
end
|
|
|
|
|
|
local function get_updated_plugins_iterator()
|
|
if kong.db.strategy ~= "off" and kong.configuration.worker_consistency == "strict" then
|
|
local ok, err = rebuild_plugins_iterator(PLUGINS_ITERATOR_SYNC_OPTS)
|
|
if not ok then
|
|
-- If an error happens while updating, log it and return non-updated
|
|
-- version
|
|
log(ERR, "could not rebuild plugins iterator: ", err,
|
|
" (stale plugins iterator will be used)")
|
|
end
|
|
end
|
|
return PLUGINS_ITERATOR
|
|
end
|
|
|
|
|
|
local function get_plugins_iterator()
|
|
return PLUGINS_ITERATOR
|
|
end
|
|
|
|
|
|
-- for tests only
|
|
local function _set_update_plugins_iterator(f)
|
|
update_plugins_iterator = f
|
|
end
|
|
|
|
|
|
local function build_wasm_state()
|
|
local version = wasm.get_version()
|
|
local ok, err = wasm.update_in_place(version)
|
|
|
|
if not ok then
|
|
return nil, err
|
|
end
|
|
|
|
WASM_STATE_VERSION = version
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
local function rebuild_wasm_state(opts)
|
|
return rebuild("filter_chains", build_wasm_state,
|
|
WASM_STATE_VERSION, opts)
|
|
end
|
|
|
|
|
|
local function wasm_attach(ctx)
|
|
if not wasm.enabled() then
|
|
return
|
|
end
|
|
|
|
if kong.db.strategy ~= "off" and kong.configuration.worker_consistency == "strict" then
|
|
local ok, err = rebuild_wasm_state(WASM_STATE_SYNC_OPTS)
|
|
if not ok then
|
|
log(ERR, "could not update wasm filter chain state: ", err,
|
|
" (stale state will be used)")
|
|
end
|
|
end
|
|
|
|
wasm.attach(ctx)
|
|
end
|
|
|
|
|
|
local reconfigure_handler
|
|
do
|
|
local get_monotonic_ms = require("kong.tools.time").get_updated_monotonic_ms
|
|
|
|
local ngx_worker_id = ngx.worker.id
|
|
local exiting = ngx.worker.exiting
|
|
|
|
local CLEAR_HEALTH_STATUS_DELAY = constants.CLEAR_HEALTH_STATUS_DELAY
|
|
|
|
-- '0' for compare with nil
|
|
local CURRENT_ROUTER_HASH = 0
|
|
local CURRENT_PLUGINS_HASH = 0
|
|
local CURRENT_BALANCER_HASH = 0
|
|
|
|
reconfigure_handler = function(data)
|
|
local worker_id = ngx_worker_id() or -1
|
|
|
|
if exiting() then
|
|
log(NOTICE, "declarative reconfigure was canceled on worker #", worker_id,
|
|
": process exiting")
|
|
return true
|
|
end
|
|
|
|
local reconfigure_started_at = get_monotonic_ms()
|
|
|
|
log(INFO, "declarative reconfigure was started on worker #", worker_id)
|
|
|
|
local default_ws
|
|
local router_hash
|
|
local plugins_hash
|
|
local balancer_hash
|
|
|
|
if type(data) == "table" then
|
|
default_ws = data[1]
|
|
router_hash = data[2]
|
|
plugins_hash = data[3]
|
|
balancer_hash = data[4]
|
|
end
|
|
|
|
local ok, err = concurrency.with_coroutine_mutex(RECONFIGURE_OPTS, function()
|
|
-- below you are encouraged to yield for cooperative threading
|
|
|
|
kong.vault.flush()
|
|
|
|
local rebuild_balancer = balancer_hash ~= CURRENT_BALANCER_HASH
|
|
if rebuild_balancer then
|
|
log(DEBUG, "stopping previously started health checkers on worker #", worker_id)
|
|
balancer.stop_healthcheckers(CLEAR_HEALTH_STATUS_DELAY)
|
|
end
|
|
|
|
kong.default_workspace = default_ws
|
|
ngx.ctx.workspace = default_ws
|
|
|
|
local router, err
|
|
if router_hash ~= CURRENT_ROUTER_HASH then
|
|
local start = get_monotonic_ms()
|
|
|
|
router, err = new_router()
|
|
if not router then
|
|
return nil, err
|
|
end
|
|
|
|
log(INFO, "building a new router took ", get_monotonic_ms() - start,
|
|
" ms on worker #", worker_id)
|
|
end
|
|
|
|
local plugins_iterator
|
|
if plugins_hash ~= CURRENT_PLUGINS_HASH then
|
|
local start = get_monotonic_ms()
|
|
plugins_iterator, err = new_plugins_iterator()
|
|
if not plugins_iterator then
|
|
return nil, err
|
|
end
|
|
|
|
log(INFO, "building a new plugins iterator took ", get_monotonic_ms() - start,
|
|
" ms on worker #", worker_id)
|
|
end
|
|
|
|
local wasm_state
|
|
if wasm.enabled() then
|
|
local start = get_monotonic_ms()
|
|
wasm_state, err = wasm.rebuild_state()
|
|
|
|
if not wasm_state then
|
|
return nil, err
|
|
end
|
|
|
|
log(INFO, "rebuilding wasm filter chain state took ", get_monotonic_ms() - start,
|
|
" ms on worker #", worker_id)
|
|
end
|
|
|
|
-- below you are not supposed to yield and this should be fast and atomic
|
|
|
|
-- TODO: we should perhaps only purge the configuration related cache.
|
|
|
|
log(DEBUG, "flushing caches as part of the reconfiguration on worker #", worker_id)
|
|
|
|
kong.core_cache:purge()
|
|
kong.cache:purge()
|
|
|
|
if router then
|
|
ROUTER = router
|
|
ROUTER_CACHE:flush_all()
|
|
ROUTER_CACHE_NEG:flush_all()
|
|
CURRENT_ROUTER_HASH = router_hash or 0
|
|
end
|
|
|
|
if plugins_iterator then
|
|
-- Before we replace plugin iterator we need to call configure handler
|
|
-- of each plugin. There is a slight chance that plugin configure handler
|
|
-- would yield, and that should be considered a bad practice.
|
|
plugins_iterator:configure()
|
|
|
|
PLUGINS_ITERATOR = plugins_iterator
|
|
CURRENT_PLUGINS_HASH = plugins_hash or 0
|
|
end
|
|
|
|
if rebuild_balancer then
|
|
-- TODO: balancer is a big blob of global state and you cannot easily
|
|
-- initialize new balancer and then atomically flip it.
|
|
log(DEBUG, "reinitializing balancer with a new configuration on worker #", worker_id)
|
|
balancer.init()
|
|
CURRENT_BALANCER_HASH = balancer_hash or 0
|
|
end
|
|
|
|
if wasm_state then
|
|
wasm.set_state(wasm_state)
|
|
end
|
|
|
|
return true
|
|
end) -- concurrency.with_coroutine_mutex
|
|
|
|
local reconfigure_time = get_monotonic_ms() - reconfigure_started_at
|
|
|
|
if ok then
|
|
log(INFO, "declarative reconfigure took ", reconfigure_time,
|
|
" ms on worker #", worker_id)
|
|
|
|
else
|
|
log(ERR, "declarative reconfigure failed after ", reconfigure_time,
|
|
" ms on worker #", worker_id, ": ", err)
|
|
end
|
|
end -- reconfigure_handler
|
|
end
|
|
|
|
|
|
local balancer_prepare
|
|
do
|
|
local function sleep_once_for_balancer_init()
|
|
ngx.sleep(0)
|
|
sleep_once_for_balancer_init = NOOP
|
|
end
|
|
|
|
function balancer_prepare(ctx, scheme, host_type, host, port,
|
|
service, route)
|
|
|
|
sleep_once_for_balancer_init()
|
|
|
|
local retries
|
|
local connect_timeout
|
|
local send_timeout
|
|
local read_timeout
|
|
|
|
if service then
|
|
retries = service.retries
|
|
connect_timeout = service.connect_timeout
|
|
send_timeout = service.write_timeout
|
|
read_timeout = service.read_timeout
|
|
end
|
|
|
|
local balancer_data = {
|
|
scheme = scheme, -- scheme for balancer: http, https
|
|
type = host_type, -- type of 'host': ipv4, ipv6, name
|
|
host = host, -- target host per `service` entity
|
|
port = port, -- final target port
|
|
try_count = 0, -- retry counter
|
|
|
|
retries = retries or 5,
|
|
connect_timeout = connect_timeout or 60000,
|
|
send_timeout = send_timeout or 60000,
|
|
read_timeout = read_timeout or 60000,
|
|
|
|
-- stores info per try, metatable is needed for basic log serializer
|
|
-- see #6390
|
|
tries = setmetatable({}, ARRAY_MT),
|
|
-- ip = nil, -- final target IP address
|
|
-- balancer = nil, -- the balancer object, if any
|
|
-- hostname = nil, -- hostname of the final target IP
|
|
-- hash_cookie = nil, -- if Upstream sets hash_on_cookie
|
|
-- balancer_handle = nil, -- balancer handle for the current connection
|
|
}
|
|
|
|
ctx.service = service
|
|
ctx.route = route
|
|
ctx.balancer_data = balancer_data
|
|
|
|
set_service_ssl(ctx)
|
|
|
|
if is_stream_module and scheme == "tcp" then
|
|
local res, err = disable_proxy_ssl()
|
|
if not res then
|
|
log(ERR, "unable to disable upstream TLS handshake: ", err)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
local function balancer_execute(ctx)
|
|
local balancer_data = ctx.balancer_data
|
|
local ok, err, errcode = balancer.execute(balancer_data, ctx)
|
|
if not ok and errcode == 500 then
|
|
err = "failed the initial dns/balancer resolve for '" ..
|
|
balancer_data.host .. "' with: " .. tostring(err)
|
|
end
|
|
return ok, err, errcode
|
|
end
|
|
|
|
|
|
local function set_init_versions_in_cache()
|
|
-- because of worker events, kong.cache can not be initialized in `init` phase
|
|
-- therefore, we need to use the shdict API directly to set the initial value
|
|
assert(ngx.get_phase() == "init")
|
|
|
|
local core_cache_shm = ngx.shared["kong_core_db_cache"]
|
|
|
|
-- ttl = forever is okay as "*:versions" keys are always manually invalidated
|
|
local marshalled_value = encode("init")
|
|
|
|
-- see kong.cache.safe_set function
|
|
local ok, err = core_cache_shm:safe_set("kong_core_db_cacheplugins_iterator:version", marshalled_value)
|
|
if not ok then
|
|
return nil, "failed to set initial plugins iterator version in cache: " .. tostring(err)
|
|
end
|
|
|
|
if kong.configuration.role ~= "control_plane" then
|
|
ok, err = core_cache_shm:safe_set("kong_core_db_cacherouter:version", marshalled_value)
|
|
if not ok then
|
|
return nil, "failed to set initial router version in cache: " .. tostring(err)
|
|
end
|
|
|
|
ok, err = core_cache_shm:safe_set("kong_core_db_cachefilter_chains:version", marshalled_value)
|
|
if not ok then
|
|
return nil, "failed to set initial wasm filter chains version in cache: " .. tostring(err)
|
|
end
|
|
end
|
|
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
-- in the table below the `before` and `after` is to indicate when they run:
|
|
-- before or after the plugins
|
|
return {
|
|
build_router = build_router,
|
|
update_router = update_router,
|
|
build_plugins_iterator = build_plugins_iterator,
|
|
update_plugins_iterator = update_plugins_iterator,
|
|
get_plugins_iterator = get_plugins_iterator,
|
|
get_updated_plugins_iterator = get_updated_plugins_iterator,
|
|
set_init_versions_in_cache = set_init_versions_in_cache,
|
|
wasm_attach = wasm_attach,
|
|
|
|
-- exposed only for tests
|
|
_set_router = _set_router,
|
|
_set_update_router = _set_update_router,
|
|
_set_build_router = _set_build_router,
|
|
_set_router_version = _set_router_version,
|
|
_set_update_plugins_iterator = _set_update_plugins_iterator,
|
|
_get_updated_router = get_updated_router,
|
|
_update_lua_mem = update_lua_mem,
|
|
|
|
init_worker = {
|
|
before = function()
|
|
local socket_path = kong.configuration.socket_path
|
|
STREAM_TLS_TERMINATE_SOCK = fmt("unix:%s/%s", socket_path, constants.SOCKETS.STREAM_TLS_TERMINATE)
|
|
STREAM_TLS_PASSTHROUGH_SOCK = fmt("unix:%s/%s", socket_path, constants.SOCKETS.STREAM_TLS_PASSTHROUGH)
|
|
|
|
log_level.init_worker()
|
|
|
|
if kong.configuration.host_ports then
|
|
HOST_PORTS = kong.configuration.host_ports
|
|
end
|
|
|
|
if kong.configuration.anonymous_reports then
|
|
reports.init(kong.configuration)
|
|
reports.add_ping_value("database_version", kong.db.infos.db_ver)
|
|
reports.init_worker(kong.configuration)
|
|
end
|
|
|
|
update_lua_mem(true)
|
|
|
|
if kong.configuration.role == "control_plane" then
|
|
return
|
|
end
|
|
|
|
events.register_events(reconfigure_handler)
|
|
|
|
-- initialize balancers for active healthchecks
|
|
timer_at(0, function()
|
|
balancer.init()
|
|
end)
|
|
|
|
local strategy = kong.db.strategy
|
|
|
|
do
|
|
local rebuild_timeout = 60
|
|
|
|
if strategy == "postgres" then
|
|
rebuild_timeout = kong.configuration.pg_timeout / 1000
|
|
end
|
|
|
|
if strategy == "off" then
|
|
RECONFIGURE_OPTS = {
|
|
name = "reconfigure",
|
|
timeout = rebuild_timeout,
|
|
}
|
|
|
|
elseif kong.configuration.worker_consistency == "strict" then
|
|
ROUTER_SYNC_OPTS = {
|
|
name = "router",
|
|
timeout = rebuild_timeout,
|
|
on_timeout = "run_unlocked",
|
|
}
|
|
|
|
PLUGINS_ITERATOR_SYNC_OPTS = {
|
|
name = "plugins_iterator",
|
|
timeout = rebuild_timeout,
|
|
on_timeout = "run_unlocked",
|
|
}
|
|
|
|
WASM_STATE_SYNC_OPTS = {
|
|
name = "wasm",
|
|
timeout = rebuild_timeout,
|
|
on_timeout = "run_unlocked",
|
|
}
|
|
end
|
|
end
|
|
|
|
-- start some rebuild timers for
|
|
-- 1. traditional mode
|
|
-- 2. DP with rpc sync on (dbless mode)
|
|
if strategy ~= "off" or kong.sync then
|
|
local worker_state_update_frequency = kong.configuration.worker_state_update_frequency or 1
|
|
|
|
--[[
|
|
+-----------+
|
|
| Start | <-------------------------------------+
|
|
+-----------+ |
|
|
| |
|
|
| |
|
|
v |
|
|
*************************** +-------+ |
|
|
* Is reconfigure running? * ---Yes--->| Sleep | ----+
|
|
*************************** +-------+
|
|
| ^
|
|
No |
|
|
| |
|
|
v |
|
|
+---------------+ |
|
|
| rebuild router|-------------------------+
|
|
+---------------+
|
|
|
|
Since reconfigure will also rebuild the router, we skip this round
|
|
of rebuilding the router.
|
|
--]]
|
|
local router_async_opts = {
|
|
name = RECONFIGURE_OPTS and RECONFIGURE_OPTS.name or "router", -- please check the above diagram for the
|
|
-- reason of using the same name as reconfigure
|
|
timeout = 0,
|
|
on_timeout = "return_true",
|
|
}
|
|
|
|
local function rebuild_router_timer(premature)
|
|
if premature then
|
|
return
|
|
end
|
|
|
|
-- Don't wait for the semaphore (timeout = 0) when updating via the
|
|
-- timer.
|
|
-- If the semaphore is locked, that means that the rebuild is
|
|
-- already ongoing.
|
|
local ok, err = rebuild_router(router_async_opts)
|
|
if not ok and err then
|
|
log(ERR, "could not rebuild router via timer: ", err)
|
|
end
|
|
end
|
|
|
|
local _, err = kong.timer:named_every("router-rebuild",
|
|
worker_state_update_frequency,
|
|
rebuild_router_timer)
|
|
if err then
|
|
log(ERR, "could not schedule timer to rebuild router: ", err)
|
|
end
|
|
|
|
local plugins_iterator_async_opts = {
|
|
name = "plugins_iterator",
|
|
timeout = 0,
|
|
on_timeout = "return_true",
|
|
}
|
|
|
|
local function rebuild_plugins_iterator_timer(premature)
|
|
if premature then
|
|
return
|
|
end
|
|
|
|
local _, err = rebuild_plugins_iterator(plugins_iterator_async_opts)
|
|
if err then
|
|
log(ERR, "could not rebuild plugins iterator via timer: ", err)
|
|
end
|
|
end
|
|
|
|
local _, err = kong.timer:named_every("plugins-iterator-rebuild",
|
|
worker_state_update_frequency,
|
|
rebuild_plugins_iterator_timer)
|
|
if err then
|
|
log(ERR, "could not schedule timer to rebuild plugins iterator: ", err)
|
|
end
|
|
|
|
if wasm.enabled() then
|
|
local wasm_async_opts = {
|
|
name = "wasm",
|
|
timeout = 0,
|
|
on_timeout = "return_true",
|
|
}
|
|
|
|
local function rebuild_wasm_filter_chains_timer(premature)
|
|
if premature then
|
|
return
|
|
end
|
|
|
|
local _, err = rebuild_wasm_state(wasm_async_opts)
|
|
if err then
|
|
log(ERR, "could not rebuild wasm filter chains via timer: ", err)
|
|
end
|
|
end
|
|
|
|
local _, err = kong.timer:named_every("wasm-rebuild",
|
|
worker_state_update_frequency,
|
|
rebuild_wasm_filter_chains_timer)
|
|
if err then
|
|
log(ERR, "could not schedule timer to rebuild WASM filter chains: ", err)
|
|
end
|
|
end
|
|
end -- rebuild timer do block
|
|
end,
|
|
},
|
|
preread = {
|
|
before = function(ctx)
|
|
local server_port = var.server_port
|
|
ctx.host_port = HOST_PORTS[server_port] or tonumber(server_port, 10)
|
|
|
|
local router = get_updated_router()
|
|
|
|
local match_t = router:exec(ctx)
|
|
if not match_t then
|
|
log(ERR, "no Route found with those values")
|
|
return exit(500)
|
|
end
|
|
|
|
local route = match_t.route
|
|
-- if matched route doesn't do tls_passthrough and we are in the preread server block
|
|
-- this request should be TLS terminated; return immediately and not run further steps
|
|
-- (even bypassing the balancer)
|
|
if var.kong_tls_preread_block == "1" then
|
|
local protocols = route.protocols
|
|
if protocols and protocols.tls then
|
|
log(DEBUG, "TLS termination required, return to second layer proxying")
|
|
var.kong_tls_preread_block_upstream = STREAM_TLS_TERMINATE_SOCK
|
|
|
|
elseif protocols and protocols.tls_passthrough then
|
|
var.kong_tls_preread_block_upstream = STREAM_TLS_PASSTHROUGH_SOCK
|
|
|
|
else
|
|
log(ERR, "unexpected protocols in matched Route")
|
|
return exit(500)
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
ctx.workspace = match_t.route and match_t.route.ws_id
|
|
|
|
local service = match_t.service
|
|
local upstream_url_t = match_t.upstream_url_t
|
|
|
|
balancer_prepare(ctx, match_t.upstream_scheme,
|
|
upstream_url_t.type,
|
|
upstream_url_t.host,
|
|
upstream_url_t.port,
|
|
service, route)
|
|
if match_t.upstream_host then
|
|
var.upstream_host = match_t.upstream_host
|
|
end
|
|
end,
|
|
after = function(ctx)
|
|
local upstream_scheme = var.upstream_scheme
|
|
|
|
local balancer_data = ctx.balancer_data
|
|
balancer_data.scheme = upstream_scheme -- COMPAT: pdk
|
|
|
|
-- The content of var.upstream_host is only set by the router if
|
|
-- preserve_host is true
|
|
--
|
|
-- We can't rely on var.upstream_host for balancer retries inside
|
|
-- `set_host_header` because it would never be empty after the first -- balancer try
|
|
local upstream_host = var.upstream_host
|
|
if upstream_host ~= nil and upstream_host ~= "" then
|
|
balancer_data.preserve_host = true
|
|
end
|
|
|
|
local ok, err, errcode = balancer_execute(ctx)
|
|
if not ok then
|
|
return kong.response.error(errcode, err)
|
|
end
|
|
|
|
local ok, err = balancer.set_host_header(balancer_data, upstream_scheme, upstream_host)
|
|
if not ok then
|
|
log(ERR, "failed to set balancer Host header: ", err)
|
|
return exit(500)
|
|
end
|
|
end
|
|
},
|
|
rewrite = {
|
|
before = function(ctx)
|
|
local server_port = var.server_port
|
|
ctx.host_port = HOST_PORTS[server_port] or tonumber(server_port, 10)
|
|
instrumentation.request(ctx)
|
|
end,
|
|
},
|
|
access = {
|
|
before = function(ctx)
|
|
-- if there is a gRPC service in the context, don't re-execute the pre-access
|
|
-- phase handler - it has been executed before the internal redirect
|
|
if ctx.service and (ctx.service.protocol == "grpc" or
|
|
ctx.service.protocol == "grpcs")
|
|
then
|
|
return
|
|
end
|
|
|
|
ctx.scheme = var.scheme
|
|
ctx.request_uri = var.request_uri
|
|
ctx.host = var.host
|
|
|
|
-- trace router
|
|
local span = instrumentation.router()
|
|
-- create the balancer span "in advance" so its ID is available
|
|
-- to plugins in the access phase for doing headers propagation
|
|
instrumentation.precreate_balancer_span(ctx)
|
|
|
|
local has_timing = ctx.has_timing
|
|
|
|
if has_timing then
|
|
req_dyn_hook_run_hook("timing", "before:router")
|
|
end
|
|
|
|
-- routing request
|
|
local router = get_updated_router()
|
|
local match_t = router:exec(ctx)
|
|
|
|
if has_timing then
|
|
req_dyn_hook_run_hook("timing", "after:router")
|
|
end
|
|
|
|
if not match_t then
|
|
-- tracing
|
|
if span then
|
|
span:set_status(2)
|
|
span:finish()
|
|
end
|
|
|
|
return kong.response.error(404, "no Route matched with those values")
|
|
end
|
|
|
|
-- ends tracing span
|
|
if span then
|
|
span:finish()
|
|
end
|
|
|
|
ctx.workspace = match_t.route and match_t.route.ws_id
|
|
|
|
if has_timing then
|
|
req_dyn_hook_run_hook("timing", "workspace_id:got", ctx.workspace)
|
|
end
|
|
|
|
local host = ctx.host
|
|
local port = ctx.host_port or tonumber(var.server_port, 10)
|
|
|
|
local route = match_t.route
|
|
local service = match_t.service
|
|
local upstream_url_t = match_t.upstream_url_t
|
|
|
|
local realip_remote_addr = var.realip_remote_addr
|
|
local forwarded_proto
|
|
local forwarded_host
|
|
local forwarded_port
|
|
local forwarded_path
|
|
local forwarded_prefix
|
|
|
|
-- X-Forwarded-* Headers Parsing
|
|
--
|
|
-- We could use $proxy_add_x_forwarded_for, but it does not work properly
|
|
-- with the realip module. The realip module overrides $remote_addr and it
|
|
-- is okay for us to use it in case no X-Forwarded-For header was present.
|
|
-- But in case it was given, we will append the $realip_remote_addr that
|
|
-- contains the IP that was originally in $remote_addr before realip
|
|
-- module overrode that (aka the client that connected us).
|
|
|
|
local trusted_ip = kong.ip.is_trusted(realip_remote_addr)
|
|
if trusted_ip then
|
|
forwarded_proto = get_header("x_forwarded_proto", ctx) or ctx.scheme
|
|
forwarded_host = get_header("x_forwarded_host", ctx) or host
|
|
forwarded_port = get_header("x_forwarded_port", ctx) or port
|
|
forwarded_path = get_header("x_forwarded_path", ctx)
|
|
forwarded_prefix = get_header("x_forwarded_prefix", ctx)
|
|
|
|
else
|
|
forwarded_proto = ctx.scheme
|
|
forwarded_host = host
|
|
forwarded_port = port
|
|
end
|
|
|
|
if not forwarded_path then
|
|
forwarded_path = ctx.request_uri
|
|
local p = find(forwarded_path, "?", 2, true)
|
|
if p then
|
|
forwarded_path = sub(forwarded_path, 1, p - 1)
|
|
end
|
|
end
|
|
|
|
if not forwarded_prefix and match_t.prefix ~= "/" then
|
|
forwarded_prefix = match_t.prefix
|
|
end
|
|
|
|
local protocols = route.protocols
|
|
if (protocols and protocols.https and not protocols.http and
|
|
forwarded_proto ~= "https")
|
|
then
|
|
local redirect_status_code = route.https_redirect_status_code or 426
|
|
|
|
if redirect_status_code == 426 then
|
|
return kong.response.error(426, "Please use HTTPS protocol", {
|
|
["Connection"] = "Upgrade",
|
|
["Upgrade"] = "TLS/1.2, HTTP/1.1",
|
|
})
|
|
end
|
|
|
|
if redirect_status_code == 301
|
|
or redirect_status_code == 302
|
|
or redirect_status_code == 307
|
|
or redirect_status_code == 308
|
|
then
|
|
header["Location"] = "https://" .. forwarded_host .. ctx.request_uri
|
|
return kong.response.exit(redirect_status_code)
|
|
end
|
|
end
|
|
|
|
local protocol_version = http_version()
|
|
if protocols.grpc or protocols.grpcs then
|
|
-- perf: branch usually not taken, don't cache var outside
|
|
local content_type = var.content_type
|
|
|
|
if content_type and sub(content_type, 1, #"application/grpc") == "application/grpc" then
|
|
if protocol_version ~= 2 then
|
|
-- mismatch: non-http/2 request matched grpc route
|
|
return kong.response.error(426, "Please use HTTP2 protocol", {
|
|
["connection"] = "Upgrade",
|
|
["upgrade"] = "HTTP/2",
|
|
})
|
|
end
|
|
|
|
else
|
|
-- mismatch: non-grpc request matched grpc route
|
|
return kong.response.error(415, "Non-gRPC request matched gRPC route")
|
|
end
|
|
|
|
if not protocols.grpc and forwarded_proto ~= "https" then
|
|
-- mismatch: grpc request matched grpcs route
|
|
return kong.response.exit(200, nil, {
|
|
["content-type"] = "application/grpc",
|
|
["grpc-status"] = 1,
|
|
["grpc-message"] = "gRPC request matched gRPCs route",
|
|
})
|
|
end
|
|
end
|
|
|
|
balancer_prepare(ctx, match_t.upstream_scheme,
|
|
upstream_url_t.type,
|
|
upstream_url_t.host,
|
|
upstream_url_t.port,
|
|
service, route)
|
|
|
|
ctx.router_matches = match_t.matches
|
|
|
|
-- `uri` is the URI with which to call upstream, as returned by the
|
|
-- router, which might have truncated it (`strip_uri`).
|
|
-- `host` is the original header to be preserved if set.
|
|
var.upstream_scheme = match_t.upstream_scheme -- COMPAT: pdk
|
|
var.upstream_uri = escape(match_t.upstream_uri)
|
|
if match_t.upstream_host then
|
|
var.upstream_host = match_t.upstream_host
|
|
end
|
|
|
|
-- Keep-Alive and WebSocket Protocol Upgrade Headers
|
|
local upgrade = get_header("upgrade", ctx)
|
|
if upgrade and lower(upgrade) == "websocket" then
|
|
var.upstream_connection = "keep-alive, Upgrade"
|
|
var.upstream_upgrade = "websocket"
|
|
|
|
else
|
|
var.upstream_connection = "keep-alive"
|
|
end
|
|
|
|
-- X-Forwarded-* Headers
|
|
local http_x_forwarded_for = get_header("x_forwarded_for", ctx)
|
|
if http_x_forwarded_for then
|
|
var.upstream_x_forwarded_for = http_x_forwarded_for .. ", " ..
|
|
realip_remote_addr
|
|
|
|
else
|
|
var.upstream_x_forwarded_for = var.remote_addr
|
|
end
|
|
|
|
var.upstream_x_forwarded_proto = forwarded_proto
|
|
var.upstream_x_forwarded_host = forwarded_host
|
|
var.upstream_x_forwarded_port = forwarded_port
|
|
var.upstream_x_forwarded_path = forwarded_path
|
|
var.upstream_x_forwarded_prefix = forwarded_prefix
|
|
|
|
do
|
|
local req_via = get_header(constants.HEADERS.VIA, ctx)
|
|
local kong_inbound_via = protocol_version and protocol_version .. " " .. SERVER_HEADER
|
|
or SERVER_HEADER
|
|
var.upstream_via = req_via and req_via .. ", " .. kong_inbound_via
|
|
or kong_inbound_via
|
|
end
|
|
|
|
-- At this point, the router and `balancer_setup_stage1` have been
|
|
-- executed; detect requests that need to be redirected from `proxy_pass`
|
|
-- to `grpc_pass`. After redirection, this function will return early
|
|
if service and var.kong_proxy_mode == "http" then
|
|
if service.protocol == "grpc" or service.protocol == "grpcs" then
|
|
return exec("@grpc")
|
|
end
|
|
|
|
if route.request_buffering == false then
|
|
if route.response_buffering == false then
|
|
return exec("@unbuffered")
|
|
end
|
|
|
|
return exec("@unbuffered_request")
|
|
end
|
|
|
|
if route.response_buffering == false then
|
|
return exec("@unbuffered_response")
|
|
end
|
|
end
|
|
end,
|
|
-- Only executed if the `router` module found a route and allows nginx to proxy it.
|
|
after = function(ctx)
|
|
-- Nginx's behavior when proxying a request with an empty querystring
|
|
-- `/foo?` is to keep `$is_args` an empty string, hence effectively
|
|
-- stripping the empty querystring.
|
|
-- We overcome this behavior with our own logic, to preserve user
|
|
-- desired semantics.
|
|
-- perf: branch usually not taken, don't cache var outside
|
|
if byte(ctx.request_uri or var.request_uri, -1) == QUESTION_MARK or var.is_args == "?" then
|
|
var.upstream_uri = var.upstream_uri .. "?" .. (var.args or "")
|
|
end
|
|
|
|
local upstream_scheme = var.upstream_scheme
|
|
|
|
local balancer_data = ctx.balancer_data
|
|
balancer_data.scheme = upstream_scheme -- COMPAT: pdk
|
|
|
|
-- The content of var.upstream_host is only set by the router if
|
|
-- preserve_host is true
|
|
--
|
|
-- We can't rely on var.upstream_host for balancer retries inside
|
|
-- `set_host_header` because it would never be empty after the first -- balancer try
|
|
local upstream_host = var.upstream_host
|
|
if upstream_host ~= nil and upstream_host ~= "" then
|
|
balancer_data.preserve_host = true
|
|
|
|
-- the nginx grpc module does not offer a way to override the
|
|
-- :authority pseudo-header; use our internal API to do so
|
|
-- this call applies to routes with preserve_host=true; for
|
|
-- preserve_host=false, the header is set in `set_host_header`,
|
|
-- so that it also applies to balancer retries
|
|
if upstream_scheme == "grpc" or upstream_scheme == "grpcs" then
|
|
local ok, err = set_authority(upstream_host)
|
|
if not ok then
|
|
log(ERR, "failed to set :authority header: ", err)
|
|
end
|
|
end
|
|
end
|
|
|
|
local ok, err, errcode = balancer_execute(ctx)
|
|
if not ok then
|
|
return kong.response.error(errcode, err)
|
|
end
|
|
|
|
local ok, err = balancer.set_host_header(balancer_data, upstream_scheme, upstream_host)
|
|
if not ok then
|
|
log(ERR, "failed to set balancer Host header: ", err)
|
|
return exit(500)
|
|
end
|
|
|
|
local header_cache = {}
|
|
-- clear hop-by-hop request headers:
|
|
local http_connection = get_header("connection", header_cache)
|
|
if http_connection ~= "keep-alive" and
|
|
http_connection ~= "close" and
|
|
http_connection ~= "upgrade"
|
|
then
|
|
for _, header_name in csv(http_connection) do
|
|
-- some of these are already handled by the proxy module,
|
|
-- upgrade being an exception that is handled below with
|
|
-- special semantics.
|
|
if header_name == "upgrade" then
|
|
if var.upstream_connection == "keep-alive" then
|
|
clear_header(header_name)
|
|
end
|
|
|
|
else
|
|
clear_header(header_name)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- add te header only when client requests trailers (proxy removes it)
|
|
local http_te = get_header("te", header_cache)
|
|
if http_te then
|
|
if http_te == "trailers" then
|
|
var.upstream_te = "trailers"
|
|
|
|
else
|
|
for _, header_name in csv(http_te) do
|
|
if header_name == "trailers" then
|
|
var.upstream_te = "trailers"
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if get_header("proxy", header_cache) then
|
|
clear_header("Proxy")
|
|
end
|
|
|
|
if get_header("proxy_connection", header_cache) then
|
|
clear_header("Proxy-Connection")
|
|
end
|
|
end
|
|
},
|
|
header_filter = {
|
|
before = function(ctx)
|
|
if not ctx.KONG_PROXIED then
|
|
instrumentation.runloop_before_header_filter(ngx.status)
|
|
return
|
|
end
|
|
|
|
-- clear hop-by-hop response headers:
|
|
local upstream_http_connection = var.upstream_http_connection
|
|
if upstream_http_connection ~= "keep-alive" and
|
|
upstream_http_connection ~= "close" and
|
|
upstream_http_connection ~= "upgrade"
|
|
then
|
|
for _, header_name in csv(upstream_http_connection) do
|
|
if header_name ~= "close" and header_name ~= "upgrade" and header_name ~= "keep-alive" then
|
|
header[header_name] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
local upgrade = var.upstream_http_upgrade
|
|
if upgrade and lower(upgrade) ~= lower(var.upstream_upgrade) then
|
|
header["Upgrade"] = nil
|
|
end
|
|
|
|
-- remove trailer response header when client didn't ask for them
|
|
if var.upstream_te == "" and var.upstream_http_trailer then
|
|
header["Trailer"] = nil
|
|
end
|
|
|
|
local upstream_status_header = constants.HEADERS.UPSTREAM_STATUS
|
|
if kong.configuration.enabled_headers[upstream_status_header] then
|
|
local upstream_status = ctx.buffered_status or tonumber(sub(var.upstream_status or "", -3)) or ngx.status
|
|
header[upstream_status_header] = upstream_status
|
|
if not header[upstream_status_header] then
|
|
log(ERR, "failed to set ", upstream_status_header, " header")
|
|
end
|
|
end
|
|
|
|
-- if this is the last try and it failed, save its state to correctly log it
|
|
local status = ngx.status
|
|
if status > 499 and ctx.balancer_data then
|
|
local balancer_data = ctx.balancer_data
|
|
local try_count = balancer_data.try_count
|
|
local retries = balancer_data.retries
|
|
if try_count > retries then
|
|
local current_try = balancer_data.tries[try_count]
|
|
current_try.state = "failed"
|
|
current_try.code = status
|
|
end
|
|
end
|
|
|
|
instrumentation.runloop_before_header_filter(status)
|
|
|
|
local hash_cookie = ctx.balancer_data.hash_cookie
|
|
if hash_cookie then
|
|
balancer.set_cookie(hash_cookie)
|
|
end
|
|
end,
|
|
after = function(ctx)
|
|
local enabled_headers = kong.configuration.enabled_headers
|
|
local headers = constants.HEADERS
|
|
if ctx.KONG_PROXIED then
|
|
if enabled_headers[headers.UPSTREAM_LATENCY] then
|
|
header[headers.UPSTREAM_LATENCY] = ctx.KONG_WAITING_TIME
|
|
end
|
|
|
|
if enabled_headers[headers.PROXY_LATENCY] then
|
|
header[headers.PROXY_LATENCY] = ctx.KONG_PROXY_LATENCY
|
|
end
|
|
|
|
if enabled_headers[headers.VIA] then
|
|
-- Kong does not support injected directives like 'nginx_location_proxy_http_version',
|
|
-- so we skip checking them.
|
|
|
|
local proxy_http_version
|
|
|
|
local upstream_scheme = var.upstream_scheme
|
|
if upstream_scheme == "grpc" or upstream_scheme == "grpcs" then
|
|
proxy_http_version = "2"
|
|
end
|
|
if not proxy_http_version then
|
|
proxy_http_version = ctx.proxy_http_version or
|
|
kong.configuration.proxy_http_version or
|
|
DEFAULT_PROXY_HTTP_VERSION
|
|
end
|
|
|
|
local kong_outbound_via = proxy_http_version .. " " .. SERVER_HEADER
|
|
local resp_via = var["upstream_http_" .. headers.VIA]
|
|
header[headers.VIA] = resp_via and resp_via .. ", " .. kong_outbound_via
|
|
or kong_outbound_via
|
|
end
|
|
|
|
-- If upstream does not provide the 'Server' header, an 'openresty' header
|
|
-- would be inserted by default. We override it with the Kong server header.
|
|
if not header[headers.SERVER] and enabled_headers[headers.SERVER] then
|
|
header[headers.SERVER] = SERVER_HEADER
|
|
end
|
|
|
|
else
|
|
if enabled_headers[headers.RESPONSE_LATENCY] then
|
|
header[headers.RESPONSE_LATENCY] = ctx.KONG_RESPONSE_LATENCY
|
|
end
|
|
|
|
-- Some plugins short-circuit the request with Via-header, and in those cases
|
|
-- we don't want to set the Server-header, if the Via-header matches with
|
|
-- the Kong server header.
|
|
if not (enabled_headers[headers.VIA] and header[headers.VIA] == SERVER_HEADER) then
|
|
if enabled_headers[headers.SERVER] then
|
|
header[headers.SERVER] = SERVER_HEADER
|
|
|
|
else
|
|
header[headers.SERVER] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
-- X-Kong-Request-Id downstream header
|
|
local rid, rid_get_err = request_id_get()
|
|
if not rid then
|
|
log(WARN, "failed to get Request ID: ", rid_get_err)
|
|
end
|
|
|
|
if enabled_headers[headers.REQUEST_ID] and rid then
|
|
header[headers.REQUEST_ID] = rid
|
|
end
|
|
end
|
|
},
|
|
log = {
|
|
before = function(ctx)
|
|
instrumentation.runloop_log_before(ctx)
|
|
end,
|
|
after = function(ctx)
|
|
instrumentation.runloop_log_after(ctx)
|
|
|
|
update_lua_mem()
|
|
|
|
if kong.configuration.anonymous_reports then
|
|
reports.log(ctx)
|
|
end
|
|
|
|
if not ctx.KONG_PROXIED then
|
|
return
|
|
end
|
|
|
|
-- If response was produced by an upstream (ie, not by a Kong plugin)
|
|
-- Report HTTP status for health checks
|
|
local balancer_data = ctx.balancer_data
|
|
if balancer_data and balancer_data.balancer_handle then
|
|
-- https://nginx.org/en/docs/http/ngx_http_upstream_module.html#variables
|
|
-- because of the way of Nginx do the upstream_status variable, it may be
|
|
-- a string or a number, so we need to parse it to get the status
|
|
local status = tonumber(ctx.buffered_status) or tonumber(sub(var.upstream_status or "", -3)) or ngx.status
|
|
if status == 504 then
|
|
balancer_data.balancer.report_timeout(balancer_data.balancer_handle)
|
|
else
|
|
balancer_data.balancer.report_http_status(
|
|
balancer_data.balancer_handle, status)
|
|
end
|
|
-- release the handle, so the balancer can update its statistics
|
|
if balancer_data.balancer_handle.release then
|
|
balancer_data.balancer_handle:release()
|
|
end
|
|
end
|
|
balancer.after_balance(balancer_data, ctx)
|
|
end
|
|
}
|
|
}
|