Quellcode durchsuchen

Merge pull request #3000 from acmesh-official/dev

sync
neil vor 5 Jahren
Ursprung
Commit
71a5f0e84e
13 geänderte Dateien mit 740 neuen und 34 gelöschten Zeilen
  1. 2 2
      README.md
  2. 14 5
      acme.sh
  3. 13 4
      deploy/ssh.sh
  4. 15 18
      deploy/synology_dsm.sh
  5. 1 1
      dnsapi/dns_1984hosting.sh
  6. 10 1
      dnsapi/dns_gdnsdk.sh
  7. 252 0
      dnsapi/dns_hetzner.sh
  8. 14 0
      dnsapi/dns_inwx.sh
  9. 2 2
      dnsapi/dns_lexicon.sh
  10. 168 0
      dnsapi/dns_njalla.sh
  11. 1 1
      dnsapi/dns_rackspace.sh
  12. 162 0
      dnsapi/dns_transip.sh
  13. 86 0
      notify/teams.sh

+ 2 - 2
README.md

@@ -246,7 +246,7 @@ More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-ce
 
 
 **(requires you to be root/sudoer, since it is required to interact with Apache server)**
 **(requires you to be root/sudoer, since it is required to interact with Apache server)**
 
 
-If you are running a web server, Apache or Nginx, it is recommended to use the `Webroot mode`.
+If you are running a web server, it is recommended to use the `Webroot mode`.
 
 
 Particularly, if you are running an Apache server, you can use Apache mode instead. This mode doesn't write any files to your web root folder.
 Particularly, if you are running an Apache server, you can use Apache mode instead. This mode doesn't write any files to your web root folder.
 
 
@@ -266,7 +266,7 @@ More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-ce
 
 
 **(requires you to be root/sudoer, since it is required to interact with Nginx server)**
 **(requires you to be root/sudoer, since it is required to interact with Nginx server)**
 
 
-If you are running a web server, Apache or Nginx, it is recommended to use the `Webroot mode`.
+If you are running a web server, it is recommended to use the `Webroot mode`.
 
 
 Particularly, if you are running an nginx server, you can use nginx mode instead. This mode doesn't write any files to your web root folder.
 Particularly, if you are running an nginx server, you can use nginx mode instead. This mode doesn't write any files to your web root folder.
 
 

+ 14 - 5
acme.sh

@@ -1,6 +1,6 @@
 #!/usr/bin/env sh
 #!/usr/bin/env sh
 
 
-VER=2.8.6
+VER=2.8.7
 
 
 PROJECT_NAME="acme.sh"
 PROJECT_NAME="acme.sh"
 
 
