Browse Source

Merge branch 'acmesh-official:master' into master

fradev 3 years ago
parent
commit
71a32477e4

+ 9 - 9
.github/workflows/DNS.yml

@@ -61,22 +61,22 @@ jobs:
       run: |
         cd ../acmetest
         if [ "${{ secrets.TokenName1}}" ] ; then
-          echo "${{ secrets.TokenName1}}=${{ secrets.TokenValue1}}" >> env.list
+          echo "${{ secrets.TokenName1}}=${{ secrets.TokenValue1}}" >> docker.env
         fi
         if [ "${{ secrets.TokenName2}}" ] ; then
-          echo "${{ secrets.TokenName2}}=${{ secrets.TokenValue2}}" >> env.list
+          echo "${{ secrets.TokenName2}}=${{ secrets.TokenValue2}}" >> docker.env
         fi
         if [ "${{ secrets.TokenName3}}" ] ; then
-          echo "${{ secrets.TokenName3}}=${{ secrets.TokenValue3}}" >> env.list
+          echo "${{ secrets.TokenName3}}=${{ secrets.TokenValue3}}" >> docker.env
         fi
         if [ "${{ secrets.TokenName4}}" ] ; then
-          echo "${{ secrets.TokenName4}}=${{ secrets.TokenValue4}}" >> env.list
+          echo "${{ secrets.TokenName4}}=${{ secrets.TokenValue4}}" >> docker.env
         fi
         if [ "${{ secrets.TokenName5}}" ] ; then
-          echo "${{ secrets.TokenName5}}=${{ secrets.TokenValue5}}" >> env.list
+          echo "${{ secrets.TokenName5}}=${{ secrets.TokenValue5}}" >> docker.env
         fi
-        echo "TEST_DNS_NO_WILDCARD" >> env.list
-        echo "TEST_DNS_SLEEP" >> env.list
+        echo "TEST_DNS_NO_WILDCARD" >> docker.env
+        echo "TEST_DNS_SLEEP" >> docker.env
     - name: Run acmetest
       run: cd ../acmetest && ./rundocker.sh  testall
 
@@ -170,7 +170,7 @@ jobs:
         ./letest.sh
 
   FreeBSD:
-    runs-on: macos-latest
+    runs-on: macos-10.15
     needs: Windows
     env:
       TEST_DNS : ${{ secrets.TEST_DNS }}
@@ -209,7 +209,7 @@ jobs:
           ./letest.sh
 
   Solaris:
-    runs-on: macos-latest
+    runs-on: macos-10.15
     needs: FreeBSD
     env:
       TEST_DNS : ${{ secrets.TEST_DNS }}

+ 7 - 4
.github/workflows/FreeBSD.yml

@@ -24,20 +24,23 @@ jobs:
            CA_ECDSA: ""
            CA: ""
            CA_EMAIL: ""
+           TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1
          - TEST_ACME_Server: "ZeroSSL.com"
            CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
            CA: "ZeroSSL RSA Domain Secure Site CA"
            CA_EMAIL: "[email protected]"
-    runs-on: macos-latest
+           TEST_PREFERRED_CHAIN: ""
+    runs-on: macos-10.15
     env:
       TEST_LOCAL: 1
       TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
       CA_ECDSA: ${{ matrix.CA_ECDSA }}
       CA: ${{ matrix.CA }}
       CA_EMAIL: ${{ matrix.CA_EMAIL }}
+      TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
     steps:
     - uses: actions/checkout@v2
-    - uses: vmactions/[email protected].2
+    - uses: vmactions/[email protected].3
       id: tunnel
       with:
         protocol: http
@@ -46,9 +49,9 @@ jobs:
       run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
     - name: Clone acmetest
       run: cd .. && git clone https://github.com/acmesh-official/acmetest.git  && cp -r acme.sh acmetest/
-    - uses: vmactions/[email protected].4
+    - uses: vmactions/[email protected].5
       with:
-        envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL'
+        envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN'
         nat: |
           "8080": "80"
         prepare: pkg install -y socat curl

+ 2 - 1
.github/workflows/Linux.yml

@@ -20,10 +20,11 @@ jobs:
   Linux:
     strategy:
       matrix:
-        os: ["ubuntu:latest", "debian:latest", "almalinux:latest", "fedora:latest", "centos:latest", "opensuse/leap:latest", "alpine:latest", "oraclelinux:8", "kalilinux/kali", "archlinux:latest", "mageia", "gentoo/stage3-amd64", "clearlinux:latest"]
+        os: ["ubuntu:latest", "debian:latest", "almalinux:latest", "fedora:latest", "centos:latest", "opensuse/leap:latest", "alpine:latest", "oraclelinux:8", "kalilinux/kali", "archlinux:latest", "mageia", "gentoo/stage3-amd64"]
     runs-on: ubuntu-latest
     env:
       TEST_LOCAL: 1
+      TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1
     steps:
     - uses: actions/checkout@v2
     - name: Clone acmetest

+ 3 - 0
.github/workflows/MacOS.yml

@@ -24,10 +24,12 @@ jobs:
            CA_ECDSA: ""
            CA: ""
            CA_EMAIL: ""
+           TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1
          - TEST_ACME_Server: "ZeroSSL.com"
            CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
            CA: "ZeroSSL RSA Domain Secure Site CA"
            CA_EMAIL: "[email protected]"
+           TEST_PREFERRED_CHAIN: ""
     runs-on: macos-latest
     env:
       TEST_LOCAL: 1
@@ -35,6 +37,7 @@ jobs:
       CA_ECDSA: ${{ matrix.CA_ECDSA }}
       CA: ${{ matrix.CA }}
       CA_EMAIL: ${{ matrix.CA_EMAIL }}
+      TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
     steps:
     - uses: actions/checkout@v2
     - name: Install tools

+ 23 - 0
.github/workflows/PebbleStrict.yml

@@ -35,5 +35,28 @@ jobs:
       run: curl --request POST --data '{"ip":"10.30.50.1"}' http://localhost:8055/set-default-ipv4
     - name: Clone acmetest
       run: cd .. && git clone https://github.com/acmesh-official/acmetest.git  && cp -r acme.sh acmetest/
+    - name: Run acmetest
+      run: cd ../acmetest && ./letest.sh
+
+  PebbleStrict_IPCert:
+    runs-on: ubuntu-latest
+    env:
+      TestingDomain: 10.30.50.1
+      ACME_DIRECTORY: https://localhost:14000/dir
+      HTTPS_INSECURE: 1
+      Le_HTTPPort: 5002
+      Le_TLSPort: 5001
+      TEST_LOCAL: 1
+      TEST_CA: "Pebble Intermediate CA"
+      TEST_IPCERT: 1
+
+    steps:
+    - uses: actions/checkout@v2
+    - name: Install tools
+      run: sudo apt-get install -y socat
+    - name: Run Pebble
+      run: cd .. && curl https://raw.githubusercontent.com/letsencrypt/pebble/master/docker-compose.yml >docker-compose.yml && docker-compose up -d
+    - name: Clone acmetest
+      run: cd .. && git clone https://github.com/acmesh-official/acmetest.git  && cp -r acme.sh acmetest/
     - name: Run acmetest
       run: cd ../acmetest && ./letest.sh

+ 6 - 3
.github/workflows/Solaris.yml

@@ -24,20 +24,23 @@ jobs:
            CA_ECDSA: ""
            CA: ""
            CA_EMAIL: ""
+           TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1
          - TEST_ACME_Server: "ZeroSSL.com"
            CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
            CA: "ZeroSSL RSA Domain Secure Site CA"
            CA_EMAIL: "[email protected]"
-    runs-on: macos-latest
+           TEST_PREFERRED_CHAIN: ""
+    runs-on: macos-10.15
     env:
       TEST_LOCAL: 1
       TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
       CA_ECDSA: ${{ matrix.CA_ECDSA }}
       CA: ${{ matrix.CA }}
       CA_EMAIL: ${{ matrix.CA_EMAIL }}
+      TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
     steps:
     - uses: actions/checkout@v2
-    - uses: vmactions/[email protected].2
+    - uses: vmactions/[email protected].3
       id: tunnel
       with:
         protocol: http
@@ -48,7 +51,7 @@ jobs:
       run: cd .. && git clone https://github.com/acmesh-official/acmetest.git  && cp -r acme.sh acmetest/
     - uses: vmactions/[email protected]
       with:
-        envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL'
+        envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN'
         nat: |
           "8080": "80"
         prepare: pkgutil -y -i socat curl

+ 3 - 0
.github/workflows/Ubuntu.yml

@@ -24,10 +24,12 @@ jobs:
            CA_ECDSA: ""
            CA: ""
            CA_EMAIL: ""
+           TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1
          - TEST_ACME_Server: "ZeroSSL.com"
            CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
            CA: "ZeroSSL RSA Domain Secure Site CA"
            CA_EMAIL: "[email protected]"
+           TEST_PREFERRED_CHAIN: ""
 
     runs-on: ubuntu-latest
     env:
@@ -37,6 +39,7 @@ jobs:
       CA: ${{ matrix.CA }}
       CA_EMAIL: ${{ matrix.CA_EMAIL }}
       NO_ECC_384: ${{ matrix.NO_ECC_384 }}
+      TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
     steps:
     - uses: actions/checkout@v2
     - name: Install tools

+ 5 - 2
.github/workflows/Windows.yml

@@ -24,10 +24,12 @@ jobs:
            CA_ECDSA: ""
            CA: ""
            CA_EMAIL: ""
+           TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1
          - TEST_ACME_Server: "ZeroSSL.com"
            CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
            CA: "ZeroSSL RSA Domain Secure Site CA"
            CA_EMAIL: "[email protected]"
+           TEST_PREFERRED_CHAIN: ""
     runs-on: windows-latest
     env:
       TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
@@ -37,6 +39,7 @@ jobs:
       TEST_LOCAL: 1
       #The 80 port is used by Windows server, we have to use a custom port, tunnel will also use this port.
       Le_HTTPPort: 8888
+      TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
     steps:
     - name: Set git to use LF
       run: |
@@ -49,12 +52,12 @@ jobs:
       shell: cmd
     - name: Install cygwin additional packages
       run: |
-          C:\tools\cygwin\cygwinsetup.exe -qgnNdO -R C:/tools/cygwin -s http://mirrors.kernel.org/sourceware/cygwin/ -P socat,curl,cron,unzip,git
+          C:\tools\cygwin\cygwinsetup.exe -qgnNdO -R C:/tools/cygwin -s http://mirrors.kernel.org/sourceware/cygwin/ -P socat,curl,cron,unzip,git,xxd
       shell: cmd
     - name: Set ENV
       shell: cmd
       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;%PATH% >> %GITHUB_ENV%
     - name: Check ENV
       shell: cmd
       run: |

+ 1 - 0
Dockerfile

@@ -55,6 +55,7 @@ RUN for verb in help \
   deactivate-account \
   set-notify \
   set-default-ca \
