kong/kong/globalpatches.lua (344 lines of code) (raw):

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" require("cjson.safe").encode_sparse_array(nil, nil, 2^15) 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 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() 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 -- implement a Lua based shm for: cli (and hence rbusted) if options.cli 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) data[key] = { value = value, info = {expire_at = expire_at} } 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) return self.data[key] and self.data[key].value, nil end SharedDict.get_stale = SharedDict.get function SharedDict:set(key, value) set(self.data, key, value) return true, nil, false end SharedDict.safe_set = SharedDict.set function SharedDict:add(key, value, exptime) if self.data[key] ~= nil then return false, "exists", false end local expire_at = nil if exptime then ngx.timer.at(exptime, function() self.data[key] = nil end) expire_at = ngx.now() + exptime end set(self.data, key, value, expire_at) return true, nil, false end SharedDict.safe_add = SharedDict.add function SharedDict:replace(key, value) if self.data[key] == nil 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) if not self.data[key] then if not init then return nil, "not found" else self.data[key] = { value = init, info = {} } if init_ttl then self.data[key].info.expire_at = ngx.now() + init_ttl ngx.timer.at(init_ttl, function() self.data[key] = nil end) end end elseif type(self.data[key].value) ~= "number" then return nil, "not a number" end self.data[key].value = self.data[key].value + value return self.data[key].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 item.info.expire_at and item.info.expire_at <= ngx.now() 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 in pairs(self.data) do keys[#keys+1] = k i = i + 1 if n ~= 0 and i == n then break end end return keys end function SharedDict:ttl(key) local item = self.data[key] if item == nil then return nil, "not found" else local expire_at = item.info.expire_at if expire_at == nil then return 0 else local remaining = expire_at - ngx.now() if remaining < 0 then return nil, "not found" else return remaining end end end 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 util = require "kong.tools.utils" local seeded = {} local randomseed = math.randomseed _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() 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 = util.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 --- 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 local toip -- 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 -- 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 = 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 -- STEP 5: load code that should be using the patched versions, if any (because of dependency chain) do local client = package.loaded["kong.resty.dns.client"] if not client then client = require("kong.tools.dns")() end toip = client.toip -- DNS query is lazily patched, it will only be wrapped -- when instrumentation module is initialized later and -- `opentelemetry_tracing` includes "dns_query" or set -- to "all". local instrumentation = require "kong.tracing.instrumentation" instrumentation.set_patch_dns_query_fn(toip, function(wrap) toip = wrap end) end end require "kong.deprecation".init(options.cli) end