@@ -1003,7 +1003,7 @@ _sign() {
 
 
   _sign_openssl="${ACME_OPENSSL_BIN:-openssl} dgst -sign $keyfile "
   _sign_openssl="${ACME_OPENSSL_BIN:-openssl} dgst -sign $keyfile "
 
 
-  if grep "BEGIN RSA PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then
+  if grep "BEGIN RSA PRIVATE KEY" "$keyfile" >/dev/null 2>&1 || grep "BEGIN PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then
     $_sign_openssl -$alg | _base64
     $_sign_openssl -$alg | _base64
   elif grep "BEGIN EC PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then
   elif grep "BEGIN EC PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then
     if ! _signedECText="$($_sign_openssl -sha$__ECC_KEY_LEN | ${ACME_OPENSSL_BIN:-openssl} asn1parse -inform DER)"; then
     if ! _signedECText="$($_sign_openssl -sha$__ECC_KEY_LEN | ${ACME_OPENSSL_BIN:-openssl} asn1parse -inform DER)"; then
@@ -1986,7 +1986,9 @@ _send_signed_request() {
       continue
       continue
     fi
     fi
     if [ "$ACME_VERSION" = "2" ]; then
     if [ "$ACME_VERSION" = "2" ]; then
-      if [ "$url" = "$ACME_NEW_ACCOUNT" ] || [ "$url" = "$ACME_REVOKE_CERT" ]; then
+      if [ "$url" = "$ACME_NEW_ACCOUNT" ]; then
+        protected="$JWK_HEADERPLACE_PART1$nonce\", \"url\": \"${url}$JWK_HEADERPLACE_PART2, \"jwk\": $jwk"'}'
+      elif [ "$url" = "$ACME_REVOKE_CERT" ] && [ "$keyfile" != "$ACCOUNT_KEY_PATH" ]; then
         protected="$JWK_HEADERPLACE_PART1$nonce\", \"url\": \"${url}$JWK_HEADERPLACE_PART2, \"jwk\": $jwk"'}'
         protected="$JWK_HEADERPLACE_PART1$nonce\", \"url\": \"${url}$JWK_HEADERPLACE_PART2, \"jwk\": $jwk"'}'
       else
       else
         protected="$JWK_HEADERPLACE_PART1$nonce\", \"url\": \"${url}$JWK_HEADERPLACE_PART2, \"kid\": \"${ACCOUNT_URL}\""'}'
         protected="$JWK_HEADERPLACE_PART1$nonce\", \"url\": \"${url}$JWK_HEADERPLACE_PART2, \"kid\": \"${ACCOUNT_URL}\""'}'
@@ -4297,7 +4299,7 @@ $_authorizations_map"
 
 
   if [ "$dns_entries" ]; then
   if [ "$dns_entries" ]; then
     if [ -z "$Le_DNSSleep" ]; then
     if [ -z "$Le_DNSSleep" ]; then
-      _info "Let's check each dns records now. Sleep 20 seconds first."
+      _info "Let's check each DNS record now. Sleep 20 seconds first."
       _sleep 20
       _sleep 20
       if ! _check_dns_entries; then
       if ! _check_dns_entries; then
         _err "check dns error."
         _err "check dns error."
@@ -4566,7 +4568,14 @@ $_authorizations_map"
         break
         break
       elif _contains "$response" "\"processing\""; then
       elif _contains "$response" "\"processing\""; then
         _info "Order status is processing, lets sleep and retry."
         _info "Order status is processing, lets sleep and retry."
-        _sleep 2
+        _retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r')
+        _debug "_retryafter" "$_retryafter"
+        if [ "$_retryafter" ]; then
+          _info "Retry after: $_retryafter"
+          _sleep $_retryafter
+        else
+          _sleep 2
+        fi
       else
       else
         _err "Sign error, wrong status"
         _err "Sign error, wrong status"
         _err "$response"
         _err "$response"

+ 13 - 4
deploy/ssh.sh

@@ -33,10 +33,7 @@ ssh_deploy() {
   _ccert="$3"
   _ccert="$3"
   _cca="$4"
   _cca="$4"
   _cfullchain="$5"
   _cfullchain="$5"
-  _err_code=0
-  _cmdstr=""
-  _backupprefix=""
-  _backupdir=""
+  _deploy_ssh_servers=""
 
 
   if [ -f "$DOMAIN_CONF" ]; then
   if [ -f "$DOMAIN_CONF" ]; then
     # shellcheck disable=SC1090
     # shellcheck disable=SC1090
@@ -102,6 +99,18 @@ ssh_deploy() {
     _cleardomainconf Le_Deploy_ssh_multi_call
     _cleardomainconf Le_Deploy_ssh_multi_call
   fi
   fi
 
 
+  _deploy_ssh_servers=$Le_Deploy_ssh_server
+  for Le_Deploy_ssh_server in $_deploy_ssh_servers; do
+    _ssh_deploy
+  done
+}
+
+_ssh_deploy() {
+  _err_code=0
+  _cmdstr=""
+  _backupprefix=""
+  _backupdir=""
+
   _info "Deploy certificates to remote server $Le_Deploy_ssh_user@$Le_Deploy_ssh_server"
   _info "Deploy certificates to remote server $Le_Deploy_ssh_user@$Le_Deploy_ssh_server"
   if [ "$Le_Deploy_ssh_multi_call" = "yes" ]; then
   if [ "$Le_Deploy_ssh_multi_call" = "yes" ]; then
     _info "Using MULTI_CALL mode... Required commands sent in multiple calls to remote host"
     _info "Using MULTI_CALL mode... Required commands sent in multiple calls to remote host"

+ 15 - 18
deploy/synology_dsm.sh

@@ -22,7 +22,7 @@
 ########  Public functions #####################
 ########  Public functions #####################
 
 
 _syno_get_cookie_data() {
 _syno_get_cookie_data() {
-  grep "\W$1=" "$HTTP_HEADER" | grep "^Set-Cookie:" | _tail_n 1 | _egrep_o "$1=[^;]*;" | tr -d ';'
+  grep "\W$1=" | grep "^Set-Cookie:" | _tail_n 1 | _egrep_o "$1=[^;]*;" | tr -d ';'
 }
 }
 
 
 #domain keyfile certfile cafile fullchain
 #domain keyfile certfile cafile fullchain
@@ -40,9 +40,7 @@ synology_dsm_deploy() {
   _getdeployconf SYNO_Password
   _getdeployconf SYNO_Password
   _getdeployconf SYNO_Create
   _getdeployconf SYNO_Create
   _getdeployconf SYNO_DID
   _getdeployconf SYNO_DID
-  if [ -z "$SYNO_Username" ] || [ -z "$SYNO_Password" ]; then
-    SYNO_Username=""
-    SYNO_Password=""
+  if [ -z "${SYNO_Username:-}" ] || [ -z "${SYNO_Password:-}" ]; then
     _err "SYNO_Username & SYNO_Password must be set"
     _err "SYNO_Username & SYNO_Password must be set"
     return 1
     return 1
   fi
   fi
@@ -70,20 +68,20 @@ synology_dsm_deploy() {
 
 
   # Get the certificate description, but don't save it until we verfiy it's real
   # Get the certificate description, but don't save it until we verfiy it's real
   _getdeployconf SYNO_Certificate
   _getdeployconf SYNO_Certificate
-  if [ -z "${SYNO_Certificate:?}" ]; then
-    _err "SYNO_Certificate needs to be defined (with the Certificate description name)"
-    return 1
-  fi
-  _debug SYNO_Certificate "$SYNO_Certificate"
+  _debug SYNO_Certificate "${SYNO_Certificate:-}"
 
 
   _base_url="$SYNO_Scheme://$SYNO_Hostname:$SYNO_Port"
   _base_url="$SYNO_Scheme://$SYNO_Hostname:$SYNO_Port"
   _debug _base_url "$_base_url"
   _debug _base_url "$_base_url"
 
 
   # Login, get the token from JSON and session id from cookie
   # Login, get the token from JSON and session id from cookie
   _info "Logging into $SYNO_Hostname:$SYNO_Port"
   _info "Logging into $SYNO_Hostname:$SYNO_Port"
-  response=$(_get "$_base_url/webman/login.cgi?username=$SYNO_Username&passwd=$SYNO_Password&enable_syno_token=yes&device_id=$SYNO_DID")
-  token=$(echo "$response" | grep "SynoToken" | sed -n 's/.*"SynoToken" *: *"\([^"]*\).*/\1/p')
+  encoded_username="$(printf "%s" "$SYNO_Username" | _url_encode)"
+  encoded_password="$(printf "%s" "$SYNO_Password" | _url_encode)"
+  encoded_did="$(printf "%s" "$SYNO_DID" | _url_encode)"
+  response=$(_get "$_base_url/webman/login.cgi?username=$encoded_username&passwd=$encoded_password&enable_syno_token=yes&device_id=$encoded_did" 1)
+  token=$(echo "$response" | grep "X-SYNO-TOKEN:" | sed -n 's/^X-SYNO-TOKEN: \(.*\)$/\1/p' | tr -d "\r\n")
   _debug3 response "$response"
   _debug3 response "$response"
+  _debug token "$token"
 
 
   if [ -z "$token" ]; then
   if [ -z "$token" ]; then
     _err "Unable to authenticate to $SYNO_Hostname:$SYNO_Port using $SYNO_Scheme."
     _err "Unable to authenticate to $SYNO_Hostname:$SYNO_Port using $SYNO_Scheme."
@@ -91,7 +89,7 @@ synology_dsm_deploy() {
     return 1
     return 1
   fi
   fi
 
 
-  _H1="Cookie: $(_syno_get_cookie_data "id"); $(_syno_get_cookie_data "smid")"
+  _H1="Cookie: $(echo "$response" | _syno_get_cookie_data "id"); $(echo "$response" | _syno_get_cookie_data "smid")"
   _H2="X-SYNO-TOKEN: $token"
   _H2="X-SYNO-TOKEN: $token"
   export _H1
   export _H1
   export _H2
   export _H2
@@ -102,7 +100,6 @@ synology_dsm_deploy() {
   _savedeployconf SYNO_Username "$SYNO_Username"
   _savedeployconf SYNO_Username "$SYNO_Username"
   _savedeployconf SYNO_Password "$SYNO_Password"
   _savedeployconf SYNO_Password "$SYNO_Password"
   _savedeployconf SYNO_DID "$SYNO_DID"
   _savedeployconf SYNO_DID "$SYNO_DID"
-  _debug token "$token"
 
 
   _info "Getting certificates in Synology DSM"
   _info "Getting certificates in Synology DSM"
   response=$(_post "api=SYNO.Core.Certificate.CRT&method=list&version=1" "$_base_url/webapi/entry.cgi")
   response=$(_post "api=SYNO.Core.Certificate.CRT&method=list&version=1" "$_base_url/webapi/entry.cgi")
@@ -110,7 +107,7 @@ synology_dsm_deploy() {
   id=$(echo "$response" | sed -n "s/.*\"desc\":\"$SYNO_Certificate\",\"id\":\"\([^\"]*\).*/\1/p")
   id=$(echo "$response" | sed -n "s/.*\"desc\":\"$SYNO_Certificate\",\"id\":\"\([^\"]*\).*/\1/p")
   _debug2 id "$id"
   _debug2 id "$id"
 
 
-  if [ -z "$id" ] && [ -z "${SYNO_Create:?}" ]; then
+  if [ -z "$id" ] && [ -z "${SYNO_Create:-}" ]; then
     _err "Unable to find certificate: $SYNO_Certificate and \$SYNO_Create is not set"
     _err "Unable to find certificate: $SYNO_Certificate and \$SYNO_Create is not set"
     return 1
     return 1
   fi
   fi
@@ -125,11 +122,11 @@ synology_dsm_deploy() {
   _debug2 default "$default"
   _debug2 default "$default"
 
 
   _info "Generate form POST request"
   _info "Generate form POST request"
-  nl="\015\012"
+  nl="\0015\0012"
   delim="--------------------------$(_utc_date | tr -d -- '-: ')"
   delim="--------------------------$(_utc_date | tr -d -- '-: ')"
-  content="--$delim${nl}Content-Disposition: form-data; name=\"key\"; filename=\"$(basename "$_ckey")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")\012"
-  content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"cert\"; filename=\"$(basename "$_ccert")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ccert")\012"
-  content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"inter_cert\"; filename=\"$(basename "$_cca")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_cca")\012"
+  content="--$delim${nl}Content-Disposition: form-data; name=\"key\"; filename=\"$(basename "$_ckey")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")\0012"
+  content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"cert\"; filename=\"$(basename "$_ccert")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ccert")\0012"
+  content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"inter_cert\"; filename=\"$(basename "$_cca")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_cca")\0012"
   content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"id\"${nl}${nl}$id"
   content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"id\"${nl}${nl}$id"
   content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"desc\"${nl}${nl}${SYNO_Certificate}"
   content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"desc\"${nl}${nl}${SYNO_Certificate}"
   content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"as_default\"${nl}${nl}${default}"
   content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"as_default\"${nl}${nl}${default}"

+ 1 - 1
dnsapi/dns_1984hosting.sh

@@ -168,7 +168,7 @@ _1984hosting_login() {
   _debug2 response "$response"
   _debug2 response "$response"
 
 
   if [ "$response" = '{"loggedin": true, "ok": true}' ]; then
   if [ "$response" = '{"loggedin": true, "ok": true}' ]; then
-    One984HOSTING_COOKIE="$(grep '^Set-Cookie:' "$HTTP_HEADER" | _tail_n 1 | _egrep_o 'sessionid=[^;]*;' | tr -d ';')"
+    One984HOSTING_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _tail_n 1 | _egrep_o 'sessionid=[^;]*;' | tr -d ';')"
     export One984HOSTING_COOKIE
     export One984HOSTING_COOKIE
     _saveaccountconf_mutable One984HOSTING_COOKIE "$One984HOSTING_COOKIE"
     _saveaccountconf_mutable One984HOSTING_COOKIE "$One984HOSTING_COOKIE"
     return 0
     return 0

+ 10 - 1
dnsapi/dns_gdnsdk.sh

@@ -157,9 +157,18 @@ _successful_update() {
 }
 }
 
 
 _findentry() {
 _findentry() {
+  #args    $1: fulldomain, $2: txtvalue
   #returns id of dns entry, if it exists
   #returns id of dns entry, if it exists
   _myget "action=dns_primary_changeDNSsetup&user_domain=$_domain"
   _myget "action=dns_primary_changeDNSsetup&user_domain=$_domain"
-  _id=$(echo "$_result" | _egrep_o "<td>$1</td>\s*<td>$2</td>[^?]*[^&]*&id=[^&]*" | sed 's/^.*=//')
+  _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
   if [ -n "$_id" ]; then
     _debug "Entry found with _id=$_id"
     _debug "Entry found with _id=$_id"
     return 0
     return 0

+ 252 - 0
dnsapi/dns_hetzner.sh

@@ -0,0 +1,252 @@
+#!/usr/bin/env sh
+
+#
+#HETZNER_Token="sdfsdfsdfljlbjkljlkjsdfoiwje"
+#
+
+HETZNER_Api="https://dns.hetzner.com/api/v1"
+
+########  Public functions #####################
+
+# Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+# Used to add txt record
+# Ref: https://dns.hetzner.com/api-docs/
+dns_hetzner_add() {
+  full_domain=$1
+  txt_value=$2
+
+  HETZNER_Token="${HETZNER_Token:-$(_readaccountconf_mutable HETZNER_Token)}"
+
+  if [ -z "$HETZNER_Token" ]; then
+    HETZNER_Token=""
+    _err "You didn't specify a Hetzner api token."
+    _err "You can get yours from here https://dns.hetzner.com/settings/api-token."
+    return 1
+  fi
+
+  #save the api key and email to the account conf file.
+  _saveaccountconf_mutable HETZNER_Token "$HETZNER_Token"
+
+  _debug "First detect the root zone"
+
+  if ! _get_root "$full_domain"; then
+    _err "Invalid domain"
+    return 1
+  fi
+  _debug _domain_id "$_domain_id"
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  _debug "Getting TXT records"
+  if ! _find_record "$_sub_domain" "$txt_value"; then
+    return 1
+  fi
+
+  if [ -z "$_record_id" ]; then
+    _info "Adding record"
+    if _hetzner_rest POST "records" "{\"zone_id\":\"${HETZNER_Zone_ID}\",\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":\"$txt_value\",\"ttl\":120}"; then
+      if _contains "$response" "$txt_value"; then
+        _info "Record added, OK"
+        _sleep 2
+        return 0
+      fi
+    fi
+    _err "Add txt record error${_response_error}"
+    return 1
+  else
+    _info "Found record id: $_record_id."
+    _info "Record found, do nothing."
+    return 0
+    # we could modify a record, if the names for txt records for *.example.com and example.com would be not the same
+    #if _hetzner_rest PUT "records/${_record_id}" "{\"zone_id\":\"${HETZNER_Zone_ID}\",\"type\":\"TXT\",\"name\":\"$full_domain\",\"value\":\"$txt_value\",\"ttl\":120}"; then
+    #  if _contains "$response" "$txt_value"; then
+    #    _info "Modified, OK"
+    #    return 0
+    #  fi
+    #fi
+    #_err "Add txt record error (modify)."
+    #return 1
+  fi
+}
+
+# Usage: full_domain txt_value
+# Used to remove the txt record after validation
+dns_hetzner_rm() {
+  full_domain=$1
+  txt_value=$2
+
+  HETZNER_Token="${HETZNER_Token:-$(_readaccountconf_mutable HETZNER_Token)}"
+
+  _debug "First detect the root zone"
+  if ! _get_root "$full_domain"; then
+    _err "Invalid domain"
+    return 1
+  fi
+  _debug _domain_id "$_domain_id"
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  _debug "Getting TXT records"
+  if ! _find_record "$_sub_domain" "$txt_value"; then
+    return 1
+  fi
+
+  if [ -z "$_record_id" ]; then
+    _info "Remove not needed. Record not found."
+  else
+    if ! _hetzner_rest DELETE "records/$_record_id"; then
+      _err "Delete record error${_response_error}"
+      return 1
+    fi
+    _sleep 2
+    _info "Record deleted"
+  fi
+}
+
+####################  Private functions below ##################################
+#returns
+# _record_id=a8d58f22d6931bf830eaa0ec6464bf81  if found; or 1 if error
+_find_record() {
+  unset _record_id
+  _record_name=$1
+  _record_value=$2
+
+  if [ -z "$_record_value" ]; then
+    _record_value='[^"]*'
+  fi
+
+  _debug "Getting all records"
+  _hetzner_rest GET "records?zone_id=${_domain_id}"
+
+  if _response_has_error; then
+    _err "Error${_response_error}"
+    return 1
+  else
+    _record_id=$(
+      echo "$response" \
+        | grep -o "{[^\{\}]*\"name\":\"$_record_name\"[^\}]*}" \
+        | grep "\"value\":\"$_record_value\"" \
+        | while read -r record; do
+          # test for type and
+          if [ -n "$(echo "$record" | _egrep_o '"type":"TXT"')" ]; then
+            echo "$record" | _egrep_o '"id":"[^"]*"' | cut -d : -f 2 | tr -d \"
+            break
+          fi
+        done
+    )
+  fi
+}
+
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+# _domain_id=sdjkglgdfewsdfg
+_get_root() {
+  domain=$1
+  i=1
+  p=1
+
+  domain_without_acme=$(echo "$domain" | cut -d . -f 2-)
+  domain_param_name=$(echo "HETZNER_Zone_ID_for_${domain_without_acme}" | sed 's/[\.\-]/_/g')
+
+  _debug "Reading zone_id for '$domain_without_acme' from config..."
+  HETZNER_Zone_ID=$(_readdomainconf "$domain_param_name")
+  if [ "$HETZNER_Zone_ID" ]; then
+    _debug "Found, using: $HETZNER_Zone_ID"
+    if ! _hetzner_rest GET "zones/${HETZNER_Zone_ID}"; then
+      _debug "Zone with id '$HETZNER_Zone_ID' not exists."
+      _cleardomainconf "$domain_param_name"
+      unset HETZNER_Zone_ID
+    else
+      if _contains "$response" "\"id\":\"$HETZNER_Zone_ID\""; then
+        _domain=$(printf "%s\n" "$response" | _egrep_o '"name":"[^"]*"' | cut -d : -f 2 | tr -d \" | head -n 1)
+        if [ "$_domain" ]; then
+          _cut_length=$((${#domain} - ${#_domain} - 1))
+          _sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cut_length")
+          _domain_id="$HETZNER_Zone_ID"
+          return 0
+        else
+          return 1
+        fi
+      else
+        return 1
+      fi
+    fi
+  fi
+
+  _debug "Trying to get zone id by domain name for '$domain_without_acme'."
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    if [ -z "$h" ]; then
+      #not valid
+      return 1
+    fi
+    _debug h "$h"
+
+    _hetzner_rest GET "zones?name=$h"
+
+    if _contains "$response" "\"name\":\"$h\"" || _contains "$response" '"total_entries":1'; then
+      _domain_id=$(echo "$response" | _egrep_o "\[.\"id\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
+      if [ "$_domain_id" ]; then
+        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+        _domain=$h
+        HETZNER_Zone_ID=$_domain_id
+        _savedomainconf "$domain_param_name" "$HETZNER_Zone_ID"
+        return 0
+      fi
+      return 1
+    fi
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+  return 1
+}
+
+#returns
+# _response_error
+_response_has_error() {
+  unset _response_error
+
+  err_part="$(echo "$response" | _egrep_o '"error":{[^}]*}')"
+
+  if [ -n "$err_part" ]; then
+    err_code=$(echo "$err_part" | _egrep_o '"code":[0-9]+' | cut -d : -f 2)
+    err_message=$(echo "$err_part" | _egrep_o '"message":"[^"]+"' | cut -d : -f 2 | tr -d \")
+
+    if [ -n "$err_code" ] && [ -n "$err_message" ]; then
+      _response_error=" - message: ${err_message}, code: ${err_code}"
+      return 0
+    fi
+  fi
+
+  return 1
+}
+
+#returns
+# response
+_hetzner_rest() {
+  m=$1
+  ep="$2"
+  data="$3"
+  _debug "$ep"
+
+  key_trimmed=$(echo "$HETZNER_Token" | tr -d \")
+
+  export _H1="Content-TType: application/json"
+  export _H2="Auth-API-Token: $key_trimmed"
+
+  if [ "$m" != "GET" ]; then
+    _debug data "$data"
+    response="$(_post "$data" "$HETZNER_Api/$ep" "" "$m")"
+  else
+    response="$(_get "$HETZNER_Api/$ep")"
+  fi
+
+  if [ "$?" != "0" ] || _response_has_error; then
+    _debug "Error$_response_error"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}

+ 14 - 0
dnsapi/dns_inwx.sh

@@ -261,6 +261,20 @@ _get_root() {
   xml_content='<?xml version="1.0" encoding="UTF-8"?>
   xml_content='<?xml version="1.0" encoding="UTF-8"?>
   <methodCall>
   <methodCall>
   <methodName>nameserver.list</methodName>
   <methodName>nameserver.list</methodName>
+  <params>
+   <param>
+    <value>
+     <struct>
+      <member>
+       <name>pagelimit</name>
+       <value>
+        <int>9999</int>
+       </value>
+      </member>
+     </struct>
+    </value>
+   </param>
+  </params>
   </methodCall>'
   </methodCall>'
 
 
   response="$(_post "$xml_content" "$INWX_Api" "" "POST")"
   response="$(_post "$xml_content" "$INWX_Api" "" "POST")"

+ 2 - 2
dnsapi/dns_lexicon.sh

@@ -92,7 +92,7 @@ dns_lexicon_add() {
   _savedomainconf LEXICON_OPTS "$LEXICON_OPTS"
   _savedomainconf LEXICON_OPTS "$LEXICON_OPTS"
 
 
   # shellcheck disable=SC2086
   # shellcheck disable=SC2086
-  $lexicon_cmd "$PROVIDER" $LEXICON_OPTS create "${domain}" TXT --name="_acme-challenge.${domain}." --content="${txtvalue}"
+  $lexicon_cmd "$PROVIDER" $LEXICON_OPTS create "${domain}" TXT --name="_acme-challenge.${domain}." --content="${txtvalue}" --output QUIET 
 
 
 }
 }
 
 
@@ -108,6 +108,6 @@ dns_lexicon_rm() {
   domain=$(printf "%s" "$fulldomain" | cut -d . -f 2-999)
   domain=$(printf "%s" "$fulldomain" | cut -d . -f 2-999)
 
 
   # shellcheck disable=SC2086
   # shellcheck disable=SC2086
-  $lexicon_cmd "$PROVIDER" $LEXICON_OPTS delete "${domain}" TXT --name="_acme-challenge.${domain}." --content="${txtvalue}"
+  $lexicon_cmd "$PROVIDER" $LEXICON_OPTS delete "${domain}" TXT --name="_acme-challenge.${domain}." --content="${txtvalue}" --output QUIET
 
 
 }
 }

+ 168 - 0
dnsapi/dns_njalla.sh

@@ -0,0 +1,168 @@
+#!/usr/bin/env sh
+
+#
+#NJALLA_Token="sdfsdfsdfljlbjkljlkjsdfoiwje"
+
+NJALLA_Api="https://njal.la/api/1/"
+
+########  Public functions #####################
+
+#Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_njalla_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  NJALLA_Token="${NJALLA_Token:-$(_readaccountconf_mutable NJALLA_Token)}"
+
+  if [ "$NJALLA_Token" ]; then
+    _saveaccountconf_mutable NJALLA_Token "$NJALLA_Token"
+  else
+    NJALLA_Token=""
+    _err "You didn't specify a Njalla api token yet."
+    return 1
+  fi
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  # For wildcard cert, the main root domain and the wildcard domain have the same txt subdomain name, so
+  # we can not use updating anymore.
+  #  count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2)
+  #  _debug count "$count"
+  #  if [ "$count" = "0" ]; then
+  _info "Adding record"
+  if _njalla_rest "{\"method\":\"add-record\",\"params\":{\"domain\":\"$_domain\",\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\",\"ttl\":120}}"; then
+    if _contains "$response" "$txtvalue"; then
+      _info "Added, OK"
+      return 0
+    else
+      _err "Add txt record error."
+      return 1
+    fi
+  fi
+  _err "Add txt record error."
+  return 1
+
+}
+
+#fulldomain txtvalue
+dns_njalla_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  NJALLA_Token="${NJALLA_Token:-$(_readaccountconf_mutable NJALLA_Token)}"
+
+  if [ "$NJALLA_Token" ]; then
+    _saveaccountconf_mutable NJALLA_Token "$NJALLA_Token"
+  else
+    NJALLA_Token=""
+    _err "You didn't specify a Njalla api token yet."
+    return 1
+  fi
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  _debug "Getting records for domain"
+  if ! _njalla_rest "{\"method\":\"list-records\",\"params\":{\"domain\":\"${_domain}\"}}"; then
+    return 1
+  fi
+
+  if ! echo "$response" | tr -d " " | grep "\"id\":" >/dev/null; then
+    _err "Error: $response"
+    return 1
+  fi
+
+  records=$(echo "$response" | _egrep_o "\"records\":\s?\[(.*)\]\}" | _egrep_o "\[.*\]" | _egrep_o "\{[^\{\}]*\"id\":[^\{\}]*\}")
+  count=$(echo "$records" | wc -l)
+  _debug count "$count"
+
+  if [ "$count" = "0" ]; then
+    _info "Don't need to remove."
+  else
+    echo "$records" | while read -r record; do
+      record_name=$(echo "$record" | _egrep_o "\"name\":\s?\"[^\"]*\"" | cut -d : -f 2 | tr -d " " | tr -d \")
+      record_content=$(echo "$record" | _egrep_o "\"content\":\s?\"[^\"]*\"" | cut -d : -f 2 | tr -d " " | tr -d \")
+      record_id=$(echo "$record" | _egrep_o "\"id\":\s?[0-9]+" | cut -d : -f 2 | tr -d " " | tr -d \")
+      if [ "$_sub_domain" = "$record_name" ]; then
+        if [ "$txtvalue" = "$record_content" ]; then
+          _debug "record_id" "$record_id"
+          if ! _njalla_rest "{\"method\":\"remove-record\",\"params\":{\"domain\":\"${_domain}\",\"id\":${record_id}}}"; then
+            _err "Delete record error."
+            return 1
+          fi
+          echo "$response" | tr -d " " | grep "\"result\"" >/dev/null
+        fi
+      fi
+    done
+  fi
+
+}
+
+####################  Private functions below ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+# _domain_id=sdjkglgdfewsdfg
+_get_root() {
+  domain=$1
+  i=1
+  p=1
+
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    _debug h "$h"
+    if [ -z "$h" ]; then
+      #not valid
+      return 1
+    fi
+
+    if ! _njalla_rest "{\"method\":\"get-domain\",\"params\":{\"domain\":\"${h}\"}}"; then
+      return 1
+    fi
+
+    if _contains "$response" "\"$h\""; then
+      _domain_returned=$(echo "$response" | _egrep_o "\{\"name\": *\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \" | tr -d " ")
+      if [ "$_domain_returned" ]; then
+        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+        _domain=$h
+        return 0
+      fi
+      return 1
+    fi
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+  return 1
+}
+
+_njalla_rest() {
+  data="$1"
+
+  token_trimmed=$(echo "$NJALLA_Token" | tr -d '"')
+
+  export _H1="Content-Type: application/json"
+  export _H2="Accept: application/json"
+  export _H3="Authorization: Njalla $token_trimmed"
+
+  _debug data "$data"
+  response="$(_post "$data" "$NJALLA_Api" "" "POST")"
+
+  if [ "$?" != "0" ]; then
+    _err "error $data"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}

+ 1 - 1
dnsapi/dns_rackspace.sh

@@ -73,7 +73,7 @@ _get_root_zone() {
       #not valid
       #not valid
       return 1
       return 1
     fi
     fi
-    if ! _rackspace_rest GET "$RACKSPACE_Tenant/domains"; then
+    if ! _rackspace_rest GET "$RACKSPACE_Tenant/domains/search?name=$h"; then
       return 1
       return 1
     fi
     fi
     _debug2 response "$response"
     _debug2 response "$response"

+ 162 - 0
dnsapi/dns_transip.sh

@@ -0,0 +1,162 @@
+#!/usr/bin/env sh
+TRANSIP_Api_Url="https://api.transip.nl/v6"
+TRANSIP_Token_Read_Only="false"
+TRANSIP_Token_Global_Key="false"
+TRANSIP_Token_Expiration="30 minutes"
+# You can't reuse a label token, so we leave this empty normally
+TRANSIP_Token_Label=""
+
+########  Public functions #####################
+#Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_transip_add() {
+  fulldomain="$1"
+  _debug fulldomain="$fulldomain"
+  txtvalue="$2"
+  _debug txtvalue="$txtvalue"
+  _transip_setup "$fulldomain" || return 1
+  _info "Creating TXT record."
+  if ! _transip_rest POST "domains/$_domain/dns" "{\"dnsEntry\":{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"expire\":300}}"; then
+    _err "Could not add TXT record."
+    return 1
+  fi
+  return 0
+}
+
+dns_transip_rm() {
+  fulldomain=$1
+  _debug fulldomain="$fulldomain"
+  txtvalue=$2
+  _debug txtvalue="$txtvalue"
+  _transip_setup "$fulldomain" || return 1
+  _info "Removing TXT record."
+  if ! _transip_rest DELETE "domains/$_domain/dns" "{\"dnsEntry\":{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"expire\":300}}"; then
+    _err "Could not remove TXT record $_sub_domain for $domain"
+    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=2
+  p=1
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+
+    if [ -z "$h" ]; then
+      #not valid
+      return 1
+    fi
+
+    _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+    _domain="$h"
+
+    if _transip_rest GET "domains/$h/dns" && _contains "$response" "dnsEntries"; then
+      return 0
+    fi
+
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+  _err "Unable to parse this domain"
+  return 1
+}
+
+_transip_rest() {
+  m="$1"
+  ep="$2"
+  data="$3"
+  _debug ep "$ep"
+  export _H1="Accept: application/json"
+  export _H2="Authorization: Bearer $_token"
+  export _H4="Content-Type: application/json"
+  if [ "$m" != "GET" ]; then
+    _debug data "$data"
+    response="$(_post "$data" "$TRANSIP_Api_Url/$ep" "" "$m")"
+    retcode=$?
+  else
+    response="$(_get "$TRANSIP_Api_Url/$ep")"
+    retcode=$?
+  fi
+
+  if [ "$retcode" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}
+
+_transip_get_token() {
+  nonce=$(echo "TRANSIP$(_time)" | _digest sha1 hex | cut -c 1-32)
+  _debug nonce "$nonce"
+
+  data="{\"login\":\"${TRANSIP_Username}\",\"nonce\":\"${nonce}\",\"read_only\":\"${TRANSIP_Token_Read_Only}\",\"expiration_time\":\"${TRANSIP_Token_Expiration}\",\"label\":\"${TRANSIP_Token_Label}\",\"global_key\":\"${TRANSIP_Token_Global_Key}\"}"
+  _debug data "$data"
+
+  #_signature=$(printf "%s" "$data" | openssl dgst -sha512 -sign "$TRANSIP_Key_File" | _base64)
+  _signature=$(printf "%s" "$data" | _sign "$TRANSIP_Key_File" "sha512")
+  _debug2 _signature "$_signature"
+
+  export _H1="Signature: $_signature"
+  export _H2="Content-Type: application/json"
+
+  response="$(_post "$data" "$TRANSIP_Api_Url/auth" "" "POST")"
+  retcode=$?
+  _debug2 response "$response"
+  if [ "$retcode" != "0" ]; then
+    _err "Authentication failed."
+    return 1
+  fi
+  if _contains "$response" "token"; then
+    _token="$(echo "$response" | _normalizeJson | sed -n 's/^{"token":"\(.*\)"}/\1/p')"
+    _debug _token "$_token"
+    return 0
+  fi
+  return 1
+}
+
+_transip_setup() {
+  fulldomain=$1
+
+  # retrieve the transip creds
+  TRANSIP_Username="${TRANSIP_Username:-$(_readaccountconf_mutable TRANSIP_Username)}"
+  TRANSIP_Key_File="${TRANSIP_Key_File:-$(_readaccountconf_mutable TRANSIP_Key_File)}"
+  # check their vals for null
+  if [ -z "$TRANSIP_Username" ] || [ -z "$TRANSIP_Key_File" ]; then
+    TRANSIP_Username=""
+    TRANSIP_Key_File=""
+    _err "You didn't specify a TransIP username and api key file location"
+    _err "Please set those values and try again."
+    return 1
+  fi
+  # save the username and api key to the account conf file.
+  _saveaccountconf_mutable TRANSIP_Username "$TRANSIP_Username"
+  _saveaccountconf_mutable TRANSIP_Key_File "$TRANSIP_Key_File"
+
+  if [ -f "$TRANSIP_Key_File" ]; then
+    if ! grep "BEGIN PRIVATE KEY" "$TRANSIP_Key_File" >/dev/null 2>&1; then
+      _err "Key file doesn't seem to be a valid key: ${TRANSIP_Key_File}"
+      return 1
+    fi
+  else
+    _err "Can't read private key file: ${TRANSIP_Key_File}"
+    return 1
+  fi
+
+  if [ -z "$_token" ]; then
+    if ! _transip_get_token; then
+      _err "Can not get token."
+      return 1
+    fi
+  fi
+
+  _get_root "$fulldomain" || return 1
+
+  return 0
+}

+ 86 - 0
notify/teams.sh

@@ -0,0 +1,86 @@
+#!/usr/bin/env sh
+
+#Support Microsoft Teams webhooks
+
+#TEAMS_WEBHOOK_URL=""
+#TEAMS_THEME_COLOR=""
+#TEAMS_SUCCESS_COLOR=""
+#TEAMS_ERROR_COLOR=""
+#TEAMS_SKIP_COLOR=""
+
+teams_send() {
+  _subject="$1"
+  _content="$2"
+  _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped
+  _debug "_statusCode" "$_statusCode"
+
+  _color_success="2cbe4e" # green
+  _color_danger="cb2431"  # red
+  _color_muted="586069"   # gray
+
+  TEAMS_WEBHOOK_URL="${TEAMS_WEBHOOK_URL:-$(_readaccountconf_mutable TEAMS_WEBHOOK_URL)}"
+  if [ -z "$TEAMS_WEBHOOK_URL" ]; then
+    TEAMS_WEBHOOK_URL=""
+    _err "You didn't specify a Microsoft Teams webhook url TEAMS_WEBHOOK_URL yet."
+    return 1
+  fi
+  _saveaccountconf_mutable TEAMS_WEBHOOK_URL "$TEAMS_WEBHOOK_URL"
+
+  TEAMS_THEME_COLOR="${TEAMS_THEME_COLOR:-$(_readaccountconf_mutable TEAMS_THEME_COLOR)}"
+  if [ -n "$TEAMS_THEME_COLOR" ]; then
+    _saveaccountconf_mutable TEAMS_THEME_COLOR "$TEAMS_THEME_COLOR"
+  fi
+
+  TEAMS_SUCCESS_COLOR="${TEAMS_SUCCESS_COLOR:-$(_readaccountconf_mutable TEAMS_SUCCESS_COLOR)}"
+  if [ -n "$TEAMS_SUCCESS_COLOR" ]; then
+    _saveaccountconf_mutable TEAMS_SUCCESS_COLOR "$TEAMS_SUCCESS_COLOR"
+  fi
+
+  TEAMS_ERROR_COLOR="${TEAMS_ERROR_COLOR:-$(_readaccountconf_mutable TEAMS_ERROR_COLOR)}"
+  if [ -n "$TEAMS_ERROR_COLOR" ]; then
+    _saveaccountconf_mutable TEAMS_ERROR_COLOR "$TEAMS_ERROR_COLOR"
+  fi
+
+  TEAMS_SKIP_COLOR="${TEAMS_SKIP_COLOR:-$(_readaccountconf_mutable TEAMS_SKIP_COLOR)}"
+  if [ -n "$TEAMS_SKIP_COLOR" ]; then
+    _saveaccountconf_mutable TEAMS_SKIP_COLOR "$TEAMS_SKIP_COLOR"
+  fi
+
+  export _H1="Content-Type: application/json"
+
+  _subject=$(echo "$_subject" | _json_encode)
+  _content=$(echo "$_content" | _json_encode)
+
+  case "$_statusCode" in
+    0)
+      _color="${TEAMS_SUCCESS_COLOR:-$_color_success}"
+      ;;
+    1)
+      _color="${TEAMS_ERROR_COLOR:-$_color_danger}"
+      ;;
+    2)
+      _color="${TEAMS_SKIP_COLOR:-$_color_muted}"
+      ;;
+  esac
+
+  _color=$(echo "$_color" | tr -cd 'a-fA-F0-9')
+  if [ -z "$_color" ]; then
+    _color=$(echo "${TEAMS_THEME_COLOR:-$_color_muted}" | tr -cd 'a-fA-F0-9')
+  fi
+
+  _data="{\"title\": \"$_subject\","
+  if [ -n "$_color" ]; then
+    _data="$_data\"themeColor\": \"$_color\", "
+  fi
+  _data="$_data\"text\": \"$_content\"}"
+
+  if response=$(_post "$_data" "$TEAMS_WEBHOOK_URL"); then
+    if ! _contains "$response" error; then
+      _info "teams send success."
+      return 0
+    fi
+  fi
+  _err "teams send error."
+  _err "$response"
+  return 1
+}