Merge pull request #7086 from acmesh-official/dev
Some checks failed
Apache / Apache (push) Has been cancelled
DNS / CheckToken (push) Has been cancelled
DragonFlyBSD / DragonFlyBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
FreeBSD / FreeBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
FreeBSD / FreeBSD (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
GhostBSD / GhostBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
GhostBSD / GhostBSD (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Haiku / Haiku (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Haiku / Haiku (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Linux / Linux (almalinux:latest) (push) Has been cancelled
Linux / Linux (alpine:latest) (push) Has been cancelled
Linux / Linux (archlinux:latest) (push) Has been cancelled
Linux / Linux (debian:latest) (push) Has been cancelled
Linux / Linux (fedora:latest) (push) Has been cancelled
Linux / Linux (gentoo/stage3) (push) Has been cancelled
Linux / Linux (kalilinux/kali) (push) Has been cancelled
Linux / Linux (opensuse/leap:latest) (push) Has been cancelled
Linux / Linux (oraclelinux:8) (push) Has been cancelled
Linux / Linux (ubuntu:latest) (push) Has been cancelled
MacOS / MacOS (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
MidnightBSD / MidnightBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
NetBSD / NetBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Nginx / Nginx (push) Has been cancelled
Omnios / Omnios (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Omnios / Omnios (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenBSD / OpenBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenBSD / OpenBSD (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenIndiana / OpenIndiana (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenIndiana / OpenIndiana (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
PebbleStrict / PebbleStrict (push) Has been cancelled
PebbleStrict / PebbleStrict_IPCert (push) Has been cancelled
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Has been cancelled
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Has been cancelled
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Build DockerHub / CheckToken (push) Has been cancelled
Shellcheck / ShellCheck (push) Has been cancelled
Shellcheck / shfmt (push) Has been cancelled
DNS / Fail (push) Has been cancelled
DNS / Docker (push) Has been cancelled
DNS / MacOS (push) Has been cancelled
DNS / Windows (push) Has been cancelled
DNS / FreeBSD (push) Has been cancelled
DNS / GhostBSD (push) Has been cancelled
DNS / OpenBSD (push) Has been cancelled
DNS / NetBSD (push) Has been cancelled
DNS / DragonFlyBSD (push) Has been cancelled
DNS / MidnightBSD (push) Has been cancelled
DNS / Solaris (push) Has been cancelled
DNS / Omnios (push) Has been cancelled
DNS / OpenIndiana (push) Has been cancelled
DNS / Tribblix (push) Has been cancelled
DNS / Haiku (push) Has been cancelled
Build DockerHub / build (push) Has been cancelled

sync
This commit is contained in:
neil 2026-07-06 10:37:29 +08:00 committed by GitHub
commit f50401a342
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 2028 additions and 286 deletions

50
.github/workflows/Apache.yml vendored Normal file
View file

@ -0,0 +1,50 @@
name: Apache
on:
push:
paths:
- '*.sh'
- '.github/workflows/Apache.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/Apache.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
Apache:
runs-on: ubuntu-latest
env:
TestingDomain: example.com
TEST_ACME_Server: https://localhost:14000/dir
HTTPS_INSECURE: 1
TEST_LOCAL: 1
TEST_CA: "Pebble Intermediate CA"
TEST_APACHE: 1
CASE: le_test_apache
steps:
- uses: actions/checkout@v6
- name: Install tools
run: sudo apt-get install -y socat apache2
- name: Run Pebble
run: cd .. && curl https://raw.githubusercontent.com/letsencrypt/pebble/master/docker-compose.yml >docker-compose.yml && docker compose up -d
- name: Set up Pebble
run: curl --request POST --data '{"ip":"10.30.50.1"}' http://localhost:8055/set-default-ipv4
- name: Set up Apache
# Apache serves on 5002, which is the HTTP-01 validation port in
# Pebble's default config; acme.sh appends the challenge Alias to
# the main config itself
run: |
echo "Listen 5002" | sudo tee /etc/apache2/ports.conf
sudo sed -i "s/\*:80/*:5002/" /etc/apache2/sites-available/000-default.conf
sudo apache2ctl configtest
sudo systemctl restart apache2
curl -s -o /dev/null -w "%{http_code}" -H "Host: example.com" http://127.0.0.1:5002/ | grep -E "200|403|404"
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- name: Run acmetest
run: cd ../acmetest && sudo --preserve-env ./letest.sh

View file

@ -26,9 +26,9 @@ jobs:
id: step_one
run: |
if [ "${{secrets.TokenName1}}" ] ; then
echo "::set-output name=hasToken::true"
echo "hasToken=true" >> "$GITHUB_OUTPUT"
else
echo "::set-output name=hasToken::false"
echo "hasToken=false" >> "$GITHUB_OUTPUT"
fi
- name: Check the value
run: echo ${{ steps.step_one.outputs.hasToken }}
@ -116,7 +116,9 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Install tools
run: brew install socat
run: |
brew untap aws/tap || true
brew install socat
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- name: Run acmetest
@ -176,9 +178,14 @@ jobs:
C:\tools\cygwin\cygwinsetup.exe -qgnNdO -R C:/tools/cygwin -s https://mirrors.kernel.org/sourceware/cygwin/ -P socat,curl,cron,unzip,git
shell: cmd
- name: Set ENV
shell: cmd
shell: bash
run: |
echo PATH=C:\tools\cygwin\bin;C:\tools\cygwin\usr\bin >> %GITHUB_ENV%
echo 'PATH=C:\tools\cygwin\bin;C:\tools\cygwin\usr\bin' >> "$GITHUB_ENV"
# cygwin git sees the runner workspace as owned by another user and
# fails with "dubious ownership" (exit 128) in the checkout post step
echo 'GIT_CONFIG_COUNT=1' >> "$GITHUB_ENV"
echo 'GIT_CONFIG_KEY_0=safe.directory' >> "$GITHUB_ENV"
echo 'GIT_CONFIG_VALUE_0=*' >> "$GITHUB_ENV"
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- name: Run acmetest

66
.github/workflows/Nginx.yml vendored Normal file
View file

@ -0,0 +1,66 @@
name: Nginx
on:
push:
paths:
- '*.sh'
- '.github/workflows/Nginx.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/Nginx.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
Nginx:
runs-on: ubuntu-latest
env:
TestingDomain: example.com
TEST_ACME_Server: https://localhost:14000/dir
HTTPS_INSECURE: 1
TEST_LOCAL: 1
TEST_CA: "Pebble Intermediate CA"
TEST_NGINX: 1
CASE: le_test_nginx
steps:
- uses: actions/checkout@v6
- name: Install tools
run: sudo apt-get install -y socat nginx
- name: Run Pebble
run: cd .. && curl https://raw.githubusercontent.com/letsencrypt/pebble/master/docker-compose.yml >docker-compose.yml && docker compose up -d
- name: Set up Pebble
run: curl --request POST --data '{"ip":"10.30.50.1"}' http://localhost:8055/set-default-ipv4
- name: Set up nginx
# a backend on 8081 plus a site with an aaPanel/BT style
# "location ^~ /" proxy block that shadows plain regex locations
# (regression for #6125); the site listens on 5002, which is the
# HTTP-01 validation port in Pebble's default config
run: |
sudo tee /etc/nginx/sites-available/default >/dev/null <<'EOF'
server {
listen 127.0.0.1:8081;
location / {
default_type text/plain;
return 200 "backend";
}
}
server {
listen 5002 default_server;
server_name example.com;
location ^~ / {
proxy_pass http://127.0.0.1:8081;
proxy_set_header Host $http_host;
}
}
EOF
sudo nginx -t
sudo systemctl restart nginx
curl -s -H "Host: example.com" http://127.0.0.1:5002/ | grep backend
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- name: Run acmetest
run: cd ../acmetest && sudo --preserve-env ./letest.sh

570
acme.sh
View file

@ -918,6 +918,15 @@ _json_decode() {
echo "$_j_str"
}
#extract the authorization URLs from an order response on stdin, as a
#comma-separated list. The entries are quoted URL strings and a quote cannot
#occur inside a URL, so the first '"]' is always the end of the array. A
#char-class scan would stop early on the brackets of an IPv6 host
#(https://[2001:db8::1]/...). Outputs nothing if the field is missing.
_authorizations_from_order() {
sed -n 's/.*"authorizations" *: *\[//p' | sed 's/" *\].*//' | tr -d '" '
}
#options file
_sed_i() {
options="$1"
@ -1179,6 +1188,11 @@ _createkey() {
length="$1"
f="$2"
_debug2 "_createkey for file:$f"
if ! _exists "${ACME_OPENSSL_BIN:-openssl}"; then
_err "Please install openssl first. ACME_OPENSSL_BIN=$ACME_OPENSSL_BIN"
_err "We need openssl to generate keys."
return 1
fi
eccname="$length"
if _startswith "$length" "ec-"; then
length=$(printf "%s" "$length" | cut -d '-' -f 2-100)
@ -1201,6 +1215,7 @@ _createkey() {
_debug "Using length $length"
_new_key_file=""
if ! [ -e "$f" ]; then
if ! touch "$f" >/dev/null 2>&1; then
_f_path="$(dirname "$f")"
@ -1214,6 +1229,7 @@ _createkey() {
return 1
fi
chmod 600 "$f"
_new_key_file="1"
fi
if _isEccKey "$length"; then
@ -1222,6 +1238,10 @@ _createkey() {
echo "$_opkey" >"$f"
else
_err "Error encountered for ECC key named $eccname"
#do not leave an empty file behind, or the next run would treat the key as existing
if [ "$_new_key_file" ]; then
rm -f "$f"
fi
return 1
fi
else
@ -1234,6 +1254,10 @@ _createkey() {
echo "$_opkey" >"$f"
else
_err "Error encountered for RSA key of length $length"
#do not leave an empty file behind, or the next run would treat the key as existing
if [ "$_new_key_file" ]; then
rm -f "$f"
fi
return 1
fi
fi
@ -1283,6 +1307,23 @@ _idn() {
}
#_createcsr cn san_list keyfile csrfile conf acmeValidationv1 extendedUsage
#cn
#The x509 Common Name is limited to 64 characters (RFC 5280 ub-common-name,
#enforced by openssl in ASN1_mbstring_ncopy), and an IP address or an empty
#name is not usable as CN either. When this rejects the name, _createcsr
#omits CN from the CSR subject and the CA takes the identifiers from the
#subjectAltName extension (issue 4867).
_is_valid_cn() {
_cn_v="$1"
if [ -z "$_cn_v" ] || [ "${#_cn_v}" -gt 64 ]; then
return 1
fi
if _isIP "$_cn_v"; then
return 1
fi
return 0
}
_createcsr() {
_debug _createcsr
domain="$1"
@ -1346,16 +1387,16 @@ _createcsr() {
_csr_cn="$(_idn "$domain")"
_debug2 _csr_cn "$_csr_cn"
if _contains "$(uname -a)" "MINGW"; then
if _isIP "$_csr_cn"; then
${ACME_OPENSSL_BIN:-openssl} req -new -sha256 -key "$csrkey" -subj "//O=$PROJECT_NAME" -config "$csrconf" -out "$csr"
else
if _is_valid_cn "$_csr_cn"; then
${ACME_OPENSSL_BIN:-openssl} req -new -sha256 -key "$csrkey" -subj "//CN=$_csr_cn" -config "$csrconf" -out "$csr"
else
${ACME_OPENSSL_BIN:-openssl} req -new -sha256 -key "$csrkey" -subj "//O=$PROJECT_NAME" -config "$csrconf" -out "$csr"
fi
else
if _isIP "$_csr_cn"; then
${ACME_OPENSSL_BIN:-openssl} req -new -sha256 -key "$csrkey" -subj "/O=$PROJECT_NAME" -config "$csrconf" -out "$csr"
else
if _is_valid_cn "$_csr_cn"; then
${ACME_OPENSSL_BIN:-openssl} req -new -sha256 -key "$csrkey" -subj "/CN=$_csr_cn" -config "$csrconf" -out "$csr"
else
${ACME_OPENSSL_BIN:-openssl} req -new -sha256 -key "$csrkey" -subj "/O=$PROJECT_NAME" -config "$csrconf" -out "$csr"
fi
fi
}
@ -1381,7 +1422,9 @@ _readSubjectFromCSR() {
_usage "_readSubjectFromCSR mycsr.csr"
return 1
fi
${ACME_OPENSSL_BIN:-openssl} req -noout -in "$_csrfile" -subject | tr ',' "\n" | _egrep_o "CN *=.*" | cut -d = -f 2 | cut -d / -f 1 | tr -d ' \n'
# -config /dev/null: reading a CSR needs no config, but a missing default
# openssl.cnf is fatal on some systems (e.g. NetBSD does not install one)
${ACME_OPENSSL_BIN:-openssl} req -noout -in "$_csrfile" -subject -config /dev/null | tr ',' "\n" | _egrep_o "CN *=.*" | cut -d = -f 2 | cut -d / -f 1 | tr -d ' \n'
}
#_csrfile
@ -1396,15 +1439,16 @@ _readSubjectAltNamesFromCSR() {
_csrsubj="$(_readSubjectFromCSR "$_csrfile")"
_debug _csrsubj "$_csrsubj"
_dnsAltnames="$(${ACME_OPENSSL_BIN:-openssl} req -noout -text -in "$_csrfile" | grep "^ *DNS:.*" | tr -d ' \n')"
_dnsAltnames="$(${ACME_OPENSSL_BIN:-openssl} req -noout -text -in "$_csrfile" -config /dev/null | grep "^ *DNS:.*" | tr -d ' \n')"
_debug _dnsAltnames "$_dnsAltnames"
if _contains "$_dnsAltnames," "DNS:$_csrsubj,"; then
# escape the wildcard '*' so it is not taken as a regex operator by grep/sed below
_excapedAlgnames="$(echo "$_dnsAltnames" | tr '*' '#')"
_debug _excapedAlgnames "$_excapedAlgnames"
_escapedSubject="$(echo "$_csrsubj" | tr '*' '#')"
_debug _escapedSubject "$_escapedSubject"
if _contains "$_excapedAlgnames," "DNS:$_escapedSubject,"; then
_debug "AltNames contains subject"
_excapedAlgnames="$(echo "$_dnsAltnames" | tr '*' '#')"
_debug _excapedAlgnames "$_excapedAlgnames"
_escapedSubject="$(echo "$_csrsubj" | tr '*' '#')"
_debug _escapedSubject "$_escapedSubject"
_dnsAltnames="$(echo "$_excapedAlgnames," | sed "s/DNS:$_escapedSubject,//g" | tr '#' '*' | sed "s/,\$//g")"
_debug _dnsAltnames "$_dnsAltnames"
else
@ -1422,7 +1466,7 @@ _readKeyLengthFromCSR() {
return 1
fi
_outcsr="$(${ACME_OPENSSL_BIN:-openssl} req -noout -text -in "$_csrfile")"
_outcsr="$(${ACME_OPENSSL_BIN:-openssl} req -noout -text -in "$_csrfile" -config /dev/null)"
_debug2 _outcsr "$_outcsr"
if _contains "$_outcsr" "Public Key Algorithm: id-ecPublicKey"; then
_debug "ECC CSR"
@ -1503,6 +1547,22 @@ _toPkcs() {
}
_toPkcs8() {
_cpkcs8="$1"
_ckey="$2"
pkcs8Password="$3"
if [ "$pkcs8Password" ]; then
${ACME_OPENSSL_BIN:-openssl} pkcs8 -topk8 -inform PEM -outform PEM -v2 aes256 -passout "pass:$pkcs8Password" -in "$_ckey" -out "$_cpkcs8"
else
${ACME_OPENSSL_BIN:-openssl} pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in "$_ckey" -out "$_cpkcs8"
fi
if [ "$?" = "0" ]; then
_savedomainconf "Le_PKCS8Password" "$pkcs8Password" "base64"
fi
}
#domain [password] [isEcc]
toPkcs() {
domain="$1"
@ -1524,20 +1584,21 @@ toPkcs() {
}
#domain [isEcc]
#domain [password] [isEcc]
toPkcs8() {
domain="$1"
pkcs8Password="$2"
if [ -z "$domain" ]; then
_usage "Usage: $PROJECT_ENTRY --to-pkcs8 --domain <domain.tld> [--ecc]"
_usage "Usage: $PROJECT_ENTRY --to-pkcs8 --domain <domain.tld> [--password <password>] [--ecc]"
return 1
fi
_isEcc="$2"
_isEcc="$3"
_initpath "$domain" "$_isEcc"
${ACME_OPENSSL_BIN:-openssl} pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in "$CERT_KEY_PATH" -out "$CERT_PKCS8_PATH"
_toPkcs8 "$CERT_PKCS8_PATH" "$CERT_KEY_PATH" "$pkcs8Password"
if [ "$?" = "0" ]; then
_info "Success, $CERT_PKCS8_PATH"
@ -1871,12 +1932,13 @@ _date2time() {
if gdate -u -d "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then
return
fi
#Omnios
if python3 -c "import datetime; print(int(datetime.datetime.strptime(\"$1\", \"%Y-%m-%d %H:%M:%S\").replace(tzinfo=datetime.timezone.utc).timestamp()))" 2>/dev/null; then
#Omnios. Pass the date as argv (sys.argv[1]) instead of interpolating it into
#the -c program text, so a quote in the input cannot inject Python code.
if python3 -c "import datetime,sys; print(int(datetime.datetime.strptime(sys.argv[1], \"%Y-%m-%d %H:%M:%S\").replace(tzinfo=datetime.timezone.utc).timestamp()))" "$1" 2>/dev/null; then
return
fi
#Omnios
if python3 -c "import datetime; print(int(datetime.datetime.strptime(\"$1\", \"%Y-%m-%dT%H:%M:%SZ\").replace(tzinfo=datetime.timezone.utc).timestamp()))" 2>/dev/null; then
if python3 -c "import datetime,sys; print(int(datetime.datetime.strptime(sys.argv[1], \"%Y-%m-%dT%H:%M:%SZ\").replace(tzinfo=datetime.timezone.utc).timestamp()))" "$1" 2>/dev/null; then
return
fi
_err "Cannot parse _date2time $1"
@ -2834,14 +2896,45 @@ __initHome() {
_debug "Using default home: $DEFAULT_INSTALL_HOME"
LE_WORKING_DIR="$DEFAULT_INSTALL_HOME"
fi
# Convert a relative --home to an absolute path: later code cd's around
# (e.g. installOnline extracts and enters the archive dir), where a
# relative path would point into the wrong directory.
# https://github.com/acmesh-official/acme.sh/issues/6477
case "$LE_WORKING_DIR" in
/*) ;;
*)
if [ -d "$LE_WORKING_DIR" ]; then
LE_WORKING_DIR="$(cd "$LE_WORKING_DIR" && pwd)"
fi
;;
esac
export LE_WORKING_DIR
if [ -z "$LE_CONFIG_HOME" ]; then
LE_CONFIG_HOME="$LE_WORKING_DIR"
fi
case "$LE_CONFIG_HOME" in
/*) ;;
*)
if [ -d "$LE_CONFIG_HOME" ]; then
LE_CONFIG_HOME="$(cd "$LE_CONFIG_HOME" && pwd)"
fi
;;
esac
_debug "Using config home: $LE_CONFIG_HOME"
export LE_CONFIG_HOME
# Paths with whitespace break the unquoted $_CURL/$_WGET command expansion,
# so fail early with a clear error instead of a cryptic curl/wget failure.
# https://github.com/acmesh-official/acme.sh/issues/2163
case "$LE_WORKING_DIR$LE_CONFIG_HOME" in
*" "*)
_err "The --home or --config-home path can not contain spaces: '$LE_WORKING_DIR'"
_err "Please install $PROJECT_NAME to a path without spaces."
exit 1
;;
esac
_DEFAULT_ACCOUNT_CONF_PATH="$LE_CONFIG_HOME/account.conf"
if [ -z "$ACCOUNT_CONF_PATH" ]; then
@ -2937,6 +3030,11 @@ _initAPI() {
return 0
fi
_err "Cannot init API for $_api_server"
if [ "$_api_server" = "$CA_ZEROSSL" ]; then
_info "$(__green "If this host is IPv6-only: ZeroSSL currently has no IPv6 endpoint.")"
_info "$(__green "Try another CA, e.g.: $PROJECT_ENTRY --set-default-ca --server letsencrypt")"
_info "See: $(__green "https://github.com/acmesh-official/acme.sh/issues/6872")"
fi
return 1
}
@ -3367,9 +3465,14 @@ _setNginx() {
fi
echo "$NGINX_START
location ~ \"^/\.well-known/acme-challenge/([-_a-zA-Z0-9]+)\$\" {
default_type text/plain;
return 200 \"\$1.$_thumbpt\";
location ^~ /.well-known/acme-challenge/ {
# the ^~ prefix wins over regex-skipping blocks like \"location ^~ /\",
# the nested regex location still captures the token as \$1
location ~ \"^/\.well-known/acme-challenge/([-_a-zA-Z0-9]+)\$\" {
default_type text/plain;
return 200 \"\$1.$_thumbpt\";
}
return 404;
}
#NGINX_START
" >>"$FOUND_REAL_NGINX_CONF"
@ -3513,7 +3616,7 @@ _restoreNginx() {
done
_info "Reloading nginx"
if ! nginx -s reload >/dev/null; then
if ! nginx -s reload >/dev/null 2>&1; then
_err "An error occurred while reloading nginx, please open an issue on $PROJECT."
return 1
fi
@ -3817,10 +3920,10 @@ _on_issue_success() {
#account_key_length eab-kid eab-hmac-key
registeraccount() {
_account_key_length="$1"
_eab_id="$2"
_eab_kid="$2"
_eab_hmac_key="$3"
_initpath
_regAccount "$_account_key_length" "$_eab_id" "$_eab_hmac_key"
_regAccount "$_account_key_length" "$_eab_kid" "$_eab_hmac_key"
}
__calcAccountKeyHash() {
@ -3831,6 +3934,16 @@ __calc_account_thumbprint() {
printf "%s" "$jwk" | tr -d ' ' | _digest "sha256" | _url_replace
}
#Reads a comma- or space-separated email list from stdin and prints
#the ACME contact list items: "mailto:a@example.com","mailto:b@example.com"
_mailto_contacts() {
_mc_out=""
for _mc_m in $(tr ',' ' '); do
_mc_out="$_mc_out,\"mailto:$_mc_m\""
done
echo "$_mc_out" | cut -c 2-
}
_getAccountEmail() {
if [ "$ACCOUNT_EMAIL" ]; then
echo "$ACCOUNT_EMAIL"
@ -3850,14 +3963,14 @@ _getAccountEmail() {
_regAccount() {
_initpath
_reg_length="$1"
_eab_id="$2"
_eab_kid="$2"
_eab_hmac_key="$3"
_debug3 _regAccount "$_regAccount"
_initAPI
mkdir -p "$CA_DIR"
if [ ! -f "$ACCOUNT_KEY_PATH" ]; then
if [ ! -s "$ACCOUNT_KEY_PATH" ]; then
if ! _create_account_key "$_reg_length"; then
_err "Error creating account key."
return 1
@ -3867,13 +3980,13 @@ _regAccount() {
if ! _calcjwk "$ACCOUNT_KEY_PATH"; then
return 1
fi
if [ "$_eab_id" ] && [ "$_eab_hmac_key" ]; then
_savecaconf CA_EAB_KEY_ID "$_eab_id"
if [ "$_eab_kid" ] && [ "$_eab_hmac_key" ]; then
_savecaconf CA_EAB_KEY_ID "$_eab_kid"
_savecaconf CA_EAB_HMAC_KEY "$_eab_hmac_key"
fi
_eab_id=$(_readcaconf "CA_EAB_KEY_ID")
_eab_kid=$(_readcaconf "CA_EAB_KEY_ID")
_eab_hmac_key=$(_readcaconf "CA_EAB_HMAC_KEY")
_secure_debug3 _eab_id "$_eab_id"
_secure_debug3 _eab_kid "$_eab_kid"
_secure_debug3 _eab_hmac_key "$_eab_hmac_key"
_email="$(_getAccountEmail)"
if [ "$_email" ]; then
@ -3881,7 +3994,7 @@ _regAccount() {
fi
if [ "$ACME_DIRECTORY" = "$CA_ZEROSSL" ]; then
if [ -z "$_eab_id" ] || [ -z "$_eab_hmac_key" ]; then
if [ -z "$_eab_kid" ] || [ -z "$_eab_hmac_key" ]; then
_info "No EAB credentials found for ZeroSSL, let's obtain them"
if [ -z "$_email" ]; then
_info "$(__green "$PROJECT_NAME is using ZeroSSL as default CA now.")"
@ -3890,17 +4003,19 @@ _regAccount() {
_info "See: $(__green "$_ZEROSSL_WIKI")"
return 1
fi
_eabresp=$(_post "email=$_email" $_ZERO_EAB_ENDPOINT)
#the ZeroSSL EAB endpoint takes a single address, use the first one
_eab_email="$(echo "$_email" | tr ',' ' ' | awk '{print $1}')"
_eabresp=$(_post "email=$_eab_email" $_ZERO_EAB_ENDPOINT)
if [ "$?" != "0" ]; then
_debug2 "$_eabresp"
_err "Cannot get EAB credentials from ZeroSSL."
return 1
fi
_secure_debug2 _eabresp "$_eabresp"
_eab_id="$(echo "$_eabresp" | tr ',}' '\n\n' | grep '"eab_kid"' | cut -d : -f 2 | tr -d '"')"
_secure_debug2 _eab_id "$_eab_id"
if [ -z "$_eab_id" ]; then
_err "Cannot resolve _eab_id"
_eab_kid="$(echo "$_eabresp" | tr ',}' '\n\n' | grep '"eab_kid"' | cut -d : -f 2 | tr -d '"')"
_secure_debug2 _eab_kid "$_eab_kid"
if [ -z "$_eab_kid" ]; then
_err "Cannot resolve _eab_kid"
return 1
fi
_eab_hmac_key="$(echo "$_eabresp" | tr ',}' '\n\n' | grep '"eab_hmac_key"' | cut -d : -f 2 | tr -d '"')"
@ -3909,12 +4024,12 @@ _regAccount() {
_err "Cannot resolve _eab_hmac_key"
return 1
fi
_savecaconf CA_EAB_KEY_ID "$_eab_id"
_savecaconf CA_EAB_KEY_ID "$_eab_kid"
_savecaconf CA_EAB_HMAC_KEY "$_eab_hmac_key"
fi
fi
if [ "$_eab_id" ] && [ "$_eab_hmac_key" ]; then
eab_protected="{\"alg\":\"HS256\",\"kid\":\"$_eab_id\",\"url\":\"${ACME_NEW_ACCOUNT}\"}"
if [ "$_eab_kid" ] && [ "$_eab_hmac_key" ]; then
eab_protected="{\"alg\":\"HS256\",\"kid\":\"$_eab_kid\",\"url\":\"${ACME_NEW_ACCOUNT}\"}"
_debug3 eab_protected "$eab_protected"
eab_protected64=$(printf "%s" "$eab_protected" | _base64 | _url_replace)
@ -3928,6 +4043,10 @@ _regAccount() {
key_hex="$(_durl_replace_base64 "$_eab_hmac_key" | _dbase64 | _hex_dump | tr -d ' ')"
_debug3 key_hex "$key_hex"
if [ -z "$key_hex" ]; then
_err "Cannot base64-decode the eab-hmac-key. Please check the value, and your openssl version."
return 1
fi
eab_signature=$(printf "%s" "$eab_sign_t" | _hmac sha256 $key_hex | _base64 | _url_replace)
_debug3 eab_signature "$eab_signature"
@ -3936,7 +4055,7 @@ _regAccount() {
_debug3 externalBinding "$externalBinding"
fi
if [ "$_email" ]; then
email_sg="\"contact\": [\"mailto:$_email\"], "
email_sg="\"contact\": [$(echo "$_email" | _mailto_contacts)], "
fi
regjson="{$email_sg\"termsOfServiceAgreed\": true$externalBinding}"
@ -3980,7 +4099,9 @@ _regAccount() {
_debug "Calc CA_KEY_HASH" "$CA_KEY_HASH"
_savecaconf CA_KEY_HASH "$CA_KEY_HASH"
if [ "$code" = '403' ]; then
#RFC 8555 sec 7.3.6 requires 401 for requests from a deactivated account,
#but Boulder (Let's Encrypt) historically returns 403. Accept both.
if [ "$code" = '403' ] || [ "$code" = '401' ]; then
_err "It seems that the account key has been deactivated, please use a new account key."
return 1
fi
@ -4014,7 +4135,7 @@ updateaccount() {
_email="$(_getAccountEmail)"
if [ "$_email" ]; then
updjson='{"contact": ["mailto:'$_email'"]}'
updjson='{"contact": ['$(echo "$_email" | _mailto_contacts)']}'
else
updjson='{"contact": []}'
fi
@ -4033,6 +4154,93 @@ updateaccount() {
fi
}
#Implement account key rollover
updateaccountkey() {
_length="$1"
_initpath
if [ ! -f "$ACCOUNT_KEY_PATH" ]; then
_err "Account key not found at: $ACCOUNT_KEY_PATH"
return 1
fi
ACCOUNT_KEY_PATH_NEW="$ACCOUNT_KEY_PATH.new"
_accUri=$(_readcaconf "ACCOUNT_URL")
_debug _accUri "$_accUri"
if [ -z "$_accUri" ]; then
_err "The account URL is empty, please run '--update-account' first to update the account info, then try again."
return 1
fi
if ! _calcjwk "$ACCOUNT_KEY_PATH"; then
return 1
fi
_inner_payload="{\"account\": \"$_accUri\", \"oldKey\": $jwk}"
_initAPI
if [ -z "$ACME_KEY_CHANGE" ]; then
_err "Server does not expose keyChange url."
return 1
fi
_url="$ACME_KEY_CHANGE"
if _createkey "$_length" "$ACCOUNT_KEY_PATH_NEW"; then
_info "New account key creation OK."
else
_err "New account key creation error."
return 1
fi
if ! _calcjwk "$ACCOUNT_KEY_PATH_NEW"; then
rm -f "$ACCOUNT_KEY_PATH_NEW"
return 1
fi
_inner_protected="{\"url\": \"${_url}$JWK_HEADERPLACE_PART2, \"jwk\": $jwk"'}'
_inner_protected64="$(printf "%s" "$_inner_protected" | _base64 | _url_replace)"
_inner_payload64="$(printf "%s" "$_inner_payload" | _base64 | _url_replace)"
if ! _inner_sig_t="$(printf "%s" "$_inner_protected64.$_inner_payload64" | _sign "$ACCOUNT_KEY_PATH_NEW" "sha256")"; then
_err "Sign request failed."
rm -f "$ACCOUNT_KEY_PATH_NEW"
return 1
fi
_debug3 _inner_sig_t "$_inner_sig_t"
_inner_sig="$(printf "%s" "$_inner_sig_t" | _url_replace)"
_debug3 _inner_sig "$_inner_sig"
_body="{\"protected\": \"$_inner_protected64\", \"payload\": \"$_inner_payload64\", \"signature\": \"$_inner_sig\"}"
if ! _send_signed_request "$_url" "$_body" "" "$ACCOUNT_KEY_PATH"; then
_err "Error rotating account key: $response."
rm -f "$ACCOUNT_KEY_PATH_NEW"
return 1
fi
if [ "$code" = '200' ]; then
echo "$response" >"$ACCOUNT_JSON_PATH"
mv -f "$ACCOUNT_KEY_PATH_NEW" "$ACCOUNT_KEY_PATH"
_info "Account key rotation success for $_accUri."
elif [ "$code" = "409" ]; then
_err "An existing account is using the new key"
rm -f "$ACCOUNT_KEY_PATH_NEW"
return 1
else
_err "Account key rollover error: $response"
rm -f "$ACCOUNT_KEY_PATH_NEW"
return 1
fi
__CACHED_JWK_KEY_FILE=""
_calcjwk "$ACCOUNT_KEY_PATH"
ACCOUNT_THUMBPRINT="$(__calc_account_thumbprint)"
_info "ACCOUNT_THUMBPRINT" "$ACCOUNT_THUMBPRINT"
CA_KEY_HASH="$(__calcAccountKeyHash)"
_debug "Calc CA_KEY_HASH" "$CA_KEY_HASH"
_savecaconf CA_KEY_HASH "$CA_KEY_HASH"
}
#Implement deactivate account
deactivateaccount() {
_initpath
@ -4060,7 +4268,8 @@ deactivateaccount() {
if _send_signed_request "$_accUri" "$_djson" && _contains "$response" '"deactivated"'; then
_info "Successfully deactivated account $_accUri."
_accid=$(echo "$response" | _egrep_o "\"id\" *: *[^,]*," | cut -d : -f 2 | tr -d ' ,')
elif [ "$code" = "403" ]; then
elif [ "$code" = "403" ] || [ "$code" = "401" ]; then
#RFC 8555 sec 7.3.6: 401 from a deactivated account; Boulder returns 403
_info "The account is already deactivated."
_accid=$(_getfield "$_accUri" "999" "/")
else
@ -4533,16 +4742,25 @@ _match_issuer() {
#ip
_isIPv4() {
for seg in $(echo "$1" | tr '.' ' '); do
_debug2 seg "$seg"
if [ "$(echo "$seg" | tr -d '[0-9]')" ]; then
#not all number
#splitting must not glob: a "*" segment would match files in cwd
set -f
_ipv4_saved_ifs="$IFS"
IFS='.'
# shellcheck disable=SC2086
set -- $1
IFS="$_ipv4_saved_ifs"
set +f
if [ $# -ne 4 ]; then
return 1
fi
for _ipv4_seg in "$@"; do
_debug2 _ipv4_seg "$_ipv4_seg"
case "$_ipv4_seg" in
*[!0-9]* | "") return 1 ;;
esac
if [ "${#_ipv4_seg}" -gt 3 ] || [ "$_ipv4_seg" -gt 255 ]; then
return 1
fi
if [ $seg -ge 0 ] && [ $seg -lt 256 ]; then
continue
fi
return 1
done
return 0
}
@ -4743,7 +4961,7 @@ issue() {
_debug2 _saved_account_key_hash "$_saved_account_key_hash"
if [ -z "$ACCOUNT_URL" ] || [ -z "$_saved_account_key_hash" ] || [ "$_saved_account_key_hash" != "$(__calcAccountKeyHash)" ]; then
if ! _regAccount "$_accountkeylength"; then
if ! _regAccount "$_accountkeylength" "$_eab_kid" "$_eab_hmac_key"; then
_on_issue_err "$_post_hook"
return 1
fi
@ -4935,7 +5153,7 @@ issue() {
#for dns manual mode
_savedomainconf "Le_OrderFinalize" "$Le_OrderFinalize"
_authorizations_seg="$(echo "$response" | _json_decode | _egrep_o '"authorizations" *: *\[[^\[]*\]' | cut -d '[' -f 2 | tr -d ']' | tr -d '"')"
_authorizations_seg="$(echo "$response" | _json_decode | _authorizations_from_order)"
_debug2 _authorizations_seg "$_authorizations_seg"
if [ -z "$_authorizations_seg" ]; then
_err "_authorizations_seg not found."
@ -5751,6 +5969,21 @@ $_authorizations_map"
fi
fi
# Warn when the scheduled renewal falls after the cert has already expired,
# e.g. a 1-day cert from an internal CA combined with the default 30-day
# schedule, which computes from the creation date and never looks at
# notAfter. Skip the warning for a fixed-date --valid-to: there
# Le_NextRenewTime equals the expiry by design and the non-renewable state
# was already reported above. https://github.com/acmesh-official/acme.sh/issues/6917
if [ -z "$_valid_to" ] || _startswith "$_valid_to" "+"; then
_renew_chk_enddate="$(_enddate "$CERT_PATH")"
_renew_chk_endtime="$(_ssldate2time "$_renew_chk_enddate")"
if [ "$Le_NextRenewTime" ] && [ "$_renew_chk_endtime" ] && [ "$Le_NextRenewTime" -ge "$_renew_chk_endtime" ]; then
_info "$(__red "WARNING: the cert expires at $_renew_chk_enddate, BEFORE the next scheduled renewal time $Le_NextRenewTimeStr.")"
_info "$(__red "The cert will already be expired when the renewal runs. If your CA issues short-lived certs, use a negative --days value (e.g. --days -1) to renew relative to the expiry time.")"
fi
fi
_savedomainconf "Le_NextRenewTimeStr" "$Le_NextRenewTimeStr"
_savedomainconf "Le_NextRenewTime" "$Le_NextRenewTime"
@ -5760,6 +5993,12 @@ $_authorizations_map"
_toPkcs "$CERT_PFX_PATH" "$CERT_KEY_PATH" "$CERT_PATH" "$CA_CERT_PATH" "$Le_PFXPassword"
fi
#convert to pkcs8
Le_PKCS8Password="$(_readdomainconf Le_PKCS8Password)"
if [ "$Le_PKCS8Password" ]; then
_toPkcs8 "$CERT_PKCS8_PATH" "$CERT_KEY_PATH" "$Le_PKCS8Password"
fi
if [ "$_real_cert$_real_key$_real_ca$_reload_cmd$_real_fullchain" ]; then
_savedomainconf "Le_RealCertPath" "$_real_cert"
_savedomainconf "Le_RealCACertPath" "$_real_ca"
@ -5858,7 +6097,6 @@ renew() {
# If the window has started, renew now even if Le_NextRenewTime is in the future.
# Set NO_ARI=1 (env, account.conf, or ca.conf) to opt out and use only
# Le_NextRenewTime for the renewal decision.
_ari_should_renew=""
if [ "$NO_ARI" = "1" ]; then
_debug "NO_ARI=1, skipping ARI suggestedWindow check"
elif [ -z "$FORCE" ] && [ -f "$CERT_PATH" ]; then
@ -5869,20 +6107,63 @@ renew() {
_ari_end="$(echo "$_ari_resp" | _egrep_o '"end" *: *"[^"]*' | sed 's/.*"//')"
_debug "ARI suggestedWindow.start" "$_ari_start"
_debug "ARI suggestedWindow.end" "$_ari_end"
if [ "$_ari_start" ]; then
if [ "$_ari_start" ] && [ "$_ari_end" ]; then
_ari_start_t="$(_date2time "$(echo "$_ari_start" | sed 's/\.[0-9]*//')")"
_ari_end_t="$(_date2time "$(echo "$_ari_end" | sed 's/\.[0-9]*//')")"
_ari_explanation_url="$(echo "$_ari_resp" | _egrep_o '"explanationURL" *: *"[^"]*' | sed 's/.*"//')"
_debug "_ari_start_t" "$_ari_start_t"
if [ "$_ari_start_t" ] && [ "$(_time)" -ge "$_ari_start_t" ]; then
_info "ARI suggestedWindow has started ($(__green "$_ari_start")), proceeding with renewal."
_ari_should_renew="1"
else
_info "ARI suggestedWindow starts at: $(__green "$_ari_start")"
_debug "_ari_end_t" "$_ari_end_t"
_debug "_ari_explanation_url" "$_ari_explanation_url"
_debug "Le_NextRenewTime" "$Le_NextRenewTime"
# Update ARI if needed
if [ "$_ari_start_t" ] && [ "$_ari_end_t" ] && [ "$Le_NextRenewTime" ] && [ "$_ari_end_t" -gt "$_ari_start_t" ] && ([ "$Le_NextRenewTime" -lt "$_ari_start_t" ] || [ "$Le_NextRenewTime" -gt "$_ari_end_t" ]); then
_ari_old_time_str="$Le_NextRenewTimeStr"
_info "Current renewal time: $(__green "$_ari_old_time_str")"
_ari_window=$(_math "$_ari_end_t" - "$_ari_start_t")
_ari_offset=$(_math "$(_time)" % "$_ari_window")
Le_NextRenewTime=$(_math "$_ari_start_t" + "$_ari_offset")
Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime")
_info "ARI suggestedWindow: $(__green "$_ari_start") to $(__green "$_ari_end")"
_info "Updating renewal time picked from ARI window: $(__green "$Le_NextRenewTimeStr")"
_savedomainconf Le_NextRenewTime "$Le_NextRenewTime"
_savedomainconf Le_NextRenewTimeStr "$Le_NextRenewTimeStr"
fi
if [ "$Le_NextRenewTime" ] && [ "$(_time)" -ge "$Le_NextRenewTime" ]; then
_info "ARI suggested renewal has passed ($(__green "$Le_NextRenewTimeStr")), proceeding with renewal."
if [ "$_ari_explanation_url" ]; then
_info "For more information on this renewal: $(__green "$_ari_explanation_url")"
fi
fi
fi
fi
fi
if [ -z "$FORCE" ] && [ -z "$_ari_should_renew" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then
if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then
_renew_retry_fixed=""
res="0"
_ensure_install "$Le_Domain"
res="$?"
if [ "$Le_DeployHook" ] && [ "$res" = "0" ]; then
_ensure_deploy "$Le_Domain"
res="$?"
fi
if [ "$res" != "0" ]; then
if [ -z "$_ACME_IN_RENEWALL" ]; then
if [ $_set_level -ge $NOTIFY_LEVEL_ERROR ]; then
_send_notify "Renew $Le_Domain error" "There is an error." "$NOTIFY_HOOK" 1
fi
fi
return 1
fi
if [ "$_renew_retry_fixed" ]; then
_info "Install/deploy retry succeeded, no renewal is needed."
if [ -z "$_ACME_IN_RENEWALL" ]; then
if [ $_set_level -ge $NOTIFY_LEVEL_RENEW ]; then
_send_notify "Renew $Le_Domain success" "Good, the cert install/deploy retry succeeded." "$NOTIFY_HOOK" 0
fi
fi
return 0
fi
_info "Skipping. Next renewal time is: $(__green "$Le_NextRenewTimeStr")"
_info "Add '$(__red '--force')' to force renewal."
if [ -z "$_ACME_IN_RENEWALL" ]; then
@ -5925,11 +6206,8 @@ renew() {
fi
issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" "$Le_ChallengeAlias" "$Le_Preferred_Chain" "$Le_Valid_From" "$Le_Valid_To" "$Le_Certificate_Profile" "$Le_ExtKeyUse"
res="$?"
if [ "$res" != "0" ]; then
return "$res"
fi
if [ "$Le_DeployHook" ]; then
if [ "$Le_DeployHook" ] && [ "$res" = "0" ]; then
_deploy "$Le_Domain" "$Le_DeployHook"
res="$?"
fi
@ -5969,6 +6247,10 @@ renewAll() {
_set_level=${NOTIFY_LEVEL:-$NOTIFY_LEVEL_DEFAULT}
_debug "_set_level" "$_set_level"
export _ACME_IN_RENEWALL=1
if ! [ -d "$CERT_HOME" ]; then
_err "$CERT_HOME is not a directory, please check your configuration."
return 1
fi
for di in "${CERT_HOME}"/*.* "${CERT_HOME}"/*:*; do
_debug di "$di"
if ! [ -d "$di" ]; then
@ -5977,12 +6259,19 @@ renewAll() {
fi
d=$(basename "$di")
_debug d "$d"
_d_ari="$di.ari"
_debug _d_ari "$_d_ari"
(
if _endswith "$d" "$ECC_SUFFIX"; then
_isEcc=$(echo "$d" | cut -d "$ECC_SEP" -f 2)
d=$(echo "$d" | cut -d "$ECC_SEP" -f 1)
fi
renew "$d" "$_isEcc" "$_server"
rc="$?"
if [ "$rc" = "0" ] && [ "$_ari_explanation_url" ]; then
echo "$_ari_explanation_url" >"$_d_ari"
fi
return $rc
)
rc="$?"
_debug "Return code: $rc"
@ -5997,8 +6286,13 @@ renewAll() {
_send_notify "Renew $d success" "Good, the cert is renewed." "$NOTIFY_HOOK" 0
fi
fi
_renewal_explanation=""
if [ -f "$_d_ari" ]; then
_renewal_explanation=" ($(cat "$_d_ari"))"
rm -f "$_d_ari"
fi
_success_msg="${_success_msg} $d
_success_msg="${_success_msg} $d$_renewal_explanation
"
elif [ "$rc" = "$RENEW_SKIP" ]; then
if [ $_error_level -gt $NOTIFY_LEVEL_SKIP ]; then
@ -6303,6 +6597,45 @@ _deploy() {
_info "$(__green Success)"
fi
done
_deploy_success_time="$(_time)"
_savedomainconf "Le_DeploySuccessTime" "$_deploy_success_time"
_savedomainconf "Le_DeploySuccessTimeStr" "$(_time2str "$_deploy_success_time")"
}
_ensure_deploy() {
_d="$1"
if [ -z "$Le_DeployHook" ]; then
return 0
fi
if [ -z "$Le_CertCreateTime" ]; then
return 0
fi
_deploy_success_time="$(_readdomainconf Le_DeploySuccessTime)"
if [ -z "$_deploy_success_time" ]; then
_debug "Le_DeploySuccessTime is empty, skip deploy retry check."
return 0
fi
case "$_deploy_success_time$Le_CertCreateTime" in
*[!0-9]*)
_debug "Le_DeploySuccessTime or Le_CertCreateTime is not a number, skip deploy retry check."
return 0
;;
esac
if [ "$_deploy_success_time" -lt "$Le_CertCreateTime" ]; then
_info "The cert was created after the last successful deploy, retrying deploy hooks."
if _deploy "$_d" "$Le_DeployHook"; then
_info "Deploy retry succeeded."
_renew_retry_fixed=1
return 0
fi
_err "Deploy retry failed."
return 1
fi
return 0
}
#domain hooks
@ -6323,7 +6656,13 @@ deploy() {
fi
_debug2 DOMAIN_CONF "$DOMAIN_CONF"
. "$DOMAIN_CONF"
# The cert dir may exist without a domain conf (e.g. the conf was deleted, or
# the cert was placed here manually). Deploy can still proceed using env-provided
# settings, and _savedomainconf below will recreate the conf, so only source it
# when present instead of failing on a missing file.
if [ -f "$DOMAIN_CONF" ]; then
. "$DOMAIN_CONF"
fi
_savedomainconf Le_DeployHook "$_hooks"
@ -6456,9 +6795,54 @@ _installcert() {
_info "$(__green "Reload successful")"
else
_err "Reload error for: $_main_domain"
return 1
fi
fi
_installcert_success_time="$(_time)"
_savedomainconf "Le_InstallCertSuccessTime" "$_installcert_success_time"
_savedomainconf "Le_InstallCertSuccessTimeStr" "$(_time2str "$_installcert_success_time")"
}
_ensure_install() {
_d="$1"
if [ -z "$Le_CertCreateTime" ]; then
return 0
fi
_real_cert="$(_readdomainconf Le_RealCertPath)"
_real_key="$(_readdomainconf Le_RealKeyPath)"
_real_ca="$(_readdomainconf Le_RealCACertPath)"
_reload_cmd="$(_readdomainconf Le_ReloadCmd)"
_real_fullchain="$(_readdomainconf Le_RealFullChainPath)"
if [ -z "$_real_cert$_real_key$_real_ca$_reload_cmd$_real_fullchain" ]; then
return 0
fi
_installcert_success_time="$(_readdomainconf Le_InstallCertSuccessTime)"
if [ -z "$_installcert_success_time" ]; then
_debug "Le_InstallCertSuccessTime is empty, skip install retry check."
return 0
fi
case "$_installcert_success_time$Le_CertCreateTime" in
*[!0-9]*)
_debug "Le_InstallCertSuccessTime or Le_CertCreateTime is not a number, skip install retry check."
return 0
;;
esac
if [ "$_installcert_success_time" -lt "$Le_CertCreateTime" ]; then
_info "The cert was created after the last successful install, retrying install cert."
if _installcert "$_d" "$_real_cert" "$_real_key" "$_real_ca" "$_real_fullchain" "$_reload_cmd"; then
_info "Install cert retry succeeded."
_renew_retry_fixed=1
return 0
fi
_err "Install cert retry failed."
return 1
fi
return 0
}
__read_password() {
@ -6505,8 +6889,10 @@ _install_win_taskscheduler() {
_info "$PROJECT_NAME will not save your password."
_info "Please input your Windows password for: $(__green "$_myname")"
_password="$(__read_password)"
#SCHTASKS.exe '/create' '/SC' 'DAILY' '/TN' "$_WINDOWS_SCHEDULER_NAME" '/F' '/ST' "00:$_randomminute" '/RU' "$_myname" '/RP' "$_password" '/TR' "$_winbash -l -c '$_lesh --cron --home \"$LE_WORKING_DIR\" $_centry'" >/dev/null
echo SCHTASKS.exe '/create' '/SC' 'DAILY' '/TN' "$_WINDOWS_SCHEDULER_NAME" '/F' '/ST' "00:$_randomminute" '/RU' "$_myname" '/RP' "$_password" '/TR' "\"$_winbash -l -c '$_lesh --cron --home \"$LE_WORKING_DIR\" $_centry'\"" | cmd.exe >/dev/null
#schtasks.exe /ST requires the HH:mm format, so the minute must be zero-padded (issue 4950)
_st_minute="$(printf "%02d" "$_randomminute")"
#SCHTASKS.exe '/create' '/SC' 'DAILY' '/TN' "$_WINDOWS_SCHEDULER_NAME" '/F' '/ST' "00:$_st_minute" '/RU' "$_myname" '/RP' "$_password" '/TR' "$_winbash -l -c '$_lesh --cron --home \"$LE_WORKING_DIR\" $_centry'" >/dev/null
echo SCHTASKS.exe '/create' '/SC' 'DAILY' '/TN' "$_WINDOWS_SCHEDULER_NAME" '/F' '/ST' "00:$_st_minute" '/RU' "$_myname" '/RP' "$_password" '/TR' "\"$_winbash -l -c '$_lesh --cron --home \"$LE_WORKING_DIR\" $_centry'\"" | cmd.exe >/dev/null
echo
}
@ -6775,7 +7161,7 @@ _deactivate() {
_err "Cannot get new order for domain."
return 1
fi
_authorizations_seg="$(echo "$response" | _egrep_o '"authorizations" *: *\[[^\]*\]' | cut -d '[' -f 2 | tr -d ']' | tr -d '"')"
_authorizations_seg="$(echo "$response" | _json_decode | _authorizations_from_order)"
_debug2 _authorizations_seg "$_authorizations_seg"
if [ -z "$_authorizations_seg" ]; then
_err "_authorizations_seg not found."
@ -7069,6 +7455,10 @@ _installalias() {
_sed_i "/^export LE_CONFIG_HOME/d" "$_envfile"
fi
_setopt "$_envfile" "alias $PROJECT_ENTRY" "=" "\"$LE_WORKING_DIR/$PROJECT_ENTRY$_c_entry\""
if [ -f "$LE_WORKING_DIR/$PROJECT_ENTRY.completion" ]; then
#the completion file does nothing when sourced by a non-bash shell
_setopt "$_envfile" ". \"$LE_WORKING_DIR/$PROJECT_ENTRY.completion\""
fi
_profile="$(_detect_profile)"
if [ "$_profile" ]; then
@ -7186,6 +7576,11 @@ install() {
_info "Installed to $LE_WORKING_DIR/$PROJECT_ENTRY"
if [ -f "$PROJECT_ENTRY.completion" ]; then
cp "$PROJECT_ENTRY.completion" "$LE_WORKING_DIR/"
_debug "Installed bash completion to $LE_WORKING_DIR/$PROJECT_ENTRY.completion"
fi
if [ "$_ACME_IN_CRON" != "1" ] && [ -z "$_noprofile" ]; then
_installalias "$_c_home"
fi
@ -7263,6 +7658,7 @@ uninstall() {
_uninstallalias
rm -f "$LE_WORKING_DIR/$PROJECT_ENTRY"
rm -f "$LE_WORKING_DIR/$PROJECT_ENTRY.completion"
_info "The keys and certs are in \"$(__green "$LE_CONFIG_HOME")\". You can remove them by yourself."
}
@ -7477,6 +7873,7 @@ Commands:
-ccr, --create-csr Create CSR, professional use.
--create-domain-key Create an domain private key, professional use.
--update-account Update account info.
--update-account-key Rotate account key.
--register-account Register account key.
--deactivate-account Deactivate the account.
--make-dns-persist-value Print the DNS TXT record(s) to enable persistent DNS validation
@ -7572,6 +7969,7 @@ Parameters:
--config-home <directory> Specifies the home dir to save all the configurations.
--useragent <string> Specifies the user agent string. it will be saved for future use too.
-m, --email <email> Specifies the account email, only valid for the '--install' and '--update-account' command.
Multiple emails can be given as a comma-separated list: 'a@example.com,b@example.com'
--accountkey <file> Specifies the account key path, only valid for the '--install' command.
--days <ndays> Specifies the days to renew the cert when using '--issue' command. The default value is $DEFAULT_RENEW days.
Negative values could be used to specify a number of days relative to the expiration date of the certificate.
@ -7623,7 +8021,7 @@ Parameters:
--revoke-reason <0-10> The reason for revocation, can be used in conjunction with the '--revoke' command.
See: $_REVOKE_WIKI
--password <password> Add a password to exported pfx file. Use with --to-pkcs12.
--password <password> Add a password to the exported pfx or pkcs8 file. Use with '--to-pkcs12' or '--to-pkcs8'.
"
@ -7653,7 +8051,9 @@ installOnline() {
cd "$PROJECT_NAME-$_branch"
chmod +x $PROJECT_ENTRY
if ./$PROJECT_ENTRY --install "$@"; then
./$PROJECT_ENTRY --install "$@"
_install_rc="$?"
if [ "$_install_rc" = "0" ]; then
_info "Install success!"
fi
@ -7661,6 +8061,9 @@ installOnline() {
rm -rf "$PROJECT_NAME-$_branch"
rm -f "$localname"
# Propagate the install result so a failed upgrade is not reported as
# success. https://github.com/acmesh-official/acme.sh/issues/6477
exit "$_install_rc"
)
}
@ -8023,6 +8426,9 @@ _process() {
--update-account | --updateaccount)
_CMD="updateaccount"
;;
--update-account-key | --updateaccountkey)
_CMD="updateaccountkey"
;;
--register-account | --registeraccount)
_CMD="registeraccount"
;;
@ -8560,6 +8966,15 @@ _process() {
_debug2 LE_WORKING_DIR "$LE_WORKING_DIR"
# --days and --valid-to are mutually exclusive by design: --valid-to pins
# the cert lifetime and the renewal time follows the expiry, so a
# creation-based --days schedule can not apply.
if [ "$_days" ] && [ "$_valid_to" ]; then
_err "--days can not be used together with --valid-to."
_err "With --valid-to, the renewal time is derived from the expiry time automatically."
return 1
fi
if [ "$DEBUG" ]; then
version
if [ "$_server" ]; then
@ -8607,6 +9022,9 @@ _process() {
updateaccount)
updateaccount
;;
updateaccountkey)
updateaccountkey "$_accountkeylength"
;;
deactivateaccount)
deactivateaccount
;;
@ -8626,7 +9044,7 @@ _process() {
toPkcs "$_domain" "$_password" "$_ecc"
;;
toPkcs8)
toPkcs8 "$_domain" "$_ecc"
toPkcs8 "$_domain" "$_password" "$_ecc"
;;
createAccountKey)
createAccountKey "$_accountkeylength"

341
acme.sh.completion Normal file
View file

@ -0,0 +1,341 @@
# Bash completion for acme.sh: https://github.com/acmesh-official/acme.sh
#
# "acme.sh --install" copies this file to the acme.sh home dir and wires
# it into acme.sh.env, so the completion is loaded automatically in new
# bash sessions after installation.
#
# To use it without installing acme.sh, source it from ~/.bashrc, or copy
# it to /usr/share/bash-completion/completions/acme.sh
#
# Zsh users can load it with:
# autoload -U +X bashcompinit && bashcompinit
# . /path/to/acme.sh.completion
# This file may also be sourced by non-bash shells via acme.sh.env,
# so silently do nothing if the "complete" builtin is not available.
if ! command -v complete >/dev/null 2>&1; then
return 0 2>/dev/null || exit 0
fi
# Add each word of $1 that starts with $cur to COMPREPLY.
# The words are read line by line, so that candidates like a wildcard
# domain "*.example.com" are never glob-expanded against the cwd.
_acme_sh_add_matches() {
local _word
while read -r _word; do
[ -n "$_word" ] || continue
case "$_word" in
"$cur"*) COMPREPLY=("${COMPREPLY[@]}" "$_word") ;;
esac
done <<EOF
$(printf '%s\n' "$1" | tr ' ' '\n')
EOF
return 0
}
_acme_sh_files() {
local _file
while IFS= read -r _file; do
[ -n "$_file" ] || continue
COMPREPLY=("${COMPREPLY[@]}" "$_file")
done <<EOF
$(compgen -f -- "$cur")
EOF
if command -v compopt >/dev/null 2>&1; then
compopt -o filenames 2>/dev/null
fi
return 0
}
_acme_sh_dirs() {
local _dir
while IFS= read -r _dir; do
[ -n "$_dir" ] || continue
COMPREPLY=("${COMPREPLY[@]}" "$_dir")
done <<EOF
$(compgen -d -- "$cur")
EOF
if command -v compopt >/dev/null 2>&1; then
compopt -o filenames 2>/dev/null
fi
return 0
}
# Complete the domains that already have a cert: every directory in the
# config home that contains a "<domain>.conf" file ("_ecc" suffix stripped).
_acme_sh_domains() {
local _dir _name _domains=""
[ -n "${ZSH_VERSION:-}" ] && setopt localoptions nonomatch 2>/dev/null
for _dir in "$_acme_conf_home"/*/; do
[ -d "$_dir" ] || continue
_name="${_dir%/}"
_name="${_name##*/}"
_name="${_name%_ecc}"
if [ -f "${_dir}${_name}.conf" ]; then
case " $_domains " in
*" $_name "*) ;;
*) _domains="$_domains $_name" ;;
esac
fi
done
_acme_sh_add_matches "$_domains"
}
# Complete hook names from a subfolder of the acme.sh home dir.
# $1: subfolder (dnsapi/deploy/notify), $2: file name prefix or empty.
_acme_sh_hooks() {
local _file _hooks=""
[ -n "${ZSH_VERSION:-}" ] && setopt localoptions nonomatch 2>/dev/null
for _file in "$_acme_home/$1/$2"*.sh; do
[ -f "$_file" ] || continue
_file="${_file##*/}"
_hooks="$_hooks ${_file%.sh}"
done
_acme_sh_add_matches "$_hooks"
}
_acme_sh_completion() {
local cur prev _acme_home _acme_conf_home
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev=""
if [ "$COMP_CWORD" -gt 0 ]; then
prev="${COMP_WORDS[COMP_CWORD - 1]}"
fi
_acme_home="${LE_WORKING_DIR:-$HOME/.acme.sh}"
_acme_conf_home="${LE_CONFIG_HOME:-$_acme_home}"
# The first argument is the command.
if [ "$COMP_CWORD" -eq 1 ]; then
_acme_sh_add_matches "
--help
--version
--install
--install-online
--uninstall
--upgrade
--issue
--deploy
--sign-csr
--show-csr
--install-cert
--renew
--renew-all
--revoke
--remove
--list
--list-profiles
--info
--to-pkcs12
--to-pkcs8
--create-account-key
--create-domain-key
--create-csr
--deactivate
--update-account
--register-account
--deactivate-account
--make-dns-persist-value
--install-cronjob
--uninstall-cronjob
--cron
--set-notify
--set-default-ca
--set-default-chain
"
return 0
fi
# Complete the value of the previous option.
case "$prev" in
-d | --domain | --challenge-alias | --domain-alias)
_acme_sh_domains
return 0
;;
--dns)
# The dns hook argument is optional, keep completing options if the
# current word already looks like one.
case "$cur" in
-*) ;;
*)
_acme_sh_hooks "dnsapi" "dns_"
return 0
;;
esac
;;
--deploy-hook)
_acme_sh_hooks "deploy" ""
return 0
;;
--notify-hook)
_acme_sh_hooks "notify" ""
return 0
;;
--server)
_acme_sh_add_matches "letsencrypt letsencrypt_test zerossl sslcom google google_test actalis"
return 0
;;
-k | --keylength | -ak | --accountkeylength)
_acme_sh_add_matches "2048 3072 4096 8192 ec-256 ec-384 ec-521"
return 0
;;
--debug)
# Optional argument.
case "$cur" in
-*) ;;
*)
_acme_sh_add_matches "0 1 2 3"
return 0
;;
esac
;;
--log)
# Optional argument.
case "$cur" in
-*) ;;
*)
_acme_sh_files
return 0
;;
esac
;;
--nginx)
# Optional argument.
case "$cur" in
-*) ;;
*)
_acme_sh_files
return 0
;;
esac
;;
--auto-upgrade | --always-force-new-domain-key)
# Optional argument.
case "$cur" in
-*) ;;
*)
_acme_sh_add_matches "0 1"
return 0
;;
esac
;;
--log-level)
_acme_sh_add_matches "1 2"
return 0
;;
--syslog)
_acme_sh_add_matches "0 3 6 7"
return 0
;;
--notify-level)
_acme_sh_add_matches "0 1 2 3"
return 0
;;
--notify-mode)
_acme_sh_add_matches "0 1"
return 0
;;
--revoke-reason)
_acme_sh_add_matches "0 1 2 3 4 5 6 7 8 9 10"
return 0
;;
--cert-file | --key-file | --ca-file | --fullchain-file | --csr | --accountconf | --accountkey | --ca-bundle | --openssl-bin)
_acme_sh_files
return 0
;;
-w | --webroot | --home | --cert-home | --config-home | --ca-path)
_acme_sh_dirs
return 0
;;
-m | --email | --password | --useragent | --days | --valid-from | --valid-to | --httpport | --tlsport | --local-address | --dnssleep | --pre-hook | --post-hook | --renew-hook | --reloadcmd | --extended-key-usage | -b | --branch | --notify-source | --eab-kid | --eab-hmac-key | --preferred-chain | --cert-profile | --certificate-profile | --dns-persist-ca-name | --dns-persist-days)
# These options take a free-form value, offer nothing.
return 0
;;
esac
# Complete the parameters.
_acme_sh_add_matches "
--accountconf
--accountkey
--accountkeylength
--alpn
--always-force-new-domain-key
--apache
--auto-upgrade
--branch
--ca-bundle
--ca-file
--ca-path
--cert-file
--cert-home
--cert-profile
--challenge-alias
--config-home
--csr
--days
--debug
--deploy-hook
--dns
--dns-persist
--dns-persist-ca-name
--dns-persist-days
--dns-persist-wildcard
--dnssleep
--domain
--domain-alias
--eab-hmac-key
--eab-kid
--ecc
--email
--extended-key-usage
--force
--force-color
--fullchain-file
--home
--httpport
--insecure
--key-file
--keylength
--listen-v4
--listen-v6
--listraw
--local-address
--log
--log-level
--nginx
--no-color
--no-cron
--no-profile
--notify-hook
--notify-level
--notify-mode
--notify-source
--ocsp-must-staple
--openssl-bin
--output-insecure
--password
--post-hook
--pre-hook
--preferred-chain
--reloadcmd
--renew-hook
--revoke-reason
--server
--staging
--standalone
--stateless
--stop-renew-on-error
--syslog
--tlsport
--treat-skip-as-success
--use-wget
--useragent
--valid-from
--valid-to
--webroot
--yes-I-know-dns-manual-mode-enough-go-ahead-please
"
return 0
}
complete -F _acme_sh_completion acme.sh

