Преглед изворни кода

Merge branch 'dev' into feature/ssh_scp

PM Extra пре 3 година
родитељ
комит
ed58f32052

+ 1 - 1
.github/FUNDING.yml

@@ -3,7 +3,7 @@
 github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
 patreon: # Replace with a single Patreon username
 open_collective: acmesh
-ko_fi: # Replace with a single Ko-fi username
+ko_fi: neilpang
 tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
 community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
 liberapay: # Replace with a single Liberapay username

+ 5 - 5
.github/workflows/FreeBSD.yml

@@ -25,11 +25,11 @@ jobs:
            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: ""
+         #- 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-10.15
     env:
       TEST_LOCAL: 1

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

@@ -25,6 +25,7 @@ jobs:
     env:
       TEST_LOCAL: 1
       TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1
+      TEST_ACME_Server: "LetsEncrypt.org_test"
     steps:
     - uses: actions/checkout@v2
     - name: Clone acmetest

+ 5 - 5
.github/workflows/MacOS.yml

@@ -25,11 +25,11 @@ jobs:
            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: ""
+         #- 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

+ 5 - 5
.github/workflows/Solaris.yml

@@ -25,11 +25,11 @@ jobs:
            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: ""
+         #- 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-10.15
     env:
       TEST_LOCAL: 1

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

@@ -25,11 +25,11 @@ jobs:
            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: ""
+         #- 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 }}

+ 2 - 1
Dockerfile

@@ -65,7 +65,8 @@ RUN for verb in help \
 RUN printf "%b" '#!'"/usr/bin/env sh\n \
 if [ \"\$1\" = \"daemon\" ];  then \n \
  trap \"echo stop && killall crond && exit 0\" SIGTERM SIGINT \n \
- crond && while true; do sleep 1; done;\n \
+ crond && sleep infinity &\n \
+ wait \n \
 else \n \
  exec -- \"\$@\"\n \
 fi" >/entry.sh && chmod +x /entry.sh

+ 1 - 0
README.md

