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