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

191 lines
6.2 KiB
Lua

local client = require "kong.plugins.acme.client"
local handler = require "kong.plugins.acme.handler"
local http = require "resty.http"
local x509 = require "resty.openssl.x509"
local type = type
local ipairs = ipairs
local os_date = os.date
local string_format = string.format
local string_find = string.find
local string_byte = string.byte
local string_sub = string.sub
local ngx_time = ngx.time
local ngx_timer_at = ngx.timer.at
local ngx_re_match = ngx.re.match
local function find_plugin()
for plugin, err in kong.db.plugins:each(1000) do
if err then
return nil, err
end
if plugin.name == "acme" then
return plugin
end
end
end
local function to_hex(s)
s = s:gsub("(.)", function(s) return string_format("%02X:", string_byte(s)) end)
-- strip last ":"
return string_sub(s, 1, -2)
end
local function bn_to_hex(bn)
local s = bn:to_hex():gsub("(..)", function (s) return s..":" end)
-- strip last ":"
return string_sub(s, 1, -2)
end
local function parse_certkey(certkey)
local cert = x509.new(certkey.cert)
local key = cert:get_pubkey()
local subject_name = cert:get_subject_name()
local host = subject_name:find("CN")
local issuer_name = cert:get_issuer_name()
local issuer_cn = issuer_name:find("CN")
local not_before = cert:get_not_before()
local not_after = cert:get_not_after()
local time = ngx_time()
return {
digest = to_hex(cert:digest()),
host = host.blob,
issuer_cn = issuer_cn.blob,
not_before = os_date("%Y-%m-%d %H:%M:%S", not_before),
not_after = os_date("%Y-%m-%d %H:%M:%S", not_after),
valid = not_before < time and not_after > time,
serial_number = bn_to_hex(cert:get_serial_number()),
pubkey_type = key:get_key_type().sn,
}
end
return {
["/acme"] = {
POST = function(self)
local plugin, err = find_plugin()
if err then
return kong.response.exit(500, { message = err })
elseif not plugin then
return kong.response.exit(404)
end
local conf = plugin.config
local host = self.params.host
if not host or type(host) ~= "string" then
return kong.response.exit(400, { message = "host must be provided and containing a single domain" })
end
-- we don't allow port for security reason in test_only mode
if string_find(host, ":", 1, true) ~= nil then
return kong.response.exit(400, { message = "port is not allowed in host" })
end
-- string "true" automatically becomes boolean true from lapis
if self.params.test_http_challenge_flow == true then
local domains_matcher = handler.build_domain_matcher(conf.domains)
if not domains_matcher or not domains_matcher[host] then
return kong.response.exit(400, { message = "problem found running sanity check for " .. host ..
": host is not included in plugin config.domains"})
end
local check_path = string_format("http://%s/.well-known/acme-challenge/", host)
local httpc = http.new()
local res, err = httpc:request_uri(check_path .. "x")
if not err then
if ngx_re_match(res.body, "no Route matched with those values") then
err = check_path .. "* doesn't map to a Route in Kong; " ..
"please refer to docs on how to create dummy Route and Service"
elseif res.body ~= "Not found\n" then
err = "unexpected response: \"" .. (res.body or "<nil>") .. "\""
if res.status ~= 404 then
err = err .. string_format(", unexpected status code: %d", res.status)
end
else
return kong.response.exit(200, { message = "sanity test for host " .. host .. " passed"})
end
end
return kong.response.exit(400, { message = "problem found running sanity check for " .. host .. ": " .. err})
end
local _, err = client.update_certificate(conf, host, nil)
if err then
return kong.response.exit(500, { message = "failed to update certificate: " .. err })
end
err = client.store_renew_config(conf, host)
if err then
return kong.response.exit(500, { message = "failed to store renew config: " .. err })
end
local msg = "certificate for host " .. host .. " is created"
return kong.response.exit(201, { message = msg })
end,
PATCH = function()
ngx_timer_at(0, handler.renew)
return kong.response.exit(202, { message = "Renewal process started successfully" })
end,
},
["/acme/certificates"] = {
GET = function(self)
local plugin, err = find_plugin()
if err then
return kong.response.exit(500, { message = err })
elseif not plugin then
return kong.response.exit(404)
end
local conf = plugin.config
local renew_hosts, err = client.load_renew_hosts(conf)
if err then
return kong.response.exit(500, { message = err })
end
local data = {}
local idx = 1
for i, host in ipairs(renew_hosts) do
local certkey, err = client.load_certkey(conf, host)
if err then
return kong.response.exit(500, { message = err })
end
if not certkey then
kong.log.warn("[acme]", host, " is defined in renew_config but its cert and key is missing")
else
certkey = parse_certkey(certkey)
if not self.params.invalid_only or not certkey.valid then
data[idx] = certkey
idx = idx + 1
end
end
end
return kong.response.exit(200, { data = data })
end,
},
["/acme/certificates/:certificates"] = {
GET = function(self)
local plugin, err = find_plugin()
if err then
return kong.response.exit(500, { message = err })
elseif not plugin then
return kong.response.exit(404)
end
local conf = plugin.config
local host = self.params.certificates
local certkey, err = client.load_certkey(conf, host)
if err then
return kong.response.exit(500, { message = err })
end
if not certkey then
return kong.response.exit(404, { message = "Certificate for host " .. host .. "not found in storage" })
end
return kong.response.exit(200, { data = parse_certkey(certkey) })
end,
},
}