Compare commits

..
11 Commits
Author SHA1 Message Date
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
8 changed files with 686 additions and 18 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
}
+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.