+  set-default-chain \
   ; do \
     printf -- "%b" "#!/usr/bin/env sh\n/root/.acme.sh/acme.sh --${verb} --config-home /acme.sh \"\$@\"" >/usr/local/bin/--${verb} && chmod +x /usr/local/bin/--${verb} \
   ; done

+ 113 - 32
acme.sh

@@ -1,6 +1,6 @@
 #!/usr/bin/env sh
 
-VER=3.0.1
+VER=3.0.2
 
 PROJECT_NAME="acme.sh"
 
@@ -20,6 +20,8 @@ _SUB_FOLDER_DEPLOY="deploy"
 
 _SUB_FOLDERS="$_SUB_FOLDER_DNSAPI $_SUB_FOLDER_DEPLOY $_SUB_FOLDER_NOTIFY"
 
+CA_LETSENCRYPT_V1="https://acme-v01.api.letsencrypt.org/directory"
+
 CA_LETSENCRYPT_V2="https://acme-v02.api.letsencrypt.org/directory"
 CA_LETSENCRYPT_V2_TEST="https://acme-staging-v02.api.letsencrypt.org/directory"
 
@@ -57,6 +59,9 @@ VTYPE_HTTP="http-01"
 VTYPE_DNS="dns-01"
 VTYPE_ALPN="tls-alpn-01"
 
+ID_TYPE_DNS="dns"
+ID_TYPE_IP="ip"
+
 LOCAL_ANY_ADDRESS="0.0.0.0"
 
 DEFAULT_RENEW=60