@@ -95,6 +95,7 @@ https://github.com/acmesh-official/acmetest
 - Letsencrypt.org CA
 - [BuyPass.com CA](https://github.com/acmesh-official/acme.sh/wiki/BuyPass.com-CA)
 - [SSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/SSL.com-CA)
+- [Google.com Public CA](https://github.com/acmesh-official/acme.sh/wiki/Google-Public-CA)
 - [Pebble strict Mode](https://github.com/letsencrypt/pebble)
 - Any other [RFC8555](https://tools.ietf.org/html/rfc8555)-compliant CA
 

+ 249 - 63
acme.sh

@@ -1,6 +1,6 @@
 #!/usr/bin/env sh
 
-VER=3.0.2
+VER=3.0.5
 
 PROJECT_NAME="acme.sh"
 
@@ -20,8 +20,6 @@ _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"
 
@@ -34,6 +32,9 @@ _ZERO_EAB_ENDPOINT="https://api.zerossl.com/acme/eab-credentials-email"
 CA_SSLCOM_RSA="https://acme.ssl.com/sslcom-dv-rsa"
 CA_SSLCOM_ECC="https://acme.ssl.com/sslcom-dv-ecc"
 
+CA_GOOGLE="https://dv.acme-v02.api.pki.goog/directory"
+CA_GOOGLE_TEST="https://dv.acme-v02.test-api.pki.goog/directory"
+
 DEFAULT_CA=$CA_ZEROSSL
 DEFAULT_STAGING_CA=$CA_LETSENCRYPT_V2_TEST
 
@@ -44,9 +45,11 @@ LetsEncrypt.org_test,letsencrypt_test,letsencrypttest
 BuyPass.com,buypass
 BuyPass.com_test,buypass_test,buypasstest
 SSL.com,sslcom
+Google.com,google
+Google.com_test,googletest,google_test
 "
 
-CA_SERVERS="$CA_ZEROSSL,$CA_LETSENCRYPT_V2,$CA_LETSENCRYPT_V2_TEST,$CA_BUYPASS,$CA_BUYPASS_TEST,$CA_SSLCOM_RSA"
+CA_SERVERS="$CA_ZEROSSL,$CA_LETSENCRYPT_V2,$CA_LETSENCRYPT_V2_TEST,$CA_BUYPASS,$CA_BUYPASS_TEST,$CA_SSLCOM_RSA,$CA_GOOGLE,$CA_GOOGLE_TEST"
 
 DEFAULT_USER_AGENT="$PROJECT_NAME/$VER ($PROJECT)"
 
@@ -172,6 +175,8 @@ _SERVER_WIKI="https://github.com/acmesh-official/acme.sh/wiki/Server"
 
 _PREFERRED_CHAIN_WIKI="https://github.com/acmesh-official/acme.sh/wiki/Preferred-Chain"
 
+_VALIDITY_WIKI="https://github.com/acmesh-official/acme.sh/wiki/Validity"
+
 _DNSCHECK_WIKI="https://github.com/acmesh-official/acme.sh/wiki/dnscheck"
 
 _DNS_MANUAL_ERR="The dns manual mode can not renew automatically, you must issue it again manually. You'd better use the other modes instead."
@@ -976,9 +981,9 @@ _base64() {
 #Usage: multiline
 _dbase64() {
   if [ "$1" ]; then
-    ${ACME_OPENSSL_BIN:-openssl} base64 -d -A
-  else
     ${ACME_OPENSSL_BIN:-openssl} base64 -d
+  else
+    ${ACME_OPENSSL_BIN:-openssl} base64 -d -A
   fi
 }
 
@@ -1141,13 +1146,19 @@ _createkey() {
 
   _debug "Use length $length"
 
-  if ! touch "$f" >/dev/null 2>&1; then
-    _f_path="$(dirname "$f")"
-    _debug _f_path "$_f_path"
-    if ! mkdir -p "$_f_path"; then
-      _err "Can not create path: $_f_path"
+  if ! [ -e "$f" ]; then
+    if ! touch "$f" >/dev/null 2>&1; then
+      _f_path="$(dirname "$f")"
+      _debug _f_path "$_f_path"
+      if ! mkdir -p "$_f_path"; then
+        _err "Can not create path: $_f_path"
+        return 1
+      fi
+    fi
+    if ! touch "$f" >/dev/null 2>&1; then
       return 1
     fi
+    chmod 600 "$f"
   fi
 
   if _isEccKey "$length"; then
@@ -1495,7 +1506,6 @@ _create_account_key() {
   else
     #generate account key
     if _createkey "$length" "$ACCOUNT_KEY_PATH"; then
-      chmod 600 "$ACCOUNT_KEY_PATH"
       _info "Create account key ok."
       return 0
     else
@@ -1593,23 +1603,22 @@ _durl_replace_base64() {
 
 _time2str() {
   #BSD
-  if date -u -r "$1" 2>/dev/null; then
+  if date -u -r "$1" -j "+%Y-%m-%dT%H:%M:%SZ" 2>/dev/null; then
     return
   fi
 
   #Linux
-  if date -u -d@"$1" 2>/dev/null; then
+  if date -u --date=@"$1" "+%Y-%m-%dT%H:%M:%SZ" 2>/dev/null; then
     return
   fi
 
   #Solaris
-  if _exists adb; then
-    _t_s_a=$(echo "0t${1}=Y" | adb)
-    echo "$_t_s_a"
+  if printf "%(%Y-%m-%dT%H:%M:%SZ)T\n" $1 2>/dev/null; then
+    return
   fi
 
   #Busybox
-  if echo "$1" | awk '{ print strftime("%c", $0); }' 2>/dev/null; then
+  if echo "$1" | awk '{ print strftime("%Y-%m-%dT%H:%M:%SZ", $0); }' 2>/dev/null; then
     return
   fi
 }
@@ -1768,6 +1777,27 @@ _time() {
   date -u "+%s"
 }
 
+#support 2 formats:
+#    2022-04-01 08:10:33   to   1648800633
+#or  2022-04-01T08:10:33Z  to   1648800633
+_date2time() {
+  #Linux
+  if date -u -d "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then
+    return
+  fi
+
+  #Solaris
+  if gdate -u -d "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then
+    return
+  fi
+  #Mac/BSD
+  if date -u -j -f "%Y-%m-%d %H:%M:%S" "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then
+    return
+  fi
+  _err "Can not parse _date2time $1"
+  return 1
+}
+
 _utc_date() {
   date -u "+%Y-%m-%d %H:%M:%S"
 }
@@ -1840,7 +1870,9 @@ _inithttp() {
       _ACME_WGET="$_ACME_WGET --max-redirect 0 "
     fi
     if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then
-      _ACME_WGET="$_ACME_WGET -d "
+      if [ "$_ACME_WGET" ] && _contains "$($_ACME_WGET --help 2>&1)" "--debug"; then
+        _ACME_WGET="$_ACME_WGET -d "
+      fi
     fi
     if [ "$CA_PATH" ]; then
       _ACME_WGET="$_ACME_WGET --ca-directory=$CA_PATH "
@@ -2657,6 +2689,12 @@ _initAPI() {
   return 1
 }
 
+_clearCA() {
+  export CA_CONF=
+  export ACCOUNT_KEY_PATH=
+  export ACCOUNT_JSON_PATH=
+}
+
 #[domain]  [keylength or isEcc flag]
 _initpath() {
   domain="$1"
@@ -3746,7 +3784,7 @@ updateaccount() {
 
   _email="$(_getAccountEmail)"
 
-  if [ "$ACCOUNT_EMAIL" ]; then
+  if [ "$_email" ]; then
     updjson='{"contact": ["mailto:'$_email'"]}'
   else
     updjson='{"contact": []}'
@@ -3756,7 +3794,7 @@ updateaccount() {
 
   if [ "$code" = '200' ]; then
     echo "$response" >"$ACCOUNT_JSON_PATH"
-    _info "account update success for $_accUri."
+    _info "Account update success for $_accUri."
   else
     _info "Error. The account was not updated."
     return 1
@@ -4195,6 +4233,40 @@ _getIdType() {
   fi
 }
 
+# beginTime dateTo
+# beginTime is full string format("2022-04-01T08:10:33Z"), beginTime can be empty, to use current time
+# dateTo can be ether in full string format("2022-04-01T08:10:33Z") or in delta format(+5d or +20h)
+_convertValidaty() {
+  _beginTime="$1"
+  _dateTo="$2"
+  _debug2 "_beginTime" "$_beginTime"
+  _debug2 "_dateTo" "$_dateTo"
+
+  if _startswith "$_dateTo" "+"; then
+    _v_begin=$(_time)
+    if [ "$_beginTime" ]; then
+      _v_begin="$(_date2time "$_beginTime")"
+    fi
+    _debug2 "_v_begin" "$_v_begin"
+    if _endswith "$_dateTo" "h"; then
+      _v_end=$(_math "$_v_begin + 60 * 60 * $(echo "$_dateTo" | tr -d '+h')")
+    elif _endswith "$_dateTo" "d"; then
+      _v_end=$(_math "$_v_begin + 60 * 60 * 24 * $(echo "$_dateTo" | tr -d '+d')")
+    else
+      _err "Not recognized format for _dateTo: $_dateTo"
+      return 1
+    fi
+    _debug2 "_v_end" "$_v_end"
+    _time2str "$_v_end"
+  else
+    if [ "$(_time)" -gt "$(_date2time "$_dateTo")" ]; then
+      _err "The validaty to is in the past: _dateTo = $_dateTo"
+      return 1
+    fi
+    echo "$_dateTo"
+  fi
+}
+
 #webroot, domain domainlist  keylength
 issue() {
   if [ -z "$2" ]; then
@@ -4228,6 +4300,8 @@ issue() {
   _local_addr="${13}"
   _challenge_alias="${14}"
   _preferred_chain="${15}"
+  _valid_from="${16}"
+  _valid_to="${17}"
 
   if [ -z "$_ACME_IS_RENEW" ]; then
     _initpath "$_main_domain" "$_key_length"
@@ -4247,11 +4321,24 @@ issue() {
     Le_NextRenewTime=$(_readdomainconf Le_NextRenewTime)
     _debug Le_NextRenewTime "$Le_NextRenewTime"
     if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then
+      _valid_to_saved=$(_readdomainconf Le_Valid_to)
+      if [ "$_valid_to_saved" ] && ! _startswith "$_valid_to_saved" "+"; then
+        _info "The domain is set to be valid to: $_valid_to_saved"
+        _info "It can not be renewed automatically"
+        _info "See: $_VALIDITY_WIKI"
+        return $RENEW_SKIP
+      fi
       _saved_domain=$(_readdomainconf Le_Domain)
       _debug _saved_domain "$_saved_domain"
       _saved_alt=$(_readdomainconf Le_Alt)
       _debug _saved_alt "$_saved_alt"
-      if [ "$_saved_domain,$_saved_alt" = "$_main_domain,$_alt_domains" ]; then
+      _normized_saved_domains="$(echo "$_saved_domain,$_saved_alt" | tr "," "\n" | sort | tr '\n' ',')"
+      _debug _normized_saved_domains "$_normized_saved_domains"
+
+      _normized_domains="$(echo "$_main_domain,$_alt_domains" | tr "," "\n" | sort | tr '\n' ',')"
+      _debug _normized_domains "$_normized_domains"
+
+      if [ "$_normized_saved_domains" = "$_normized_domains" ]; then
         _info "Domains not changed."
         _info "Skip, Next renewal time is: $(__green "$(_readdomainconf Le_NextRenewTimeStr)")"
         _info "Add '$(__red '--force')' to force to renew."
@@ -4299,10 +4386,6 @@ issue() {
     _alt_domains=""
   fi
 
-  if [ "$_key_length" = "$NO_VALUE" ]; then
-    _key_length=""
-  fi
-
   if ! _on_before_issue "$_web_roots" "$_main_domain" "$_alt_domains" "$_pre_hook" "$_local_addr"; then
     _err "_on_before_issue."
     return 1
@@ -4323,7 +4406,13 @@ issue() {
   if [ -f "$CSR_PATH" ] && [ ! -f "$CERT_KEY_PATH" ]; then
     _info "Signing from existing CSR."
   else
+    # When renewing from an old version, the empty Le_Keylength means 2048.
+    # Note, do not use DEFAULT_DOMAIN_KEY_LENGTH as that value may change over
+    # time but an empty value implies 2048 specifically.
     _key=$(_readdomainconf Le_Keylength)
+    if [ -z "$_key" ]; then
+      _key=2048
+    fi
     _debug "Read key length:$_key"
     if [ ! -f "$CERT_KEY_PATH" ] || [ "$_key_length" != "$_key" ] || [ "$Le_ForceNewDomainKey" = "1" ]; then
       if ! createDomainKey "$_main_domain" "$_key_length"; then
@@ -4363,12 +4452,52 @@ issue() {
       _identifiers="$_identifiers,{\"type\":\"$(_getIdType "$d")\",\"value\":\"$(_idn "$d")\"}"
     done
     _debug2 _identifiers "$_identifiers"
-    if ! _send_signed_request "$ACME_NEW_ORDER" "{\"identifiers\": [$_identifiers]}"; then
+    _notBefore=""
+    _notAfter=""
+
+    if [ "$_valid_from" ]; then
+      _savedomainconf "Le_Valid_From" "$_valid_from"
+      _debug2 "_valid_from" "$_valid_from"
+      _notBefore="$(_convertValidaty "" "$_valid_from")"
+      if [ "$?" != "0" ]; then
+        _err "Can not parse _valid_from: $_valid_from"
+        return 1
+      fi
+      if [ "$(_time)" -gt "$(_date2time "$_notBefore")" ]; then
+        _notBefore=""
+      fi
+    else
+      _cleardomainconf "Le_Valid_From"
+    fi
+    _debug2 _notBefore "$_notBefore"
+
+    if [ "$_valid_to" ]; then
+      _debug2 "_valid_to" "$_valid_to"
+      _savedomainconf "Le_Valid_To" "$_valid_to"
+      _notAfter="$(_convertValidaty "$_notBefore" "$_valid_to")"
+      if [ "$?" != "0" ]; then
+        _err "Can not parse _valid_to: $_valid_to"
+        return 1
+      fi
+    else
+      _cleardomainconf "Le_Valid_To"
+    fi
+    _debug2 "_notAfter" "$_notAfter"
+
+    _newOrderObj="{\"identifiers\": [$_identifiers]"
+    if [ "$_notBefore" ]; then
+      _newOrderObj="$_newOrderObj,\"notBefore\": \"$_notBefore\""
+    fi
+    if [ "$_notAfter" ]; then
+      _newOrderObj="$_newOrderObj,\"notAfter\": \"$_notAfter\""
+    fi
+    if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then
       _err "Create new order error."
       _clearup
       _on_issue_err "$_post_hook"
       return 1
     fi
+
     Le_LinkOrder="$(echo "$responseHeaders" | grep -i '^Location.*$' | _tail_n 1 | tr -d "\r\n " | cut -d ":" -f 2-)"
     _debug Le_LinkOrder "$Le_LinkOrder"
     Le_OrderFinalize="$(echo "$response" | _egrep_o '"finalize" *: *"[^"]*"' | cut -d '"' -f 4)"
@@ -4407,7 +4536,7 @@ issue() {
 
       response="$(echo "$response" | _normalizeJson)"
       _debug2 response "$response"
-      _d="$(echo "$response" | _egrep_o '"value" *: *"[^"]*"' | cut -d : -f 2 | tr -d ' "')"
+      _d="$(echo "$response" | _egrep_o '"value" *: *"[^"]*"' | cut -d : -f 2- | tr -d ' "')"
       if _contains "$response" "\"wildcard\" *: *true"; then
         _d="*.$_d"
       fi
@@ -4557,6 +4686,7 @@ $_authorizations_map"
           _dns_root_d="$(echo "$_dns_root_d" | sed 's/*.//')"
         fi
         _d_alias="$(_getfield "$_challenge_alias" "$_alias_index")"
+        test "$_d_alias" = "$NO_VALUE" && _d_alias=""
         _alias_index="$(_math "$_alias_index" + 1)"
         _debug "_d_alias" "$_d_alias"
         if [ "$_d_alias" ]; then
@@ -4851,7 +4981,7 @@ $_authorizations_map"
         return 1
       fi
       _debug "sleep 2 secs to verify again"
-      sleep 2
+      _sleep 2
       _debug "checking"
 
       _send_signed_request "$uri"
@@ -5028,7 +5158,7 @@ $_authorizations_map"
   Le_CertCreateTime=$(_time)
   _savedomainconf "Le_CertCreateTime" "$Le_CertCreateTime"
 
-  Le_CertCreateTimeStr=$(date -u)
+  Le_CertCreateTimeStr=$(_time2str "$Le_CertCreateTime")
   _savedomainconf "Le_CertCreateTimeStr" "$Le_CertCreateTimeStr"
 
   if [ -z "$Le_RenewalDays" ] || [ "$Le_RenewalDays" -lt "0" ]; then
@@ -5068,13 +5198,20 @@ $_authorizations_map"
   else
     _cleardomainconf Le_ForceNewDomainKey
   fi
-
-  Le_NextRenewTime=$(_math "$Le_CertCreateTime" + "$Le_RenewalDays" \* 24 \* 60 \* 60)
-
-  Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime")
+  if [ "$_notAfter" ]; then
+    Le_NextRenewTime=$(_date2time "$_notAfter")
+    Le_NextRenewTimeStr="$_notAfter"
+    if [ "$_valid_to" ] && ! _startswith "$_valid_to" "+"; then
+      _info "The domain is set to be valid to: $_valid_to"
+      _info "It can not be renewed automatically"
+      _info "See: $_VALIDITY_WIKI"
+    fi
+  else
+    Le_NextRenewTime=$(_math "$Le_CertCreateTime" + "$Le_RenewalDays" \* 24 \* 60 \* 60)
+    Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime")
+    Le_NextRenewTime=$(_math "$Le_NextRenewTime" - 86400)
+  fi
   _savedomainconf "Le_NextRenewTimeStr" "$Le_NextRenewTimeStr"
-
-  Le_NextRenewTime=$(_math "$Le_NextRenewTime" - 86400)
   _savedomainconf "Le_NextRenewTime" "$Le_NextRenewTime"
 
   if [ "$_real_cert$_real_key$_real_ca$_reload_cmd$_real_fullchain" ]; then
@@ -5110,17 +5247,20 @@ _split_cert_chain() {
   fi
 }
 
-#domain  [isEcc]
+#domain  [isEcc] [server]
 renew() {
   Le_Domain="$1"
   if [ -z "$Le_Domain" ]; then
-    _usage "Usage: $PROJECT_ENTRY --renew --domain <domain.tld> [--ecc]"
+    _usage "Usage: $PROJECT_ENTRY --renew --domain <domain.tld> [--ecc] [--server server]"
     return 1
   fi
 
   _isEcc="$2"
+  _renewServer="$3"
+  _debug "_renewServer" "$_renewServer"
 
   _initpath "$Le_Domain" "$_isEcc"
+
   _set_level=${NOTIFY_LEVEL:-$NOTIFY_LEVEL_DEFAULT}
   _info "$(__green "Renew: '$Le_Domain'")"
   if [ ! -f "$DOMAIN_CONF" ]; then
@@ -5134,24 +5274,34 @@ renew() {
 
   . "$DOMAIN_CONF"
   _debug Le_API "$Le_API"
-  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
+
+  case "$Le_API" in
+  "$CA_LETSENCRYPT_V2_TEST")
+    _info "Switching back to $CA_LETSENCRYPT_V2"
     Le_API="$CA_LETSENCRYPT_V2"
-  fi
+    ;;
+  "$CA_BUYPASS_TEST")
+    _info "Switching back to $CA_BUYPASS"
+    Le_API="$CA_BUYPASS"
+    ;;
+  "$CA_GOOGLE_TEST")
+    _info "Switching back to $CA_GOOGLE"
+    Le_API="$CA_GOOGLE"
+    ;;
+  esac
 
-  if [ "$Le_API" ]; then
-    if [ "$Le_API" != "$ACME_DIRECTORY" ]; then
-      _clearAPI
-    fi
-    export ACME_DIRECTORY="$Le_API"
-    #reload ca configs
-    ACCOUNT_KEY_PATH=""
-    ACCOUNT_JSON_PATH=""
-    CA_CONF=""
-    _debug3 "initpath again."
-    _initpath "$Le_Domain" "$_isEcc"
+  if [ "$_server" ]; then
+    Le_API="$_server"
   fi
+  _info "Renew to Le_API=$Le_API"
+
+  _clearAPI
+  _clearCA
+  export ACME_DIRECTORY="$Le_API"
+
+  #reload ca configs
+  _debug2 "initpath again."
+  _initpath "$Le_Domain" "$_isEcc"
 
   if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then
     _info "Skip, Next renewal time is: $(__green "$Le_NextRenewTimeStr")"
@@ -5175,7 +5325,14 @@ renew() {
   Le_PostHook="$(_readdomainconf Le_PostHook)"
   Le_RenewHook="$(_readdomainconf Le_RenewHook)"
   Le_Preferred_Chain="$(_readdomainconf Le_Preferred_Chain)"
-  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"
+  # When renewing from an old version, the empty Le_Keylength means 2048.
+  # Note, do not use DEFAULT_DOMAIN_KEY_LENGTH as that value may change over
+  # time but an empty value implies 2048 specifically.
+  Le_Keylength="$(_readdomainconf Le_Keylength)"
+  if [ -z "$Le_Keylength" ]; then
+    Le_Keylength=2048
+  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"
   res="$?"
   if [ "$res" != "0" ]; then
     return "$res"
@@ -5202,11 +5359,16 @@ renew() {
   return "$res"
 }
 
-#renewAll  [stopRenewOnError]
+#renewAll  [stopRenewOnError] [server]
 renewAll() {
   _initpath
+  _clearCA
   _stopRenewOnError="$1"
   _debug "_stopRenewOnError" "$_stopRenewOnError"
+
+  _server="$2"
+  _debug "_server" "$_server"
+
   _ret="0"
   _success_msg=""
   _error_msg=""
@@ -5229,7 +5391,7 @@ renewAll() {
         _isEcc=$(echo "$d" | cut -d "$ECC_SEP" -f 2)
         d=$(echo "$d" | cut -d "$ECC_SEP" -f 1)
       fi
-      renew "$d" "$_isEcc"
+      renew "$d" "$_isEcc" "$_server"
     )
     rc="$?"
     _debug "Return code: $rc"
@@ -5395,10 +5557,13 @@ showcsr() {
   _initpath
 
   _csrsubj=$(_readSubjectFromCSR "$_csrfile")
-  if [ "$?" != "0" ] || [ -z "$_csrsubj" ]; then
+  if [ "$?" != "0" ]; then
     _err "Can not read subject from csr: $_csrfile"
     return 1
   fi
+  if [ -z "$_csrsubj" ]; then
+    _info "The Subject is empty"
+  fi
 
   _info "Subject=$_csrsubj"
 
@@ -5611,8 +5776,9 @@ _installcert() {
     if [ -f "$_real_key" ]; then
       cat "$CERT_KEY_PATH" >"$_real_key" || return 1
     else
-      cat "$CERT_KEY_PATH" >"$_real_key" || return 1
+      touch "$_real_key" || return 1
       chmod 600 "$_real_key"
+      cat "$CERT_KEY_PATH" >"$_real_key" || return 1
     fi
   fi
 
@@ -6610,6 +6776,11 @@ Parameters:
                                     If no match, the default offered chain will be used. (default: empty)
                                     See: $_PREFERRED_CHAIN_WIKI
 
+  --valid-to    <date-time>         Request the NotAfter field of the cert.
+                                    See: $_VALIDITY_WIKI
+  --valid-from  <date-time>         Request the NotBefore field of the cert.
+                                    See: $_VALIDITY_WIKI
+
   -f, --force                       Force install, force cert renewal or override sudo restrictions.
   --staging, --test                 Use staging server, for testing.
   --debug [0|1|2|3]                 Output debug info. Defaults to 1 if argument is omitted.
@@ -6801,6 +6972,10 @@ _processAccountConf() {
 }
 
 _checkSudo() {
+  if [ -z "__INTERACTIVE" ]; then
+    #don't check if it's not in an interactive shell
+    return 0
+  fi
   if [ "$SUDO_GID" ] && [ "$SUDO_COMMAND" ] && [ "$SUDO_USER" ] && [ "$SUDO_UID" ]; then
     if [ "$SUDO_USER" = "root" ] && [ "$SUDO_UID" = "0" ]; then
       #it's root using sudo, no matter it's using sudo or not, just fine
@@ -6922,8 +7097,8 @@ _process() {
   _altdomains="$NO_VALUE"
   _webroot=""
   _challenge_alias=""
-  _keylength=""
-  _accountkeylength=""
+  _keylength="$DEFAULT_DOMAIN_KEY_LENGTH"
+  _accountkeylength="$DEFAULT_ACCOUNT_KEY_LENGTH"
   _cert_file=""
   _key_file=""
   _ca_file=""
@@ -6970,6 +7145,8 @@ _process() {
   _eab_kid=""
   _eab_hmac_key=""
   _preferred_chain=""
+  _valid_from=""
+  _valid_to=""
   while [ ${#} -gt 0 ]; do
     case "${1}" in
 
@@ -7277,6 +7454,14 @@ _process() {
       Le_RenewalDays="$_days"
       shift
       ;;
+    --valid-from)
+      _valid_from="$2"
+      shift
+      ;;
+    --valid-to)
+      _valid_to="$2"
+      shift
+      ;;
     --httpport)
       _httpport="$2"
       Le_HTTPPort="$_httpport"
@@ -7479,6 +7664,7 @@ _process() {
 
   if [ "$_server" ]; then
     _selectServer "$_server" "${_ecc:-$_keylength}"
+    _server="$ACME_DIRECTORY"
   fi
 
   if [ "${_CMD}" != "install" ]; then
@@ -7538,7 +7724,7 @@ _process() {
   uninstall) uninstall "$_nocron" ;;
   upgrade) upgrade ;;
   issue)
-    issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain"
+    issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" "$_valid_from" "$_valid_to"
     ;;
   deploy)
     deploy "$_domain" "$_deploy_hook" "$_ecc"
@@ -7553,10 +7739,10 @@ _process() {
     installcert "$_domain" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_ecc"
     ;;
   renew)
-    renew "$_domain" "$_ecc"
+    renew "$_domain" "$_ecc" "$_server"
     ;;
   renewAll)
-    renewAll "$_stopRenewOnError"
+    renewAll "$_stopRenewOnError" "$_server"
     ;;
   revoke)
     revoke "$_domain" "$_ecc" "$_revoke_reason"

+ 16 - 12
deploy/mailcow.sh

@@ -20,18 +20,23 @@ mailcow_deploy() {
   _debug _cca "$_cca"
   _debug _cfullchain "$_cfullchain"
 
-  _mailcow_path="${DEPLOY_MAILCOW_PATH}"
+  _getdeployconf DEPLOY_MAILCOW_PATH
+  _getdeployconf DEPLOY_MAILCOW_RELOAD
 
-  if [ -z "$_mailcow_path" ]; then
+  _debug DEPLOY_MAILCOW_PATH "$DEPLOY_MAILCOW_PATH"
+  _debug DEPLOY_MAILCOW_RELOAD "$DEPLOY_MAILCOW_RELOAD"
+
+  if [ -z "$DEPLOY_MAILCOW_PATH" ]; then
     _err "Mailcow path is not found, please define DEPLOY_MAILCOW_PATH."
     return 1
   fi
 
-  #Tests if _ssl_path is the mailcow root directory.
-  if [ -f "${_mailcow_path}/generate_config.sh" ]; then
-    _ssl_path="${_mailcow_path}/data/assets/ssl/"
-  else
-    _ssl_path="${_mailcow_path}"
+  _savedeployconf DEPLOY_MAILCOW_PATH "$DEPLOY_MAILCOW_PATH"
+  [ -n "$DEPLOY_MAILCOW_RELOAD" ] && _savedeployconf DEPLOY_MAILCOW_RELOAD "$DEPLOY_MAILCOW_RELOAD"
+
+  _ssl_path="$DEPLOY_MAILCOW_PATH"
+  if [ -f "$DEPLOY_MAILCOW_PATH/generate_config.sh" ]; then
+    _ssl_path="$DEPLOY_MAILCOW_PATH/data/assets/ssl/"
   fi
 
   if [ ! -d "$_ssl_path" ]; then
@@ -40,16 +45,15 @@ mailcow_deploy() {
   fi
 
   # ECC or RSA
-  if [ -z "${Le_Keylength}" ]; then
-    Le_Keylength=""
-  fi
-  if _isEccKey "${Le_Keylength}"; then
+  length=$(_readdomainconf Le_Keylength)
+  if _isEccKey "$length"; then
     _info "ECC key type detected"
     _cert_name_prefix="ecdsa-"
   else
     _info "RSA key type detected"
     _cert_name_prefix=""
   fi
+
   _info "Copying key and cert"
   _real_key="$_ssl_path/${_cert_name_prefix}key.pem"
   if ! cat "$_ckey" >"$_real_key"; then
@@ -63,7 +67,7 @@ mailcow_deploy() {
     return 1
   fi
 
-  DEFAULT_MAILCOW_RELOAD="docker restart $(docker ps -qaf name=postfix-mailcow); docker restart $(docker ps -qaf name=nginx-mailcow); docker restart $(docker ps -qaf name=dovecot-mailcow)"
+  DEFAULT_MAILCOW_RELOAD="docker restart \$(docker ps --quiet --filter name=nginx-mailcow --filter name=dovecot-mailcow)"
   _reload="${DEPLOY_MAILCOW_RELOAD:-$DEFAULT_MAILCOW_RELOAD}"
 
   _info "Run reload: $_reload"

+ 95 - 13
deploy/routeros.sh

@@ -23,6 +23,7 @@
 # ```sh
 # export ROUTER_OS_USERNAME=certuser
 # export ROUTER_OS_HOST=router.example.com
+# export ROUTER_OS_PORT=22
 #
 # acme.sh --deploy -d ftp.example.com --deploy-hook routeros
 # ```
@@ -48,6 +49,16 @@
 # One optional thing to do as well is to create a script that updates
 # all the required services and run that script in a single command.
 #
+# To adopt parameters to `scp` and/or `ssh` set the optional
+# `ROUTER_OS_SSH_CMD` and `ROUTER_OS_SCP_CMD` variables accordingly,
+# see ssh(1) and scp(1) for parameters to those commands.
+#
+# Example:
+# ```ssh
+# export ROUTER_OS_SSH_CMD="ssh -i /acme.sh/.ssh/router.example.com -o UserKnownHostsFile=/acme.sh/.ssh/known_hosts"
+# export ROUTER_OS_SCP_CMD="scp -i /acme.sh/.ssh/router.example.com -o UserKnownHostsFile=/acme.sh/.ssh/known_hosts"
+# ````
+#
 # returns 0 means success, otherwise error.
 
 ########  Public functions #####################
@@ -59,6 +70,7 @@ routeros_deploy() {
   _ccert="$3"
   _cca="$4"
   _cfullchain="$5"
+  _err_code=0
 
   _debug _cdomain "$_cdomain"
   _debug _ckey "$_ckey"
@@ -80,6 +92,27 @@ routeros_deploy() {
     return 1
   fi
 
+  _getdeployconf ROUTER_OS_PORT
+
+  if [ -z "$ROUTER_OS_PORT" ]; then
+    _debug "Using default port 22 as ROUTER_OS_PORT, please set if not correct."
+    ROUTER_OS_PORT=22
+  fi
+
+  _getdeployconf ROUTER_OS_SSH_CMD
+
+  if [ -z "$ROUTER_OS_SSH_CMD" ]; then
+    _debug "Use default ssh setup."
+    ROUTER_OS_SSH_CMD="ssh -p $ROUTER_OS_PORT"
+  fi
+
+  _getdeployconf ROUTER_OS_SCP_CMD
+
+  if [ -z "$ROUTER_OS_SCP_CMD" ]; then
+    _debug "USe default scp setup."
+    ROUTER_OS_SCP_CMD="scp -P $ROUTER_OS_PORT"
+  fi
+
   _getdeployconf ROUTER_OS_ADDITIONAL_SERVICES
 
   if [ -z "$ROUTER_OS_ADDITIONAL_SERVICES" ]; then
@@ -89,16 +122,26 @@ routeros_deploy() {
 
   _savedeployconf ROUTER_OS_HOST "$ROUTER_OS_HOST"
   _savedeployconf ROUTER_OS_USERNAME "$ROUTER_OS_USERNAME"
+  _savedeployconf ROUTER_OS_PORT "$ROUTER_OS_PORT"
+  _savedeployconf ROUTER_OS_SSH_CMD "$ROUTER_OS_SSH_CMD"
+  _savedeployconf ROUTER_OS_SCP_CMD "$ROUTER_OS_SCP_CMD"
   _savedeployconf ROUTER_OS_ADDITIONAL_SERVICES "$ROUTER_OS_ADDITIONAL_SERVICES"
 
-  _info "Trying to push key '$_ckey' to router"
-  scp "$_ckey" "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST:$_cdomain.key"
-  _info "Trying to push cert '$_cfullchain' to router"
-  scp "$_cfullchain" "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST:$_cdomain.cer"
-  DEPLOY_SCRIPT_CMD="/system script add name=\"LE Cert Deploy - $_cdomain\" owner=admin policy=ftp,read,write,password,sensitive \
-source=\"## generated by routeros deploy script in acme.sh;\
-\n/certificate remove [ find name=$_cdomain.cer_0 ];\
+  # push key to routeros
+  if ! _scp_certificate "$_ckey" "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST:$_cdomain.key"; then
+    return $_err_code
+  fi
+
+  # push certificate chain to routeros
+  if ! _scp_certificate "$_cfullchain" "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST:$_cdomain.cer"; then
+    return $_err_code
+  fi
+
+  DEPLOY_SCRIPT_CMD="/system script add name=\"LE Cert Deploy - $_cdomain\" owner=$ROUTER_OS_USERNAME \
+comment=\"generated by routeros deploy script in acme.sh\" \
+source=\"/certificate remove [ find name=$_cdomain.cer_0 ];\
 \n/certificate remove [ find name=$_cdomain.cer_1 ];\
+\n/certificate remove [ find name=$_cdomain.cer_2 ];\
 \ndelay 1;\
 \n/certificate import file-name=$_cdomain.cer passphrase=\\\"\\\";\
 \n/certificate import file-name=$_cdomain.key passphrase=\\\"\\\";\
@@ -110,12 +153,51 @@ source=\"## generated by routeros deploy script in acme.sh;\
 \n$ROUTER_OS_ADDITIONAL_SERVICES;\
 \n\"
 "
-  # shellcheck disable=SC2029
-  ssh "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST" "$DEPLOY_SCRIPT_CMD"
-  # shellcheck disable=SC2029
-  ssh "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST" "/system script run \"LE Cert Deploy - $_cdomain\""
-  # shellcheck disable=SC2029
-  ssh "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST" "/system script remove \"LE Cert Deploy - $_cdomain\""
+
+  if ! _ssh_remote_cmd "$DEPLOY_SCRIPT_CMD"; then
+    return $_err_code
+  fi
+
+  if ! _ssh_remote_cmd "/system script run \"LE Cert Deploy - $_cdomain\""; then
+    return $_err_code
+  fi
+
+  if ! _ssh_remote_cmd "/system script remove \"LE Cert Deploy - $_cdomain\""; then
+    return $_err_code
+  fi
 
   return 0
 }
+
+# inspired by deploy/ssh.sh
+_ssh_remote_cmd() {
+  _cmd="$1"
+  _secure_debug "Remote commands to execute: $_cmd"
+  _info "Submitting sequence of commands to routeros"
+  # quotations in bash cmd below intended.  Squash travis spellcheck error
+  # shellcheck disable=SC2029
+  $ROUTER_OS_SSH_CMD "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST" "$_cmd"
+  _err_code="$?"
+
+  if [ "$_err_code" != "0" ]; then
+    _err "Error code $_err_code returned from routeros"
+  fi
+
+  return $_err_code
+}
+
+_scp_certificate() {
+  _src="$1"
+  _dst="$2"
+  _secure_debug "scp '$_src' to '$_dst'"
+  _info "Push key '$_src' to routeros"
+
+  $ROUTER_OS_SCP_CMD "$_src" "$_dst"
+  _err_code="$?"
+
+  if [ "$_err_code" != "0" ]; then
+    _err "Error code $_err_code returned from scp"
+  fi
+
+  return $_err_code
+}

+ 6 - 1
deploy/synology_dsm.sh

@@ -94,7 +94,12 @@ synology_dsm_deploy() {
 
   otp_code=""
   if [ -n "$SYNO_TOTP_SECRET" ]; then
-    otp_code="$(oathtool --base32 --totp "${SYNO_TOTP_SECRET}" 2>/dev/null)"
+    if _exists oathtool; then
+      otp_code="$(oathtool --base32 --totp "${SYNO_TOTP_SECRET}" 2>/dev/null)"
+    else
+      _err "oathtool could not be found, install oathtool to use SYNO_TOTP_SECRET"
+      return 1
+    fi
   fi
 
   if [ -n "$SYNO_DID" ]; then

+ 47 - 25
deploy/truenas.sh

@@ -38,7 +38,7 @@ truenas_deploy() {
   _getdeployconf DEPLOY_TRUENAS_APIKEY
 
   if [ -z "$DEPLOY_TRUENAS_APIKEY" ]; then
-    _err "TrueNAS Api Key is not found, please define DEPLOY_TRUENAS_APIKEY."
+    _err "TrueNAS API key not found, please set the DEPLOY_TRUENAS_APIKEY environment variable."
     return 1
   fi
   _secure_debug2 DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY"
@@ -62,15 +62,14 @@ truenas_deploy() {
 
   _info "Testing Connection TrueNAS"
   _response=$(_get "$_api_url/system/state")
-  _info "TrueNAS System State: $_response."
+  _info "TrueNAS system state: $_response."
 
   if [ -z "$_response" ]; then
     _err "Unable to authenticate to $_api_url."
-    _err 'Check your Connection and set DEPLOY_TRUENAS_HOSTNAME="192.168.178.x".'
-    _err 'or'
-    _err 'set DEPLOY_TRUENAS_HOSTNAME="<truenas_dnsname>".'
-    _err 'Check your Connection and set DEPLOY_TRUENAS_SCHEME="https".'
-    _err "Check your Api Key."
+    _err 'Check your connection settings are correct, e.g.'
+    _err 'DEPLOY_TRUENAS_HOSTNAME="192.168.x.y" or DEPLOY_TRUENAS_HOSTNAME="truenas.example.com".'
+    _err 'DEPLOY_TRUENAS_SCHEME="https" or DEPLOY_TRUENAS_SCHEME="http".'
+    _err "Verify your TrueNAS API key is valid and set correctly, e.g. DEPLOY_TRUENAS_APIKEY=xxxx...."
     return 1
   fi
 
@@ -78,7 +77,7 @@ truenas_deploy() {
   _savedeployconf DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME"
   _savedeployconf DEPLOY_TRUENAS_SCHEME "$DEPLOY_TRUENAS_SCHEME"
 
-  _info "Getting active certificate from TrueNAS"
+  _info "Getting current active certificate from TrueNAS"
   _response=$(_get "$_api_url/system/general")
   _active_cert_id=$(echo "$_response" | grep -B2 '"name":' | grep 'id' | tr -d -- '"id: ,')
   _active_cert_name=$(echo "$_response" | grep '"name":' | sed -n 's/.*: "\(.\{1,\}\)",$/\1/p')
@@ -88,14 +87,14 @@ truenas_deploy() {
   _debug Active_UI_http_redirect "$_param_httpsredirect"
 
   if [ "$DEPLOY_TRUENAS_SCHEME" = "http" ] && [ "$_param_httpsredirect" = "true" ]; then
-    _info "http Redirect active"
+    _info "HTTP->HTTPS redirection is enabled"
     _info "Setting DEPLOY_TRUENAS_SCHEME to 'https'"
     DEPLOY_TRUENAS_SCHEME="https"
     _api_url="$DEPLOY_TRUENAS_SCHEME://$DEPLOY_TRUENAS_HOSTNAME/api/v2.0"
     _savedeployconf DEPLOY_TRUENAS_SCHEME "$DEPLOY_TRUENAS_SCHEME"
   fi
 
-  _info "Upload new certifikate to TrueNAS"
+  _info "Uploading new certificate to TrueNAS"
   _certname="Letsencrypt_$(_utc_date | tr ' ' '_' | tr -d -- ':')"
   _debug3 _certname "$_certname"
 
@@ -104,30 +103,30 @@ truenas_deploy() {
 
   _debug3 _add_cert_result "$_add_cert_result"
 
-  _info "Getting Certificate list to get new Cert ID"
+  _info "Fetching list of installed certificates"
   _cert_list=$(_get "$_api_url/system/general/ui_certificate_choices")
   _cert_id=$(echo "$_cert_list" | grep "$_certname" | sed -n 's/.*"\([0-9]\{1,\}\)".*$/\1/p')
 
   _debug3 _cert_id "$_cert_id"
 
-  _info "Activate Certificate ID: $_cert_id"
+  _info "Current activate certificate ID: $_cert_id"
   _activateData="{\"ui_certificate\": \"${_cert_id}\"}"
   _activate_result="$(_post "$_activateData" "$_api_url/system/general" "" "PUT" "application/json")"
 
   _debug3 _activate_result "$_activate_result"
 
-  _info "Check if WebDAV certificate is the same as the WEB UI"
+  _info "Checking if WebDAV certificate is the same as the TrueNAS web UI"
   _webdav_list=$(_get "$_api_url/webdav")
   _webdav_cert_id=$(echo "$_webdav_list" | grep '"certssl":' | tr -d -- '"certsl: ,')
 
   if [ "$_webdav_cert_id" = "$_active_cert_id" ]; then
-    _info "Update the WebDAV Certificate"
+    _info "Updating the WebDAV certificate"
     _debug _webdav_cert_id "$_webdav_cert_id"
     _webdav_data="{\"certssl\": \"${_cert_id}\"}"
     _activate_webdav_cert="$(_post "$_webdav_data" "$_api_url/webdav" "" "PUT" "application/json")"
-    _webdav_new_cert_id=$(echo "$_activate_webdav_cert" | _json_decode | sed -n 's/.*: \([0-9]\{1,\}\) }$/\1/p')
+    _webdav_new_cert_id=$(echo "$_activate_webdav_cert" | _json_decode | grep '"certssl":' | sed -n 's/.*: \([0-9]\{1,\}\),\{0,1\}$/\1/p')
     if [ "$_webdav_new_cert_id" -eq "$_cert_id" ]; then
-      _info "WebDAV Certificate update successfully"
+      _info "WebDAV certificate updated successfully"
     else
       _err "Unable to set WebDAV certificate"
       _debug3 _activate_webdav_cert "$_activate_webdav_cert"
@@ -136,21 +135,21 @@ truenas_deploy() {
     fi
     _debug3 _webdav_new_cert_id "$_webdav_new_cert_id"
   else
-    _info "WebDAV certificate not set or not the same as Web UI"
+    _info "WebDAV certificate is not configured or is not the same as TrueNAS web UI"
   fi
 
-  _info "Check if FTP certificate is the same as the WEB UI"
+  _info "Checking if FTP certificate is the same as the TrueNAS web UI"
   _ftp_list=$(_get "$_api_url/ftp")
   _ftp_cert_id=$(echo "$_ftp_list" | grep '"ssltls_certificate":' | tr -d -- '"certislfa:_ ,')
 
   if [ "$_ftp_cert_id" = "$_active_cert_id" ]; then
-    _info "Update the FTP Certificate"
+    _info "Updating the FTP certificate"
     _debug _ftp_cert_id "$_ftp_cert_id"
     _ftp_data="{\"ssltls_certificate\": \"${_cert_id}\"}"
     _activate_ftp_cert="$(_post "$_ftp_data" "$_api_url/ftp" "" "PUT" "application/json")"
-    _ftp_new_cert_id=$(echo "$_activate_ftp_cert" | _json_decode | sed -n 's/.*: \([0-9]\{1,\}\) }$/\1/p')
+    _ftp_new_cert_id=$(echo "$_activate_ftp_cert" | _json_decode | grep '"ssltls_certificate":' | sed -n 's/.*: \([0-9]\{1,\}\),\{0,1\}$/\1/p')
     if [ "$_ftp_new_cert_id" -eq "$_cert_id" ]; then
-      _info "FTP Certificate update successfully"
+      _info "FTP certificate updated successfully"
     else
       _err "Unable to set FTP certificate"
       _debug3 _activate_ftp_cert "$_activate_ftp_cert"
@@ -159,22 +158,45 @@ truenas_deploy() {
     fi
     _debug3 _activate_ftp_cert "$_activate_ftp_cert"
   else
-    _info "FTP certificate not set or not the same as Web UI"
+    _info "FTP certificate is not configured or is not the same as TrueNAS web UI"
   fi
 
-  _info "Delete old Certificate"
+  _info "Checking if S3 certificate is the same as the TrueNAS web UI"
+  _s3_list=$(_get "$_api_url/s3")
+  _s3_cert_id=$(echo "$_s3_list" | grep '"certificate":' | tr -d -- '"certifa:_ ,')
+
+  if [ "$_s3_cert_id" = "$_active_cert_id" ]; then
+    _info "Updating the S3 certificate"
+    _debug _s3_cert_id "$_s3_cert_id"
+    _s3_data="{\"certificate\": \"${_cert_id}\"}"
+    _activate_s3_cert="$(_post "$_s3_data" "$_api_url/s3" "" "PUT" "application/json")"
+    _s3_new_cert_id=$(echo "$_activate_s3_cert" | _json_decode | grep '"certificate":' | sed -n 's/.*: \([0-9]\{1,\}\),\{0,1\}$/\1/p')
+    if [ "$_s3_new_cert_id" -eq "$_cert_id" ]; then
+      _info "S3 certificate updated successfully"
+    else
+      _err "Unable to set S3 certificate"
+      _debug3 _activate_s3_cert "$_activate_s3_cert"
+      _debug3 _s3_new_cert_id "$_s3_new_cert_id"
+      return 1
+    fi
+    _debug3 _activate_s3_cert "$_activate_s3_cert"
+  else
+    _info "S3 certificate is not configured or is not the same as TrueNAS web UI"
+  fi
+
+  _info "Deleting old certificate"
   _delete_result="$(_post "" "$_api_url/certificate/id/$_active_cert_id" "" "DELETE" "application/json")"
 
   _debug3 _delete_result "$_delete_result"
 
-  _info "Reload WebUI from TrueNAS"
+  _info "Reloading TrueNAS web UI"
   _restart_UI=$(_get "$_api_url/system/general/ui_restart")
   _debug2 _restart_UI "$_restart_UI"
 
   if [ -n "$_add_cert_result" ] && [ -n "$_activate_result" ]; then
     return 0
   else
-    _err "Certupdate was not succesfull, please use --debug"
+    _err "Certificate update was not succesful, please try again with --debug"
     return 1
   fi
 }

+ 7 - 7
dnsapi/dns_1984hosting.sh

@@ -42,7 +42,7 @@ dns_1984hosting_add() {
 
   _debug "Add TXT record $fulldomain with value '$txtvalue'"
   value="$(printf '%s' "$txtvalue" | _url_encode)"
-  url="https://management.1984hosting.com/domains/entry/"
+  url="https://1984.hosting/domains/entry/"
 
   postdata="entry=new"
   postdata="$postdata&type=TXT"
@@ -95,7 +95,7 @@ dns_1984hosting_rm() {
   _debug _domain "$_domain"
   _debug "Delete $fulldomain TXT record"
 
-  url="https://management.1984hosting.com/domains"
+  url="https://1984.hosting/domains"
   if ! _get_zone_id "$url" "$_domain"; then
     _err "invalid zone" "$_domain"
     return 1
@@ -138,7 +138,7 @@ _1984hosting_login() {
   _debug "Login to 1984Hosting as user $One984HOSTING_Username"
   username=$(printf '%s' "$One984HOSTING_Username" | _url_encode)
   password=$(printf '%s' "$One984HOSTING_Password" | _url_encode)
-  url="https://management.1984hosting.com/accounts/checkuserauth/"
+  url="https://1984.hosting/accounts/checkuserauth/"
 
   response="$(_post "username=$username&password=$password&otpkey=" $url)"
   response="$(echo "$response" | _normalizeJson)"
@@ -175,7 +175,7 @@ _check_cookies() {
     return 1
   fi
 
-  _authget "https://management.1984hosting.com/accounts/loginstatus/"
+  _authget "https://1984.hosting/accounts/loginstatus/"
   if _contains "$response" '"ok": true'; then
     _debug "Cached cookies still valid"
     return 0
@@ -204,7 +204,7 @@ _get_root() {
       return 1
     fi
 
-    _authget "https://management.1984hosting.com/domains/soacheck/?zone=$h&nameserver=ns0.1984.is."
+    _authget "https://1984.hosting/domains/soacheck/?zone=$h&nameserver=ns0.1984.is."
     if _contains "$_response" "serial" && ! _contains "$_response" "null"; then
       _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
       _domain="$h"
@@ -251,11 +251,11 @@ _htmlget() {
 
 # add extra headers to request
 _authpost() {
-  url="https://management.1984hosting.com/domains"
+  url="https://1984.hosting/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 _H2="Referer: https://1984.hosting/domains/$_zone_id"
   export _H3="X-CSRFToken: $csrf_header"
   _response=$(_post "$1" "$2")
 }

+ 1 - 1
dnsapi/dns_aws.sh

@@ -152,7 +152,7 @@ dns_aws_rm() {
 
 _get_root() {
   domain=$1
-  i=2
+  i=1
   p=1
 
   if aws_rest GET "2013-04-01/hostedzone"; then

+ 9 - 3
dnsapi/dns_cf.sh

@@ -25,9 +25,15 @@ dns_cf_add() {
   CF_Email="${CF_Email:-$(_readaccountconf_mutable CF_Email)}"
 
   if [ "$CF_Token" ]; then
-    _saveaccountconf_mutable CF_Token "$CF_Token"
-    _saveaccountconf_mutable CF_Account_ID "$CF_Account_ID"
-    _saveaccountconf_mutable CF_Zone_ID "$CF_Zone_ID"
+    if [ "$CF_Zone_ID" ]; then
+      _savedomainconf CF_Token "$CF_Token"
+      _savedomainconf CF_Account_ID "$CF_Account_ID"
+      _savedomainconf CF_Zone_ID "$CF_Zone_ID"
+    else
+      _saveaccountconf_mutable CF_Token "$CF_Token"
+      _saveaccountconf_mutable CF_Account_ID "$CF_Account_ID"
+      _saveaccountconf_mutable CF_Zone_ID "$CF_Zone_ID"
+    fi
   else
     if [ -z "$CF_Key" ] || [ -z "$CF_Email" ]; then
       CF_Key=""

+ 159 - 0
dnsapi/dns_curanet.sh

@@ -0,0 +1,159 @@
+#!/usr/bin/env sh
+
+#Script to use with curanet.dk, scannet.dk, wannafind.dk, dandomain.dk DNS management.
+#Requires api credentials with scope: dns
+#Author: Peter L. Hansen <[email protected]>
+#Version 1.0
+
+CURANET_REST_URL="https://api.curanet.dk/dns/v1/Domains"
+CURANET_AUTH_URL="https://apiauth.dk.team.blue/auth/realms/Curanet/protocol/openid-connect/token"
+CURANET_ACCESS_TOKEN=""
+
+########  Public functions #####################
+
+#Usage: dns_curanet_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_curanet_add() {
+  fulldomain=$1
+  txtvalue=$2
+  _info "Using curanet"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  CURANET_AUTHCLIENTID="${CURANET_AUTHCLIENTID:-$(_readaccountconf_mutable CURANET_AUTHCLIENTID)}"
+  CURANET_AUTHSECRET="${CURANET_AUTHSECRET:-$(_readaccountconf_mutable CURANET_AUTHSECRET)}"
+  if [ -z "$CURANET_AUTHCLIENTID" ] || [ -z "$CURANET_AUTHSECRET" ]; then
+    CURANET_AUTHCLIENTID=""
+    CURANET_AUTHSECRET=""
+    _err "You don't specify curanet api client and secret."
+    _err "Please create your auth info and try again."
+    return 1
+  fi
+
+  #save the credentials to the account conf file.
+  _saveaccountconf_mutable CURANET_AUTHCLIENTID "$CURANET_AUTHCLIENTID"
+  _saveaccountconf_mutable CURANET_AUTHSECRET "$CURANET_AUTHSECRET"
+
+  if ! _get_token; then
+    _err "Unable to get token"
+    return 1
+  fi
+
+  if ! _get_root "$fulldomain"; then
+    _err "Invalid domain"
+    return 1
+  fi
+
+  export _H1="Content-Type: application/json-patch+json"
+  export _H2="Accept: application/json"
+  export _H3="Authorization: Bearer $CURANET_ACCESS_TOKEN"
+  data="{\"name\": \"$fulldomain\",\"type\": \"TXT\",\"ttl\": 60,\"priority\": 0,\"data\": \"$txtvalue\"}"
+  response="$(_post "$data" "$CURANET_REST_URL/${_domain}/Records" "" "")"
+
+  if _contains "$response" "$txtvalue"; then
+    _debug "TXT record added OK"
+  else
+    _err "Unable to add TXT record"
+    return 1
+  fi
+
+  return 0
+}
+
+#Usage: fulldomain txtvalue
+#Remove the txt record after validation.
+dns_curanet_rm() {
+  fulldomain=$1
+  txtvalue=$2
+  _info "Using curanet"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  CURANET_AUTHCLIENTID="${CURANET_AUTHCLIENTID:-$(_readaccountconf_mutable CURANET_AUTHCLIENTID)}"
+  CURANET_AUTHSECRET="${CURANET_AUTHSECRET:-$(_readaccountconf_mutable CURANET_AUTHSECRET)}"
+
+  if ! _get_token; then
+    _err "Unable to get token"
+    return 1
+  fi
+
+  if ! _get_root "$fulldomain"; then
+    _err "Invalid domain"
+    return 1
+  fi
+
+  _debug "Getting current record list to identify TXT to delete"
+
+  export _H1="Content-Type: application/json"
+  export _H2="Accept: application/json"
+  export _H3="Authorization: Bearer $CURANET_ACCESS_TOKEN"
+
+  response="$(_get "$CURANET_REST_URL/${_domain}/Records" "" "")"
+
+  if ! _contains "$response" "$txtvalue"; then
+    _err "Unable to delete record (does not contain $txtvalue )"
+    return 1
+  fi
+
+  recordid=$(echo "$response" | _egrep_o "{\"id\":[0-9]+,\"name\":\"$fulldomain\",\"type\":\"TXT\",\"ttl\":60,\"priority\":0,\"data\":\"..$txtvalue" | _egrep_o "id\":[0-9]+" | cut -c 5-)
+
+  if [ -z "$recordid" ]; then
+    _err "Unable to get recordid"
+    _debug "regex {\"id\":[0-9]+,\"name\":\"$fulldomain\",\"type\":\"TXT\",\"ttl\":60,\"priority\":0,\"data\":\"..$txtvalue"
+    _debug "response $response"
+    return 1
+  fi
+
+  _debug "Deleting recordID $recordid"
+  response="$(_post "" "$CURANET_REST_URL/${_domain}/Records/$recordid" "" "DELETE")"
+  return 0
+}
+
+####################  Private functions below ##################################
+
+_get_token() {
+  response="$(_post "grant_type=client_credentials&client_id=$CURANET_AUTHCLIENTID&client_secret=$CURANET_AUTHSECRET&scope=dns" "$CURANET_AUTH_URL" "" "")"
+  if ! _contains "$response" "access_token"; then
+    _err "Unable get access token"
+    return 1
+  fi
+  CURANET_ACCESS_TOKEN=$(echo "$response" | _egrep_o "\"access_token\":\"[^\"]+" | cut -c 17-)
+
+  if [ -z "$CURANET_ACCESS_TOKEN" ]; then
+    _err "Unable to get token"
+    return 1
+  fi
+
+  return 0
+
+}
+
+#_acme-challenge.www.domain.com
+#returns
+# _domain=domain.com
+# _domain_id=sdjkglgdfewsdfg
+_get_root() {
+  domain=$1
+  i=1
+
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    _debug h "$h"
+    if [ -z "$h" ]; then
+      #not valid
+      return 1
+    fi
+
+    export _H1="Content-Type: application/json"
+    export _H2="Accept: application/json"
+    export _H3="Authorization: Bearer $CURANET_ACCESS_TOKEN"
+    response="$(_get "$CURANET_REST_URL/$h/Records" "" "")"
+
+    if [ ! "$(echo "$response" | _egrep_o "Entity not found")" ]; then
+      _domain=$h
+      return 0
+    fi
+
+    i=$(_math "$i" + 1)
+  done
+  return 1
+}

+ 0 - 185
dnsapi/dns_cx.sh

@@ -1,185 +0,0 @@
-#!/usr/bin/env sh
-
-# CloudXNS Domain api
-#
-#CX_Key="1234"
-#
-#CX_Secret="sADDsdasdgdsf"
-
-CX_Api="https://www.cloudxns.net/api2"
-
-#REST_API
-########  Public functions #####################
-
-#Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
-dns_cx_add() {
-  fulldomain=$1
-  txtvalue=$2
-
-  CX_Key="${CX_Key:-$(_readaccountconf_mutable CX_Key)}"
-  CX_Secret="${CX_Secret:-$(_readaccountconf_mutable CX_Secret)}"
-  if [ -z "$CX_Key" ] || [ -z "$CX_Secret" ]; then
-    CX_Key=""
-    CX_Secret=""
-    _err "You don't specify cloudxns.net  api key or secret yet."
-    _err "Please create you key and try again."
-    return 1
-  fi
-
-  REST_API="$CX_Api"
-
-  #save the api key and email to the account conf file.
-  _saveaccountconf_mutable CX_Key "$CX_Key"
-  _saveaccountconf_mutable CX_Secret "$CX_Secret"
-
-  _debug "First detect the root zone"
-  if ! _get_root "$fulldomain"; then
-    _err "invalid domain"
-    return 1
-  fi
-
-  add_record "$_domain" "$_sub_domain" "$txtvalue"
-}
-
-#fulldomain txtvalue
-dns_cx_rm() {
-  fulldomain=$1
-  txtvalue=$2
-  CX_Key="${CX_Key:-$(_readaccountconf_mutable CX_Key)}"
-  CX_Secret="${CX_Secret:-$(_readaccountconf_mutable CX_Secret)}"
-  REST_API="$CX_Api"
-  if _get_root "$fulldomain"; then
-    record_id=""
-    existing_records "$_domain" "$_sub_domain" "$txtvalue"
-    if [ "$record_id" ]; then
-      _rest DELETE "record/$record_id/$_domain_id" "{}"
-      _info "Deleted record ${fulldomain}"
-    fi
-  fi
-}
-
-#usage:  root  sub
-#return if the sub record already exists.
-#echos the existing records count.
-# '0' means doesn't exist
-existing_records() {
-  _debug "Getting txt records"
-  root=$1
-  sub=$2
-  if ! _rest GET "record/$_domain_id?:domain_id?host_id=0&offset=0&row_num=100"; then
-    return 1
-  fi
-
-  seg=$(printf "%s\n" "$response" | _egrep_o '"record_id":[^{]*host":"'"$_sub_domain"'"[^}]*\}')
-  _debug seg "$seg"
-  if [ -z "$seg" ]; then
-    return 0
-  fi
-
-  if printf "%s" "$response" | grep '"type":"TXT"' >/dev/null; then
-    record_id=$(printf "%s\n" "$seg" | _egrep_o '"record_id":"[^"]*"' | cut -d : -f 2 | tr -d \" | _head_n 1)
-    _debug record_id "$record_id"
-    return 0
-  fi
-
-}
-
-#add the txt record.
-#usage: root  sub  txtvalue
-add_record() {
-  root=$1
-  sub=$2
-  txtvalue=$3
-  fulldomain="$sub.$root"
-
-  _info "Adding record"
-
-  if ! _rest POST "record" "{\"domain_id\": $_domain_id, \"host\":\"$_sub_domain\", \"value\":\"$txtvalue\", \"type\":\"TXT\",\"ttl\":600, \"line_id\":1}"; then
-    return 1
-  fi
-
-  return 0
-}
-
-####################  Private functions below ##################################
-#_acme-challenge.www.domain.com
-#returns
-# _sub_domain=_acme-challenge.www
-# _domain=domain.com
-# _domain_id=sdjkglgdfewsdfg
-_get_root() {
-  domain=$1
-  i=2
-  p=1
-
-  if ! _rest GET "domain"; 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" "$h."; then
-      seg=$(printf "%s\n" "$response" | _egrep_o '"id":[^{]*"'"$h"'."[^}]*}')
-      _debug seg "$seg"
-      _domain_id=$(printf "%s\n" "$seg" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")
-      _debug _domain_id "$_domain_id"
-      if [ "$_domain_id" ]; then
-        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
-        _debug _sub_domain "$_sub_domain"
-        _domain="$h"
-        _debug _domain "$_domain"
-        return 0
-      fi
-      return 1
-    fi
-    p="$i"
-    i=$(_math "$i" + 1)
-  done
-  return 1
-}
-
-#Usage: method  URI  data
-_rest() {
-  m=$1
-  ep="$2"
-  _debug ep "$ep"
-  url="$REST_API/$ep"
-  _debug url "$url"
-
-  cdate=$(date -u "+%Y-%m-%d %H:%M:%S UTC")
-  _debug cdate "$cdate"
-
-  data="$3"
-  _debug data "$data"
-
-  sec="$CX_Key$url$data$cdate$CX_Secret"
-  _debug sec "$sec"
-  hmac=$(printf "%s" "$sec" | _digest md5 hex)
-  _debug hmac "$hmac"
-
-  export _H1="API-KEY: $CX_Key"
-  export _H2="API-REQUEST-DATE: $cdate"
-  export _H3="API-HMAC: $hmac"
-  export _H4="Content-Type: application/json"
-
-  if [ "$data" ]; then
-    response="$(_post "$data" "$url" "" "$m")"
-  else
-    response="$(_get "$url")"
-  fi
-
-  if [ "$?" != "0" ]; then
-    _err "error $ep"
-    return 1
-  fi
-  _debug2 response "$response"
-
-  _contains "$response" '"code":1'
-
-}

+ 146 - 0
dnsapi/dns_fornex.sh

@@ -0,0 +1,146 @@
+#!/usr/bin/env sh
+
+#Author: Timur Umarov <[email protected]>
+
+FORNEX_API_URL="https://fornex.com/api/dns/v0.1"
+
+########  Public functions #####################
+
+#Usage: dns_fornex_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_fornex_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if ! _Fornex_API; then
+    return 1
+  fi
+
+  if ! _get_root "$fulldomain"; then
+    _err "Unable to determine root domain"
+    return 1
+  else
+    _debug _domain "$_domain"
+  fi
+
+  _info "Adding record"
+  if _rest POST "$_domain/entry_set/add/" "host=$fulldomain&type=TXT&value=$txtvalue&apikey=$FORNEX_API_KEY"; then
+    _debug _response "$response"
+    if _contains "$response" '"ok": true' || _contains "$response" 'Такая запись уже существует.'; then
+      _info "Added, OK"
+      return 0
+    fi
+  fi
+  _err "Add txt record error."
+  return 1
+}
+
+#Usage: dns_fornex_rm   _acme-challenge.www.domain.com
+dns_fornex_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if ! _Fornex_API; then
+    return 1
+  fi
+
+  if ! _get_root "$fulldomain"; then
+    _err "Unable to determine root domain"
+    return 1
+  else
+    _debug _domain "$_domain"
+  fi
+
+  _debug "Getting txt records"
+  _rest GET "$_domain/entry_set.json?apikey=$FORNEX_API_KEY"
+
+  if ! _contains "$response" "$txtvalue"; then
+    _err "Txt record not found"
+    return 1
+  fi
+
+  _record_id="$(echo "$response" | _egrep_o "{[^{]*\"value\"*:*\"$txtvalue\"[^}]*}" | sed -n -e 's#.*"id": \([0-9]*\).*#\1#p')"
+  _debug "_record_id" "$_record_id"
+  if [ -z "$_record_id" ]; then
+    _err "can not find _record_id"
+    return 1
+  fi
+
+  if ! _rest POST "$_domain/entry_set/$_record_id/delete/" "apikey=$FORNEX_API_KEY"; then
+    _err "Delete record error."
+    return 1
+  fi
+  return 0
+}
+
+####################  Private functions below ##################################
+
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+_get_root() {
+  domain=$1
+
+  i=1
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    _debug h "$h"
+    if [ -z "$h" ]; then
+      #not valid
+      return 1
+    fi
+
+    if ! _rest GET "domain_list.json?q=$h&apikey=$FORNEX_API_KEY"; then
+      return 1
+    fi
+
+    if _contains "$response" "\"$h\"" >/dev/null; then
+      _domain=$h
+      return 0
+    else
+      _debug "$h not found"
+    fi
+    i=$(_math "$i" + 1)
+  done
+
+  return 1
+}
+
+_Fornex_API() {
+  FORNEX_API_KEY="${FORNEX_API_KEY:-$(_readaccountconf_mutable FORNEX_API_KEY)}"
+  if [ -z "$FORNEX_API_KEY" ]; then
+    FORNEX_API_KEY=""
+
+    _err "You didn't specify the Fornex API key yet."
+    _err "Please create your key and try again."
+
+    return 1
+  fi
+
+  _saveaccountconf_mutable FORNEX_API_KEY "$FORNEX_API_KEY"
+}
+
+#method method action data
+_rest() {
+  m=$1
+  ep="$2"
+  data="$3"
+  _debug "$ep"
+
+  export _H1="Accept: application/json"
+
+  if [ "$m" != "GET" ]; then
+    _debug data "$data"
+    response="$(_post "$data" "$FORNEX_API_URL/$ep" "" "$m")"
+  else
+    response="$(_get "$FORNEX_API_URL/$ep" | _normalizeJson)"
+  fi
+
+  _ret="$?"
+  if [ "$_ret" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}

+ 0 - 177
dnsapi/dns_gdnsdk.sh

@@ -1,177 +0,0 @@
-#!/usr/bin/env sh
-#Author: Herman Sletteng
-#Report Bugs here: https://github.com/loial/acme.sh
-#
-#
-# Note, gratisdns requires a login first, so the script needs to handle
-# temporary cookies. Since acme.sh _get/_post currently don't directly support
-# cookies, I've defined wrapper functions _myget/_mypost to set the headers
-
-GDNSDK_API="https://admin.gratisdns.com"
-########  Public functions #####################
-#Usage: dns_gdnsdk_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
-dns_gdnsdk_add() {
-  fulldomain=$1
-  txtvalue=$2
-  _info "Using gratisdns.dk"
-  _debug fulldomain "$fulldomain"
-  _debug txtvalue "$txtvalue"
-  if ! _gratisdns_login; then
-    _err "Login failed!"
-    return 1
-  fi
-  #finding domain zone
-  if ! _get_domain; then
-    _err "No matching root domain for $fulldomain found"
-    return 1
-  fi
-  # adding entry
-  _info "Adding the entry"
-  _mypost "action=dns_primary_record_added_txt&user_domain=$_domain&name=$fulldomain&txtdata=$txtvalue&ttl=1"
-  if _successful_update; then return 0; fi
-  _err "Couldn't create entry!"
-  return 1
-}
-
-#Usage: fulldomain txtvalue
-#Remove the txt record after validation.
-dns_gdnsdk_rm() {
-  fulldomain=$1
-  txtvalue=$2
-  _info "Using gratisdns.dk"
-  _debug fulldomain "$fulldomain"
-  _debug txtvalue "$txtvalue"
-  if ! _gratisdns_login; then
-    _err "Login failed!"
-    return 1
-  fi
-  if ! _get_domain; 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..."
-  _mypost "action=dns_primary_delete_txt&user_domain=$_domain&id=$_id"
-  # removing entry
-
-  if _successful_update; then return 0; fi
-  _err "Couldn't delete entry!"
-  return 1
-}
-
-####################  Private functions below ##################################
-
-_checkcredentials() {
-  GDNSDK_Username="${GDNSDK_Username:-$(_readaccountconf_mutable GDNSDK_Username)}"
-  GDNSDK_Password="${GDNSDK_Password:-$(_readaccountconf_mutable GDNSDK_Password)}"
-
-  if [ -z "$GDNSDK_Username" ] || [ -z "$GDNSDK_Password" ]; then
-    GDNSDK_Username=""
-    GDNSDK_Password=""
-    _err "You haven't specified gratisdns.dk username and password yet."
-    _err "Please add credentials and try again."
-    return 1
-  fi
-  #save the credentials to the account conf file.
-  _saveaccountconf_mutable GDNSDK_Username "$GDNSDK_Username"
-  _saveaccountconf_mutable GDNSDK_Password "$GDNSDK_Password"
-  return 0
-}
-
-_checkcookie() {
-  GDNSDK_Cookie="${GDNSDK_Cookie:-$(_readaccountconf_mutable GDNSDK_Cookie)}"
-  if [ -z "$GDNSDK_Cookie" ]; then
-    _debug "No cached cookie found"
-    return 1
-  fi
-  _myget "action="
-  if (echo "$_result" | grep -q "logmeout"); then
-    _debug "Cached cookie still valid"
-    return 0
-  fi
-  _debug "Cached cookie no longer valid"
-  GDNSDK_Cookie=""
-  _saveaccountconf_mutable GDNSDK_Cookie "$GDNSDK_Cookie"
-  return 1
-}
-
-_gratisdns_login() {
-  if ! _checkcredentials; then return 1; fi
-
-  if _checkcookie; then
-    _debug "Already logged in"
-    return 0
-  fi
-  _debug "Logging into GratisDNS with user $GDNSDK_Username"
-
-  if ! _mypost "login=$GDNSDK_Username&password=$GDNSDK_Password&action=logmein"; then
-    _err "GratisDNS login failed for user $GDNSDK_Username bad RC from _post"
-    return 1
-  fi
-
-  GDNSDK_Cookie="$(grep -A 15 '302 Found' "$HTTP_HEADER" | _egrep_o 'Cookie: [^;]*' | _head_n 1 | cut -d ' ' -f2)"
-
-  if [ -z "$GDNSDK_Cookie" ]; then
-    _err "GratisDNS login failed for user $GDNSDK_Username. Check $HTTP_HEADER file"
-    return 1
-  fi
-  export GDNSDK_Cookie
-  _saveaccountconf_mutable GDNSDK_Cookie "$GDNSDK_Cookie"
-  return 0
-}
-
-_myget() {
-  #Adds cookie to request
-  export _H1="Cookie: $GDNSDK_Cookie"
-  _result=$(_get "$GDNSDK_API?$1")
-}
-_mypost() {
-  #Adds cookie to request
-  export _H1="Cookie: $GDNSDK_Cookie"
-  _result=$(_post "$1" "$GDNSDK_API")
-}
-
-_get_domain() {
-  _myget 'action=dns_primarydns'
-  _domains=$(echo "$_result" | _egrep_o ' domain="[[:alnum:]._-]+' | sed 's/^.*"//')
-  if [ -z "$_domains" ]; then
-    _err "Primary domain list not found!"
-    return 1
-  fi
-  for _domain in $_domains; do
-    if (_endswith "$fulldomain" "$_domain"); then
-      _debug "Root domain: $_domain"
-      return 0
-    fi
-  done
-  return 1
-}
-
-_successful_update() {
-  if (echo "$_result" | grep -q 'table-success'); then return 0; fi
-  return 1
-}
-
-_findentry() {
-  #args    $1: fulldomain, $2: txtvalue
-  #returns id of dns entry, if it exists
-  _myget "action=dns_primary_changeDNSsetup&user_domain=$_domain"
-  _debug3 "_result: $_result"
-
-  _tmp_result=$(echo "$_result" | tr -d '\n\r' | _egrep_o "<td>$1</td>\s*<td>$2</td>[^?]*[^&]*&id=[^&]*")
-  _debug _tmp_result "$_tmp_result"
-  if [ -z "${_tmp_result:-}" ]; then
-    _debug "The variable is _tmp_result is not supposed to be empty, there may be something wrong with the script"
-  fi
-
-  _id=$(echo "$_tmp_result" | sed 's/^.*=//')
-  if [ -n "$_id" ]; then
-    _debug "Entry found with _id=$_id"
-    return 0
-  fi
-  return 1
-}

+ 232 - 0
dnsapi/dns_geoscaling.sh

@@ -0,0 +1,232 @@
+#!/usr/bin/env sh
+
+########################################################################
+# Geoscaling hook script for acme.sh
+#
+# Environment variables:
+#
+#  - $GEOSCALING_Username  (your Geoscaling username - this is usually NOT an amail address)
+#  - $GEOSCALING_Password  (your Geoscaling password)
+
+#-- dns_geoscaling_add() - Add TXT record --------------------------------------
+# Usage: dns_geoscaling_add _acme-challenge.subdomain.domain.com "XyZ123..."
+
+dns_geoscaling_add() {
+  full_domain=$1
+  txt_value=$2
+  _info "Using DNS-01 Geoscaling DNS2 hook"
+
+  GEOSCALING_Username="${GEOSCALING_Username:-$(_readaccountconf_mutable GEOSCALING_Username)}"
+  GEOSCALING_Password="${GEOSCALING_Password:-$(_readaccountconf_mutable GEOSCALING_Password)}"
+  if [ -z "$GEOSCALING_Username" ] || [ -z "$GEOSCALING_Password" ]; then
+    GEOSCALING_Username=
+    GEOSCALING_Password=
+    _err "No auth details provided. Please set user credentials using the \$GEOSCALING_Username and \$GEOSCALING_Password environment variables."
+    return 1
+  fi
+  _saveaccountconf_mutable GEOSCALING_Username "${GEOSCALING_Username}"
+  _saveaccountconf_mutable GEOSCALING_Password "${GEOSCALING_Password}"
+
+  # Fills in the $zone_id and $zone_name
+  find_zone "${full_domain}" || return 1
+  _debug "Zone id '${zone_id}' will be used."
+
+  # We're logged in here
+
+  # we should add ${full_domain} minus the trailing ${zone_name}
+
+  prefix=$(echo "${full_domain}" | sed "s|\\.${zone_name}\$||")
+
+  body="id=${zone_id}&name=${prefix}&type=TXT&content=${txt_value}&ttl=300&prio=0"
+
+  do_post "$body" "https://www.geoscaling.com/dns2/ajax/add_record.php"
+  exit_code="$?"
+  if [ "${exit_code}" -eq 0 ]; then
+    _info "TXT record added successfully."
+  else
+    _err "Couldn't add the TXT record."
+  fi
+  do_logout
+  return "${exit_code}"
+}
+
+#-- dns_geoscaling_rm() - Remove TXT record ------------------------------------
+# Usage: dns_geoscaling_rm _acme-challenge.subdomain.domain.com "XyZ123..."
+
+dns_geoscaling_rm() {
+  full_domain=$1
+  txt_value=$2
+  _info "Cleaning up after DNS-01 Geoscaling DNS2 hook"
+
+  GEOSCALING_Username="${GEOSCALING_Username:-$(_readaccountconf_mutable GEOSCALING_Username)}"
+  GEOSCALING_Password="${GEOSCALING_Password:-$(_readaccountconf_mutable GEOSCALING_Password)}"
+  if [ -z "$GEOSCALING_Username" ] || [ -z "$GEOSCALING_Password" ]; then
+    GEOSCALING_Username=
+    GEOSCALING_Password=
+    _err "No auth details provided. Please set user credentials using the \$GEOSCALING_Username and \$GEOSCALING_Password environment variables."
+    return 1
+  fi
+  _saveaccountconf_mutable GEOSCALING_Username "${GEOSCALING_Username}"
+  _saveaccountconf_mutable GEOSCALING_Password "${GEOSCALING_Password}"
+
+  # fills in the $zone_id
+  find_zone "${full_domain}" || return 1
+  _debug "Zone id '${zone_id}' will be used."
+
+  # Here we're logged in
+  # Find the record id to clean
+
+  # get the domain
+  response=$(do_get "https://www.geoscaling.com/dns2/index.php?module=domain&id=${zone_id}")
+  _debug2 "response" "$response"
+
+  table="$(echo "${response}" | tr -d '\n' | sed 's|.*<div class="box"><div class="boxtitle">Basic Records</div><div class="boxtext"><table|<table|; s|</table>.*|</table>|')"
+  _debug2 table "${table}"
+  names=$(echo "${table}" | _egrep_o 'id="[0-9]+\.name">[^<]*</td>' | sed 's|</td>||; s|.*>||')
+  ids=$(echo "${table}" | _egrep_o 'id="[0-9]+\.name">[^<]*</td>' | sed 's|\.name">.*||; s|id="||')
+  types=$(echo "${table}" | _egrep_o 'id="[0-9]+\.type">[^<]*</td>' | sed 's|</td>||; s|.*>||')
+  values=$(echo "${table}" | _egrep_o 'id="[0-9]+\.content">[^<]*</td>' | sed 's|</td>||; s|.*>||')
+
+  _debug2 names "${names}"
+  _debug2 ids "${ids}"
+  _debug2 types "${types}"
+  _debug2 values "${values}"
+
+  # look for line whose name is ${full_domain}, whose type is TXT, and whose value is ${txt_value}
+  line_num="$(echo "${values}" | grep -F -n -- "${txt_value}" | _head_n 1 | cut -d ':' -f 1)"
+  _debug2 line_num "${line_num}"
+  found_id=
+  if [ -n "$line_num" ]; then
+    type=$(echo "${types}" | sed -n "${line_num}p")
+    name=$(echo "${names}" | sed -n "${line_num}p")
+    id=$(echo "${ids}" | sed -n "${line_num}p")
+
+    _debug2 type "$type"
+    _debug2 name "$name"
+    _debug2 id "$id"
+    _debug2 full_domain "$full_domain"
+
+    if [ "${type}" = "TXT" ] && [ "${name}" = "${full_domain}" ]; then
+      found_id=${id}
+    fi
+  fi
+
+  if [ "${found_id}" = "" ]; then
+    _err "Can not find record id."
+    return 0
+  fi
+
+  # Remove the record
+  body="id=${zone_id}&record_id=${found_id}"
+  response=$(do_post "$body" "https://www.geoscaling.com/dns2/ajax/delete_record.php")
+  exit_code="$?"
+  if [ "$exit_code" -eq 0 ]; then
+    _info "Record removed successfully."
+  else
+    _err "Could not clean (remove) up the record. Please go to Geoscaling administration interface and clean it by hand."
+  fi
+  do_logout
+  return "${exit_code}"
+}
+
+########################## PRIVATE FUNCTIONS ###########################
+
+do_get() {
+  _url=$1
+  export _H1="Cookie: $geoscaling_phpsessid_cookie"
+  _get "${_url}"
+}
+
+do_post() {
+  _body=$1
+  _url=$2
+  export _H1="Cookie: $geoscaling_phpsessid_cookie"
+  _post "${_body}" "${_url}"
+}
+
+do_login() {
+
+  _info "Logging in..."
+
+  username_encoded="$(printf "%s" "${GEOSCALING_Username}" | _url_encode)"
+  password_encoded="$(printf "%s" "${GEOSCALING_Password}" | _url_encode)"
+  body="username=${username_encoded}&password=${password_encoded}"
+
+  response=$(_post "$body" "https://www.geoscaling.com/dns2/index.php?module=auth")
+  _debug2 response "${response}"
+
+  #retcode=$(grep '^HTTP[^ ]*' "${HTTP_HEADER}" | _head_n 1 | _egrep_o '[0-9]+$')
+  retcode=$(grep '^HTTP[^ ]*' "${HTTP_HEADER}" | _head_n 1 | cut -d ' ' -f 2)
+
+  if [ "$retcode" != "302" ]; then
+    _err "Geoscaling login failed for user ${GEOSCALING_Username}. Check ${HTTP_HEADER} file"
+    return 1
+  fi
+
+  geoscaling_phpsessid_cookie="$(grep -i '^set-cookie:' "${HTTP_HEADER}" | _egrep_o 'PHPSESSID=[^;]*;' | tr -d ';')"
+  return 0
+
+}
+
+do_logout() {
+  _info "Logging out."
+  response="$(do_get "https://www.geoscaling.com/dns2/index.php?module=auth")"
+  _debug2 response "$response"
+  return 0
+}
+
+find_zone() {
+  domain="$1"
+
+  # do login
+  do_login || return 1
+
+  # get zones
+  response="$(do_get "https://www.geoscaling.com/dns2/index.php?module=domains")"
+
+  table="$(echo "${response}" | tr -d '\n' | sed 's|.*<div class="box"><div class="boxtitle">Your domains</div><div class="boxtext"><table|<table|; s|</table>.*|</table>|')"
+  _debug2 table "${table}"
+  zone_names="$(echo "${table}" | _egrep_o '<b>[^<]*</b>' | sed 's|<b>||;s|</b>||')"
+  _debug2 _matches "${zone_names}"
+  # Zone names and zone IDs are in same order
+  zone_ids=$(echo "${table}" | _egrep_o '<a href=.index\.php\?module=domain&id=[0-9]+. onclick="javascript:show_loader\(\);">' | sed 's|.*id=||;s|. .*||')
+
+  _debug2 "These are the zones on this Geoscaling account:"
+  _debug2 "zone_names" "${zone_names}"
+  _debug2 "And these are their respective IDs:"
+  _debug2 "zone_ids" "${zone_ids}"
+  if [ -z "${zone_names}" ] || [ -z "${zone_ids}" ]; then
+    _err "Can not get zone names or IDs."
+    return 1
+  fi
+  # Walk through all possible zone names
+  strip_counter=1
+  while true; do
+    attempted_zone=$(echo "${domain}" | cut -d . -f ${strip_counter}-)
+
+    # All possible zone names have been tried
+    if [ -z "${attempted_zone}" ]; then
+      _err "No zone for domain '${domain}' found."
+      return 1
+    fi
+
+    _debug "Looking for zone '${attempted_zone}'"
+
+    line_num="$(echo "${zone_names}" | grep -n "^${attempted_zone}\$" | _head_n 1 | cut -d : -f 1)"
+    _debug2 line_num "${line_num}"
+    if [ "$line_num" ]; then
+      zone_id=$(echo "${zone_ids}" | sed -n "${line_num}p")
+      zone_name=$(echo "${zone_names}" | sed -n "${line_num}p")
+      if [ -z "${zone_id}" ]; then
+        _err "Can not find zone id."
+        return 1
+      fi
+      _debug "Found relevant zone '${attempted_zone}' with id '${zone_id}' - will be used for domain '${domain}'."
+      return 0
+    fi
+
+    _debug "Zone '${attempted_zone}' doesn't exist, let's try a less specific zone."
+    strip_counter=$(_math "${strip_counter}" + 1)
+  done
+}
+# vim: et:ts=2:sw=2:

+ 1 - 1
dnsapi/dns_ispconfig.sh

@@ -32,7 +32,7 @@ dns_ispconfig_rm() {
 ####################  Private functions below ##################################
 
 _ISPC_credentials() {
-  if [ -z "${ISPC_User}" ] || [ -z "$ISPC_Password" ] || [ -z "${ISPC_Api}" ] || [ -n "${ISPC_Api_Insecure}" ]; then
+  if [ -z "${ISPC_User}" ] || [ -z "${ISPC_Password}" ] || [ -z "${ISPC_Api}" ] || [ -z "${ISPC_Api_Insecure}" ]; then
     ISPC_User=""
     ISPC_Password=""
     ISPC_Api=""

+ 54 - 30
dnsapi/dns_loopia.sh

@@ -32,8 +32,12 @@ dns_loopia_add() {
 
   _info "Adding record"
 
-  _loopia_add_sub_domain "$_domain" "$_sub_domain"
-  _loopia_add_record "$_domain" "$_sub_domain" "$txtvalue"
+  if ! _loopia_add_sub_domain "$_domain" "$_sub_domain"; then
+    return 1
+  fi
+  if ! _loopia_add_record "$_domain" "$_sub_domain" "$txtvalue"; then
+    return 1
+  fi
 
 }
 
@@ -70,12 +74,13 @@ dns_loopia_rm() {
         <value><string>%s</string></value>
       </param>
     </params>
-  </methodCall>' "$LOOPIA_User" "$LOOPIA_Password" "$_domain" "$_sub_domain")
+  </methodCall>' "$LOOPIA_User" "$Encoded_Password" "$_domain" "$_sub_domain")
 
   response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")"
 
   if ! _contains "$response" "OK"; then
-    _err "Error could not get txt records"
+    err_response=$(echo "$response" | sed 's/.*<string>\(.*\)<\/string>.*/\1/')
+    _err "Error could not get txt records: $err_response"
     return 1
   fi
 }
@@ -101,6 +106,12 @@ _loopia_load_config() {
     return 1
   fi
 
+  if _contains "$LOOPIA_Password" "'" || _contains "$LOOPIA_Password" '"'; then
+    _err "Password contains quoute or double quoute and this is not supported by dns_loopia.sh"
+    return 1
+  fi
+
+  Encoded_Password=$(_xml_encode "$LOOPIA_Password")
   return 0
 }
 
@@ -133,11 +144,12 @@ _loopia_get_records() {
         <value><string>%s</string></value>
       </param>
     </params>
-  </methodCall>' $LOOPIA_User $LOOPIA_Password "$domain" "$sub_domain")
+  </methodCall>' "$LOOPIA_User" "$Encoded_Password" "$domain" "$sub_domain")
 
   response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")"
   if ! _contains "$response" "<array>"; then
-    _err "Error"
+    err_response=$(echo "$response" | sed 's/.*<string>\(.*\)<\/string>.*/\1/')
+    _err "Error: $err_response"
     return 1
   fi
   return 0
@@ -162,7 +174,7 @@ _get_root() {
     <value><string>%s</string></value>
    </param>
   </params>
-  </methodCall>' $LOOPIA_User $LOOPIA_Password)
+  </methodCall>' "$LOOPIA_User" "$Encoded_Password")
 
   response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")"
   while true; do
@@ -206,32 +218,35 @@ _loopia_add_record() {
         <value><string>%s</string></value>
       </param>
       <param>
-        <struct>
-          <member>
-            <name>type</name>
-            <value><string>TXT</string></value>
-          </member>
-          <member>
-            <name>priority</name>
-            <value><int>0</int></value>
-          </member>
-          <member>
-            <name>ttl</name>
-            <value><int>300</int></value>
-          </member>
-          <member>
-            <name>rdata</name>
-            <value><string>%s</string></value>
-          </member>
-        </struct>
+        <value>
+          <struct>
+            <member>
+              <name>type</name>
+              <value><string>TXT</string></value>
+            </member>
+            <member>
+              <name>priority</name>
+              <value><int>0</int></value>
+            </member>
+            <member>
+              <name>ttl</name>
+              <value><int>300</int></value>
+            </member>
+            <member>
+              <name>rdata</name>
+              <value><string>%s</string></value>
+            </member>
+          </struct>
+        </value>
       </param>
     </params>
-  </methodCall>' $LOOPIA_User $LOOPIA_Password "$domain" "$sub_domain" "$txtval")
+  </methodCall>' "$LOOPIA_User" "$Encoded_Password" "$domain" "$sub_domain" "$txtval")
 
   response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")"
 
   if ! _contains "$response" "OK"; then
-    _err "Error"
+    err_response=$(echo "$response" | sed 's/.*<string>\(.*\)<\/string>.*/\1/')
+    _err "Error: $err_response"
     return 1
   fi
   return 0
@@ -255,7 +270,7 @@ _sub_domain_exists() {
         <value><string>%s</string></value>
       </param>
     </params>
-  </methodCall>' $LOOPIA_User $LOOPIA_Password "$domain")
+  </methodCall>' "$LOOPIA_User" "$Encoded_Password" "$domain")
 
   response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")"
 
@@ -290,13 +305,22 @@ _loopia_add_sub_domain() {
         <value><string>%s</string></value>
       </param>
     </params>
-  </methodCall>' $LOOPIA_User $LOOPIA_Password "$domain" "$sub_domain")
+  </methodCall>' "$LOOPIA_User" "$Encoded_Password" "$domain" "$sub_domain")
 
   response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")"
 
   if ! _contains "$response" "OK"; then
-    _err "Error"
+    err_response=$(echo "$response" | sed 's/.*<string>\(.*\)<\/string>.*/\1/')
+    _err "Error: $err_response"
     return 1
   fi
   return 0
 }
+
+_xml_encode() {
+  encoded_string=$1
+  encoded_string=$(echo "$encoded_string" | sed 's/&/\&amp;/')
+  encoded_string=$(echo "$encoded_string" | sed 's/</\&lt;/')
+  encoded_string=$(echo "$encoded_string" | sed 's/>/\&gt;/')
+  printf "%s" "$encoded_string"
+}

+ 3 - 1
dnsapi/dns_mydevil.sh

@@ -74,7 +74,7 @@ dns_mydevil_rm() {
   validRecords="^${num}${w}${fulldomain}${w}TXT${w}${any}${txtvalue}$"
   for id in $(devil dns list "$domain" | tail -n+2 | grep "${validRecords}" | cut -w -s -f 1); do
     _info "Removing record $id from domain $domain"
-    devil dns del "$domain" "$id" || _err "Could not remove DNS record."
+    echo "y" | devil dns del "$domain" "$id" || _err "Could not remove DNS record."
   done
 }
 
@@ -87,7 +87,9 @@ mydevil_get_domain() {
   domain=""
 
   for domain in $(devil dns list | cut -w -s -f 1 | tail -n+2); do
+    _debug "Checking domain: $domain"
     if _endswith "$fulldomain" "$domain"; then
+      _debug "Fulldomain '$fulldomain' matches '$domain'"
       printf -- "%s" "$domain"
       return 0
     fi

+ 1 - 1
dnsapi/dns_netlify.sh

@@ -114,7 +114,7 @@ _get_root() {
     fi
 
     if _contains "$response" "\"name\":\"$h\"" >/dev/null; then
-      _domain_id=$(echo "$response" | _egrep_o "\"[^\"]*\",\"name\":\"$h" | cut -d , -f 1 | tr -d \")
+      _domain_id=$(echo "$response" | _egrep_o "\"[^\"]*\",\"name\":\"$h\"" | cut -d , -f 1 | tr -d \")
       if [ "$_domain_id" ]; then
         if [ "$i" = 1 ]; then
           #create the record at the domain apex (@) if only the domain name was provided as --domain-alias

+ 1 - 2
dnsapi/dns_opnsense.sh

@@ -150,8 +150,7 @@ _get_root() {
       return 1
     fi
     _debug h "$h"
-    id=$(echo "$_domain_response" | _egrep_o "\"[^\"]*\":{\"enabled\":\"1\",\"type\":{\"master\":{\"value\":\"master\",\"selected\":1},\"slave\":{\"value\":\"slave\",\"selected\":0}},\"masterip\":{\"\":{[^}]*}}(,\"allownotifyslave\":{\"\":{[^}]*}},|,)\"domainname\":\"${h}\"" | cut -d ':' -f 1 | cut -d '"' -f 2)
-
+    id=$(echo "$_domain_response" | _egrep_o "\"[^\"]*\":{\"enabled\":\"1\",\"type\":{\"master\":{\"value\":\"master\",\"selected\":1},\"slave\":{\"value\":\"slave\",\"selected\":0}},\"masterip\":{\"[^\"]*\":{[^}]*}},\"transferkeyalgo\":{[^{]*{[^{]*{[^{]*{[^{]*{[^{]*{[^{]*{[^{]*{[^}]*}},\"transferkey\":\"[^\"]*\"(,\"allownotifyslave\":{\"\":{[^}]*}},|,)\"domainname\":\"${h}\"" | cut -d ':' -f 1 | cut -d '"' -f 2)
     if [ -n "$id" ]; then
       _debug id "$id"
       _host=$(printf "%s" "$domain" | cut -d . -f 1-$p)

+ 2 - 0
dnsapi/dns_ovh.sh

@@ -198,6 +198,8 @@ dns_ovh_rm() {
       if ! _ovh_rest DELETE "domain/zone/$_domain/record/$rid"; then
         return 1
       fi
+      _ovh_rest POST "domain/zone/$_domain/refresh"
+      _debug "Refresh:$response"
       return 0
     fi
   done

+ 11 - 5
dnsapi/dns_simply.sh

@@ -5,8 +5,8 @@
 #SIMPLY_AccountName="accountname"
 #SIMPLY_ApiKey="apikey"
 #
-#SIMPLY_Api="https://api.simply.com/1/[ACCOUNTNAME]/[APIKEY]"
-SIMPLY_Api_Default="https://api.simply.com/1"
+#SIMPLY_Api="https://api.simply.com/2/"
+SIMPLY_Api_Default="https://api.simply.com/2"
 
 #This is used for determining success of REST call
 SIMPLY_SUCCESS_CODE='"status":200'
@@ -237,12 +237,18 @@ _simply_rest() {
   _debug2 ep "$ep"
   _debug2 m "$m"
 
-  export _H1="Content-Type: application/json"
+  basicauth=$(printf "%s:%s" "$SIMPLY_AccountName" "$SIMPLY_ApiKey" | _base64)
+
+  if [ "$basicauth" ]; then
+    export _H1="Authorization: Basic $basicauth"
+  fi
+
+  export _H2="Content-Type: application/json"
 
   if [ "$m" != "GET" ]; then
-    response="$(_post "$data" "$SIMPLY_Api/$SIMPLY_AccountName/$SIMPLY_ApiKey/$ep" "" "$m")"
+    response="$(_post "$data" "$SIMPLY_Api/$ep" "" "$m")"
   else
-    response="$(_get "$SIMPLY_Api/$SIMPLY_AccountName/$SIMPLY_ApiKey/$ep")"
+    response="$(_get "$SIMPLY_Api/$ep")"
   fi
 
   if [ "$?" != "0" ]; then

+ 160 - 0
dnsapi/dns_udr.sh

@@ -0,0 +1,160 @@
+#!/usr/bin/env sh
+
+# united-domains Reselling (https://www.ud-reselling.com/) DNS API
+# Author: Andreas Scherer (https://github.com/andischerer)
+# Created: 2021-02-01
+#
+# Set the environment variables as below:
+#
+#    export UDR_USER="your_username_goes_here"
+#    export UDR_PASS="some_password_goes_here"
+#
+
+UDR_API="https://api.domainreselling.de/api/call.cgi"
+UDR_TTL="30"
+
+########  Public functions #####################
+
+#Usage: add _acme-challenge.www.domain.com "some_long_string_of_characters_go_here_from_lets_encrypt"
+dns_udr_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  UDR_USER="${UDR_USER:-$(_readaccountconf_mutable UDR_USER)}"
+  UDR_PASS="${UDR_PASS:-$(_readaccountconf_mutable UDR_PASS)}"
+  if [ -z "$UDR_USER" ] || [ -z "$UDR_PASS" ]; then
+    UDR_USER=""
+    UDR_PASS=""
+    _err "You didn't specify an UD-Reselling username and password yet"
+    return 1
+  fi
+  # save the username and password to the account conf file.
+  _saveaccountconf_mutable UDR_USER "$UDR_USER"
+  _saveaccountconf_mutable UDR_PASS "$UDR_PASS"
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+
+  _debug _dnszone "${_dnszone}"
+
+  _debug "Getting txt records"
+  if ! _udr_rest "QueryDNSZoneRRList" "dnszone=${_dnszone}"; then
+    return 1
+  fi
+
+  rr="${fulldomain}. ${UDR_TTL} IN TXT ${txtvalue}"
+  _debug resource_record "${rr}"
+  if _contains "$response" "$rr" >/dev/null; then
+    _err "Error, it would appear that this record already exists. Please review existing TXT records for this domain."
+    return 1
+  fi
+
+  _info "Adding record"
+  if ! _udr_rest "UpdateDNSZone" "dnszone=${_dnszone}&addrr0=${rr}"; then
+    _err "Adding the record did not succeed, please verify/check."
+    return 1
+  fi
+
+  _info "Added, OK"
+  return 0
+}
+
+dns_udr_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  UDR_USER="${UDR_USER:-$(_readaccountconf_mutable UDR_USER)}"
+  UDR_PASS="${UDR_PASS:-$(_readaccountconf_mutable UDR_PASS)}"
+  if [ -z "$UDR_USER" ] || [ -z "$UDR_PASS" ]; then
+    UDR_USER=""
+    UDR_PASS=""
+    _err "You didn't specify an UD-Reselling username and password yet"
+    return 1
+  fi
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+  _debug _dnszone "${_dnszone}"
+
+  _debug "Getting txt records"
+  if ! _udr_rest "QueryDNSZoneRRList" "dnszone=${_dnszone}"; then
+    return 1
+  fi
+
+  rr="${fulldomain}. ${UDR_TTL} IN TXT ${txtvalue}"
+  _debug resource_record "${rr}"
+  if _contains "$response" "$rr" >/dev/null; then
+    if ! _udr_rest "UpdateDNSZone" "dnszone=${_dnszone}&delrr0=${rr}"; then
+      _err "Deleting the record did not succeed, please verify/check."
+      return 1
+    fi
+    _info "Removed, OK"
+    return 0
+  else
+    _info "Text record is not present, will not delete anything."
+    return 0
+  fi
+}
+
+####################  Private functions below ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+_get_root() {
+  domain=$1
+  i=1
+
+  if ! _udr_rest "QueryDNSZoneList" ""; 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}" "${h}." >/dev/null; then
+      _dnszone=$(echo "$response" | _egrep_o "${h}")
+      if [ "$_dnszone" ]; then
+        return 0
+      fi
+      return 1
+    fi
+    i=$(_math "$i" + 1)
+  done
+  return 1
+}
+
+_udr_rest() {
+  if [ -n "$2" ]; then
+    data="command=$1&$2"
+  else
+    data="command=$1"
+  fi
+
+  _debug data "${data}"
+  response="$(_post "${data}" "${UDR_API}?s_login=${UDR_USER}&s_pw=${UDR_PASS}" "" "POST")"
+
+  _code=$(echo "$response" | _egrep_o "code = ([0-9]+)" | _head_n 1 | cut -d = -f 2 | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
+  _description=$(echo "$response" | _egrep_o "description = .*" | _head_n 1 | cut -d = -f 2 | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
+
+  _debug response_code "$_code"
+  _debug response_description "$_description"
+
+  if [ ! "$_code" = "200" ]; then
+    _err "DNS-API-Error: $_description"
+    return 1
+  fi
+
+  return 0
+}

+ 142 - 0
dnsapi/dns_vercel.sh

@@ -0,0 +1,142 @@
+#!/usr/bin/env sh
+
+# Vercel DNS API
+#
+# This is your API token which can be acquired on the account page.
+# https://vercel.com/account/tokens
+#
+# VERCEL_TOKEN="sdfsdfsdfljlbjkljlkjsdfoiwje"
+
+VERCEL_API="https://api.vercel.com"
+
+#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_vercel_add() {
+  fulldomain=$1
+  txtvalue=$2
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  VERCEL_TOKEN="${VERCEL_TOKEN:-$(_readaccountconf_mutable VERCEL_TOKEN)}"
+
+  if [ -z "$VERCEL_TOKEN" ]; then
+    VERCEL_TOKEN=""
+    _err "You have not set the Vercel API token yet."
+    _err "Please visit https://vercel.com/account/tokens to generate it."
+    return 1
+  fi
+
+  _saveaccountconf_mutable VERCEL_TOKEN "$VERCEL_TOKEN"
+
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  _info "Adding record"
+  if _vercel_rest POST "v2/domains/$_domain/records" "{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":\"$txtvalue\"}"; then
+    if printf -- "%s" "$response" | grep "\"uid\":\"" >/dev/null; then
+      _info "Added"
+      return 0
+    else
+      _err "Unexpected response while adding text record."
+      return 1
+    fi
+  fi
+  _err "Add txt record error."
+}
+
+dns_vercel_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+
+  _vercel_rest GET "v2/domains/$_domain/records"
+
+  count=$(printf "%s\n" "$response" | _egrep_o "\"name\":\"$_sub_domain\",[^{]*\"type\":\"TXT\"" | wc -l | tr -d " ")
+
+  if [ "$count" = "0" ]; then
+    _info "Don't need to remove."
+  else
+    _record_id=$(printf "%s" "$response" | _egrep_o "\"id\":[^,]*,\"slug\":\"[^,]*\",\"name\":\"$_sub_domain\",[^{]*\"type\":\"TXT\",\"value\":\"$txtvalue\"" | cut -d: -f2 | cut -d, -f1 | tr -d '"')
+
+    if [ "$_record_id" ]; then
+      echo "$_record_id" | while read -r item; do
+        if _vercel_rest DELETE "v2/domains/$_domain/records/$item"; then
+          _info "removed record" "$item"
+          return 0
+        else
+          _err "failed to remove record" "$item"
+          return 1
+        fi
+      done
+    fi
+  fi
+}
+
+####################  Private functions below ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+_get_root() {
+  domain="$1"
+  ep="$2"
+  i=1
+  p=1
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    if [ -z "$h" ]; then
+      #not valid
+      return 1
+    fi
+
+    if ! _vercel_rest GET "v4/domains/$h"; then
+      return 1
+    fi
+
+    if _contains "$response" "\"name\":\"$h\"" >/dev/null; then
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      _domain=$h
+      return 0
+    fi
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+  return 1
+}
+
+_vercel_rest() {
+  m="$1"
+  ep="$2"
+  data="$3"
+
+  path="$VERCEL_API/$ep"
+
+  export _H1="Content-Type: application/json"
+  export _H2="Authorization: Bearer $VERCEL_TOKEN"
+
+  if [ "$m" != "GET" ]; then
+    _secure_debug2 data "$data"
+    response="$(_post "$data" "$path" "" "$m")"
+  else
+    response="$(_get "$path")"
+  fi
+  _ret="$?"
+  _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
+  _debug "http response code $_code"
+  _secure_debug2 response "$response"
+  if [ "$_ret" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+
+  response="$(printf "%s" "$response" | _normalizeJson)"
+  return 0
+}

+ 14 - 16
dnsapi/dns_world4you.sh

@@ -24,7 +24,7 @@ dns_world4you_add() {
   fi
 
   export _H1="Cookie: W4YSESSID=$sessid"
-  form=$(_get "$WORLD4YOU_API/dashboard/paketuebersicht")
+  form=$(_get "$WORLD4YOU_API/")
   _get_paketnr "$fqdn" "$form"
   paketnr="$PAKETNR"
   if [ -z "$paketnr" ]; then
@@ -54,15 +54,14 @@ dns_world4you_add() {
     if _contains "$res" "successfully"; then
       return 0
     else
-      msg=$(echo "$res" | tr '\n' '\t' | sed 's/.*<h3 class="mb-5">[^\t]*\t *\([^\t]*\)\t.*/\1/')
-      if _contains "$msg" '^<\!DOCTYPE html>'; then
-        msg='Unknown error'
-      fi
-      _err "Unable to add record: $msg"
-      if _contains "$msg" '^<\!DOCTYPE html>'; then
+      msg=$(echo "$res" | grep -A 15 'data-type="danger"' | grep "<h3[^>]*>[^<]" | sed 's/<[^>]*>\|^\s*//g')
+      if [ "$msg" = '' ]; then
+        _err "Unable to add record: Unknown error"
         echo "$ret" >'error-01.html'
         echo "$res" >'error-02.html'
         _err "View error-01.html and error-02.html for debugging"
+      else
+        _err "Unable to add record: my.world4you.com: $msg"
       fi
       return 1
     fi
@@ -87,7 +86,7 @@ dns_world4you_rm() {
   fi
 
   export _H1="Cookie: W4YSESSID=$sessid"
-  form=$(_get "$WORLD4YOU_API/dashboard/paketuebersicht")
+  form=$(_get "$WORLD4YOU_API/")
   _get_paketnr "$fqdn" "$form"
   paketnr="$PAKETNR"
   if [ -z "$paketnr" ]; then
@@ -119,15 +118,14 @@ dns_world4you_rm() {
     if _contains "$res" "successfully"; then
       return 0
     else
-      msg=$(echo "$res" | tr '\n' '\t' | sed 's/.*<h3 class="mb-5">[^\t]*\t *\([^\t]*\)\t.*/\1/')
-      if _contains "$msg" '^<\!DOCTYPE html>'; then
-        msg='Unknown error'
-      fi
-      _err "Unable to remove record: $msg"
-      if _contains "$msg" '^<\!DOCTYPE html>'; then
+      msg=$(echo "$res" | grep -A 15 'data-type="danger"' | grep "<h3[^>]*>[^<]" | sed 's/<[^>]*>\|^\s*//g')
+      if [ "$msg" = '' ]; then
+        _err "Unable to remove record: Unknown error"
         echo "$ret" >'error-01.html'
         echo "$res" >'error-02.html'
         _err "View error-01.html and error-02.html for debugging"
+      else
+        _err "Unable to remove record: my.world4you.com: $msg"
       fi
       return 1
     fi
@@ -184,7 +182,7 @@ _get_paketnr() {
   fqdn="$1"
   form="$2"
 
-  domains=$(echo "$form" | grep '^ *[A-Za-z0-9_\.-]*\.[A-Za-z0-9_-]*$' | sed 's/^ *\(.*\)$/\1/')
+  domains=$(echo "$form" | grep 'header-paket-domain' | sed 's/<[^>]*>//g' | sed 's/^.*>\([^>]*\)$/\1/')
   domain=''
   for domain in $domains; do
     if _contains "$fqdn" "$domain\$"; then
@@ -199,6 +197,6 @@ _get_paketnr() {
   TLD="$domain"
   _debug domain "$domain"
   RECORD=$(echo "$fqdn" | cut -c"1-$((${#fqdn} - ${#TLD} - 1))")
-  PAKETNR=$(echo "$form" | grep "data-textfilter=\".* $domain " | _head_n 1 | sed 's/^.* \([0-9]*\) .*$/\1/')
+  PAKETNR=$(echo "$form" | grep "data-textfilter=\".* $domain " | _tail_n 1 | sed "s|.*$WORLD4YOU_API/\\([0-9]*\\)/.*|\\1|")
   return 0
 }

+ 44 - 0
notify/callmebotWhatsApp.sh

@@ -0,0 +1,44 @@
+#!/usr/bin/env sh
+
+#Support CallMeBot Whatsapp webhooks
+
+#CALLMEBOT_YOUR_PHONE_NO=""
+#CALLMEBOT_API_KEY=""
+
+callmebotWhatsApp_send() {
+  _subject="$1"
+  _content="$2"
+  _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped
+  _debug "_statusCode" "$_statusCode"
+
+  CALLMEBOT_YOUR_PHONE_NO="${CALLMEBOT_YOUR_PHONE_NO:-$(_readaccountconf_mutable CALLMEBOT_YOUR_PHONE_NO)}"
+  if [ -z "$CALLMEBOT_YOUR_PHONE_NO" ]; then
+    CALLMEBOT_YOUR_PHONE_NO=""
+    _err "You didn't specify a Slack webhook url CALLMEBOT_YOUR_PHONE_NO yet."
+    return 1
+  fi
+  _saveaccountconf_mutable CALLMEBOT_YOUR_PHONE_NO "$CALLMEBOT_YOUR_PHONE_NO"
+
+  CALLMEBOT_API_KEY="${CALLMEBOT_API_KEY:-$(_readaccountconf_mutable CALLMEBOT_API_KEY)}"
+  if [ "$CALLMEBOT_API_KEY" ]; then
+    _saveaccountconf_mutable CALLMEBOT_API_KEY "$CALLMEBOT_API_KEY"
+  fi
+
+  _waUrl="https://api.callmebot.com/whatsapp.php"
+
+  _Phone_No="$(printf "%s" "$CALLMEBOT_YOUR_PHONE_NO" | _url_encode)"
+  _apikey="$(printf "%s" "$CALLMEBOT_API_KEY" | _url_encode)"
+  _message="$(printf "*%s*\\n%s" "$_subject" "$_content" | _url_encode)"
+
+  _finalUrl="$_waUrl?phone=$_Phone_No&apikey=$_apikey&text=$_message"
+  response="$(_get "$_finalUrl")"
+
+  if [ "$?" = "0" ] && _contains ".<p><b>Message queued.</b> You will receive it in a few seconds."; then
+    _info "wa send success."
+    return 0
+  fi
+  _err "wa send error."
+  _debug "URL" "$_finalUrl"
+  _debug "Response" "$response"
+  return 1
+}

+ 57 - 0
notify/discord.sh

@@ -0,0 +1,57 @@
+#!/usr/bin/env sh
+
+#Support Discord webhooks
+
+# Required:
+#DISCORD_WEBHOOK_URL=""
+# Optional:
+#DISCORD_USERNAME=""
+#DISCORD_AVATAR_URL=""
+
+discord_send() {
+  _subject="$1"
+  _content="$2"
+  _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped
+  _debug "_statusCode" "$_statusCode"
+
+  DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-$(_readaccountconf_mutable DISCORD_WEBHOOK_URL)}"
+  if [ -z "$DISCORD_WEBHOOK_URL" ]; then
+    DISCORD_WEBHOOK_URL=""
+    _err "You didn't specify a Discord webhook url DISCORD_WEBHOOK_URL yet."
+    return 1
+  fi
+  _saveaccountconf_mutable DISCORD_WEBHOOK_URL "$DISCORD_WEBHOOK_URL"
+
+  DISCORD_USERNAME="${DISCORD_USERNAME:-$(_readaccountconf_mutable DISCORD_USERNAME)}"
+  if [ "$DISCORD_USERNAME" ]; then
+    _saveaccountconf_mutable DISCORD_USERNAME "$DISCORD_USERNAME"
+  fi
+
+  DISCORD_AVATAR_URL="${DISCORD_AVATAR_URL:-$(_readaccountconf_mutable DISCORD_AVATAR_URL)}"
+  if [ "$DISCORD_AVATAR_URL" ]; then
+    _saveaccountconf_mutable DISCORD_AVATAR_URL "$DISCORD_AVATAR_URL"
+  fi
+
+  export _H1="Content-Type: application/json"
+
+  _content="$(printf "**%s**\n%s" "$_subject" "$_content" | _json_encode)"
+  _data="{\"content\": \"$_content\" "
+  if [ "$DISCORD_USERNAME" ]; then
+    _data="$_data, \"username\": \"$DISCORD_USERNAME\" "
+  fi
+  if [ "$DISCORD_AVATAR_URL" ]; then
+    _data="$_data, \"avatar_url\": \"$DISCORD_AVATAR_URL\" "
+  fi
+  _data="$_data}"
+
+  if _post "$_data" "$DISCORD_WEBHOOK_URL?wait=true"; then
+    # shellcheck disable=SC2154
+    if [ "$response" ]; then
+      _info "discord send success."
+      return 0
+    fi
+  fi
+  _err "discord send error."
+  _err "$response"
+  return 1
+}

+ 49 - 0
notify/weixin_work.sh

@@ -0,0 +1,49 @@
+#!/usr/bin/env sh
+
+#Support weixin work webhooks api
+
+#WEIXIN_WORK_WEBHOOK="xxxx"
+
+#optional
+#WEIXIN_WORK_KEYWORD="yyyy"
+
+#`WEIXIN_WORK_SIGNING_KEY`="SEC08ffdbd403cbc3fc8a65xxxxxxxxxxxxxxxxxxxx"
+
+# subject  content statusCode
+weixin_work_send() {
+  _subject="$1"
+  _content="$2"
+  _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped
+  _debug "_subject" "$_subject"
+  _debug "_content" "$_content"
+  _debug "_statusCode" "$_statusCode"
+
+  WEIXIN_WORK_WEBHOOK="${WEIXIN_WORK_WEBHOOK:-$(_readaccountconf_mutable WEIXIN_WORK_WEBHOOK)}"
+  if [ -z "$WEIXIN_WORK_WEBHOOK" ]; then
+    WEIXIN_WORK_WEBHOOK=""
+    _err "You didn't specify a weixin_work webhooks WEIXIN_WORK_WEBHOOK yet."
+    _err "You can get yours from https://work.weixin.qq.com/api/doc/90000/90136/91770"
+    return 1
+  fi
+  _saveaccountconf_mutable WEIXIN_WORK_WEBHOOK "$WEIXIN_WORK_WEBHOOK"
+
+  WEIXIN_WORK_KEYWORD="${WEIXIN_WORK_KEYWORD:-$(_readaccountconf_mutable WEIXIN_WORK_KEYWORD)}"
+  if [ "$WEIXIN_WORK_KEYWORD" ]; then
+    _saveaccountconf_mutable WEIXIN_WORK_KEYWORD "$WEIXIN_WORK_KEYWORD"
+  fi
+
+  _content=$(echo "$_content" | _json_encode)
+  _subject=$(echo "$_subject" | _json_encode)
+  _data="{\"msgtype\": \"text\", \"text\": {\"content\": \"[$WEIXIN_WORK_KEYWORD]\n$_subject\n$_content\"}}"
+
+  response="$(_post "$_data" "$WEIXIN_WORK_WEBHOOK" "" "POST" "application/json")"
+
+  if [ "$?" = "0" ] && _contains "$response" "errmsg\":\"ok"; then
+    _info "weixin_work webhooks event fired success."
+    return 0
+  fi
+
+  _err "weixin_work webhooks event fired error."
+  _err "$response"
+  return 1
+}