Files
kong--kong/kong/plugins/acme/handler.lua
T
2026-07-13 12:32:21 +08:00

258 lines
6.6 KiB
Lua

local kong_certificate = require "kong.runloop.certificate"
local client = require "kong.plugins.acme.client"
local ngx_ssl = require "ngx.ssl"
local kong_meta = require "kong.meta"
local ipairs = ipairs
local setmetatable = setmetatable
local string_sub = string.sub
local string_gsub = string.gsub
local string_find = string.find
local string_lower = string.lower
local table_insert = table.insert
local table_concat = table.concat
local ngx_timer_at = ngx.timer.at
local ngx_re_match = ngx.re.match
local server_name = ngx_ssl.server_name
local acme_challenge_path = [[^/\.well-known/acme-challenge/(.+)]]
-- cache for dummy cert kong generated (it's a table)
local default_cert_key
local ACMEHandler = {}
-- this has to be higher than auth plugins,
-- otherwise acme-challenges endpoints may be blocked by auth plugins
-- causing validation failures
ACMEHandler.VERSION = kong_meta.version
ACMEHandler.PRIORITY = 1705
local function build_domain_matcher(domains)
local domains_plain = {}
local domains_wildcard = {}
local domains_wildcard_count = 0
if domains == nil or domains == ngx.null then
return false
end
for _, d in ipairs(domains) do
if string_sub(d, 1, 1) == "*" then
d = string_gsub(string_sub(d, 2), "%.", "\\.")
table_insert(domains_wildcard, d)
domains_wildcard_count = domains_wildcard_count + 1
else
domains_plain[d] = true
end
end
local domains_pattern
if domains_wildcard_count > 0 then
domains_pattern = "(" .. table_concat(domains_wildcard, "|") .. ")$"
end
return setmetatable(domains_plain, {
__index = function(_, k)
if not domains_pattern then
return false
end
return ngx_re_match(k, domains_pattern, "jo")
end
})
end
-- cache the domains_matcher. ACME is a global plugin.
local domains_matcher
-- expose it for use in api.lua
ACMEHandler.build_domain_matcher = build_domain_matcher
local CONFIG
local function renew(premature)
if premature or not CONFIG then
return
end
client.renew_certificate(CONFIG)
end
ACMEHandler.renew = renew
function ACMEHandler:init_worker()
local worker_id = ngx.worker.id() or -1
kong.log.info("acme renew timer started on worker ", worker_id)
ngx.timer.every(86400, renew)
end
function ACMEHandler:configure(configs)
CONFIG = configs and configs[1] or nil
if CONFIG then
domains_matcher = build_domain_matcher(CONFIG.domains)
end
end
local function check_domains(conf, host)
if not conf.enable_ipv4_common_name and string_find(host, "^(%d+)%.(%d+)%.(%d+)%.(%d+)$") then
return false
end
if conf.allow_any_domain then
return true
end
-- create the cache at first usage
if domains_matcher == nil then
domains_matcher = build_domain_matcher(conf.domains)
end
return domains_matcher and domains_matcher[host]
end
function ACMEHandler:certificate(conf)
-- we can't check for Host header in this phase
local host, err = server_name()
if err then
kong.log.warn("failed to read SNI server name: ", err)
return
elseif not host then
kong.log.debug("ignoring because no SNI provided by client")
return
end
host = string_lower(host)
if not check_domains(conf, host) then
kong.log.debug("ignoring because domain is not in allowed-list: ", host)
return
end
local cert_and_key, err = kong_certificate.find_certificate(host)
if err then
kong.log.err("error find certificate for current request:", err)
return
end
if not default_cert_key then
-- hack: find_certificate() returns default cert and key if no sni defined
default_cert_key = kong_certificate.find_certificate()
end
-- note we compare the table address, this relies on the fact that Kong doesn't
-- copy the default cert table around
if cert_and_key ~= default_cert_key then
kong.log.debug("ignoring because non-default cert is served")
return
end
local certkey, err = client.load_certkey_cached(conf, host)
if err then
kong.log.warn("can't load cert and key from storage: ", err)
return
end
-- cert not found, get a new one and serve default cert for now
if not certkey then
if kong.configuration.role == "data_plane" and conf.storage == "kong" then
kong.log.err("creating new certificate through proxy side with ",
"\"kong\" storage in Hybrid mode is not supported; ",
"consider create certificate using Admin API or ",
"use other external storages")
return
end
ngx_timer_at(0, function()
local ok, err = client.update_certificate(conf, host, nil)
if err then
kong.log.err("failed to update certificate for host: ", host, " err:", err)
return
end
-- if not ok and err is nil, meaning the update is running by another worker
if ok then
err = client.store_renew_config(conf, host)
if err then
kong.log.err("failed to store renew config for host: ", host, " err:", err)
return
end
end
end)
return
end
-- this will only be run in dbless
kong.log.debug("set certificate for host: ", host)
local _, err
_, err = ngx_ssl.clear_certs()
if err then
kong.log.warn("failed to clear certs: ", err)
end
_, err = ngx_ssl.set_der_cert(certkey.cert)
if err then
kong.log.warn("failed to set cert: ", err)
end
_, err = ngx_ssl.set_der_priv_key(certkey.key)
if err then
kong.log.warn("failed to set key: ", err)
end
end
-- access phase is to terminate the http-01 challenge request if necessary
function ACMEHandler:access(conf)
local protocol = kong.client.get_protocol()
-- http-01 challenge only sends to http port
if protocol == 'http' then
local host = kong.request.get_host()
if not host then
return
end
if not check_domains(conf, host) then
-- We do not log here because it would flood the log
return
end
local captures, err =
ngx_re_match(kong.request.get_path(), acme_challenge_path, "jo")
if err then
kong.log(kong.WARN, "error matching acme-challenge uri: ", err)
return
end
if captures then
-- if this is just a sanity test, we always return 404 status
if captures[1] == "x" then
return kong.response.exit(404, "Not found\n")
end
-- TODO: race condition creating account?
local err = client.create_account(conf)
if err then
kong.log.err("failed to create account:", err)
return
end
local acme_client, err = client.new(conf)
if err then
kong.log.err("failed to create acme client:", err)
return
end
acme_client:serve_http_challenge()
end
return
end
end
return ACMEHandler