@@ -424,19 +429,27 @@ _secure_debug3() {
 }
 
 _upper_case() {
-  # shellcheck disable=SC2018,SC2019
-  tr 'a-z' 'A-Z'
+  if _is_solaris; then
+    tr '[:lower:]' '[:upper:]'
+  else
+    # shellcheck disable=SC2018,SC2019
+    tr 'a-z' 'A-Z'
+  fi
 }
 
 _lower_case() {
-  # shellcheck disable=SC2018,SC2019
-  tr 'A-Z' 'a-z'
+  if _is_solaris; then
+    tr '[:upper:]' '[:lower:]'
+  else
+    # shellcheck disable=SC2018,SC2019
+    tr 'A-Z' 'a-z'
+  fi
 }
 
 _startswith() {
   _str="$1"
   _sub="$2"
-  echo "$_str" | grep "^$_sub" >/dev/null 2>&1
+  echo "$_str" | grep -- "^$_sub" >/dev/null 2>&1
 }
 
 _endswith() {
@@ -1220,19 +1233,27 @@ _createcsr() {
 
   if [ "$acmeValidationv1" ]; then
     domainlist="$(_idn "$domainlist")"
-    printf -- "\nsubjectAltName=DNS:$domainlist" >>"$csrconf"
+    _debug2 domainlist "$domainlist"
+    alt=""
+    for dl in $(echo "$domainlist" | tr "," ' '); do
+      if [ "$alt" ]; then
+        alt="$alt,$(_getIdType "$dl" | _upper_case):$dl"
+      else
+        alt="$(_getIdType "$dl" | _upper_case):$dl"
+      fi
+    done
+    printf -- "\nsubjectAltName=$alt" >>"$csrconf"
   elif [ -z "$domainlist" ] || [ "$domainlist" = "$NO_VALUE" ]; then
     #single domain
     _info "Single domain" "$domain"
-    printf -- "\nsubjectAltName=DNS:$(_idn "$domain")" >>"$csrconf"
+    printf -- "\nsubjectAltName=$(_getIdType "$domain" | _upper_case):$(_idn "$domain")" >>"$csrconf"
   else
     domainlist="$(_idn "$domainlist")"
     _debug2 domainlist "$domainlist"
-    if _contains "$domainlist" ","; then
-      alt="DNS:$(_idn "$domain"),DNS:$(echo "$domainlist" | sed "s/,,/,/g" | sed "s/,/,DNS:/g")"
-    else
-      alt="DNS:$(_idn "$domain"),DNS:$domainlist"
-    fi
+    alt="$(_getIdType "$domain" | _upper_case):$domain"
+    for dl in $(echo "$domainlist" | tr "," ' '); do
+      alt="$alt,$(_getIdType "$dl" | _upper_case):$dl"
+    done
     #multi
     _info "Multi domain" "$alt"
     printf -- "\nsubjectAltName=$alt" >>"$csrconf"
@@ -3161,12 +3182,12 @@ _checkConf() {
       FOUND_REAL_NGINX_CONF="$2"
       return 0
     fi
-    if cat "$2" | tr "\t" " " | grep "^ *include +.*;" >/dev/null; then
+    if cat "$2" | tr "\t" " " | grep "^ *include *.*;" >/dev/null; then
       _debug "Try include files"
-      for included in $(cat "$2" | tr "\t" " " | grep "^ *include +.*;" | sed "s/include //" | tr -d " ;"); do
+      for included in $(cat "$2" | tr "\t" " " | grep "^ *include *.*;" | sed "s/include //" | tr -d " ;"); do
         _debug "check included $included"
         if ! _startswith "$included" "/" && _exists dirname; then
-          _relpath="$(dirname "$_c_file")"
+          _relpath="$(dirname "$2")"
           _debug "_relpath" "$_relpath"
           included="$_relpath/$included"
         fi
@@ -3380,6 +3401,8 @@ _on_before_issue() {
   if [ "$_chk_pre_hook" ]; then
     _info "Run pre hook:'$_chk_pre_hook'"
     if ! (
+      export Le_Domain="$_chk_main_domain"
+      export Le_Alt="$_chk_alt_domains"
       cd "$DOMAIN_PATH" && eval "$_chk_pre_hook"
     ); then
       _err "Error when run pre hook."
@@ -4170,6 +4193,36 @@ _match_issuer() {
   _contains "$_rootissuer" "$_missuer"
 }
 
+#ip
+_isIPv4() {
+  for seg in $(echo "$1" | tr '.' ' '); do
+    if [ $seg -ge 0 ] 2>/dev/null && [ $seg -le 255 ] 2>/dev/null; then
+      continue
+    fi
+    return 1
+  done
+  return 0
+}
+
+#ip6
+_isIPv6() {
+  _contains "$1" ":"
+}
+
+#ip
+_isIP() {
+  _isIPv4 "$1" || _isIPv6 "$1"
+}
+
+#identifier
+_getIdType() {
+  if _isIP "$1"; then
+    echo "$ID_TYPE_IP"
+  else
+    echo "$ID_TYPE_DNS"
+  fi
+}
+
 #webroot, domain domainlist  keylength
 issue() {
   if [ -z "$2" ]; then
@@ -4218,12 +4271,6 @@ issue() {
     return 1
   fi
 
-  _debug "Using ACME_DIRECTORY: $ACME_DIRECTORY"
-
-  if ! _initAPI; then
-    return 1
-  fi
-
   if [ -f "$DOMAIN_CONF" ]; then
     Le_NextRenewTime=$(_readdomainconf Le_NextRenewTime)
     _debug Le_NextRenewTime "$Le_NextRenewTime"
@@ -4243,6 +4290,11 @@ issue() {
     fi
   fi
 
+  _debug "Using ACME_DIRECTORY: $ACME_DIRECTORY"
+  if ! _initAPI; then
+    return 1
+  fi
+
   _savedomainconf "Le_Domain" "$_main_domain"
   _savedomainconf "Le_Alt" "$_alt_domains"
   _savedomainconf "Le_Webroot" "$_web_roots"
@@ -4327,7 +4379,7 @@ issue() {
   dvsep=','
   if [ -z "$vlist" ]; then
     #make new order request
-    _identifiers="{\"type\":\"dns\",\"value\":\"$(_idn "$_main_domain")\"}"
+    _identifiers="{\"type\":\"$(_getIdType "$_main_domain")\",\"value\":\"$(_idn "$_main_domain")\"}"
     _w_index=1
     while true; do
       d="$(echo "$_alt_domains," | cut -d , -f "$_w_index")"
@@ -4336,7 +4388,7 @@ issue() {
       if [ -z "$d" ]; then
         break
       fi
-      _identifiers="$_identifiers,{\"type\":\"dns\",\"value\":\"$(_idn "$d")\"}"
+      _identifiers="$_identifiers,{\"type\":\"$(_getIdType "$d")\",\"value\":\"$(_idn "$d")\"}"
     done
     _debug2 _identifiers "$_identifiers"
     if ! _send_signed_request "$ACME_NEW_ORDER" "{\"identifiers\": [$_identifiers]}"; then
@@ -4930,7 +4982,9 @@ $_authorizations_map"
 
   echo "$response" >"$CERT_PATH"
   _split_cert_chain "$CERT_PATH" "$CERT_FULLCHAIN_PATH" "$CA_CERT_PATH"
-
+  if [ -z "$_preferred_chain" ]; then
+    _preferred_chain=$(_readcaconf DEFAULT_PREFERRED_CHAIN)
+  fi
   if [ "$_preferred_chain" ] && [ -f "$CERT_FULLCHAIN_PATH" ]; then
     if [ "$DEBUG" ]; then
       _debug "default chain issuers: " "$(_get_chain_issuers "$CERT_FULLCHAIN_PATH")"
@@ -5108,7 +5162,7 @@ renew() {
 
   . "$DOMAIN_CONF"
   _debug Le_API "$Le_API"
-  if [ -z "$Le_API" ]; then
+  if [ -z "$Le_API" ] || [ "$CA_LETSENCRYPT_V1" = "$Le_API" ]; then
     #if this is from an old version, Le_API is empty,
     #so, we force to use letsencrypt server
     Le_API="$CA_LETSENCRYPT_V2"
@@ -5125,7 +5179,6 @@ renew() {
     CA_CONF=""
     _debug3 "initpath again."
     _initpath "$Le_Domain" "$_isEcc"
-    _initAPI
   fi
 
   if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then
@@ -5670,8 +5723,16 @@ installcronjob() {
   if [ -f "$LE_WORKING_DIR/$PROJECT_ENTRY" ]; then
     lesh="\"$LE_WORKING_DIR\"/$PROJECT_ENTRY"
   else
-    _err "Can not install cronjob, $PROJECT_ENTRY not found."
-    return 1
+    _debug "_SCRIPT_" "$_SCRIPT_"
+    _script="$(_readlink "$_SCRIPT_")"
+    _debug _script "$_script"
+    if [ -f "$_script" ]; then
+      _info "Using the current script from: $_script"
+      lesh="$_script"
+    else
+      _err "Can not install cronjob, $PROJECT_ENTRY not found."
+      return 1
+    fi
   fi
   if [ "$_c_home" ]; then
     _c_entry="--config-home \"$_c_home\" "
@@ -5743,7 +5804,7 @@ uninstallcronjob() {
   _info "Removing cron job"
   cr="$($_CRONTAB -l | grep "$PROJECT_ENTRY --cron")"
   if [ "$cr" ]; then
-    if _exists uname && uname -a | grep solaris >/dev/null; then
+    if _exists uname && uname -a | grep SunOS >/dev/null; then
       $_CRONTAB -l | sed "/$PROJECT_ENTRY --cron/d" | $_CRONTAB --
     else
       $_CRONTAB -l | sed "/$PROJECT_ENTRY --cron/d" | $_CRONTAB -
@@ -5898,7 +5959,7 @@ _deactivate() {
     _initAPI
   fi
 
-  _identifiers="{\"type\":\"dns\",\"value\":\"$_d_domain\"}"
+  _identifiers="{\"type\":\"$(_getIdType "$_d_domain")\",\"value\":\"$_d_domain\"}"
   if ! _send_signed_request "$ACME_NEW_ORDER" "{\"identifiers\": [$_identifiers]}"; then
     _err "Can not get domain new order."
     return 1
@@ -5934,7 +5995,7 @@ _deactivate() {
       thumbprint="$(__calc_account_thumbprint)"
     fi
     _debug "Trigger validation."
-    vtype="$VTYPE_DNS"
+    vtype="$(_getIdType "$_d_domain")"
     entry="$(echo "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')"
     _debug entry "$entry"
     if [ -z "$entry" ]; then
@@ -6543,6 +6604,8 @@ Commands:
   --deactivate             Deactivate the domain authz, professional use.
   --set-default-ca         Used with '--server', Set the default CA to use.
                            See: $_SERVER_WIKI
+  --set-default-chain      Set the default preferred chain for a CA.
+                           See: $_PREFERRED_CHAIN_WIKI
 
 
 Parameters:
@@ -6829,6 +6892,18 @@ setdefaultca() {
   _info "Changed default CA to: $(__green "$ACME_DIRECTORY")"
 }
 
+#preferred-chain
+setdefaultchain() {
+  _initpath
+  _preferred_chain="$1"
+  if [ -z "$_preferred_chain" ]; then
+    _err "Please give a '--preferred-chain value' value."
+    return 1
+  fi
+  mkdir -p "$CA_DIR"
+  _savecaconf "DEFAULT_PREFERRED_CHAIN" "$_preferred_chain"
+}
+
 _process() {
   _CMD=""
   _domain=""
@@ -6980,6 +7055,9 @@ _process() {
     --set-default-ca)
       _CMD="setdefaultca"
       ;;
+    --set-default-chain)
+      _CMD="setdefaultchain"
+      ;;
     -d | --domain)
       _dvalue="$2"
 
@@ -7510,6 +7588,9 @@ _process() {
   setdefaultca)
     setdefaultca
     ;;
+  setdefaultchain)
+    setdefaultchain "$_preferred_chain"
+    ;;
   *)
     if [ "$_CMD" ]; then
       _err "Invalid command: $_CMD"

+ 15 - 10
deploy/haproxy.sh

@@ -54,11 +54,6 @@ haproxy_deploy() {
   DEPLOY_HAPROXY_ISSUER_DEFAULT="no"
   DEPLOY_HAPROXY_RELOAD_DEFAULT="true"
 
-  if [ -f "${DOMAIN_CONF}" ]; then
-    # shellcheck disable=SC1090
-    . "${DOMAIN_CONF}"
-  fi
-
   _debug _cdomain "${_cdomain}"
   _debug _ckey "${_ckey}"
   _debug _ccert "${_ccert}"
@@ -66,6 +61,8 @@ haproxy_deploy() {
   _debug _cfullchain "${_cfullchain}"
 
   # PEM_PATH is optional. If not provided then assume "${DEPLOY_HAPROXY_PEM_PATH_DEFAULT}"
+  _getdeployconf DEPLOY_HAPROXY_PEM_PATH
+  _debug2 DEPLOY_HAPROXY_PEM_PATH "${DEPLOY_HAPROXY_PEM_PATH}"
   if [ -n "${DEPLOY_HAPROXY_PEM_PATH}" ]; then
     Le_Deploy_haproxy_pem_path="${DEPLOY_HAPROXY_PEM_PATH}"
     _savedomainconf Le_Deploy_haproxy_pem_path "${Le_Deploy_haproxy_pem_path}"
@@ -82,6 +79,8 @@ haproxy_deploy() {
   fi
 
   # PEM_NAME is optional. If not provided then assume "${DEPLOY_HAPROXY_PEM_NAME_DEFAULT}"
+  _getdeployconf DEPLOY_HAPROXY_PEM_NAME
+  _debug2 DEPLOY_HAPROXY_PEM_NAME "${DEPLOY_HAPROXY_PEM_NAME}"
   if [ -n "${DEPLOY_HAPROXY_PEM_NAME}" ]; then
     Le_Deploy_haproxy_pem_name="${DEPLOY_HAPROXY_PEM_NAME}"
     _savedomainconf Le_Deploy_haproxy_pem_name "${Le_Deploy_haproxy_pem_name}"
@@ -90,6 +89,8 @@ haproxy_deploy() {
   fi
 
   # BUNDLE is optional. If not provided then assume "${DEPLOY_HAPROXY_BUNDLE_DEFAULT}"
+  _getdeployconf DEPLOY_HAPROXY_BUNDLE
+  _debug2 DEPLOY_HAPROXY_BUNDLE "${DEPLOY_HAPROXY_BUNDLE}"
   if [ -n "${DEPLOY_HAPROXY_BUNDLE}" ]; then
     Le_Deploy_haproxy_bundle="${DEPLOY_HAPROXY_BUNDLE}"
     _savedomainconf Le_Deploy_haproxy_bundle "${Le_Deploy_haproxy_bundle}"
@@ -98,6 +99,8 @@ haproxy_deploy() {
   fi
 
   # ISSUER is optional. If not provided then assume "${DEPLOY_HAPROXY_ISSUER_DEFAULT}"
+  _getdeployconf DEPLOY_HAPROXY_ISSUER
+  _debug2 DEPLOY_HAPROXY_ISSUER "${DEPLOY_HAPROXY_ISSUER}"
   if [ -n "${DEPLOY_HAPROXY_ISSUER}" ]; then
     Le_Deploy_haproxy_issuer="${DEPLOY_HAPROXY_ISSUER}"
     _savedomainconf Le_Deploy_haproxy_issuer "${Le_Deploy_haproxy_issuer}"
@@ -106,6 +109,8 @@ haproxy_deploy() {
   fi
 
   # RELOAD is optional. If not provided then assume "${DEPLOY_HAPROXY_RELOAD_DEFAULT}"
+  _getdeployconf DEPLOY_HAPROXY_RELOAD
+  _debug2 DEPLOY_HAPROXY_RELOAD "${DEPLOY_HAPROXY_RELOAD}"
   if [ -n "${DEPLOY_HAPROXY_RELOAD}" ]; then
     Le_Deploy_haproxy_reload="${DEPLOY_HAPROXY_RELOAD}"
     _savedomainconf Le_Deploy_haproxy_reload "${Le_Deploy_haproxy_reload}"
@@ -190,7 +195,7 @@ haproxy_deploy() {
     _info "Updating OCSP stapling info"
     _debug _ocsp "${_ocsp}"
     _info "Extracting OCSP URL"
-    _ocsp_url=$(openssl x509 -noout -ocsp_uri -in "${_pem}")
+    _ocsp_url=$(${ACME_OPENSSL_BIN:-openssl} x509 -noout -ocsp_uri -in "${_pem}")
     _debug _ocsp_url "${_ocsp_url}"
 
     # Only process OCSP if URL was present
@@ -203,9 +208,9 @@ haproxy_deploy() {
       # Only process the certificate if we have a .issuer file
       if [ -r "${_issuer}" ]; then
         # Check if issuer cert is also a root CA cert
-        _subjectdn=$(openssl x509 -in "${_issuer}" -subject -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10)
+        _subjectdn=$(${ACME_OPENSSL_BIN:-openssl} x509 -in "${_issuer}" -subject -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10)
         _debug _subjectdn "${_subjectdn}"
-        _issuerdn=$(openssl x509 -in "${_issuer}" -issuer -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10)
+        _issuerdn=$(${ACME_OPENSSL_BIN:-openssl} x509 -in "${_issuer}" -issuer -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10)
         _debug _issuerdn "${_issuerdn}"
         _info "Requesting OCSP response"
         # If the issuer is a CA cert then our command line has "-CAfile" added
@@ -216,7 +221,7 @@ haproxy_deploy() {
         fi
         _debug _cafile_argument "${_cafile_argument}"
         # if OpenSSL/LibreSSL is v1.1 or above, the format for the -header option has changed
-        _openssl_version=$(openssl version | cut -d' ' -f2)
+        _openssl_version=$(${ACME_OPENSSL_BIN:-openssl} version | cut -d' ' -f2)
         _debug _openssl_version "${_openssl_version}"
         _openssl_major=$(echo "${_openssl_version}" | cut -d '.' -f1)
         _openssl_minor=$(echo "${_openssl_version}" | cut -d '.' -f2)
@@ -226,7 +231,7 @@ haproxy_deploy() {
           _header_sep=" "
         fi
         # Request the OCSP response from the issuer and store it
-        _openssl_ocsp_cmd="openssl ocsp \
+        _openssl_ocsp_cmd="${ACME_OPENSSL_BIN:-openssl} ocsp \
           -issuer \"${_issuer}\" \
           -cert \"${_pem}\" \
           -url \"${_ocsp_url}\" \

+ 1 - 1
deploy/kong.sh

@@ -45,7 +45,7 @@ kong_deploy() {
   #Generate data for request (Multipart/form-data with mixed content)
   if [ -z "$ssl_uuid" ]; then
     #set sni to domain
-    content="--$delim${nl}Content-Disposition: form-data; name=\"snis\"${nl}${nl}$_cdomain"
+    content="--$delim${nl}Content-Disposition: form-data; name=\"snis[]\"${nl}${nl}$_cdomain"
   fi
   #add key
   content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"key\"; filename=\"$(basename "$_ckey")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")"

+ 280 - 0
deploy/lighttpd.sh

@@ -0,0 +1,280 @@
+#!/usr/bin/env sh
+
+# Script for acme.sh to deploy certificates to lighttpd
+#
+# The following variables can be exported:
+#
+# export DEPLOY_LIGHTTPD_PEM_NAME="${domain}.pem"
+#
+# Defines the name of the PEM file.
+# Defaults to "<domain>.pem"
+#
+# export DEPLOY_LIGHTTPD_PEM_PATH="/etc/lighttpd"
+#
+# Defines location of PEM file for Lighttpd.
+# Defaults to /etc/lighttpd
+#
+# export DEPLOY_LIGHTTPD_RELOAD="systemctl reload lighttpd"
+#
+# OPTIONAL: Reload command used post deploy
+# This defaults to be a no-op (ie "true").
+# It is strongly recommended to set this something that makes sense
+# for your distro.
+#
+# export DEPLOY_LIGHTTPD_ISSUER="yes"
+#
+# OPTIONAL: Places CA file as "${DEPLOY_LIGHTTPD_PEM}.issuer"
+# Note: Required for OCSP stapling to work
+#
+# export DEPLOY_LIGHTTPD_BUNDLE="no"
+#
+# OPTIONAL: Deploy this certificate as part of a multi-cert bundle
+# This adds a suffix to the certificate based on the certificate type
+# eg RSA certificates will have .rsa as a suffix to the file name
+# Lighttpd will load all certificates and provide one or the other
+# depending on client capabilities
+# Note: This functionality requires Lighttpd was compiled against
+# a version of OpenSSL that supports this.
+#
+
+########  Public functions #####################
+
+#domain keyfile certfile cafile fullchain
+lighttpd_deploy() {
+  _cdomain="$1"
+  _ckey="$2"
+  _ccert="$3"
+  _cca="$4"
+  _cfullchain="$5"
+
+  # Some defaults
+  DEPLOY_LIGHTTPD_PEM_PATH_DEFAULT="/etc/lighttpd"
+  DEPLOY_LIGHTTPD_PEM_NAME_DEFAULT="${_cdomain}.pem"
+  DEPLOY_LIGHTTPD_BUNDLE_DEFAULT="no"
+  DEPLOY_LIGHTTPD_ISSUER_DEFAULT="yes"
+  DEPLOY_LIGHTTPD_RELOAD_DEFAULT="true"
+
+  _debug _cdomain "${_cdomain}"
+  _debug _ckey "${_ckey}"
+  _debug _ccert "${_ccert}"
+  _debug _cca "${_cca}"
+  _debug _cfullchain "${_cfullchain}"
+
+  # PEM_PATH is optional. If not provided then assume "${DEPLOY_LIGHTTPD_PEM_PATH_DEFAULT}"
+  _getdeployconf DEPLOY_LIGHTTPD_PEM_PATH
+  _debug2 DEPLOY_LIGHTTPD_PEM_PATH "${DEPLOY_LIGHTTPD_PEM_PATH}"
+  if [ -n "${DEPLOY_LIGHTTPD_PEM_PATH}" ]; then
+    Le_Deploy_lighttpd_pem_path="${DEPLOY_LIGHTTPD_PEM_PATH}"
+    _savedomainconf Le_Deploy_lighttpd_pem_path "${Le_Deploy_lighttpd_pem_path}"
+  elif [ -z "${Le_Deploy_lighttpd_pem_path}" ]; then
+    Le_Deploy_lighttpd_pem_path="${DEPLOY_LIGHTTPD_PEM_PATH_DEFAULT}"
+  fi
+
+  # Ensure PEM_PATH exists
+  if [ -d "${Le_Deploy_lighttpd_pem_path}" ]; then
+    _debug "PEM_PATH ${Le_Deploy_lighttpd_pem_path} exists"
+  else
+    _err "PEM_PATH ${Le_Deploy_lighttpd_pem_path} does not exist"
+    return 1
+  fi
+
+  # PEM_NAME is optional. If not provided then assume "${DEPLOY_LIGHTTPD_PEM_NAME_DEFAULT}"
+  _getdeployconf DEPLOY_LIGHTTPD_PEM_NAME
+  _debug2 DEPLOY_LIGHTTPD_PEM_NAME "${DEPLOY_LIGHTTPD_PEM_NAME}"
+  if [ -n "${DEPLOY_LIGHTTPD_PEM_NAME}" ]; then
+    Le_Deploy_lighttpd_pem_name="${DEPLOY_LIGHTTPD_PEM_NAME}"
+    _savedomainconf Le_Deploy_lighttpd_pem_name "${Le_Deploy_lighttpd_pem_name}"
+  elif [ -z "${Le_Deploy_lighttpd_pem_name}" ]; then
+    Le_Deploy_lighttpd_pem_name="${DEPLOY_LIGHTTPD_PEM_NAME_DEFAULT}"
+  fi
+
+  # BUNDLE is optional. If not provided then assume "${DEPLOY_LIGHTTPD_BUNDLE_DEFAULT}"
+  _getdeployconf DEPLOY_LIGHTTPD_BUNDLE
+  _debug2 DEPLOY_LIGHTTPD_BUNDLE "${DEPLOY_LIGHTTPD_BUNDLE}"
+  if [ -n "${DEPLOY_LIGHTTPD_BUNDLE}" ]; then
+    Le_Deploy_lighttpd_bundle="${DEPLOY_LIGHTTPD_BUNDLE}"
+    _savedomainconf Le_Deploy_lighttpd_bundle "${Le_Deploy_lighttpd_bundle}"
+  elif [ -z "${Le_Deploy_lighttpd_bundle}" ]; then
+    Le_Deploy_lighttpd_bundle="${DEPLOY_LIGHTTPD_BUNDLE_DEFAULT}"
+  fi
+
+  # ISSUER is optional. If not provided then assume "${DEPLOY_LIGHTTPD_ISSUER_DEFAULT}"
+  _getdeployconf DEPLOY_LIGHTTPD_ISSUER
+  _debug2 DEPLOY_LIGHTTPD_ISSUER "${DEPLOY_LIGHTTPD_ISSUER}"
+  if [ -n "${DEPLOY_LIGHTTPD_ISSUER}" ]; then
+    Le_Deploy_lighttpd_issuer="${DEPLOY_LIGHTTPD_ISSUER}"
+    _savedomainconf Le_Deploy_lighttpd_issuer "${Le_Deploy_lighttpd_issuer}"
+  elif [ -z "${Le_Deploy_lighttpd_issuer}" ]; then
+    Le_Deploy_lighttpd_issuer="${DEPLOY_LIGHTTPD_ISSUER_DEFAULT}"
+  fi
+
+  # RELOAD is optional. If not provided then assume "${DEPLOY_LIGHTTPD_RELOAD_DEFAULT}"
+  _getdeployconf DEPLOY_LIGHTTPD_RELOAD
+  _debug2 DEPLOY_LIGHTTPD_RELOAD "${DEPLOY_LIGHTTPD_RELOAD}"
+  if [ -n "${DEPLOY_LIGHTTPD_RELOAD}" ]; then
+    Le_Deploy_lighttpd_reload="${DEPLOY_LIGHTTPD_RELOAD}"
+    _savedomainconf Le_Deploy_lighttpd_reload "${Le_Deploy_lighttpd_reload}"
+  elif [ -z "${Le_Deploy_lighttpd_reload}" ]; then
+    Le_Deploy_lighttpd_reload="${DEPLOY_LIGHTTPD_RELOAD_DEFAULT}"
+  fi
+
+  # Set the suffix depending if we are creating a bundle or not
+  if [ "${Le_Deploy_lighttpd_bundle}" = "yes" ]; then
+    _info "Bundle creation requested"
+    # Initialise $Le_Keylength if its not already set
+    if [ -z "${Le_Keylength}" ]; then
+      Le_Keylength=""
+    fi
+    if _isEccKey "${Le_Keylength}"; then
+      _info "ECC key type detected"
+      _suffix=".ecdsa"
+    else
+      _info "RSA key type detected"
+      _suffix=".rsa"
+    fi
+  else
+    _suffix=""
+  fi
+  _debug _suffix "${_suffix}"
+
+  # Set variables for later
+  _pem="${Le_Deploy_lighttpd_pem_path}/${Le_Deploy_lighttpd_pem_name}${_suffix}"
+  _issuer="${_pem}.issuer"
+  _ocsp="${_pem}.ocsp"
+  _reload="${Le_Deploy_lighttpd_reload}"
+
+  _info "Deploying PEM file"
+  # Create a temporary PEM file
+  _temppem="$(_mktemp)"
+  _debug _temppem "${_temppem}"
+  cat "${_ckey}" "${_ccert}" "${_cca}" >"${_temppem}"
+  _ret="$?"
+
+  # Check that we could create the temporary file
+  if [ "${_ret}" != "0" ]; then
+    _err "Error code ${_ret} returned during PEM file creation"
+    [ -f "${_temppem}" ] && rm -f "${_temppem}"
+    return ${_ret}
+  fi
+
+  # Move PEM file into place
+  _info "Moving new certificate into place"
+  _debug _pem "${_pem}"
+  cat "${_temppem}" >"${_pem}"
+  _ret=$?
+
+  # Clean up temp file
+  [ -f "${_temppem}" ] && rm -f "${_temppem}"
+
+  # Deal with any failure of moving PEM file into place
+  if [ "${_ret}" != "0" ]; then
+    _err "Error code ${_ret} returned while moving new certificate into place"
+    return ${_ret}
+  fi
+
+  # Update .issuer file if requested
+  if [ "${Le_Deploy_lighttpd_issuer}" = "yes" ]; then
+    _info "Updating .issuer file"
+    _debug _issuer "${_issuer}"
+    cat "${_cca}" >"${_issuer}"
+    _ret="$?"
+
+    if [ "${_ret}" != "0" ]; then
+      _err "Error code ${_ret} returned while copying issuer/CA certificate into place"
+      return ${_ret}
+    fi
+  else
+    [ -f "${_issuer}" ] && _err "Issuer file update not requested but .issuer file exists"
+  fi
+
+  # Update .ocsp file if certificate was requested with --ocsp/--ocsp-must-staple option
+  if [ -z "${Le_OCSP_Staple}" ]; then
+    Le_OCSP_Staple="0"
+  fi
+  if [ "${Le_OCSP_Staple}" = "1" ]; then
+    _info "Updating OCSP stapling info"
+    _debug _ocsp "${_ocsp}"
+    _info "Extracting OCSP URL"
+    _ocsp_url=$(${ACME_OPENSSL_BIN:-openssl} x509 -noout -ocsp_uri -in "${_pem}")
+    _debug _ocsp_url "${_ocsp_url}"
+
+    # Only process OCSP if URL was present
+    if [ "${_ocsp_url}" != "" ]; then
+      # Extract the hostname from the OCSP URL
+      _info "Extracting OCSP URL"
+      _ocsp_host=$(echo "${_ocsp_url}" | cut -d/ -f3)
+      _debug _ocsp_host "${_ocsp_host}"
+
+      # Only process the certificate if we have a .issuer file
+      if [ -r "${_issuer}" ]; then
+        # Check if issuer cert is also a root CA cert
+        _subjectdn=$(${ACME_OPENSSL_BIN:-openssl} x509 -in "${_issuer}" -subject -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10)
+        _debug _subjectdn "${_subjectdn}"
+        _issuerdn=$(${ACME_OPENSSL_BIN:-openssl} x509 -in "${_issuer}" -issuer -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10)
+        _debug _issuerdn "${_issuerdn}"
+        _info "Requesting OCSP response"
+        # If the issuer is a CA cert then our command line has "-CAfile" added
+        if [ "${_subjectdn}" = "${_issuerdn}" ]; then
+          _cafile_argument="-CAfile \"${_issuer}\""
+        else
+          _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_version=$(${ACME_OPENSSL_BIN:-openssl} version | cut -d' ' -f2)
+        _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
+          _header_sep="="
+        else
+          _header_sep=" "
+        fi
+        # Request the OCSP response from the issuer and store it
+        _openssl_ocsp_cmd="${ACME_OPENSSL_BIN:-openssl} ocsp \
+          -issuer \"${_issuer}\" \
+          -cert \"${_pem}\" \
+          -url \"${_ocsp_url}\" \
+          -header Host${_header_sep}\"${_ocsp_host}\" \
+          -respout \"${_ocsp}\" \
+          -verify_other \"${_issuer}\" \
+          ${_cafile_argument} \
+          | grep -q \"${_pem}: good\""
+        _debug _openssl_ocsp_cmd "${_openssl_ocsp_cmd}"
+        eval "${_openssl_ocsp_cmd}"
+        _ret=$?
+      else
+        # Non fatal: No issuer file was present so no OCSP stapling file created
+        _err "OCSP stapling in use but no .issuer file was present"
+      fi
+    else
+      # Non fatal: No OCSP url was found int the certificate
+      _err "OCSP update requested but no OCSP URL was found in certificate"
+    fi
+
+    # Non fatal: Check return code of openssl command
+    if [ "${_ret}" != "0" ]; then
+      _err "Updating OCSP stapling failed with return code ${_ret}"
+    fi
+  else
+    # An OCSP file was already present but certificate did not have OCSP extension
+    if [ -f "${_ocsp}" ]; then
+      _err "OCSP was not requested but .ocsp file exists."
+      # Could remove the file at this step, although Lighttpd just ignores it in this case
+      # rm -f "${_ocsp}" || _err "Problem removing stale .ocsp file"
+    fi
+  fi
+
+  # Reload Lighttpd
+  _debug _reload "${_reload}"
+  eval "${_reload}"
+  _ret=$?
+  if [ "${_ret}" != "0" ]; then
+    _err "Error code ${_ret} during reload"
+    return ${_ret}
+  else
+    _info "Reload successful"
+  fi
+
+  return 0
+}

+ 22 - 5
deploy/ssh.sh

@@ -37,11 +37,6 @@ ssh_deploy() {
   _cfullchain="$5"
   _deploy_ssh_servers=""
 
-  if [ -f "$DOMAIN_CONF" ]; then
-    # shellcheck disable=SC1090
-    . "$DOMAIN_CONF"
-  fi
-
   _debug _cdomain "$_cdomain"
   _debug _ckey "$_ckey"
   _debug _ccert "$_ccert"
@@ -49,6 +44,8 @@ ssh_deploy() {
   _debug _cfullchain "$_cfullchain"
 
   # USER is required to login by SSH to remote host.
+  _getdeployconf DEPLOY_SSH_USER
+  _debug2 DEPLOY_SSH_USER "$DEPLOY_SSH_USER"
   if [ -z "$DEPLOY_SSH_USER" ]; then
     if [ -z "$Le_Deploy_ssh_user" ]; then
       _err "DEPLOY_SSH_USER not defined."
@@ -60,6 +57,8 @@ ssh_deploy() {
   fi
 
   # SERVER is optional. If not provided then use _cdomain
+  _getdeployconf DEPLOY_SSH_SERVER
+  _debug2 DEPLOY_SSH_SERVER "$DEPLOY_SSH_SERVER"
   if [ -n "$DEPLOY_SSH_SERVER" ]; then
     Le_Deploy_ssh_server="$DEPLOY_SSH_SERVER"
     _savedomainconf Le_Deploy_ssh_server "$Le_Deploy_ssh_server"
@@ -68,6 +67,8 @@ ssh_deploy() {
   fi
 
   # CMD is optional. If not provided then use ssh
+  _getdeployconf DEPLOY_SSH_CMD
+  _debug2 DEPLOY_SSH_CMD "$DEPLOY_SSH_CMD"
   if [ -n "$DEPLOY_SSH_CMD" ]; then
     Le_Deploy_ssh_cmd="$DEPLOY_SSH_CMD"
     _savedomainconf Le_Deploy_ssh_cmd "$Le_Deploy_ssh_cmd"
@@ -94,6 +95,8 @@ ssh_deploy() {
   fi
 
   # BACKUP is optional. If not provided then default to previously saved value or yes.
+  _getdeployconf DEPLOY_SSH_BACKUP
+  _debug2 DEPLOY_SSH_BACKUP "$DEPLOY_SSH_BACKUP"
   if [ "$DEPLOY_SSH_BACKUP" = "no" ]; then
     Le_Deploy_ssh_backup="no"
   elif [ -z "$Le_Deploy_ssh_backup" ] || [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
@@ -102,6 +105,8 @@ ssh_deploy() {
   _savedomainconf Le_Deploy_ssh_backup "$Le_Deploy_ssh_backup"
 
   # BACKUP_PATH is optional. If not provided then default to previously saved value or .acme_ssh_deploy
+  _getdeployconf DEPLOY_SSH_BACKUP_PATH
+  _debug2 DEPLOY_SSH_BACKUP_PATH "$DEPLOY_SSH_BACKUP_PATH"
   if [ -n "$DEPLOY_SSH_BACKUP_PATH" ]; then
     Le_Deploy_ssh_backup_path="$DEPLOY_SSH_BACKUP_PATH"
   elif [ -z "$Le_Deploy_ssh_backup_path" ]; then
@@ -111,6 +116,8 @@ ssh_deploy() {
 
   # MULTI_CALL is optional. If not provided then default to previously saved
   # value (which may be undefined... equivalent to "no").
+  _getdeployconf DEPLOY_SSH_MULTI_CALL
+  _debug2 DEPLOY_SSH_MULTI_CALL "$DEPLOY_SSH_MULTI_CALL"
   if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
     Le_Deploy_ssh_multi_call="yes"
     _savedomainconf Le_Deploy_ssh_multi_call "$Le_Deploy_ssh_multi_call"
@@ -189,6 +196,8 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
 
   # KEYFILE is optional.
   # If provided then private key will be copied to provided filename.
+  _getdeployconf DEPLOY_SSH_KEYFILE
+  _debug2 DEPLOY_SSH_KEYFILE "$DEPLOY_SSH_KEYFILE"
   if [ -n "$DEPLOY_SSH_KEYFILE" ]; then
     Le_Deploy_ssh_keyfile="$DEPLOY_SSH_KEYFILE"
     _savedomainconf Le_Deploy_ssh_keyfile "$Le_Deploy_ssh_keyfile"
@@ -225,6 +234,8 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
 
   # CERTFILE is optional.
   # If provided then certificate will be copied or appended to provided filename.
+  _getdeployconf DEPLOY_SSH_CERTFILE
+  _debug2 DEPLOY_SSH_CERTFILE "$DEPLOY_SSH_CERTFILE"
   if [ -n "$DEPLOY_SSH_CERTFILE" ]; then
     Le_Deploy_ssh_certfile="$DEPLOY_SSH_CERTFILE"
     _savedomainconf Le_Deploy_ssh_certfile "$Le_Deploy_ssh_certfile"
@@ -273,6 +284,8 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
 
   # CAFILE is optional.
   # If provided then CA intermediate certificate will be copied or appended to provided filename.
+  _getdeployconf DEPLOY_SSH_CAFILE
+  _debug2 DEPLOY_SSH_CAFILE "$DEPLOY_SSH_CAFILE"
   if [ -n "$DEPLOY_SSH_CAFILE" ]; then
     Le_Deploy_ssh_cafile="$DEPLOY_SSH_CAFILE"
     _savedomainconf Le_Deploy_ssh_cafile "$Le_Deploy_ssh_cafile"
@@ -329,6 +342,8 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
 
   # FULLCHAIN is optional.
   # If provided then fullchain certificate will be copied or appended to provided filename.
+  _getdeployconf DEPLOY_SSH_FULLCHAIN
+  _debug2 DEPLOY_SSH_FULLCHAIN "$DEPLOY_SSH_FULLCHAIN"
   if [ -n "$DEPLOY_SSH_FULLCHAIN" ]; then
     Le_Deploy_ssh_fullchain="$DEPLOY_SSH_FULLCHAIN"
     _savedomainconf Le_Deploy_ssh_fullchain "$Le_Deploy_ssh_fullchain"
@@ -399,6 +414,8 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
 
   # REMOTE_CMD is optional.
   # If provided then this command will be executed on remote host.
+  _getdeployconf DEPLOY_SSH_REMOTE_CMD
+  _debug2 DEPLOY_SSH_REMOTE_CMD "$DEPLOY_SSH_REMOTE_CMD"
   if [ -n "$DEPLOY_SSH_REMOTE_CMD" ]; then
     Le_Deploy_ssh_remote_cmd="$DEPLOY_SSH_REMOTE_CMD"
     _savedomainconf Le_Deploy_ssh_remote_cmd "$Le_Deploy_ssh_remote_cmd"

+ 1 - 0
deploy/synology_dsm.sh

@@ -100,6 +100,7 @@ synology_dsm_deploy() {
   if [ -z "$token" ]; then
     _err "Unable to authenticate to $SYNO_Hostname:$SYNO_Port using $SYNO_Scheme."
     _err "Check your username and password."
+    _err "If two-factor authentication is enabled for the user, you have to choose another user."
     return 1
   fi
   sid=$(echo "$response" | grep "sid" | sed -n 's/.*"sid" *: *"\([^"]*\).*/\1/p')

+ 51 - 26
dnsapi/dns_1984hosting.sh

@@ -46,7 +46,7 @@ dns_1984hosting_add() {
 
   postdata="entry=new"
   postdata="$postdata&type=TXT"
-  postdata="$postdata&ttl=3600"
+  postdata="$postdata&ttl=900"
   postdata="$postdata&zone=$_domain"
   postdata="$postdata&host=$_sub_domain"
   postdata="$postdata&rdata=%22$value%22"
@@ -93,20 +93,15 @@ dns_1984hosting_rm() {
   fi
   _debug _sub_domain "$_sub_domain"
   _debug _domain "$_domain"
-
   _debug "Delete $fulldomain TXT record"
-  url="https://management.1984hosting.com/domains"
 
-  _htmlget "$url" "$_domain"
-  _debug2 _response "$_response"
-  zone_id="$(echo "$_response" | _egrep_o 'zone\/[0-9]+')"
-  _debug2 zone_id "$zone_id"
-  if [ -z "$zone_id" ]; then
-    _err "Error getting zone_id for $1"
+  url="https://management.1984hosting.com/domains"
+  if ! _get_zone_id "$url" "$_domain"; then
+    _err "invalid zone" "$_domain"
     return 1
   fi
 
-  _htmlget "$url/$zone_id" "$_sub_domain"
+  _htmlget "$url/$_zone_id" "$txtvalue"
   _debug2 _response "$_response"
   entry_id="$(echo "$_response" | _egrep_o 'entry_[0-9]+' | sed 's/entry_//')"
   _debug2 entry_id "$entry_id"
@@ -135,7 +130,7 @@ dns_1984hosting_rm() {
 _1984hosting_login() {
   if ! _check_credentials; then return 1; fi
 
-  if _check_cookie; then
+  if _check_cookies; then
     _debug "Already logged in"
     return 0
   fi
@@ -150,9 +145,12 @@ _1984hosting_login() {
   _debug2 response "$response"
 
   if _contains "$response" '"loggedin": true'; then
-    One984HOSTING_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _tail_n 1 | _egrep_o 'sessionid=[^;]*;' | tr -d ';')"
-    export One984HOSTING_COOKIE
-    _saveaccountconf_mutable One984HOSTING_COOKIE "$One984HOSTING_COOKIE"
+    One984HOSTING_SESSIONID_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'sessionid=[^;]*;' | tr -d ';')"
+    One984HOSTING_CSRFTOKEN_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | tr -d ';')"
+    export One984HOSTING_SESSIONID_COOKIE
+    export One984HOSTING_CSRFTOKEN_COOKIE
+    _saveaccountconf_mutable One984HOSTING_SESSIONID_COOKIE "$One984HOSTING_SESSIONID_COOKIE"
+    _saveaccountconf_mutable One984HOSTING_CSRFTOKEN_COOKIE "$One984HOSTING_CSRFTOKEN_COOKIE"
     return 0
   fi
   return 1
@@ -169,21 +167,24 @@ _check_credentials() {
   return 0
 }
 
-_check_cookie() {
-  One984HOSTING_COOKIE="${One984HOSTING_COOKIE:-$(_readaccountconf_mutable One984HOSTING_COOKIE)}"
-  if [ -z "$One984HOSTING_COOKIE" ]; then
-    _debug "No cached cookie found"
+_check_cookies() {
+  One984HOSTING_SESSIONID_COOKIE="${One984HOSTING_SESSIONID_COOKIE:-$(_readaccountconf_mutable One984HOSTING_SESSIONID_COOKIE)}"
+  One984HOSTING_CSRFTOKEN_COOKIE="${One984HOSTING_CSRFTOKEN_COOKIE:-$(_readaccountconf_mutable One984HOSTING_CSRFTOKEN_COOKIE)}"
+  if [ -z "$One984HOSTING_SESSIONID_COOKIE" ] || [ -z "$One984HOSTING_CSRFTOKEN_COOKIE" ]; then
+    _debug "No cached cookie(s) found"
     return 1
   fi
 
   _authget "https://management.1984hosting.com/accounts/loginstatus/"
   if _contains "$response" '"ok": true'; then
-    _debug "Cached cookie still valid"
+    _debug "Cached cookies still valid"
     return 0
   fi
-  _debug "Cached cookie no longer valid"
-  One984HOSTING_COOKIE=""
-  _saveaccountconf_mutable One984HOSTING_COOKIE "$One984HOSTING_COOKIE"
+  _debug "Cached cookies no longer valid"
+  One984HOSTING_SESSIONID_COOKIE=""
+  One984HOSTING_CSRFTOKEN_COOKIE=""
+  _saveaccountconf_mutable One984HOSTING_SESSIONID_COOKIE "$One984HOSTING_SESSIONID_COOKIE"
+  _saveaccountconf_mutable One984HOSTING_CSRFTOKEN_COOKIE "$One984HOSTING_CSRFTOKEN_COOKIE"
   return 1
 }
 
@@ -215,9 +216,25 @@ _get_root() {
   return 1
 }
 
+#usage: _get_zone_id url domain.com
+#returns zone id for domain.com
+_get_zone_id() {
+  url=$1
+  domain=$2
+  _htmlget "$url" "$domain"
+  _debug2 _response "$_response"
+  _zone_id="$(echo "$_response" | _egrep_o 'zone\/[0-9]+' | _head_n 1)"
+  _debug2 _zone_id "$_zone_id"
+  if [ -z "$_zone_id" ]; then
+    _err "Error getting _zone_id for $2"
+    return 1
+  fi
+  return 0
+}
+
 # add extra headers to request
 _authget() {
-  export _H1="Cookie: $One984HOSTING_COOKIE"
+  export _H1="Cookie: $One984HOSTING_CSRFTOKEN_COOKIE;$One984HOSTING_SESSIONID_COOKIE"
   _response=$(_get "$1" | _normalizeJson)
   _debug2 _response "$_response"
 }
@@ -225,12 +242,20 @@ _authget() {
 # truncate huge HTML response
 # echo: Argument list too long
 _htmlget() {
-  export _H1="Cookie: $One984HOSTING_COOKIE"
-  _response=$(_get "$1" | grep "$2" | _head_n 1)
+  export _H1="Cookie: $One984HOSTING_CSRFTOKEN_COOKIE;$One984HOSTING_SESSIONID_COOKIE"
+  _response=$(_get "$1" | grep "$2")
+  if _contains "$_response" "@$2"; then
+    _response=$(echo "$_response" | grep -v "[@]" | _head_n 1)
+  fi
 }
 
 # add extra headers to request
 _authpost() {
-  export _H1="Cookie: $One984HOSTING_COOKIE"
+  url="https://management.1984hosting.com/domains"
+  _get_zone_id "$url" "$_domain"
+  csrf_header="$(echo "$One984HOSTING_CSRFTOKEN_COOKIE" | _egrep_o "=[^=][0-9a-zA-Z]*" | tr -d "=")"
+  export _H1="Cookie: $One984HOSTING_CSRFTOKEN_COOKIE;$One984HOSTING_SESSIONID_COOKIE"
+  export _H2="Referer: https://management.1984hosting.com/domains/$_zone_id"
+  export _H3="X-CSRFToken: $csrf_header"
   _response=$(_post "$1" "$2")
 }

+ 159 - 0
dnsapi/dns_cpanel.sh

@@ -0,0 +1,159 @@
+#!/usr/bin/env sh
+#
+#Author: Bjarne Saltbaek
+#Report Bugs here: https://github.com/acmesh-official/acme.sh/issues/3732
+#
+#
+########  Public functions #####################
+#
+# Export CPANEL username,api token and hostname in the following variables
+#
+# cPanel_Username=username
+# cPanel_Apitoken=apitoken
+# cPanel_Hostname=hostname
+#
+# Usage: add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+# Used to add txt record
+dns_cpanel_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  _info "Adding TXT record to cPanel based system"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+  _debug cPanel_Username "$cPanel_Username"
+  _debug cPanel_Apitoken "$cPanel_Apitoken"
+  _debug cPanel_Hostname "$cPanel_Hostname"
+
+  if ! _cpanel_login; then
+    _err "cPanel Login failed for user $cPanel_Username. Check $HTTP_HEADER file"
+    return 1
+  fi
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "No matching root domain for $fulldomain found"
+    return 1
+  fi
+  # adding entry
+  _info "Adding the entry"
+  stripped_fulldomain=$(echo "$fulldomain" | sed "s/.$_domain//")
+  _debug "Adding $stripped_fulldomain to $_domain zone"
+  _myget "json-api/cpanel?cpanel_jsonapi_apiversion=2&cpanel_jsonapi_module=ZoneEdit&cpanel_jsonapi_func=add_zone_record&domain=$_domain&name=$stripped_fulldomain&type=TXT&txtdata=$txtvalue&ttl=1"
+  if _successful_update; then return 0; fi
+  _err "Couldn't create entry!"
+  return 1
+}
+
+# Usage: fulldomain txtvalue
+# Used to remove the txt record after validation
+dns_cpanel_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  _info "Using cPanel based system"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  if ! _cpanel_login; then
+    _err "cPanel Login failed for user $cPanel_Username. Check $HTTP_HEADER file"
+    return 1
+  fi
+
+  if ! _get_root; then
+    _err "No matching root domain for $fulldomain found"
+    return 1
+  fi
+
+  _findentry "$fulldomain" "$txtvalue"
+  if [ -z "$_id" ]; then
+    _info "Entry doesn't exist, nothing to delete"
+    return 0
+  fi
+  _debug "Deleting record..."
+  _myget "json-api/cpanel?cpanel_jsonapi_apiversion=2&cpanel_jsonapi_module=ZoneEdit&cpanel_jsonapi_func=remove_zone_record&domain=$_domain&line=$_id"
+  # removing entry
+  _debug "_result is: $_result"
+
+  if _successful_update; then return 0; fi
+  _err "Couldn't delete entry!"
+  return 1
+}
+
+####################  Private functions below ##################################
+
+_checkcredentials() {
+  cPanel_Username="${cPanel_Username:-$(_readaccountconf_mutable cPanel_Username)}"
+  cPanel_Apitoken="${cPanel_Apitoken:-$(_readaccountconf_mutable cPanel_Apitoken)}"
+  cPanel_Hostname="${cPanel_Hostname:-$(_readaccountconf_mutable cPanel_Hostname)}"
+
+  if [ -z "$cPanel_Username" ] || [ -z "$cPanel_Apitoken" ] || [ -z "$cPanel_Hostname" ]; then
+    cPanel_Username=""
+    cPanel_Apitoken=""
+    cPanel_Hostname=""
+    _err "You haven't specified cPanel username, apitoken and hostname yet."
+    _err "Please add credentials and try again."
+    return 1
+  fi
+  #save the credentials to the account conf file.
+  _saveaccountconf_mutable cPanel_Username "$cPanel_Username"
+  _saveaccountconf_mutable cPanel_Apitoken "$cPanel_Apitoken"
+  _saveaccountconf_mutable cPanel_Hostname "$cPanel_Hostname"
+  return 0
+}
+
+_cpanel_login() {
+  if ! _checkcredentials; then return 1; fi
+
+  if ! _myget "json-api/cpanel?cpanel_jsonapi_apiversion=2&cpanel_jsonapi_module=CustInfo&cpanel_jsonapi_func=displaycontactinfo"; then
+    _err "cPanel login failed for user $cPanel_Username."
+    return 1
+  fi
+  return 0
+}
+
+_myget() {
+  #Adds auth header to request
+  export _H1="Authorization: cpanel $cPanel_Username:$cPanel_Apitoken"
+  _result=$(_get "$cPanel_Hostname/$1")
+}
+
+_get_root() {
+  _myget 'json-api/cpanel?cpanel_jsonapi_apiversion=2&cpanel_jsonapi_module=ZoneEdit&cpanel_jsonapi_func=fetchzones'
+  _domains=$(echo "$_result" | sed 's/.*\(zones.*\[\).*/\1/' | cut -d':' -f2 | sed 's/"//g' | sed 's/{//g')
+  _debug "_result is: $_result"
+  _debug "_domains is: $_domains"
+  if [ -z "$_domains" ]; then
+    _err "Primary domain list not found!"
+    return 1
+  fi
+  for _domain in $_domains; do
+    _debug "Checking if $fulldomain ends with $_domain"
+    if (_endswith "$fulldomain" "$_domain"); then
+      _debug "Root domain: $_domain"
+      return 0
+    fi
+  done
+  return 1
+}
+
+_successful_update() {
+  if (echo "$_result" | grep -q 'newserial'); then return 0; fi
+  return 1
+}
+
+_findentry() {
+  _debug "In _findentry"
+  #returns id of dns entry, if it exists
+  _myget "json-api/cpanel?cpanel_jsonapi_apiversion=2&cpanel_jsonapi_module=ZoneEdit&cpanel_jsonapi_func=fetchzone_records&domain=$_domain"
+  _id=$(echo "$_result" | sed "s/.*\(line.*$fulldomain.*$txtvalue\).*/\1/" | cut -d ':' -f 2 | cut -d ',' -f 1)
+  _debug "_result is: $_result"
+  _debug "fulldomain. is $fulldomain."
+  _debug "txtvalue is $txtvalue"
+  _debug "_id is: $_id"
+  if [ -n "$_id" ]; then
+    _debug "Entry found with _id=$_id"
+    return 0
+  fi
+  return 1
+}

+ 4 - 1
dnsapi/dns_gcloud.sh

@@ -163,5 +163,8 @@ _dns_gcloud_get_rrdatas() {
     return 1
   fi
   ttl=$(echo "$rrdatas" | cut -f1)
-  rrdatas=$(echo "$rrdatas" | cut -f2 | sed 's/","/"\n"/g')
+  # starting with version 353.0.0 gcloud seems to
+  # separate records with a semicolon instead of commas
+  # see also https://cloud.google.com/sdk/docs/release-notes#35300_2021-08-17
+  rrdatas=$(echo "$rrdatas" | cut -f2 | sed 's/"[,;]"/"\n"/g')
 }

+ 1 - 1
dnsapi/dns_he.sh

@@ -85,7 +85,7 @@ dns_he_rm() {
     _debug "The txt record is not found, just skip"
     return 0
   fi
-  _record_id="$(echo "$response" | tr -d "#" | sed "s/<tr/#<tr/g" | tr -d "\n" | tr "#" "\n" | grep "$_full_domain" | grep '"dns_tr"' | grep "$_txt_value" | cut -d '"' -f 4)"
+  _record_id="$(echo "$response" | tr -d "#" | sed "s/<tr/#<tr/g" | tr -d "\n" | tr "#" "\n" | grep "$_full_domain" | grep '"dns_tr"' | grep -- "$_txt_value" | cut -d '"' -f 4)"
   _debug2 _record_id "$_record_id"
   if [ -z "$_record_id" ]; then
     _err "Can not find record id"

+ 4 - 4
dnsapi/dns_netcup.sh

@@ -119,16 +119,16 @@ login() {
   tmp=$(_post "{\"action\": \"login\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apipassword\": \"$NC_Apipw\", \"customernumber\": \"$NC_CID\"}}" "$end" "" "POST")
   sid=$(echo "$tmp" | tr '{}' '\n' | grep apisessionid | cut -d '"' -f 4)
   _debug "$tmp"
-  if [ "$(_getfield "$msg" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
-    _err "$msg"
+  if [ "$(_getfield "$tmp" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
+    _err "$tmp"
     return 1
   fi
 }
 logout() {
   tmp=$(_post "{\"action\": \"logout\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apisessionid\": \"$sid\", \"customernumber\": \"$NC_CID\"}}" "$end" "" "POST")
   _debug "$tmp"
-  if [ "$(_getfield "$msg" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
-    _err "$msg"
+  if [ "$(_getfield "$tmp" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
+    _err "$tmp"
     return 1
   fi
 }

+ 3 - 2
dnsapi/dns_rackspace.sh

@@ -7,6 +7,7 @@
 
 RACKSPACE_Endpoint="https://dns.api.rackspacecloud.com/v1.0"
 
+# 20210923 - RS changed the fields in the API response; fix sed
 # 20190213 - The name & id fields swapped in the API response; fix sed
 # 20190101 - Duplicating file for new pull request to dev branch
 # Original - tcocca:rackspace_dnsapi https://github.com/acmesh-official/acme.sh/pull/1297
@@ -79,8 +80,8 @@ _get_root_zone() {
     _debug2 response "$response"
     if _contains "$response" "\"name\":\"$h\"" >/dev/null; then
       # Response looks like:
-      #   {"ttl":300,"accountId":12345,"id":1111111,"name":"example.com","emailAddress": ...<and so on>
-      _domain_id=$(echo "$response" | sed -n "s/^.*\"id\":\([^,]*\),\"name\":\"$h\",.*/\1/p")
+      #   {"id":"12345","accountId":"1111111","name": "example.com","ttl":3600,"emailAddress": ... <and so on>
+      _domain_id=$(echo "$response" | sed -n "s/^.*\"id\":\"\([^,]*\)\",\"accountId\":\"[0-9]*\",\"name\":\"$h\",.*/\1/p")
       _debug2 domain_id "$_domain_id"
       if [ -n "$_domain_id" ]; then
         _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)

+ 158 - 0
dnsapi/dns_veesp.sh

@@ -0,0 +1,158 @@
+#!/usr/bin/env sh
+
+# bug reports to [email protected]
+
+#
+#     export VEESP_User="username"
+#     export VEESP_Password="password"
+
+VEESP_Api="https://secure.veesp.com/api"
+
+########  Public functions #####################
+
+#Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_veesp_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  VEESP_Password="${VEESP_Password:-$(_readaccountconf_mutable VEESP_Password)}"
+  VEESP_User="${VEESP_User:-$(_readaccountconf_mutable VEESP_User)}"
+  VEESP_auth=$(printf "%s" "$VEESP_User:$VEESP_Password" | _base64)
+
+  if [ -z "$VEESP_Password" ] || [ -z "$VEESP_User" ]; then
+    VEESP_Password=""
+    VEESP_User=""
+    _err "You don't specify veesp api key and email yet."
+    _err "Please create you key and try again."
+    return 1
+  fi
+
+  #save the api key and email to the account conf file.
+  _saveaccountconf_mutable VEESP_Password "$VEESP_Password"
+  _saveaccountconf_mutable VEESP_User "$VEESP_User"
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+  _debug _domain_id "$_domain_id"
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  _info "Adding record"
+  if VEESP_rest POST "service/$_service_id/dns/$_domain_id/records" "{\"name\":\"$fulldomain\",\"ttl\":1,\"priority\":0,\"type\":\"TXT\",\"content\":\"$txtvalue\"}"; then
+    if _contains "$response" "\"success\":true"; then
+      _info "Added"
+      #todo: check if the record takes effect
+      return 0
+    else
+      _err "Add txt record error."
+      return 1
+    fi
+  fi
+}
+
+# Usage: fulldomain txtvalue
+# Used to remove the txt record after validation
+dns_veesp_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  VEESP_Password="${VEESP_Password:-$(_readaccountconf_mutable VEESP_Password)}"
+  VEESP_User="${VEESP_User:-$(_readaccountconf_mutable VEESP_User)}"
+  VEESP_auth=$(printf "%s" "$VEESP_User:$VEESP_Password" | _base64)
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+  _debug _domain_id "$_domain_id"
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  _debug "Getting txt records"
+  VEESP_rest GET "service/$_service_id/dns/$_domain_id"
+
+  count=$(printf "%s\n" "$response" | _egrep_o "\"type\":\"TXT\",\"content\":\".\"$txtvalue.\"\"" | wc -l | tr -d " ")
+  _debug count "$count"
+  if [ "$count" = "0" ]; then
+    _info "Don't need to remove."
+  else
+    record_id=$(printf "%s\n" "$response" | _egrep_o "{\"id\":[^}]*\"type\":\"TXT\",\"content\":\".\"$txtvalue.\"\"" | cut -d\" -f4)
+    _debug "record_id" "$record_id"
+    if [ -z "$record_id" ]; then
+      _err "Can not get record id to remove."
+      return 1
+    fi
+    if ! VEESP_rest DELETE "service/$_service_id/dns/$_domain_id/records/$record_id"; then
+      _err "Delete record error."
+      return 1
+    fi
+    _contains "$response" "\"success\":true"
+  fi
+}
+
+####################  Private functions below ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+# _domain_id=sdjkglgdfewsdfg
+_get_root() {
+  domain=$1
+  i=2
+  p=1
+  if ! VEESP_rest GET "dns"; then
+    return 1
+  fi
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    _debug h "$h"
+    if [ -z "$h" ]; then
+      #not valid
+      return 1
+    fi
+
+    if _contains "$response" "\"name\":\"$h\""; then
+      _domain_id=$(printf "%s\n" "$response" | _egrep_o "\"domain_id\":[^,]*,\"name\":\"$h\"" | cut -d : -f 2 | cut -d , -f 1 | cut -d '"' -f 2)
+      _debug _domain_id "$_domain_id"
+      _service_id=$(printf "%s\n" "$response" | _egrep_o "\"name\":\"$h\",\"service_id\":[^}]*" | cut -d : -f 3 | cut -d '"' -f 2)
+      _debug _service_id "$_service_id"
+      if [ "$_domain_id" ]; then
+        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+        _domain="$h"
+        return 0
+      fi
+      return 1
+    fi
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+  return 1
+}
+
+VEESP_rest() {
+  m=$1
+  ep="$2"
+  data="$3"
+  _debug "$ep"
+
+  export _H1="Accept: application/json"
+  export _H2="Authorization: Basic $VEESP_auth"
+  if [ "$m" != "GET" ]; then
+    _debug data "$data"
+    export _H3="Content-Type: application/json"
+    response="$(_post "$data" "$VEESP_Api/$ep" "" "$m")"
+  else
+    response="$(_get "$VEESP_Api/$ep")"
+  fi
+
+  if [ "$?" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}

+ 4 - 10
dnsapi/dns_world4you.sh

@@ -36,7 +36,6 @@ dns_world4you_add() {
   export _H1="Cookie: W4YSESSID=$sessid"
   form=$(_get "$WORLD4YOU_API/$paketnr/dns")
   formiddp=$(echo "$form" | grep 'AddDnsRecordForm\[uniqueFormIdDP\]' | sed 's/^.*name="AddDnsRecordForm\[uniqueFormIdDP\]" value="\([^"]*\)".*$/\1/')
-  formidttl=$(echo "$form" | grep 'AddDnsRecordForm\[uniqueFormIdTTL\]' | sed 's/^.*name="AddDnsRecordForm\[uniqueFormIdTTL\]" value="\([^"]*\)".*$/\1/')
   form_token=$(echo "$form" | grep 'AddDnsRecordForm\[_token\]' | sed 's/^.*name="AddDnsRecordForm\[_token\]" value="\([^"]*\)".*$/\1/')
   if [ -z "$formiddp" ]; then
     _err "Unable to parse form"
@@ -45,9 +44,7 @@ dns_world4you_add() {
 
   _resethttp
   export ACME_HTTP_NO_REDIRECTS=1
-  body="AddDnsRecordForm[name]=$RECORD&AddDnsRecordForm[dnsType][type]=TXT&\
-AddDnsRecordForm[value]=$value&AddDnsRecordForm[aktivPaket]=$paketnr&AddDnsRecordForm[uniqueFormIdDP]=$formiddp&\
-AddDnsRecordForm[uniqueFormIdTTL]=$formidttl&AddDnsRecordForm[_token]=$form_token"
+  body="AddDnsRecordForm[name]=$RECORD&AddDnsRecordForm[dnsType][type]=TXT&AddDnsRecordForm[value]=$value&AddDnsRecordForm[uniqueFormIdDP]=$formiddp&AddDnsRecordForm[_token]=$form_token"
   _info "Adding record..."
   ret=$(_post "$body" "$WORLD4YOU_API/$paketnr/dns" '' POST 'application/x-www-form-urlencoded')
   _resethttp
@@ -101,7 +98,6 @@ dns_world4you_rm() {
 
   form=$(_get "$WORLD4YOU_API/$paketnr/dns")
   formiddp=$(echo "$form" | grep 'DeleteDnsRecordForm\[uniqueFormIdDP\]' | sed 's/^.*name="DeleteDnsRecordForm\[uniqueFormIdDP\]" value="\([^"]*\)".*$/\1/')
-  formidttl=$(echo "$form" | grep 'DeleteDnsRecordForm\[uniqueFormIdTTL\]' | sed 's/^.*name="DeleteDnsRecordForm\[uniqueFormIdTTL\]" value="\([^"]*\)".*$/\1/')
   form_token=$(echo "$form" | grep 'DeleteDnsRecordForm\[_token\]' | sed 's/^.*name="DeleteDnsRecordForm\[_token\]" value="\([^"]*\)".*$/\1/')
   if [ -z "$formiddp" ]; then
     _err "Unable to parse form"
@@ -113,11 +109,9 @@ dns_world4you_rm() {
 
   _resethttp
   export ACME_HTTP_NO_REDIRECTS=1
-  body="DeleteDnsRecordForm[recordId]=$recordid&DeleteDnsRecordForm[aktivPaket]=$paketnr&\
-DeleteDnsRecordForm[uniqueFormIdDP]=$formiddp&DeleteDnsRecordForm[uniqueFormIdTTL]=$formidttl&\
-DeleteDnsRecordForm[_token]=$form_token"
+  body="DeleteDnsRecordForm[recordId]=$recordid&DeleteDnsRecordForm[uniqueFormIdDP]=$formiddp&DeleteDnsRecordForm[_token]=$form_token"
   _info "Removing record..."
-  ret=$(_post "$body" "$WORLD4YOU_API/$paketnr/deleteRecord" '' POST 'application/x-www-form-urlencoded')
+  ret=$(_post "$body" "$WORLD4YOU_API/$paketnr/dns/record/delete" '' POST 'application/x-www-form-urlencoded')
   _resethttp
 
   if _contains "$(_head_n 3 <"$HTTP_HEADER")" '302'; then
@@ -190,7 +184,7 @@ _get_paketnr() {
   fqdn="$1"
   form="$2"
 
-  domains=$(echo "$form" | grep '^ *[A-Za-z0-9_\.-]*\.[A-Za-z0-9_-]*$' | sed 's/^\s*\(\S*\)$/\1/')
+  domains=$(echo "$form" | grep '^ *[A-Za-z0-9_\.-]*\.[A-Za-z0-9_-]*$' | sed 's/^ *\(.*\)$/\1/')
   domain=''
   for domain in $domains; do
     if _contains "$fqdn" "$domain\$"; then

+ 51 - 0
notify/bark.sh

@@ -0,0 +1,51 @@
+#!/usr/bin/env sh
+
+#Support iOS Bark Notification
+
+#BARK_API_URL="https://api.day.app/xxxx"
+#BARK_SOUND="yyyy"
+#BARK_GROUP="zzzz"
+
+# subject  content statusCode
+bark_send() {
+  _subject="$1"
+  _content="$2"
+  _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped
+  _debug "_subject" "$_subject"
+  _debug "_content" "$_content"
+  _debug "_statusCode" "$_statusCode"
+
+  BARK_API_URL="${BARK_API_URL:-$(_readaccountconf_mutable BARK_API_URL)}"
+  if [ -z "$BARK_API_URL" ]; then
+    BARK_API_URL=""
+    _err "You didn't specify a Bark API URL BARK_API_URL yet."
+    _err "You can download Bark from App Store and get yours."
+    return 1
+  fi
+  _saveaccountconf_mutable BARK_API_URL "$BARK_API_URL"
+
+  BARK_SOUND="${BARK_SOUND:-$(_readaccountconf_mutable BARK_SOUND)}"
+  _saveaccountconf_mutable BARK_SOUND "$BARK_SOUND"
+
+  BARK_GROUP="${BARK_GROUP:-$(_readaccountconf_mutable BARK_GROUP)}"
+  if [ -z "$BARK_GROUP" ]; then
+    BARK_GROUP="ACME"
+    _info "The BARK_GROUP is not set, so use the default ACME as group name."
+  else
+    _saveaccountconf_mutable BARK_GROUP "$BARK_GROUP"
+  fi
+
+  _content=$(echo "$_content" | _url_encode)
+  _subject=$(echo "$_subject" | _url_encode)
+
+  response="$(_get "$BARK_API_URL/$_subject/$_content?sound=$BARK_SOUND&group=$BARK_GROUP")"
+
+  if [ "$?" = "0" ] && _contains "$response" "success"; then
+    _info "Bark API fired success."
+    return 0
+  fi
+
+  _err "Bark API fired error."
+  _err "$response"
+  return 1
+}

+ 48 - 0
notify/feishu.sh

@@ -0,0 +1,48 @@
+#!/usr/bin/env sh
+
+#Support feishu webhooks api
+
+#required
+#FEISHU_WEBHOOK="xxxx"
+
+#optional
+#FEISHU_KEYWORD="yyyy"
+
+# subject content statusCode
+feishu_send() {
+  _subject="$1"
+  _content="$2"
+  _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped
+  _debug "_subject" "$_subject"
+  _debug "_content" "$_content"
+  _debug "_statusCode" "$_statusCode"
+
+  FEISHU_WEBHOOK="${FEISHU_WEBHOOK:-$(_readaccountconf_mutable FEISHU_WEBHOOK)}"
+  if [ -z "$FEISHU_WEBHOOK" ]; then
+    FEISHU_WEBHOOK=""
+    _err "You didn't specify a feishu webhooks FEISHU_WEBHOOK yet."
+    _err "You can get yours from https://www.feishu.cn"
+    return 1
+  fi
+  _saveaccountconf_mutable FEISHU_WEBHOOK "$FEISHU_WEBHOOK"
+
+  FEISHU_KEYWORD="${FEISHU_KEYWORD:-$(_readaccountconf_mutable FEISHU_KEYWORD)}"
+  if [ "$FEISHU_KEYWORD" ]; then
+    _saveaccountconf_mutable FEISHU_KEYWORD "$FEISHU_KEYWORD"
+  fi
+
+  _content=$(echo "$_content" | _json_encode)
+  _subject=$(echo "$_subject" | _json_encode)
+  _data="{\"msg_type\": \"text\", \"content\": {\"text\": \"[$FEISHU_KEYWORD]\n$_subject\n$_content\"}}"
+
+  response="$(_post "$_data" "$FEISHU_WEBHOOK" "" "POST" "application/json")"
+
+  if [ "$?" = "0" ] && _contains "$response" "StatusCode\":0"; then
+    _info "feishu webhooks event fired success."
+    return 0
+  fi
+
+  _err "feishu webhooks event fired error."
+  _err "$response"
+  return 1
+}

+ 2 - 1
notify/mail.sh

@@ -62,7 +62,7 @@ mail_send() {
   fi
 
   contenttype="text/plain; charset=utf-8"
-  subject="=?UTF-8?B?$(echo "$_subject" | _base64)?="
+  subject="=?UTF-8?B?$(printf -- "%b" "$_subject" | _base64)?="
   result=$({ _mail_body | eval "$(_mail_cmnd)"; } 2>&1)
 
   # shellcheck disable=SC2181
@@ -131,6 +131,7 @@ _mail_body() {
     echo "To: $MAIL_TO"
     echo "Subject: $subject"
     echo "Content-Type: $contenttype"
+    echo "MIME-Version: 1.0"
     echo
     ;;
   esac

+ 44 - 0
notify/pushbullet.sh

@@ -0,0 +1,44 @@
+#!/usr/bin/env sh
+
+#Support for pushbullet.com's api. Push notification, notification sync and message platform for multiple platforms
+#PUSHBULLET_TOKEN="" Required, pushbullet application token
+#PUSHBULLET_DEVICE="" Optional, Specific device, ignore to send to all devices
+
+PUSHBULLET_URI="https://api.pushbullet.com/v2/pushes"
+pushbullet_send() {
+  _subject="$1"
+  _content="$2"
+  _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped
+  _debug "_statusCode" "$_statusCode"
+
+  PUSHBULLET_TOKEN="${PUSHBULLET_TOKEN:-$(_readaccountconf_mutable PUSHBULLET_TOKEN)}"
+  if [ -z "$PUSHBULLET_TOKEN" ]; then
+    PUSHBULLET_TOKEN=""
+    _err "You didn't specify a Pushbullet application token yet."
+    return 1
+  fi
+  _saveaccountconf_mutable PUSHBULLET_TOKEN "$PUSHBULLET_TOKEN"
+
+  PUSHBULLET_DEVICE="${PUSHBULLET_DEVICE:-$(_readaccountconf_mutable PUSHBULLET_DEVICE)}"
+  if [ -z "$PUSHBULLET_DEVICE" ]; then
+    _clearaccountconf_mutable PUSHBULLET_DEVICE
+  else
+    _saveaccountconf_mutable PUSHBULLET_DEVICE "$PUSHBULLET_DEVICE"
+  fi
+
+  export _H1="Content-Type: application/json"
+  export _H2="Access-Token: ${PUSHBULLET_TOKEN}"
+  _content="$(printf "*%s*\n" "$_content" | _json_encode)"
+  _subject="$(printf "*%s*\n" "$_subject" | _json_encode)"
+  _data="{\"type\": \"note\",\"title\": \"${_subject}\",\"body\": \"${_content}\",\"device_iden\": \"${PUSHBULLET_DEVICE}\"}"
+  response="$(_post "$_data" "$PUSHBULLET_URI")"
+
+  if [ "$?" != "0" ] || _contains "$response" "\"error_code\""; then
+    _err "PUSHBULLET send error."
+    _err "$response"
+    return 1
+  fi
+
+  _info "PUSHBULLET send success."
+  return 0
+}