* Add DNSMint DNS API
DNSMint mints hostnames on domains it operates and serves from its own
authoritative nameservers, so a customer has no zone of their own and the
challenge is published through its API.
The hostname is derived server-side from the challenge name, so there is
no root zone to detect and no record id to track: the value published is
the value removed. Wildcards work because the API keeps the two newest
values for a name, which is what the apex and wildcard pair needs.
Tested against Let's Encrypt staging for a wildcard plus its apex.
* Add DNSMint DNS API
DNSMint mints hostnames on domains it operates and serves from its own
authoritative nameservers, so records are published through its API
rather than a zone you run.
An ACME challenge goes to the DNS-01 endpoint, which derives the hostname
from the challenge name: no root zone to detect, no record id to track,
and the value published is the value removed. Wildcards work because the
API keeps the two newest values for a name.
Any other TXT name is an ordinary record under a hostname and goes to the
records endpoint instead, which is why the add and rm functions branch on
the _acme-challenge label. Issuing certificates needs only the dns01:write
scope; the record endpoint needs hostnames:read and hostnames:write.
Tested against Let's Encrypt staging for a wildcard plus its apex, and
the non-challenge TXT path against the live API.
* dnsmint: honour DNSMint_Api instead of overwriting it
The assignment was unconditional, so exporting DNSMint_Api did nothing - the
plugin reset it to the default every time it was sourced. Every neighbouring
plugin with an _Api variable treats it as an override; this one only looked
like it did.
Found while writing the dnsapi2 entry: the sentence documenting the override
would have been false.
* dnsmint: format case blocks with shfmt -i 2
* dnsmint: portable record lookup on removal
grep -F is not on Solaris /usr/bin/grep, and "\n" in a sed replacement is a
GNU extension that BSD sed writes as a literal n. The second is the one that
loses data: without the split the whole reply stays on one line, so the match
succeeds whatever the value is and the id taken is the first record under the
hostname rather than the one holding the challenge. Removal then deletes
somebody else's TXT record.
Literal newline in the replacement, as dns_glesys.sh does it, and a case
pattern per line with the case outside a command substitution.
Docs and Issues now point at dnsapi2 and at the tracking issue upstream.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* dnsmint: echo into sed, and keep _head_n 1 on the id
Both from review on #7238.
The reply arrives with no trailing newline, so printf "%s" hands sed an
incomplete final line. Solaris /usr/bin/sed discards one, and here that line
is the whole reply: the loop then sees nothing and every removal reports the
record already gone, leaving the challenge TXT behind. echo, as lines 154 and
172 of this file already do.
_head_n 1 was dropped in 61ef816c. A record object carrying a nested id under
"data" makes _rid two lines and the DELETE URL is then malformed.
---------
Co-authored-by: kxbnb <hello@dnsmint.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
244 lines
6.6 KiB
Bash
244 lines
6.6 KiB
Bash
#!/usr/bin/env sh
|
|
# shellcheck disable=SC2034
|
|
dns_dnsmint_info='DNSMint.com
|
|
DNSMint mints hostnames on domains it operates and serves from its own
|
|
authoritative nameservers, so records are published through its API rather
|
|
than a zone you run.
|
|
Site: dnsmint.com
|
|
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_dnsmint
|
|
Options:
|
|
DNSMINT_API_KEY API key. dns01:write is enough to issue certificates.
|
|
Issues: github.com/acmesh-official/acme.sh/issues/7251
|
|
Author: DNSMint
|
|
'
|
|
|
|
DNSMint_Api="${DNSMint_Api:-https://dnsmint.com/api}"
|
|
|
|
######## Public functions #####################
|
|
|
|
#Usage: dns_dnsmint_add _acme-challenge.q7k4m2.example.dev "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
|
dns_dnsmint_add() {
|
|
fulldomain=$1
|
|
txtvalue=$2
|
|
|
|
_info "Using DNSMint"
|
|
_debug fulldomain "$fulldomain"
|
|
_debug txtvalue "$txtvalue"
|
|
|
|
if ! _dnsmint_key; then
|
|
return 1
|
|
fi
|
|
|
|
# An ACME challenge goes to the DNS-01 endpoint, which derives the hostname
|
|
# itself and needs only dns01:write. Any other name is an ordinary record
|
|
# under a hostname, which is a different endpoint and a wider scope.
|
|
if _startswith "$fulldomain" "_acme-challenge."; then
|
|
if _dnsmint_challenge present "$fulldomain" "$txtvalue"; then
|
|
_info "Added, OK"
|
|
return 0
|
|
fi
|
|
return 1
|
|
fi
|
|
|
|
if _dnsmint_record_add "$fulldomain" "$txtvalue"; then
|
|
_info "Added, OK"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
#Usage: dns_dnsmint_rm _acme-challenge.q7k4m2.example.dev "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
|
dns_dnsmint_rm() {
|
|
fulldomain=$1
|
|
txtvalue=$2
|
|
|
|
_info "Using DNSMint"
|
|
_debug fulldomain "$fulldomain"
|
|
_debug txtvalue "$txtvalue"
|
|
|
|
if ! _dnsmint_key; then
|
|
return 1
|
|
fi
|
|
|
|
if _startswith "$fulldomain" "_acme-challenge."; then
|
|
if _dnsmint_challenge cleanup "$fulldomain" "$txtvalue"; then
|
|
_info "Removed, OK"
|
|
return 0
|
|
fi
|
|
return 1
|
|
fi
|
|
|
|
if _dnsmint_record_rm "$fulldomain" "$txtvalue"; then
|
|
_info "Removed, OK"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
#################### Private functions below ##################################
|
|
|
|
_dnsmint_key() {
|
|
DNSMINT_API_KEY="${DNSMINT_API_KEY:-$(_readaccountconf_mutable DNSMINT_API_KEY)}"
|
|
if [ -z "$DNSMINT_API_KEY" ]; then
|
|
DNSMINT_API_KEY=""
|
|
_err "You did not specify DNSMINT_API_KEY yet."
|
|
_err "Create a key with the dns01:write scope at https://dnsmint.com/dashboard"
|
|
_err "e.g."
|
|
_err "export DNSMINT_API_KEY=dnsm_xxxxxxxxxxxx_xxxxxxxx"
|
|
return 1
|
|
fi
|
|
_saveaccountconf_mutable DNSMINT_API_KEY "$DNSMINT_API_KEY"
|
|
return 0
|
|
}
|
|
|
|
_dnsmint_headers() {
|
|
export _H1="Authorization: Bearer $DNSMINT_API_KEY"
|
|
export _H2="Accept: application/json"
|
|
export _H3="Content-Type: application/json"
|
|
}
|
|
|
|
# One request. Sets $response and $_code; returns non-zero on a transport error.
|
|
_dnsmint_rest() {
|
|
_m="$1"
|
|
_ep="$2"
|
|
_data="$3"
|
|
|
|
_dnsmint_headers
|
|
if [ "$_m" = "GET" ]; then
|
|
response="$(_get "$DNSMint_Api$_ep")"
|
|
else
|
|
_secure_debug2 _data "$_data"
|
|
response="$(_post "$_data" "$DNSMint_Api$_ep" "" "$_m")"
|
|
fi
|
|
_ret="$?"
|
|
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
|
|
_debug "http response code $_code"
|
|
_debug2 response "$response"
|
|
if [ "$_ret" != "0" ]; then
|
|
_err "error $_ep"
|
|
return 1
|
|
fi
|
|
case "$_code" in
|
|
2*) return 0 ;;
|
|
*)
|
|
# The API says why in the body - a key narrowed to another hostname, a
|
|
# name that is not live - and that is more use than the status alone.
|
|
_err "error $_ep: HTTP $_code $response"
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# The DNS-01 endpoint. It derives the hostname from the challenge name, so
|
|
# there is no zone to look up and no record id to track: the value published
|
|
# is the value removed.
|
|
_dnsmint_challenge() {
|
|
_action="$1"
|
|
_fqdn="$2"
|
|
_value="$3"
|
|
_dnsmint_rest POST "/httpreq/$_action" "{\"fqdn\":\"$_fqdn\",\"value\":\"$_value\"}"
|
|
}
|
|
|
|
# Everything below here is for names that are not ACME challenges. A record
|
|
# under a hostname is addressed by the hostname's id and a name relative to
|
|
# it, so the hostname has to be found first.
|
|
_dnsmint_host() {
|
|
_name="$1"
|
|
_host_id=""
|
|
_host_sub=""
|
|
|
|
if ! _dnsmint_rest GET "/v1/hostnames?limit=500"; then
|
|
return 1
|
|
fi
|
|
|
|
for _h in $(echo "$response" | _egrep_o '"hostname":"[^"]*"' | cut -d'"' -f4); do
|
|
case "$_name" in
|
|
*".$_h")
|
|
# Longest suffix wins, so a.b.example.dev prefers b.example.dev over
|
|
# example.dev when both are hostnames on the account.
|
|
if [ "${#_h}" -gt "${#_host_sub}" ]; then
|
|
_host_sub="$_h"
|
|
fi
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [ -z "$_host_sub" ]; then
|
|
_err "$_name is not under a hostname on this account"
|
|
return 1
|
|
fi
|
|
|
|
# The id sits next to the hostname in the same object.
|
|
_host_id="$(echo "$response" | _egrep_o "\"id\":\"[^\"]*\",\"hostname\":\"$_host_sub\"" | cut -d'"' -f4)"
|
|
if [ -z "$_host_id" ]; then
|
|
_err "could not read the id for $_host_sub"
|
|
return 1
|
|
fi
|
|
|
|
_record_name="${_name%".$_host_sub"}"
|
|
_debug _host_sub "$_host_sub"
|
|
_debug _record_name "$_record_name"
|
|
return 0
|
|
}
|
|
|
|
_dnsmint_record_add() {
|
|
_name="$1"
|
|
_value="$2"
|
|
|
|
if ! _dnsmint_host "$_name"; then
|
|
return 1
|
|
fi
|
|
_dnsmint_rest POST "/v1/hostnames/$_host_id/records" \
|
|
"{\"name\":\"$_record_name\",\"type\":\"TXT\",\"text\":\"$_value\"}"
|
|
}
|
|
|
|
_dnsmint_record_rm() {
|
|
_name="$1"
|
|
_value="$2"
|
|
|
|
if ! _dnsmint_host "$_name"; then
|
|
return 1
|
|
fi
|
|
if ! _dnsmint_rest GET "/v1/hostnames/$_host_id/records"; then
|
|
return 1
|
|
fi
|
|
|
|
# Records come back as {"id":...,"name":"<fqdn>","type":"TXT","ttl":...,
|
|
# "data":{...,"text":["<value>"]}}. Match on the value so a name holding
|
|
# several TXT records loses only the one that was added.
|
|
#
|
|
# The replacement carries a literal newline: "\n" there is a GNU extension
|
|
# and BSD sed inserts the letter n, which would leave the whole reply on one
|
|
# line and match the first record under the hostname whatever its value.
|
|
#
|
|
# echo rather than printf "%s": the reply arrives with no trailing newline,
|
|
# and Solaris /usr/bin/sed discards an incomplete final line. Here that line
|
|
# is the entire reply, so every removal would report the record already gone.
|
|
_records="$(
|
|
echo "$response" | sed 's/},{/}\
|
|
{/g'
|
|
)"
|
|
|
|
_rid=""
|
|
while IFS= read -r _line; do
|
|
case "$_line" in
|
|
*"\"$_value\""*)
|
|
# _head_n 1 because a record object may carry a nested id under "data",
|
|
# and two lines in _rid would break the DELETE URL.
|
|
_rid="$(echo "$_line" | _egrep_o '"id":"[^"]*"' | cut -d'"' -f4 | _head_n 1)"
|
|
if [ -n "$_rid" ]; then
|
|
break
|
|
fi
|
|
;;
|
|
esac
|
|
done <<EOF
|
|
$_records
|
|
EOF
|
|
|
|
if [ -z "$_rid" ]; then
|
|
_info "Record already gone, nothing to remove"
|
|
return 0
|
|
fi
|
|
_dnsmint_rest DELETE "/v1/hostnames/$_host_id/records/$_rid" ""
|
|
}
|