View file

@ -194,7 +194,8 @@ __cpanel_parse_response() {
printf("%s%s=%s\n", prefix, $2, $3);
}
}' |
sed -En -e 's/^result\/data\/(main_domain|sub_domains\/-|addon_domains\/-|parked_domains\/-)=(.*)$/\2/p'
sed -En -e 's/^result\/data\/(main_domain|sub_domains\/-|addon_domains\/-|parked_domains\/-)=(.*)$/\2/p' |
sed -e 's/^"//' -e 's/"$//' # YAML double-quotes values starting with '*' (wildcard subdomains)
}
# Load parameter by prefix+name - fallback to default if not set, and save to config

View file

@ -3,6 +3,8 @@
#DEPLOY_DOCKER_CONTAINER_LABEL="xxxxxxx"
#DEPLOY_DOCKER_CONTAINER_KEY_FILE="/path/to/key.pem"
#DEPLOY_DOCKER_CONTAINER_KEY_MODE="0640"
#DEPLOY_DOCKER_CONTAINER_KEY_OWNER="1000:1000"
#DEPLOY_DOCKER_CONTAINER_CERT_FILE="/path/to/cert.pem"
#DEPLOY_DOCKER_CONTAINER_CA_FILE="/path/to/ca.pem"
#DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE="/path/to/fullchain.pem"
@ -71,6 +73,18 @@ docker_deploy() {
_savedeployconf DEPLOY_DOCKER_CONTAINER_KEY_FILE "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"
fi
_getdeployconf DEPLOY_DOCKER_CONTAINER_KEY_MODE
_debug2 DEPLOY_DOCKER_CONTAINER_KEY_MODE "$DEPLOY_DOCKER_CONTAINER_KEY_MODE"
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_MODE" ]; then
_savedeployconf DEPLOY_DOCKER_CONTAINER_KEY_MODE "$DEPLOY_DOCKER_CONTAINER_KEY_MODE"
fi
_getdeployconf DEPLOY_DOCKER_CONTAINER_KEY_OWNER
_debug2 DEPLOY_DOCKER_CONTAINER_KEY_OWNER "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER"
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER" ]; then
_savedeployconf DEPLOY_DOCKER_CONTAINER_KEY_OWNER "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER"
fi
_getdeployconf DEPLOY_DOCKER_CONTAINER_CERT_FILE
_debug2 DEPLOY_DOCKER_CONTAINER_CERT_FILE "$DEPLOY_DOCKER_CONTAINER_CERT_FILE"
if [ "$DEPLOY_DOCKER_CONTAINER_CERT_FILE" ]; then
@ -112,6 +126,20 @@ docker_deploy() {
if ! _docker_cp "$_cid" "$_ckey" "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"; then
return 1
fi
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER" ]; then
_info "Setting key file owner to $DEPLOY_DOCKER_CONTAINER_KEY_OWNER"
if ! _docker_exec "$_cid" chown "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER" "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"; then
_err "Can not change owner of key file in container"
return 1
fi
fi
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_MODE" ]; then
_info "Setting key file mode to $DEPLOY_DOCKER_CONTAINER_KEY_MODE"
if ! _docker_exec "$_cid" chmod "$DEPLOY_DOCKER_CONTAINER_KEY_MODE" "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"; then
_err "Can not change mode of key file in container"
return 1
fi
fi
fi
if [ "$DEPLOY_DOCKER_CONTAINER_CERT_FILE" ]; then
@ -189,10 +217,22 @@ _docker_exec() {
_debug2 cjson "$cjson"
execid="$(echo "$cjson" | cut -d '"' -f 4)"
_debug execid "$execid"
ejson="$(_curl_unix_sock "$_DOCKER_SOCK" POST "/exec/$execid/start" "{\"Detach\": false,\"Tty\": false}")"
#Detach:true is required for podman's docker-compatible API: with
#Detach:false it streams the command output on the connection, so the
#non-empty response was misread as an error (issue #4977). The real
#result is checked via the exec inspect ExitCode below instead.
ejson="$(_curl_unix_sock "$_DOCKER_SOCK" POST "/exec/$execid/start" "{\"Detach\": true,\"Tty\": false}")"
_debug2 ejson "$ejson"
if [ "$ejson" ]; then
_err "$ejson"
_et=0
ijson="$(_curl_unix_sock "$_DOCKER_SOCK" GET "/exec/$execid/json")"
while _contains "$ijson" "\"Running\":true" && [ "$_et" -lt 10 ]; do
sleep 1
_et="$(_math "$_et" + 1)"
ijson="$(_curl_unix_sock "$_DOCKER_SOCK" GET "/exec/$execid/json")"
done
_debug2 ijson "$ijson"
if ! echo "$ijson" | _egrep_o "\"ExitCode\": *0[,}]" >/dev/null 2>&1; then
_err "docker exec error: $ijson"
return 1
fi
else

View file

@ -57,7 +57,7 @@ gcore_cdn_deploy() {
_request="{\"username\":\"$Le_Deploy_gcore_cdn_username\",\"password\":\"$Le_Deploy_gcore_cdn_password\"}"
_debug _request "$_request"
export _H1="Content-Type:application/json"
_response=$(_post "$_request" "https://api.gcore.com/auth/jwt/login")
_response=$(_post "$_request" "https://api.gcore.com/iam/auth/jwt/login")
_debug _response "$_response"
_regex=".*\"access\":\"\([-._0-9A-Za-z]*\)\".*$"
_debug _regex "$_regex"

View file

@ -43,7 +43,8 @@
# needing to reload HAProxy. Default is "no".
#
# Require the socat binary. DEPLOY_HAPROXY_STATS_SOCKET variable uses the socat
# address format.
# address format. The certificate can be deployed to a comma separated ',' list
# of hosts ("TCP4:10.0.0.1:1999,TCP4:10.0.0.2:1999")
#
# export DEPLOY_HAPROXY_MASTER_CLI="UNIX:/run/haproxy-master.sock"
#
@ -193,7 +194,6 @@ haproxy_deploy() {
_issuer="${_pem}.issuer"
_ocsp="${_pem}.ocsp"
_reload="${Le_Deploy_haproxy_reload}"
_statssock="${Le_Deploy_haproxy_stats_socket}"
_info "Deploying PEM file"
# Create a temporary PEM file
@ -272,12 +272,18 @@ haproxy_deploy() {
_cafile_argument=""
fi
_debug _cafile_argument "${_cafile_argument}"
# if OpenSSL/LibreSSL is v1.1 or above, the format for the -header option has changed
# OpenSSL 1.1+ expects -header Host=value (one argument), while
# LibreSSL keeps the old two-argument form -header Host value at any
# version (3.x/4.x), so it must be detected by name, not by number.
_openssl_name=$(${ACME_OPENSSL_BIN:-openssl} version | cut -d' ' -f1)
_openssl_version=$(${ACME_OPENSSL_BIN:-openssl} version | cut -d' ' -f2)
_debug _openssl_name "${_openssl_name}"
_debug _openssl_version "${_openssl_version}"
_openssl_major=$(echo "${_openssl_version}" | cut -d '.' -f1)
_openssl_minor=$(echo "${_openssl_version}" | cut -d '.' -f2)
if [ "${_openssl_major}" -eq "1" ] && [ "${_openssl_minor}" -ge "1" ] || [ "${_openssl_major}" -ge "2" ]; then
if [ "${_openssl_name}" = "LibreSSL" ]; then
_header_sep=" "
elif [ "${_openssl_major}" -eq "1" ] && [ "${_openssl_minor}" -ge "1" ] || [ "${_openssl_major}" -ge "2" ]; then
_header_sep="="
else
_header_sep=" "
@ -327,62 +333,67 @@ haproxy_deploy() {
# Update certificate over HAProxy stats socket or master CLI.
if _exists socat; then
# look for the certificate on the stats socket, to chose between updating or creating one
_socat_cert_cmd="echo '${_cmdpfx}show ssl cert' | socat '${_statssock}' - | grep -q '^${_pem}$'"
_debug _socat_cert_cmd "${_socat_cert_cmd}"
eval "${_socat_cert_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_newcert="1"
_info "Creating new certificate '${_pem}' over HAProxy ${_socketname}."
# certificate wasn't found, it's a new one. We should check if the crt-list exists and creates/inserts the certificate.
_socat_crtlist_show_cmd="echo '${_cmdpfx}show ssl crt-list' | socat '${_statssock}' - | grep -q '^${Le_Deploy_haproxy_pem_path}$'"
_debug _socat_crtlist_show_cmd "${_socat_crtlist_show_cmd}"
eval "${_socat_crtlist_show_cmd}"
IFS=','
for _statssock in ${Le_Deploy_haproxy_stats_socket}; do
# look for the certificate on the stats socket, to choose between updating or creating one
_socat_cert_cmd="echo '${_cmdpfx}show ssl cert' | socat '${_statssock}' - | grep -q '^${_pem}$'"
_debug _socat_cert_cmd "${_socat_cert_cmd}"
eval "${_socat_cert_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Couldn't find '${Le_Deploy_haproxy_pem_path}' in haproxy 'show ssl crt-list'"
return "${_ret}"
_newcert="1"
_info "Creating new certificate '${_pem}' over HAProxy ${_socketname}."
# certificate wasn't found, it's a new one. We should check if the crt-list exists and creates/inserts the certificate.
_socat_crtlist_show_cmd="echo '${_cmdpfx}show ssl crt-list' | socat '${_statssock}' - | grep -q '^${Le_Deploy_haproxy_pem_path}$'"
_debug _socat_crtlist_show_cmd "${_socat_crtlist_show_cmd}"
eval "${_socat_crtlist_show_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Couldn't find '${Le_Deploy_haproxy_pem_path}' in haproxy 'show ssl crt-list'"
return "${_ret}"
fi
# create a new certificate
_socat_new_cmd="echo '${_cmdpfx}new ssl cert ${_pem}' | socat '${_statssock}' - | grep -q 'New empty'"
_debug _socat_new_cmd "${_socat_new_cmd}"
eval "${_socat_new_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Couldn't create '${_pem}' in haproxy"
return "${_ret}"
fi
else
_info "Update existing certificate '${_pem}' over HAProxy ${_socketname}."
fi
# create a new certificate
_socat_new_cmd="echo '${_cmdpfx}new ssl cert ${_pem}' | socat '${_statssock}' - | grep -q 'New empty'"
_debug _socat_new_cmd "${_socat_new_cmd}"
eval "${_socat_new_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Couldn't create '${_pem}' in haproxy"
return "${_ret}"
fi
else
_info "Update existing certificate '${_pem}' over HAProxy ${_socketname}."
fi
_socat_cert_set_cmd="echo -e '${_cmdpfx}set ssl cert ${_pem} <<\n$(cat "${_pem}")\n' | socat '${_statssock}' - | grep -q 'Transaction created'"
_secure_debug _socat_cert_set_cmd "${_socat_cert_set_cmd}"
eval "${_socat_cert_set_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Can't update '${_pem}' in haproxy"
return "${_ret}"
fi
_socat_cert_commit_cmd="echo '${_cmdpfx}commit ssl cert ${_pem}' | socat '${_statssock}' - | grep -q '^Success!$'"
_debug _socat_cert_commit_cmd "${_socat_cert_commit_cmd}"
eval "${_socat_cert_commit_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Can't commit '${_pem}' in haproxy"
return ${_ret}
fi
if [ "${_newcert}" = "1" ]; then
# if this is a new certificate, it needs to be inserted into the crt-list`
_socat_cert_add_cmd="echo '${_cmdpfx}add ssl crt-list ${Le_Deploy_haproxy_pem_path} ${_pem}' | socat '${_statssock}' - | grep -q 'Success!'"
_debug _socat_cert_add_cmd "${_socat_cert_add_cmd}"
eval "${_socat_cert_add_cmd}"
# printf %b, not "echo -e": dash's echo has no -e and sends a literal "-e " to the socket.
# "Transaction updated" is replied instead of "created" when an uncommitted transaction exists.
_socat_cert_set_cmd="printf '%b\n' '${_cmdpfx}set ssl cert ${_pem} <<\n$(cat "${_pem}")\n' | socat '${_statssock}' - | grep -qE 'Transaction (created|updated)'"
_secure_debug _socat_cert_set_cmd "${_socat_cert_set_cmd}"
eval "${_socat_cert_set_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Can't update '${_pem}' in haproxy"
return "${_ret}"
fi
fi
_socat_cert_commit_cmd="echo '${_cmdpfx}commit ssl cert ${_pem}' | socat '${_statssock}' - | grep -q '^Success!$'"
_debug _socat_cert_commit_cmd "${_socat_cert_commit_cmd}"
eval "${_socat_cert_commit_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Can't commit '${_pem}' in haproxy"
return ${_ret}
fi
if [ "${_newcert}" = "1" ]; then
# if this is a new certificate, it needs to be inserted into the crt-list`
_socat_cert_add_cmd="echo '${_cmdpfx}add ssl crt-list ${Le_Deploy_haproxy_pem_path} ${_pem}' | socat '${_statssock}' - | grep -q 'Success!'"
_debug _socat_cert_add_cmd "${_socat_cert_add_cmd}"
eval "${_socat_cert_add_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Can't update '${_pem}' in haproxy"
return "${_ret}"
fi
fi
done
else
_err "'socat' is not available, couldn't update over ${_socketname}"
fi

View file

@ -296,9 +296,20 @@ panos_deploy() {
_err "Unable to generate an API key. The user and pass may be invalid or not authorized to generate a new key. Please check the PANOS_USER and PANOS_PASS credentials and try again"
return 1
else
deployer cert
deployer key
deployer commit
# A commit of a failed import would leave a mismatched cert/key pair
# on the firewall and can lock the admin out of the management
# interface, see https://github.com/acmesh-official/acme.sh/issues/4716
if ! deployer cert; then
_err "Cert import failed. Aborting without committing."
return 1
fi
if ! deployer key; then
_err "Key import failed. Aborting without committing. Warning: the firewall now has an uncommitted mismatched cert/key pair in its candidate config."
return 1
fi
if ! deployer commit; then
return 1
fi
if [ "$_panos_template_stack" ]; then
# try to get job status for 20 times in 30 sec interval
i=0

View file

@ -170,10 +170,16 @@ ssh_deploy() {
_info "Required commands batched and sent in single call to remote host"
fi
_returnCode=0
_deploy_ssh_servers="$DEPLOY_SSH_SERVER"
for DEPLOY_SSH_SERVER in $_deploy_ssh_servers; do
_ssh_deploy
if ! _ssh_deploy; then
# in case of an error, remember it, but keep going for the remaining servers
_returnCode=1
fi
done
return $_returnCode
}
_ssh_deploy() {

View file

@ -276,7 +276,7 @@ synology_dsm_deploy() {
if [ -n "$error_code" ]; then
if [ "$error_code" == "403" ] && [ -n "$SYNO_DEVICE_ID" ]; then
_cleardeployconf SYNO_DEVICE_ID
_err "Failed to authenticate with SYNO_DEVICE_ID (may expired or invalid), please try again in a new terminal window."
_err "Failed to authenticate with SYNO_DEVICE_ID (may be expired or invalid), please try again in a new terminal window."
elif [ "$error_code" == "404" ]; then
_err "Failed to authenticate with provided 2FA-OTP code, please try again in a new terminal window."
elif [ "$error_code" == "406" ]; then
@ -322,8 +322,8 @@ synology_dsm_deploy() {
_savedeployconf SYNO_USE_TEMP_ADMIN "$SYNO_USE_TEMP_ADMIN"
_savedeployconf SYNO_LOCAL_HOSTNAME "$SYNO_LOCAL_HOSTNAME"
else
_savedeployconf SYNO_USERNAME "$SYNO_USERNAME"
_savedeployconf SYNO_PASSWORD "$SYNO_PASSWORD"
_savedeployconf SYNO_USERNAME "$SYNO_USERNAME" "base64"
_savedeployconf SYNO_PASSWORD "$SYNO_PASSWORD" "base64"
_savedeployconf SYNO_DEVICE_ID "$SYNO_DEVICE_ID"
_savedeployconf SYNO_DEVICE_NAME "$SYNO_DEVICE_NAME"
fi

View file

@ -37,6 +37,16 @@ dns_acmedns_add() {
ACMEDNS_PASSWORD="${ACMEDNS_PASSWORD:-$(_readdomainconf ACMEDNS_PASSWORD)}"
ACMEDNS_SUBDOMAIN="${ACMEDNS_SUBDOMAIN:-$(_readdomainconf ACMEDNS_SUBDOMAIN)}"
#for compatibility: old versions stored ACMEDNS_UPDATE_URL in the account
#conf (issue 3899). Do not clear it here: it must stay available for the
#other domains that have not migrated to their domain conf yet.
if [ -z "$ACMEDNS_BASE_URL" ]; then
_acmedns_update_url="$(_readaccountconf_mutable ACMEDNS_UPDATE_URL)"
if [ "$_acmedns_update_url" ]; then
ACMEDNS_BASE_URL="$(echo "$_acmedns_update_url" | sed 's#/update$##')"
fi
fi
if [ "$ACMEDNS_BASE_URL" = "" ]; then
ACMEDNS_BASE_URL="https://auth.acme-dns.io"
fi
@ -71,7 +81,7 @@ dns_acmedns_add() {
data="{\"subdomain\":\"$ACMEDNS_SUBDOMAIN\", \"txt\": \"$txtvalue\"}"
_debug data "$data"
response="$(_post "$data" "$ACMEDNS_UPDATE_URL" "" "POST")"
response="$(_post "$data" "$ACMEDNS_UPDATE_URL" "" "POST" "application/json")"
_debug response "$response"
if ! echo "$response" | grep "\"$txtvalue\"" >/dev/null; then

View file

@ -69,8 +69,8 @@ _ali_rest() {
ign="$2"
mtd="${3:-GET}"
signature=$(printf "%s" "$mtd&%2F&$(printf "%s" "$query" | _url_encode upper-hex)" | _hmac "sha1" "$(printf "%s" "$Ali_Secret&" | _hex_dump | tr -d " ")" | _base64)
signature=$(printf "%s" "$signature" | _url_encode upper-hex)
signature=$(printf "%s" "$mtd&%2F&$(printf "%s" "$query" | _ali_urlencode_upper)" | _hmac "sha1" "$(printf "%s" "$Ali_Secret&" | _hex_dump | tr -d " ")" | _base64)
signature=$(printf "%s" "$signature" | _ali_urlencode_upper)
url="$endpoint?Signature=$signature"
if [ "$mtd" = "GET" ]; then
@ -96,6 +96,20 @@ _ali_rest() {
fi
}
# stdin stdout
# The Aliyun signature requires percent-encoding with upper-case hex.
# Do not use "_url_encode upper-hex" here: this file is also bundled by
# third parties (e.g. Proxmox VE proxmox-acme) whose older copies of the
# acme.sh function library ignore the upper-hex argument and output
# lower-case hex, which invalidates the signature.
# https://github.com/acmesh-official/acme.sh/issues/6272
_ali_urlencode_upper() {
{
_url_encode
echo
} | sed 's/%a/%A/g;s/%b/%B/g;s/%c/%C/g;s/%d/%D/g;s/%e/%E/g;s/%f/%F/g;s/%\(.\)a/%\1A/g;s/%\(.\)b/%\1B/g;s/%\(.\)c/%\1C/g;s/%\(.\)d/%\1D/g;s/%\(.\)e/%\1E/g;s/%\(.\)f/%\1F/g'
}
_ali_nonce() {
if [ "$ACME_OPENSSL_BIN" ]; then
"$ACME_OPENSSL_BIN" rand -hex 16 2>/dev/null && return 0

View file

@ -139,12 +139,21 @@ _get_autodns_zone() {
return 1
}
# Escape the XML special characters (& < > ' ") so that credentials
# containing them do not break the request document (issue 5317).
_autodns_xml_encode() {
sed "s/&/\&amp;/g;s/</\&lt;/g;s/>/\&gt;/g;s/'/\&apos;/g;s/\"/\&quot;/g"
}
_build_request_auth_xml() {
_autodns_user_xml="$(printf "%s" "$AUTODNS_USER" | _autodns_xml_encode)"
_autodns_password_xml="$(printf "%s" "$AUTODNS_PASSWORD" | _autodns_xml_encode)"
_autodns_context_xml="$(printf "%s" "$AUTODNS_CONTEXT" | _autodns_xml_encode)"
printf "<auth>
<user>%s</user>
<password>%s</password>
<context>%s</context>
</auth>" "$AUTODNS_USER" "$AUTODNS_PASSWORD" "$AUTODNS_CONTEXT"
</auth>" "$_autodns_user_xml" "$_autodns_password_xml" "$_autodns_context_xml"
}
# Arguments:

137
dnsapi/dns_calrissia.sh Normal file
View file

@ -0,0 +1,137 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_calrissia_info='Calrissia.be DNS API
Site: calrissia.be
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_calrissia
Options:
CALRISSIA_TOKEN Personal access token
Issues: github.com/acmesh-official/acme.sh/issues/6809
Author: Ward Hus
'
CALRISSIA_API="https://my.calrissia.com/api"
dns_calrissia_add() {
fulldomain="$1"
txtvalue="$2"
_calrissia_load_token || return 1
if ! _calrissia_get_root "$fulldomain"; then
_err "Unable to find domain in Calrissia account for: $fulldomain"
return 1
fi
_debug "domain='$_domain' id='$_domain_id' sub='$_sub_domain'"
_info "Adding TXT record for $fulldomain"
_body="{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":120,\"prio\":0}"
_response="$(_calrissia_request POST "/domain/$_domain_id/record" "$_body")"
if ! _contains "$_response" '"id"'; then
_err "Failed to create TXT record: $_response"
return 1
fi
return 0
}
dns_calrissia_rm() {
fulldomain="$1"
txtvalue="$2"
_calrissia_load_token || return 1
if ! _calrissia_get_root "$fulldomain"; then
_err "Unable to find domain in Calrissia account for: $fulldomain"
return 1
fi
_debug "domain='$_domain' id='$_domain_id' sub='$_sub_domain'"
# Look the record up from the API instead of relying on local state.
# The record list is embedded in the domain object.
_response="$(_calrissia_request GET "/domain/$_domain_id")"
_debug2 "Response: $_response"
# Split the record objects onto separate lines, then match on both the
# subdomain name and the TXT value to find the record id to delete.
_record_id="$(printf "%s" "$_response" |
tr '{}' '\n' |
grep "\"name\" *: *\"$_sub_domain\"" |
grep "\"content\" *: *\"$txtvalue\"" |
_egrep_o '"id" *: *[0-9]+' |
_head_n 1 |
_egrep_o '[0-9]+')"
if [ -z "$_record_id" ]; then
_info "No matching TXT record found for $fulldomain; nothing to remove"
return 0
fi
_info "Removing TXT record id=$_record_id from domain id=$_domain_id"
if ! _response="$(_calrissia_request DELETE "/domain/$_domain_id/record/$_record_id")" || _contains "$_response" '"error"'; then
_err "Failed to remove TXT record: $_response"
return 1
fi
return 0
}
####################
# Private helpers #
####################
_calrissia_load_token() {
CALRISSIA_TOKEN="${CALRISSIA_TOKEN:-$(_readaccountconf_mutable CALRISSIA_TOKEN)}"
if [ -z "$CALRISSIA_TOKEN" ]; then
_err "CALRISSIA_TOKEN is not set. Generate one at https://identity.calrissia.com under API Keys."
return 1
fi
_saveaccountconf_mutable CALRISSIA_TOKEN "$CALRISSIA_TOKEN"
}
# Sets _domain, _domain_id, _sub_domain for a given FQDN.
_calrissia_get_root() {
_fqdn="$1"
i=1
while true; do
_candidate="$(printf "%s" "$_fqdn" | cut -d . -f "$i"-)"
[ -z "$_candidate" ] && return 1
_debug "Trying root domain: $_candidate"
_response="$(_calrissia_request GET "/domain?full_domain_name=$_candidate")"
_debug2 "Response: $_response"
_domain_id="$(printf "%s" "$_response" |
_egrep_o '"id" *: *[0-9]+' |
_head_n 1 |
_egrep_o '[0-9]+')"
if [ -n "$_domain_id" ]; then
if [ "$i" = "1" ]; then
# The FQDN itself is the zone apex, e.g. a challenge-alias domain.
_sub_domain=""
else
_sub_domain="$(printf "%s" "$_fqdn" | cut -d . -f "1-$((i - 1))")"
fi
_domain="$_candidate"
return 0
fi
i=$((i + 1))
done
}
_calrissia_request() {
_method="$1"
_path="$2"
_body="$3"
export _H1="Authorization: Bearer $CALRISSIA_TOKEN"
export _H2="Accept: application/json"
if [ "$_method" = "GET" ]; then
_get "$CALRISSIA_API$_path"
else
_post "$_body" "$CALRISSIA_API$_path" "" "$_method" "application/json"
fi
}

View file

@ -15,7 +15,8 @@ CN_API="https://beta.api.core-networks.de"
######## Public functions #####################
dns_cn_add() {
fulldomain=$1
# Core-Networks API requires punycode for IDN domains
fulldomain=$(_idn "$1")
txtvalue=$2
if ! _cn_login; then
@ -58,7 +59,8 @@ dns_cn_add() {
}
dns_cn_rm() {
fulldomain=$1
# Core-Networks API requires punycode for IDN domains
fulldomain=$(_idn "$1")
txtvalue=$2
if ! _cn_login; then

View file

@ -4,7 +4,7 @@ dns_da_info='DirectAdmin Server API
Site: DirectAdmin.com/api.php
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_da
Options:
DA_Api API Server URL. E.g. "https://remoteUser:remotePassword@da.domain.tld:8443"
DA_Api API Server URL. E.g. "https://remoteUser:remotePassword@da.domain.tld:8443". Special characters in the user/password must be percent-encoded, e.g. "@" -> "%40".
DA_Api_Insecure Insecure TLS. 0: check for cert validity, 1: always accept
Issues: github.com/TigerP/acme.sh/issues
'

View file

@ -4,7 +4,7 @@ dns_desec_info='deSEC.io
Site: desec.readthedocs.io/en/latest/
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_desec
Options:
DDNSS_Token API Token
DEDYN_TOKEN API Token
Issues: github.com/acmesh-official/acme.sh/issues/2180
Author: Zheng Qian
'

View file

@ -5,14 +5,11 @@ Site: DNSExit.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_dnsexit
Options:
DNSEXIT_API_KEY API Key
DNSEXIT_AUTH_USER Username
DNSEXIT_AUTH_PASS Password
Issues: github.com/acmesh-official/acme.sh/issues/4719
Author: Samuel Jimenez
'
DNSEXIT_API_URL="https://api.dnsexit.com/dns/"
DNSEXIT_HOSTS_URL="https://update.dnsexit.com/ipupdate/hosts.jsp"
######## Public functions #####################
#Usage: dns_dnsexit_add _acme-challenge.*.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
@ -28,20 +25,7 @@ dns_dnsexit_add() {
return 1
fi
_debug 'First detect the root zone'
if ! _get_root "$fulldomain"; then
return 1
fi
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
if ! _dnsexit_rest "{\"domain\":\"$_domain\",\"add\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\",\"ttl\":0,\"overwrite\":false}}"; then
_err "$response"
return 1
fi
_debug2 _response "$response"
return 0
_dnsexit_zone_op add ',"ttl":0,"overwrite":false'
}
#Usage: fulldomain txtvalue
@ -58,54 +42,43 @@ dns_dnsexit_rm() {
return 1
fi
_debug 'First detect the root zone'
if ! _get_root "$fulldomain"; then
_err "$response"
return 1
fi
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
if ! _dnsexit_rest "{\"domain\":\"$_domain\",\"delete\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\"}}"; then
_err "$response"
return 1
fi
_debug2 _response "$response"
return 0
_dnsexit_zone_op delete ''
}
#################### Private functions below ##################################
#_acme-challenge.www.domain.com
#returns
# _sub_domain=_acme-challenge.www
# _domain=domain.com
_get_root() {
domain=$1
# The legacy zone-detection endpoint (update.dnsexit.com/ipupdate/hosts.jsp)
# was shut down by DNSExit and now returns 503, and the JSON API offers no
# zone-list call. So find the root zone by attempting the actual operation at
# each domain level: the API answers "code":0 only when the domain matches a
# zone of the account. https://github.com/acmesh-official/acme.sh/issues/6914
#Usage: _dnsexit_zone_op <add|delete> <extra-json-fields>
_dnsexit_zone_op() {
_op="$1"
_extra="$2"
i=1
while true; do
_domain=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug h "$_domain"
_domain=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-100)
_debug _domain "$_domain"
if [ -z "$_domain" ]; then
_err "Could not find the root zone of $fulldomain in your DNSExit account"
return 1
fi
_debug login "$DNSEXIT_AUTH_USER"
_debug password "$DNSEXIT_AUTH_PASS"
_debug domain "$_domain"
_sub_domain="$(printf "%s" "$fulldomain" | sed "s/\\.$_domain\$//")"
if [ "$_sub_domain" = "$fulldomain" ]; then
_sub_domain=""
fi
_debug _sub_domain "$_sub_domain"
_dnsexit_http "login=$DNSEXIT_AUTH_USER&password=$DNSEXIT_AUTH_PASS&domain=$_domain"
if _contains "$response" "0=$_domain"; then
_sub_domain="$(echo "$fulldomain" | sed "s/\\.$_domain\$//")"
return 0
else
_debug "Go to next level of $_domain"
if _dnsexit_rest "{\"domain\":\"$_domain\",\"$_op\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\"$_extra}}"; then
if _contains "$response" "\"code\":0" || _contains "$response" "\"code\": 0"; then
_debug2 _response "$response"
return 0
fi
_debug "Zone $_domain was not accepted, trying the next level" "$response"
fi
i=$(_math "$i" + 1)
done
return 1
}
_dnsexit_rest() {
@ -136,27 +109,7 @@ _dnsexit_rest() {
return 0
}
_dnsexit_http() {
m=GET
param="$1"
_debug param "$param"
_debug get "$DNSEXIT_HOSTS_URL?$param"
response="$(_get "$DNSEXIT_HOSTS_URL?$param")"
_debug response "$response"
if [ "$?" != "0" ]; then
_err "Error $param"
return 1
fi
_debug2 response "$response"
return 0
}
get_account_info() {
DNSEXIT_API_KEY="${DNSEXIT_API_KEY:-$(_readaccountconf_mutable DNSEXIT_API_KEY)}"
if test -z "$DNSEXIT_API_KEY"; then
DNSEXIT_API_KEY=''
@ -166,23 +119,5 @@ get_account_info() {
_saveaccountconf_mutable DNSEXIT_API_KEY "$DNSEXIT_API_KEY"
DNSEXIT_AUTH_USER="${DNSEXIT_AUTH_USER:-$(_readaccountconf_mutable DNSEXIT_AUTH_USER)}"
if test -z "$DNSEXIT_AUTH_USER"; then
DNSEXIT_AUTH_USER=""
_err 'DNSEXIT_AUTH_USER was not exported'
return 1
fi
_saveaccountconf_mutable DNSEXIT_AUTH_USER "$DNSEXIT_AUTH_USER"
DNSEXIT_AUTH_PASS="${DNSEXIT_AUTH_PASS:-$(_readaccountconf_mutable DNSEXIT_AUTH_PASS)}"
if test -z "$DNSEXIT_AUTH_PASS"; then
DNSEXIT_AUTH_PASS=""
_err 'DNSEXIT_AUTH_PASS was not exported'
return 1
fi
_saveaccountconf_mutable DNSEXIT_AUTH_PASS "$DNSEXIT_AUTH_PASS"
return 0
}

View file

@ -5,6 +5,7 @@ Site: DNSimple.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_dnsimple
Options:
DNSimple_OAUTH_TOKEN OAuth Token
DNSimple_ACCOUNT_ID Account ID. Optional, only needed when the token can access multiple accounts.
Issues: github.com/pho3nixf1re/acme.sh/issues
'
@ -28,7 +29,7 @@ dns_dnsimple_add() {
_saveaccountconf DNSimple_OAUTH_TOKEN "$DNSimple_OAUTH_TOKEN"
if ! _get_account_id; then
_err "failed to retrive account id"
_err "failed to retrieve account id"
return 1
fi
@ -57,7 +58,7 @@ dns_dnsimple_rm() {
fulldomain=$1
if ! _get_account_id; then
_err "failed to retrive account id"
_err "failed to retrieve account id"
return 1
fi
@ -122,13 +123,16 @@ _get_root() {
# returns _account_id
_get_account_id() {
_debug "retrive account id"
if ! _dnsimple_rest GET "whoami"; then
return 1
DNSimple_ACCOUNT_ID="${DNSimple_ACCOUNT_ID:-$(_readaccountconf DNSimple_ACCOUNT_ID)}"
if [ "$DNSimple_ACCOUNT_ID" ]; then
_saveaccountconf DNSimple_ACCOUNT_ID "$DNSimple_ACCOUNT_ID"
_account_id="$DNSimple_ACCOUNT_ID"
_debug _account_id "$_account_id"
return 0
fi
if _contains "$response" "\"account\":null"; then
_err "no account associated with this token"
_debug "retrieve account id"
if ! _dnsimple_rest GET "whoami"; then
return 1
fi
@ -137,7 +141,25 @@ _get_account_id() {
return 1
fi
if _contains "$response" "\"account\":null"; then
# the whoami of a user token (dnsimple_u_*) carries no account,
# so list the accounts the token can access instead
# https://github.com/acmesh-official/acme.sh/issues/6491
if ! _dnsimple_rest GET "accounts"; then
return 1
fi
fi
_account_id=$(printf "%s" "$response" | _egrep_o "\"id\":[^,]*,\"email\":" | cut -d: -f2 | cut -d, -f1)
if [ -z "$_account_id" ]; then
_err "no account associated with this token"
return 1
fi
if [ "$(echo "$_account_id" | wc -l)" -gt 1 ]; then
_err "The token has access to multiple accounts, please pick one and set it explicitly:"
_err "export DNSimple_ACCOUNT_ID=<one of: $(echo "$_account_id" | tr '\n' ' ')>"
return 1
fi
_debug _account_id "$_account_id"
return 0

View file

@ -363,17 +363,12 @@ _edgedns_rest() {
_edgedns_eg_timestamp() {
_debug "Generating signature Timestamp"
_debug3 "Retriving ntp time"
_timeheaders="$(_get "https://www.ntp.org" "onlyheader")"
_debug3 "_timeheaders" "$_timeheaders"
_ntpdate="$(echo "$_timeheaders" | grep -i "Date:" | _head_n 1 | cut -d ':' -f 2- | tr -d "\r\n")"
_debug3 "_ntpdate" "$_ntpdate"
_ntpdate="$(echo "${_ntpdate}" | sed -e 's/^[[:space:]]*//')"
_debug3 "_NTPDATE" "$_ntpdate"
_ntptime="$(echo "${_ntpdate}" | _head_n 1 | cut -d " " -f 5 | tr -d "\r\n")"
_debug3 "_ntptime" "$_ntptime"
_eg_timestamp=$(date -u "+%Y%m%dT")
_eg_timestamp="$(printf "%s%s+0000" "$_eg_timestamp" "$_ntptime")"
#Akamai accepts a clock skew of +/-30s, so use the system clock directly.
#The previous code fetched the Date header from www.ntp.org, which is not
#a reliable time source (it served a wrong time for hours, issue 3973),
#cost an extra https round-trip for every API request, and combined the
#remote time of day with the LOCAL date, breaking around UTC midnight.
_eg_timestamp="$(date -u "+%Y%m%dT%H:%M:%S+0000")"
_debug "_eg_timestamp" "$_eg_timestamp"
}

View file

@ -69,7 +69,12 @@ dns_gd_add() {
return 1
fi
if ! _contains "$response" "$txtvalue"; then
if _contains "$response" "UNKNOWN_DOMAIN"; then
# GoDaddy sometimes returns UNKNOWN_DOMAIN when reading a record back even
# though the PUT above succeeded; skip the local readback check and let
# acme.sh's own DNS propagation check verify the record was published.
_info "GoDaddy API won't allow reading the record back; skipping local verification."
elif ! _contains "$response" "$txtvalue"; then
_err "TXT record '${txtvalue}' for '${fulldomain}', value wasn't set!"
return 1
fi
@ -145,8 +150,8 @@ dns_gd_rm() {
# _domain=domain.com
_get_root() {
domain=$1
i=2
p=1
i=1
p=0
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
if [ -z "$h" ]; then
@ -154,17 +159,41 @@ _get_root() {
return 1
fi
if ! _gd_rest GET "domains/$h"; then
return 1
# The record name is whatever precedes the candidate zone. Do not assume
# _acme-challenge here: with DNS alias mode it can be any name, and the
# record may even sit at the zone apex (name "@").
if [ "$p" = "0" ]; then
_probe_sub="@"
else
_probe_sub=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
fi
if _contains "$response" '"code":"NOT_FOUND"'; then
_debug "$h not found"
else
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
# Probe with the records endpoint instead of "GET domains/$h": since
# 2024-05 GoDaddy rejects the domain details call for accounts with
# fewer than 10 domains, while record-level calls keep working.
# https://github.com/acmesh-official/acme.sh/issues/4487
if ! _gd_rest GET "domains/$h/records/TXT/$_probe_sub"; then
return 1
fi
if _startswith "$response" '\['; then
_sub_domain="$_probe_sub"
_domain="$h"
return 0
fi
# Some accounts get UNKNOWN_DOMAIN when reading records of a valid zone
# even though writes succeed (see issue #6517); fall back to the domain
# details call for them.
if ! _gd_rest GET "domains/$h"; then
return 1
fi
if _contains "$response" '"domainId"'; then
_sub_domain="$_probe_sub"
_domain="$h"
return 0
fi
_debug "$h not found"
p="$i"
i=$(_math "$i" + 1)
done

View file

@ -40,6 +40,11 @@ _hostingde_apiKey() {
return 1
fi
# The endpoint is the base URL only; the api path is appended below.
# hosting.de's own docs show the full api URL, so strip it if pasted in.
# https://github.com/acmesh-official/acme.sh/issues/6896
HOSTINGDE_ENDPOINT="$(echo "$HOSTINGDE_ENDPOINT" | sed 's|/api/dns/v1/json||; s|/*$||')"
_saveaccountconf_mutable HOSTINGDE_APIKEY "$HOSTINGDE_APIKEY"
_saveaccountconf_mutable HOSTINGDE_ENDPOINT "$HOSTINGDE_ENDPOINT"
}

View file

@ -7,11 +7,11 @@ Options:
HUAWEICLOUD_Username Username
HUAWEICLOUD_Password Password
HUAWEICLOUD_DomainName DomainName
HUAWEICLOUD_Region Region. E.g. "cn-north-4". Optional, defaults to "ap-southeast-1".
Issues: github.com/acmesh-official/acme.sh/issues/3265
'
iam_api="https://iam.myhuaweicloud.com"
dns_api="https://dns.ap-southeast-1.myhuaweicloud.com" # Should work
######## Public functions #####################
@ -30,6 +30,7 @@ dns_huaweicloud_add() {
HUAWEICLOUD_Username="${HUAWEICLOUD_Username:-$(_readaccountconf_mutable HUAWEICLOUD_Username)}"
HUAWEICLOUD_Password="${HUAWEICLOUD_Password:-$(_readaccountconf_mutable HUAWEICLOUD_Password)}"
HUAWEICLOUD_DomainName="${HUAWEICLOUD_DomainName:-$(_readaccountconf_mutable HUAWEICLOUD_DomainName)}"
HUAWEICLOUD_Region="${HUAWEICLOUD_Region:-$(_readaccountconf_mutable HUAWEICLOUD_Region)}"
# Check information
if [ -z "${HUAWEICLOUD_Username}" ] || [ -z "${HUAWEICLOUD_Password}" ] || [ -z "${HUAWEICLOUD_DomainName}" ]; then
@ -37,8 +38,11 @@ dns_huaweicloud_add() {
return 1
fi
_huaweicloud_region="${HUAWEICLOUD_Region:-ap-southeast-1}"
dns_api="https://dns.${_huaweicloud_region}.myhuaweicloud.com"
unset token # Clear token
token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_DomainName}")"
token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_DomainName}" "${_huaweicloud_region}")"
if [ -z "${token}" ]; then # Check token
_err "dns_api(dns_huaweicloud): Error getting token."
return 1
@ -65,6 +69,9 @@ dns_huaweicloud_add() {
_saveaccountconf_mutable HUAWEICLOUD_Username "${HUAWEICLOUD_Username}"
_saveaccountconf_mutable HUAWEICLOUD_Password "${HUAWEICLOUD_Password}"
_saveaccountconf_mutable HUAWEICLOUD_DomainName "${HUAWEICLOUD_DomainName}"
if [ -n "${HUAWEICLOUD_Region}" ]; then
_saveaccountconf_mutable HUAWEICLOUD_Region "${HUAWEICLOUD_Region}"
fi
return 0
}
@ -81,6 +88,7 @@ dns_huaweicloud_rm() {
HUAWEICLOUD_Username="${HUAWEICLOUD_Username:-$(_readaccountconf_mutable HUAWEICLOUD_Username)}"
HUAWEICLOUD_Password="${HUAWEICLOUD_Password:-$(_readaccountconf_mutable HUAWEICLOUD_Password)}"
HUAWEICLOUD_DomainName="${HUAWEICLOUD_DomainName:-$(_readaccountconf_mutable HUAWEICLOUD_DomainName)}"
HUAWEICLOUD_Region="${HUAWEICLOUD_Region:-$(_readaccountconf_mutable HUAWEICLOUD_Region)}"
# Check information
if [ -z "${HUAWEICLOUD_Username}" ] || [ -z "${HUAWEICLOUD_Password}" ] || [ -z "${HUAWEICLOUD_DomainName}" ]; then
@ -88,8 +96,11 @@ dns_huaweicloud_rm() {
return 1
fi
_huaweicloud_region="${HUAWEICLOUD_Region:-ap-southeast-1}"
dns_api="https://dns.${_huaweicloud_region}.myhuaweicloud.com"
unset token # Clear token
token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_DomainName}")"
token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_DomainName}" "${_huaweicloud_region}")"
if [ -z "${token}" ]; then # Check token
_err "dns_api(dns_huaweicloud): Error getting token."
return 1
@ -298,6 +309,7 @@ _get_token() {
_username=$1
_password=$2
_domain_name=$3
_region_name=$4
_debug "Getting Token"
body="{
@ -318,7 +330,7 @@ _get_token() {
},
\"scope\": {
\"project\": {
\"name\": \"ap-southeast-1\"
\"name\": \"${_region_name}\"
}
}
}

View file

@ -307,13 +307,18 @@ _get_root() {
return 1
fi
if _contains "$response" "$h"; then
# Anchor the match to the XML tag and escape dots so $h is compared
# literally: _contains uses grep, which treats "$h" as a regex, and a
# bare "g.berlight.de" would match "<string>berlight.de" (the 'g' from
# "<string>" plus '.' matching '>'). See issue #5129.
_hregex=$(printf "%s" "$h" | sed 's/\./\\./g')
if _contains "$response" "<string>$_hregex</string>"; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain="$h"
return 0
fi
# IDN fallback: INWX returns Unicode zone names; when $h is ACE/punycode,
# encode each zone name via _idn() and compare no python dependency.
# encode each zone name via _idn() and compare -- no python dependency.
if _contains "$h" "xn--"; then
_zone_unicode=$(printf "%s" "$response" | _egrep_o '<string>[^<]*' |
sed 's/<[^>]*>//g' | while IFS= read -r _z; do

197
dnsapi/dns_laodc.sh Normal file
View file

@ -0,0 +1,197 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_laodc_info='LaoDC DNS API Server
Site: laodc.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_laodc
Options:
LaoDC_Key API Key
Issues: github.com/acmesh-official/acme.sh/issues/6973
Author: @laodc
'
# Usage:
# export LaoDC_Key="your-api-key"
# acme.sh --issue --dns dns_laodc -d example.la -d *.example.la --dnssleep 120
#
# The credentials will be saved in ~/.acme.sh/account.conf
LAODC_VER="0.1.2"
LAODC_API_ENDPOINT="https://dns.laodc.com/v1"
######## Public functions #####################
# Usage: dns_laodc_add _acme-challenge.example.la ZPXvna6tBhq7XQMH7_t2WC2sg0F-BdmtmmpUJiK6Ho
dns_laodc_add() {
fulldomain=$1
txtvalue=$2
_info "Using LaoDC DNS API"
_laodc_validate_key || return 1
_debug "Checking root zone exists for [$fulldomain]"
if ! _get_root "$fulldomain"; then
_err "Invalid domain"
return 1
fi
domain_hash=$(echo "$response" | _egrep_o "\"hash\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
_debug _domain "$_domain"
_debug _sub_domain "$_sub_domain"
_debug _domain_hash "$domain_hash"
_info "Adding acme record"
if _laodc_api "POST" "$domain_hash" "$_sub_domain" "$txtvalue"; then
if [ "$_code" = "201" ]; then
_info "Added, OK"
return 0
else
_err "Add TXT record error, invalid code. Code: $_code"
return 1
fi
fi
_err "Add TXT record error."
return 1
}
dns_laodc_rm() {
fulldomain=$1
txtvalue=$2
_laodc_validate_key || return 1
_debug "Checking root zone exists for [$fulldomain]"
if ! _get_root "$fulldomain"; then
_err "Invalid domain"
return 1
fi
domain_hash=$(echo "$response" | _egrep_o "\"hash\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
_debug _root_domain "$_domain"
_debug _sub_domain "$_sub_domain"
_debug _domain_hash "$domain_hash"
_info "Deleting acme record"
if _laodc_api "DELETE" "$domain_hash" "$_sub_domain" "$txtvalue"; then
if [ "$_code" = "204" ]; then
_info "Deleted, OK"
return 0
else
_err "Delete TXT record error, invalid code. Code: $_code"
return 1
fi
fi
_err "Delete TXT record error."
return 1
}
#################### Private functions below ##################################
# _acme-challenge.www.domain.com
# returns
# _domain=domain.com
# _sub_domain=www
_get_root() {
fqdn=$1
p=1
i=1
while true; do
h=$(printf "%s" "$fqdn" | cut -d . -f "$i"-100)
if [ -z "$h" ]; then
return 1 # not valid domain
fi
# Check API if domain exists
if _laodc_api "GET" "$h"; then
if [ "$_code" = "200" ]; then
_domain="$h"
# DNS alias mode - @ is alias for fqdn
_sub_domain=$(printf "%s" "$fqdn" | cut -d . -f 1-"$p")
if [ "$i" = "1" ]; then
_sub_domain="@"
fi
return 0
fi
fi
p="$i"
i=$(_math "$i" + 1)
done
return 1
}
_laodc_validate_key() {
LaoDC_Key="${LaoDC_Key:-$(_readaccountconf_mutable LaoDC_Key)}"
if [ -z "$LaoDC_Key" ]; then
LaoDC_Key=""
_err "You didn't specify a LaoDC API Key yet."
_err "Please export LaoDC_Key and try again."
return 1
fi
# Save the api key to the account conf file.
_saveaccountconf_mutable LaoDC_Key "$LaoDC_Key"
}
_laodc_api() {
method=$1
domain=$2
subdomain=$3
value=$4
export _H1="Content-Type: application/json"
export _H2="User-Agent: acme.sh/$VER laodc-dns-acme-sh/$LAODC_VER"
export _H3="Authorization: Bearer $LaoDC_Key"
case $method in
GET)
if [ -n "$subdomain" ]; then
response="$(_get "$LAODC_API_ENDPOINT/$domain/$subdomain?type=TXT")"
else
response="$(_get "$LAODC_API_ENDPOINT/$domain")"
fi
;;
POST)
# Sanitize value input
value=$(printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g')
data="{ \"type\": \"TXT\", \"value\": \"$value\", \"ttl\": \"60\" }"
response="$(_post "$data" "$LAODC_API_ENDPOINT/$domain/$subdomain" "" "POST" "application/json")"
;;
DELETE)
# Sanitize value input
value=$(printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g')
data="{ \"type\": \"TXT\", \"value\": \"$value\" }"
response="$(_post "$data" "$LAODC_API_ENDPOINT/$domain/$subdomain" "" "DELETE" "application/json")"
;;
esac
_ret=$?
# Unset immediately after request to prevent leaks
export _H1=
export _H2=
export _H3=
if [ "$_ret" != "0" ]; then
_err "Error $domain"
return 1
fi
responseHeaders="$(cat "$HTTP_HEADER")"
if echo "$responseHeaders" | grep -i "Content-Type: *application/json" >/dev/null 2>&1; then
response="$(echo "$response" | _json_decode | _normalizeJson)"
fi
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
_debug "http response code $_code"
_debug response "$response"
return 0
}

167
dnsapi/dns_muumuu.sh Executable file
View file

@ -0,0 +1,167 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_muumuu_info='muumuu-domain.com
Site: muumuu-domain.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_muumuu
Options:
MUUMUU_PAT Personal Access Token (scopes: domains:read, dns:read, dns:write)
Issues: github.com/acmesh-official/acme.sh/issues/7011
'
MUUMUU_API="https://muumuu-domain.com/api/v2"
######## Public functions #####################
dns_muumuu_add() {
fulldomain="$(echo "$1" | _lower_case)"
txtvalue="$2"
_info "Using muumuu-domain.com DNS API"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
MUUMUU_PAT="${MUUMUU_PAT:-$(_readaccountconf_mutable MUUMUU_PAT)}"
if [ -z "$MUUMUU_PAT" ]; then
_err "MUUMUU_PAT is not set."
_err "Please create a Personal Access Token at https://muumuu-domain.com"
_err "with scopes: domains:read, dns:read, dns:write"
return 1
fi
_saveaccountconf_mutable MUUMUU_PAT "$MUUMUU_PAT"
if ! _muumuu_get_root "$fulldomain"; then
_err "Unable to find the root domain for $fulldomain"
return 1
fi
_debug _domain_id "$_domain_id"
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_info "Adding TXT record for ${fulldomain}"
body="{\"fqdn\":\"${fulldomain}.\",\"type\":\"TXT\",\"value\":\"${txtvalue}\",\"ttl\":3600}"
if _muumuu_rest POST "/me/domains/${_domain_id}/dns-records" "$body"; then
if [ "$_muumuu_code" = "201" ]; then
_info "TXT record added successfully"
return 0
fi
fi
_err "Failed to add TXT record (HTTP ${_muumuu_code})"
return 1
}
dns_muumuu_rm() {
fulldomain="$(echo "$1" | _lower_case)"
txtvalue="$2"
_info "Using muumuu-domain.com DNS API"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
MUUMUU_PAT="${MUUMUU_PAT:-$(_readaccountconf_mutable MUUMUU_PAT)}"
if [ -z "$MUUMUU_PAT" ]; then
_err "MUUMUU_PAT is not set."
return 1
fi
if ! _muumuu_get_root "$fulldomain"; then
_err "Unable to find the root domain for $fulldomain"
return 1
fi
_debug _domain_id "$_domain_id"
_info "Looking up TXT record for ${fulldomain}"
if ! _muumuu_rest GET "/me/domains/${_domain_id}/dns-records?type=TXT&fqdn=${fulldomain}."; then
_err "Failed to list TXT records"
return 1
fi
record_id=$(echo "$response" | _egrep_o "\"id\":[0-9]+[^}]*\"value\":\"${txtvalue}\"" | _egrep_o "\"id\":[0-9]+" | _head_n 1 | cut -d: -f2)
if [ -z "$record_id" ]; then
_info "TXT record not found, nothing to remove"
return 0
fi
_debug record_id "$record_id"
if _muumuu_rest DELETE "/me/domains/${_domain_id}/dns-records/${record_id}"; then
if [ "$_muumuu_code" = "204" ]; then
_info "TXT record deleted successfully"
return 0
fi
fi
_err "Failed to delete TXT record (HTTP ${_muumuu_code})"
return 1
}
#################### Private functions below ##################################
# _acme-challenge.www.example.com
# sets:
# _domain_id MU00000001
# _sub_domain _acme-challenge.www
# _domain example.com
_muumuu_get_root() {
domain="$1"
i=1
p=0
h=""
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
if [ -z "$h" ]; then
return 1
fi
if ! _muumuu_rest GET "/me/domains?fqdn=${h}&page-size=1"; then
return 1
fi
if [ "$_muumuu_code" = "401" ] || [ "$_muumuu_code" = "403" ]; then
_err "Authentication failed (HTTP ${_muumuu_code}). Check MUUMUU_PAT."
return 1
fi
if _contains "$response" "\"fqdn\":\"${h}\""; then
_domain_id=$(echo "$response" | _egrep_o "\"id\":\"MU[0-9]+\"" | _head_n 1 | cut -d: -f2 | tr -d '"')
_domain="$h"
if [ "$p" = "0" ]; then
_sub_domain=""
else
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
fi
return 0
fi
p="$i"
i=$(_math "$i" + 1)
done
}
_muumuu_rest() {
_muumuu_method="$1"
_muumuu_path="$2"
_muumuu_data="$3"
_muumuu_url="${MUUMUU_API}${_muumuu_path}"
export _H1="Authorization: Bearer ${MUUMUU_PAT}"
export _H2="Content-Type: application/json"
export _H3="Accept: application/json"
export _H4=""
export _H5=""
_secure_debug2 data "$_muumuu_data"
if [ "$_muumuu_method" = "GET" ]; then
response="$(_get "$_muumuu_url")"
else
response="$(_post "$_muumuu_data" "$_muumuu_url" "" "$_muumuu_method")"
fi
_muumuu_ret="$?"
_muumuu_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
_debug "HTTP code: ${_muumuu_code}"
_secure_debug2 response "$response"
if [ "$_muumuu_ret" != "0" ]; then
_err "Error accessing ${_muumuu_url}"
return 1
fi
response="$(printf "%s" "$response" | _normalizeJson)"
return 0
}

View file

@ -186,7 +186,7 @@ _oauth2() {
_oauth2_std() {
# HTTP Basic Authentication
_H1="Authorization: Basic $(echo "$MB_AK:$MB_AS" | _base64)"
_H2="Accepts: application/json"
_H2="Accept: application/json"
export _H1 _H2
body="grant_type=client_credentials"
@ -210,7 +210,7 @@ _oauth2_std() {
}
_oauth2_github() {
_H1="Accepts: application/json"
_H1="Accept: application/json"
export _H1
body="{\"login\":{\"handle\":\"$MB_AK\",\"pass\":\"$MB_AS\",\"floating\":1}}"
@ -241,7 +241,7 @@ _mb_rest() {
fi
_H1="Authorization: Bearer $MB_TK"
_H2="Accepts: application/json"
_H2="Accept: application/json"
export _H1 _H2
if [ "$data" ] || [ "$m" = "POST" ] || [ "$m" = "PUT" ] || [ "$m" = "DELETE" ]; then
# body url [needbase64] [POST|PUT|DELETE] [ContentType]

View file

@ -153,10 +153,9 @@ _namecom_get_root() {
i=2
p=1
if ! _namecom_rest GET "domains"; then
return 1
fi
# Probe each candidate with GetDomain (GET /v4/domains/{domainName}) instead
# of listing all domains: the list is paginated at 1000 domains per page, so
# larger accounts never found their domain on the first page.
# Need to exclude the last field (tld)
numfields=$(echo "$domain" | _egrep_o "\." | wc -l)
while [ "$i" -le "$numfields" ]; do
@ -166,7 +165,7 @@ _namecom_get_root() {
return 1
fi
if _contains "$response" "$host"; then
if _namecom_rest GET "domains/$host" && _contains "$response" "\"domainName\":\"$host\""; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain="$host"
return 0

View file

@ -96,8 +96,8 @@ _get_root() {
for ITEM in ${domains_list}; do
IDN_ITEM=${ITEM}
case "${domain}" in
*${IDN_ITEM}*)
case ".${domain}" in
*.${IDN_ITEM}*)
_domain="$(_idn "${ITEM}")"
_debug _domain "${_domain}"
return 0

217
dnsapi/dns_wedos.sh Normal file
View file

@ -0,0 +1,217 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_wedos_info='WEDOS.com
Site: wedos.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_wedos
Options:
WEDOS_Username WAPI login (account email)
WEDOS_Wapipass WAPI password
Issues: github.com/acmesh-official/acme.sh/issues/7071
Author: Jan Forman <jforman@jflab.cz>
'
WEDOS_Api="https://api.wedos.com/wapi/json"
######## Public functions #####################
#Usage: dns_wedos_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_wedos_add() {
fulldomain=$(echo "$1" | _lower_case)
txtvalue=$2
if ! _wedos_init; then
return 1
fi
_debug "Detecting root zone for $fulldomain"
if ! _get_root "$fulldomain"; then
_err "Cannot determine root zone for: $fulldomain"
return 1
fi
_debug _domain "$_domain"
_debug _sub_domain "$_sub_domain"
_info "Adding TXT record: $_sub_domain.$_domain"
if ! _wedos_request "dns-row-add" "{\"domain\":\"$_domain\",\"name\":\"$_sub_domain\",\"ttl\":\"300\",\"type\":\"TXT\",\"rdata\":\"$txtvalue\"}"; then
_err "Failed to add TXT record"
return 1
fi
_info "Committing DNS changes for $_domain"
if ! _wedos_request "dns-domain-commit" "{\"name\":\"$_domain\"}"; then
_err "Failed to commit DNS changes"
return 1
fi
return 0
}
#Usage: dns_wedos_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_wedos_rm() {
fulldomain=$(echo "$1" | _lower_case)
txtvalue=$2
if ! _wedos_init; then
return 1
fi
_debug "Detecting root zone for $fulldomain"
if ! _get_root "$fulldomain"; then
_err "Cannot determine root zone for: $fulldomain"
return 1
fi
_debug _domain "$_domain"
_debug _sub_domain "$_sub_domain"
# _get_root leaves the dns-rows-list response for $_domain in $response
_debug "Looking up row IDs for TXT value: $txtvalue"
_row_ids=$(echo "$response" | tr '{' '\n' | grep -F -- "\"rdata\":\"$txtvalue\"" | grep -F -- "\"name\":\"$_sub_domain\"" | _egrep_o '"ID": *"[0-9]*"' | tr -dc '0-9\n')
_debug _row_ids "$_row_ids"
if [ -z "$_row_ids" ]; then
_info "TXT record not found, nothing to remove"
return 0
fi
for _row_id in $_row_ids; do
_info "Removing TXT record ID $_row_id from $_domain"
if ! _wedos_request "dns-row-delete" "{\"domain\":\"$_domain\",\"row_id\":\"$_row_id\"}"; then
_err "Failed to delete TXT record"
return 1
fi
done
_info "Committing DNS changes for $_domain"
if ! _wedos_request "dns-domain-commit" "{\"name\":\"$_domain\"}"; then
_err "Failed to commit DNS changes"
return 1
fi
return 0
}
#################### Private functions below ##################################
_wedos_init() {
WEDOS_Username="${WEDOS_Username:-$(_readaccountconf_mutable WEDOS_Username)}"
WEDOS_Wapipass="${WEDOS_Wapipass:-$(_readaccountconf_mutable WEDOS_Wapipass)}"
if [ -z "$WEDOS_Username" ] || [ -z "$WEDOS_Wapipass" ]; then
WEDOS_Username=""
WEDOS_Wapipass=""
_err "You didn't specify the WEDOS WAPI credentials yet."
_err "Please export WEDOS_Username and WEDOS_Wapipass and try again."
return 1
fi
_saveaccountconf_mutable WEDOS_Username "$WEDOS_Username"
_saveaccountconf_mutable WEDOS_Wapipass "$WEDOS_Wapipass"
return 0
}
# WAPI auth token: sha1(login + sha1(password) + hour), where the hour is
# the current hour on the WEDOS servers (Europe/Prague timezone).
# The POSIX TZ string is used so no tzdata is required on the client.
_wedos_auth() {
if [ "$_wedos_utc" ]; then
# fallback: WAPI accepts 1 hour of skew, UTC+1 fits both CET and CEST
_wedos_hour=$(date -u +%H)
_wedos_hour=$(printf '%02d' "$(((${_wedos_hour#0} + 1) % 24))")
else
_wedos_hour=$(TZ='CET-1CEST,M3.5.0,M10.5.0/3' date +%H)
fi
_wedos_phash=$(printf '%s' "$WEDOS_Wapipass" | _digest sha1 hex)
printf '%s' "${WEDOS_Username}${_wedos_phash}${_wedos_hour}" | _digest sha1 hex
}
#Usage: _wedos_request <command> <data-json>
#Returns 0 and sets $response on WAPI code 1000, returns 1 otherwise.
_wedos_request() {
_wedos_cmd="$1"
_wedos_data="$2"
_wedos_token=$(_wedos_auth)
_secure_debug _wedos_token "$_wedos_token"
_wedos_json="{\"request\":{\"user\":\"$WEDOS_Username\",\"auth\":\"$_wedos_token\",\"command\":\"$_wedos_cmd\",\"data\":$_wedos_data}}"
_debug2 "WAPI command: $_wedos_cmd"
_debug2 "WAPI data: $_wedos_data"
# _post sends the global _H1.._H5 headers with every request; clear them so
# headers from earlier API calls are not leaked to the WAPI endpoint.
export _H1=""
export _H2=""
export _H3=""
export _H4=""
export _H5=""
_wedos_body="request=$(printf '%s' "$_wedos_json" | _url_encode)"
response=$(_post "$_wedos_body" "$WEDOS_Api" "" "POST" "application/x-www-form-urlencoded")
if [ "$?" != "0" ]; then
_err "WAPI request failed for command '$_wedos_cmd'"
return 1
fi
_debug2 "WAPI response: $response"
_wedos_code=$(echo "$response" | _egrep_o '"code": *[0-9]*' | _head_n 1 | tr -dc '0-9')
_debug2 "WAPI result code: $_wedos_code"
if [ "$_wedos_code" = "1000" ]; then
return 0
fi
# some systems ignore the TZ variable (Haiku), sending a wrong auth hour;
# retry once with the UTC fallback in _wedos_auth
if [ "$_wedos_code" = "2050" ] && [ -z "$_wedos_utc" ]; then
_wedos_utc=1
_wedos_request "$_wedos_cmd" "$_wedos_data"
return $?
fi
# 2050 = bad credentials, 2051 = IP not whitelisted, 2052 = IP blocked
if [ "$_wedos_code" = "2050" ] || [ "$_wedos_code" = "2051" ] || [ "$_wedos_code" = "2052" ]; then
_wedos_result=$(echo "$response" | _egrep_o '"result": *"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
_err "WAPI authentication error $_wedos_code: $_wedos_result"
_err "Check WEDOS_Username, WEDOS_Wapipass and the WAPI IP whitelist."
_wedos_autherr=1
return 1
fi
_debug "WAPI error for command '$_wedos_cmd': $response"
return 1
}
# Determine the registered domain (_domain) and subdomain prefix (_sub_domain)
# by walking up the labels and calling dns-rows-list until WAPI accepts one.
# _acme-challenge.www.example.co.uk
# -> _sub_domain=_acme-challenge.www _domain=example.co.uk
# The full domain itself is tried first, so a zone apex (e.g. DNS alias mode
# pointing at the registered domain) resolves to an empty _sub_domain.
_get_root() {
_gr_full="$1"
_gr_i=1
_wedos_autherr=""
while true; do
_gr_candidate=$(printf '%s' "$_gr_full" | cut -d . -f "${_gr_i}"-100)
_debug2 "Checking zone candidate: $_gr_candidate"
if [ -z "$_gr_candidate" ]; then
return 1
fi
if _wedos_request "dns-rows-list" "{\"domain\":\"$_gr_candidate\"}"; then
_domain="$_gr_candidate"
if [ "$_gr_i" = "1" ]; then
_sub_domain=""
else
_sub_domain=$(printf '%s' "$_gr_full" | cut -d . -f 1-"$((_gr_i - 1))")
fi
return 0
fi
# auth error hits every candidate, stop the walk
if [ "$_wedos_autherr" ]; then
return 1
fi
_gr_i=$((_gr_i + 1))
done
}

View file

@ -83,7 +83,43 @@ aws_ses_send() {
response="$(aws_rest POST "" "" "$_data")"
}
_use_container_role() {
# automatically set if running inside ECS
if [ -z "$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" ]; then
_debug "No ECS environment variable detected"
return 1
fi
_use_metadata "169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"
}
_use_instance_role() {
_instance_role_name_url="http://169.254.169.254/latest/meta-data/iam/security-credentials/"
if _get "$_instance_role_name_url" true 1 | _head_n 1 | grep -Fq 401; then
_debug "Using IMDSv2"
_token_url="http://169.254.169.254/latest/api/token"
export _H1="X-aws-ec2-metadata-token-ttl-seconds: 21600"
_token="$(_post "" "$_token_url" "" "PUT")"
_secure_debug3 "_token" "$_token"
if [ -z "$_token" ]; then
_debug "Unable to fetch IMDSv2 token from instance metadata"
return 1
fi
export _H1="X-aws-ec2-metadata-token: $_token"
fi
if ! _get "$_instance_role_name_url" true 1 | _head_n 1 | grep -Fq 200; then
_debug "Unable to fetch IAM role from instance metadata"
return 1
fi
_instance_role_name=$(_get "$_instance_role_name_url" "" 1)
_debug "_instance_role_name" "$_instance_role_name"
_use_metadata "$_instance_role_name_url$_instance_role_name" "$_token"
}
_use_metadata() {
export _H1="X-aws-ec2-metadata-token: $2"
_aws_creds="$(
_get "$1" "" 1 |
_normalizeJson |