Compare commits

...
13 Commits
Author SHA1 Message Date
844ab257b2 Add DNS API for ConnByte (#7343)
* Add DNS API for ConnByte

Adds dns_connbyte for the ConnByte DNS API (https://api.connbyte.com).
Docs: https://connbyte.com/

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* dns_connbyte: split JSON objects with tr for illumos egrep

* dns_connbyte: avoid grep -F for Solaris compatibility

* dns_connbyte: always save CONNBYTE_TTL, use @ for the zone apex

Saving the TTL unconditionally lets a user go back to the default 120;
before, an older saved value stayed in account.conf.

The ConnByte API rejects an empty record name and uses "@" for the
zone apex, which is needed in DNS alias mode.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

---------

Co-authored-by: ConnByte <team@connbyte.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-22 21:16:44 +08:00
abolfazl rajabpour 37b26c6e2c Fix dns_arvan zone lookup, TXT cleanup and HTTP errors (#6789)
* Fix Arvan DNS zone lookup and TXT cleanup

* Fix Arvan matching with native Solaris tools
2026-10-22 14:59:17 +08:00
Manu 292407d1e2 Add deploy hook for pfSense (pfREST) (#7339)
* Add deploy hook for pfSense (pfREST)

* Fix certificate lookup and deploy config handling

* Reject non-ASCII pfSense certificate names

* Fix shfmt formatting

* Use case instead of grep -F, fix shfmt formatting
2026-10-22 11:46:36 +08:00
neil c9e9e24e24 _json_encode: fix empty output on Solaris, newline loss in _hex_dump fallback
_hex_dump strips the trailing newline, and Oracle Solaris sed drops an
unterminated last line, so the sed step in _json_encode produced nothing
and every caller got an empty string on Solaris. Capture the hex first and
re-emit it with printf "%s\n".

The _ascii_hex fallback of _hex_dump (used when od/hexdump/xxd are not in
PATH, e.g. Tribblix) captured stdin with $(cat), losing trailing newlines,
and ran cut -c per line, so multi-line input was mis-encoded. Process the
input line by line instead.

Both surfaced by le_test_json_encode (#7212).
2026-10-21 20:22:33 +08:00
neil aef81df5eb dns_gandi_livedns: delete the TXT record when rm removes its last value
rm PUT the remaining values back even when none were left, leaving an
empty TXT rrset behind. Use DELETE (204 on success) in that case; keep
PUT when other values remain, since the rrset can hold values from
other pending validations.

Refs #7347
2026-10-21 18:49:59 +08:00
neil e3004b84a1 dns_gandi_livedns: load saved credentials in rm
Since 329dab9a (3.1.3) the token/key is saved with
_saveaccountconf_mutable, but only _add read it back. rm runs in its
own subshell, so on cron renewal it called the API unauthenticated;
the 401 body has no "code": 401, so _get_root took the first probe as
the zone and the TXT record was never removed.

Fixes #7347
2026-10-21 17:52:45 +08:00
neil 3288c35c2a _json_encode/_json_decode: use printf instead of echo
dash's echo interprets backslash escapes, so the \. MarkdownV2 escapes
built by notify/telegram.sh collapsed to \. in the JSON body and
Telegram rejected the message with "Character '.' is reserved".
bash's echo does not, which is why only dash-based systems were hit.

Fixes #7212
2026-10-21 17:52:08 +08:00
5d07634e0f dns_namecheap: let NAMECHEAP_* env vars override saved credentials (#7344)
Co-authored-by: zorba <zorba-claude@pavlovian.net>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 14:09:31 +08:00
a57c8a45fa Add hp_ews deploy hook for HP printers (#7198)
* Add hp_ews deploy hook for HP printers

Deploys the certificate to an HP printer's Embedded Web Server by building
a PKCS#12 bundle from the key, certificate and CA chain and POSTing it to
/Security/DeviceCertificates/NewCertWithPassword/Upload.

Tested on an HP LaserJet Pro MFP M479fdw.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* hp_ews: upload with _post_file and Basic auth

Replace the hand-rolled curl/wget helper with _post_file, as requested
in review. Authenticate with a Basic Authorization header like the other
deploy hooks, and map HP_EWS_INSECURE onto HTTPS_INSECURE, which stays
inside the hook's subshell. Printers that only offer Digest auth are no
longer supported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* hp_ews: declare and export _H1 separately (SC2155)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Sean Graham <sean.graham@vizit.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-19 15:23:35 +08:00
Ilya Vassyutovich 0d1d0092b8 dns_yc: use absolute record names (#7338)
Record names were sent relative to the zone, derived from _sub_domain.
That has no valid form when the TXT name is the zone apex, which
happens with --domain-alias pointing at a dedicated zone:

- YC_Zone_ID: _sub_domain ends up empty and the API rejects the
  request ("Length must be in the range of 1 to 254").
- YC_Folder_ID: _get_root returns the first label as _sub_domain, so
  the record lands at <alias>.<alias>. and validation never succeeds.

The API accepts absolute names (trailing dot) everywhere a relative one
is accepted, so send $fulldomain, which is already lowercased and
dot-terminated. Ordinary _acme-challenge.<sub> names keep working.

Supersedes #6862.
2026-10-18 14:34:17 +08:00
Voyo c472636bae Merge pull request #7333 from voyo/add-dns-primedns
Add PrimeDNS DNS API (dns_primedns)
2026-10-16 20:50:38 +08:00
579c994fad Handle DNSimple API response for nonexistent subdomain (#7324)
* handle OAuth response

* revert comment removal

* handle lowercased domain value

* expand error detection logic for all non-expected responses

* shell-independent lowercasing

* revert; domain value already lowercased

* add missing double-quotes

* use zone records endpoint fallback when zone cannot be identified

* add logic missing from previous

---------

Co-authored-by: neil <github@neilpang.com>
Co-authored-by: Mashiro <adadam@qq.com>
Co-authored-by: MBWhitestone <25477219+MBWhitestone@users.noreply.github.com>
Co-authored-by: Pablo <Pablo1@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-16 16:11:30 +08:00
neil f659ba2ea2 Skip conf writes that change nothing
Since #7278 every conf write is a temp file plus a rename, and _setopt
and _clear_conf rewrote the conf even when the content stayed the same:
_savedeployconf clears a legacy key that is almost never there, and
deploy hooks re-save the same values on every run. On storage where
file creation and rename are expensive (ZFS on HDD) a multideploy run
went from about 23 seconds to over 2 minutes.

Return early when the new content equals the old one. A simulated
3-service docker multideploy drops from 96 conf writes to 32.

Fixes #7335
2026-10-15 15:03:18 +08:00
10 changed files with 1149 additions and 128 deletions

No files matched your search

+35 -7
View File
@@ -666,8 +666,15 @@ _hex_dump() {
xxd -ps -c 20 -i | sed "s/ 0x/ /g" | tr -d ",\n" | tr -s " "
else
_debug3 "using _ascii_hex"
str=$(cat)
_ascii_hex "$str"
#line by line: the cut -c in _ascii_hex works per line, and a $(cat)
#capture would drop the trailing newlines (illumos without od in PATH)
while IFS= read -r _hd_line; do
_ascii_hex "$_hd_line"
printf " 0a"
done
if [ -n "$_hd_line" ]; then
_ascii_hex "$_hd_line"
fi
fi
}
@@ -907,7 +914,12 @@ _json_encode() {
_j_str="$(sed 's/"/\\"/g' | sed "s/\r/\\r/g")"
_debug3 "_json_encode"
_debug3 "_j_str" "$_j_str"
echo "$_j_str" | _hex_dump | _lower_case | sed 's/0a/5c 6e/g' | tr -d ' ' | _h2b | tr -d "\r\n"
#printf, not echo: dash's echo interprets backslash escapes and would
#collapse the \\ sequences (e.g. Telegram MarkdownV2 escapes, issue 7212)
_j_hex="$(printf "%s\n" "$_j_str" | _hex_dump | _lower_case)"
#the hex line must end with a newline: Solaris sed drops an unterminated
#last line, which emptied the whole output
printf "%s\n" "$_j_hex" | sed 's/0a/5c 6e/g' | tr -d ' ' | _h2b | tr -d "\r\n"
}
#from: http:\/\/ to http://
@@ -915,7 +927,7 @@ _json_decode() {
_j_str="$(sed 's#\\/#/#g')"
_debug3 "_json_decode"
_debug3 "_j_str" "$_j_str"
echo "$_j_str"
printf "%s\n" "$_j_str"
}
#extract the authorization URLs from an order response on stdin, as a
@@ -2914,6 +2926,7 @@ _setopt() {
_err "Cannot read $__conf."
return 1
fi
__old_text="$__text"
#build the new content first and write it once through _write_conf:
#redirecting straight into the conf truncates it before anything is
#written, so a failing sed (#2426) or a failing write (#7247) left a
@@ -2930,6 +2943,13 @@ _setopt() {
_debug3 APP
__text="$__text${__text:+$__nl}$__opt$__sep$__val$__end"
fi
if [ -z "$__sed_err" ] && [ "$__text" = "$__old_text" ]; then
#the value is already saved: callers re-save the same values on every
#run, and each write is a temp file plus a rename, which is slow on
#some storage (#7335)
_debug3 "Unchanged, skip writing $__conf"
return 0
fi
if [ "$__sed_err" ] || ! _write_conf "$__conf" "$__text"; then
_err "Cannot save '$__opt' to $__conf."
return 1
@@ -2959,11 +2979,19 @@ _clear_conf() {
_c_c_f="$1"
_sdkey="$2"
if [ "$_c_c_f" ]; then
_conf_data="$(cat "$_c_c_f")"
_c_c_old="$(cat "$_c_c_f")"
#printf, not echo: dash's builtin echo interprets backslash escapes and
#would corrupt saved values that contain them on every rewrite
if ! _conf_data="$(printf -- "%s\n" "$_conf_data" | sed "/^$_sdkey *=.*$/d")" ||
! _write_conf "$_c_c_f" "$_conf_data"; then
if ! _conf_data="$(printf -- "%s\n" "$_c_c_old" | sed "/^$_sdkey *=.*$/d")"; then
_err "Cannot clear '$_sdkey' in $_c_c_f."
return 1
fi
#the key is not there, the usual case for the legacy keys cleared on
#every _savedeployconf: nothing to write (#7335)
if [ "$_conf_data" = "$_c_c_old" ]; then
return 0
fi
if ! _write_conf "$_c_c_f" "$_conf_data"; then
_err "Cannot clear '$_sdkey' in $_c_c_f."
return 1
fi
+207
View File
@@ -0,0 +1,207 @@
#!/usr/bin/env sh
# Here is a script to deploy cert to an HP printer's Embedded Web Server (EWS).
#
# Docs: https://github.com/acmesh-official/acme.sh/wiki/deployhooks#47-deploy-the-certificate-to-an-hp-printer-ews
#
# Tested on an HP Color LaserJet Pro MFP M479fdw, firmware
# CLRWTRXXXN002.2539E.00. It should work on any modern HP printer whose EWS
# exposes the "Install Certificate" flow at
# /Security/DeviceCertificates/NewCertWithPassword/Upload -- that endpoint
# takes a password-protected PKCS#12 bundle, which this hook builds from the
# issued certificate, its key and the CA chain.
#
# ```sh
# export HP_EWS_PASSWORD=mysecret
# export HP_EWS_INSECURE=1 # needed until the printer serves a trusted cert
# acme.sh --deploy -d printer.example.com --deploy-hook hp_ews
# ```
#
# Environment variables:
# HP_EWS_PASSWORD (required) EWS administrator password.
# HP_EWS_HOST (optional) IP or hostname of the printer.
# Default: the certificate's domain.
# HP_EWS_USERNAME (optional) EWS administrator user. Default: admin
# HP_EWS_PFX_PASSWORD (optional) Password for the generated PKCS#12
# bundle. A random one is used per run if
# unset; set it only if you want the
# bundle to be reproducible.
# HP_EWS_INSECURE (optional) Set to a non-empty value to skip TLS
# verification when talking to the
# printer. Needed on the first deploy,
# because the printer still serves its
# self-signed certificate at that point.
# acme.sh's global HTTPS_INSECURE is
# honoured as well.
#
# The hook authenticates with HTTP Basic auth. Printers whose EWS only offers
# Digest auth are not supported.
#
# Requires openssl, to build the PKCS#12 bundle.
#
# returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain pfx
hp_ews_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_cpfx="$6"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_debug _cpfx "$_cpfx"
_getdeployconf HP_EWS_HOST
_getdeployconf HP_EWS_USERNAME
_getdeployconf HP_EWS_PASSWORD
_getdeployconf HP_EWS_PFX_PASSWORD
_getdeployconf HP_EWS_INSECURE
HP_EWS_HOST="${HP_EWS_HOST:-$_cdomain}"
HP_EWS_USERNAME="${HP_EWS_USERNAME:-admin}"
_debug HP_EWS_HOST "$HP_EWS_HOST"
_debug HP_EWS_USERNAME "$HP_EWS_USERNAME"
_secure_debug HP_EWS_PASSWORD "$HP_EWS_PASSWORD"
_secure_debug HP_EWS_PFX_PASSWORD "$HP_EWS_PFX_PASSWORD"
_debug HP_EWS_INSECURE "$HP_EWS_INSECURE"
if [ -z "$HP_EWS_PASSWORD" ]; then
_err "HP_EWS_PASSWORD is required. Please set the password of the printer's EWS administrator account."
return 1
fi
_savedeployconf HP_EWS_HOST "$HP_EWS_HOST"
_savedeployconf HP_EWS_USERNAME "$HP_EWS_USERNAME"
_savedeployconf HP_EWS_PASSWORD "$HP_EWS_PASSWORD" "base64"
_savedeployconf HP_EWS_PFX_PASSWORD "$HP_EWS_PFX_PASSWORD" "base64"
_savedeployconf HP_EWS_INSECURE "$HP_EWS_INSECURE"
if ! _exists "${ACME_OPENSSL_BIN:-openssl}"; then
_err "openssl is required by this deploy hook, but was not found."
return 1
fi
# The bundle password only has to survive the single upload below, so a
# fresh random one is used unless the operator asked for a fixed value.
_HP_EWS_PFXPW="$HP_EWS_PFX_PASSWORD"
if [ -z "$_HP_EWS_PFXPW" ]; then
_HP_EWS_PFXPW="$(${ACME_OPENSSL_BIN:-openssl} rand -hex 24)"
if [ -z "$_HP_EWS_PFXPW" ]; then
_err "Could not generate a random password for the PKCS#12 bundle."
return 1
fi
fi
export _HP_EWS_PFXPW
# $6 is only materialised when Le_PFXPassword is set for this domain, and
# it would be built without the CA chain, so build our own bundle instead.
_hp_ews_pfx="$(_mktemp)"
_hp_ews_body="$(_mktemp)"
if [ -z "$_hp_ews_pfx" ] || [ -z "$_hp_ews_body" ]; then
_err "Could not create the temporary files needed for the upload."
_hp_ews_cleanup
return 1
fi
_hp_ews_ret=0
if ! _hp_ews_make_pfx; then
_hp_ews_ret=1
elif ! _hp_ews_upload; then
_hp_ews_ret=1
fi
_hp_ews_cleanup
return "$_hp_ews_ret"
}
#################### Private functions below ##################################
_hp_ews_cleanup() {
[ -n "$_hp_ews_pfx" ] && rm -f "$_hp_ews_pfx"
[ -n "$_hp_ews_body" ] && rm -f "$_hp_ews_body"
unset _HP_EWS_PFXPW
# Do not leak the request header into any hook that runs after this one.
export _H1=""
return 0
}
# Bundle key, certificate and chain into $_hp_ews_pfx, protected by
# $_HP_EWS_PFXPW. The password goes through the environment rather than the
# command line so that it does not show up in ps output.
_hp_ews_make_pfx() {
_info "Building the PKCS#12 bundle"
if ! ${ACME_OPENSSL_BIN:-openssl} pkcs12 -export \
-out "$_hp_ews_pfx" \
-inkey "$_ckey" \
-in "$_ccert" \
-certfile "$_cca" \
-passout env:_HP_EWS_PFXPW; then
_err "Failed to build the PKCS#12 bundle."
return 1
fi
if [ ! -s "$_hp_ews_pfx" ]; then
_err "The PKCS#12 bundle is empty."
return 1
fi
return 0
}
# Build the multipart body in $_hp_ews_body and POST it to the printer.
_hp_ews_upload() {
_hp_ews_url="https://${HP_EWS_HOST}/Security/DeviceCertificates/NewCertWithPassword/Upload"
_hp_ews_boundary="----------------------------acmesh$(${ACME_OPENSSL_BIN:-openssl} rand -hex 16)"
{
printf -- "--%s\r\n" "$_hp_ews_boundary"
printf "Content-Disposition: form-data; name=\"certificate\"; filename=\"%s.pfx\"\r\n" "$_cdomain"
printf "Content-Type: application/x-pkcs12\r\n\r\n"
cat "$_hp_ews_pfx"
printf "\r\n"
printf -- "--%s\r\n" "$_hp_ews_boundary"
printf "Content-Disposition: form-data; name=\"password\"\r\n\r\n%s\r\n" "$_HP_EWS_PFXPW"
printf -- "--%s--\r\n" "$_hp_ews_boundary"
} >"$_hp_ews_body"
_H1="Authorization: Basic $(printf "%s:%s" "$HP_EWS_USERNAME" "$HP_EWS_PASSWORD" | _base64)"
export _H1
if [ "$HP_EWS_INSECURE" ]; then
export HTTPS_INSECURE=1
fi
_info "Uploading the certificate to $_hp_ews_url"
if ! _post_file "$_hp_ews_body" "$_hp_ews_url" "" "POST" "multipart/form-data; boundary=$_hp_ews_boundary" >/dev/null; then
if [ -z "$HTTPS_INSECURE" ]; then
_err "If the printer still serves its self-signed certificate, set HP_EWS_INSECURE=1 and try again."
fi
return 1
fi
# The EWS answers 201 Created on a successful install.
_hp_ews_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
_debug _hp_ews_code "$_hp_ews_code"
if [ "$_hp_ews_code" != "201" ] && [ "$_hp_ews_code" != "200" ]; then
_err "The printer rejected the certificate (HTTP $_hp_ews_code)."
if [ "$_hp_ews_code" = "401" ] || [ "$_hp_ews_code" = "403" ]; then
_err "Check HP_EWS_USERNAME and HP_EWS_PASSWORD. Only HTTP Basic authentication is"
_err "supported; printers that only offer Digest cannot be used with this hook."
fi
return 1
fi
_info "Certificate installed (HTTP $_hp_ews_code). The printer restarts its web server shortly."
return 0
}
+247
View File
@@ -0,0 +1,247 @@
#!/usr/bin/env sh
# Deploy hook to deploy a certificate to pfSense using the pfREST API
# (https://pfrest.org). Tested with pfSense 2.9.0 and pfREST 2.10.2.
#
# The certificate is created under DEPLOY_PFSENSE_CERT_NAME, or key and
# certificate are replaced in place if it already exists. Then it is
# optionally assigned to the Unbound DNS-over-TLS listener and finally to the
# web UI, which gets restarted (the API is briefly unreachable afterwards).
#
# Preparation on the pfSense:
#
# - Install pfREST and enable key authentication:
# https://pfrest.org/INSTALL_AND_CONFIG/
# https://pfrest.org/AUTHENTICATION_AND_AUTHORIZATION/
# - Create the API key (System > REST API > Keys) with a user that has
# "WebCfg - All pages". Afterwards use a dedicated user with only these
# privileges, the key is bound to the user that creates it:
# REST API - /api/v2/system/certificate PATCH
# REST API - /api/v2/system/certificate POST
# REST API - /api/v2/system/certificates GET
# REST API - /api/v2/system/webgui/settings PATCH
# REST API - /api/v2/services/dns_resolver/apply POST
# REST API - /api/v2/services/dns_resolver/settings GET
# REST API - /api/v2/services/dns_resolver/settings PATCH
# - DNS-over-TLS only: "Enable SSL/TLS Service" must already be enabled in
# Services > DNS Resolver, otherwise Unbound is skipped.
#
# Environment variables:
#
# DEPLOY_PFSENSE_APIKEY - (REQUIRED)
# pfREST API key.
# DEPLOY_PFSENSE_HOST - pfsense.example.com, 192.168.1.1, etc. (OPTIONAL)
# Host/IP of the pfSense. Defaults to the
# certificate domain, required for wildcard
# certificates.
# DEPLOY_PFSENSE_PORT - 443, 8443, etc. (OPTIONAL)
# Web UI/API port. Defaults to 443.
# DEPLOY_PFSENSE_CERT_NAME - My Certificate, etc. (OPTIONAL)
# Certificate name shown in pfSense (printable
# ASCII only). Defaults to the certificate
# domain.
# DEPLOY_PFSENSE_WEBUI - yes/no (OPTIONAL)
# Assign to the web UI and restart it. Defaults
# to "yes".
# DEPLOY_PFSENSE_UNBOUND_DOT - yes/no (OPTIONAL)
# Also assign to Unbound DNS-over-TLS. Defaults
# to "no".
# DEPLOY_PFSENSE_INSECURE - yes/no (OPTIONAL)
# Do not verify the pfSense TLS certificate,
# needed on the first run with the self-signed
# default certificate. Defaults to "no".
#
# Example:
#
# export DEPLOY_PFSENSE_APIKEY="<api key>"
# acme.sh --deploy -d pfsense.example.com --deploy-hook pfsense
#
# returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
pfsense_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
if [ ! -f "$_ckey" ] || [ ! -f "$_cfullchain" ]; then
_err "Key or fullchain file not found."
return 1
fi
_getdeployconf DEPLOY_PFSENSE_APIKEY
if [ -z "$DEPLOY_PFSENSE_APIKEY" ]; then
_err "DEPLOY_PFSENSE_APIKEY is not set."
return 1
fi
_secure_debug2 DEPLOY_PFSENSE_APIKEY "$DEPLOY_PFSENSE_APIKEY"
_getdeployconf DEPLOY_PFSENSE_HOST
_getdeployconf DEPLOY_PFSENSE_PORT
_getdeployconf DEPLOY_PFSENSE_CERT_NAME
_getdeployconf DEPLOY_PFSENSE_WEBUI
_getdeployconf DEPLOY_PFSENSE_UNBOUND_DOT
_getdeployconf DEPLOY_PFSENSE_INSECURE
if [ -z "$DEPLOY_PFSENSE_HOST" ]; then
if _startswith "$_cdomain" "*"; then
_err "DEPLOY_PFSENSE_HOST must be set for wildcard certificates."
return 1
fi
DEPLOY_PFSENSE_HOST="$_cdomain"
fi
[ -n "$DEPLOY_PFSENSE_PORT" ] || DEPLOY_PFSENSE_PORT="443"
[ -n "$DEPLOY_PFSENSE_CERT_NAME" ] || DEPLOY_PFSENSE_CERT_NAME="$_cdomain"
[ -n "$DEPLOY_PFSENSE_WEBUI" ] || DEPLOY_PFSENSE_WEBUI="yes"
[ -n "$DEPLOY_PFSENSE_UNBOUND_DOT" ] || DEPLOY_PFSENSE_UNBOUND_DOT="no"
[ -n "$DEPLOY_PFSENSE_INSECURE" ] || DEPLOY_PFSENSE_INSECURE="no"
# pfREST returns non-ASCII as \uXXXX, which cannot be matched reliably
if printf '%s' "$DEPLOY_PFSENSE_CERT_NAME" | LC_ALL=C grep -q '[^ -~]'; then
_err "DEPLOY_PFSENSE_CERT_NAME must only contain printable ASCII characters."
return 1
fi
# pfREST rejects these characters in a certificate description
case "$DEPLOY_PFSENSE_CERT_NAME" in
*[\?\>\<\&/\\\"\']*)
_err "DEPLOY_PFSENSE_CERT_NAME must not contain any of: ? > < & / \\ \" '"
return 1
;;
esac
_debug2 DEPLOY_PFSENSE_HOST "$DEPLOY_PFSENSE_HOST"
_debug2 DEPLOY_PFSENSE_PORT "$DEPLOY_PFSENSE_PORT"
_debug2 DEPLOY_PFSENSE_CERT_NAME "$DEPLOY_PFSENSE_CERT_NAME"
_debug2 DEPLOY_PFSENSE_WEBUI "$DEPLOY_PFSENSE_WEBUI"
_debug2 DEPLOY_PFSENSE_UNBOUND_DOT "$DEPLOY_PFSENSE_UNBOUND_DOT"
_debug2 DEPLOY_PFSENSE_INSECURE "$DEPLOY_PFSENSE_INSECURE"
_pfsense_api_url="https://${DEPLOY_PFSENSE_HOST}:${DEPLOY_PFSENSE_PORT}/api/v2"
if [ "$DEPLOY_PFSENSE_INSECURE" = "yes" ]; then
export HTTPS_INSECURE=1
fi
_info "Looking up certificate '$DEPLOY_PFSENSE_CERT_NAME' on pfSense"
_pfsense_call GET "/system/certificates?descr=$(printf '%s' "$DEPLOY_PFSENSE_CERT_NAME" | _url_encode)" || return 1
# do not rely on the server-side filter, only accept an exact descr match
_pf_match=""
_pf_objs="$(printf '%s\n' "$_pfsense_response" | sed 's/} *, *{/}\
{/g')"
while IFS= read -r _pf_line; do
case "$_pf_line" in
*"\"descr\":\"$DEPLOY_PFSENSE_CERT_NAME\""*)
_pf_match="$_pf_line"
break
;;
esac
done <<EOF
$_pf_objs
EOF
_pf_id="$(printf '%s' "$_pf_match" | _egrep_o '"id":[0-9]+' | _head_n 1 | cut -d : -f 2)"
_pf_refid="$(printf '%s' "$_pf_match" | _egrep_o '"refid":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)"
_debug2 "existing id/refid" "$_pf_id/$_pf_refid"
_pf_crt="$(_json_encode <"$_cfullchain")"
_pf_prv="$(_json_encode <"$_ckey")"
if [ -n "$_pf_id" ] && [ -n "$_pf_refid" ]; then
_info "Updating existing certificate (refid $_pf_refid)"
_pfsense_call PATCH "/system/certificate" \
"{\"id\": $_pf_id, \"crt\": \"$_pf_crt\", \"prv\": \"$_pf_prv\"}" || return 1
else
_info "Creating new certificate"
_pfsense_call POST "/system/certificate" \
"{\"descr\": \"$DEPLOY_PFSENSE_CERT_NAME\", \"type\": \"server\", \"crt\": \"$_pf_crt\", \"prv\": \"$_pf_prv\"}" || return 1
_pf_refid="$(printf '%s' "$_pfsense_response" | _egrep_o '"refid":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)"
if [ -z "$_pf_refid" ]; then
_err "Certificate was created but no refid was returned."
return 1
fi
fi
_savedeployconf DEPLOY_PFSENSE_APIKEY "$DEPLOY_PFSENSE_APIKEY"
_savedeployconf DEPLOY_PFSENSE_HOST "$DEPLOY_PFSENSE_HOST"
_savedeployconf DEPLOY_PFSENSE_PORT "$DEPLOY_PFSENSE_PORT"
_savedeployconf DEPLOY_PFSENSE_CERT_NAME "$DEPLOY_PFSENSE_CERT_NAME"
_savedeployconf DEPLOY_PFSENSE_WEBUI "$DEPLOY_PFSENSE_WEBUI"
_savedeployconf DEPLOY_PFSENSE_UNBOUND_DOT "$DEPLOY_PFSENSE_UNBOUND_DOT"
_savedeployconf DEPLOY_PFSENSE_INSECURE "$DEPLOY_PFSENSE_INSECURE"
if [ "$DEPLOY_PFSENSE_UNBOUND_DOT" = "yes" ]; then
_pfsense_unbound_dot "$_pf_refid" || return 1
fi
if [ "$DEPLOY_PFSENSE_WEBUI" != "no" ]; then
_info "Assigning certificate to the web UI and restarting it"
_pfsense_call PATCH "/system/webgui/settings" "{\"sslcertref\": \"$_pf_refid\"}" || return 1
fi
_info "Certificate successfully deployed to pfSense"
return 0
}
#################### Private functions below ##################################
# method path [body] -> sets _pfsense_response, returns 1 on non-2xx
_pfsense_call() {
_pf_method="$1"
_pf_path="$2"
_pf_body="$3"
_H1="x-api-key: $DEPLOY_PFSENSE_APIKEY"
export _H1
_debug "$_pf_method" "$_pfsense_api_url$_pf_path"
if [ "$_pf_method" = "GET" ]; then
_pfsense_response="$(_get "$_pfsense_api_url$_pf_path")"
else
_pfsense_response="$(_post "$_pf_body" "$_pfsense_api_url$_pf_path" "" "$_pf_method" "application/json")"
fi
_pf_ret=$?
_pf_status="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
_pfsense_response="$(printf '%s' "$_pfsense_response" | tr -d '\r\n' | sed 's/" *: */":/g')"
_debug2 "HTTP status" "$_pf_status"
_secure_debug3 response "$_pfsense_response"
case "$_pf_status" in
2[0-9][0-9])
[ "$_pf_ret" = "0" ] && return 0
;;
esac
_pf_msg="$(printf '%s' "$_pfsense_response" | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)"
_err "pfSense API call failed: $_pf_method $_pf_path (HTTP ${_pf_status:-none}). $_pf_msg"
if [ "$_pf_status" = "401" ] || [ "$_pf_status" = "403" ] || [ -z "$_pf_status" ]; then
_err "Check DEPLOY_PFSENSE_APIKEY, the user privileges and DEPLOY_PFSENSE_HOST/PORT. Use DEPLOY_PFSENSE_INSECURE=yes for an untrusted web UI certificate."
fi
return 1
}
# refid
_pfsense_unbound_dot() {
_pf_dot_refid="$1"
_info "Checking Unbound DNS-over-TLS"
_pfsense_call GET "/services/dns_resolver/settings" || return 1
if ! _contains "$_pfsense_response" '"enablessl":true'; then
_info "SSL/TLS is not enabled in the DNS Resolver, skipping Unbound"
return 0
fi
_info "Assigning certificate to Unbound and applying"
_pfsense_call PATCH "/services/dns_resolver/settings" "{\"sslcertref\": \"$_pf_dot_refid\"}" || return 1
_pfsense_call POST "/services/dns_resolver/apply" "{}" || return 1
return 0
}
+273 -110
View File
@@ -4,153 +4,316 @@ dns_arvan_info='ArvanCloud.ir
Site: ArvanCloud.ir
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_arvan
Options:
Arvan_Token API Token
Arvan_Token API key, with or without the Apikey prefix
Issues: github.com/acmesh-official/acme.sh/issues/2796
Author: Vahid Fardi
Author: Vahid Fardi, Abolfazl Rajabpour (abooraja)
'
ARVAN_API_URL="https://napi.arvancloud.ir/cdn/4.0/domains"
######## Public functions #####################
#Usage: dns_arvan_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
# Usage: dns_arvan_add _acme-challenge.www.example.com "TXT value"
dns_arvan_add() {
fulldomain=$1
txtvalue=$2
_info "Using Arvan"
Arvan_Token="${Arvan_Token:-$(_readaccountconf_mutable Arvan_Token)}"
if [ -z "$Arvan_Token" ]; then
_err "You didn't specify \"Arvan_Token\" token yet."
_err "You can get yours from here https://npanel.arvancloud.ir/profile/api-keys"
if ! _arvan_init; then
return 1
fi
#save the api token to the account conf file.
_saveaccountconf_mutable Arvan_Token "$Arvan_Token"
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain"
if ! _saveaccountconf_mutable Arvan_Token "$Arvan_Token"; then
_err "Unable to save the Arvan API credentials."
return 1
fi
_debug _domain_id "$_domain_id"
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_info "Adding record"
if _arvan_rest POST "$_domain/dns-records" "{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":{\"text\":\"$txtvalue\"},\"ttl\":120}"; then
if _contains "$response" "$txtvalue"; then
_info "response id is $response"
_info "Added, OK"
return 0
elif _contains "$response" "Record Data is duplicate"; then
_info "Already exists, OK"
return 0
else
_err "Add txt record error."
return 1
fi
if ! _get_root "$fulldomain" || ! _arvan_find_record; then
return 1
fi
_err "Add txt record error."
if [ "$_arvan_record_ids" ]; then
_info "TXT record already exists."
return 0
fi
# Append a separate record: apex and wildcard challenges can share a name.
if ! _arvan_rest POST "$_domain/dns-records" "{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":{\"text\":\"$txtvalue\"},\"ttl\":120}"; then
_arvan_add_code=$_arvan_code
# Another client may have added the same value since the lookup.
case "$_arvan_add_code" in
409 | 422)
if _arvan_find_record && [ "$_arvan_record_ids" ]; then
_info "TXT record already exists."
return 0
fi
;;
esac
_err "Unable to add the Arvan TXT record (HTTP $_arvan_add_code)."
return 1
fi
_info "TXT record added."
return 0
}
#Usage: fulldomain txtvalue
#Remove the txt record after validation.
# Usage: dns_arvan_rm _acme-challenge.www.example.com "TXT value"
dns_arvan_rm() {
fulldomain=$1
txtvalue=$2
_info "Using Arvan"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
Arvan_Token="${Arvan_Token:-$(_readaccountconf_mutable Arvan_Token)}"
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain"
if ! _arvan_init || ! _get_root "$fulldomain" || ! _arvan_find_record; then
return 1
fi
_debug _domain_id "$_domain_id"
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_debug "Getting txt records"
_arvan_rest GET "${_domain}/dns-records"
if ! printf "%s" "$response" | grep \"current_page\":1 >/dev/null; then
_err "Error on Arvan Api"
_err "Please create a github issue with debbug log"
return 1
if [ -z "$_arvan_record_ids" ]; then
_info "TXT record is already absent."
return 0
fi
_record_id=$(echo "$response" | _egrep_o ".\"id\":\"[^\"]*\",\"type\":\"txt\",\"name\":\"_acme-challenge\",\"value\":{\"text\":\"$txtvalue\"}" | cut -d : -f 2 | cut -d , -f 1 | tr -d \")
if ! _arvan_rest "DELETE" "${_domain}/dns-records/${_record_id}"; then
_err "Error on Arvan Api"
return 1
fi
_debug "$response"
_contains "$response" 'dns record deleted'
# Record IDs are validated before they are used as path components.
for _arvan_record_id in $_arvan_record_ids; do
if ! _arvan_rest DELETE "$_domain/dns-records/$_arvan_record_id"; then
if [ "$_arvan_code" != "404" ]; then
_err "Unable to remove the Arvan TXT record (HTTP $_arvan_code)."
return 1
fi
fi
done
_info "TXT record removed."
return 0
}
#################### Private functions below ##################################
#_acme-challenge.www.domain.com
#returns
# _sub_domain=_acme-challenge.www
# _domain=domain.com
# _domain_id=sdjkglgdfewsdfg
_get_root() {
domain=$1
i=2
p=1
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug h "$h"
if [ -z "$h" ]; then
#not valid
return 1
fi
_arvan_init() {
Arvan_Token="${Arvan_Token:-$(_readaccountconf_mutable Arvan_Token)}"
Arvan_Token=${Arvan_Token#\"}
Arvan_Token=${Arvan_Token%\"}
if [ -z "$Arvan_Token" ]; then
_err "You didn't specify Arvan_Token."
_err "Get an API key from https://npanel.arvancloud.ir/profile/api-keys"
return 1
fi
}
if ! _arvan_rest GET "$h"; then
return 1
fi
if _contains "$response" "\"domain\":\"$h\""; then
_domain_id=$(echo "$response" | cut -d : -f 3 | cut -d , -f 1 | tr -d \")
if [ "$_domain_id" ]; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain=$h
return 0
# Find the longest managed suffix, including the full name for DNS alias mode.
_get_root() {
domain=$(printf "%s" "$1" | _lower_case)
domain=${domain%.}
_domain=""
_sub_domain=""
case "$domain" in
"" | *[!a-z0-9_.-]* | .* | *..*)
_err "Invalid DNS name."
return 1
;;
esac
if ! _arvan_list ""; then
return 1
fi
if ! _arvan_zones=$(_arvan_get_data domains); then
_err "Invalid Arvan domains response."
return 1
fi
h=$domain
while true; do
case "
$_arvan_zones
" in
*"
$h
"*)
_domain=$h
_sub_domain=${domain%."$h"}
if [ "$domain" = "$h" ]; then
_sub_domain="@"
fi
return 1
fi
p=$i
i=$(_math "$i" + 1)
_debug _domain "$_domain"
_debug _sub_domain "$_sub_domain"
return 0
;;
esac
case "$h" in
*.*) h=${h#*.} ;;
*) break ;;
esac
done
_err "No matching zone found in the Arvan account."
return 1
}
_arvan_rest() {
mtd="$1"
ep="$2"
data="$3"
token_trimmed=$(echo "$Arvan_Token" | tr -d '"')
export _H1="Authorization: $token_trimmed"
if [ "$mtd" = "DELETE" ]; then
#DELETE Request shouldn't have Content-Type
_debug data "$data"
response="$(_post "$data" "$ARVAN_API_URL/$ep" "" "$mtd")"
elif [ "$mtd" = "POST" ]; then
export _H2="Content-Type: application/json"
export _H3="Accept: application/json"
_debug data "$data"
response="$(_post "$data" "$ARVAN_API_URL/$ep" "" "$mtd")"
else
response="$(_get "$ARVAN_API_URL/$ep$data")"
_arvan_find_record() {
_arvan_record_ids=""
if ! _arvan_list "$_domain/dns-records?type=txt"; then
return 1
fi
if ! _arvan_record_ids=$(_arvan_get_data records); then
_err "Invalid Arvan DNS records response."
return 1
fi
return 0
}
# Collect all pages before matching, so a delegated zone on a later page wins.
_arvan_list() {
_arvan_path=$1
_arvan_page=1
_arvan_result=""
while true; do
case "$_arvan_path" in
*\?*) _arvan_page_endpoint="$_arvan_path&page=$_arvan_page" ;;
*) _arvan_page_endpoint="$_arvan_path?page=$_arvan_page" ;;
esac
if ! _arvan_rest GET "$_arvan_page_endpoint"; then
_err "Unable to list Arvan resources (HTTP $_arvan_code)."
return 1
fi
_arvan_current_page=$(printf "%s" "$response" | _egrep_o '"current_page"[ ]*:[ ]*[0-9][0-9]*' | cut -d : -f 2 | tr -d ' \t')
_arvan_last_page=$(printf "%s" "$response" | _egrep_o '"last_page"[ ]*:[ ]*[0-9][0-9]*' | cut -d : -f 2 | tr -d ' \t')
case "$_arvan_last_page" in
"" | *[!0-9]*)
_err "Invalid Arvan pagination."
return 1
;;
esac
if [ "$_arvan_current_page" != "$_arvan_page" ] || [ "$_arvan_last_page" -lt "$_arvan_page" ]; then
_err "Unexpected Arvan page number."
return 1
fi
_arvan_result="$_arvan_result
$response"
if [ "$_arvan_page" -eq "$_arvan_last_page" ]; then
response=$_arvan_result
return 0
fi
_arvan_page=$(_math "$_arvan_page" + 1)
done
}
# Read only fields in data[] objects, independently of JSON field order.
# Tokenize quoted strings first so braces in unrelated TXT values stay intact.
_arvan_get_data() {
_arvan_mode=$1
printf "%s\n" "$response" | sed 's/\("[^"\\]*\(\\.[^"\\]*\)*"\)/\
\1\
/g' | sed '/^"/!{
s/[ ]//g
s/\([][{}:,]\)/\
\1\
/g
}
/^$/d' | (
_arvan_depth=0
_arvan_key=""
_arvan_in_data=""
_arvan_in_value=""
_arvan_seen_data=""
while IFS= read -r _arvan_token; do
case "$_arvan_token" in
"{" | "[")
if [ "$_arvan_depth" = "1" ] && [ "$_arvan_key" = '"data"' ] && [ "$_arvan_token" = "[" ]; then
_arvan_in_data=1
_arvan_seen_data=1
elif [ "$_arvan_in_data" ] && [ "$_arvan_depth" = "2" ]; then
_arvan_id=""
_arvan_type=""
_arvan_name=""
_arvan_text=""
_arvan_zone=""
elif [ "$_arvan_in_data" ] && [ "$_arvan_depth" = "3" ] && [ "$_arvan_key" = '"value"' ] && [ "$_arvan_token" = "{" ]; then
_arvan_in_value=1
fi
_arvan_depth=$(_math "$_arvan_depth" + 1)
_arvan_key=""
;;
"}" | "]")
if [ "$_arvan_in_data" ] && [ "$_arvan_depth" = "3" ] && [ "$_arvan_token" = "}" ]; then
if [ "$_arvan_mode" = "domains" ]; then
if [ "$_arvan_zone" ]; then
printf "%s\n" "$_arvan_zone"
fi
elif [ "$_arvan_name" = "$_sub_domain" ] && [ "$_arvan_text" = "$txtvalue" ]; then
case "$_arvan_type" in
[Tt][Xx][Tt])
case "$_arvan_id" in
"" | *[!a-zA-Z0-9-]*) return 1 ;;
esac
printf "%s\n" "$_arvan_id"
;;
esac
fi
elif [ "$_arvan_depth" = "2" ]; then
_arvan_in_data=""
fi
if [ "$_arvan_depth" = "4" ]; then
_arvan_in_value=""
fi
_arvan_depth=$(_math "$_arvan_depth" - 1)
_arvan_key=""
;;
"" | ":") ;;
",") _arvan_key="" ;;
*)
if [ -z "$_arvan_key" ]; then
_arvan_key=$_arvan_token
else
_arvan_value=${_arvan_token#\"}
_arvan_value=${_arvan_value%\"}
if [ "$_arvan_in_data" ]; then
case "$_arvan_depth:$_arvan_key" in
'3:"domain"') _arvan_zone=$_arvan_value ;;
'3:"id"') _arvan_id=$_arvan_value ;;
'3:"type"') _arvan_type=$_arvan_value ;;
'3:"name"') _arvan_name=$_arvan_value ;;
'4:"text"')
if [ "$_arvan_in_value" ]; then
_arvan_text=$_arvan_value
fi
;;
esac
fi
_arvan_key=""
fi
;;
esac
done
[ "$_arvan_depth" = "0" ] && [ "$_arvan_seen_data" = "1" ]
)
}
_arvan_rest() {
_arvan_method=$1
_arvan_endpoint=$2
_arvan_data=$3
_arvan_code=""
_arvan_auth=$Arvan_Token
# Preserve existing authorization values; accept a bare UUID API key too.
case "$_arvan_auth" in
????????-????-????-????-????????????) _arvan_auth="Apikey $_arvan_auth" ;;
esac
export _H1="Authorization: $_arvan_auth"
export _H2="Accept: application/json"
export _H3=""
export _H4=""
export _H5=""
if [ "$_arvan_method" = "POST" ]; then
export _H3="Content-Type: application/json"
fi
case "$_arvan_endpoint" in
"" | \?*) _arvan_url="$ARVAN_API_URL$_arvan_endpoint" ;;
*) _arvan_url="$ARVAN_API_URL/$_arvan_endpoint" ;;
esac
if [ "$_arvan_method" = "GET" ]; then
if ! response=$(_get "$_arvan_url"); then
_err "Arvan API request failed."
return 1
fi
elif ! response=$(_post "$_arvan_data" "$_arvan_url" "" "$_arvan_method"); then
_err "Arvan API request failed."
return 1
fi
_arvan_code=$(grep '^HTTP/' "$HTTP_HEADER" | _tail_n 1 | cut -d ' ' -f 2 | tr -d '\r\n')
_debug "Arvan HTTP status" "$_arvan_code"
_secure_debug2 response "$response"
response=$(printf "%s" "$response" | _normalizeJson)
case "$_arvan_code" in
2[0-9][0-9]) return 0 ;;
esac
return 1
}
+190
View File
@@ -0,0 +1,190 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_connbyte_info='ConnByte
Site: connbyte.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_connbyte
Options:
CONNBYTE_Token API Token. Create it in the ConnByte customer panel, see https://connbyte.com/
CONNBYTE_TTL TTL of the TXT record in seconds. Optional. Default "120".
Issues: github.com/acmesh-official/acme.sh/issues/7342
Author: ConnByte <team@connbyte.com>
'
CONNBYTE_Api="https://api.connbyte.com"
######## Public functions #####################
# Usage: dns_connbyte_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_connbyte_add() {
fulldomain="$(echo "$1" | _lower_case)"
txtvalue="$2"
if ! _connbyte_init; then
return 1
fi
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain: $fulldomain"
return 1
fi
_debug _zone_id "$_zone_id"
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_info "Adding TXT record for $fulldomain"
data="{\"zoneId\":\"$_zone_id\",\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":$CONNBYTE_TTL,\"priority\":0,\"disabled\":false}"
if ! _connbyte_rest "ns/record/create" "$data"; then
_err "Could not add TXT record for $fulldomain"
return 1
fi
_info "TXT record added"
return 0
}
# Usage: dns_connbyte_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_connbyte_rm() {
fulldomain="$(echo "$1" | _lower_case)"
txtvalue="$2"
if ! _connbyte_init; then
return 1
fi
if ! _get_root "$fulldomain"; then
_err "invalid domain: $fulldomain"
return 1
fi
_debug _zone_id "$_zone_id"
_debug _sub_domain "$_sub_domain"
if ! _connbyte_rest "ns/record/list" "{\"zoneId\":\"$_zone_id\"}"; then
_err "Could not list records of zone $_domain"
return 1
fi
# Each record is an innermost JSON object, e.g. (TXT content may be stored quoted)
# {"id":435,"name":"_acme-challenge.example.com","type":"TXT","content":"...","ttl":120,"priority":0,"disabled":0}
# Split on braces with tr: the egrep on illumos (OmniOS) does not handle the "\{[^{}]*\}" pattern.
record_ids="$(echo "$response" | tr -d '\n\r' | tr '{}' '\n' |
grep "\"type\" *: *\"TXT\"" |
grep "\"name\" *: *\"$fulldomain\"" |
grep "\"$txtvalue" |
_egrep_o "\"id\" *: *[0-9]+" | _egrep_o "[0-9]+")"
_debug record_ids "$record_ids"
if [ -z "$record_ids" ]; then
_info "TXT record not found, nothing to remove"
return 0
fi
for record_id in $record_ids; do
_info "Removing TXT record $record_id"
if ! _connbyte_rest "ns/record/delete" "{\"zoneId\":\"$_zone_id\",\"recordId\":$record_id}"; then
_err "Could not remove TXT record $record_id"
return 1
fi
done
return 0
}
#################### Private functions below ##################################
_connbyte_init() {
CONNBYTE_Token="${CONNBYTE_Token:-$(_readaccountconf_mutable CONNBYTE_Token)}"
CONNBYTE_TTL="${CONNBYTE_TTL:-$(_readaccountconf_mutable CONNBYTE_TTL)}"
if [ -z "$CONNBYTE_Token" ]; then
CONNBYTE_Token=""
_err "You did not specify the ConnByte API token."
_err "Please export CONNBYTE_Token and try again."
return 1
fi
if [ -z "$CONNBYTE_TTL" ]; then
CONNBYTE_TTL=120
fi
case "$CONNBYTE_TTL" in
'' | *[!0-9]*)
_err "CONNBYTE_TTL must be a number"
return 1
;;
esac
_saveaccountconf_mutable CONNBYTE_Token "$CONNBYTE_Token"
_saveaccountconf_mutable CONNBYTE_TTL "$CONNBYTE_TTL"
return 0
}
# _acme-challenge.www.domain.com
# returns
# _sub_domain=_acme-challenge.www ("@" for the zone apex)
# _domain=domain.com
# _zone_id=01DXF6DT00ZZZHQPP3JFJS8VEE
_get_root() {
domain=$1
if ! _connbyte_rest "ns/zone/list" "{}"; then
return 1
fi
zones="$response"
i=1
p=1
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug h "$h"
if [ -z "$h" ]; then
# not valid
return 1
fi
zone_id="$(echo "$zones" | tr -d '\n\r' | tr '{}' '\n' |
grep "\"zone\" *: *\"$h\"" | _head_n 1 |
_egrep_o "\"id\" *: *\"[0-9A-Za-z]+\"" | cut -d '"' -f 4)"
if [ -n "$zone_id" ]; then
_zone_id="$zone_id"
_domain="$h"
if [ "$i" -gt 1 ]; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
else
# The API rejects an empty name and uses "@" for the zone apex (DNS alias mode).
_sub_domain="@"
fi
return 0
fi
p=$i
i=$(_math "$i" + 1)
done
}
# Usage: _connbyte_rest path json_body
# Sets $response. Returns 1 on transport error or when the API status code is not 2xx.
_connbyte_rest() {
ep="$1"
data="$2"
export _H1="Accept: application/json"
export _H2="Content-Type: application/json"
export _H3="Authorization: Bearer $CONNBYTE_Token"
_debug ep "$ep"
_debug2 data "$data"
response="$(_post "$data" "$CONNBYTE_Api/$ep" "" "POST")"
ret="$?"
_debug2 response "$response"
if [ "$ret" != "0" ]; then
_err "error $ep"
return 1
fi
status_code="$(echo "$response" | tr -d '\n\r' | _egrep_o "\"status\" *: *\{ *\"code\" *: *[0-9]+" | _egrep_o "[0-9]+$")"
_debug status_code "$status_code"
case "$status_code" in
2*) return 0 ;;
esac
_err "ConnByte API error: $response"
return 1
}
+10 -5
View File
@@ -15,7 +15,7 @@ DNSimple_API="https://api.dnsimple.com/v2"
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_dnsimple_add() {
fulldomain=$1
fulldomain=$(echo "$1" | _lower_case)
txtvalue=$2
DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}"
@@ -56,7 +56,7 @@ dns_dnsimple_add() {
# fulldomain
dns_dnsimple_rm() {
fulldomain=$1
fulldomain=$(echo "$1" | _lower_case)
DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}"
if [ -z "$DNSimple_OAUTH_TOKEN" ]; then
@@ -110,9 +110,13 @@ _get_root() {
return 1
fi
if _contains "$response" 'not found'; then
_debug "$h not found"
else
if ! _contains "$response" "\"name\":\"$h\""; then
if ! _dnsimple_rest GET "$_account_id/zones/$h/records?per_page=1"; then
return 1
fi
fi
if _contains "$response" "\"name\":\"$h\"" || _contains "$response" '"pagination":'; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$previous")
_domain="$h"
@@ -121,6 +125,7 @@ _get_root() {
return 0
fi
_debug "$h not found"
previous="$i"
i=$(_math "$i" + 1)
+21
View File
@@ -59,6 +59,13 @@ dns_gandi_livedns_rm() {
fulldomain=$1
txtvalue=$2
GANDI_LIVEDNS_KEY="${GANDI_LIVEDNS_KEY:-$(_readaccountconf_mutable GANDI_LIVEDNS_KEY)}"
GANDI_LIVEDNS_TOKEN="${GANDI_LIVEDNS_TOKEN:-$(_readaccountconf_mutable GANDI_LIVEDNS_TOKEN)}"
if [ -z "$GANDI_LIVEDNS_KEY" ] && [ -z "$GANDI_LIVEDNS_TOKEN" ]; then
_err "No Token or API key (deprecated) specified for Gandi LiveDNS."
return 1
fi
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain"
@@ -80,6 +87,20 @@ dns_gandi_livedns_rm() {
_new_rrset_values=$(echo "$_new_rrset_values" | sed "s/\[ *,/\[/g")
_debug "New rrset_values" "$_new_rrset_values"
# Last value removed: delete the record instead of leaving an empty TXT rrset.
if [ "$(echo "$_new_rrset_values" | tr -d ' ')" = "[]" ]; then
if ! _gandi_livedns_rest DELETE "domains/$_domain/records/$_sub_domain/TXT"; then
return 1
fi
_gandi_livedns_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
if [ "$_gandi_livedns_code" != "204" ]; then
_err "Error deleting record: $response"
return 1
fi
_info "Removing record $(__green "success")"
return 0
fi
_gandi_livedns_rest PUT \
"domains/$_domain/records/$_sub_domain/TXT" \
"{\"rrset_ttl\": 300, \"rrset_values\": $_new_rrset_values}" &&
+6 -3
View File
@@ -185,13 +185,14 @@ _get_root_by_getHosts() {
}
_namecheap_set_publicip() {
NAMECHEAP_SOURCEIP="${NAMECHEAP_SOURCEIP:-$(_readaccountconf_mutable NAMECHEAP_SOURCEIP)}"
if [ -z "$NAMECHEAP_SOURCEIP" ]; then
_err "No Source IP specified for Namecheap API."
_err "Use your public ip address or an url to retrieve it (e.g. https://ifconfig.co/ip) and export it as NAMECHEAP_SOURCEIP"
return 1
else
_saveaccountconf NAMECHEAP_SOURCEIP "$NAMECHEAP_SOURCEIP"
_saveaccountconf_mutable NAMECHEAP_SOURCEIP "$NAMECHEAP_SOURCEIP"
_debug sourceip "$NAMECHEAP_SOURCEIP"
ip=$(echo "$NAMECHEAP_SOURCEIP" | _egrep_o '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}')
@@ -252,6 +253,8 @@ _namecheap_parse_host() {
}
_namecheap_check_config() {
NAMECHEAP_API_KEY="${NAMECHEAP_API_KEY:-$(_readaccountconf_mutable NAMECHEAP_API_KEY)}"
NAMECHEAP_USERNAME="${NAMECHEAP_USERNAME:-$(_readaccountconf_mutable NAMECHEAP_USERNAME)}"
if [ -z "$NAMECHEAP_API_KEY" ]; then
_err "No API key specified for Namecheap API."
@@ -265,8 +268,8 @@ _namecheap_check_config() {
return 1
fi
_saveaccountconf NAMECHEAP_API_KEY "$NAMECHEAP_API_KEY"
_saveaccountconf NAMECHEAP_USERNAME "$NAMECHEAP_USERNAME"
_saveaccountconf_mutable NAMECHEAP_API_KEY "$NAMECHEAP_API_KEY"
_saveaccountconf_mutable NAMECHEAP_USERNAME "$NAMECHEAP_USERNAME"
return 0
}
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_primedns_info='PrimeDNS
Site: primedns.net
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_primedns
Options:
PRIMEDNS_API_KEY API Key. Scope acme:name:_acme-challenge.<hostname> for each name you issue (or dns:write)
OptionsAlt:
PRIMEDNS_API_URL API base URL. Optional, default https://api.primedns.net/v1
Issues: github.com/acmesh-official/acme.sh/issues/7330
Author: PrimeDNS <acme@primedns.net>
'
PRIMEDNS_Api_Default="https://api.primedns.net/v1"
######## Public functions #####################
#Usage: dns_primedns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_primedns_add() {
fulldomain=$(_idn "$1")
txtvalue=$2
PRIMEDNS_API_KEY="${PRIMEDNS_API_KEY:-$(_readaccountconf_mutable PRIMEDNS_API_KEY)}"
if [ -z "$PRIMEDNS_API_KEY" ]; then
PRIMEDNS_API_KEY=""
_err "You didn't specify a PrimeDNS API key (PRIMEDNS_API_KEY) yet."
_err "Create one in the PrimeDNS panel (Account -> API Keys) with the scope acme:name:_acme-challenge.<hostname> for each name you issue."
return 1
fi
PRIMEDNS_API_URL="${PRIMEDNS_API_URL:-$(_readaccountconf_mutable PRIMEDNS_API_URL)}"
#save the credentials to the account conf file.
_saveaccountconf_mutable PRIMEDNS_API_KEY "$PRIMEDNS_API_KEY"
if [ "$PRIMEDNS_API_URL" ]; then
_saveaccountconf_mutable PRIMEDNS_API_URL "$PRIMEDNS_API_URL"
fi
_info "Using PrimeDNS"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
if ! _primedns_rest POST "$fulldomain" "$txtvalue"; then
return 1
fi
_info "Added, OK"
return 0
}
#Usage: dns_primedns_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_primedns_rm() {
fulldomain=$(_idn "$1")
txtvalue=$2
PRIMEDNS_API_KEY="${PRIMEDNS_API_KEY:-$(_readaccountconf_mutable PRIMEDNS_API_KEY)}"
if [ -z "$PRIMEDNS_API_KEY" ]; then
PRIMEDNS_API_KEY=""
_err "You didn't specify a PrimeDNS API key (PRIMEDNS_API_KEY) yet."
return 1
fi
PRIMEDNS_API_URL="${PRIMEDNS_API_URL:-$(_readaccountconf_mutable PRIMEDNS_API_URL)}"
_info "Using PrimeDNS"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
if ! _primedns_rest DELETE "$fulldomain" "$txtvalue"; then
return 1
fi
_info "Removed, OK"
return 0
}
#################### Private functions below ##################################
#Usage: _primedns_rest POST|DELETE _acme-challenge.www.domain.com txtvalue
#The API creates/removes the TXT record with exactly the name given, so the full
#name acme.sh passes (_acme-challenge.<domain>, or the name of a DNS alias) is
#sent as it is.
_primedns_rest() {
_method=$1
_name=$2
_value=$3
_url="${PRIMEDNS_API_URL:-$PRIMEDNS_Api_Default}/dns/acme-challenge"
#The body is built by hand, so refuse anything that would need JSON escaping.
case "$_name$_value" in
*\"* | *\\*)
_err "Refusing to send '$_name': it contains a quote or a backslash."
return 1
;;
esac
if [ -z "$_name" ] || _is_idn "$_name"; then
_err "Cannot send '$_name': it is not a plain ASCII host name. For an IDN name install idn (acme.sh converts it to punycode) or pass the punycode name."
return 1
fi
_body="{\"name\":\"$_name\",\"value\":\"$_value\"}"
_debug _body "$_body"
_primedns_retried=""
while true; do
#acme.sh sends _H1.._H5 with every later request of this process, so the API key
#header must not stay set once this call is done (it would reach other hosts).
_primedns_old_h1="$_H1"
export _H1="X-API-Key: $PRIMEDNS_API_KEY"
: >"$HTTP_HEADER"
response="$(_post "$_body" "$_url" "" "$_method" "application/json")"
_ret=$?
export _H1="$_primedns_old_h1"
_status=$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d '\r\n')
if [ "$_ret" != "0" ]; then
_err "Error calling the PrimeDNS API ($_method /dns/acme-challenge)."
return 1
fi
response="$(echo "$response" | _normalizeJson)"
_debug response "$response"
_debug _status "$_status"
#Rate limited or temporary server error: wait and try once more.
if [ -z "$_primedns_retried" ] && { [ "$_status" = "429" ] || [ "$_status" = "502" ] || [ "$_status" = "503" ] || [ "$_status" = "504" ]; }; then
_wait="$(_retryafter_seconds <"$HTTP_HEADER")"
case "$_wait" in
'' | *[!0-9]*) _wait=5 ;;
esac
[ "$_wait" -gt 60 ] && _wait=60
_info "PrimeDNS API answered HTTP $_status, retrying in $_wait seconds."
_sleep "$_wait"
_primedns_retried=1
continue
fi
break
done
if _contains "$response" '"success":true'; then
return 0
fi
if [ "$_status" = "401" ]; then
_err "The PrimeDNS API refused the key (HTTP 401): it is wrong, revoked or expired. Check PRIMEDNS_API_KEY."
elif _contains "$response" 'IP_NOT_ALLOWED'; then
_err "This PrimeDNS API key may not be used from this address (allowed_ips). Add the address to the key or run acme.sh from an allowed host."
elif _contains "$response" 'NAME_NOT_ALLOWED'; then
_err "This PrimeDNS API key may not write $_name: add the scope acme:name:$_name to the key, or the zone to its allowed zones."
elif _contains "$response" 'INSUFFICIENT_SCOPE'; then
_err "This PrimeDNS API key needs the scope acme:name:$_name (or dns:write)."
elif _contains "$response" 'DOMAIN_NOT_FOUND'; then
_err "$_name is not in a zone of the PrimeDNS account that owns this API key."
elif [ "$_status" = "429" ]; then
_err "PrimeDNS API rate limit still hit after a retry. Try again in a minute."
else
_err "PrimeDNS API error (HTTP $_status): $response"
fi
return 1
}
+6 -3
View File
@@ -106,13 +106,16 @@ dns_yc_add() {
_debug _domain "$_domain"
_debug "Getting txt records"
if ! _yc_rest GET "zones/${_domain_id}:getRecordSet?type=TXT&name=$_sub_domain"; then
if ! _yc_rest GET "zones/${_domain_id}:getRecordSet?type=TXT&name=$fulldomain"; then
_err "Error: $response"
return 1
fi
_info "Adding record"
if _yc_rest POST "zones/$_domain_id:upsertRecordSets" "{\"merges\": [ { \"name\":\"$_sub_domain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":[\"$txtvalue\"]}]}"; then
# The absolute name (trailing dot) also covers a record at the zone apex,
# e.g. with --domain-alias, which has no relative form: an empty name is
# rejected by the API.
if _yc_rest POST "zones/$_domain_id:upsertRecordSets" "{\"merges\": [ { \"name\":\"$fulldomain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":[\"$txtvalue\"]}]}"; then
if _contains "$response" "\"done\": *true"; then
_info "Added, OK"
return 0
@@ -167,7 +170,7 @@ dns_yc_rm() {
# upsertRecordSets.deletions removes only the given value from the rrset,
# leaving any other values at the same name (e.g. base + wildcard domain)
# intact -- no need to read the current data set and recompute it.
if _yc_rest POST "zones/$_domain_id:upsertRecordSets" "{\"deletions\": [ { \"name\":\"$_sub_domain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":[\"$txtvalue\"]}]}"; then
if _yc_rest POST "zones/$_domain_id:upsertRecordSets" "{\"deletions\": [ { \"name\":\"$fulldomain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":[\"$txtvalue\"]}]}"; then
if _contains "$response" "\"done\": *true"; then
_info "Delete, OK"
return 0
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.