-
Notifications
You must be signed in to change notification settings - Fork 41
feat: support dns-01 challenge #114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 14 commits
3043b4a
9cf45c3
b1573f3
8ba73f7
b21b6bc
7e50a86
ed63b1c
61d3256
75511ed
0a6fe92
42abe99
5acab9f
63313a2
c10638b
4316537
459b845
8c87956
9717f73
fd8fcc9
6e04625
e5d0060
bbf07e6
0f88c8d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
local util = require("resty.acme.util") | ||
local digest = require("resty.openssl.digest") | ||
local base64 = require("ngx.base64") | ||
local cjson = require("cjson") | ||
local log = util.log | ||
local encode_base64url = base64.encode_base64url | ||
|
||
local _M = {} | ||
local mt = {__index = _M} | ||
|
||
function _M.new(storage) | ||
local self = setmetatable({ | ||
storage = storage, | ||
-- domain_used_dnsapi_key_detail = { | ||
-- ["*.domain.com"] = { | ||
-- provider = "cloudflare", | ||
-- content = "token" | ||
-- }, | ||
-- ["www.domain.com"] = { | ||
-- provider = "dynv6", | ||
-- content = "token" | ||
-- } | ||
-- } | ||
domain_used_dnsapi_key_detail = {} | ||
}, mt) | ||
return self | ||
end | ||
|
||
local function calculate_txt_record(keyauthorization) | ||
local dgst = assert(digest.new("sha256"):final(keyauthorization)) | ||
local txt_record = encode_base64url(dgst) | ||
log(ngx.DEBUG, "calculate txt record: ", txt_record) | ||
return txt_record | ||
end | ||
|
||
local function ch_key(challenge) | ||
return challenge .. "#dns-01" | ||
end | ||
|
||
local function choose_dnsapi(self, domain) | ||
if not self.domain_used_dnsapi_key_detail[domain] then | ||
return nil, "not dnsapi key for domain" | ||
end | ||
local provider = self.domain_used_dnsapi_key_detail[domain].provider | ||
if not provider then | ||
return nil, "dnsapi provider not support" | ||
end | ||
log(ngx.DEBUG, "use dnsapi provider: ", provider) | ||
local content = self.domain_used_dnsapi_key_detail[domain].content | ||
if not content or content == "" then | ||
return nil, "dnsapi key content is empty" | ||
end | ||
local ok, module = pcall(require, "resty.acme.dnsapi." .. provider) | ||
if ok then | ||
local handler, err = module.new(content) | ||
if not err then | ||
return handler | ||
end | ||
end | ||
return nil, "require dnsapi error: " .. provider | ||
end | ||
|
||
function _M:update_dnsapi_info(domain_used_dnsapi_key_detail) | ||
log(ngx.INFO, "update_dnsapi_info: " .. cjson.encode(domain_used_dnsapi_key_detail)) | ||
self.domain_used_dnsapi_key_detail = domain_used_dnsapi_key_detail | ||
end | ||
|
||
function _M:register_challenge(_, response, domains) | ||
local dnsapi, err | ||
for _, domain in ipairs(domains) do | ||
err = self.storage:set(ch_key(domain), response, 3600) | ||
if err then | ||
return err | ||
end | ||
dnsapi, err = choose_dnsapi(self, domain) | ||
if err then | ||
return err | ||
end | ||
local trim_domain = domain:gsub("*.", "") | ||
local txt_record = calculate_txt_record(response) | ||
local result, err = dnsapi:post_txt_record("_acme-challenge." .. trim_domain, txt_record) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we might want to wait the dns record to propogate after we set the record. use openresty/lua-resty-dns to query the record would be good to avoid the acme server try to validate it prematurely. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe we can handle it in |
||
if err then | ||
return err | ||
end | ||
log(ngx.INFO, "dnsapi post_txt_record returns: ", result) | ||
end | ||
end | ||
|
||
function _M:cleanup_challenge(_--[[challenge]], domains) | ||
local dnsapi, err | ||
for _, domain in ipairs(domains) do | ||
err = self.storage:delete(ch_key(domain)) | ||
if err then | ||
return err | ||
end | ||
dnsapi, err = choose_dnsapi(self, domain) | ||
if err then | ||
return err | ||
end | ||
local trim_domain = domain:gsub("*.", "") | ||
local result, err = dnsapi:delete_txt_record("_acme-challenge." .. trim_domain) | ||
if err then | ||
return err | ||
end | ||
log(ngx.INFO, "dnsapi delete_txt_record returns: ", result) | ||
end | ||
end | ||
|
||
return _M |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
local http = require("resty.http") | ||
local cjson = require("cjson") | ||
|
||
local _M = {} | ||
local mt = {__index = _M} | ||
|
||
function _M.new(token) | ||
if not token or token == "" then | ||
return nil, "api token is needed" | ||
end | ||
|
||
local self = setmetatable({ | ||
endpoint = "https://api.cloudflare.com/client/v4", | ||
httpc = nil, | ||
token = token, | ||
zone = nil, | ||
headers = { | ||
["Content-Type"] = "application/json", | ||
["Authorization"] = "Bearer " .. token, | ||
} | ||
}, mt) | ||
|
||
self.httpc = http.new() | ||
return self | ||
end | ||
|
||
function _M:get_zone_id(fqdn) | ||
local url = self.endpoint .. "/zones" | ||
local resp, err = self.httpc:request_uri(url, | ||
{ | ||
method = "GET", | ||
headers = self.headers | ||
} | ||
) | ||
if err then | ||
return nil, err | ||
end | ||
|
||
if resp and resp.status == 200 then | ||
-- { | ||
-- "result": | ||
-- [{ | ||
-- "id":"12345abcde", | ||
-- "name":"domain.com", | ||
-- ... | ||
-- }], | ||
-- "result_info":{"page":1,"per_page":20,"total_pages":1,"count":1,"total_count":1}, | ||
-- "success":true, | ||
-- "errors":[], | ||
-- "messages":[] | ||
-- } | ||
local body = cjson.decode(resp.body) | ||
if not body then | ||
return nil, "json decode error" | ||
end | ||
|
||
for _, zone in ipairs(body.result) do | ||
local start, _, err = fqdn:find(zone.name, 1, true) | ||
if err then | ||
return nil, err | ||
end | ||
if start then | ||
self.zone = zone.name | ||
return zone.id | ||
end | ||
end | ||
end | ||
|
||
return nil, "no matched dns zone found" | ||
end | ||
|
||
function _M:post_txt_record(fqdn, content) | ||
local zone_id, err = self:get_zone_id(fqdn) | ||
if err then | ||
return nil, err | ||
end | ||
local url = self.endpoint .. "/zones/" .. zone_id .. "/dns_records" | ||
local body = { | ||
["type"] = "TXT", | ||
["name"] = fqdn, | ||
["content"] = content | ||
} | ||
local resp, err = self.httpc:request_uri(url, | ||
{ | ||
method = "POST", | ||
headers = self.headers, | ||
body = cjson.encode(body) | ||
} | ||
) | ||
if err then | ||
return nil, err | ||
end | ||
|
||
return resp.status | ||
end | ||
|
||
function _M:get_record_id(zone_id, fqdn) | ||
local url = self.endpoint .. "/zones/" .. zone_id .. "/dns_records" | ||
local resp, err = self.httpc:request_uri(url, | ||
{ | ||
method = "GET", | ||
headers = self.headers | ||
} | ||
) | ||
if err then | ||
return nil, err | ||
end | ||
|
||
if resp and resp.status == 200 then | ||
-- { | ||
-- "result": | ||
-- [{ | ||
-- "id":"12345abcdefghti", | ||
-- "zone_id":"12345abcde", | ||
-- "zone_name":"domain.com", | ||
-- "name":"_acme-challenge.domain.com", | ||
-- "type":"TXT", | ||
-- "content":"record_content", | ||
-- ... | ||
-- }], | ||
-- "success":true, | ||
-- "errors":[], | ||
-- "messages":[], | ||
-- "result_info":{"page":1,"per_page":100,"count":1,"total_count":1,"total_pages":1} | ||
-- } | ||
local body = cjson.decode(resp.body) | ||
if not body then | ||
return nil, "json decode error" | ||
end | ||
|
||
for _, record in ipairs(body.result) do | ||
if fqdn == record.name then | ||
return record.id | ||
end | ||
end | ||
end | ||
|
||
return nil, "no matched dns record found" | ||
end | ||
|
||
function _M:delete_txt_record(fqdn) | ||
local zone_id, err = self:get_zone_id(fqdn) | ||
if err then | ||
return nil, err | ||
end | ||
local record_id, err = self:get_record_id(zone_id, fqdn) | ||
if err then | ||
return nil, err | ||
end | ||
local url = self.endpoint .. "/zones/" .. zone_id .. "/dns_records/" .. record_id | ||
local resp, err = self.httpc:request_uri(url, | ||
{ | ||
method = "DELETE", | ||
headers = self.headers | ||
} | ||
) | ||
if err then | ||
return nil, err | ||
end | ||
|
||
-- return 200 ok | ||
return resp.status | ||
end | ||
|
||
return _M |
Uh oh!
There was an error while loading. Please reload this page.