653 lines
20 KiB
Lua
653 lines
20 KiB
Lua
local ran_before
|
|
|
|
|
|
return function(options)
|
|
|
|
if ran_before then
|
|
ngx.log(ngx.WARN, debug.traceback("attempt to re-run the globalpatches", 2))
|
|
return
|
|
end
|
|
ngx.log(ngx.DEBUG, "installing the globalpatches")
|
|
ran_before = true
|
|
|
|
|
|
options = options or {}
|
|
local meta = require "kong.meta"
|
|
local constants = require "kong.constants"
|
|
|
|
|
|
local cjson_safe = require("cjson.safe")
|
|
cjson_safe.encode_sparse_array(nil, nil, 2^15)
|
|
cjson_safe.encode_number_precision(constants.CJSON_MAX_PRECISION)
|
|
|
|
local pb = require "pb"
|
|
|
|
-- let pb decode arrays to table cjson.empty_array_mt metatable
|
|
-- so empty arrays are encoded as `[]` instead of `nil` or `{}` by cjson.
|
|
pb.option("decode_default_array")
|
|
pb.defaults("*array", cjson_safe.empty_array_mt)
|
|
|
|
if options.cli then
|
|
-- disable the _G write guard alert log introduced in OpenResty 1.15.8.1
|
|
-- when in CLI or when running tests in resty-cli
|
|
--local _G_mt = getmetatable(_G)
|
|
setmetatable(_G, nil)
|
|
end
|
|
|
|
|
|
_G._KONG = {
|
|
_NAME = meta._NAME,
|
|
_VERSION = meta._VERSION
|
|
}
|
|
|
|
if options.cli then
|
|
ngx.IS_CLI = true
|
|
-- luacheck: globals ngx.exit
|
|
ngx.exit = function() end
|
|
end
|
|
|
|
|
|
|
|
do -- implement `sleep` in the `init_worker` context
|
|
|
|
-- initialization code regularly uses the shm and locks.
|
|
-- the resty-lock is based on sleeping while waiting, but that api
|
|
-- is unavailable. Hence we implement a BLOCKING sleep, only in
|
|
-- the init_worker context.
|
|
local get_phase = ngx.get_phase
|
|
local ngx_sleep = ngx.sleep
|
|
local alternative_sleep = function(t)
|
|
require("socket").sleep(t)
|
|
-- the ngx sleep will yield and hence update time, this implementation
|
|
-- does not, so we must force a time update to prevent time based loops
|
|
-- from getting into a deadlock/spin.
|
|
-- See https://github.com/Kong/lua-resty-worker-events/issues/41
|
|
ngx.update_time()
|
|
end
|
|
|
|
-- luacheck: globals ngx.sleep
|
|
local blocking_sleep_phases = {
|
|
init = true,
|
|
init_worker = true,
|
|
}
|
|
ngx.sleep = function(s)
|
|
if blocking_sleep_phases[get_phase()] then
|
|
ngx.log(ngx.NOTICE, "executing a blocking 'sleep' (", s, " seconds)")
|
|
return alternative_sleep(s)
|
|
end
|
|
return ngx_sleep(s)
|
|
end
|
|
|
|
_G.native_ngx_sleep = ngx_sleep
|
|
|
|
end
|
|
|
|
|
|
do
|
|
_G.native_timer_at = ngx.timer.at
|
|
_G.native_timer_every = ngx.timer.every
|
|
|
|
local _timerng
|
|
|
|
if options.cli or options.rbusted then
|
|
_timerng = require("resty.timerng").new({
|
|
min_threads = 16,
|
|
max_threads = 32,
|
|
})
|
|
|
|
_timerng:start()
|
|
|
|
else
|
|
_timerng = require("resty.timerng").new({
|
|
min_threads = 256,
|
|
max_threads = 1024,
|
|
})
|
|
end
|
|
|
|
_G.timerng = _timerng
|
|
|
|
_G.ngx.timer.at = function (delay, callback, ...)
|
|
return _timerng:at(delay, callback, ...)
|
|
end
|
|
|
|
_G.ngx.timer.every = function (interval, callback, ...)
|
|
return _timerng:every(interval, callback, ...)
|
|
end
|
|
end
|
|
|
|
|
|
do
|
|
if ngx.config.subsystem == "http" then
|
|
local get_request = require("resty.core.base").get_request
|
|
|
|
local error = error
|
|
|
|
local get_req_headers = ngx.req.get_headers
|
|
local get_resp_headers = ngx.resp.get_headers
|
|
local get_uri_args = ngx.req.get_uri_args
|
|
local get_post_args = ngx.req.get_post_args
|
|
local decode_args = ngx.decode_args
|
|
local read_req_body = ngx.req.read_body
|
|
|
|
local DEFAULT_MAX_REQ_HEADERS = 100
|
|
local DEFAULT_MAX_RESP_HEADERS = 100
|
|
local DEFAULT_MAX_URI_ARGS = 100
|
|
local DEFAULT_MAX_POST_ARGS = 100
|
|
local DEFAULT_MAX_DECODE_ARGS = 100
|
|
|
|
local MAX_REQ_HEADERS
|
|
local MAX_RESP_HEADERS
|
|
local MAX_URI_ARGS
|
|
local MAX_POST_ARGS
|
|
local MAX_DECODE_ARGS
|
|
|
|
-- REQUEST HEADERS [
|
|
local function get_req_headers_real(max_req_headers, ...)
|
|
local request_headers, err = get_req_headers(max_req_headers or MAX_REQ_HEADERS or DEFAULT_MAX_REQ_HEADERS, ...)
|
|
if err == "truncated" then
|
|
kong.log.notice("request headers truncated")
|
|
end
|
|
return request_headers, err
|
|
end
|
|
|
|
_G.ngx.req.get_headers = function(max_req_headers, ...)
|
|
if not get_request() then
|
|
error("no request found")
|
|
end
|
|
MAX_REQ_HEADERS = kong and kong.configuration and kong.configuration.lua_max_req_headers or DEFAULT_MAX_REQ_HEADERS
|
|
_G.ngx.req.get_headers = get_req_headers_real
|
|
return get_req_headers_real(max_req_headers or MAX_REQ_HEADERS, ...)
|
|
end
|
|
-- ]
|
|
|
|
-- RESPONSE HEADERS [
|
|
local function get_resp_headers_real(max_resp_headers, ...)
|
|
local response_headers, err = get_resp_headers(max_resp_headers or MAX_RESP_HEADERS or DEFAULT_MAX_RESP_HEADERS, ...)
|
|
if err == "truncated" then
|
|
kong.log.notice("response headers truncated")
|
|
end
|
|
return response_headers, err
|
|
end
|
|
|
|
_G.ngx.resp.get_headers = function(max_resp_headers, ...)
|
|
if not get_request() then
|
|
error("no request found")
|
|
end
|
|
MAX_RESP_HEADERS = kong and kong.configuration and kong.configuration.lua_max_resp_headers or DEFAULT_MAX_RESP_HEADERS
|
|
_G.ngx.resp.get_headers = get_resp_headers_real
|
|
return get_resp_headers_real(max_resp_headers or MAX_RESP_HEADERS, ...)
|
|
end
|
|
-- ]
|
|
|
|
-- URI ARGS [
|
|
local function get_uri_args_real(max_uri_args, ...)
|
|
local uri_args, err = get_uri_args(max_uri_args or MAX_URI_ARGS or DEFAULT_MAX_URI_ARGS, ...)
|
|
if err == "truncated" then
|
|
kong.log.notice("uri args truncated")
|
|
end
|
|
return uri_args, err
|
|
end
|
|
|
|
_G.ngx.req.get_uri_args = function(max_uri_args, ...)
|
|
if not get_request() then
|
|
error("no request found")
|
|
end
|
|
MAX_URI_ARGS = kong and kong.configuration and kong.configuration.lua_max_uri_args or DEFAULT_MAX_URI_ARGS
|
|
_G.ngx.req.get_uri_args = get_uri_args_real
|
|
return get_uri_args_real(max_uri_args or MAX_URI_ARGS, ...)
|
|
end
|
|
-- ]
|
|
|
|
-- POST ARGS [
|
|
local function get_post_args_real(max_post_args, ...)
|
|
local post_args, err = get_post_args(max_post_args or MAX_POST_ARGS or DEFAULT_MAX_POST_ARGS, ...)
|
|
if err == "truncated" then
|
|
kong.log.notice("post args truncated")
|
|
end
|
|
return post_args, err
|
|
end
|
|
|
|
_G.ngx.req.get_post_args = function(max_post_args, ...)
|
|
if not get_request() then
|
|
error("no request found")
|
|
end
|
|
MAX_POST_ARGS = kong and kong.configuration and kong.configuration.lua_max_post_args or DEFAULT_MAX_POST_ARGS
|
|
_G.ngx.req.get_post_args = get_post_args_real
|
|
return get_post_args_real(max_post_args or MAX_POST_ARGS, ...)
|
|
end
|
|
-- ]
|
|
|
|
-- DECODE ARGS [
|
|
local function decode_args_real(str, max_args, ...)
|
|
local args, err = decode_args(str, max_args or MAX_DECODE_ARGS or DEFAULT_MAX_DECODE_ARGS, ...)
|
|
if err == "truncated" then
|
|
kong.log.notice("decode args truncated")
|
|
end
|
|
return args, err
|
|
end
|
|
|
|
_G.ngx.decode_args = function(str, max_args, ...)
|
|
-- Currently the kong.configuration.lua_max_uri_args is used for this too.
|
|
MAX_DECODE_ARGS = kong and kong.configuration and kong.configuration.lua_max_uri_args or DEFAULT_MAX_DECODE_ARGS
|
|
_G.ngx.decode_args = decode_args_real
|
|
return decode_args_real(str, max_args or MAX_DECODE_ARGS, ...)
|
|
end
|
|
-- ]
|
|
|
|
-- READ REQUEST BODY [
|
|
_G.ngx.req.read_body = function()
|
|
-- for the same request, only one `read_body` call is needed
|
|
if not ngx.ctx._req_body_has_read then
|
|
read_req_body()
|
|
ngx.ctx._req_body_has_read = true
|
|
end
|
|
end
|
|
-- ]
|
|
end
|
|
end
|
|
|
|
|
|
do -- implement a Lua based shm for: cli
|
|
|
|
if options.cli and not options.rbusted then
|
|
-- ngx.shared.DICT proxy
|
|
-- https://github.com/bsm/fakengx/blob/master/fakengx.lua
|
|
-- with minor fixes and additions such as exptime
|
|
--
|
|
-- See https://github.com/openresty/resty-cli/pull/12
|
|
-- for a definitive solution of using shms in CLI
|
|
local SharedDict = {}
|
|
local function set(data, key, value, expire_at, flags)
|
|
data[key] = {
|
|
value = value,
|
|
info = {expire_at = expire_at, flags=flags}
|
|
}
|
|
return data[key]
|
|
end
|
|
local function is_stale(item)
|
|
return item.info.expire_at and item.info.expire_at <= ngx.now()
|
|
end
|
|
local function get(data, key)
|
|
local item = data[key]
|
|
if item and is_stale(item) then
|
|
item = nil
|
|
end
|
|
return item
|
|
end
|
|
function SharedDict:new()
|
|
return setmetatable({data = {}}, {__index = self})
|
|
end
|
|
function SharedDict:capacity()
|
|
return 0
|
|
end
|
|
function SharedDict:free_space()
|
|
return 0
|
|
end
|
|
function SharedDict:get(key)
|
|
local item = get(self.data, key)
|
|
if item then
|
|
return item.value, item.info.flags
|
|
end
|
|
return nil
|
|
end
|
|
function SharedDict:get_stale(key)
|
|
local item = self.data[key]
|
|
if item then
|
|
return item.value, item.info.flags, is_stale(item)
|
|
end
|
|
return nil
|
|
end
|
|
function SharedDict:set(key, value, exptime)
|
|
local expire_at = (exptime and exptime ~= 0) and (ngx.now() + exptime)
|
|
set(self.data, key, value, expire_at)
|
|
return true, nil, false
|
|
end
|
|
SharedDict.safe_set = SharedDict.set
|
|
function SharedDict:add(key, value, exptime)
|
|
if get(self.data, key) then
|
|
return false, "exists", false
|
|
end
|
|
|
|
return self:set(key, value, exptime)
|
|
end
|
|
SharedDict.safe_add = SharedDict.add
|
|
function SharedDict:replace(key, value)
|
|
if not get(key) then
|
|
return false, "not found", false
|
|
end
|
|
set(self.data, key, value)
|
|
return true, nil, false
|
|
end
|
|
function SharedDict:delete(key)
|
|
if self.data[key] ~= nil then
|
|
self.data[key] = nil
|
|
end
|
|
return true
|
|
end
|
|
function SharedDict:incr(key, value, init, init_ttl)
|
|
local item = get(self.data, key)
|
|
if not item then
|
|
if not init then
|
|
return nil, "not found"
|
|
end
|
|
item = set(self.data, key, init, init_ttl and ngx.now() + init_ttl)
|
|
elseif type(item.value) ~= "number" then
|
|
return nil, "not a number"
|
|
end
|
|
item.value = item.value + value
|
|
return item.value, nil
|
|
end
|
|
function SharedDict:flush_all()
|
|
for _, item in pairs(self.data) do
|
|
item.info.expire_at = ngx.now()
|
|
end
|
|
end
|
|
function SharedDict:flush_expired(n)
|
|
local data = self.data
|
|
local flushed = 0
|
|
|
|
for key, item in pairs(self.data) do
|
|
if is_stale(item) then
|
|
data[key] = nil
|
|
flushed = flushed + 1
|
|
if n and flushed == n then
|
|
break
|
|
end
|
|
end
|
|
end
|
|
self.data = data
|
|
return flushed
|
|
end
|
|
function SharedDict:get_keys(n)
|
|
n = n or 1024
|
|
local i = 0
|
|
local keys = {}
|
|
for k, item in pairs(self.data) do
|
|
if not is_stale(item) then
|
|
keys[#keys+1] = k
|
|
i = i + 1
|
|
if n ~= 0 and i == n then
|
|
break
|
|
end
|
|
end
|
|
end
|
|
return keys
|
|
end
|
|
function SharedDict:ttl(key)
|
|
local item = self.data[key]
|
|
if item == nil then
|
|
return nil, "not found"
|
|
end
|
|
local expire_at = item.info.expire_at
|
|
if expire_at == nil then
|
|
return 0
|
|
end
|
|
-- There is a problem that also exists in the official OpenResty:
|
|
-- 0 means the key never expires. So it's hard to distinguish between a
|
|
-- never-expired key and an expired key with a TTL value of 0.
|
|
return expire_at - ngx.now()
|
|
end
|
|
|
|
-- hack
|
|
_G.ngx.shared = setmetatable({}, {
|
|
__index = function(self, key)
|
|
local shm = rawget(self, key)
|
|
if not shm then
|
|
shm = SharedDict:new()
|
|
rawset(self, key, shm)
|
|
end
|
|
return shm
|
|
end
|
|
})
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
do -- randomseeding patch for: cli, rbusted and OpenResty
|
|
|
|
--- Seeds the random generator, use with care.
|
|
-- Once - properly - seeded, this method is replaced with a stub
|
|
-- one. This is to enforce best-practices for seeding in ngx_lua,
|
|
-- and prevents third-party modules from overriding our correct seed
|
|
-- (many modules make a wrong usage of `math.randomseed()` by calling
|
|
-- it multiple times or by not using unique seeds for Nginx workers).
|
|
--
|
|
-- This patched method will create a unique seed per worker process,
|
|
-- using a combination of both time and the worker's pid.
|
|
local get_rand_bytes = require("kong.tools.rand").get_rand_bytes
|
|
local seeded = {}
|
|
local randomseed = math.randomseed
|
|
|
|
if options.rbusted then
|
|
_G.math.native_randomseed = randomseed
|
|
end
|
|
|
|
_G.math.randomseed = function()
|
|
local pid = ngx.worker.pid()
|
|
local id
|
|
local is_seeded
|
|
local phase = ngx.get_phase()
|
|
if phase == "init" then
|
|
id = "master"
|
|
is_seeded = seeded.master
|
|
|
|
else
|
|
id = ngx.worker.id() or -1
|
|
is_seeded = seeded[pid]
|
|
end
|
|
|
|
|
|
if is_seeded then
|
|
ngx.log(ngx.DEBUG, debug.traceback("attempt to seed already seeded random number " ..
|
|
"generator on process #" .. tostring(pid), 2))
|
|
return
|
|
end
|
|
|
|
if not options.cli and (phase ~= "init_worker" and phase ~= "init") then
|
|
ngx.log(ngx.WARN, debug.traceback("math.randomseed() must be called in " ..
|
|
"init or init_worker context", 2))
|
|
end
|
|
|
|
local seed
|
|
local bytes, err = get_rand_bytes(8)
|
|
if bytes then
|
|
ngx.log(ngx.DEBUG, "seeding PRNG from OpenSSL RAND_bytes()")
|
|
|
|
local t = {}
|
|
for i = 1, #bytes do
|
|
local byte = string.byte(bytes, i)
|
|
t[#t+1] = byte
|
|
end
|
|
|
|
local str = table.concat(t)
|
|
if #str > 12 then
|
|
-- truncate the final number to prevent integer overflow,
|
|
-- since math.randomseed() could get cast to a platform-specific
|
|
-- integer with a different size and get truncated, hence, lose
|
|
-- randomness.
|
|
-- double-precision floating point should be able to represent numbers
|
|
-- without rounding with up to 15/16 digits but let's use 12 of them.
|
|
str = string.sub(str, 1, 12)
|
|
end
|
|
|
|
seed = tonumber(str)
|
|
|
|
else
|
|
ngx.log(ngx.ERR, "could not seed from OpenSSL RAND_bytes, seeding ",
|
|
"PRNG with time and process id instead (this can ",
|
|
"result to duplicated seeds): ", err)
|
|
|
|
seed = ngx.now() * 1000 + pid
|
|
end
|
|
|
|
if not options.cli then
|
|
local kong_shm = ngx.shared.kong
|
|
if id == "master" then
|
|
local worker_count = ngx.worker.count()
|
|
local old_worker_count = kong_shm:get("worker:count")
|
|
if old_worker_count and old_worker_count > worker_count then
|
|
for i = worker_count, old_worker_count - 1 do
|
|
local old_worker_pid = kong_shm:get("pids:" .. i)
|
|
if old_worker_pid then
|
|
seeded[old_worker_pid] = nil
|
|
kong_shm:delete("pids:" .. i)
|
|
kong_shm:delete("kong:mem:" .. old_worker_pid)
|
|
end
|
|
end
|
|
end
|
|
|
|
if old_worker_count ~= worker_count then
|
|
local ok, err = kong_shm:safe_set("worker:count", worker_count)
|
|
if not ok then
|
|
ngx.log(ngx.WARN, "could not store worker count in kong shm: ", err)
|
|
end
|
|
end
|
|
|
|
seeded.master = true
|
|
|
|
else
|
|
local old_worker_pid = kong_shm:get("pids:" .. id)
|
|
if old_worker_pid then
|
|
seeded[old_worker_pid] = nil
|
|
kong_shm:delete("kong:mem:" .. old_worker_pid)
|
|
end
|
|
|
|
local ok, err = kong_shm:safe_set("pids:" .. id, pid)
|
|
if not ok then
|
|
ngx.log(ngx.WARN, "could not store process id in kong shm: ", err)
|
|
end
|
|
|
|
seeded[pid] = true
|
|
end
|
|
end
|
|
|
|
return randomseed(seed)
|
|
end
|
|
end
|
|
|
|
do -- cosockets connect patch for dns resolution for: cli, rbusted and OpenResty
|
|
local sub = string.sub
|
|
|
|
local client = package.loaded["kong.resty.dns.client"]
|
|
if not client then
|
|
-- dns initialization here is essential for busted tests.
|
|
client = require("kong.tools.dns")()
|
|
end
|
|
|
|
--- Patch the TCP connect and UDP setpeername methods such that all
|
|
-- connections will be resolved first by the internal DNS resolver.
|
|
-- STEP 1: load code that should not be using the patched versions
|
|
require "resty.dns.resolver" -- will cache TCP and UDP functions
|
|
|
|
-- STEP 2: forward declaration of locals to hold stuff loaded AFTER patching
|
|
|
|
-- STEP 3: store original unpatched versions
|
|
local old_tcp = ngx.socket.tcp
|
|
local old_udp = ngx.socket.udp
|
|
|
|
local old_tcp_connect
|
|
local old_udp_setpeername
|
|
|
|
local old_ngx_log = ngx.log
|
|
|
|
-- need to do the extra check here: https://github.com/openresty/lua-nginx-module/issues/860
|
|
local function strip_nils(first, second)
|
|
if second then
|
|
return first, second
|
|
elseif first then
|
|
return first
|
|
end
|
|
end
|
|
|
|
local function resolve_connect(f, sock, host, port, opts)
|
|
if sub(host, 1, 5) ~= "unix:" then
|
|
local try_list
|
|
host, port, try_list = client.toip(host, port)
|
|
if not host then
|
|
return nil, "[cosocket] DNS resolution failed: " .. tostring(port) ..
|
|
". Tried: " .. tostring(try_list)
|
|
end
|
|
end
|
|
|
|
return f(sock, host, strip_nils(port, opts))
|
|
end
|
|
|
|
local function tcp_resolve_connect(sock, host, port, opts)
|
|
return resolve_connect(old_tcp_connect, sock, host, port, opts)
|
|
end
|
|
|
|
local function udp_resolve_setpeername(sock, host, port)
|
|
return resolve_connect(old_udp_setpeername, sock, host, port)
|
|
end
|
|
|
|
-- STEP 4: patch globals
|
|
_G.ngx.socket.tcp = function(...)
|
|
local sock = old_tcp(...)
|
|
|
|
if not old_tcp_connect then
|
|
old_tcp_connect = sock.connect
|
|
end
|
|
|
|
sock.connect = tcp_resolve_connect
|
|
|
|
return sock
|
|
end
|
|
|
|
_G.ngx.socket.udp = function(...)
|
|
local sock = old_udp(...)
|
|
|
|
if not old_udp_setpeername then
|
|
old_udp_setpeername = sock.setpeername
|
|
end
|
|
|
|
sock.setpeername = udp_resolve_setpeername
|
|
|
|
return sock
|
|
end
|
|
|
|
-- OTel-formatted logs feature
|
|
local dynamic_hook = require "kong.dynamic_hook"
|
|
local hook_called = false
|
|
_G.ngx.log = function(...)
|
|
if hook_called then
|
|
-- detect recursive loops or yielding from the hook:
|
|
old_ngx_log(ngx.ERR, debug.traceback("concurrent execution detected for: ngx.log", 2))
|
|
return old_ngx_log(...)
|
|
end
|
|
|
|
-- stack level = 5:
|
|
-- 1: maybe_push
|
|
-- 2: dynamic_hook.pcall
|
|
-- 3: dynamic_hook.run_hook
|
|
-- 4: patched function
|
|
-- 5: caller
|
|
hook_called = true
|
|
dynamic_hook.run_hook("observability_logs", "push", 5, nil, ...)
|
|
hook_called = false
|
|
return old_ngx_log(...)
|
|
end
|
|
-- export native ngx.log to be used where
|
|
-- the patched code must not be executed
|
|
_G.native_ngx_log = old_ngx_log
|
|
|
|
if not options.cli and not options.rbusted then
|
|
local timing = require "kong.timing"
|
|
timing.register_hooks()
|
|
end
|
|
|
|
-- STEP 5: load code that should be using the patched versions, if any (because of dependency chain)
|
|
do
|
|
-- dns query patch
|
|
local instrumentation = require "kong.observability.tracing.instrumentation"
|
|
client.toip = instrumentation.get_wrapped_dns_query(client.toip)
|
|
|
|
-- patch request_uri to record http_client spans
|
|
instrumentation.http_client()
|
|
end
|
|
end
|
|
|
|
require "kong.deprecation".init(options.cli)
|
|
end
|