瀏覽代碼

Merge with latest dev branch.

Martin Kammerlander 7 年之前
父節點
當前提交
c34aadfbf7

+ 1 - 10
.travis.yml

@@ -13,12 +13,6 @@ env:
   global:
     - SHFMT_URL=https://github.com/mvdan/sh/releases/download/v0.4.0/shfmt_v0.4.0_linux_amd64
 
-addons:
-  apt:
-    sources:
-    - debian-sid    # Grab shellcheck from the Debian repo (o_O)
-    packages:
-    - shellcheck
 
 install:
   - if [ "$TRAVIS_OS_NAME" = 'osx' ]; then
@@ -29,9 +23,7 @@ install:
 script:
   - echo "NGROK_TOKEN=$(echo "$NGROK_TOKEN" | wc -c)"
   - command -V openssl && openssl version
-  - if [ "$TRAVIS_OS_NAME" = "linux" ]; then curl -sSL $SHFMT_URL -o ~/shfmt ; fi
-  - if [ "$TRAVIS_OS_NAME" = "linux" ]; then chmod +x ~/shfmt ; fi
-  - if [ "$TRAVIS_OS_NAME" = "linux" ]; then ~/shfmt -l -w -i 2 . ; fi
+  - if [ "$TRAVIS_OS_NAME" = "linux" ]; then curl -sSL $SHFMT_URL -o ~/shfmt && chmod +x ~/shfmt && ~/shfmt -l -w -i 2 . ; fi
   - if [ "$TRAVIS_OS_NAME" = "linux" ]; then git diff --exit-code && echo "shfmt OK" ; fi
   - if [ "$TRAVIS_OS_NAME" = "linux" ]; then shellcheck -V ; fi
   - if [ "$TRAVIS_OS_NAME" = "linux" ]; then shellcheck -e SC2181 **/*.sh && echo "shellcheck OK" ; fi
@@ -40,7 +32,6 @@ script:
   - if [ "$TRAVIS_OS_NAME" = "linux" -a "$NGROK_TOKEN" ]; then sudo TEST_LOCAL="$TEST_LOCAL" NGROK_TOKEN="$NGROK_TOKEN" ./rundocker.sh testplat ubuntu:latest ; fi
   - if [ "$TRAVIS_OS_NAME" = "osx" -a "$NGROK_TOKEN" ]; then sudo TEST_LOCAL="$TEST_LOCAL" NGROK_TOKEN="$NGROK_TOKEN" ACME_OPENSSL_BIN="$ACME_OPENSSL_BIN" ./letest.sh ; fi
 
-
 matrix:
   fast_finish: true
   

+ 1 - 0
Dockerfile

@@ -3,6 +3,7 @@ FROM alpine:3.6
 RUN apk update -f \
   && apk --no-cache add -f \
   openssl \
+  coreutils \
   curl \
   socat \
   && rm -rf /var/cache/apk/*

+ 9 - 3
README.md

@@ -315,8 +315,12 @@ You don't have to do anything manually!
 1. zonomi.com DNS API
 1. DreamHost.com API
 1. DirectAdmin API
-1. All-inkl/Kasserver API
-
+1. KingHost (https://www.kinghost.com.br/)
+1. Zilore (https://zilore.com)
+1. Loopia.se API
+1. acme-dns (https://github.com/joohoi/acme-dns)
+1. TELE3 (https://www.tele3.cz)
+1. All-inkl/Kasserver API (https://all-inkl.com)
 
 And: 
 
@@ -332,6 +336,8 @@ For more details: [How to use DNS API](dnsapi)
 
 # 8. Use DNS manual mode:
 
+See: https://github.com/Neilpang/acme.sh/wiki/dns-manual-mode first.
+
 If your dns provider doesn't support any api access, you can add the txt record by your hand.
 
 ```bash
@@ -401,7 +407,7 @@ Valid values are:
 It's simple, just give a wildcard domain as the `-d` parameter.
 
 ```sh
-acme.sh  --issue -d example.com  -d *.example.com  --dns dns_cf
+acme.sh  --issue -d example.com  -d '*.example.com'  --dns dns_cf
 ```
 
 

+ 79 - 33
acme.sh

@@ -1,6 +1,6 @@
 #!/usr/bin/env sh
 
-VER=2.7.8
+VER=2.7.9
 
 PROJECT_NAME="acme.sh"
 
@@ -110,31 +110,35 @@ _STATELESS_WIKI="https://github.com/Neilpang/acme.sh/wiki/Stateless-Mode"
 
 _DNS_ALIAS_WIKI="https://github.com/Neilpang/acme.sh/wiki/DNS-alias-mode"
 
+_DNS_MANUAL_WIKI="https://github.com/Neilpang/acme.sh/wiki/dns-manual-mode"
+
 _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."
 
 _DNS_MANUAL_WARN="It seems that you are using dns manual mode. please take care: $_DNS_MANUAL_ERR"
 
+_DNS_MANUAL_ERROR="It seems that you are using dns manual mode. Read this link first: $_DNS_MANUAL_WIKI"
+
 __INTERACTIVE=""
 if [ -t 1 ]; then
   __INTERACTIVE="1"
 fi
 
 __green() {
-  if [ "$__INTERACTIVE${ACME_NO_COLOR}" = "1" ]; then
+  if [ "$__INTERACTIVE${ACME_NO_COLOR}" = "1" -o "${ACME_FORCE_COLOR}" = "1" ]; then
     printf '\033[1;31;32m'
   fi
   printf -- "%b" "$1"
-  if [ "$__INTERACTIVE${ACME_NO_COLOR}" = "1" ]; then
+  if [ "$__INTERACTIVE${ACME_NO_COLOR}" = "1" -o "${ACME_FORCE_COLOR}" = "1" ]; then
     printf '\033[0m'
   fi
 }
 
 __red() {
-  if [ "$__INTERACTIVE${ACME_NO_COLOR}" = "1" ]; then
+  if [ "$__INTERACTIVE${ACME_NO_COLOR}" = "1" -o "${ACME_FORCE_COLOR}" = "1" ]; then
     printf '\033[1;31;40m'
   fi
   printf -- "%b" "$1"
-  if [ "$__INTERACTIVE${ACME_NO_COLOR}" = "1" ]; then
+  if [ "$__INTERACTIVE${ACME_NO_COLOR}" = "1" -o "${ACME_FORCE_COLOR}" = "1" ]; then
     printf '\033[0m'
   fi
 }
@@ -1617,6 +1621,7 @@ _post() {
   _debug $httpmethod
   _debug "_post_url" "$_post_url"
   _debug2 "body" "$body"
+  _debug2 "_postContentType" "$_postContentType"
 
   _inithttp
 
@@ -1625,14 +1630,19 @@ _post() {
     if [ "$HTTPS_INSECURE" ]; then
       _CURL="$_CURL --insecure  "
     fi
-    if [ "$_postContentType" ]; then
-      _CURL="$_CURL -H \"Content-Type: $_postContentType\" "
-    fi
     _debug "_CURL" "$_CURL"
     if [ "$needbase64" ]; then
-      response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url" | _base64)"
+      if [ "$_postContentType" ]; then
+        response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "Content-Type: $_postContentType" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url" | _base64)"
+      else
+        response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url" | _base64)"
+      fi
     else
-      response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url")"
+      if [ "$_postContentType" ]; then
+        response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "Content-Type: $_postContentType" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url")"
+      else
+        response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url")"
+      fi
     fi
     _ret="$?"
     if [ "$_ret" != "0" ]; then
@@ -1785,19 +1795,25 @@ _send_signed_request() {
     return 1
   fi
 
+  if [ "$ACME_VERSION" = "2" ]; then
+    __request_conent_type="$CONTENT_TYPE_JSON"
+  else
+    __request_conent_type=""
+  fi
   payload64=$(printf "%s" "$payload" | _base64 | _url_replace)
   _debug3 payload64 "$payload64"
 
   MAX_REQUEST_RETRY_TIMES=5
   _request_retry_times=0
   while [ "${_request_retry_times}" -lt "$MAX_REQUEST_RETRY_TIMES" ]; do
+    _request_retry_times=$(_math "$_request_retry_times" + 1)
     _debug3 _request_retry_times "$_request_retry_times"
     if [ -z "$_CACHED_NONCE" ]; then
       _headers=""
       if [ "$ACME_NEW_NONCE" ]; then
         _debug2 "Get nonce. ACME_NEW_NONCE" "$ACME_NEW_NONCE"
         nonceurl="$ACME_NEW_NONCE"
-        if _post "" "$nonceurl" "" "HEAD" "$CONTENT_TYPE_JSON"; then
+        if _post "" "$nonceurl" "" "HEAD" "$__request_conent_type"; then
           _headers="$(cat "$HTTP_HEADER")"
         fi
       fi
@@ -1821,7 +1837,11 @@ _send_signed_request() {
     fi
     nonce="$_CACHED_NONCE"
     _debug2 nonce "$nonce"
-
+    if [ -z "$nonce" ]; then
+      _info "Could not get nonce, let's try again."
+      _sleep 2
+      continue
+    fi
     if [ "$ACME_VERSION" = "2" ]; then
       if [ "$url" = "$ACME_NEW_ACCOUNT" ] || [ "$url" = "$ACME_REVOKE_CERT" ]; then
         protected="$JWK_HEADERPLACE_PART1$nonce\", \"url\": \"${url}$JWK_HEADERPLACE_PART2, \"jwk\": $jwk"'}'
@@ -1852,7 +1872,7 @@ _send_signed_request() {
     fi
     _debug3 body "$body"
 
-    response="$(_post "$body" "$url" "$needbase64" "POST" "$CONTENT_TYPE_JSON")"
+    response="$(_post "$body" "$url" "$needbase64" "POST" "$__request_conent_type")"
     _CACHED_NONCE=""
 
     if [ "$?" != "0" ]; then
@@ -1879,7 +1899,6 @@ _send_signed_request() {
 
     if _contains "$_body" "JWS has invalid anti-replay nonce"; then
       _info "It seems the CA server is busy now, let's wait and retry."
-      _request_retry_times=$(_math "$_request_retry_times" + 1)
       _sleep 5
       continue
     fi
@@ -3247,10 +3266,16 @@ _regAccount() {
     return 1
   fi
 
+  _debug2 responseHeaders "$responseHeaders"
   _accUri="$(echo "$responseHeaders" | grep "^Location:" | _head_n 1 | cut -d ' ' -f 2 | tr -d "\r\n")"
   _debug "_accUri" "$_accUri"
+  if [ -z "$_accUri" ]; then
+    _err "Can not find account id url."
+    _err "$responseHeaders"
+    return 1
+  fi
   _savecaconf "ACCOUNT_URL" "$_accUri"
-  export ACCOUNT_URL="$ACCOUNT_URL"
+  export ACCOUNT_URL="$_accUri"
 
   CA_KEY_HASH="$(__calcAccountKeyHash)"
   _debug "Calc CA_KEY_HASH" "$CA_KEY_HASH"
@@ -3460,6 +3485,11 @@ issue() {
     mkdir -p "$DOMAIN_PATH"
   fi
 
+  if _hasfield "$_web_roots" "$W_DNS" && [ -z "$FORCE_DNS_MANUAL" ]; then
+    _err "$_DNS_MANUAL_ERROR"
+    return 1
+  fi
+
   _debug "Using ACME_DIRECTORY: $ACME_DIRECTORY"
 
   _initAPI
@@ -3521,7 +3551,7 @@ issue() {
   _saved_account_key_hash="$(_readcaconf "CA_KEY_HASH")"
   _debug2 _saved_account_key_hash "$_saved_account_key_hash"
 
-  if [ -z "$_saved_account_key_hash" ] || [ "$_saved_account_key_hash" != "$(__calcAccountKeyHash)" ]; then
+  if [ -z "$ACCOUNT_URL" ] || [ -z "$_saved_account_key_hash" ] || [ "$_saved_account_key_hash" != "$(__calcAccountKeyHash)" ]; then
     if ! _regAccount "$_accountkeylength"; then
       _on_issue_err "$_post_hook"
       return 1
@@ -4083,13 +4113,15 @@ $_authorizations_map"
     fi
     if [ "$code" != "200" ]; then
       _err "Sign failed, code is not 200."
+      _err "$response"
       _on_issue_err "$_post_hook"
       return 1
     fi
     Le_LinkCert="$(echo "$response" | tr -d '\r\n' | _egrep_o '"certificate" *: *"[^"]*"' | cut -d '"' -f 4)"
 
     if ! _get "$Le_LinkCert" >"$CERT_PATH"; then
-      _err "Sign failed, code is not 200."
+      _err "Sign failed, can not download cert:$Le_LinkCert."
+      _err "$response"
       _on_issue_err "$_post_hook"
       return 1
     fi
@@ -4105,12 +4137,12 @@ $_authorizations_map"
     fi
   else
     if ! _send_signed_request "${ACME_NEW_ORDER}" "{\"resource\": \"$ACME_NEW_ORDER_RES\", \"csr\": \"$der\"}" "needbase64"; then
-      _err "Sign failed."
+      _err "Sign failed. $response"
       _on_issue_err "$_post_hook"
       return 1
     fi
     _rcert="$response"
-    Le_LinkCert="$(grep -i '^Location.*$' "$HTTP_HEADER" | _head_n 1 | tr -d "\r\n" | cut -d " " -f 2)"
+    Le_LinkCert="$(grep -i '^Location.*$' "$HTTP_HEADER" | _tail_n 1 | tr -d "\r\n" | cut -d " " -f 2)"
     echo "$BEGIN_CERT" >"$CERT_PATH"
 
     #if ! _get "$Le_LinkCert" | _base64 "multiline"  >> "$CERT_PATH" ; then
@@ -4255,20 +4287,21 @@ $_authorizations_map"
   Le_NextRenewTime=$(_math "$Le_NextRenewTime" - 86400)
   _savedomainconf "Le_NextRenewTime" "$Le_NextRenewTime"
 
-  if ! _on_issue_success "$_post_hook" "$_renew_hook"; then
-    _err "Call hook error."
-    return 1
-  fi
-
   if [ "$_real_cert$_real_key$_real_ca$_reload_cmd$_real_fullchain" ]; then
     _savedomainconf "Le_RealCertPath" "$_real_cert"
     _savedomainconf "Le_RealCACertPath" "$_real_ca"
     _savedomainconf "Le_RealKeyPath" "$_real_key"
     _savedomainconf "Le_ReloadCmd" "$_reload_cmd"
     _savedomainconf "Le_RealFullChainPath" "$_real_fullchain"
-    _installcert "$_main_domain" "$_real_cert" "$_real_key" "$_real_ca" "$_real_fullchain" "$_reload_cmd"
+    if ! _installcert "$_main_domain" "$_real_cert" "$_real_key" "$_real_ca" "$_real_fullchain" "$_reload_cmd"; then
+      return 1
+    fi
   fi
 
+  if ! _on_issue_success "$_post_hook" "$_renew_hook"; then
+    _err "Call hook error."
+    return 1
+  fi
 }
 
 #domain  [isEcc]
@@ -4643,19 +4676,19 @@ _installcert() {
     if [ -f "$_real_cert" ] && [ ! "$IS_RENEW" ]; then
       cp "$_real_cert" "$_backup_path/cert.bak"
     fi
-    cat "$CERT_PATH" >"$_real_cert"
+    cat "$CERT_PATH" >"$_real_cert" || return 1
   fi
 
   if [ "$_real_ca" ]; then
     _info "Installing CA to:$_real_ca"
     if [ "$_real_ca" = "$_real_cert" ]; then
       echo "" >>"$_real_ca"
-      cat "$CA_CERT_PATH" >>"$_real_ca"
+      cat "$CA_CERT_PATH" >>"$_real_ca" || return 1
     else
       if [ -f "$_real_ca" ] && [ ! "$IS_RENEW" ]; then
         cp "$_real_ca" "$_backup_path/ca.bak"
       fi
-      cat "$CA_CERT_PATH" >"$_real_ca"
+      cat "$CA_CERT_PATH" >"$_real_ca" || return 1
     fi
   fi
 
@@ -4665,9 +4698,9 @@ _installcert() {
       cp "$_real_key" "$_backup_path/key.bak"
     fi
     if [ -f "$_real_key" ]; then
-      cat "$CERT_KEY_PATH" >"$_real_key"
+      cat "$CERT_KEY_PATH" >"$_real_key" || return 1
     else
-      cat "$CERT_KEY_PATH" >"$_real_key"
+      cat "$CERT_KEY_PATH" >"$_real_key" || return 1
       chmod 600 "$_real_key"
     fi
   fi
@@ -4677,7 +4710,7 @@ _installcert() {
     if [ -f "$_real_fullchain" ] && [ ! "$IS_RENEW" ]; then
       cp "$_real_fullchain" "$_backup_path/fullchain.bak"
     fi
-    cat "$CERT_FULLCHAIN_PATH" >"$_real_fullchain"
+    cat "$CERT_FULLCHAIN_PATH" >"$_real_fullchain" || return 1
   fi
 
   if [ "$_reload_cmd" ]; then
@@ -5456,8 +5489,8 @@ Parameters:
   --cert-home                       Specifies the home dir to save all the certs, only valid for '--install' command.
   --config-home                     Specifies the home dir to save all the configurations.
   --useragent                       Specifies the user agent string. it will be saved for future use too.
-  --accountemail                    Specifies the account email for registering, Only valid for the '--install' command.
-  --accountkey                      Specifies the account key path, Only valid for the '--install' command.
+  --accountemail                    Specifies the account email, only valid for the '--install' and '--update-account' command.
+  --accountkey                      Specifies the account key path, only valid for the '--install' command.
   --days                            Specifies the days to renew the cert when using '--issue' command. The max value is $MAX_RENEW days.
   --httpport                        Specifies the standalone listening port. Only valid if the server is behind a reverse proxy or load balancer.
   --local-address                   Specifies the standalone/tls server listening address, in case you have multiple ip addresses.
@@ -5468,6 +5501,7 @@ Parameters:
   --ca-path                         Specifies directory containing CA certificates in PEM format, used by wget or curl.
   --nocron                          Only valid for '--install' command, which means: do not install the default cron job. In this case, the certs will not be renewed automatically.
   --no-color                        Do not output color text.
+  --force-color                     Force output of color text. Useful for non-interactive use with the aha tool for HTML E-Mails.
   --ecc                             Specifies to use the ECC cert. Valid for '--install-cert', '--renew', '--revoke', '--toPkcs' and '--createCSR'
   --csr                             Specifies the input csr.
   --pre-hook                        Command to be run before obtaining any certificates.
@@ -5481,6 +5515,8 @@ Parameters:
   --listen-v6                       Force standalone/tls server to listen at ipv6.
   --openssl-bin                     Specifies a custom openssl bin location.
   --use-wget                        Force to use wget, if you have both curl and wget installed.
+  --yes-I-know-dns-manual-mode-enough-go-ahead-please  Force to use dns manual mode: $_DNS_MANUAL_WIKI
+  --branch, -b                      Only valid for '--upgrade' command, specifies the branch name to upgrade to.
   "
 }
 
@@ -5931,6 +5967,9 @@ _process() {
       --no-color)
         export ACME_NO_COLOR=1
         ;;
+      --force-color)
+        export ACME_FORCE_COLOR=1
+        ;;
       --ecc)
         _ecc="isEcc"
         ;;
@@ -5969,6 +6008,9 @@ _process() {
           shift
         fi
         ;;
+      --yes-I-know-dns-manual-mode-enough-go-ahead-please)
+        export FORCE_DNS_MANUAL=1
+        ;;
       --log | --logfile)
         _log="1"
         _logfile="$2"
@@ -6022,6 +6064,10 @@ _process() {
         _use_wget="1"
         ACME_USE_WGET="1"
         ;;
+      --branch | -b)
+        export BRANCH="$2"
+        shift
+        ;;
       *)
         _err "Unknown parameter : $1"
         return 1

+ 0 - 6
deploy/keychain.sh

@@ -1,11 +1,5 @@
 #!/usr/bin/env sh
 
-#Here is a sample custom api script.
-#This file name is "myapi.sh"
-#So, here must be a method   myapi_deploy()
-#Which will be called by acme.sh to deploy the cert
-#returns 0 means success, otherwise error.
-
 ########  Public functions #####################
 
 #domain keyfile certfile cafile fullchain

+ 1 - 0
deploy/vault_cli.sh

@@ -51,6 +51,7 @@ vault_cli_deploy() {
 
   $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}/cert.pem" value=@"$_ccert" || return 1
   $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}/cert.key" value=@"$_ckey" || return 1
+  $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}/chain.pem" value=@"$_cca" || return 1
   $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}/fullchain.pem" value=@"$_cfullchain" || return 1
 
 }

+ 97 - 4
dnsapi/README.md

@@ -1,5 +1,9 @@
 # How to use DNS API
 
+If your dns provider doesn't provide api access, you can use our dns alias mode: 
+
+https://github.com/Neilpang/acme.sh/wiki/DNS-alias-mode
+
 ## 1. Use CloudFlare domain API to automatically issue cert
 
 First you need to login to your CloudFlare account to get your API key.
@@ -325,6 +329,8 @@ The `CY_Username`, `CY_Password` and `CY_OTP_Secret` will be saved in `~/.acme.s
 
 ## 17. Use Domain-Offensive/Resellerinterface/Domainrobot API
 
+ATTENTION: You need to be a registered Reseller to be able to use the ResellerInterface. As a normal user you can not use this method.
+
 You will need your login credentials (Partner ID+Password) to the Resellerinterface, and export them before you run `acme.sh`:
 ```
 export DO_PID="KD-1234567"
@@ -525,8 +531,9 @@ For issues, please report to https://github.com/raidenii/acme.sh/issues.
 
 ## 28. Use Name.com API
 
-You'll need to fill out the form at https://www.name.com/reseller/apply to apply
-for API username and token.
+Create your API token here: https://www.name.com/account/settings/api
+
+Note: `Namecom_Username` should be your Name.com username and not the token name.  If you accidentally run the script with the token name as the username see `~/.acme.sh/account.conf` to fix the issue
 
 ```
 export Namecom_Username="testuser"
@@ -638,6 +645,14 @@ acme.sh --issue --dns dns_inwx -d example.com -d www.example.com
 
 The `INWX_User` and `INWX_Password` settings will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
 
+If your account is secured by mobile tan you have also defined the shared secret.
+
+```
+export INWX_Shared_Secret="shared secret"
+```
+
+You may need to re-enable the mobile tan to gain the shared secret.
+
 ## 34. User Servercow API v1
 
 Create a new user from the servercow control center. Don't forget to activate **DNS API** for this user.
@@ -750,7 +765,7 @@ DNS API keys may be created at https://panel.dreamhost.com/?tree=home.api.
 Ensure the created key has add and remove privelages.
 
 ```
-export DH_API_Key="<api key>"
+export DH_API_KEY="<api key>"
 acme.sh --issue --dns dns_dreamhost -d example.com -d www.example.com
 ```
 
@@ -784,7 +799,85 @@ acme.sh --issue --dns dns_da -d example.com -d www.example.com
 
 The `DA_Api` and `DA_Api_Insecure` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
 
-## 42. Use All-inkl Kasserver API
+## 42. Use KingHost DNS API
+
+API access must be enabled at https://painel.kinghost.com.br/painel.api.php
+
+```
+export KINGHOST_Username="yourusername"
+export KINGHOST_Password="yourpassword"
+acme.sh --issue --dns dns_kinghost -d example.com -d *.example.com
+```
+
+The `KINGHOST_username` and `KINGHOST_Password` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 43. Use Zilore DNS API
+
+First, get your API key at https://my.zilore.com/account/api
+
+```
+export Zilore_Key="5dcad3a2-36cb-50e8-cb92-000002f9"
+```
+
+Ok, let's issue a cert now:
+```
+acme.sh --issue --dns dns_zilore -d example.com -d *.example.com
+```
+
+The `Zilore_Key` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 44. Use Loopia.se API
+User must provide login credentials to the Loopia API.
+The user needs the following permissions:
+
+- addSubdomain
+- updateZoneRecord
+- getDomains
+- removeSubdomain
+
+Set the login credentials:
+```
+export LOOPIA_User="user@loopiaapi"
+export LOOPIA_Password="password"
+```
+
+And to issue a cert:
+```
+acme.sh --issue --dns dns_loopia -d example.com -d *.example.com
+```
+
+The username and password will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+## 45. Use ACME DNS API
+
+ACME DNS is a limited DNS server with RESTful HTTP API to handle ACME DNS challenges easily and securely. 
+https://github.com/joohoi/acme-dns
+
+```
+export ACMEDNS_UPDATE_URL="https://auth.acme-dns.io/update"
+export ACMEDNS_USERNAME="<username>"
+export ACMEDNS_PASSWORD="<password>"
+export ACMEDNS_SUBDOMAIN="<subdomain>"
+
+acme.sh --issue --dns dns_acmedns -d example.com -d www.example.com
+```
+
+The credentials will be saved in `~/.acme.sh/account.conf` and will
+be reused when needed.
+## 46. Use TELE3 API
+
+First you need to login to your TELE3 account to set your API-KEY.
+https://www.tele3.cz/system-acme-api.html
+
+```
+export TELE3_Key="MS2I4uPPaI..."
+export TELE3_Secret="kjhOIHGJKHg"
+
+acme.sh --issue --dns dns_tele3 -d example.com -d *.example.com
+```
+
+The TELE3_Key and TELE3_Secret will be saved in ~/.acme.sh/account.conf and will be reused when needed.
+
+## 47. Use All-inkl Kasserver API
 
 All-inkl Kasserver API (https://kasapi.kasserver.com/dokumentation) needs you to set your Login credentials like so:
 

+ 55 - 0
dnsapi/dns_acmedns.sh

@@ -0,0 +1,55 @@
+#!/usr/bin/env sh
+#
+#Author: Wolfgang Ebner
+#Report Bugs here: https://github.com/webner/acme.sh
+#
+########  Public functions #####################
+
+#Usage: dns_acmedns_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_acmedns_add() {
+  fulldomain=$1
+  txtvalue=$2
+  _info "Using acme-dns"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  ACMEDNS_UPDATE_URL="${ACMEDNS_UPDATE_URL:-$(_readaccountconf_mutable ACMEDNS_UPDATE_URL)}"
+  ACMEDNS_USERNAME="${ACMEDNS_USERNAME:-$(_readaccountconf_mutable ACMEDNS_USERNAME)}"
+  ACMEDNS_PASSWORD="${ACMEDNS_PASSWORD:-$(_readaccountconf_mutable ACMEDNS_PASSWORD)}"
+  ACMEDNS_SUBDOMAIN="${ACMEDNS_SUBDOMAIN:-$(_readaccountconf_mutable ACMEDNS_SUBDOMAIN)}"
+
+  if [ "$ACMEDNS_UPDATE_URL" = "" ]; then
+    ACMEDNS_UPDATE_URL="https://auth.acme-dns.io/update"
+  fi
+
+  _saveaccountconf_mutable ACMEDNS_UPDATE_URL "$ACMEDNS_UPDATE_URL"
+  _saveaccountconf_mutable ACMEDNS_USERNAME "$ACMEDNS_USERNAME"
+  _saveaccountconf_mutable ACMEDNS_PASSWORD "$ACMEDNS_PASSWORD"
+  _saveaccountconf_mutable ACMEDNS_SUBDOMAIN "$ACMEDNS_SUBDOMAIN"
+
+  export _H1="X-Api-User: $ACMEDNS_USERNAME"
+  export _H2="X-Api-Key: $ACMEDNS_PASSWORD"
+  data="{\"subdomain\":\"$ACMEDNS_SUBDOMAIN\", \"txt\": \"$txtvalue\"}"
+
+  _debug data "$data"
+  response="$(_post "$data" "$ACMEDNS_UPDATE_URL" "" "POST")"
+  _debug response "$response"
+
+  if ! echo "$response" | grep "\"$txtvalue\"" >/dev/null; then
+    _err "invalid response of acme-dns"
+    return 1
+  fi
+
+}
+
+#Usage: fulldomain txtvalue
+#Remove the txt record after validation.
+dns_acmedns_rm() {
+  fulldomain=$1
+  txtvalue=$2
+  _info "Using acme-dns"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+}
+
+####################  Private functions below ##################################

+ 12 - 7
dnsapi/dns_azure.sh

@@ -76,10 +76,10 @@ dns_azure_add() {
   values="{\"value\":[\"$txtvalue\"]}"
   timestamp="$(_time)"
   if [ "$_code" = "200" ]; then
-    vlist="$(echo "$response" | _egrep_o "\"value\"\s*:\s*\[\s*\"[^\"]*\"\s*]" | cut -d : -f 2 | tr -d "[]\"")"
+    vlist="$(echo "$response" | _egrep_o "\"value\"\\s*:\\s*\\[\\s*\"[^\"]*\"\\s*]" | cut -d : -f 2 | tr -d "[]\"")"
     _debug "existing TXT found"
     _debug "$vlist"
-    existingts="$(echo "$response" | _egrep_o "\"acmetscheck\"\s*:\s*\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d "\"")"
+    existingts="$(echo "$response" | _egrep_o "\"acmetscheck\"\\s*:\\s*\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d "\"")"
     if [ -z "$existingts" ]; then
       # the record was not created by acme.sh. Copy the exisiting entires
       existingts=$timestamp
@@ -172,7 +172,7 @@ dns_azure_rm() {
   _azure_rest GET "$acmeRecordURI" "" "$accesstoken"
   timestamp="$(_time)"
   if [ "$_code" = "200" ]; then
-    vlist="$(echo "$response" | _egrep_o "\"value\"\s*:\s*\[\s*\"[^\"]*\"\s*]" | cut -d : -f 2 | tr -d "[]\"" | grep -v "$txtvalue")"
+    vlist="$(echo "$response" | _egrep_o "\"value\"\\s*:\\s*\\[\\s*\"[^\"]*\"\\s*]" | cut -d : -f 2 | tr -d "[]\"" | grep -v "$txtvalue")"
     values=""
     comma=""
     for v in $vlist; do
@@ -230,7 +230,7 @@ _azure_rest() {
     fi
     _ret="$?"
     _secure_debug2 "response $response"
-    _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
+    _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
     _debug "http response code $_code"
     if [ "$_code" = "401" ]; then
       # we have an invalid access token set to expired
@@ -308,7 +308,7 @@ _get_root() {
   domain=$1
   subscriptionId=$2
   accesstoken=$3
-  i=2
+  i=1
   p=1
 
   ## Ref: https://docs.microsoft.com/en-us/rest/api/dns/zones/list
@@ -328,9 +328,14 @@ _get_root() {
     fi
 
     if _contains "$response" "\"name\":\"$h\"" >/dev/null; then
-      _domain_id=$(echo "$response" | _egrep_o "\{\"id\":\"[^\"]*$h\"" | head -n 1 | cut -d : -f 2 | tr -d \")
+      _domain_id=$(echo "$response" | _egrep_o "\\{\"id\":\"[^\"]*$h\"" | head -n 1 | cut -d : -f 2 | tr -d \")
       if [ "$_domain_id" ]; then
-        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+        if [ "$i" = 1 ]; then
+          #create the record at the domain apex (@) if only the domain name was provided as --domain-alias
+          _sub_domain="@"
+        else
+          _sub_domain=$(echo "$domain" | cut -d . -f 1-$p)
+        fi
         _domain=$h
         return 0
       fi

+ 18 - 35
dnsapi/dns_dnsimple.sh

@@ -39,34 +39,17 @@ dns_dnsimple_add() {
 
   _get_records "$_account_id" "$_domain" "$_sub_domain"
 
-  if [ "$_records_count" = "0" ]; then
-    _info "Adding record"
-    if _dnsimple_rest POST "$_account_id/zones/$_domain/records" "{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\",\"ttl\":120}"; then
-      if printf -- "%s" "$response" | grep "\"name\":\"$_sub_domain\"" >/dev/null; then
-        _info "Added"
-        return 0
-      else
-        _err "Unexpected response while adding text record."
-        return 1
-      fi
-    fi
-    _err "Add txt record error."
-  else
-    _info "Updating record"
-    _extract_record_id "$_records" "$_sub_domain"
-
-    if _dnsimple_rest \
-      PATCH \
-      "$_account_id/zones/$_domain/records/$_record_id" \
-      "{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\",\"ttl\":120}"; then
-
-      _info "Updated!"
+  _info "Adding record"
+  if _dnsimple_rest POST "$_account_id/zones/$_domain/records" "{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\",\"ttl\":120}"; then
+    if printf -- "%s" "$response" | grep "\"name\":\"$_sub_domain\"" >/dev/null; then
+      _info "Added"
       return 0
+    else
+      _err "Unexpected response while adding text record."
+      return 1
     fi
-
-    _err "Update error"
-    return 1
   fi
+  _err "Add txt record error."
 }
 
 # fulldomain
@@ -84,19 +67,19 @@ dns_dnsimple_rm() {
   fi
 
   _get_records "$_account_id" "$_domain" "$_sub_domain"
-  _extract_record_id "$_records" "$_sub_domain"
 
+  _extract_record_id "$_records" "$_sub_domain"
   if [ "$_record_id" ]; then
-
-    if _dnsimple_rest DELETE "$_account_id/zones/$_domain/records/$_record_id"; then
-      _info "removed record" "$_record_id"
-      return 0
-    fi
+    echo "$_record_id" | while read -r item; do
+      if _dnsimple_rest DELETE "$_account_id/zones/$_domain/records/$item"; then
+        _info "removed record" "$item"
+        return 0
+      else
+        _err "failed to remove record" "$item"
+        return 1
+      fi
+    done
   fi
-
-  _err "failed to remove record" "$_record_id"
-  return 1
-
 }
 
 ####################  Private functions bellow ##################################

+ 1 - 1
dnsapi/dns_freedns.sh

@@ -279,7 +279,7 @@ _freedns_add_txt_record() {
   domain_id="$2"
   subdomain="$3"
   value="$(printf '%s' "$4" | _url_encode)"
-  url="http://freedns.afraid.org/subdomain/save.php?step=2"
+  url="https://freedns.afraid.org/subdomain/save.php?step=2"
 
   htmlpage="$(_post "type=TXT&domain_id=$domain_id&subdomain=$subdomain&address=%22$value%22&send=Save%21" "$url")"
 

+ 5 - 11
dnsapi/dns_gd.sh

@@ -59,19 +59,13 @@ dns_gd_add() {
 
   _info "Adding record"
   if _gd_rest PUT "domains/$_domain/records/TXT/$_sub_domain" "[$_add_data]"; then
-    if [ "$response" = "{}" ]; then
-      _info "Added, sleeping 10 seconds"
-      _sleep 10
-      #todo: check if the record takes effect
-      return 0
-    else
-      _err "Add txt record error."
-      _err "$response"
-      return 1
-    fi
+    _info "Added, sleeping 10 seconds"
+    _sleep 10
+    #todo: check if the record takes effect
+    return 0
   fi
   _err "Add txt record error."
-
+  return 1
 }
 
 #fulldomain

+ 14 - 5
dnsapi/dns_he.sh

@@ -33,8 +33,9 @@ dns_he_add() {
   # Fills in the $_zone_id
   _find_zone "$_full_domain" || return 1
   _debug "Zone id \"$_zone_id\" will be used."
-
-  body="email=${HE_Username}&pass=${HE_Password}"
+  username_encoded="$(printf "%s" "${HE_Username}" | _url_encode)"
+  password_encoded="$(printf "%s" "${HE_Password}" | _url_encode)"
+  body="email=${username_encoded}&pass=${password_encoded}"
   body="$body&account="
   body="$body&menu=edit_zone"
   body="$body&Type=TXT"
@@ -71,7 +72,9 @@ dns_he_rm() {
   _debug "Zone id \"$_zone_id\" will be used."
 
   # Find the record id to clean
-  body="email=${HE_Username}&pass=${HE_Password}"
+  username_encoded="$(printf "%s" "${HE_Username}" | _url_encode)"
+  password_encoded="$(printf "%s" "${HE_Password}" | _url_encode)"
+  body="email=${username_encoded}&pass=${password_encoded}"
   body="$body&hosted_dns_zoneid=$_zone_id"
   body="$body&menu=edit_zone"
   body="$body&hosted_dns_editzone="
@@ -112,9 +115,15 @@ dns_he_rm() {
 
 _find_zone() {
   _domain="$1"
-  body="email=${HE_Username}&pass=${HE_Password}"
+  username_encoded="$(printf "%s" "${HE_Username}" | _url_encode)"
+  password_encoded="$(printf "%s" "${HE_Password}" | _url_encode)"
+  body="email=${username_encoded}&pass=${password_encoded}"
   response="$(_post "$body" "https://dns.he.net/")"
   _debug2 response "$response"
+  if _contains "$response" '>Incorrect<'; then
+    _err "Unable to login to dns.he.net please check username and password"
+    return 1
+  fi
   _table="$(echo "$response" | tr -d "#" | sed "s/<table/#<table/g" | tr -d "\n" | tr "#" "\n" | grep 'id="domains_table"')"
   _debug2 _table "$_table"
   _matches="$(echo "$_table" | sed "s/<tr/#<tr/g" | tr "#" "\n" | grep 'alt="edit"' | tr -d " " | sed "s/<td/#<td/g" | tr "#" "\n" | grep 'hosted_dns_zoneid')"
@@ -143,7 +152,7 @@ _find_zone() {
 
     _debug "Looking for zone \"${_attempted_zone}\""
 
-    line_num="$(echo "$_zone_names" | grep -n "$_attempted_zone" | cut -d : -f 1)"
+    line_num="$(echo "$_zone_names" | grep -n "^$_attempted_zone" | cut -d : -f 1)"
 
     if [ "$line_num" ]; then
       _zone_id=$(echo "$_zone_ids" | sed -n "${line_num}p")

+ 47 - 3
dnsapi/dns_inwx.sh

@@ -4,6 +4,10 @@
 #INWX_User="username"
 #
 #INWX_Password="password"
+#
+# Dependencies:
+# -------------
+# - oathtool (When using 2 Factor Authentication)
 
 INWX_Api="https://api.domrobot.com/xmlrpc/"
 
@@ -16,6 +20,7 @@ dns_inwx_add() {
 
   INWX_User="${INWX_User:-$(_readaccountconf_mutable INWX_User)}"
   INWX_Password="${INWX_Password:-$(_readaccountconf_mutable INWX_Password)}"
+  INWX_Shared_Secret="${INWX_Shared_Secret:-$(_readaccountconf_mutable INWX_Shared_Secret)}"
   if [ -z "$INWX_User" ] || [ -z "$INWX_Password" ]; then
     INWX_User=""
     INWX_Password=""
@@ -27,6 +32,7 @@ dns_inwx_add() {
   #save the api key and email to the account conf file.
   _saveaccountconf_mutable INWX_User "$INWX_User"
   _saveaccountconf_mutable INWX_Password "$INWX_Password"
+  _saveaccountconf_mutable INWX_Shared_Secret "$INWX_Shared_Secret"
 
   _debug "First detect the root zone"
   if ! _get_root "$fulldomain"; then
@@ -148,8 +154,46 @@ _inwx_login() {
   </methodCall>' $INWX_User $INWX_Password)
 
   response="$(_post "$xml_content" "$INWX_Api" "" "POST")"
+  _H1=$(printf "Cookie: %s" "$(grep "domrobot=" "$HTTP_HEADER" | grep "^Set-Cookie:" | _tail_n 1 | _egrep_o 'domrobot=[^;]*;' | tr -d ';')")
+  export _H1
 
-  printf "Cookie: %s" "$(grep "domrobot=" "$HTTP_HEADER" | grep "^Set-Cookie:" | _tail_n 1 | _egrep_o 'domrobot=[^;]*;' | tr -d ';')"
+  #https://github.com/inwx/php-client/blob/master/INWX/Domrobot.php#L71
+  if _contains "$response" "tfa"; then
+    if [ -z "$INWX_Shared_Secret" ]; then
+      _err "Mobile TAN detected."
+      _err "Please define a shared secret."
+      return 1
+    fi
+
+    if ! _exists oathtool; then
+      _err "Please install oathtool to use 2 Factor Authentication."
+      _err ""
+      return 1
+    fi
+
+    tan="$(oathtool --base32 --totp "${INWX_Shared_Secret}" 2>/dev/null)"
+
+    xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
+    <methodCall>
+    <methodName>account.unlock</methodName>
+    <params>
+     <param>
+      <value>
+       <struct>
+        <member>
+         <name>tan</name>
+         <value>
+          <string>%s</string>
+         </value>
+        </member>
+       </struct>
+      </value>
+     </param>
+    </params>
+    </methodCall>' "$tan")
+
+    response="$(_post "$xml_content" "$INWX_Api" "" "POST")"
+  fi
 
 }
 
@@ -161,8 +205,8 @@ _get_root() {
   i=2
   p=1
 
-  _H1=$(_inwx_login)
-  export _H1
+  _inwx_login
+
   xml_content='<?xml version="1.0" encoding="UTF-8"?>
   <methodCall>
   <methodName>nameserver.list</methodName>

+ 107 - 0
dnsapi/dns_kinghost.sh

@@ -0,0 +1,107 @@
+#!/usr/bin/env sh
+
+############################################################
+# KingHost API support                                     #
+# http://api.kinghost.net/doc/                             #
+#                                                          #
+# Author: Felipe Keller Braz <[email protected]>  #
+# Report Bugs here: https://github.com/kinghost/acme.sh    #
+#                                                          #
+# Values to export:                                        #
+# export KINGHOST_Username="[email protected]"            #
+# export KINGHOST_Password="xxxxxxxxxx"                    #
+############################################################
+
+KING_Api="https://api.kinghost.net/acme"
+
+# Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+# Used to add txt record
+dns_kinghost_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  KINGHOST_Username="${KINGHOST_Username:-$(_readaccountconf_mutable KINGHOST_Username)}"
+  KINGHOST_Password="${KINGHOST_Password:-$(_readaccountconf_mutable KINGHOST_Password)}"
+  if [ -z "$KINGHOST_Username" ] || [ -z "$KINGHOST_Password" ]; then
+    KINGHOST_Username=""
+    KINGHOST_Password=""
+    _err "You don't specify KingHost api password and email yet."
+    _err "Please create you key and try again."
+    return 1
+  fi
+
+  #save the credentials to the account conf file.
+  _saveaccountconf_mutable KINGHOST_Username "$KINGHOST_Username"
+  _saveaccountconf_mutable KINGHOST_Password "$KINGHOST_Password"
+
+  _debug "Getting txt records"
+  _kinghost_rest GET "dns" "name=$fulldomain&content=$txtvalue"
+
+  #This API call returns "status":"ok" if dns record does not exists
+  #We are creating a new txt record here, so we expect the "ok" status
+  if ! echo "$response" | grep '"status":"ok"' >/dev/null; then
+    _err "Error"
+    _err "$response"
+    return 1
+  fi
+
+  _kinghost_rest POST "dns" "name=$fulldomain&content=$txtvalue"
+  if ! echo "$response" | grep '"status":"ok"' >/dev/null; then
+    _err "Error"
+    _err "$response"
+    return 1
+  fi
+
+  return 0
+}
+
+# Usage: fulldomain txtvalue
+# Used to remove the txt record after validation
+dns_kinghost_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  KINGHOST_Password="${KINGHOST_Password:-$(_readaccountconf_mutable KINGHOST_Password)}"
+  KINGHOST_Username="${KINGHOST_Username:-$(_readaccountconf_mutable KINGHOST_Username)}"
+  if [ -z "$KINGHOST_Password" ] || [ -z "$KINGHOST_Username" ]; then
+    KINGHOST_Password=""
+    KINGHOST_Username=""
+    _err "You don't specify KingHost api key and email yet."
+    _err "Please create you key and try again."
+    return 1
+  fi
+
+  _kinghost_rest DELETE "dns" "name=$fulldomain&content=$txtvalue"
+  if ! echo "$response" | grep '"status":"ok"' >/dev/null; then
+    _err "Error"
+    _err "$response"
+    return 1
+  fi
+
+  return 0
+}
+
+####################  Private functions below ##################################
+_kinghost_rest() {
+  method=$1
+  uri="$2"
+  data="$3"
+  _debug "$uri"
+
+  export _H1="X-Auth-Email: $KINGHOST_Username"
+  export _H2="X-Auth-Key: $KINGHOST_Password"
+
+  if [ "$method" != "GET" ]; then
+    _debug data "$data"
+    response="$(_post "$data" "$KING_Api/$uri.json" "" "$method")"
+  else
+    response="$(_get "$KING_Api/$uri.json?$data")"
+  fi
+
+  if [ "$?" != "0" ]; then
+    _err "error $uri"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}

+ 227 - 0
dnsapi/dns_loopia.sh

@@ -0,0 +1,227 @@
+#!/usr/bin/env sh
+
+#
+#LOOPIA_User="username"
+#
+#LOOPIA_Password="password"
+
+LOOPIA_Api="https://api.loopia.se/RPCSERV"
+
+########  Public functions #####################
+
+#Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_loopia_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  LOOPIA_User="${LOOPIA_User:-$(_readaccountconf_mutable LOOPIA_User)}"
+  LOOPIA_Password="${LOOPIA_Password:-$(_readaccountconf_mutable LOOPIA_Password)}"
+  if [ -z "$LOOPIA_User" ] || [ -z "$LOOPIA_Password" ]; then
+    LOOPIA_User=""
+    LOOPIA_Password=""
+    _err "You don't specify loopia user and password yet."
+    _err "Please create you key and try again."
+    return 1
+  fi
+
+  #save the api key and email to the account conf file.
+  _saveaccountconf_mutable LOOPIA_User "$LOOPIA_User"
+  _saveaccountconf_mutable LOOPIA_Password "$LOOPIA_Password"
+
+  _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"
+
+  _info "Adding record"
+
+  _loopia_add_record "$_domain" "$_sub_domain"
+  _loopia_update_record "$_domain" "$_sub_domain" "$txtvalue"
+
+}
+
+dns_loopia_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  LOOPIA_User="${LOOPIA_User:-$(_readaccountconf_mutable LOOPIA_User)}"
+  LOOPIA_Password="${LOOPIA_Password:-$(_readaccountconf_mutable LOOPIA_Password)}"
+  if [ -z "$LOOPIA_User" ] || [ -z "$LOOPIA_Password" ]; then
+    LOOPIA_User=""
+    LOOPIA_Password=""
+    _err "You don't specify LOOPIA user and password yet."
+    _err "Please create you key and try again."
+    return 1
+  fi
+
+  #save the api key and email to the account conf file.
+  _saveaccountconf_mutable LOOPIA_User "$LOOPIA_User"
+  _saveaccountconf_mutable LOOPIA_Password "$LOOPIA_Password"
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+
+  xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
+  <methodCall>
+    <methodName>removeSubdomain</methodName>
+    <params>
+      <param>
+        <value><string>%s</string></value>
+      </param>
+      <param>
+        <value><string>%s</string></value>
+      </param>
+      <param>
+        <value><string>%s</string></value>
+      </param>
+      <param>
+        <value><string>%s</string></value>
+      </param>
+    </params>
+  </methodCall>' $LOOPIA_User $LOOPIA_Password "$_domain" "$_sub_domain")
+
+  response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")"
+
+  if ! _contains "$response" "OK"; then
+    _err "Error could not get txt records"
+    return 1
+  fi
+}
+
+####################  Private functions below ##################################
+
+_get_root() {
+  domain=$1
+  _debug "get root"
+
+  domain=$1
+  i=2
+  p=1
+
+  xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
+  <methodCall>
+  <methodName>getDomains</methodName>
+  <params>
+   <param>
+    <value><string>%s</string></value>
+   </param>
+   <param>
+    <value><string>%s</string></value>
+   </param>
+  </params>
+  </methodCall>' $LOOPIA_User $LOOPIA_Password)
+
+  response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")"
+  while true; do
+    h=$(echo "$domain" | cut -d . -f $i-100)
+    if [ -z "$h" ]; then
+      #not valid
+      return 1
+    fi
+
+    if _contains "$response" "$h"; 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
+
+}
+
+_loopia_update_record() {
+  domain=$1
+  sub_domain=$2
+  txtval=$3
+
+  xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
+  <methodCall>
+    <methodName>updateZoneRecord</methodName>
+    <params>
+      <param>
+        <value><string>%s</string></value>
+      </param>
+      <param>
+        <value><string>%s</string></value>
+      </param>
+      <param>
+        <value><string>%s</string></value>
+      </param>
+      <param>
+        <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>60</int></value>
+          </member>
+          <member>
+            <name>rdata</name>
+            <value><string>%s</string></value>
+          </member>
+          <member>
+            <name>record_id</name>
+            <value><int>0</int></value>
+          </member>
+        </struct>
+      </param>
+    </params>
+  </methodCall>' $LOOPIA_User $LOOPIA_Password "$domain" "$sub_domain" "$txtval")
+
+  response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")"
+
+  if ! _contains "$response" "OK"; then
+    _err "Error"
+    return 1
+  fi
+  return 0
+}
+
+_loopia_add_record() {
+  domain=$1
+  sub_domain=$2
+
+  xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
+  <methodCall>
+    <methodName>addSubdomain</methodName>
+    <params>
+      <param>
+        <value><string>%s</string></value>
+      </param>
+      <param>
+        <value><string>%s</string></value>
+      </param>
+      <param>
+        <value><string>%s</string></value>
+      </param>
+      <param>
+        <value><string>%s</string></value>
+      </param>
+    </params>
+  </methodCall>' $LOOPIA_User $LOOPIA_Password "$domain" "$sub_domain")
+
+  response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")"
+
+  if ! _contains "$response" "OK"; then
+    _err "Error"
+    return 1
+  fi
+  return 0
+}

+ 1 - 1
dnsapi/dns_namecom.sh

@@ -123,7 +123,7 @@ _namecom_login() {
   # Auth string
   # Name.com API v4 uses http basic auth to authenticate
   # need to convert the token for http auth
-  _namecom_auth=$(printf "%s:%s" "$Namecom_Username" "$Namecom_Token" | base64)
+  _namecom_auth=$(printf "%s:%s" "$Namecom_Username" "$Namecom_Token" | _base64)
 
   if _namecom_rest GET "hello"; then
     retcode=$(printf "%s\n" "$response" | _egrep_o "\"username\"\:\"$Namecom_Username\"")

+ 5 - 2
dnsapi/dns_nsupdate.sh

@@ -8,12 +8,14 @@ dns_nsupdate_add() {
   txtvalue=$2
   _checkKeyFile || return 1
   [ -n "${NSUPDATE_SERVER}" ] || NSUPDATE_SERVER="localhost"
+  [ -n "${NSUPDATE_SERVER_PORT}" ] || NSUPDATE_SERVER_PORT=53
   # save the dns server and key to the account conf file.
   _saveaccountconf NSUPDATE_SERVER "${NSUPDATE_SERVER}"
+  _saveaccountconf NSUPDATE_SERVER_PORT "${NSUPDATE_SERVER_PORT}"
   _saveaccountconf NSUPDATE_KEY "${NSUPDATE_KEY}"
   _info "adding ${fulldomain}. 60 in txt \"${txtvalue}\""
   nsupdate -k "${NSUPDATE_KEY}" <<EOF
-server ${NSUPDATE_SERVER}
+server ${NSUPDATE_SERVER}  ${NSUPDATE_SERVER_PORT} 
 update add ${fulldomain}. 60 in txt "${txtvalue}"
 send
 EOF
@@ -30,9 +32,10 @@ dns_nsupdate_rm() {
   fulldomain=$1
   _checkKeyFile || return 1
   [ -n "${NSUPDATE_SERVER}" ] || NSUPDATE_SERVER="localhost"
+  [ -n "${NSUPDATE_SERVER_PORT}" ] || NSUPDATE_SERVER_PORT=53
   _info "removing ${fulldomain}. txt"
   nsupdate -k "${NSUPDATE_KEY}" <<EOF
-server ${NSUPDATE_SERVER}
+server ${NSUPDATE_SERVER}  ${NSUPDATE_SERVER_PORT} 
 update delete ${fulldomain}. txt
 send
 EOF

+ 62 - 14
dnsapi/dns_pdns.sh

@@ -69,15 +69,21 @@ dns_pdns_add() {
 #fulldomain
 dns_pdns_rm() {
   fulldomain=$1
+  txtvalue=$2
+
+  if [ -z "$PDNS_Ttl" ]; then
+    PDNS_Ttl="$DEFAULT_PDNS_TTL"
+  fi
 
   _debug "Detect root zone"
   if ! _get_root "$fulldomain"; then
     _err "invalid domain"
     return 1
   fi
+
   _debug _domain "$_domain"
 
-  if ! rm_record "$_domain" "$fulldomain"; then
+  if ! rm_record "$_domain" "$fulldomain" "$txtvalue"; then
     return 1
   fi
 
@@ -88,9 +94,16 @@ set_record() {
   _info "Adding record"
   root=$1
   full=$2
-  txtvalue=$3
+  new_challenge=$3
 
-  if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root." "{\"rrsets\": [{\"changetype\": \"REPLACE\", \"name\": \"$full.\", \"type\": \"TXT\", \"ttl\": $PDNS_Ttl, \"records\": [{\"name\": \"$full.\", \"type\": \"TXT\", \"content\": \"\\\"$txtvalue\\\"\", \"disabled\": false, \"ttl\": $PDNS_Ttl}]}]}"; then
+  _record_string=""
+  _build_record_string "$new_challenge"
+  _list_existingchallenges
+  for oldchallenge in $_existing_challenges; do
+    _build_record_string "$oldchallenge"
+  done
+
+  if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root" "{\"rrsets\": [{\"changetype\": \"REPLACE\", \"name\": \"$full.\", \"type\": \"TXT\", \"ttl\": $PDNS_Ttl, \"records\": [$_record_string]}]}"; then
     _err "Set txt record error."
     return 1
   fi
@@ -106,14 +119,37 @@ rm_record() {
   _info "Remove record"
   root=$1
   full=$2
+  txtvalue=$3
 
-  if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root." "{\"rrsets\": [{\"changetype\": \"DELETE\", \"name\": \"$full.\", \"type\": \"TXT\"}]}"; then
-    _err "Delete txt record error."
-    return 1
-  fi
+  #Enumerate existing acme challenges
+  _list_existingchallenges
 
-  if ! notify_slaves "$root"; then
-    return 1
+  if _contains "$_existing_challenges" "$txtvalue"; then
+    #Delete all challenges (PowerDNS API does not allow to delete content)
+    if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root" "{\"rrsets\": [{\"changetype\": \"DELETE\", \"name\": \"$full.\", \"type\": \"TXT\"}]}"; then
+      _err "Delete txt record error."
+      return 1
+    fi
+    _record_string=""
+    #If the only existing challenge was the challenge to delete: nothing to do
+    if ! [ "$_existing_challenges" = "$txtvalue" ]; then
+      for oldchallenge in $_existing_challenges; do
+        #Build up the challenges to re-add, ommitting the one what should be deleted
+        if ! [ "$oldchallenge" = "$txtvalue" ]; then
+          _build_record_string "$oldchallenge"
+        fi
+      done
+      #Recreate the existing challenges
+      if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root" "{\"rrsets\": [{\"changetype\": \"REPLACE\", \"name\": \"$full.\", \"type\": \"TXT\", \"ttl\": $PDNS_Ttl, \"records\": [$_record_string]}]}"; then
+        _err "Set txt record error."
+        return 1
+      fi
+    fi
+    if ! notify_slaves "$root"; then
+      return 1
+    fi
+  else
+    _info "Record not found, nothing to remove"
   fi
 
   return 0
@@ -122,7 +158,7 @@ rm_record() {
 notify_slaves() {
   root=$1
 
-  if ! _pdns_rest "PUT" "/api/v1/servers/$PDNS_ServerId/zones/$root./notify"; then
+  if ! _pdns_rest "PUT" "/api/v1/servers/$PDNS_ServerId/zones/$root/notify"; then
     _err "Notify slaves error."
     return 1
   fi
@@ -144,15 +180,18 @@ _get_root() {
 
   while true; do
     h=$(printf "%s" "$domain" | cut -d . -f $i-100)
-    if [ -z "$h" ]; then
-      return 1
-    fi
 
     if _contains "$_zones_response" "\"name\": \"$h.\""; then
-      _domain="$h"
+      _domain="$h."
+      if [ -z "$h" ]; then
+        _domain="=2E"
+      fi
       return 0
     fi
 
+    if [ -z "$h" ]; then
+      return 1
+    fi
     i=$(_math $i + 1)
   done
   _debug "$domain not found"
@@ -182,3 +221,12 @@ _pdns_rest() {
 
   return 0
 }
+
+_build_record_string() {
+  _record_string="${_record_string:+${_record_string}, }{\"content\": \"\\\"${1}\\\"\", \"disabled\": false}"
+}
+
+_list_existingchallenges() {
+  _pdns_rest "GET" "/api/v1/servers/$PDNS_ServerId/zones/$root"
+  _existing_challenges=$(echo "$response" | _normalizeJson | _egrep_o "\"name\":\"${fulldomain}[^]]*}" | _egrep_o 'content\":\"\\"[^\\]*' | sed -n 's/^content":"\\"//p')
+}

+ 69 - 0
dnsapi/dns_tele3.sh

@@ -0,0 +1,69 @@
+#!/usr/bin/env sh
+#
+# tele3.cz DNS API
+#
+# Author: Roman Blizik
+# Report Bugs here: https://github.com/par-pa/acme.sh
+#
+# --
+# export TELE3_Key="MS2I4uPPaI..."
+# export TELE3_Secret="kjhOIHGJKHg"
+# --
+
+TELE3_API="https://www.tele3.cz/acme/"
+
+########  Public functions  #####################
+
+dns_tele3_add() {
+  _info "Using TELE3 DNS"
+  data="\"ope\":\"add\", \"domain\":\"$1\", \"value\":\"$2\""
+  if ! _tele3_call; then
+    _err "Publish zone failed"
+    return 1
+  fi
+
+  _info "Zone published"
+}
+
+dns_tele3_rm() {
+  _info "Using TELE3 DNS"
+  data="\"ope\":\"rm\", \"domain\":\"$1\", \"value\":\"$2\""
+  if ! _tele3_call; then
+    _err "delete TXT record failed"
+    return 1
+  fi
+
+  _info "TXT record successfully deleted"
+}
+
+####################  Private functions below  ##################################
+
+_tele3_init() {
+  TELE3_Key="${TELE3_Key:-$(_readaccountconf_mutable TELE3_Key)}"
+  TELE3_Secret="${TELE3_Secret:-$(_readaccountconf_mutable TELE3_Secret)}"
+  if [ -z "$TELE3_Key" ] || [ -z "$TELE3_Secret" ]; then
+    TELE3_Key=""
+    TELE3_Secret=""
+    _err "You must export variables: TELE3_Key and TELE3_Secret"
+    return 1
+  fi
+
+  #save the config variables to the account conf file.
+  _saveaccountconf_mutable TELE3_Key "$TELE3_Key"
+  _saveaccountconf_mutable TELE3_Secret "$TELE3_Secret"
+}
+
+_tele3_call() {
+  _tele3_init
+  data="{\"key\":\"$TELE3_Key\", \"secret\":\"$TELE3_Secret\", $data}"
+
+  _debug data "$data"
+
+  response="$(_post "$data" "$TELE3_API" "" "POST")"
+  _debug response "$response"
+
+  if [ "$response" != "success" ]; then
+    _err "$response"
+    return 1
+  fi
+}

+ 3 - 3
dnsapi/dns_yandex.sh

@@ -50,9 +50,9 @@ _PDD_get_domain() {
   __last=0
   while [ $__last -eq 0 ]; do
     uri1="https://pddimp.yandex.ru/api2/admin/domain/domains?page=${__page}&on_page=20"
-    res1=$(_get "$uri1" | _normalizeJson)
-    #_debug "$res1"
-    __found=$(echo "$res1" | sed -n -e 's#.* "found": \([^,]*\),.*#\1#p')
+    res1="$(_get "$uri1" | _normalizeJson)"
+    _debug2 "res1" "$res1"
+    __found="$(echo "$res1" | sed -n -e 's#.* "found": \([^,]*\),.*#\1#p')"
     _debug "found: $__found results on page"
     if [ "$__found" -lt 20 ]; then
       _debug "last page: $__page"

+ 139 - 0
dnsapi/dns_zilore.sh

@@ -0,0 +1,139 @@
+#!/usr/bin/env sh
+
+Zilore_API="https://api.zilore.com/dns/v1"
+# Zilore_Key="YOUR-ZILORE-API-KEY"
+
+########  Public functions #####################
+
+dns_zilore_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  _info "Using Zilore"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  Zilore_Key="${Zilore_Key:-$(_readaccountconf_mutable Zilore_Key)}"
+  if [ -z "$Zilore_Key" ]; then
+    Zilore_Key=""
+    _err "Please define Zilore API key"
+    return 1
+  fi
+  _saveaccountconf_mutable Zilore_Key "$Zilore_Key"
+
+  if ! _get_root "$fulldomain"; then
+    _err "Unable to determine root domain"
+    return 1
+  else
+    _debug _domain "$_domain"
+  fi
+
+  if _zilore_rest POST "domains/$_domain/records?record_type=TXT&record_ttl=600&record_name=$fulldomain&record_value=\"$txtvalue\""; then
+    if _contains "$response" '"added"' >/dev/null; then
+      _info "Added TXT record, waiting for validation"
+      return 0
+    else
+      _debug response "$response"
+      _err "Error while adding DNS records"
+      return 1
+    fi
+  fi
+
+  return 1
+}
+
+dns_zilore_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  _info "Using Zilore"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  Zilore_Key="${Zilore_Key:-$(_readaccountconf_mutable Zilore_Key)}"
+  if [ -z "$Zilore_Key" ]; then
+    Zilore_Key=""
+    _err "Please define Zilore API key"
+    return 1
+  fi
+  _saveaccountconf_mutable Zilore_Key "$Zilore_Key"
+
+  if ! _get_root "$fulldomain"; then
+    _err "Unable to determine root domain"
+    return 1
+  else
+    _debug _domain "$_domain"
+  fi
+
+  _debug "Getting TXT records"
+  _zilore_rest GET "domains/${_domain}/records?search_text=$txtvalue&search_record_type=TXT"
+  _debug response "$response"
+
+  if ! _contains "$response" '"ok"' >/dev/null; then
+    _err "Error while getting records list"
+    return 1
+  else
+    _record_id=$(printf "%s\n" "$response" | _egrep_o "\"record_id\":\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1)
+    if [ -z "$_record_id" ]; then
+      _err "Cannot determine _record_id"
+      return 1
+    else
+      _debug _record_id "$_record_id"
+    fi
+    if ! _zilore_rest DELETE "domains/${_domain}/records?record_id=$_record_id"; then
+      _err "Error while deleting chosen record"
+      return 1
+    fi
+    _contains "$response" '"ok"'
+  fi
+}
+
+####################  Private functions below ##################################
+
+_get_root() {
+  domain=$1
+  i=2
+  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 ! _zilore_rest GET "domains?search_text=$h"; 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
+}
+
+_zilore_rest() {
+  method=$1
+  param=$2
+  data=$3
+
+  export _H1="X-Auth-Key: $Zilore_Key"
+
+  if [ "$method" != "GET" ]; then
+    response="$(_post "$data" "$Zilore_API/$param" "" "$method")"
+  else
+    response="$(_get "$Zilore_API/$param")"
+  fi
+
+  if [ "$?" != "0" ]; then
+    _err "error $param"
+    return 1
+  fi
+
+  _debug2 response "$response"
+  return 0
+}