Browse Source

Updated with latest changes from Neilpang/dev

Pål Håland 6 years ago
parent
commit
1dab2ac7d3
58 changed files with 6041 additions and 475 deletions
  1. 1 10
      .travis.yml
  2. 4 1
      Dockerfile
  3. 65 22
      README.md
  4. 298 170
      acme.sh
  5. 137 1
      deploy/README.md
  6. 8 10
      deploy/cpanel_uapi.sh
  7. 9 3
      deploy/fritzbox.sh
  8. 80 0
      deploy/gitlab.sh
  9. 34 2
      deploy/haproxy.sh
  10. 0 6
      deploy/keychain.sh
  11. 59 0
      deploy/mydevil.sh
  12. 92 0
      deploy/qiniu.sh
  13. 3 3
      deploy/ssh.sh
  14. 8 3
      deploy/vault_cli.sh
  15. 504 10
      dnsapi/README.md
  16. 55 0
      dnsapi/dns_acmedns.sh
  17. 141 0
      dnsapi/dns_active24.sh
  18. 7 7
      dnsapi/dns_aws.sh
  19. 13 8
      dnsapi/dns_azure.sh
  20. 9 6
      dnsapi/dns_cf.sh
  21. 253 0
      dnsapi/dns_conoha.sh
  22. 2 2
      dnsapi/dns_cx.sh
  23. 89 64
      dnsapi/dns_dgon.sh
  24. 19 36
      dnsapi/dns_dnsimple.sh
  25. 59 0
      dnsapi/dns_doapi.sh
  26. 1 1
      dnsapi/dns_dp.sh
  27. 161 0
      dnsapi/dns_dpi.sh
  28. 13 13
      dnsapi/dns_dynu.sh
  29. 358 0
      dnsapi/dns_euserv.sh
  30. 168 0
      dnsapi/dns_exoscale.sh
  31. 57 5
      dnsapi/dns_gandi_livedns.sh
  32. 167 0
      dnsapi/dns_gcloud.sh
  33. 9 11
      dnsapi/dns_gd.sh
  34. 168 0
      dnsapi/dns_gdnsdk.sh
  35. 17 6
      dnsapi/dns_he.sh
  36. 161 0
      dnsapi/dns_hostingde.sh
  37. 48 3
      dnsapi/dns_inwx.sh
  38. 2 2
      dnsapi/dns_ispconfig.sh
  39. 107 0
      dnsapi/dns_kinghost.sh
  40. 40 15
      dnsapi/dns_lexicon.sh
  41. 185 0
      dnsapi/dns_linode_v4.sh
  42. 286 0
      dnsapi/dns_loopia.sh
  43. 97 0
      dnsapi/dns_mydevil.sh
  44. 210 0
      dnsapi/dns_mydnsjp.sh
  45. 407 0
      dnsapi/dns_namecheap.sh
  46. 1 1
      dnsapi/dns_namecom.sh
  47. 181 0
      dnsapi/dns_neodigit.sh
  48. 133 0
      dnsapi/dns_netcup.sh
  49. 2 2
      dnsapi/dns_nsone.sh
  50. 30 4
      dnsapi/dns_nsupdate.sh
  51. 211 0
      dnsapi/dns_nw.sh
  52. 217 0
      dnsapi/dns_online.sh
  53. 54 9
      dnsapi/dns_pdns.sh
  54. 164 0
      dnsapi/dns_pointhq.sh
  55. 207 0
      dnsapi/dns_rackspace.sh
  56. 69 0
      dnsapi/dns_tele3.sh
  57. 22 39
      dnsapi/dns_unoeuro.sh
  58. 139 0
      dnsapi/dns_zilore.sh

+ 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
   

+ 4 - 1
Dockerfile

@@ -1,10 +1,13 @@
-FROM alpine:3.6
+FROM alpine:3.9
 
 RUN apk update -f \
   && apk --no-cache add -f \
   openssl \
+  coreutils \
+  bind-tools \
   curl \
   socat \
+  tzdata \
   && rm -rf /var/cache/apk/*
 
 ENV LE_CONFIG_HOME /acme.sh

+ 65 - 22
README.md

@@ -33,9 +33,9 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
 - [webfaction](https://community.webfaction.com/questions/19988/using-letsencrypt)
 - [Loadbalancer.org](https://www.loadbalancer.org/blog/loadbalancer-org-with-lets-encrypt-quick-and-dirty)
 - [discourse.org](https://meta.discourse.org/t/setting-up-lets-encrypt/40709)
-- [Centminmod](http://centminmod.com/letsencrypt-acmetool-https.html)
+- [Centminmod](https://centminmod.com/letsencrypt-acmetool-https.html)
 - [splynx](https://forum.splynx.com/t/free-ssl-cert-for-splynx-lets-encrypt/297)
-- [archlinux](https://aur.archlinux.org/packages/acme.sh-git/)
+- [archlinux](https://www.archlinux.org/packages/community/any/acme.sh)
 - [opnsense.org](https://github.com/opnsense/plugins/tree/master/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient)
 - [CentOS Web Panel](http://centos-webpanel.com/)
 - [lnmp.org](https://lnmp.org/)
@@ -70,11 +70,16 @@ For all build statuses, check our [weekly build project](https://github.com/Neil
 
 https://github.com/Neilpang/acmetest
 
+# Supported CA
+
+- Letsencrypt.org CA(default)
+- [BuyPass.com CA](https://github.com/Neilpang/acme.sh/wiki/BuyPass.com-CA)
 
 # Supported modes
 
 - Webroot mode
 - Standalone mode
+- Standalone tls-alpn mode
 - Apache mode
 - Nginx mode
 - DNS mode
@@ -221,8 +226,20 @@ acme.sh --issue --standalone -d example.com -d www.example.com -d cp.example.com
 
 More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert
 
+# 5. Use Standalone ssl server to issue cert
+
+**(requires you to be root/sudoer or have permission to listen on port 443 (TCP))**
 
-# 5. Use Apache mode
+Port `443` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again.
+
+```bash
+acme.sh --issue --alpn -d example.com -d www.example.com -d cp.example.com
+```
+
+More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert
+
+
+# 6. Use Apache mode
 
 **(requires you to be root/sudoer, since it is required to interact with Apache server)**
 
@@ -236,13 +253,13 @@ Just set string "apache" as the second argument and it will force use of apache
 acme.sh --issue --apache -d example.com -d www.example.com -d cp.example.com
 ```
 
-**This apache mode is only to issue the cert, it will not change your apache config files. 
+**This apache mode is only to issue the cert, it will not change your apache config files.
 You will need to configure your website config files to use the cert by yourself.
 We don't want to mess your apache server, don't worry.**
 
 More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert
 
-# 6. Use Nginx mode
+# 7. Use Nginx mode
 
 **(requires you to be root/sudoer, since it is required to interact with Nginx server)**
 
@@ -260,13 +277,13 @@ So, the config is not changed.
 acme.sh --issue --nginx -d example.com -d www.example.com -d cp.example.com
 ```
 
-**This nginx mode is only to issue the cert, it will not change your nginx config files. 
+**This nginx mode is only to issue the cert, it will not change your nginx config files.
 You will need to configure your website config files to use the cert by yourself.
 We don't want to mess your nginx server, don't worry.**
 
 More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert
 
-# 7. Automatic DNS API integration
+# 8. Automatic DNS API integration
 
 If your DNS provider supports API access, we can use that API to automatically issue the certs.
 
@@ -315,9 +332,33 @@ You don't have to do anything manually!
 1. zonomi.com DNS API
 1. DreamHost.com API
 1. DirectAdmin API
-
-
-And: 
+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. EUSERV.EU (https://www.euserv.eu)
+1. DNSPod.com API (https://www.dnspod.com)
+1. Google Cloud DNS API
+1. ConoHa (https://www.conoha.jp)
+1. netcup DNS API (https://www.netcup.de)
+1. GratisDNS.dk (https://gratisdns.dk)
+1. Namecheap API (https://www.namecheap.com/)
+1. MyDNS.JP API (https://www.mydns.jp/)
+1. hosting.de (https://www.hosting.de)
+1. Neodigit.net API (https://www.neodigit.net)
+1. Exoscale.com API (https://www.exoscale.com/)
+1. PointDNS API (https://pointhq.com/)
+1. Active24.cz API (https://www.active24.cz/)
+1. do.de API (https://www.do.de/)
+1. Nexcess API (https://www.nexcess.net)
+1. Thermo.io API (https://www.thermo.io)
+1. Futurehosting API (https://www.futurehosting.com)
+1. Rackspace Cloud DNS (https://www.rackspace.com)
+1. Online.net API (https://online.net/)
+1. MyDevil.net (https://www.mydevil.net/)
+
+And:
 
 **lexicon DNS API: https://github.com/Neilpang/acme.sh/wiki/How-to-use-lexicon-dns-api
    (DigitalOcean, DNSimple, DNSMadeEasy, DNSPark, EasyDNS, Namesilo, NS1, PointHQ, Rage4 and Vultr etc.)**
@@ -329,7 +370,9 @@ If your DNS provider is not on the supported list above, you can write your own
 
 For more details: [How to use DNS API](dnsapi)
 
-# 8. Use DNS manual mode:
+# 9. 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.
 
@@ -363,7 +406,7 @@ Ok, it's done.
 
 **Please use dns api mode instead.**
 
-# 9. Issue ECC certificates
+# 10. Issue ECC certificates
 
 `Let's Encrypt` can now issue **ECDSA** certificates.
 
@@ -395,7 +438,7 @@ Valid values are:
 
 
 
-# 10. Issue Wildcard certificates
+# 11. Issue Wildcard certificates
 
 It's simple, just give a wildcard domain as the `-d` parameter.
 
@@ -405,7 +448,7 @@ acme.sh  --issue -d example.com  -d '*.example.com'  --dns dns_cf
 
 
 
-# 11. How to renew the certs
+# 12. How to renew the certs
 
 No, you don't need to renew the certs manually. All the certs will be renewed automatically every **60** days.
 
@@ -422,7 +465,7 @@ acme.sh --renew -d example.com --force --ecc
 ```
 
 
-# 12. How to stop cert renewal
+# 13. How to stop cert renewal
 
 To stop renewal of a cert, you can execute the following to remove the cert from the renewal list:
 
@@ -435,7 +478,7 @@ The cert/key file is not removed from the disk.
 You can remove the respective directory (e.g. `~/.acme.sh/example.com`) by yourself.
 
 
-# 13. How to upgrade `acme.sh`
+# 14. How to upgrade `acme.sh`
 
 acme.sh is in constant development, so it's strongly recommended to use the latest code.
 
@@ -460,25 +503,25 @@ acme.sh --upgrade --auto-upgrade 0
 ```
 
 
-# 14. Issue a cert from an existing CSR
+# 15. Issue a cert from an existing CSR
 
 https://github.com/Neilpang/acme.sh/wiki/Issue-a-cert-from-existing-CSR
 
 
-# 15. Under the Hood
+# 16. Under the Hood
 
 Speak ACME language using shell, directly to "Let's Encrypt".
 
 TODO:
 
 
-# 16. Acknowledgments
+# 17. Acknowledgments
 
 1. Acme-tiny: https://github.com/diafygi/acme-tiny
 2. ACME protocol: https://github.com/ietf-wg-acme/acme
 
 
-# 17. License & Others
+# 18. License & Others
 
 License is GPLv3
 
@@ -487,9 +530,9 @@ Please Star and Fork me.
 [Issues](https://github.com/Neilpang/acme.sh/issues) and [pull requests](https://github.com/Neilpang/acme.sh/pulls) are welcome.
 
 
-# 18. Donate
+# 19. Donate
 Your donation makes **acme.sh** better:
 
 1. PayPal/Alipay(支付宝)/Wechat(微信): [https://donate.acme.sh/](https://donate.acme.sh/)
-  
+
 [Donate List](https://github.com/Neilpang/acme.sh/wiki/Donate-list)

File diff suppressed because it is too large
+ 298 - 170
acme.sh


+ 137 - 1
deploy/README.md

@@ -256,7 +256,143 @@ acme.sh --deploy -d fritzbox.example.com --deploy-hook fritzbox
 acme.sh --deploy -d ftp.example.com --deploy-hook strongswan
 ```
 
-## 10. Deploy the cert to remote routeros
+## 10. Deploy the cert to HAProxy
+
+You must specify the path where you want the concatenated key and certificate chain written.
+```sh
+export DEPLOY_HAPROXY_PEM_PATH=/etc/haproxy
+```
+
+You may optionally define the command to reload HAProxy. The value shown below will be used as the default if you don't set this environment variable.
+
+```sh
+export DEPLOY_HAPROXY_RELOAD="/usr/sbin/service haproxy restart"
+```
+
+You can then deploy the certificate as follows
+```sh
+acme.sh --deploy -d haproxy.example.com --deploy-hook haproxy
+```
+
+The path for the PEM file will be stored with the domain configuration and will be available when renewing, so that deploy will happen automatically when renewed.
+
+## 11. Deploy your cert to Gitlab pages
+
+You must define the API key and the informations for the project and Gitlab page you are updating the certificate for.
+
+```sh
+# The token can be created in your user settings under "Access Tokens"
+export GITLAB_TOKEN="xxxxxxxxxxx"
+
+# The project ID is displayed on the home page of the project
+export GITLAB_PROJECT_ID=12345678
+
+# The domain must match the one defined for the Gitlab page, without "https://"
+export GITLAB_DOMAIN="www.mydomain.com"
+```
+
+You can then deploy the certificate as follows
+
+```sh
+acme.sh --deploy -d www.mydomain.com --deploy-hook gitlab
+```
+
+## 12. Deploy your cert to Hashicorp Vault
+
+```sh
+export VAULT_PREFIX="acme"
+```
+
+You can then deploy the certificate as follows
+
+```sh
+acme.sh --deploy -d www.mydomain.com --deploy-hook vault_cli
+```
+
+Your certs will be saved in Vault using this structure:
+
+```sh
+vault write "${VAULT_PREFIX}/${domain}/cert.pem"      value=@"..."
+vault write "${VAULT_PREFIX}/${domain}/cert.key"      value=@"..."
+vault write "${VAULT_PREFIX}/${domain}/chain.pem"     value=@"..."
+vault write "${VAULT_PREFIX}/${domain}/fullchain.pem" value=@"..."
+```
+
+You might be using Fabio load balancer (which can get certs from
+Vault). It needs a bit different structure of your certs in Vault. It
+gets certs only from keys that were saved in `prefix/domain`, like this:
+
+```bash
+vault write <PREFIX>/www.domain.com cert=@cert.pem key=@key.pem
+```
+
+If you want to save certs in Vault this way just set "FABIO" env
+variable to anything (ex: "1") before running `acme.sh`:
+
+```sh
+export FABIO="1"
+```
+
+## 13. Deploy your certificate to Qiniu.com
+
+使用 acme.sh 部署到七牛之前,需要确保部署的域名已打开 HTTPS 功能,您可以访问[融合 CDN - 域名管理](https://portal.qiniu.com/cdn/domain) 设置。
+另外还需要先导出 AK/SK 环境变量,您可以访问[密钥管理](https://portal.qiniu.com/user/key) 获得。
+
+```sh
+$ export QINIU_AK="foo"
+$ export QINIU_SK="bar"
+```
+
+完成准备工作之后,您就可以通过下面的命令开始部署 SSL 证书到七牛上:
+
+```sh
+$ acme.sh --deploy -d example.com --deploy-hook qiniu
+```
+
+假如您部署的证书为泛域名证书,您还需要设置 `QINIU_CDN_DOMAIN` 变量,指定实际需要部署的域名:
+
+```sh
+$ export QINIU_CDN_DOMAIN="cdn.example.com"
+$ acme.sh --deploy -d example.com --deploy-hook qiniu
+```
+
+### English version
+
+You should create AccessKey/SecretKey pair in https://portal.qiniu.com/user/key 
+before deploying your certificate, and please ensure you have enabled HTTPS for
+your domain name. You can enable it in https://portal.qiniu.com/cdn/domain.
+
+```sh
+$ export QINIU_AK="foo"
+$ export QINIU_SK="bar"
+```
+
+then you can deploy certificate by following command:
+
+```sh
+$ acme.sh --deploy -d example.com --deploy-hook qiniu
+```
+
+(Optional), If you are using wildcard certificate,
+you may need export `QINIU_CDN_DOMAIN` to specify which domain
+you want to update:
+
+```sh
+$ export QINIU_CDN_DOMAIN="cdn.example.com"
+$ acme.sh --deploy -d example.com --deploy-hook qiniu
+```
+
+## 14. Deploy your cert on MyDevil.net
+
+Once you have acme.sh installed and certificate issued (see info in [DNS API](../dnsapi/README.md#61-use-mydevilnet)), you can install it by following command:
+
+```sh
+acme.sh --deploy --deploy-hook mydevil -d example.com
+```
+
+That will remove old certificate and install new one.
+
+## 15. Deploy the cert to remote routeros
 
 ```sh
 acme.sh --deploy -d ftp.example.com --deploy-hook routeros

+ 8 - 10
deploy/cpanel_uapi.sh

@@ -2,8 +2,12 @@
 # Here is the script to deploy the cert to your cpanel using the cpanel API.
 # Uses command line uapi.  --user option is needed only if run as root.
 # Returns 0 when success.
-# Written by Santeri Kannisto <santeri.kannisto@2globalnomads.info>
-# Public domain, 2017
+#
+# Please note that I am no longer using Github. If you want to report an issue
+# or contact me, visit https://forum.webseodesigners.com/web-design-seo-and-hosting-f16/
+#
+# Written by Santeri Kannisto <santeri.kannisto@webseodesigners.com>
+# Public domain, 2017-2018
 
 #export DEPLOY_CPANEL_USER=myusername
 
@@ -28,15 +32,9 @@ cpanel_uapi_deploy() {
     _err "The command uapi is not found."
     return 1
   fi
-  if ! _exists php; then
-    _err "The command php is not found."
-    return 1
-  fi
   # read cert and key files and urlencode both
-  _certstr=$(cat "$_ccert")
-  _keystr=$(cat "$_ckey")
-  _cert=$(php -r "echo urlencode(\"$_certstr\");")
-  _key=$(php -r "echo urlencode(\"$_keystr\");")
+  _cert=$(_url_encode <"$_ccert")
+  _key=$(_url_encode <"$_ckey")
 
   _debug _cert "$_cert"
   _debug _key "$_key"

+ 9 - 3
deploy/fritzbox.sh

@@ -28,8 +28,10 @@ fritzbox_deploy() {
   _debug _cfullchain "$_cfullchain"
 
   if ! _exists iconv; then
-    _err "iconv not found"
-    return 1
+    if ! _exists perl; then
+      _err "iconv or perl not found"
+      return 1
+    fi
   fi
 
   _fritzbox_username="${DEPLOY_FRITZBOX_USERNAME}"
@@ -61,7 +63,11 @@ fritzbox_deploy() {
 
   _info "Log in to the FRITZ!Box"
   _fritzbox_challenge="$(_get "${_fritzbox_url}/login_sid.lua" | sed -e 's/^.*<Challenge>//' -e 's/<\/Challenge>.*$//')"
-  _fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${_fritzbox_password}" | iconv -f ASCII -t UTF16LE | md5sum | awk '{print $1}')"
+  if _exists iconv; then
+    _fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${_fritzbox_password}" | iconv -f ASCII -t UTF16LE | md5sum | awk '{print $1}')"
+  else
+    _fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${_fritzbox_password}" | perl -p -e 'use Encode qw/encode/; print encode("UTF-16LE","$_"); $_="";' | md5sum | awk '{print $1}')"
+  fi
   _fritzbox_sid="$(_get "${_fritzbox_url}/login_sid.lua?sid=0000000000000000&username=${_fritzbox_username}&response=${_fritzbox_challenge}-${_fritzbox_hash}" | sed -e 's/^.*<SID>//' -e 's/<\/SID>.*$//')"
 
   if [ -z "${_fritzbox_sid}" ] || [ "${_fritzbox_sid}" = "0000000000000000" ]; then

+ 80 - 0
deploy/gitlab.sh

@@ -0,0 +1,80 @@
+#!/usr/bin/env sh
+
+# Script to deploy certificate to a Gitlab hosted page
+
+# The following variables exported from environment will be used.
+# If not set then values previously saved in domain.conf file are used.
+
+# All the variables are required
+
+# export GITLAB_TOKEN="xxxxxxx"
+# export GITLAB_PROJECT_ID=012345
+# export GITLAB_DOMAIN="mydomain.com"
+
+gitlab_deploy() {
+  _cdomain="$1"
+  _ckey="$2"
+  _ccert="$3"
+  _cca="$4"
+  _cfullchain="$5"
+
+  _debug _cdomain "$_cdomain"
+  _debug _ckey "$_ckey"
+  _debug _ccert "$_ccert"
+  _debug _cca "$_cca"
+  _debug _cfullchain "$_cfullchain"
+
+  if [ -z "$GITLAB_TOKEN" ]; then
+    if [ -z "$Le_Deploy_gitlab_token" ]; then
+      _err "GITLAB_TOKEN not defined."
+      return 1
+    fi
+  else
+    Le_Deploy_gitlab_token="$GITLAB_TOKEN"
+    _savedomainconf Le_Deploy_gitlab_token "$Le_Deploy_gitlab_token"
+  fi
+
+  if [ -z "$GITLAB_PROJECT_ID" ]; then
+    if [ -z "$Le_Deploy_gitlab_project_id" ]; then
+      _err "GITLAB_PROJECT_ID not defined."
+      return 1
+    fi
+  else
+    Le_Deploy_gitlab_project_id="$GITLAB_PROJECT_ID"
+    _savedomainconf Le_Deploy_gitlab_project_id "$Le_Deploy_gitlab_project_id"
+  fi
+
+  if [ -z "$GITLAB_DOMAIN" ]; then
+    if [ -z "$Le_Deploy_gitlab_domain" ]; then
+      _err "GITLAB_DOMAIN not defined."
+      return 1
+    fi
+  else
+    Le_Deploy_gitlab_domain="$GITLAB_DOMAIN"
+    _savedomainconf Le_Deploy_gitlab_domain "$Le_Deploy_gitlab_domain"
+  fi
+
+  string_fullchain=$(_url_encode <"$_cfullchain")
+  string_key=$(_url_encode <"$_ckey")
+
+  body="certificate=$string_fullchain&key=$string_key"
+
+  export _H1="PRIVATE-TOKEN: $Le_Deploy_gitlab_token"
+
+  gitlab_url="https://gitlab.com/api/v4/projects/$Le_Deploy_gitlab_project_id/pages/domains/$Le_Deploy_gitlab_domain"
+
+  _response=$(_post "$body" "$gitlab_url" 0 PUT | _dbase64 "multiline")
+
+  error_response="error"
+
+  if test "${_response#*$error_response}" != "$_response"; then
+    _err "Error in deploying certificate:"
+    _err "$_response"
+    return 1
+  fi
+
+  _debug response "$_response"
+  _info "Certificate successfully deployed"
+
+  return 0
+}

+ 34 - 2
deploy/haproxy.sh

@@ -20,7 +20,39 @@ haproxy_deploy() {
   _debug _cca "$_cca"
   _debug _cfullchain "$_cfullchain"
 
-  _err "deploy cert to haproxy server, Not implemented yet"
-  return 1
+  # handle reload preference
+  DEFAULT_HAPROXY_RELOAD="/usr/sbin/service haproxy restart"
+  if [ -z "${DEPLOY_HAPROXY_RELOAD}" ]; then
+    _reload="${DEFAULT_HAPROXY_RELOAD}"
+    _cleardomainconf DEPLOY_HAPROXY_RELOAD
+  else
+    _reload="${DEPLOY_HAPROXY_RELOAD}"
+    _savedomainconf DEPLOY_HAPROXY_RELOAD "$DEPLOY_HAPROXY_RELOAD"
+  fi
+  _savedomainconf DEPLOY_HAPROXY_PEM_PATH "$DEPLOY_HAPROXY_PEM_PATH"
+
+  # work out the path where the PEM file should go
+  _pem_path="${DEPLOY_HAPROXY_PEM_PATH}"
+  if [ -z "$_pem_path" ]; then
+    _err "Path to save PEM file not found. Please define DEPLOY_HAPROXY_PEM_PATH."
+    return 1
+  fi
+  _pem_full_path="$_pem_path/$_cdomain.pem"
+  _info "Full path to PEM $_pem_full_path"
+
+  # combine the key and fullchain into a single pem and install
+  cat "$_cfullchain" "$_ckey" >"$_pem_full_path"
+  chmod 600 "$_pem_full_path"
+  _info "Certificate successfully deployed"
+
+  # restart HAProxy
+  _info "Run reload: $_reload"
+  if eval "$_reload"; then
+    _info "Reload success!"
+    return 0
+  else
+    _err "Reload error"
+    return 1
+  fi
 
 }

+ 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

+ 59 - 0
deploy/mydevil.sh

@@ -0,0 +1,59 @@
+#!/usr/bin/env sh
+
+# MyDevil.net API (2019-02-03)
+#
+# MyDevil.net already supports automatic Let's Encrypt certificates,
+# except for wildcard domains.
+#
+# This script depends on `devil` command that MyDevil.net provides,
+# which means that it works only on server side.
+#
+# Author: Marcin Konicki <https://ahwayakchih.neoni.net>
+#
+########  Public functions #####################
+
+# Usage: mydevil_deploy domain keyfile certfile cafile fullchain
+mydevil_deploy() {
+  _cdomain="$1"
+  _ckey="$2"
+  _ccert="$3"
+  _cca="$4"
+  _cfullchain="$5"
+  ip=""
+
+  _debug _cdomain "$_cdomain"
+  _debug _ckey "$_ckey"
+  _debug _ccert "$_ccert"
+  _debug _cca "$_cca"
+  _debug _cfullchain "$_cfullchain"
+
+  if ! _exists "devil"; then
+    _err "Could not find 'devil' command."
+    return 1
+  fi
+
+  ip=$(mydevil_get_ip "$_cdomain")
+  if [ -z "$ip" ]; then
+    _err "Could not find IP for domain $_cdomain."
+    return 1
+  fi
+
+  # Delete old certificate first
+  _info "Removing old certificate for $_cdomain at $ip"
+  devil ssl www del "$ip" "$_cdomain"
+
+  # Add new certificate
+  _info "Adding new certificate for $_cdomain at $ip"
+  devil ssl www add "$ip" "$_cfullchain" "$_ckey" "$_cdomain" || return 1
+
+  return 0
+}
+
+####################  Private functions below ##################################
+
+# Usage: ip=$(mydevil_get_ip domain.com)
+#        echo $ip
+mydevil_get_ip() {
+  devil dns list "$1" | cut -w -s -f 3,7 | grep "^A$(printf '\t')" | cut -w -s -f 2 || return 1
+  return 0
+}

+ 92 - 0
deploy/qiniu.sh

@@ -0,0 +1,92 @@
+#!/usr/bin/env sh
+
+# Script to create certificate to qiniu.com 
+#
+# This deployment required following variables
+# export QINIU_AK="QINIUACCESSKEY"
+# export QINIU_SK="QINIUSECRETKEY"
+# export QINIU_CDN_DOMAIN="cdn.example.com"
+
+QINIU_API_BASE="https://api.qiniu.com"
+
+qiniu_deploy() {
+  _cdomain="$1"
+  _ckey="$2"
+  _ccert="$3"
+  _cca="$4"
+  _cfullchain="$5"
+
+  _debug _cdomain "$_cdomain"
+  _debug _ckey "$_ckey"
+  _debug _ccert "$_ccert"
+  _debug _cca "$_cca"
+  _debug _cfullchain "$_cfullchain"
+
+  if [ -z "$QINIU_AK" ]; then
+    _err "QINIU_AK is not defined."
+    return 1
+  else
+    _savedomainconf QINIU_AK "$QINIU_AK"
+  fi
+
+  if [ -z "$QINIU_SK" ]; then
+    _err "QINIU_SK is not defined."
+    return 1
+  else
+    _savedomainconf QINIU_SK "$QINIU_SK"
+  fi
+
+  if [ "$QINIU_CDN_DOMAIN" ]; then
+    _savedomainconf QINIU_CDN_DOMAIN "$QINIU_CDN_DOMAIN"
+  else
+    QINIU_CDN_DOMAIN="$_cdomain"
+  fi
+
+  ## upload certificate
+  string_fullchain=$(sed 's/$/\\n/' "$_cfullchain" | tr -d '\n')
+  string_key=$(sed 's/$/\\n/' "$_ckey" | tr -d '\n')
+
+  sslcert_path="/sslcert"
+  sslcerl_body="{\"name\":\"$_cdomain\",\"common_name\":\"$QINIU_CDN_DOMAIN\",\"ca\":\"$string_fullchain\",\"pri\":\"$string_key\"}"
+  sslcert_access_token="$(_make_access_token "$sslcert_path")"
+  _debug sslcert_access_token "$sslcert_access_token"
+  export _H1="Authorization: QBox $sslcert_access_token"
+  sslcert_response=$(_post "$sslcerl_body" "$QINIU_API_BASE$sslcert_path" 0 "POST" "application/json" | _dbase64 "multiline")
+
+  if ! _contains "$sslcert_response" "certID"; then
+    _err "Error in creating certificate:"
+    _err "$sslcert_response"
+    return 1
+  fi
+
+  _debug sslcert_response "$sslcert_response"
+  _info "Certificate successfully uploaded, updating domain $_cdomain"
+
+  ## extract certId
+  _certId="$(printf "%s" "$sslcert_response" | _normalizeJson | _egrep_o "certID\": *\"[^\"]*\"" | cut -d : -f 2)"
+  _debug certId "$_certId"
+
+  ## update domain ssl config
+  update_path="/domain/$QINIU_CDN_DOMAIN/httpsconf"
+  update_body="{\"certid\":$_certId,\"forceHttps\":false}"
+  update_access_token="$(_make_access_token "$update_path")"
+  _debug update_access_token "$update_access_token"
+  export _H1="Authorization: QBox $update_access_token"
+  update_response=$(_post "$update_body" "$QINIU_API_BASE$update_path" 0 "PUT" "application/json" | _dbase64 "multiline")
+
+  if _contains "$update_response" "error"; then
+    _err "Error in updating domain httpsconf:"
+    _err "$update_response"
+    return 1
+  fi
+
+  _debug update_response "$update_response"
+  _info "Certificate successfully deployed"
+
+  return 0
+}
+
+_make_access_token() {
+  _token="$(printf "%s\n" "$1" | _hmac "sha1" "$(printf "%s" "$QINIU_SK" | _hex_dump | tr -d " ")" | _base64)"
+  echo "$QINIU_AK:$_token"
+}

+ 3 - 3
deploy/ssh.sh

@@ -11,7 +11,7 @@
 #
 # Only a username is required.  All others are optional.
 #
-# The following examples are for QNAP NAS running QTS 4.2 
+# The following examples are for QNAP NAS running QTS 4.2
 # export DEPLOY_SSH_CMD=""  # defaults to ssh
 # export DEPLOY_SSH_USER="admin"  # required
 # export DEPLOY_SSH_SERVER="qnap"  # defaults to domain name
@@ -101,7 +101,7 @@ ssh_deploy() {
   fi
 
   # CERTFILE is optional.
-  # If provided then private key will be copied or appended to provided filename.
+  # If provided then certificate will be copied or appended to provided filename.
   if [ -n "$DEPLOY_SSH_CERTFILE" ]; then
     Le_Deploy_ssh_certfile="$DEPLOY_SSH_CERTFILE"
     _savedomainconf Le_Deploy_ssh_certfile "$Le_Deploy_ssh_certfile"
@@ -190,7 +190,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
     _info "Backup directories erased after 180 days."
   fi
 
-  _debug "Remote commands to execute: $_cmdstr"
+  _secure_debug "Remote commands to execute: " "$_cmdstr"
   _info "Submitting sequence of commands to remote server by ssh"
   # quotations in bash cmd below intended.  Squash travis spellcheck error
   # shellcheck disable=SC2029

+ 8 - 3
deploy/vault_cli.sh

@@ -49,8 +49,13 @@ vault_cli_deploy() {
     return 1
   fi
 
-  $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}/fullchain.pem" value=@"$_cfullchain" || return 1
+  if [ -n "$FABIO" ]; then
+    $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}" cert=@"$_cfullchain" key=@"$_ckey" || return 1
+  else
+    $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
+  fi
 
 }

+ 504 - 10
dnsapi/README.md

@@ -1,8 +1,12 @@
 # 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.
+First you need to login to your CloudFlare account to get your [API key](https://dash.cloudflare.com/profile). 
 
 ```
 export CF_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
@@ -142,13 +146,17 @@ Finally, make the DNS server and update Key available to `acme.sh`
 export NSUPDATE_SERVER="dns.example.com"
 export NSUPDATE_KEY="/path/to/your/nsupdate.key"
 ```
+and optionally (depending on DNS server)
+```
+export NSUPDATE_ZONE="example.com"
+```
 
 Ok, let's issue a cert now:
 ```
 acme.sh --issue --dns dns_nsupdate -d example.com -d www.example.com
 ```
 
-The `NSUPDATE_SERVER` and `NSUPDATE_KEY` settings will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+The `NSUPDATE_SERVER`, `NSUPDATE_KEY`, and `NSUPDATE_ZONE` settings will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
 
 
 ## 8. Use LuaDNS domain API
@@ -259,16 +267,26 @@ when needed.
 
 ## 14. Use Linode domain API
 
+The tokens created in the classic manager and cloud manager are incompatible
+with one another. While the classic manager makes an all or nothing API, the
+newer cloud manager interface promises to produce API keys with a finer
+permission system. However, either way works just fine.
+
+### Classic Manager ###
+
+Classic Manager: https://manager.linode.com/profile/api
+
 First you need to login to your Linode account to get your API Key.
-[https://manager.linode.com/profile/api](https://manager.linode.com/profile/api)
 
-Then add an API key with label *ACME* and copy the new key.
+Then add an API key with label *ACME* and copy the new key into the following
+command.
 
 ```sh
 export LINODE_API_KEY="..."
 ```
 
-Due to the reload time of any changes in the DNS records, we have to use the `dnssleep` option to wait at least 15 minutes for the changes to take effect.
+Due to the reload time of any changes in the DNS records, we have to use the
+`dnssleep` option to wait at least 15 minutes for the changes to take effect.
 
 Ok, let's issue a cert now:
 
@@ -276,7 +294,35 @@ Ok, let's issue a cert now:
 acme.sh --issue --dns dns_linode --dnssleep 900 -d example.com -d www.example.com
 ```
 
-The `LINODE_API_KEY` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+The `LINODE_API_KEY` will be saved in `~/.acme.sh/account.conf` and will be
+reused when needed.
+
+### Cloud Manager ###
+
+Cloud Manager: https://cloud.linode.com/profile/tokens
+
+First you need to login to your Linode account to get your API Key.
+
+   1. Click on "Add a Personal Access Token".
+   2. Give the new key a "Label" (we recommend *ACME*)
+   3. Give it Read/Write access to "Domains"
+   4. "Submit" and copy the new key into the `LINODE_V4_API_KEY` command below.
+
+```sh
+export LINODE_V4_API_KEY="..."
+```
+
+Due to the reload time of any changes in the DNS records, we have to use the
+`dnssleep` option to wait at least 15 minutes for the changes to take effect.
+
+Ok, let's issue a cert now:
+
+```sh
+acme.sh --issue --dns dns_linode_v4 --dnssleep 900 -d example.com -d www.example.com
+```
+
+The `LINODE_V4_API_KEY` will be saved in `~/.acme.sh/account.conf` and will be
+reused when needed.
 
 ## 15. Use FreeDNS
 
@@ -325,6 +371,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"
@@ -448,7 +496,7 @@ The `Infoblox_Creds` and `Infoblox_Server` will be saved in `~/.acme.sh/account.
 First you need to create/obtain API tokens on your [settings panel](https://vscale.io/panel/settings/tokens/).
 
 ```
-VSCALE_API_KEY="sdfsdfsdfljlbjkljlkjsdfoiwje"
+export VSCALE_API_KEY="sdfsdfsdfljlbjkljlkjsdfoiwje"
 ```
 
 Ok, let's issue a cert now:
@@ -525,8 +573,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 +687,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 +807,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,6 +841,443 @@ 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 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 Euserv.eu API
+
+First you need to login to your euserv.eu account and activate your API Administration (API Verwaltung).
+[https://support.euserv.com](https://support.euserv.com)
+
+Once you've activate, login to your API Admin Interface and create an API account.
+Please specify the scope (active groups: domain) and assign the allowed IPs.
+
+```
+export EUSERV_Username="99999.user123"
+export EUSERV_Password="Asbe54gHde"
+```
+
+Ok, let's issue a cert now: (Be aware to use the `--insecure` flag, cause euserv.eu is still using self-signed certificates!)
+```
+acme.sh --issue --dns dns_euserv -d example.com -d *.example.com --insecure
+```
+
+The `EUSERV_Username` and `EUSERV_Password` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+Please report any issues to https://github.com/initit/acme.sh or to <github@initit.de>
+
+## 48. Use DNSPod.com domain API to automatically issue cert
+
+First you need to get your API Key and ID by this [get-the-user-token](https://www.dnspod.com/docs/info.html#get-the-user-token).
+
+```
+export DPI_Id="1234"
+export DPI_Key="sADDsdasdgdsf"
+```
+
+Ok, let's issue a cert now:
+```
+acme.sh --issue --dns dns_dpi -d example.com -d www.example.com
+```
+
+The `DPI_Id` and `DPI_Key` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 49. Use Google Cloud DNS API to automatically issue cert
+
+First you need to authenticate to gcloud.
+
+```
+gcloud init
+```
+
+**The `dns_gcloud` script uses the active gcloud configuration and credentials.**
+There is no logic inside `dns_gcloud` to override the project and other settings.
+If needed, create additional [gcloud configurations](https://cloud.google.com/sdk/gcloud/reference/topic/configurations).
+You can change the configuration being used without *activating* it; simply set the `CLOUDSDK_ACTIVE_CONFIG_NAME` environment variable.
+
+To issue a certificate you can:
+```
+export CLOUDSDK_ACTIVE_CONFIG_NAME=default  # see the note above
+acme.sh --issue --dns dns_gcloud -d example.com -d '*.example.com'
+```
+
+`dns_gcloud` also supports [DNS alias mode](https://github.com/Neilpang/acme.sh/wiki/DNS-alias-mode).
+
+## 50. Use ConoHa API
+
+First you need to login to your ConoHa account to get your API credentials.
+
+```
+export CONOHA_Username="xxxxxx"
+export CONOHA_Password="xxxxxx"
+export CONOHA_TenantId="xxxxxx"
+export CONOHA_IdentityServiceApi="https://identity.xxxx.conoha.io/v2.0"
+```
+
+To issue a cert:
+```
+acme.sh --issue --dns dns_conoha -d example.com -d www.example.com
+```
+
+The `CONOHA_Username`, `CONOHA_Password`, `CONOHA_TenantId` and `CONOHA_IdentityServiceApi` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 51. Use netcup DNS API to automatically issue cert
+
+First you need to login in your CCP account to get your API Key and API Password.
+```
+export NC_Apikey="<Apikey>"
+export NC_Apipw="<Apipassword>"
+export NC_CID="<Customernumber>"
+```
+
+Now, let's issue a cert:
+```
+acme.sh --issue --dns dns_netcup -d example.com -d www.example.com
+```
+
+The `NC_Apikey`,`NC_Apipw` and `NC_CID` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+## 52. Use GratisDNS.dk
+
+GratisDNS.dk (https://gratisdns.dk/) does not provide an API to update DNS records (other than IPv4 and IPv6
+dynamic DNS addresses).  The acme.sh plugin therefore retrieves and updates domain TXT records by logging
+into the GratisDNS website to read the HTML and posting updates as HTTP.  The plugin needs to know your
+userid and password for the GratisDNS website.
+
+```sh
+export GDNSDK_Username="..."
+export GDNSDK_Password="..."
+```
+The username and password will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+
+Now you can issue a certificate.
+
+Note: It usually takes a few minutes (usually 3-4 minutes) before the changes propagates to gratisdns.dk nameservers (ns3.gratisdns.dk often are slow),
+and in rare cases I have seen over 5 minutes before google DNS catches it. Therefor a DNS sleep of at least 300 seconds are recommended-
+
+```sh
+acme.sh --issue --dns dns_gdnsdk --dnssleep 300 -d example.com -d *.example.com
+```
+
+## 53. Use Namecheap
+
+You will need your namecheap username, API KEY (https://www.namecheap.com/support/api/intro.aspx) and your external IP address (or an URL to get it), this IP will need to be whitelisted at Namecheap.
+Due to Namecheap's API limitation all the records of your domain will be read and re applied, make sure to have a backup of your records you could apply if any issue would arise.
+
+```sh
+export NAMECHEAP_USERNAME="..."
+export NAMECHEAP_API_KEY="..."
+export NAMECHEAP_SOURCEIP="..."
+```
+
+NAMECHEAP_SOURCEIP can either be an IP address or an URL to provide it (e.g. https://ifconfig.co/ip).
+
+The username and password will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+Now you can issue a certificate.
+
+```sh
+acme.sh --issue --dns dns_namecheap -d example.com -d *.example.com
+```
+
+## 54. Use MyDNS.JP API
+
+First, register to MyDNS.JP and get MasterID and Password.
+
+```
+export MYDNSJP_MasterID=MasterID
+export MYDNSJP_Password=Password
+```
+
+To issue a certificate:
+
+```
+acme.sh --issue --dns dns_mydnsjp -d example.com -d www.example.com
+```
+The `MYDNSJP_MasterID` and `MYDNSJP_Password` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 55. Use hosting.de API
+
+Create an API key in your hosting.de account here: https://secure.hosting.de
+
+The key needs the following rights:
+- DNS_ZONES_EDIT
+- DNS_ZONES_LIST
+
+Set your API Key and endpoint:
+
+```
+export HOSTINGDE_APIKEY='xxx'
+export HOSTINGDE_ENDPOINT='https://secure.hosting.de'
+```
+
+The plugin can also be used for the http.net API. http.net customers have to set endpoint to https://partner.http.net.
+
+Ok, let's issue a cert now:
+```
+acme.sh --issue --dns dns_hostingde -d example.com -d *.example.com
+```
+
+The hosting.de API key and endpoint will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 56. Use Neodigit.net API
+
+```
+export NEODIGIT_API_TOKEN="eXJxTkdUVUZmcHQ3QWJackQ4ZGlMejRDSklRYmo5VG5zcFFKK2thYnE0WnVnNnMy"
+```
+
+Ok, let's issue a cert now:
+```
+acme.sh --issue --dns dns_neodigit -d example.com -d www.example.com
+```
+
+Neodigit API Token will be saved in `~/.acme.sh/account.conf` and will be used when needed.
+
+## 57. Use Exoscale API
+
+Create an API key and secret key in the Exoscale account section
+
+Set your API and secret key:
+
+```
+export EXOSCALE_API_KEY='xxx'
+export EXOSCALE_SECRET_KEY='xxx'
+```
+
+Now, let's issue a cert:
+```
+acme.sh --issue --dns dns_exoscale -d example.com -d www.example.com
+```
+
+The `EXOSCALE_API_KEY` and `EXOSCALE_SECRET_KEY` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 58. Using PointHQ API to issue certs
+
+Log into [PointHQ account management](https://app.pointhq.com/profile) and copy the API key from the page there.
+
+```export PointHQ_Key="apikeystringgoeshere"
+exportPointHQ_Email="accountemail@yourdomain.com"
+```
+
+You can then issue certs by using:
+```acme.sh --issue --dns dns_pointhq -d example.com -d www.example.com
+```
+
+## 59. Use Active24 API
+
+Create an API token in the Active24 account section, documentation on https://faq.active24.com/cz/790131-REST-API-rozhran%C3%AD.
+
+Set your API token:
+
+```
+export ACTIVE24_Token='xxx'
+```
+
+Now, let's issue a cert, set `dnssleep` for propagation new DNS record:
+```
+acme.sh --issue --dns dns_active24 -d example.com -d www.example.com --dnssleep 1000
+```
+
+The `ACTIVE24_Token` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 60. Use do.de API
+
+Create an API token in your do.de account.
+
+Set your API token:
+```
+export DO_LETOKEN='FmD408PdqT1E269gUK57'
+```
+
+To issue a certificate run:
+```
+acme.sh --issue --dns dns_doapi -d example.com -d *.example.com
+```
+
+The API token will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 61. Use Nexcess API
+
+First, you'll need to login to the [Nexcess.net Client Portal](https://portal.nexcess.net) and [generate a new API token](https://portal.nexcess.net/api-token).
+
+Once you have a token, set it in your systems environment:
+
+```
+export NW_API_TOKEN="YOUR_TOKEN_HERE"
+export NW_API_ENDPOINT="https://portal.nexcess.net"
+```
+
+Finally, we'll issue the certificate: (Nexcess DNS publishes at max every 15 minutes, we recommend setting a 900 second `--dnssleep`)
+
+```
+acme.sh --issue --dns dns_nw -d example.com --dnssleep 900
+```
+
+The `NW_API_TOKEN` and `NW_API_ENDPOINT` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 62. Use Thermo.io API
+
+First, you'll need to login to the [Thermo.io Client Portal](https://core.thermo.io) and [generate a new API token](https://core.thermo.io/api-token).
+
+Once you have a token, set it in your systems environment:
+
+```
+export NW_API_TOKEN="YOUR_TOKEN_HERE"
+export NW_API_ENDPOINT="https://core.thermo.io"
+```
+
+Finally, we'll issue the certificate: (Thermo DNS publishes at max every 15 minutes, we recommend setting a 900 second `--dnssleep`)
+
+```
+acme.sh --issue --dns dns_nw -d example.com --dnssleep 900
+```
+
+The `NW_API_TOKEN` and `NW_API_ENDPOINT` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 63. Use Futurehosting API
+
+First, you'll need to login to the [Futurehosting Client Portal](https://my.futurehosting.com) and [generate a new API token](https://my.futurehosting.com/api-token).
+
+Once you have a token, set it in your systems environment:
+
+```
+export NW_API_TOKEN="YOUR_TOKEN_HERE"
+export NW_API_ENDPOINT="https://my.futurehosting.com"
+```
+
+Finally, we'll issue the certificate: (Futurehosting DNS publishes at max every 15 minutes, we recommend setting a 900 second `--dnssleep`)
+
+```
+acme.sh --issue --dns dns_nw -d example.com --dnssleep 900
+```
+
+The `NW_API_TOKEN` and `NW_API_ENDPOINT` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 64. Use Rackspace API
+
+Set username and API key, which is available under "My Profile & Settings"
+
+```
+export RACKSPACE_Username='username'
+export RACKSPACE_Apikey='xxx'
+```
+
+Now, let's issue a cert:
+
+```
+acme.sh --issue --dns dns_rackspace -d example.com -d www.example.com
+```
+
+## 65. Use Online API
+
+First, you'll need to retrive your API key, which is available under https://console.online.net/en/api/access
+
+```
+export ONLINE_API_KEY='xxx'
+```
+
+To issue a cert run:
+
+```
+acme.sh --issue --dns dns_online -d example.com -d www.example.com
+```
+
+`ONLINE_API_KEY` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 66. Use MyDevil.net
+
+Make sure that you can execute own binaries:
+
+```sh
+devil binexec on
+```
+
+Install acme.sh, or simply `git clone` it into some directory on your MyDevil host account (in which case you should link to it from your `~/bin` directory).
+
+If you're not using private IP and depend on default IP provided by host, you may want to edit `crontab` too, and make sure that `acme.sh --cron` is run also after reboot (you can find out how to do that on their wiki pages).
+
+To issue a new certificate, run:
+
+```sh
+acme.sh --issue --dns dns_mydevil -d example.com -d *.example.com
+```
+
+After certificate is ready, you can install it with [deploy command](../deploy/README.md#14-deploy-your-cert-on-mydevilnet).
 
 # Use custom API
 

+ 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 ##################################

+ 141 - 0
dnsapi/dns_active24.sh

@@ -0,0 +1,141 @@
+#!/usr/bin/env sh
+
+#ACTIVE24_Token="sdfsdfsdfljlbjkljlkjsdfoiwje"
+
+ACTIVE24_Api="https://api.active24.com"
+
+########  Public functions #####################
+
+# Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+# Used to add txt record
+dns_active24_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  _active24_init
+
+  _info "Adding txt record"
+  if _active24_rest POST "dns/$_domain/txt/v1" "{\"name\":\"$_sub_domain\",\"text\":\"$txtvalue\",\"ttl\":0}"; then
+    if _contains "$response" "errors"; then
+      _err "Add txt record error."
+      return 1
+    else
+      _info "Added, OK"
+      return 0
+    fi
+  fi
+  _err "Add txt record error."
+  return 1
+}
+
+# Usage: fulldomain txtvalue
+# Used to remove the txt record after validation
+dns_active24_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  _active24_init
+
+  _debug "Getting txt records"
+  _active24_rest GET "dns/$_domain/records/v1"
+
+  if _contains "$response" "errors"; then
+    _err "Error"
+    return 1
+  fi
+
+  hash_ids=$(echo "$response" | _egrep_o "[^{]+${txtvalue}[^}]+" | _egrep_o "hashId\":\"[^\"]+" | cut -c10-)
+
+  for hash_id in $hash_ids; do
+    _debug "Removing hash_id" "$hash_id"
+    if _active24_rest DELETE "dns/$_domain/$hash_id/v1" ""; then
+      if _contains "$response" "errors"; then
+        _err "Unable to remove txt record."
+        return 1
+      else
+        _info "Removed txt record."
+        return 0
+      fi
+    fi
+  done
+
+  _err "No txt records found."
+  return 1
+}
+
+####################  Private functions below ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+# _domain_id=sdjkglgdfewsdfg
+_get_root() {
+  domain=$1
+
+  if ! _active24_rest GET "dns/domains/v1"; then
+    return 1
+  fi
+
+  i=2
+  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 _contains "$response" "\"$h\"" >/dev/null; then
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      _domain=$h
+      return 0
+    fi
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+  return 1
+}
+
+_active24_rest() {
+  m=$1
+  ep="$2"
+  data="$3"
+  _debug "$ep"
+
+  export _H1="Authorization: Bearer $ACTIVE24_Token"
+
+  if [ "$m" != "GET" ]; then
+    _debug "data" "$data"
+    response="$(_post "$data" "$ACTIVE24_Api/$ep" "" "$m" "application/json")"
+  else
+    response="$(_get "$ACTIVE24_Api/$ep")"
+  fi
+
+  if [ "$?" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}
+
+_active24_init() {
+  ACTIVE24_Token="${ACTIVE24_Token:-$(_readaccountconf_mutable ACTIVE24_Token)}"
+  if [ -z "$ACTIVE24_Token" ]; then
+    ACTIVE24_Token=""
+    _err "You didn't specify a Active24 api token yet."
+    _err "Please create the token and try again."
+    return 1
+  fi
+
+  _saveaccountconf_mutable ACTIVE24_Token "ACTIVE24_Token"
+
+  _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"
+}

+ 7 - 7
dnsapi/dns_aws.sh

@@ -29,7 +29,7 @@ dns_aws_add() {
   if [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ]; then
     AWS_ACCESS_KEY_ID=""
     AWS_SECRET_ACCESS_KEY=""
-    _err "You don't specify aws route53 api key id and and api key secret yet."
+    _err "You haven't specifed the aws route53 api key id and and api key secret yet."
     _err "Please create your key and try again. see $(__green $AWS_WIKI)"
     return 1
   fi
@@ -62,7 +62,7 @@ dns_aws_add() {
   fi
 
   if [ "$_resource_record" ] && _contains "$response" "$txtvalue"; then
-    _info "The txt record already exists, skip"
+    _info "The TXT record already exists. Skipping."
     return 0
   fi
 
@@ -71,7 +71,7 @@ dns_aws_add() {
   _aws_tmpl_xml="<ChangeResourceRecordSetsRequest xmlns=\"https://route53.amazonaws.com/doc/2013-04-01/\"><ChangeBatch><Changes><Change><Action>UPSERT</Action><ResourceRecordSet><Name>$fulldomain</Name><Type>TXT</Type><TTL>300</TTL><ResourceRecords>$_resource_record<ResourceRecord><Value>\"$txtvalue\"</Value></ResourceRecord></ResourceRecords></ResourceRecordSet></Change></Changes></ChangeBatch></ChangeResourceRecordSetsRequest>"
 
   if aws_rest POST "2013-04-01$_domain_id/rrset/" "" "$_aws_tmpl_xml" && _contains "$response" "ChangeResourceRecordSetsResponse"; then
-    _info "txt record updated success."
+    _info "TXT record updated successfully."
     return 0
   fi
 
@@ -99,7 +99,7 @@ dns_aws_rm() {
   _debug _sub_domain "$_sub_domain"
   _debug _domain "$_domain"
 
-  _info "Geting existing records for $fulldomain"
+  _info "Getting existing records for $fulldomain"
   if ! aws_rest GET "2013-04-01$_domain_id/rrset" "name=$fulldomain&type=TXT"; then
     return 1
   fi
@@ -108,14 +108,14 @@ dns_aws_rm() {
     _resource_record="$(echo "$response" | sed 's/<ResourceRecordSet>/"/g' | tr '"' "\n" | grep "<Name>$fulldomain.</Name>" | _egrep_o "<ResourceRecords.*</ResourceRecords>" | sed "s/<ResourceRecords>//" | sed "s#</ResourceRecords>##")"
     _debug "_resource_record" "$_resource_record"
   else
-    _debug "no records exists, skip"
+    _debug "no records exist, skip"
     return 0
   fi
 
   _aws_tmpl_xml="<ChangeResourceRecordSetsRequest xmlns=\"https://route53.amazonaws.com/doc/2013-04-01/\"><ChangeBatch><Changes><Change><Action>DELETE</Action><ResourceRecordSet><ResourceRecords>$_resource_record</ResourceRecords><Name>$fulldomain.</Name><Type>TXT</Type><TTL>300</TTL></ResourceRecordSet></Change></Changes></ChangeBatch></ChangeResourceRecordSetsRequest>"
 
   if aws_rest POST "2013-04-01$_domain_id/rrset/" "" "$_aws_tmpl_xml" && _contains "$response" "ChangeResourceRecordSetsResponse"; then
-    _info "txt record deleted success."
+    _info "TXT record deleted successfully."
     return 0
   fi
 
@@ -163,7 +163,7 @@ _get_root() {
             _domain=$h
             return 0
           fi
-          _err "Can not find domain id: $h"
+          _err "Can't find domain with id: $h"
           return 1
         fi
       fi

+ 13 - 8
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
@@ -316,7 +316,7 @@ _get_root() {
   ## (ZoneListResult with  continuation token for the next page of results)
   ## Per https://docs.microsoft.com/en-us/azure/azure-subscription-service-limits#dns-limits you are limited to 100 Zone/subscriptions anyways
   ##
-  _azure_rest GET "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.Network/dnszones?api-version=2017-09-01" "" "$accesstoken"
+  _azure_rest GET "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.Network/dnszones?\$top=500&api-version=2017-09-01" "" "$accesstoken"
   # Find matching domain name is Json response
   while true; do
     h=$(printf "%s" "$domain" | cut -d . -f $i-100)
@@ -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

+ 9 - 6
dnsapi/dns_cf.sh

@@ -19,8 +19,8 @@ dns_cf_add() {
   if [ -z "$CF_Key" ] || [ -z "$CF_Email" ]; then
     CF_Key=""
     CF_Email=""
-    _err "You didn't specify a cloudflare api key and email yet."
-    _err "Please create the key and try again."
+    _err "You didn't specify a Cloudflare api key and email yet."
+    _err "You can get yours from here https://dash.cloudflare.com/profile."
     return 1
   fi
 
@@ -58,9 +58,12 @@ dns_cf_add() {
   #  if [ "$count" = "0" ]; then
   _info "Adding record"
   if _cf_rest POST "zones/$_domain_id/dns_records" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":120}"; then
-    if printf -- "%s" "$response" | grep "$fulldomain" >/dev/null; then
+    if _contains "$response" "$fulldomain"; then
       _info "Added, OK"
       return 0
+    elif _contains "$response" "The record already exists"; then
+      _info "Already exists, OK"
+      return 0
     else
       _err "Add txt record error."
       return 1
@@ -94,8 +97,8 @@ dns_cf_rm() {
   if [ -z "$CF_Key" ] || [ -z "$CF_Email" ]; then
     CF_Key=""
     CF_Email=""
-    _err "You didn't specify a cloudflare api key and email yet."
-    _err "Please create the key and try again."
+    _err "You didn't specify a Cloudflare api key and email yet."
+    _err "You can get yours from here https://dash.cloudflare.com/profile."
     return 1
   fi
 
@@ -159,7 +162,7 @@ _get_root() {
     fi
 
     if _contains "$response" "\"name\":\"$h\"" >/dev/null; then
-      _domain_id=$(printf "%s\n" "$response" | _egrep_o "\[.\"id\":\"[^\"]*\"" | head -n 1 | cut -d : -f 2 | tr -d \")
+      _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

+ 253 - 0
dnsapi/dns_conoha.sh

@@ -0,0 +1,253 @@
+#!/usr/bin/env sh
+
+CONOHA_DNS_EP_PREFIX_REGEXP="https://dns-service\."
+
+########  Public functions #####################
+
+#Usage: dns_conoha_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_conoha_add() {
+  fulldomain=$1
+  txtvalue=$2
+  _info "Using conoha"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  _debug "Check uesrname and password"
+  CONOHA_Username="${CONOHA_Username:-$(_readaccountconf_mutable CONOHA_Username)}"
+  CONOHA_Password="${CONOHA_Password:-$(_readaccountconf_mutable CONOHA_Password)}"
+  CONOHA_TenantId="${CONOHA_TenantId:-$(_readaccountconf_mutable CONOHA_TenantId)}"
+  CONOHA_IdentityServiceApi="${CONOHA_IdentityServiceApi:-$(_readaccountconf_mutable CONOHA_IdentityServiceApi)}"
+  if [ -z "$CONOHA_Username" ] || [ -z "$CONOHA_Password" ] || [ -z "$CONOHA_TenantId" ] || [ -z "$CONOHA_IdentityServiceApi" ]; then
+    CONOHA_Username=""
+    CONOHA_Password=""
+    CONOHA_TenantId=""
+    CONOHA_IdentityServiceApi=""
+    _err "You didn't specify a conoha api username and password yet."
+    _err "Please create the user and try again."
+    return 1
+  fi
+
+  _saveaccountconf_mutable CONOHA_Username "$CONOHA_Username"
+  _saveaccountconf_mutable CONOHA_Password "$CONOHA_Password"
+  _saveaccountconf_mutable CONOHA_TenantId "$CONOHA_TenantId"
+  _saveaccountconf_mutable CONOHA_IdentityServiceApi "$CONOHA_IdentityServiceApi"
+
+  if token="$(_conoha_get_accesstoken "$CONOHA_IdentityServiceApi/tokens" "$CONOHA_Username" "$CONOHA_Password" "$CONOHA_TenantId")"; then
+    accesstoken="$(printf "%s" "$token" | sed -n 1p)"
+    CONOHA_Api="$(printf "%s" "$token" | sed -n 2p)"
+  else
+    return 1
+  fi
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain" "$CONOHA_Api" "$accesstoken"; then
+    _err "invalid domain"
+    return 1
+  fi
+  _debug _domain_id "$_domain_id"
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  _info "Adding record"
+  body="{\"type\":\"TXT\",\"name\":\"$fulldomain.\",\"data\":\"$txtvalue\",\"ttl\":60}"
+  if _conoha_rest POST "$CONOHA_Api/v1/domains/$_domain_id/records" "$body" "$accesstoken"; then
+    if _contains "$response" '"data":"'"$txtvalue"'"'; then
+      _info "Added, OK"
+      return 0
+    else
+      _err "Add txt record error."
+      return 1
+    fi
+  fi
+
+  _err "Add txt record error."
+  return 1
+}
+
+#Usage: fulldomain txtvalue
+#Remove the txt record after validation.
+dns_conoha_rm() {
+  fulldomain=$1
+  txtvalue=$2
+  _info "Using conoha"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  _debug "Check uesrname and password"
+  CONOHA_Username="${CONOHA_Username:-$(_readaccountconf_mutable CONOHA_Username)}"
+  CONOHA_Password="${CONOHA_Password:-$(_readaccountconf_mutable CONOHA_Password)}"
+  CONOHA_TenantId="${CONOHA_TenantId:-$(_readaccountconf_mutable CONOHA_TenantId)}"
+  CONOHA_IdentityServiceApi="${CONOHA_IdentityServiceApi:-$(_readaccountconf_mutable CONOHA_IdentityServiceApi)}"
+  if [ -z "$CONOHA_Username" ] || [ -z "$CONOHA_Password" ] || [ -z "$CONOHA_TenantId" ] || [ -z "$CONOHA_IdentityServiceApi" ]; then
+    CONOHA_Username=""
+    CONOHA_Password=""
+    CONOHA_TenantId=""
+    CONOHA_IdentityServiceApi=""
+    _err "You didn't specify a conoha api username and password yet."
+    _err "Please create the user and try again."
+    return 1
+  fi
+
+  _saveaccountconf_mutable CONOHA_Username "$CONOHA_Username"
+  _saveaccountconf_mutable CONOHA_Password "$CONOHA_Password"
+  _saveaccountconf_mutable CONOHA_TenantId "$CONOHA_TenantId"
+  _saveaccountconf_mutable CONOHA_IdentityServiceApi "$CONOHA_IdentityServiceApi"
+
+  if token="$(_conoha_get_accesstoken "$CONOHA_IdentityServiceApi/tokens" "$CONOHA_Username" "$CONOHA_Password" "$CONOHA_TenantId")"; then
+    accesstoken="$(printf "%s" "$token" | sed -n 1p)"
+    CONOHA_Api="$(printf "%s" "$token" | sed -n 2p)"
+  else
+    return 1
+  fi
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain" "$CONOHA_Api" "$accesstoken"; 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 ! _conoha_rest GET "$CONOHA_Api/v1/domains/$_domain_id/records" "" "$accesstoken"; then
+    _err "Error"
+    return 1
+  fi
+
+  record_id=$(printf "%s" "$response" | _egrep_o '{[^}]*}' \
+    | grep '"type":"TXT"' | grep "\"data\":\"$txtvalue\"" | _egrep_o "\"id\":\"[^\"]*\"" \
+    | _head_n 1 | cut -d : -f 2 | tr -d \")
+  if [ -z "$record_id" ]; then
+    _err "Can not get record id to remove."
+    return 1
+  fi
+  _debug record_id "$record_id"
+
+  _info "Removing the txt record"
+  if ! _conoha_rest DELETE "$CONOHA_Api/v1/domains/$_domain_id/records/$record_id" "" "$accesstoken"; then
+    _err "Delete record error."
+    return 1
+  fi
+
+  return 0
+}
+
+####################  Private functions below ##################################
+
+_conoha_rest() {
+  m="$1"
+  ep="$2"
+  data="$3"
+  accesstoken="$4"
+
+  export _H1="Accept: application/json"
+  export _H2="Content-Type: application/json"
+  if [ -n "$accesstoken" ]; then
+    export _H3="X-Auth-Token: $accesstoken"
+  fi
+
+  _debug "$ep"
+  if [ "$m" != "GET" ]; then
+    _secure_debug2 data "$data"
+    response="$(_post "$data" "$ep" "" "$m")"
+  else
+    response="$(_get "$ep")"
+  fi
+  _ret="$?"
+  _secure_debug2 response "$response"
+  if [ "$_ret" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+
+  response="$(printf "%s" "$response" | _normalizeJson)"
+  return 0
+}
+
+_conoha_get_accesstoken() {
+  ep="$1"
+  username="$2"
+  password="$3"
+  tenantId="$4"
+
+  accesstoken="$(_readaccountconf_mutable conoha_accesstoken)"
+  expires="$(_readaccountconf_mutable conoha_tokenvalidto)"
+  CONOHA_Api="$(_readaccountconf_mutable conoha_dns_ep)"
+
+  # can we reuse the access token?
+  if [ -n "$accesstoken" ] && [ -n "$expires" ] && [ -n "$CONOHA_Api" ]; then
+    utc_date="$(_utc_date | sed "s/ /T/")"
+    if expr "$utc_date" "<" "$expires" >/dev/null; then
+      # access token is still valid - reuse it
+      _debug "reusing access token"
+      printf "%s\n%s\n" "$accesstoken" "$CONOHA_Api"
+      return 0
+    else
+      _debug "access token expired"
+    fi
+  fi
+  _debug "getting new access token"
+
+  body="$(printf '{"auth":{"passwordCredentials":{"username":"%s","password":"%s"},"tenantId":"%s"}}' "$username" "$password" "$tenantId")"
+  if ! _conoha_rest POST "$ep" "$body" ""; then
+    _err error "$response"
+    return 1
+  fi
+  accesstoken=$(printf "%s" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
+  expires=$(printf "%s" "$response" | _egrep_o "\"expires\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2-4 | tr -d \" | tr -d Z) #expect UTC
+  if [ -z "$accesstoken" ] || [ -z "$expires" ]; then
+    _err "no acccess token received. Check your Conoha settings see $WIKI"
+    return 1
+  fi
+  _saveaccountconf_mutable conoha_accesstoken "$accesstoken"
+  _saveaccountconf_mutable conoha_tokenvalidto "$expires"
+
+  CONOHA_Api=$(printf "%s" "$response" | _egrep_o 'publicURL":"'"$CONOHA_DNS_EP_PREFIX_REGEXP"'[^"]*"' | _head_n 1 | cut -d : -f 2-3 | tr -d \")
+  if [ -z "$CONOHA_Api" ]; then
+    _err "failed to get conoha dns endpoint url"
+    return 1
+  fi
+  _saveaccountconf_mutable conoha_dns_ep "$CONOHA_Api"
+
+  printf "%s\n%s\n" "$accesstoken" "$CONOHA_Api"
+  return 0
+}
+
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+# _domain_id=sdjkglgdfewsdfg
+_get_root() {
+  domain="$1"
+  ep="$2"
+  accesstoken="$3"
+  i=2
+  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 ! _conoha_rest GET "$ep/v1/domains?name=$h" "" "$accesstoken"; then
+      return 1
+    fi
+
+    if _contains "$response" "\"name\":\"$h\"" >/dev/null; then
+      _domain_id=$(printf "%s\n" "$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
+        return 0
+      fi
+      return 1
+    fi
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+  return 1
+}

+ 2 - 2
dnsapi/dns_cx.sh

@@ -1,6 +1,6 @@
 #!/usr/bin/env sh
 
-# Cloudxns.com Domain api
+# CloudXNS Domain api
 #
 #CX_Key="1234"
 #
@@ -19,7 +19,7 @@ dns_cx_add() {
   if [ -z "$CX_Key" ] || [ -z "$CX_Secret" ]; then
     CX_Key=""
     CX_Secret=""
-    _err "You don't specify cloudxns.com  api key or secret yet."
+    _err "You don't specify cloudxns.net  api key or secret yet."
     _err "Please create you key and try again."
     return 1
   fi

+ 89 - 64
dnsapi/dns_dgon.sh

@@ -104,47 +104,59 @@ dns_dgon_rm() {
   ## may get: "links":{"pages":{"last":".../v2/domains/DOM/records?page=2","next":".../v2/domains/DOM/records?page=2"}}
   GURL="https://api.digitalocean.com/v2/domains/$_domain/records"
 
-  ## while we dont have a record ID we keep going
-  while [ -z "$record" ]; do
+  ## Get all the matching records
+  while true; do
     ## 1) get the URL
     ## the create request - get
     ## args: URL, [onlyheader, timeout]
     domain_list="$(_get "$GURL")"
-    ## 2) find record
-    ## check for what we are looing for: "type":"A","name":"$_sub_domain"
-    record="$(echo "$domain_list" | _egrep_o "\"id\"\s*\:\s*\"*[0-9]+\"*[^}]*\"name\"\s*\:\s*\"$_sub_domain\"[^}]*\"data\"\s*\:\s*\"$txtvalue\"")"
-    ## 3) check record and get next page
-    if [ -z "$record" ]; then
-      ## find the next page if we dont have a match
-      nextpage="$(echo "$domain_list" | _egrep_o "\"links\".*" | _egrep_o "\"next\".*" | _egrep_o "http.*page\=[0-9]+")"
-      if [ -z "$nextpage" ]; then
-        _err "no record and no nextpage in digital ocean DNS removal"
-        return 1
-      fi
-      _debug2 nextpage "$nextpage"
-      GURL="$nextpage"
+
+    ## check response
+    if [ "$?" != "0" ]; then
+      _err "error in domain_list response: $domain_list"
+      return 1
     fi
-    ## we break out of the loop when we have a record
-  done
+    _debug2 domain_list "$domain_list"
 
-  ## we found the record
-  rec_id="$(echo "$record" | _egrep_o "id\"\s*\:\s*\"*[0-9]+" | _egrep_o "[0-9]+")"
-  _debug rec_id "$rec_id"
+    ## 2) find records
+    ## check for what we are looking for: "type":"A","name":"$_sub_domain"
+    record="$(echo "$domain_list" | _egrep_o "\"id\"\s*\:\s*\"*[0-9]+\"*[^}]*\"name\"\s*\:\s*\"$_sub_domain\"[^}]*\"data\"\s*\:\s*\"$txtvalue\"")"
 
-  ## delete the record
-  ## delete URL for removing the one we dont want
-  DURL="https://api.digitalocean.com/v2/domains/$_domain/records/$rec_id"
+    if [ ! -z "$record" ]; then
+
+      ## we found records
+      rec_ids="$(echo "$record" | _egrep_o "id\"\s*\:\s*\"*[0-9]+" | _egrep_o "[0-9]+")"
+      _debug rec_ids "$rec_ids"
+      if [ ! -z "$rec_ids" ]; then
+        echo "$rec_ids" | while IFS= read -r rec_id; do
+          ## delete the record
+          ## delete URL for removing the one we dont want
+          DURL="https://api.digitalocean.com/v2/domains/$_domain/records/$rec_id"
+
+          ## the create request - delete
+          ## args: BODY, URL, [need64, httpmethod]
+          response="$(_post "" "$DURL" "" "DELETE")"
+
+          ## check response (sort of)
+          if [ "$?" != "0" ]; then
+            _err "error in remove response: $response"
+            return 1
+          fi
+          _debug2 response "$response"
+
+        done
+      fi
+    fi
 
-  ## the create request - delete
-  ## args: BODY, URL, [need64, httpmethod]
-  response="$(_post "" "$DURL" "" "DELETE")"
+    ## 3) find the next page
+    nextpage="$(echo "$domain_list" | _egrep_o "\"links\".*" | _egrep_o "\"next\".*" | _egrep_o "http.*page\=[0-9]+")"
+    if [ -z "$nextpage" ]; then
+      break
+    fi
+    _debug2 nextpage "$nextpage"
+    GURL="$nextpage"
 
-  ## check response (sort of)
-  if [ "$?" != "0" ]; then
-    _err "error in remove response: $response"
-    return 1
-  fi
-  _debug2 response "$response"
+  done
 
   ## finished correctly
   return 0
@@ -178,44 +190,57 @@ _get_base_domain() {
   export _H2="Authorization: Bearer $DO_API_KEY"
   _debug DO_API_KEY "$DO_API_KEY"
   ## get URL for the list of domains
-  ## havent seen this request paginated, tested with 18 domains (more requires manual requests with DO)
+  ## may get: "links":{"pages":{"last":".../v2/domains/DOM/records?page=2","next":".../v2/domains/DOM/records?page=2"}}
   DOMURL="https://api.digitalocean.com/v2/domains"
 
-  ## get the domain list (DO gives basically a full XFER!)
-  domain_list="$(_get "$DOMURL")"
+  ## while we dont have a matching domain we keep going
+  while [ -z "$found" ]; do
+    ## get the domain list (current page)
+    domain_list="$(_get "$DOMURL")"
 
-  ## check response
-  if [ "$?" != "0" ]; then
-    _err "error in domain_list response: $domain_list"
-    return 1
-  fi
-  _debug2 domain_list "$domain_list"
-
-  ## for each shortening of our $fulldomain, check if it exists in the $domain_list
-  ## can never start on 1 (aka whole $fulldomain) as $fulldomain starts with "_acme-challenge"
-  i=2
-  while [ $i -gt 0 ]; do
-    ## get next longest domain
-    _domain=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-"$MAX_DOM")
-    ## check we got something back from our cut (or are we at the end)
-    if [ -z "$_domain" ]; then
-      ## we got to the end of the domain - invalid domain
-      _err "domain not found in DigitalOcean account"
+    ## check response
+    if [ "$?" != "0" ]; then
+      _err "error in domain_list response: $domain_list"
       return 1
     fi
-    ## we got part of a domain back - grep it out
-    found="$(echo "$domain_list" | _egrep_o "\"name\"\s*\:\s*\"$_domain\"")"
-    ## check if it exists
-    if [ ! -z "$found" ]; then
-      ## exists - exit loop returning the parts
-      sub_point=$(_math $i - 1)
-      _sub_domain=$(printf "%s" "$fulldomain" | cut -d . -f 1-"$sub_point")
-      _debug _domain "$_domain"
-      _debug _sub_domain "$_sub_domain"
-      return 0
+    _debug2 domain_list "$domain_list"
+
+    ## for each shortening of our $fulldomain, check if it exists in the $domain_list
+    ## can never start on 1 (aka whole $fulldomain) as $fulldomain starts with "_acme-challenge"
+    i=2
+    while [ $i -gt 0 ]; do
+      ## get next longest domain
+      _domain=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-"$MAX_DOM")
+      ## check we got something back from our cut (or are we at the end)
+      if [ -z "$_domain" ]; then
+        break
+      fi
+      ## we got part of a domain back - grep it out
+      found="$(echo "$domain_list" | _egrep_o "\"name\"\s*\:\s*\"$_domain\"")"
+      ## check if it exists
+      if [ ! -z "$found" ]; then
+        ## exists - exit loop returning the parts
+        sub_point=$(_math $i - 1)
+        _sub_domain=$(printf "%s" "$fulldomain" | cut -d . -f 1-"$sub_point")
+        _debug _domain "$_domain"
+        _debug _sub_domain "$_sub_domain"
+        return 0
+      fi
+      ## increment cut point $i
+      i=$(_math $i + 1)
+    done
+
+    if [ -z "$found" ]; then
+      ## find the next page if we dont have a match
+      nextpage="$(echo "$domain_list" | _egrep_o "\"links\".*" | _egrep_o "\"next\".*" | _egrep_o "http.*page\=[0-9]+")"
+      if [ -z "$nextpage" ]; then
+        _err "no record and no nextpage in digital ocean DNS removal"
+        return 1
+      fi
+      _debug2 nextpage "$nextpage"
+      DOMURL="$nextpage"
     fi
-    ## increment cut point $i
-    i=$(_math $i + 1)
+
   done
 
   ## we went through the entire domain zone list and dint find one that matched

+ 19 - 36
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 ##################################
@@ -169,7 +152,7 @@ _get_records() {
   sub_domain=$3
 
   _debug "fetching txt records"
-  _dnsimple_rest GET "$account_id/zones/$domain/records?per_page=100"
+  _dnsimple_rest GET "$account_id/zones/$domain/records?per_page=5000&sort=id:desc"
 
   if ! _contains "$response" "\"id\":"; then
     _err "failed to retrieve records"

+ 59 - 0
dnsapi/dns_doapi.sh

@@ -0,0 +1,59 @@
+#!/usr/bin/env sh
+
+# Official Let's Encrypt API for do.de / Domain-Offensive
+# 
+# This is different from the dns_do adapter, because dns_do is only usable for enterprise customers
+# This API is also available to private customers/individuals
+# 
+# Provide the required LetsEncrypt token like this: 
+# DO_LETOKEN="FmD408PdqT1E269gUK57"
+
+DO_API="https://www.do.de/api/letsencrypt"
+
+########  Public functions #####################
+
+#Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_doapi_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  DO_LETOKEN="${DO_LETOKEN:-$(_readaccountconf_mutable DO_LETOKEN)}"
+  if [ -z "$DO_LETOKEN" ]; then
+    DO_LETOKEN=""
+    _err "You didn't configure a do.de API token yet."
+    _err "Please set DO_LETOKEN and try again."
+    return 1
+  fi
+  _saveaccountconf_mutable DO_LETOKEN "$DO_LETOKEN"
+
+  _info "Adding TXT record to ${fulldomain}"
+  response="$(_get "$DO_API?token=$DO_LETOKEN&domain=${fulldomain}&value=${txtvalue}")"
+  if _contains "${response}" 'success'; then
+    return 0
+  fi
+  _err "Could not create resource record, check logs"
+  _err "${response}"
+  return 1
+}
+
+dns_doapi_rm() {
+  fulldomain=$1
+
+  DO_LETOKEN="${DO_LETOKEN:-$(_readaccountconf_mutable DO_LETOKEN)}"
+  if [ -z "$DO_LETOKEN" ]; then
+    DO_LETOKEN=""
+    _err "You didn't configure a do.de API token yet."
+    _err "Please set DO_LETOKEN and try again."
+    return 1
+  fi
+  _saveaccountconf_mutable DO_LETOKEN "$DO_LETOKEN"
+
+  _info "Deleting resource record $fulldomain"
+  response="$(_get "$DO_API?token=$DO_LETOKEN&domain=${fulldomain}&action=delete")"
+  if _contains "${response}" 'success'; then
+    return 0
+  fi
+  _err "Could not delete resource record, check logs"
+  _err "${response}"
+  return 1
+}

+ 1 - 1
dnsapi/dns_dp.sh

@@ -63,7 +63,7 @@ dns_dp_rm() {
     return 0
   fi
 
-  record_id=$(echo "$response" | _egrep_o '{[^{]*"value":"'"$txtvalue"'"' | cut -d , -f 1 | cut -d : -f 2 | tr -d \")
+  record_id=$(echo "$response" | tr "{" "\n" | grep "$txtvalue" | grep '^"id"' | cut -d : -f 2 | cut -d '"' -f 2)
   _debug record_id "$record_id"
   if [ -z "$record_id" ]; then
     _err "Can not get record id."

+ 161 - 0
dnsapi/dns_dpi.sh

@@ -0,0 +1,161 @@
+#!/usr/bin/env sh
+
+# Dnspod.com Domain api
+#
+#DPI_Id="1234"
+#
+#DPI_Key="sADDsdasdgdsf"
+
+REST_API="https://api.dnspod.com"
+
+########  Public functions #####################
+
+#Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_dpi_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  DPI_Id="${DPI_Id:-$(_readaccountconf_mutable DPI_Id)}"
+  DPI_Key="${DPI_Key:-$(_readaccountconf_mutable DPI_Key)}"
+  if [ -z "$DPI_Id" ] || [ -z "$DPI_Key" ]; then
+    DPI_Id=""
+    DPI_Key=""
+    _err "You don't specify dnspod api key and key id 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 DPI_Id "$DPI_Id"
+  _saveaccountconf_mutable DPI_Key "$DPI_Key"
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+
+  add_record "$_domain" "$_sub_domain" "$txtvalue"
+
+}
+
+#fulldomain txtvalue
+dns_dpi_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  DPI_Id="${DPI_Id:-$(_readaccountconf_mutable DPI_Id)}"
+  DPI_Key="${DPI_Key:-$(_readaccountconf_mutable DPI_Key)}"
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+
+  if ! _rest POST "Record.List" "user_token=$DPI_Id,$DPI_Key&format=json&domain_id=$_domain_id&sub_domain=$_sub_domain"; then
+    _err "Record.Lis error."
+    return 1
+  fi
+
+  if _contains "$response" 'No records'; then
+    _info "Don't need to remove."
+    return 0
+  fi
+
+  record_id=$(echo "$response" | _egrep_o '{[^{]*"value":"'"$txtvalue"'"' | cut -d , -f 1 | cut -d : -f 2 | tr -d \")
+  _debug record_id "$record_id"
+  if [ -z "$record_id" ]; then
+    _err "Can not get record id."
+    return 1
+  fi
+
+  if ! _rest POST "Record.Remove" "user_token=$DPI_Id,$DPI_Key&format=json&domain_id=$_domain_id&record_id=$record_id"; then
+    _err "Record.Remove error."
+    return 1
+  fi
+
+  _contains "$response" "Action completed successful"
+
+}
+
+#add the txt record.
+#usage: root  sub  txtvalue
+add_record() {
+  root=$1
+  sub=$2
+  txtvalue=$3
+  fulldomain="$sub.$root"
+
+  _info "Adding record"
+
+  if ! _rest POST "Record.Create" "user_token=$DPI_Id,$DPI_Key&format=json&domain_id=$_domain_id&sub_domain=$_sub_domain&record_type=TXT&value=$txtvalue&record_line=default"; then
+    return 1
+  fi
+
+  _contains "$response" "Action completed successful" || _contains "$response" "Domain record already exists"
+}
+
+####################  Private functions below ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+# _domain_id=sdjkglgdfewsdfg
+_get_root() {
+  domain=$1
+  i=2
+  p=1
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    if [ -z "$h" ]; then
+      #not valid
+      return 1
+    fi
+
+    if ! _rest POST "Domain.Info" "user_token=$DPI_Id,$DPI_Key&format=json&domain=$h"; then
+      return 1
+    fi
+
+    if _contains "$response" "Action completed successful"; then
+      _domain_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")
+      _debug _domain_id "$_domain_id"
+      if [ "$_domain_id" ]; then
+        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+        _debug _sub_domain "$_sub_domain"
+        _domain="$h"
+        _debug _domain "$_domain"
+        return 0
+      fi
+      return 1
+    fi
+    p="$i"
+    i=$(_math "$i" + 1)
+  done
+  return 1
+}
+
+#Usage: method  URI  data
+_rest() {
+  m="$1"
+  ep="$2"
+  data="$3"
+  _debug "$ep"
+  url="$REST_API/$ep"
+
+  _debug url "$url"
+
+  if [ "$m" = "GET" ]; then
+    response="$(_get "$url" | tr -d '\r')"
+  else
+    _debug2 data "$data"
+    response="$(_post "$data" "$url" | tr -d '\r')"
+  fi
+
+  if [ "$?" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}

+ 13 - 13
dnsapi/dns_dynu.sh

@@ -10,7 +10,7 @@
 Dynu_Token=""
 #
 #Endpoint
-Dynu_EndPoint="https://api.dynu.com/v1"
+Dynu_EndPoint="https://api.dynu.com/v2"
 #
 #Author: Dynu Systems, Inc.
 #Report Bugs here: https://github.com/shar0119/acme.sh
@@ -51,11 +51,11 @@ dns_dynu_add() {
   _debug _domain_name "$_domain_name"
 
   _info "Creating TXT record."
-  if ! _dynu_rest POST "dns/record/add" "{\"domain_name\":\"$_domain_name\",\"node_name\":\"$_node\",\"record_type\":\"TXT\",\"text_data\":\"$txtvalue\",\"state\":true,\"ttl\":90}"; then
+  if ! _dynu_rest POST "dns/$dnsId/record" "{\"domainId\":\"$dnsId\",\"nodeName\":\"$_node\",\"recordType\":\"TXT\",\"textData\":\"$txtvalue\",\"state\":true,\"ttl\":90}"; then
     return 1
   fi
 
-  if ! _contains "$response" "text_data"; then
+  if ! _contains "$response" "200"; then
     _err "Could not add TXT record."
     return 1
   fi
@@ -132,11 +132,12 @@ _get_root() {
       return 1
     fi
 
-    if ! _dynu_rest GET "dns/get/$h"; then
+    if ! _dynu_rest GET "dns/getroot/$h"; then
       return 1
     fi
 
-    if _contains "$response" "\"name\":\"$h\"" >/dev/null; then
+    if _contains "$response" "\"domainName\":\"$h\"" >/dev/null; then
+      dnsId=$(printf "%s" "$response" | tr -d "{}" | cut -d , -f 2 | cut -d : -f 2)
       _domain_name=$h
       _node=$(printf "%s" "$domain" | cut -d . -f 1-$p)
       return 0
@@ -152,7 +153,7 @@ _get_recordid() {
   fulldomain=$1
   txtvalue=$2
 
-  if ! _dynu_rest GET "dns/record/get?hostname=$fulldomain&rrtype=TXT"; then
+  if ! _dynu_rest GET "dns/$dnsId/record"; then
     return 1
   fi
 
@@ -161,19 +162,18 @@ _get_recordid() {
     return 0
   fi
 
-  _dns_record_id=$(printf "%s" "$response" | _egrep_o "{[^}]*}" | grep "\"text_data\":\"$txtvalue\"" | _egrep_o ",[^,]*," | grep ',"id":' | tr -d ",," | cut -d : -f 2)
-
+  _dns_record_id=$(printf "%s" "$response" | sed -e 's/[^{]*\({[^}]*}\)[^{]*/\1\n/g' | grep "\"textData\":\"$txtvalue\"" | sed -e 's/.*"id":\([^,]*\).*/\1/')
   return 0
 }
 
 _delete_txt_record() {
   _dns_record_id=$1
 
-  if ! _dynu_rest GET "dns/record/delete/$_dns_record_id"; then
+  if ! _dynu_rest DELETE "dns/$dnsId/record/$_dns_record_id"; then
     return 1
   fi
 
-  if ! _contains "$response" "true"; then
+  if ! _contains "$response" "200"; then
     return 1
   fi
 
@@ -189,7 +189,7 @@ _dynu_rest() {
   export _H1="Authorization: Bearer $Dynu_Token"
   export _H2="Content-Type: application/json"
 
-  if [ "$data" ]; then
+  if [ "$data" ] || [ "$m" = "DELETE" ]; then
     _debug data "$data"
     response="$(_post "$data" "$Dynu_EndPoint/$ep" "" "$m")"
   else
@@ -216,8 +216,8 @@ _dynu_authentication() {
     _err "Authentication failed."
     return 1
   fi
-  if _contains "$response" "accessToken"; then
-    Dynu_Token=$(printf "%s" "$response" | tr -d "[]" | cut -d , -f 2 | cut -d : -f 2 | cut -d '"' -f 2)
+  if _contains "$response" "access_token"; then
+    Dynu_Token=$(printf "%s" "$response" | tr -d "{}" | cut -d , -f 1 | cut -d : -f 2 | cut -d '"' -f 2)
   fi
   if _contains "$Dynu_Token" "null"; then
     Dynu_Token=""

+ 358 - 0
dnsapi/dns_euserv.sh

@@ -0,0 +1,358 @@
+#!/usr/bin/env sh
+
+#This is the euserv.eu api wrapper for acme.sh
+#
+#Author: Michael Brueckner
+#Report Bugs: https://www.github.com/initit/acme.sh  or  mbr@initit.de
+
+#
+#EUSERV_Username="username"
+#
+#EUSERV_Password="password"
+#
+# Dependencies:
+# -------------
+# - none -
+
+EUSERV_Api="https://api.euserv.net"
+
+########  Public functions #####################
+
+#Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_euserv_add() {
+  fulldomain="$(echo "$1" | _lower_case)"
+  txtvalue=$2
+
+  EUSERV_Username="${EUSERV_Username:-$(_readaccountconf_mutable EUSERV_Username)}"
+  EUSERV_Password="${EUSERV_Password:-$(_readaccountconf_mutable EUSERV_Password)}"
+  if [ -z "$EUSERV_Username" ] || [ -z "$EUSERV_Password" ]; then
+    EUSERV_Username=""
+    EUSERV_Password=""
+    _err "You don't specify euserv user and password yet."
+    _err "Please create your key and try again."
+    return 1
+  fi
+
+  #save the user and email to the account conf file.
+  _saveaccountconf_mutable EUSERV_Username "$EUSERV_Username"
+  _saveaccountconf_mutable EUSERV_Password "$EUSERV_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"
+  if ! _euserv_add_record "$_domain" "$_sub_domain" "$txtvalue"; then
+    return 1
+  fi
+
+}
+
+#fulldomain txtvalue
+dns_euserv_rm() {
+
+  fulldomain="$(echo "$1" | _lower_case)"
+  txtvalue=$2
+
+  EUSERV_Username="${EUSERV_Username:-$(_readaccountconf_mutable EUSERV_Username)}"
+  EUSERV_Password="${EUSERV_Password:-$(_readaccountconf_mutable EUSERV_Password)}"
+  if [ -z "$EUSERV_Username" ] || [ -z "$EUSERV_Password" ]; then
+    EUSERV_Username=""
+    EUSERV_Password=""
+    _err "You don't specify euserv user and password yet."
+    _err "Please create your key and try again."
+    return 1
+  fi
+
+  #save the user and email to the account conf file.
+  _saveaccountconf_mutable EUSERV_Username "$EUSERV_Username"
+  _saveaccountconf_mutable EUSERV_Password "$EUSERV_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"
+
+  _debug "Getting txt records"
+
+  xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
+  <methodCall>
+    <methodName>domain.dns_get_active_records</methodName>
+    <params>
+      <param>
+       <value>
+         <struct>
+           <member>
+             <name>login</name>
+             <value>
+               <string>%s</string>
+             </value>
+            </member>
+            <member>
+              <name>password</name>
+              <value>
+                <string>%s</string>
+              </value>
+            </member>
+            <member>
+              <name>domain_id</name>
+              <value>
+                <int>%s</int>
+              </value>
+            </member>
+          </struct>
+        </value>
+      </param>
+    </params>
+  </methodCall>' "$EUSERV_Username" "$EUSERV_Password" "$_euserv_domain_id")
+
+  export _H1="Content-Type: text/xml"
+  response="$(_post "$xml_content" "$EUSERV_Api" "" "POST")"
+
+  if ! _contains "$response" "<member><name>status</name><value><i4>100</i4></value></member>"; then
+    _err "Error could not get txt records"
+    _debug "xml_content" "$xml_content"
+    _debug "response" "$response"
+    return 1
+  fi
+
+  if ! echo "$response" | grep '>dns_record_content<.*>'"$txtvalue"'<' >/dev/null; then
+    _info "Do not need to delete record"
+  else
+    # find XML block where txtvalue is in. The record_id is allways prior this line!
+    _endLine=$(echo "$response" | grep -n '>dns_record_content<.*>'"$txtvalue"'<' | cut -d ':' -f 1)
+    # record_id is the last <name> Tag with a number before the row _endLine, identified by </name><value><struct> 
+    _record_id=$(echo "$response" | sed -n '1,'"$_endLine"'p' | grep '</name><value><struct>' | _tail_n 1 | sed 's/.*<name>\([0-9]*\)<\/name>.*/\1/')
+    _info "Deleting record"
+    _euserv_delete_record "$_record_id"
+  fi
+
+}
+
+####################  Private functions below ##################################
+
+_get_root() {
+  domain=$1
+  _debug "get root"
+
+  # Just to read the domain_orders once
+
+  domain=$1
+  i=2
+  p=1
+
+  if ! _euserv_get_domain_orders; then
+    return 1
+  fi
+
+  # Get saved response with domain_orders
+  response="$_euserv_domain_orders"
+
+  while true; do
+    h=$(echo "$domain" | cut -d . -f $i-100)
+    _debug h "$h"
+    if [ -z "$h" ]; then
+      #not valid
+      return 1
+    fi
+
+    if _contains "$response" "$h"; then
+      _sub_domain=$(echo "$domain" | cut -d . -f 1-$p)
+      _domain="$h"
+      if ! _euserv_get_domain_id "$_domain"; then
+        _err "invalid domain"
+        return 1
+      fi
+      return 0
+    fi
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+
+  return 1
+}
+
+_euserv_get_domain_orders() {
+  # returns: _euserv_domain_orders
+
+  _debug "get domain_orders"
+
+  xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
+  <methodCall>
+    <methodName>domain.get_domain_orders</methodName>
+    <params>
+      <param>
+        <value>
+          <struct>
+            <member>
+              <name>login</name>
+              <value><string>%s</string></value>
+            </member>
+            <member>
+              <name>password</name>
+              <value><string>%s</string></value>
+            </member>
+          </struct>
+        </value>
+      </param>
+    </params>
+  </methodCall>' "$EUSERV_Username" "$EUSERV_Password")
+
+  export _H1="Content-Type: text/xml"
+  response="$(_post "$xml_content" "$EUSERV_Api" "" "POST")"
+
+  if ! _contains "$response" "<member><name>status</name><value><i4>100</i4></value></member>"; then
+    _err "Error could not get domain orders"
+    _debug "xml_content" "$xml_content"
+    _debug "response" "$response"
+    return 1
+  fi
+
+  # save response to reduce API calls
+  _euserv_domain_orders="$response"
+  return 0
+}
+
+_euserv_get_domain_id() {
+  # returns: _euserv_domain_id
+  domain=$1
+  _debug "get domain_id"
+
+  # find line where the domain name is within the $response
+  _startLine=$(echo "$_euserv_domain_orders" | grep -n '>domain_name<.*>'"$domain"'<' | cut -d ':' -f 1)
+  # next occurency of domain_id after the domain_name is the correct one
+  _euserv_domain_id=$(echo "$_euserv_domain_orders" | sed -n "$_startLine"',$p' | grep '>domain_id<' | _head_n 1 | sed 's/.*<i4>\([0-9]*\)<\/i4>.*/\1/')
+
+  if [ -z "$_euserv_domain_id" ]; then
+    _err "Could not find domain_id for domain $domain"
+    _debug "_euserv_domain_orders" "$_euserv_domain_orders"
+    return 1
+  fi
+
+  return 0
+}
+
+_euserv_delete_record() {
+  record_id=$1
+  xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
+  <methodCall>
+    <methodName>domain.dns_delete_record</methodName>
+    <params>
+      <param>
+       <value>
+         <struct>
+           <member>
+             <name>login</name>
+             <value>
+               <string>%s</string>
+             </value>
+            </member>
+            <member>
+              <name>password</name>
+              <value>
+                <string>%s</string>
+              </value>
+            </member>
+            <member>
+              <name>dns_record_id</name>
+              <value>
+                <int>%s</int>
+              </value>
+            </member>
+          </struct>
+        </value>
+      </param>
+    </params>
+  </methodCall>' "$EUSERV_Username" "$EUSERV_Password" "$record_id")
+
+  export _H1="Content-Type: text/xml"
+  response="$(_post "$xml_content" "$EUSERV_Api" "" "POST")"
+
+  if ! _contains "$response" "<member><name>status</name><value><i4>100</i4></value></member>"; then
+    _err "Error deleting record"
+    _debug "xml_content" "$xml_content"
+    _debug "response" "$response"
+    return 1
+  fi
+
+  return 0
+
+}
+
+_euserv_add_record() {
+  domain=$1
+  sub_domain=$2
+  txtval=$3
+
+  xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
+  <methodCall>
+  <methodName>domain.dns_create_record</methodName>
+  <params>
+   <param>
+    <value>
+     <struct>
+      <member>
+       <name>login</name>
+       <value>
+        <string>%s</string>
+       </value>
+      </member>
+      <member>
+       <name>password</name>
+       <value>
+        <string>%s</string></value>
+      </member>
+      <member>
+       <name>domain_id</name>
+       <value>
+        <int>%s</int>
+       </value>
+      </member>
+      <member>
+       <name>dns_record_subdomain</name>
+       <value>
+        <string>%s</string>
+       </value>
+      </member>
+      <member>
+       <name>dns_record_type</name>
+       <value>
+        <string>TXT</string>
+       </value>
+      </member>
+      <member>
+       <name>dns_record_value</name>
+       <value>
+        <string>%s</string>
+       </value>
+      </member>
+      <member>
+       <name>dns_record_ttl</name>
+       <value>
+        <int>300</int>
+       </value>
+      </member>
+     </struct>
+    </value>
+   </param>
+  </params>
+  </methodCall>' "$EUSERV_Username" "$EUSERV_Password" "$_euserv_domain_id" "$sub_domain" "$txtval")
+
+  export _H1="Content-Type: text/xml"
+  response="$(_post "$xml_content" "$EUSERV_Api" "" "POST")"
+
+  if ! _contains "$response" "<member><name>status</name><value><i4>100</i4></value></member>"; then
+    _err "Error could not create record"
+    _debug "xml_content" "$xml_content"
+    _debug "response" "$response"
+    return 1
+  fi
+
+  return 0
+}

+ 168 - 0
dnsapi/dns_exoscale.sh

@@ -0,0 +1,168 @@
+#!/usr/bin/env sh
+
+EXOSCALE_API=https://api.exoscale.com/dns/v1
+
+########  Public functions #####################
+
+# Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+# Used to add txt record
+dns_exoscale_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if ! _checkAuth; then
+    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"
+
+  _info "Adding record"
+  if _exoscale_rest POST "domains/$_domain_id/records" "{\"record\":{\"name\":\"$_sub_domain\",\"record_type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":120}}" "$_domain_token"; then
+    if _contains "$response" "$txtvalue"; then
+      _info "Added, OK"
+      return 0
+    fi
+  fi
+  _err "Add txt record error."
+  return 1
+
+}
+
+# Usage: fulldomain txtvalue
+# Used to remove the txt record after validation
+dns_exoscale_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if ! _checkAuth; then
+    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 txt records"
+  _exoscale_rest GET "domains/${_domain_id}/records?type=TXT&name=$_sub_domain" "" "$_domain_token"
+  if _contains "$response" "\"name\":\"$_sub_domain\"" >/dev/null; then
+    _record_id=$(echo "$response" | tr '{' "\n" | grep "\"content\":\"$txtvalue\"" | _egrep_o "\"id\":[^,]+" | _head_n 1 | cut -d : -f 2 | tr -d \")
+  fi
+
+  if [ -z "$_record_id" ]; then
+    _err "Can not get record id to remove."
+    return 1
+  fi
+
+  _debug "Deleting record $_record_id"
+
+  if ! _exoscale_rest DELETE "domains/$_domain_id/records/$_record_id" "" "$_domain_token"; then
+    _err "Delete record error."
+    return 1
+  fi
+
+  return 0
+}
+
+####################  Private functions below ##################################
+
+_checkAuth() {
+  EXOSCALE_API_KEY="${EXOSCALE_API_KEY:-$(_readaccountconf_mutable EXOSCALE_API_KEY)}"
+  EXOSCALE_SECRET_KEY="${EXOSCALE_SECRET_KEY:-$(_readaccountconf_mutable EXOSCALE_SECRET_KEY)}"
+
+  if [ -z "$EXOSCALE_API_KEY" ] || [ -z "$EXOSCALE_SECRET_KEY" ]; then
+    EXOSCALE_API_KEY=""
+    EXOSCALE_SECRET_KEY=""
+    _err "You don't specify Exoscale application key and application secret yet."
+    _err "Please create you key and try again."
+    return 1
+  fi
+
+  _saveaccountconf_mutable EXOSCALE_API_KEY "$EXOSCALE_API_KEY"
+  _saveaccountconf_mutable EXOSCALE_SECRET_KEY "$EXOSCALE_SECRET_KEY"
+
+  return 0
+}
+
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+# _domain_id=sdjkglgdfewsdfg
+# _domain_token=sdjkglgdfewsdfg
+_get_root() {
+
+  if ! _exoscale_rest GET "domains"; then
+    return 1
+  fi
+
+  domain=$1
+  i=2
+  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 _contains "$response" "\"name\":\"$h\"" >/dev/null; then
+      _domain_id=$(echo "$response" | tr '{' "\n" | grep "\"name\":\"$h\"" | _egrep_o "\"id\":[^,]+" | _head_n 1 | cut -d : -f 2 | tr -d \")
+      _domain_token=$(echo "$response" | tr '{' "\n" | grep "\"name\":\"$h\"" | _egrep_o "\"token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
+      if [ "$_domain_token" ] && [ "$_domain_id" ]; then
+        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+        _domain=$h
+        return 0
+      fi
+      return 1
+    fi
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+  return 1
+}
+
+# returns response
+_exoscale_rest() {
+  method=$1
+  path="$2"
+  data="$3"
+  token="$4"
+  request_url="$EXOSCALE_API/$path"
+  _debug "$path"
+
+  export _H1="Accept: application/json"
+
+  if [ "$token" ]; then
+    export _H2="X-DNS-Domain-Token: $token"
+  else
+    export _H2="X-DNS-Token: $EXOSCALE_API_KEY:$EXOSCALE_SECRET_KEY"
+  fi
+
+  if [ "$data" ] || [ "$method" = "DELETE" ]; then
+    export _H3="Content-Type: application/json"
+    _debug data "$data"
+    response="$(_post "$data" "$request_url" "" "$method")"
+  else
+    response="$(_get "$request_url" "" "" "$method")"
+  fi
+
+  if [ "$?" != "0" ]; then
+    _err "error $request_url"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}

+ 57 - 5
dnsapi/dns_gandi_livedns.sh

@@ -7,6 +7,7 @@
 # Requires GANDI API KEY set in GANDI_LIVEDNS_KEY set as environment variable
 #
 #Author: Frédéric Crozat <fcrozat@suse.com>
+#        Dominik Röttsches <drott@google.com>
 #Report Bugs here: https://github.com/fcrozat/acme.sh
 #
 ########  Public functions #####################
@@ -36,9 +37,7 @@ dns_gandi_livedns_add() {
   _debug domain "$_domain"
   _debug sub_domain "$_sub_domain"
 
-  _gandi_livedns_rest PUT "domains/$_domain/records/$_sub_domain/TXT" "{\"rrset_ttl\": 300, \"rrset_values\":[\"$txtvalue\"]}" \
-    && _contains "$response" '{"message": "DNS Record Created"}' \
-    && _info "Add $(__green "success")"
+  _dns_gandi_append_record "$_domain" "$_sub_domain" "$txtvalue"
 }
 
 #Usage: fulldomain txtvalue
@@ -56,9 +55,23 @@ dns_gandi_livedns_rm() {
   _debug fulldomain "$fulldomain"
   _debug domain "$_domain"
   _debug sub_domain "$_sub_domain"
+  _debug txtvalue "$txtvalue"
 
-  _gandi_livedns_rest DELETE "domains/$_domain/records/$_sub_domain/TXT" ""
-
+  if ! _dns_gandi_existing_rrset_values "$_domain" "$_sub_domain"; then
+    return 1
+  fi
+  _new_rrset_values=$(echo "$_rrset_values" | sed "s/...$txtvalue...//g")
+  # Cleanup dangling commata.
+  _new_rrset_values=$(echo "$_new_rrset_values" | sed "s/, ,/ ,/g")
+  _new_rrset_values=$(echo "$_new_rrset_values" | sed "s/, *\]/\]/g")
+  _new_rrset_values=$(echo "$_new_rrset_values" | sed "s/\[ *,/\[/g")
+  _debug "New rrset_values" "$_new_rrset_values"
+
+  _gandi_livedns_rest PUT \
+    "domains/$_domain/records/$_sub_domain/TXT" \
+    "{\"rrset_ttl\": 300, \"rrset_values\": $_new_rrset_values}" \
+    && _contains "$response" '{"message": "DNS Record Created"}' \
+    && _info "Removing record $(__green "success")"
 }
 
 ####################  Private functions below ##################################
@@ -98,6 +111,45 @@ _get_root() {
   return 1
 }
 
+_dns_gandi_append_record() {
+  domain=$1
+  sub_domain=$2
+  txtvalue=$3
+
+  if _dns_gandi_existing_rrset_values "$domain" "$sub_domain"; then
+    _debug "Appending new value"
+    _rrset_values=$(echo "$_rrset_values" | sed "s/\"]/\",\"$txtvalue\"]/")
+  else
+    _debug "Creating new record" "$_rrset_values"
+    _rrset_values="[\"$txtvalue\"]"
+  fi
+  _debug new_rrset_values "$_rrset_values"
+  _gandi_livedns_rest PUT "domains/$_domain/records/$sub_domain/TXT" \
+    "{\"rrset_ttl\": 300, \"rrset_values\": $_rrset_values}" \
+    && _contains "$response" '{"message": "DNS Record Created"}' \
+    && _info "Adding record $(__green "success")"
+}
+
+_dns_gandi_existing_rrset_values() {
+  domain=$1
+  sub_domain=$2
+  if ! _gandi_livedns_rest GET "domains/$domain/records/$sub_domain"; then
+    return 1
+  fi
+  if ! _contains "$response" '"rrset_type": "TXT"'; then
+    _debug "Does not have a _acme-challenge TXT record yet."
+    return 1
+  fi
+  if _contains "$response" '"rrset_values": \[\]'; then
+    _debug "Empty rrset_values for TXT record, no previous TXT record."
+    return 1
+  fi
+  _debug "Already has TXT record."
+  _rrset_values=$(echo "$response" | _egrep_o 'rrset_values.*\[.*\]' \
+    | _egrep_o '\[".*\"]')
+  return 0
+}
+
 _gandi_livedns_rest() {
   m=$1
   ep="$2"

+ 167 - 0
dnsapi/dns_gcloud.sh

@@ -0,0 +1,167 @@
+#!/usr/bin/env sh
+
+# Author: Janos Lenart <janos@lenart.io>
+
+########  Public functions #####################
+
+# Usage: dns_gcloud_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_gcloud_add() {
+  fulldomain=$1
+  txtvalue=$2
+  _info "Using gcloud"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  _dns_gcloud_find_zone || return $?
+
+  # Add an extra RR
+  _dns_gcloud_start_tr || return $?
+  _dns_gcloud_get_rrdatas || return $?
+  echo "$rrdatas" | _dns_gcloud_remove_rrs || return $?
+  printf "%s\n%s\n" "$rrdatas" "\"$txtvalue\"" | grep -v '^$' | _dns_gcloud_add_rrs || return $?
+  _dns_gcloud_execute_tr || return $?
+
+  _info "$fulldomain record added"
+}
+
+# Usage: dns_gcloud_rm   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+# Remove the txt record after validation.
+dns_gcloud_rm() {
+  fulldomain=$1
+  txtvalue=$2
+  _info "Using gcloud"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  _dns_gcloud_find_zone || return $?
+
+  # Remove one RR
+  _dns_gcloud_start_tr || return $?
+  _dns_gcloud_get_rrdatas || return $?
+  echo "$rrdatas" | _dns_gcloud_remove_rrs || return $?
+  echo "$rrdatas" | grep -F -v "\"$txtvalue\"" | _dns_gcloud_add_rrs || return $?
+  _dns_gcloud_execute_tr || return $?
+
+  _info "$fulldomain record added"
+}
+
+####################  Private functions below ##################################
+
+_dns_gcloud_start_tr() {
+  if ! trd=$(mktemp -d); then
+    _err "_dns_gcloud_start_tr: failed to create temporary directory"
+    return 1
+  fi
+  tr="$trd/tr.yaml"
+  _debug tr "$tr"
+
+  if ! gcloud dns record-sets transaction start \
+    --transaction-file="$tr" \
+    --zone="$managedZone"; then
+    rm -r "$trd"
+    _err "_dns_gcloud_start_tr: failed to execute transaction"
+    return 1
+  fi
+}
+
+_dns_gcloud_execute_tr() {
+  if ! gcloud dns record-sets transaction execute \
+    --transaction-file="$tr" \
+    --zone="$managedZone"; then
+    _debug tr "$(cat "$tr")"
+    rm -r "$trd"
+    _err "_dns_gcloud_execute_tr: failed to execute transaction"
+    return 1
+  fi
+  rm -r "$trd"
+
+  for i in $(seq 1 120); do
+    if gcloud dns record-sets changes list \
+      --zone="$managedZone" \
+      --filter='status != done' \
+      | grep -q '^.*'; then
+      _info "_dns_gcloud_execute_tr: waiting for transaction to be comitted ($i/120)..."
+      sleep 5
+    else
+      return 0
+    fi
+  done
+
+  _err "_dns_gcloud_execute_tr: transaction is still pending after 10 minutes"
+  rm -r "$trd"
+  return 1
+}
+
+_dns_gcloud_remove_rrs() {
+  if ! xargs --no-run-if-empty gcloud dns record-sets transaction remove \
+    --name="$fulldomain." \
+    --ttl="$ttl" \
+    --type=TXT \
+    --zone="$managedZone" \
+    --transaction-file="$tr"; then
+    _debug tr "$(cat "$tr")"
+    rm -r "$trd"
+    _err "_dns_gcloud_remove_rrs: failed to remove RRs"
+    return 1
+  fi
+}
+
+_dns_gcloud_add_rrs() {
+  ttl=60
+  if ! xargs --no-run-if-empty gcloud dns record-sets transaction add \
+    --name="$fulldomain." \
+    --ttl="$ttl" \
+    --type=TXT \
+    --zone="$managedZone" \
+    --transaction-file="$tr"; then
+    _debug tr "$(cat "$tr")"
+    rm -r "$trd"
+    _err "_dns_gcloud_add_rrs: failed to add RRs"
+    return 1
+  fi
+}
+
+_dns_gcloud_find_zone() {
+  # Prepare a filter that matches zones that are suiteable for this entry.
+  # For example, _acme-challenge.something.domain.com might need to go into something.domain.com or domain.com;
+  # this function finds the longest postfix that has a managed zone.
+  part="$fulldomain"
+  filter="dnsName=( "
+  while [ "$part" != "" ]; do
+    filter="$filter$part. "
+    part="$(echo "$part" | sed 's/[^.]*\.*//')"
+  done
+  filter="$filter)"
+  _debug filter "$filter"
+
+  # List domains and find the longest match (in case of some levels of delegation)
+  if ! match=$(gcloud dns managed-zones list \
+    --format="value(name, dnsName)" \
+    --filter="$filter" \
+    | while read -r dnsName name; do
+      printf "%s\t%s\t%s\n" "${#dnsName}" "$dnsName" "$name"
+    done \
+    | sort -n -r | _head_n 1 | cut -f2,3 | grep '^.*'); then
+    _err "_dns_gcloud_find_zone: Can't find a matching managed zone! Perhaps wrong project or gcloud credentials?"
+    return 1
+  fi
+
+  dnsName=$(echo "$match" | cut -f2)
+  _debug dnsName "$dnsName"
+  managedZone=$(echo "$match" | cut -f1)
+  _debug managedZone "$managedZone"
+}
+
+_dns_gcloud_get_rrdatas() {
+  if ! rrdatas=$(gcloud dns record-sets list \
+    --zone="$managedZone" \
+    --name="$fulldomain." \
+    --type=TXT \
+    --format="value(ttl,rrdatas)"); then
+    _err "_dns_gcloud_get_rrdatas: Failed to list record-sets"
+    rm -r "$trd"
+    return 1
+  fi
+  ttl=$(echo "$rrdatas" | cut -f1)
+  rrdatas=$(echo "$rrdatas" | cut -f2 | sed 's/","/"\n"/g')
+}

+ 9 - 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
@@ -174,5 +168,9 @@ _gd_rest() {
     return 1
   fi
   _debug2 response "$response"
+  if _contains "$response" "UNABLE_TO_AUTHENTICATE"; then
+    _err "It seems that your api key or secret is not correct."
+    return 1
+  fi
   return 0
 }

+ 168 - 0
dnsapi/dns_gdnsdk.sh

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

+ 17 - 6
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="
@@ -89,7 +92,9 @@ dns_he_rm() {
     return 1
   fi
   # Remove the record
-  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&menu=edit_zone"
   body="$body&hosted_dns_zoneid=$_zone_id"
   body="$body&hosted_dns_recordid=$_record_id"
@@ -112,9 +117,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 +154,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")

+ 161 - 0
dnsapi/dns_hostingde.sh

@@ -0,0 +1,161 @@
+#!/usr/bin/env sh
+
+# hosting.de API
+
+# Values to export:
+# export HOSTINGDE_ENDPOINT='https://secure.hosting.de'
+# export HOSTINGDE_APIKEY='xxxxx'
+
+########  Public functions #####################
+
+dns_hostingde_add() {
+  fulldomain="${1}"
+  txtvalue="${2}"
+  _debug "Calling: _hostingde_addRecord() '${fulldomain}' '${txtvalue}'"
+  _hostingde_apiKey && _hostingde_getZoneConfig && _hostingde_addRecord
+  return $?
+}
+
+dns_hostingde_rm() {
+  fulldomain="${1}"
+  txtvalue="${2}"
+  _debug "Calling: _hostingde_removeRecord() '${fulldomain}' '${txtvalue}'"
+  _hostingde_apiKey && _hostingde_getZoneConfig && _hostingde_removeRecord
+  return $?
+}
+
+#################### own Private functions below ##################################
+
+_hostingde_apiKey() {
+  HOSTINGDE_APIKEY="${HOSTINGDE_APIKEY:-$(_readaccountconf_mutable HOSTINGDE_APIKEY)}"
+  if [ -z "$HOSTINGDE_APIKEY" ] || [ -z "$HOSTINGDE_ENDPOINT" ]; then
+    HOSTINGDE_APIKEY=""
+    HOSTINGDE_ENDPOINT=""
+    _err "You haven't specified hosting.de API key or endpoint yet."
+    _err "Please create your key and try again."
+    return 1
+  fi
+
+  _saveaccountconf_mutable HOSTINGDE_APIKEY "$HOSTINGDE_APIKEY"
+  _saveaccountconf_mutable HOSTINGDE_ENDPOINT "$HOSTINGDE_ENDPOINT"
+}
+
+_hostingde_parse() {
+  find="${1}"
+  if [ "${2}" ]; then
+    notfind="${2}"
+  fi
+  if [ "${notfind}" ]; then
+    _egrep_o \""${find}\":.*" | grep -v "${notfind}" | cut -d ':' -f 2 | cut -d ',' -f 1 | tr -d ' '
+  else
+    _egrep_o \""${find}\":.*" | cut -d ':' -f 2 | cut -d ',' -f 1 | tr -d ' '
+  fi
+}
+
+_hostingde_getZoneConfig() {
+  _info "Getting ZoneConfig"
+  curZone="${fulldomain#*.}"
+  returnCode=1
+  while _contains "${curZone}" "\\."; do
+    curData="{\"filter\":{\"field\":\"zoneName\",\"value\":\"${curZone}\"},\"limit\":1,\"authToken\":\"${HOSTINGDE_APIKEY}\"}"
+    curResult="$(_post "${curData}" "${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneConfigsFind")"
+    _debug "Calling zoneConfigsFind: '${curData}' '${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneConfigsFind'"
+    _debug "Result of zoneConfigsFind: '$curResult'"
+    if _contains "${curResult}" '"status": "error"'; then
+      if _contains "${curResult}" '"code": 10109'; then
+        _err "The API-Key is invalid or could not be found"
+      else
+        _err "UNKNOWN API ERROR"
+      fi
+      returnCode=1
+      break
+    fi
+    if _contains "${curResult}" '"totalEntries": 1'; then
+      _info "Retrieved zone data."
+      _debug "Zone data: '${curResult}'"
+      zoneConfigId=$(echo "${curResult}" | _hostingde_parse "id")
+      zoneConfigName=$(echo "${curResult}" | _hostingde_parse "name")
+      zoneConfigType=$(echo "${curResult}" | _hostingde_parse "type" "FindZoneConfigsResult")
+      zoneConfigExpire=$(echo "${curResult}" | _hostingde_parse "expire")
+      zoneConfigNegativeTtl=$(echo "${curResult}" | _hostingde_parse "negativeTtl")
+      zoneConfigRefresh=$(echo "${curResult}" | _hostingde_parse "refresh")
+      zoneConfigRetry=$(echo "${curResult}" | _hostingde_parse "retry")
+      zoneConfigTtl=$(echo "${curResult}" | _hostingde_parse "ttl")
+      zoneConfigDnsServerGroupId=$(echo "${curResult}" | _hostingde_parse "dnsServerGroupId")
+      zoneConfigEmailAddress=$(echo "${curResult}" | _hostingde_parse "emailAddress")
+      zoneConfigDnsSecMode=$(echo "${curResult}" | _hostingde_parse "dnsSecMode")
+      if [ "${zoneConfigType}" != "\"NATIVE\"" ]; then
+        _err "Zone is not native"
+        returnCode=1
+        break
+      fi
+      _debug "zoneConfigId '${zoneConfigId}'"
+      returnCode=0
+      break
+    fi
+    curZone="${curZone#*.}"
+  done
+  if [ $returnCode -ne 0 ]; then
+    _info "ZoneEnd reached, Zone ${curZone} not found in hosting.de API"
+  fi
+  return $returnCode
+}
+
+_hostingde_getZoneStatus() {
+  _debug "Checking Zone status"
+  curData="{\"filter\":{\"field\":\"zoneConfigId\",\"value\":${zoneConfigId}},\"limit\":1,\"authToken\":\"${HOSTINGDE_APIKEY}\"}"
+  curResult="$(_post "${curData}" "${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zonesFind")"
+  _debug "Calling zonesFind '${curData}' '${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zonesFind'"
+  _debug "Result of zonesFind '$curResult'"
+  zoneStatus=$(echo "${curResult}" | _hostingde_parse "status" "success")
+  _debug "zoneStatus '${zoneStatus}'"
+  return 0
+}
+
+_hostingde_addRecord() {
+  _info "Adding record to zone"
+  _hostingde_getZoneStatus
+  _debug "Result of zoneStatus: '${zoneStatus}'"
+  while [ "${zoneStatus}" != "\"active\"" ]; do
+    _sleep 5
+    _hostingde_getZoneStatus
+    _debug "Result of zoneStatus: '${zoneStatus}'"
+  done
+  curData="{\"authToken\":\"${HOSTINGDE_APIKEY}\",\"zoneConfig\":{\"id\":${zoneConfigId},\"name\":${zoneConfigName},\"type\":${zoneConfigType},\"dnsServerGroupId\":${zoneConfigDnsServerGroupId},\"dnsSecMode\":${zoneConfigDnsSecMode},\"emailAddress\":${zoneConfigEmailAddress},\"soaValues\":{\"expire\":${zoneConfigExpire},\"negativeTtl\":${zoneConfigNegativeTtl},\"refresh\":${zoneConfigRefresh},\"retry\":${zoneConfigRetry},\"ttl\":${zoneConfigTtl}}},\"recordsToAdd\":[{\"name\":\"${fulldomain}\",\"type\":\"TXT\",\"content\":\"\\\"${txtvalue}\\\"\",\"ttl\":3600}]}"
+  curResult="$(_post "${curData}" "${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneUpdate")"
+  _debug "Calling zoneUpdate: '${curData}' '${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneUpdate'"
+  _debug "Result of zoneUpdate: '$curResult'"
+  if _contains "${curResult}" '"status": "error"'; then
+    if _contains "${curResult}" '"code": 10109'; then
+      _err "The API-Key is invalid or could not be found"
+    else
+      _err "UNKNOWN API ERROR"
+    fi
+    return 1
+  fi
+  return 0
+}
+
+_hostingde_removeRecord() {
+  _info "Removing record from zone"
+  _hostingde_getZoneStatus
+  _debug "Result of zoneStatus: '$zoneStatus'"
+  while [ "$zoneStatus" != "\"active\"" ]; do
+    _sleep 5
+    _hostingde_getZoneStatus
+    _debug "Result of zoneStatus: '$zoneStatus'"
+  done
+  curData="{\"authToken\":\"${HOSTINGDE_APIKEY}\",\"zoneConfig\":{\"id\":${zoneConfigId},\"name\":${zoneConfigName},\"type\":${zoneConfigType},\"dnsServerGroupId\":${zoneConfigDnsServerGroupId},\"dnsSecMode\":${zoneConfigDnsSecMode},\"emailAddress\":${zoneConfigEmailAddress},\"soaValues\":{\"expire\":${zoneConfigExpire},\"negativeTtl\":${zoneConfigNegativeTtl},\"refresh\":${zoneConfigRefresh},\"retry\":${zoneConfigRetry},\"ttl\":${zoneConfigTtl}}},\"recordsToDelete\":[{\"name\":\"${fulldomain}\",\"type\":\"TXT\",\"content\":\"\\\"${txtvalue}\\\"\"}]}"
+  curResult="$(_post "${curData}" "${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneUpdate")"
+  _debug "Calling zoneUpdate: '${curData}' '${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneUpdate'"
+  _debug "Result of zoneUpdate: '$curResult'"
+  if _contains "${curResult}" '"status": "error"'; then
+    if _contains "${curResult}" '"code": 10109'; then
+      _err "The API-Key is invalid or could not be found"
+    else
+      _err "UNKNOWN API ERROR"
+    fi
+    return 1
+  fi
+  return 0
+}

+ 48 - 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,47 @@ _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" "<member><name>code</name><value><int>1000</int></value></member>" \
+    && _contains "$response" "<member><name>tfa</name><value><string>GOOGLE-AUTH</string></value></member>"; 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 +206,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>

+ 2 - 2
dnsapi/dns_ispconfig.sh

@@ -128,7 +128,7 @@ _ISPC_addTxt() {
   curSerial="$(date +%s)"
   curStamp="$(date +'%F %T')"
   params="\"server_id\":\"${server_id}\",\"zone\":\"${zone}\",\"name\":\"${fulldomain}.\",\"type\":\"txt\",\"data\":\"${txtvalue}\",\"aux\":\"0\",\"ttl\":\"3600\",\"active\":\"y\",\"stamp\":\"${curStamp}\",\"serial\":\"${curSerial}\""
-  curData="{\"session_id\":\"${sessionID}\",\"client_id\":\"${client_id}\",\"params\":{${params}}}"
+  curData="{\"session_id\":\"${sessionID}\",\"client_id\":\"${client_id}\",\"params\":{${params}},\"update_serial\":true}"
   curResult="$(_post "${curData}" "${ISPC_Api}?dns_txt_add")"
   _debug "Calling _ISPC_addTxt: '${curData}' '${ISPC_Api}?dns_txt_add'"
   _debug "Result of _ISPC_addTxt: '$curResult'"
@@ -160,7 +160,7 @@ _ISPC_rmTxt() {
       *)
         unset IFS
         _info "Retrieved Record ID."
-        curData="{\"session_id\":\"${sessionID}\",\"primary_id\":\"${record_id}\"}"
+        curData="{\"session_id\":\"${sessionID}\",\"primary_id\":\"${record_id}\",\"update_serial\":true}"
         curResult="$(_post "${curData}" "${ISPC_Api}?dns_txt_delete")"
         _debug "Calling _ISPC_rmTxt: '${curData}' '${ISPC_Api}?dns_txt_delete'"
         _debug "Result of _ISPC_rmTxt: '$curResult'"

+ 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 <felipebraz@kinghost.com.br>  #
+# Report Bugs here: https://github.com/kinghost/acme.sh    #
+#                                                          #
+# Values to export:                                        #
+# export KINGHOST_Username="email@provider.com"            #
+# 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
+}

+ 40 - 15
dnsapi/dns_lexicon.sh

@@ -7,20 +7,13 @@ lexicon_cmd="lexicon"
 
 wiki="https://github.com/Neilpang/acme.sh/wiki/How-to-use-lexicon-dns-api"
 
-########  Public functions #####################
-
-#Usage: add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
-dns_lexicon_add() {
-  fulldomain=$1
-  txtvalue=$2
-
-  domain=$(printf "%s" "$fulldomain" | cut -d . -f 2-999)
-
+_lexicon_init() {
   if ! _exists "$lexicon_cmd"; then
     _err "Please install $lexicon_cmd first: $wiki"
     return 1
   fi
 
+  PROVIDER="${PROVIDER:-$(_readdomainconf PROVIDER)}"
   if [ -z "$PROVIDER" ]; then
     PROVIDER=""
     _err "Please define env PROVIDER first: $wiki"
@@ -33,46 +26,78 @@ dns_lexicon_add() {
   # e.g. busybox-ash does not know [:upper:]
   # shellcheck disable=SC2018,SC2019
   Lx_name=$(echo LEXICON_"${PROVIDER}"_USERNAME | tr 'a-z' 'A-Z')
+  eval "$Lx_name=\${$Lx_name:-$(_readaccountconf_mutable "$Lx_name")}"
   Lx_name_v=$(eval echo \$"$Lx_name")
   _secure_debug "$Lx_name" "$Lx_name_v"
   if [ "$Lx_name_v" ]; then
-    _saveaccountconf "$Lx_name" "$Lx_name_v"
+    _saveaccountconf_mutable "$Lx_name" "$Lx_name_v"
     eval export "$Lx_name"
   fi
 
   # shellcheck disable=SC2018,SC2019
   Lx_token=$(echo LEXICON_"${PROVIDER}"_TOKEN | tr 'a-z' 'A-Z')
+  eval "$Lx_token=\${$Lx_token:-$(_readaccountconf_mutable "$Lx_token")}"
   Lx_token_v=$(eval echo \$"$Lx_token")
   _secure_debug "$Lx_token" "$Lx_token_v"
   if [ "$Lx_token_v" ]; then
-    _saveaccountconf "$Lx_token" "$Lx_token_v"
+    _saveaccountconf_mutable "$Lx_token" "$Lx_token_v"
     eval export "$Lx_token"
   fi
 
   # shellcheck disable=SC2018,SC2019
   Lx_password=$(echo LEXICON_"${PROVIDER}"_PASSWORD | tr 'a-z' 'A-Z')
+  eval "$Lx_password=\${$Lx_password:-$(_readaccountconf_mutable "$Lx_password")}"
   Lx_password_v=$(eval echo \$"$Lx_password")
   _secure_debug "$Lx_password" "$Lx_password_v"
   if [ "$Lx_password_v" ]; then
-    _saveaccountconf "$Lx_password" "$Lx_password_v"
+    _saveaccountconf_mutable "$Lx_password" "$Lx_password_v"
     eval export "$Lx_password"
   fi
 
   # shellcheck disable=SC2018,SC2019
   Lx_domaintoken=$(echo LEXICON_"${PROVIDER}"_DOMAINTOKEN | tr 'a-z' 'A-Z')
+  eval "$Lx_domaintoken=\${$Lx_domaintoken:-$(_readaccountconf_mutable "$Lx_domaintoken")}"
   Lx_domaintoken_v=$(eval echo \$"$Lx_domaintoken")
   _secure_debug "$Lx_domaintoken" "$Lx_domaintoken_v"
   if [ "$Lx_domaintoken_v" ]; then
+    _saveaccountconf_mutable "$Lx_domaintoken" "$Lx_domaintoken_v"
     eval export "$Lx_domaintoken"
-    _saveaccountconf "$Lx_domaintoken" "$Lx_domaintoken_v"
   fi
+}
+
+########  Public functions #####################
+
+#Usage: dns_lexicon_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_lexicon_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if ! _lexicon_init; then
+    return 1
+  fi
+
+  domain=$(printf "%s" "$fulldomain" | cut -d . -f 2-999)
+
+  _secure_debug LEXICON_OPTS "$LEXICON_OPTS"
+  _savedomainconf LEXICON_OPTS "$LEXICON_OPTS"
 
-  $lexicon_cmd "$PROVIDER" create "${domain}" TXT --name="_acme-challenge.${domain}." --content="${txtvalue}"
+  # shellcheck disable=SC2086
+  $lexicon_cmd "$PROVIDER" $LEXICON_OPTS create "${domain}" TXT --name="_acme-challenge.${domain}." --content="${txtvalue}"
 
 }
 
-#fulldomain
+#Usage: dns_lexicon_rm   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
 dns_lexicon_rm() {
   fulldomain=$1
+  txtvalue=$2
+
+  if ! _lexicon_init; then
+    return 1
+  fi
+
+  domain=$(printf "%s" "$fulldomain" | cut -d . -f 2-999)
+
+  # shellcheck disable=SC2086
+  $lexicon_cmd "$PROVIDER" $LEXICON_OPTS delete "${domain}" TXT --name="_acme-challenge.${domain}." --content="${txtvalue}"
 
 }

+ 185 - 0
dnsapi/dns_linode_v4.sh

@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#Original Author: Philipp Grosswiler <philipp.grosswiler@swiss-design.net>
+#v4 Update Author: Aaron W. Swenson <aaron@grandmasfridge.org>
+
+LINODE_V4_API_URL="https://api.linode.com/v4/domains"
+
+########  Public functions #####################
+
+#Usage: dns_linode_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_linode_v4_add() {
+  fulldomain="${1}"
+  txtvalue="${2}"
+
+  if ! _Linode_API; then
+    return 1
+  fi
+
+  _info "Using Linode"
+  _debug "Calling: dns_linode_add() '${fulldomain}' '${txtvalue}'"
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "Domain does not exist."
+    return 1
+  fi
+  _debug _domain_id "$_domain_id"
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  _payload="{
+              \"type\": \"TXT\",
+              \"name\": \"$_sub_domain\",
+              \"target\": \"$txtvalue\"
+            }"
+
+  if _rest POST "/$_domain_id/records" "$_payload" && [ -n "$response" ]; then
+    _resource_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\s*[0-9]+" | cut -d : -f 2 | tr -d " " | _head_n 1)
+    _debug _resource_id "$_resource_id"
+
+    if [ -z "$_resource_id" ]; then
+      _err "Error adding the domain resource."
+      return 1
+    fi
+
+    _info "Domain resource successfully added."
+    return 0
+  fi
+
+  return 1
+}
+
+#Usage: dns_linode_rm   _acme-challenge.www.domain.com
+dns_linode_v4_rm() {
+  fulldomain="${1}"
+
+  if ! _Linode_API; then
+    return 1
+  fi
+
+  _info "Using Linode"
+  _debug "Calling: dns_linode_rm() '${fulldomain}'"
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "Domain does not exist."
+    return 1
+  fi
+  _debug _domain_id "$_domain_id"
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  if _rest GET "/$_domain_id/records" && [ -n "$response" ]; then
+    response="$(echo "$response" | tr -d "\n" | tr '{' "|" | sed 's/|/&{/g' | tr "|" "\n")"
+
+    resource="$(echo "$response" | _egrep_o "{.*\"name\":\s*\"$_sub_domain\".*}")"
+    if [ "$resource" ]; then
+      _resource_id=$(printf "%s\n" "$resource" | _egrep_o "\"id\":\s*[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ )
+      if [ "$_resource_id" ]; then
+        _debug _resource_id "$_resource_id"
+
+        if _rest DELETE "/$_domain_id/records/$_resource_id" && [ -n "$response" ]; then
+          # On 200/OK, empty set is returned. Check for error, if any.
+          _error_response=$(printf "%s\n" "$response" | _egrep_o "\"errors\"" | cut -d : -f 2 | tr -d " " | _head_n 1)
+
+          if [ -n "$_error_response" ]; then
+            _err "Error deleting the domain resource: $_error_response"
+            return 1
+          fi
+
+          _info "Domain resource successfully deleted."
+          return 0
+        fi
+      fi
+
+      return 1
+    fi
+
+    return 0
+  fi
+
+  return 1
+}
+
+####################  Private functions below ##################################
+
+_Linode_API() {
+  if [ -z "$LINODE_V4_API_KEY" ]; then
+    LINODE_V4_API_KEY=""
+
+    _err "You didn't specify the Linode v4 API key yet."
+    _err "Please create your key and try again."
+
+    return 1
+  fi
+
+  _saveaccountconf LINODE_V4_API_KEY "$LINODE_V4_API_KEY"
+}
+
+####################  Private functions below ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+# _domain_id=12345
+_get_root() {
+  domain=$1
+  i=2
+  p=1
+
+  if _rest GET; then
+    response="$(echo "$response" | tr -d "\n" | tr '{' "|" | sed 's/|/&{/g' | tr "|" "\n")"
+    while true; do
+      h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+      _debug h "$h"
+      if [ -z "$h" ]; then
+        #not valid
+        return 1
+      fi
+
+      hostedzone="$(echo "$response" | _egrep_o "{.*\"domain\":\s*\"$h\".*}")"
+      if [ "$hostedzone" ]; then
+        _domain_id=$(printf "%s\n" "$hostedzone" | _egrep_o "\"id\":\s*[0-9]+" | _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
+          return 0
+        fi
+        return 1
+      fi
+      p=$i
+      i=$(_math "$i" + 1)
+    done
+  fi
+  return 1
+}
+
+#method method action data
+_rest() {
+  mtd="$1"
+  ep="$2"
+  data="$3"
+
+  _debug mtd "$mtd"
+  _debug ep "$ep"
+
+  export _H1="Accept: application/json"
+  export _H2="Content-Type: application/json"
+  export _H3="Authorization: Bearer $LINODE_V4_API_KEY"
+
+  if [ "$mtd" != "GET" ]; then
+    # both POST and DELETE.
+    _debug data "$data"
+    response="$(_post "$data" "$LINODE_V4_API_URL$ep" "" "$mtd")"
+  else
+    response="$(_get "$LINODE_V4_API_URL$ep$data")"
+  fi
+
+  if [ "$?" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}

+ 286 - 0
dnsapi/dns_loopia.sh

@@ -0,0 +1,286 @@
+#!/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_sub_domain "$_domain" "$_sub_domain"
+  _loopia_add_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 ##################################
+
+_loopia_get_records() {
+  domain=$1
+  sub_domain=$2
+
+  xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
+  <methodCall>
+    <methodName>getZoneRecords</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" "<array>"; then
+    _err "Error"
+    return 1
+  fi
+  return 0
+}
+
+_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_add_record() {
+  domain=$1
+  sub_domain=$2
+  txtval=$3
+
+  xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
+  <methodCall>
+    <methodName>addZoneRecord</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>
+        </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
+}
+
+_sub_domain_exists() {
+  domain=$1
+  sub_domain=$2
+
+  xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
+  <methodCall>
+    <methodName>getSubdomains</methodName>
+    <params>
+      <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")
+
+  response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")"
+
+  if _contains "$response" "$sub_domain"; then
+    return 0
+  fi
+  return 1
+}
+
+_loopia_add_sub_domain() {
+  domain=$1
+  sub_domain=$2
+
+  if _sub_domain_exists "$domain" "$sub_domain"; then
+    return 0
+  fi
+
+  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
+}

+ 97 - 0
dnsapi/dns_mydevil.sh

@@ -0,0 +1,97 @@
+#!/usr/bin/env sh
+
+# MyDevil.net API (2019-02-03)
+#
+# MyDevil.net already supports automatic Let's Encrypt certificates,
+# except for wildcard domains.
+#
+# This script depends on `devil` command that MyDevil.net provides,
+# which means that it works only on server side.
+#
+# Author: Marcin Konicki <https://ahwayakchih.neoni.net>
+#
+########  Public functions #####################
+
+#Usage: dns_mydevil_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_mydevil_add() {
+  fulldomain=$1
+  txtvalue=$2
+  domain=""
+
+  if ! _exists "devil"; then
+    _err "Could not find 'devil' command."
+    return 1
+  fi
+
+  _info "Using mydevil"
+
+  domain=$(mydevil_get_domain "$fulldomain")
+  if [ -z "$domain" ]; then
+    _err "Invalid domain name: could not find root domain of $fulldomain."
+    return 1
+  fi
+
+  # No need to check if record name exists, `devil` always adds new record.
+  # In worst case scenario, we end up with multiple identical records.
+
+  _info "Adding $fulldomain record for domain $domain"
+  if devil dns add "$domain" "$fulldomain" TXT "$txtvalue"; then
+    _info "Successfully added TXT record, ready for validation."
+    return 0
+  else
+    _err "Unable to add DNS record."
+    return 1
+  fi
+}
+
+#Usage: fulldomain txtvalue
+#Remove the txt record after validation.
+dns_mydevil_rm() {
+  fulldomain=$1
+  txtvalue=$2
+  domain=""
+
+  if ! _exists "devil"; then
+    _err "Could not find 'devil' command."
+    return 1
+  fi
+
+  _info "Using mydevil"
+
+  domain=$(mydevil_get_domain "$fulldomain")
+  if [ -z "$domain" ]; then
+    _err "Invalid domain name: could not find root domain of $fulldomain."
+    return 1
+  fi
+
+  # catch one or more numbers
+  num='[0-9][0-9]*'
+  # catch one or more whitespace
+  w=$(printf '[\t ][\t ]*')
+  # catch anything, except newline
+  any='.*'
+  # filter to make sure we do not delete other records
+  validRecords="^${num}${w}${fulldomain}${w}TXT${w}${any}${txtvalue}$"
+  for id in $(devil dns list "$domain" | tail -n+2 | grep "${validRecords}" | cut -w -s -f 1); do
+    _info "Removing record $id from domain $domain"
+    devil dns del "$domain" "$id" || _err "Could not remove DNS record."
+  done
+}
+
+####################  Private functions below ##################################
+
+# Usage: domain=$(mydevil_get_domain "_acme-challenge.www.domain.com" || _err "Invalid domain name")
+#        echo $domain
+mydevil_get_domain() {
+  fulldomain=$1
+  domain=""
+
+  for domain in $(devil dns list | cut -w -s -f 1 | tail -n+2); do
+    if _endswith "$fulldomain" "$domain"; then
+      printf -- "%s" "$domain"
+      return 0
+    fi
+  done
+
+  return 1
+}

+ 210 - 0
dnsapi/dns_mydnsjp.sh

@@ -0,0 +1,210 @@
+#!/usr/bin/env sh
+
+#Here is a api script for MyDNS.JP.
+#This file name is "dns_mydnsjp.sh"
+#So, here must be a method   dns_mydnsjp_add()
+#Which will be called by acme.sh to add the txt record to your api system.
+#returns 0 means success, otherwise error.
+#
+#Author: epgdatacapbon
+#Report Bugs here: https://github.com/epgdatacapbon/acme.sh
+#
+########  Public functions #####################
+
+# Export MyDNS.JP MasterID and Password in following variables...
+#  MYDNSJP_MasterID=MasterID
+#  MYDNSJP_Password=Password
+
+MYDNSJP_API="https://www.mydns.jp"
+
+#Usage: dns_mydnsjp_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_mydnsjp_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  _info "Using mydnsjp"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  # Load the credentials from the account conf file
+  MYDNSJP_MasterID="${MYDNSJP_MasterID:-$(_readaccountconf_mutable MYDNSJP_MasterID)}"
+  MYDNSJP_Password="${MYDNSJP_Password:-$(_readaccountconf_mutable MYDNSJP_Password)}"
+  if [ -z "$MYDNSJP_MasterID" ] || [ -z "$MYDNSJP_Password" ]; then
+    MYDNSJP_MasterID=""
+    MYDNSJP_Password=""
+    _err "You don't specify mydnsjp api MasterID and Password yet."
+    _err "Please export as MYDNSJP_MasterID / MYDNSJP_Password and try again."
+    return 1
+  fi
+
+  # Save the credentials to the account conf file
+  _saveaccountconf_mutable MYDNSJP_MasterID "$MYDNSJP_MasterID"
+  _saveaccountconf_mutable MYDNSJP_Password "$MYDNSJP_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"
+
+  if _mydnsjp_api "REGIST" "$_domain" "$txtvalue"; then
+    if printf -- "%s" "$response" | grep "OK." >/dev/null; then
+      _info "Added, OK"
+      return 0
+    else
+      _err "Add txt record error."
+      return 1
+    fi
+  fi
+  _err "Add txt record error."
+
+  return 1
+}
+
+#Usage: fulldomain txtvalue
+#Remove the txt record after validation.
+dns_mydnsjp_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  _info "Removing TXT record"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  # Load the credentials from the account conf file
+  MYDNSJP_MasterID="${MYDNSJP_MasterID:-$(_readaccountconf_mutable MYDNSJP_MasterID)}"
+  MYDNSJP_Password="${MYDNSJP_Password:-$(_readaccountconf_mutable MYDNSJP_Password)}"
+  if [ -z "$MYDNSJP_MasterID" ] || [ -z "$MYDNSJP_Password" ]; then
+    MYDNSJP_MasterID=""
+    MYDNSJP_Password=""
+    _err "You don't specify mydnsjp api MasterID and Password yet."
+    _err "Please export as MYDNSJP_MasterID / MYDNSJP_Password and try again."
+    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"
+
+  if _mydnsjp_api "DELETE" "$_domain" "$txtvalue"; then
+    if printf -- "%s" "$response" | grep "OK." >/dev/null; then
+      _info "Deleted, OK"
+      return 0
+    else
+      _err "Delete txt record error."
+      return 1
+    fi
+  fi
+  _err "Delete txt record error."
+
+  return 1
+}
+
+####################  Private functions below ##################################
+# _acme-challenge.www.domain.com
+# returns
+#  _sub_domain=_acme-challenge.www
+#  _domain=domain.com
+_get_root() {
+  fulldomain=$1
+  i=2
+  p=1
+
+  # Get the root domain
+  _mydnsjp_retrieve_domain
+  if [ "$?" != "0" ]; then
+    # not valid
+    return 1
+  fi
+
+  while true; do
+    _domain=$(printf "%s" "$fulldomain" | cut -d . -f $i-100)
+
+    if [ -z "$_domain" ]; then
+      # not valid
+      return 1
+    fi
+
+    if [ "$_domain" = "$_root_domain" ]; then
+      _sub_domain=$(printf "%s" "$fulldomain" | cut -d . -f 1-$p)
+      return 0
+    fi
+
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+
+  return 1
+}
+
+# Retrieve the root domain
+# returns 0 success
+_mydnsjp_retrieve_domain() {
+  _debug "Login to MyDNS.JP"
+
+  response="$(_post "masterid=$MYDNSJP_MasterID&masterpwd=$MYDNSJP_Password" "$MYDNSJP_API/?MENU=100")"
+  cookie="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2)"
+
+  # If cookies is not empty then logon successful
+  if [ -z "$cookie" ]; then
+    _err "Fail to get a cookie."
+    return 1
+  fi
+
+  _debug "Retrieve DOMAIN INFO page"
+
+  export _H1="Cookie:${cookie}"
+
+  response="$(_get "$MYDNSJP_API/?MENU=300")"
+
+  if [ "$?" != "0" ]; then
+    _err "Fail to retrieve DOMAIN INFO."
+    return 1
+  fi
+
+  _root_domain=$(echo "$response" | grep "DNSINFO\[domainname\]" | sed 's/^.*value="\([^"]*\)".*/\1/')
+
+  # Logout
+  response="$(_get "$MYDNSJP_API/?MENU=090")"
+
+  _debug _root_domain "$_root_domain"
+
+  if [ -z "$_root_domain" ]; then
+    _err "Fail to get the root domain."
+    return 1
+  fi
+
+  return 0
+}
+
+_mydnsjp_api() {
+  cmd=$1
+  domain=$2
+  txtvalue=$3
+
+  # Base64 encode the credentials
+  credentials=$(printf "%s:%s" "$MYDNSJP_MasterID" "$MYDNSJP_Password" | _base64)
+
+  # Construct the HTTP Authorization header
+  export _H1="Content-Type: application/x-www-form-urlencoded"
+  export _H2="Authorization: Basic ${credentials}"
+
+  response="$(_post "CERTBOT_DOMAIN=$domain&CERTBOT_VALIDATION=$txtvalue&EDIT_CMD=$cmd" "$MYDNSJP_API/directedit.html")"
+
+  if [ "$?" != "0" ]; then
+    _err "error $domain"
+    return 1
+  fi
+
+  _debug2 response "$response"
+
+  return 0
+}

+ 407 - 0
dnsapi/dns_namecheap.sh

@@ -0,0 +1,407 @@
+#!/usr/bin/env sh
+
+# Namecheap API
+# https://www.namecheap.com/support/api/intro.aspx
+#
+# Requires Namecheap API key set in 
+#NAMECHEAP_API_KEY, 
+#NAMECHEAP_USERNAME,
+#NAMECHEAP_SOURCEIP 
+# Due to Namecheap's API limitation all the records of your domain will be read and re applied, make sure to have a backup of your records you could apply if any issue would arise.
+
+########  Public functions #####################
+
+NAMECHEAP_API="https://api.namecheap.com/xml.response"
+
+#Usage: dns_namecheap_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_namecheap_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if ! _namecheap_check_config; then
+    _err "$error"
+    return 1
+  fi
+
+  if ! _namecheap_set_publicip; then
+    return 1
+  fi
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+  _debug domain "$_domain"
+  _debug sub_domain "$_sub_domain"
+
+  _set_namecheap_TXT "$_domain" "$_sub_domain" "$txtvalue"
+}
+
+#Usage: fulldomain txtvalue
+#Remove the txt record after validation.
+dns_namecheap_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if ! _namecheap_set_publicip; then
+    return 1
+  fi
+
+  if ! _namecheap_check_config; then
+    _err "$error"
+    return 1
+  fi
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+  _debug domain "$_domain"
+  _debug sub_domain "$_sub_domain"
+
+  _del_namecheap_TXT "$_domain" "$_sub_domain" "$txtvalue"
+}
+
+####################  Private functions below ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+_get_root() {
+  fulldomain=$1
+
+  if ! _get_root_by_getList "$fulldomain"; then
+    _debug "Failed domain lookup via domains.getList api call. Trying domain lookup via domains.dns.getHosts api."
+    # The above "getList" api will only return hosts *owned* by the calling user. However, if the calling
+    # user is not the owner, but still has administrative rights, we must query the getHosts api directly.
+    # See this comment and the official namecheap response: http://disq.us/p/1q6v9x9
+    if ! _get_root_by_getHosts "$fulldomain"; then
+      return 1
+    fi
+  fi
+
+  return 0
+}
+
+_get_root_by_getList() {
+  domain=$1
+
+  if ! _namecheap_post "namecheap.domains.getList"; then
+    _err "$error"
+    return 1
+  fi
+
+  i=2
+  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 ! _contains "$h" "\\."; then
+      #not valid
+      return 1
+    fi
+
+    if ! _contains "$response" "$h"; then
+      _debug "$h not found"
+    else
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      _domain="$h"
+      return 0
+    fi
+    p="$i"
+    i=$(_math "$i" + 1)
+  done
+  return 1
+}
+
+_get_root_by_getHosts() {
+  i=100
+  p=99
+
+  while [ $p -ne 0 ]; do
+
+    h=$(printf "%s" "$1" | cut -d . -f $i-100)
+    if [ -n "$h" ]; then
+      if _contains "$h" "\\."; then
+        _debug h "$h"
+        if _namecheap_set_tld_sld "$h"; then
+          _sub_domain=$(printf "%s" "$1" | cut -d . -f 1-$p)
+          _domain="$h"
+          return 0
+        else
+          _debug "$h not found"
+        fi
+      fi
+    fi
+    i="$p"
+    p=$(_math "$p" - 1)
+  done
+  return 1
+}
+
+_namecheap_set_publicip() {
+
+  if [ -z "$NAMECHEAP_SOURCEIP" ]; then
+    _err "No Source IP specified for Namecheap API."
+    _err "Use your public ip address or an url to retrieve it (e.g. https://ipconfig.co/ip) and export it as NAMECHEAP_SOURCEIP"
+    return 1
+  else
+    _saveaccountconf NAMECHEAP_SOURCEIP "$NAMECHEAP_SOURCEIP"
+    _debug sourceip "$NAMECHEAP_SOURCEIP"
+
+    ip=$(echo "$NAMECHEAP_SOURCEIP" | _egrep_o '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}')
+    addr=$(echo "$NAMECHEAP_SOURCEIP" | _egrep_o '(http|https)://.*')
+
+    _debug2 ip "$ip"
+    _debug2 addr "$addr"
+
+    if [ -n "$ip" ]; then
+      _publicip="$ip"
+    elif [ -n "$addr" ]; then
+      _publicip=$(_get "$addr")
+    else
+      _err "No Source IP specified for Namecheap API."
+      _err "Use your public ip address or an url to retrieve it (e.g. https://ipconfig.co/ip) and export it as NAMECHEAP_SOURCEIP"
+      return 1
+    fi
+  fi
+
+  _debug publicip "$_publicip"
+
+  return 0
+}
+
+_namecheap_post() {
+  command=$1
+  data="ApiUser=${NAMECHEAP_USERNAME}&ApiKey=${NAMECHEAP_API_KEY}&ClientIp=${_publicip}&UserName=${NAMECHEAP_USERNAME}&Command=${command}"
+  _debug2 "_namecheap_post data" "$data"
+  response="$(_post "$data" "$NAMECHEAP_API" "" "POST")"
+  _debug2 response "$response"
+
+  if _contains "$response" "Status=\"ERROR\"" >/dev/null; then
+    error=$(echo "$response" | _egrep_o ">.*<\\/Error>" | cut -d '<' -f 1 | tr -d '>')
+    _err "error $error"
+    return 1
+  fi
+
+  return 0
+}
+
+_namecheap_parse_host() {
+  _host=$1
+  _debug _host "$_host"
+
+  _hostid=$(echo "$_host" | _egrep_o ' HostId="[^"]*' | cut -d '"' -f 2)
+  _hostname=$(echo "$_host" | _egrep_o ' Name="[^"]*' | cut -d '"' -f 2)
+  _hosttype=$(echo "$_host" | _egrep_o ' Type="[^"]*' | cut -d '"' -f 2)
+  _hostaddress=$(echo "$_host" | _egrep_o ' Address="[^"]*' | cut -d '"' -f 2)
+  _hostmxpref=$(echo "$_host" | _egrep_o ' MXPref="[^"]*' | cut -d '"' -f 2)
+  _hostttl=$(echo "$_host" | _egrep_o ' TTL="[^"]*' | cut -d '"' -f 2)
+
+  _debug hostid "$_hostid"
+  _debug hostname "$_hostname"
+  _debug hosttype "$_hosttype"
+  _debug hostaddress "$_hostaddress"
+  _debug hostmxpref "$_hostmxpref"
+  _debug hostttl "$_hostttl"
+}
+
+_namecheap_check_config() {
+
+  if [ -z "$NAMECHEAP_API_KEY" ]; then
+    _err "No API key specified for Namecheap API."
+    _err "Create your key and export it as NAMECHEAP_API_KEY"
+    return 1
+  fi
+
+  if [ -z "$NAMECHEAP_USERNAME" ]; then
+    _err "No username key specified for Namecheap API."
+    _err "Create your key and export it as NAMECHEAP_USERNAME"
+    return 1
+  fi
+
+  _saveaccountconf NAMECHEAP_API_KEY "$NAMECHEAP_API_KEY"
+  _saveaccountconf NAMECHEAP_USERNAME "$NAMECHEAP_USERNAME"
+
+  return 0
+}
+
+_set_namecheap_TXT() {
+  subdomain=$2
+  txt=$3
+
+  if ! _namecheap_set_tld_sld "$1"; then
+    return 1
+  fi
+
+  request="namecheap.domains.dns.getHosts&SLD=${_sld}&TLD=${_tld}"
+
+  if ! _namecheap_post "$request"; then
+    _err "$error"
+    return 1
+  fi
+
+  hosts=$(echo "$response" | _egrep_o '<host[^>]*')
+  _debug hosts "$hosts"
+
+  if [ -z "$hosts" ]; then
+    _error "Hosts not found"
+    return 1
+  fi
+
+  _namecheap_reset_hostList
+
+  while read -r host; do
+    if _contains "$host" "<host"; then
+      _namecheap_parse_host "$host"
+      _debug2 _hostname "_hostname"
+      _debug2 _hosttype "_hosttype"
+      _debug2 _hostaddress "_hostaddress"
+      _debug2 _hostmxpref "_hostmxpref"
+      _hostaddress="$(printf "%s" "$_hostaddress" | _url_encode)"
+      _debug2 "encoded _hostaddress" "_hostaddress"
+      _namecheap_add_host "$_hostname" "$_hosttype" "$_hostaddress" "$_hostmxpref" "$_hostttl"
+    fi
+  done <<EOT
+echo "$hosts"
+EOT
+
+  _namecheap_add_host "$subdomain" "TXT" "$txt" 10 120
+
+  _debug hostrequestfinal "$_hostrequest"
+
+  request="namecheap.domains.dns.setHosts&SLD=${_sld}&TLD=${_tld}${_hostrequest}"
+
+  if ! _namecheap_post "$request"; then
+    _err "$error"
+    return 1
+  fi
+
+  return 0
+}
+
+_del_namecheap_TXT() {
+  subdomain=$2
+  txt=$3
+
+  if ! _namecheap_set_tld_sld "$1"; then
+    return 1
+  fi
+
+  request="namecheap.domains.dns.getHosts&SLD=${_sld}&TLD=${_tld}"
+
+  if ! _namecheap_post "$request"; then
+    _err "$error"
+    return 1
+  fi
+
+  hosts=$(echo "$response" | _egrep_o '<host[^>]*')
+  _debug hosts "$hosts"
+
+  if [ -z "$hosts" ]; then
+    _error "Hosts not found"
+    return 1
+  fi
+
+  _namecheap_reset_hostList
+
+  found=0
+
+  while read -r host; do
+    if _contains "$host" "<host"; then
+      _namecheap_parse_host "$host"
+      if [ "$_hosttype" = "TXT" ] && [ "$_hostname" = "$subdomain" ] && [ "$_hostaddress" = "$txt" ]; then
+        _debug "TXT entry found"
+        found=1
+      else
+        _hostaddress="$(printf "%s" "$_hostaddress" | _url_encode)"
+        _namecheap_add_host "$_hostname" "$_hosttype" "$_hostaddress" "$_hostmxpref" "$_hostttl"
+      fi
+    fi
+  done <<EOT
+echo "$hosts"
+EOT
+
+  if [ $found -eq 0 ]; then
+    _debug "TXT entry not found"
+    return 0
+  fi
+
+  _debug hostrequestfinal "$_hostrequest"
+
+  request="namecheap.domains.dns.setHosts&SLD=${_sld}&TLD=${_tld}${_hostrequest}"
+
+  if ! _namecheap_post "$request"; then
+    _err "$error"
+    return 1
+  fi
+
+  return 0
+}
+
+_namecheap_reset_hostList() {
+  _hostindex=0
+  _hostrequest=""
+}
+
+#Usage: _namecheap_add_host HostName RecordType Address MxPref TTL
+_namecheap_add_host() {
+  _hostindex=$(_math "$_hostindex" + 1)
+  _hostrequest=$(printf '%s&HostName%d=%s&RecordType%d=%s&Address%d=%s&MXPref%d=%d&TTL%d=%d' "$_hostrequest" "$_hostindex" "$1" "$_hostindex" "$2" "$_hostindex" "$3" "$_hostindex" "$4" "$_hostindex" "$5")
+}
+
+_namecheap_set_tld_sld() {
+  domain=$1
+  _tld=""
+  _sld=""
+
+  i=2
+
+  while true; do
+
+    _tld=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    _debug tld "$_tld"
+
+    if [ -z "$_tld" ]; then
+      _debug "invalid tld"
+      return 1
+    fi
+
+    j=$(_math "$i" - 1)
+
+    _sld=$(printf "%s" "$domain" | cut -d . -f 1-"$j")
+    _debug sld "$_sld"
+
+    if [ -z "$_sld" ]; then
+      _debug "invalid sld"
+      return 1
+    fi
+
+    request="namecheap.domains.dns.getHosts&SLD=$_sld&TLD=$_tld"
+
+    if ! _namecheap_post "$request"; then
+      _debug "sld($_sld)/tld($_tld) not found"
+    else
+      _debug "sld($_sld)/tld($_tld) found"
+      return 0
+    fi
+
+    i=$(_math "$i" + 1)
+
+  done
+
+}

+ 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\"")

+ 181 - 0
dnsapi/dns_neodigit.sh

@@ -0,0 +1,181 @@
+#!/usr/bin/env sh
+
+#
+# NEODIGIT_API_TOKEN="jasdfhklsjadhflnhsausdfas"
+
+# This is Neodigit.net api wrapper for acme.sh
+#
+# Author: Adrian Almenar
+# Report Bugs here: https://github.com/tecnocratica/acme.sh
+#
+NEODIGIT_API_URL="https://api.neodigit.net/v1"
+#
+########  Public functions #####################
+
+# Usage: dns_myapi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_neodigit_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  NEODIGIT_API_TOKEN="${NEODIGIT_API_TOKEN:-$(_readaccountconf_mutable NEODIGIT_API_TOKEN)}"
+  if [ -z "$NEODIGIT_API_TOKEN" ]; then
+    NEODIGIT_API_TOKEN=""
+    _err "You haven't specified a Token api key."
+    _err "Please create the key and try again."
+    return 1
+  fi
+
+  #save the api key and email to the account conf file.
+  _saveaccountconf_mutable NEODIGIT_API_TOKEN "$NEODIGIT_API_TOKEN"
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+  _debug domain "$_domain"
+  _debug sub_domain "$_sub_domain"
+
+  _debug "Getting txt records"
+  _neo_rest GET "dns/zones/${_domain_id}/records?type=TXT&name=$fulldomain"
+
+  _debug _code "$_code"
+
+  if [ "$_code" != "200" ]; then
+    _err "error retrieving data!"
+    return 1
+  fi
+
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+  _debug domain "$_domain"
+  _debug sub_domain "$_sub_domain"
+
+  _info "Adding record"
+  if _neo_rest POST "dns/zones/$_domain_id/records" "{\"record\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\",\"ttl\":60}}"; then
+    if printf -- "%s" "$response" | grep "$_sub_domain" >/dev/null; 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_neodigit_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  NEODIGIT_API_TOKEN="${NEODIGIT_API_TOKEN:-$(_readaccountconf_mutable NEODIGIT_API_TOKEN)}"
+  if [ -z "$NEODIGIT_API_TOKEN" ]; then
+    NEODIGIT_API_TOKEN=""
+    _err "You haven't specified a Token api key."
+    _err "Please create the key and try again."
+    return 1
+  fi
+
+  #save the api key and email to the account conf file.
+  _saveaccountconf_mutable NEODIGIT_API_TOKEN "$NEODIGIT_API_TOKEN"
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+
+  _debug _domain_id "$_domain_id"
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  _debug "Getting txt records"
+  _neo_rest GET "dns/zones/${_domain_id}/records?type=TXT&name=$fulldomain&content=$txtvalue"
+
+  if [ "$_code" != "200" ]; then
+    _err "error retrieving data!"
+    return 1
+  fi
+
+  record_id=$(echo "$response" | _egrep_o "\"id\":\s*[0-9]+" | _head_n 1 | cut -d: -f2 | cut -d, -f1)
+  _debug "record_id" "$record_id"
+  if [ -z "$record_id" ]; then
+    _err "Can not get record id to remove."
+    return 1
+  fi
+  if ! _neo_rest DELETE "dns/zones/$_domain_id/records/$record_id"; then
+    _err "Delete record error."
+    return 1
+  fi
+
+}
+
+####################  Private functions below ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+# _domain_id=dasfdsafsadg5ythd
+_get_root() {
+  domain=$1
+  i=2
+  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 ! _neo_rest GET "dns/zones?name=$h"; then
+      return 1
+    fi
+
+    _debug p "$p"
+
+    if _contains "$response" "\"name\":\"$h\"" >/dev/null; then
+      _domain_id=$(echo "$response" | _egrep_o "\"id\":\s*[0-9]+" | _head_n 1 | cut -d: -f2 | cut -d, -f1)
+      if [ "$_domain_id" ]; then
+        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+        _domain=$h
+        return 0
+      fi
+      return 1
+    fi
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+  return 1
+}
+
+_neo_rest() {
+  m=$1
+  ep="$2"
+  data="$3"
+  _debug "$ep"
+
+  export _H1="X-TCPanel-Token: $NEODIGIT_API_TOKEN"
+  export _H2="Content-Type: application/json"
+
+  if [ "$m" != "GET" ]; then
+    _debug data "$data"
+    response="$(_post "$data" "$NEODIGIT_API_URL/$ep" "" "$m")"
+  else
+    response="$(_get "$NEODIGIT_API_URL/$ep")"
+  fi
+
+  _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
+
+  if [ "$?" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}

+ 133 - 0
dnsapi/dns_netcup.sh

@@ -0,0 +1,133 @@
+#!/usr/bin/env sh
+#developed by linux-insideDE
+
+NC_Apikey="${NC_Apikey:-$(_readaccountconf_mutable NC_Apikey)}"
+NC_Apipw="${NC_Apipw:-$(_readaccountconf_mutable NC_Apipw)}"
+NC_CID="${NC_CID:-$(_readaccountconf_mutable NC_CID)}"
+end="https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON"
+client=""
+
+dns_netcup_add() {
+  login
+  if [ "$NC_Apikey" = "" ] || [ "$NC_Apipw" = "" ] || [ "$NC_CID" = "" ]; then
+    _err "No Credentials given"
+    return 1
+  fi
+  _saveaccountconf_mutable NC_Apikey "$NC_Apikey"
+  _saveaccountconf_mutable NC_Apipw "$NC_Apipw"
+  _saveaccountconf_mutable NC_CID "$NC_CID"
+  fulldomain=$1
+  txtvalue=$2
+  domain=""
+  exit=$(echo "$fulldomain" | tr -dc '.' | wc -c)
+  exit=$(_math "$exit" + 1)
+  i=$exit
+
+  while
+    [ "$exit" -gt 0 ]
+  do
+    tmp=$(echo "$fulldomain" | cut -d'.' -f"$exit")
+    if [ "$(_math "$i" - "$exit")" -eq 0 ]; then
+      domain="$tmp"
+    else
+      domain="$tmp.$domain"
+    fi
+    if [ "$(_math "$i" - "$exit")" -ge 1 ]; then
+      msg=$(_post "{\"action\": \"updateDnsRecords\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apisessionid\": \"$sid\", \"customernumber\": \"$NC_CID\",\"clientrequestid\": \"$client\" , \"domainname\": \"$domain\", \"dnsrecordset\": { \"dnsrecords\": [ {\"id\": \"\", \"hostname\": \"$fulldomain.\", \"type\": \"TXT\", \"priority\": \"\", \"destination\": \"$txtvalue\", \"deleterecord\": \"false\", \"state\": \"yes\"} ]}}}" "$end" "" "POST")
+      _debug "$msg"
+      if [ "$(_getfield "$msg" "5" | sed 's/"statuscode"://g')" != 5028 ]; then
+        if [ "$(_getfield "$msg" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
+          _err "$msg"
+          return 1
+        else
+          break
+        fi
+      fi
+    fi
+    exit=$(_math "$exit" - 1)
+  done
+  logout
+}
+
+dns_netcup_rm() {
+  login
+  fulldomain=$1
+  txtvalue=$2
+
+  domain=""
+  exit=$(echo "$fulldomain" | tr -dc '.' | wc -c)
+  exit=$(_math "$exit" + 1)
+  i=$exit
+  rec=""
+
+  while
+    [ "$exit" -gt 0 ]
+  do
+    tmp=$(echo "$fulldomain" | cut -d'.' -f"$exit")
+    if [ "$(_math "$i" - "$exit")" -eq 0 ]; then
+      domain="$tmp"
+    else
+      domain="$tmp.$domain"
+    fi
+    if [ "$(_math "$i" - "$exit")" -ge 1 ]; then
+      msg=$(_post "{\"action\": \"infoDnsRecords\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apisessionid\": \"$sid\", \"customernumber\": \"$NC_CID\", \"domainname\": \"$domain\"}}" "$end" "" "POST")
+      rec=$(echo "$msg" | sed 's/\[//g' | sed 's/\]//g' | sed 's/{\"serverrequestid\".*\"dnsrecords\"://g' | sed 's/},{/};{/g' | sed 's/{//g' | sed 's/}//g')
+      _debug "$msg"
+      if [ "$(_getfield "$msg" "5" | sed 's/"statuscode"://g')" != 5028 ]; then
+        if [ "$(_getfield "$msg" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
+          _err "$msg"
+          return 1
+        else
+          break
+        fi
+      fi
+    fi
+    exit=$(_math "$exit" - 1)
+  done
+
+  ida=0000
+  idv=0001
+  ids=0000000000
+  i=1
+  while
+    [ "$i" -ne 0 ]
+  do
+    specrec=$(_getfield "$rec" "$i" ";")
+    idv="$ida"
+    ida=$(_getfield "$specrec" "1" "," | sed 's/\"id\":\"//g' | sed 's/\"//g')
+    txtv=$(_getfield "$specrec" "5" "," | sed 's/\"destination\":\"//g' | sed 's/\"//g')
+    i=$(_math "$i" + 1)
+    if [ "$txtvalue" = "$txtv" ]; then
+      i=0
+      ids="$ida"
+    fi
+    if [ "$ida" = "$idv" ]; then
+      i=0
+    fi
+  done
+  msg=$(_post "{\"action\": \"updateDnsRecords\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apisessionid\": \"$sid\", \"customernumber\": \"$NC_CID\",\"clientrequestid\": \"$client\" , \"domainname\": \"$domain\", \"dnsrecordset\": { \"dnsrecords\": [ {\"id\": \"$ids\", \"hostname\": \"$fulldomain.\", \"type\": \"TXT\", \"priority\": \"\", \"destination\": \"$txtvalue\", \"deleterecord\": \"TRUE\", \"state\": \"yes\"} ]}}}" "$end" "" "POST")
+  _debug "$msg"
+  if [ "$(_getfield "$msg" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
+    _err "$msg"
+    return 1
+  fi
+  logout
+}
+
+login() {
+  tmp=$(_post "{\"action\": \"login\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apipassword\": \"$NC_Apipw\", \"customernumber\": \"$NC_CID\"}}" "$end" "" "POST")
+  sid=$(_getfield "$tmp" "8" | sed s/\"responsedata\":\{\"apisessionid\":\"//g | sed 's/\"\}\}//g')
+  _debug "$tmp"
+  if [ "$(_getfield "$msg" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
+    _err "$msg"
+    return 1
+  fi
+}
+logout() {
+  tmp=$(_post "{\"action\": \"logout\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apisessionid\": \"$sid\", \"customernumber\": \"$NC_CID\"}}" "$end" "" "POST")
+  _debug "$tmp"
+  if [ "$(_getfield "$msg" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
+    _err "$msg"
+    return 1
+  fi
+}

+ 2 - 2
dnsapi/dns_nsone.sh

@@ -46,7 +46,7 @@ dns_nsone_add() {
   if [ "$count" = "0" ]; then
     _info "Adding record"
 
-    if _nsone_rest PUT "zones/$_domain/$fulldomain/TXT" "{\"answers\":[{\"answer\":[\"$txtvalue\"]}],\"type\":\"TXT\",\"domain\":\"$fulldomain\",\"zone\":\"$_domain\"}"; then
+    if _nsone_rest PUT "zones/$_domain/$fulldomain/TXT" "{\"answers\":[{\"answer\":[\"$txtvalue\"]}],\"type\":\"TXT\",\"domain\":\"$fulldomain\",\"zone\":\"$_domain\",\"ttl\":0}"; then
       if _contains "$response" "$fulldomain"; then
         _info "Added"
         #todo: check if the record takes effect
@@ -62,7 +62,7 @@ dns_nsone_add() {
     prev_txt=$(printf "%s\n" "$response" | _egrep_o "\"domain\":\"$fulldomain\",\"short_answers\":\[\"[^,]*\]" | _head_n 1 | cut -d: -f3 | cut -d, -f1)
     _debug "prev_txt" "$prev_txt"
 
-    _nsone_rest POST "zones/$_domain/$fulldomain/TXT" "{\"answers\": [{\"answer\": [\"$txtvalue\"]},{\"answer\": $prev_txt}],\"type\": \"TXT\",\"domain\":\"$fulldomain\",\"zone\": \"$_domain\"}"
+    _nsone_rest POST "zones/$_domain/$fulldomain/TXT" "{\"answers\": [{\"answer\": [\"$txtvalue\"]},{\"answer\": $prev_txt}],\"type\": \"TXT\",\"domain\":\"$fulldomain\",\"zone\": \"$_domain\",\"ttl\":0}"
     if [ "$?" = "0" ] && _contains "$response" "$fulldomain"; then
       _info "Updated!"
       #todo: check if the record takes effect

+ 30 - 4
dnsapi/dns_nsupdate.sh

@@ -8,15 +8,29 @@ 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}"
+  _saveaccountconf NSUPDATE_ZONE "${NSUPDATE_ZONE}"
   _info "adding ${fulldomain}. 60 in txt \"${txtvalue}\""
-  nsupdate -k "${NSUPDATE_KEY}" <<EOF
-server ${NSUPDATE_SERVER}
+  [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_1" ] && nsdebug="-d"
+  [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_2" ] && nsdebug="-D"
+  if [ -z "${NSUPDATE_ZONE}" ]; then
+    nsupdate -k "${NSUPDATE_KEY}" $nsdebug <<EOF
+server ${NSUPDATE_SERVER}  ${NSUPDATE_SERVER_PORT} 
 update add ${fulldomain}. 60 in txt "${txtvalue}"
 send
 EOF
+  else
+    nsupdate -k "${NSUPDATE_KEY}" $nsdebug <<EOF
+server ${NSUPDATE_SERVER}  ${NSUPDATE_SERVER_PORT}
+zone ${NSUPDATE_ZONE}.
+update add ${fulldomain}. 60 in txt "${txtvalue}"
+send
+EOF
+  fi
   if [ $? -ne 0 ]; then
     _err "error updating domain"
     return 1
@@ -30,12 +44,24 @@ 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}
+  [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_1" ] && nsdebug="-d"
+  [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_2" ] && nsdebug="-D"
+  if [ -z "${NSUPDATE_ZONE}" ]; then
+    nsupdate -k "${NSUPDATE_KEY}" $nsdebug <<EOF
+server ${NSUPDATE_SERVER}  ${NSUPDATE_SERVER_PORT} 
 update delete ${fulldomain}. txt
 send
 EOF
+  else
+    nsupdate -k "${NSUPDATE_KEY}" $nsdebug <<EOF
+server ${NSUPDATE_SERVER}  ${NSUPDATE_SERVER_PORT}
+zone ${NSUPDATE_ZONE}.
+update delete ${fulldomain}. txt
+send
+EOF
+  fi
   if [ $? -ne 0 ]; then
     _err "error updating domain"
     return 1

+ 211 - 0
dnsapi/dns_nw.sh

@@ -0,0 +1,211 @@
+#!/usr/bin/env sh
+########################################################################
+# NocWorx script for acme.sh
+#
+# Handles DNS Updates for the Following vendors:
+#   - Nexcess.net
+#   - Thermo.io
+#   - Futurehosting.com
+#
+# Environment variables:
+#
+#  - NW_API_TOKEN  (Your API Token)
+#  - NW_API_ENDPOINT (One of the following listed below)
+#
+# Endpoints:
+#   - https://portal.nexcess.net (default)
+#   - https://core.thermo.io
+#   - https://my.futurehosting.com
+#
+#  Note: If you do not have an API token, one can be generated at one
+#        of the following URLs:
+#        - https://portal.nexcess.net/api-token
+#        - https://core.thermo.io/api-token
+#        - https://my.futurehosting.com/api-token
+#
+# Author: Frank Laszlo <flaszlo@nexcess.net>
+
+NW_API_VERSION="0"
+
+# dns_nw_add() - Add TXT record
+# Usage: dns_nw_add _acme-challenge.subdomain.domain.com "XyZ123..."
+dns_nw_add() {
+  host="${1}"
+  txtvalue="${2}"
+
+  _debug host "${host}"
+  _debug txtvalue "${txtvalue}"
+
+  if ! _check_nw_api_creds; then
+    return 1
+  fi
+
+  _info "Using NocWorx (${NW_API_ENDPOINT})"
+  _debug "Calling: dns_nw_add() '${host}' '${txtvalue}'"
+
+  _debug "Detecting root zone"
+  if ! _get_root "${host}"; then
+    _err "Zone for domain does not exist."
+    return 1
+  fi
+  _debug _zone_id "${_zone_id}"
+  _debug _sub_domain "${_sub_domain}"
+  _debug _domain "${_domain}"
+
+  _post_data="{\"zone_id\": \"${_zone_id}\", \"type\": \"TXT\", \"host\": \"${host}\", \"target\": \"${txtvalue}\", \"ttl\": \"300\"}"
+
+  if _rest POST "dns-record" "${_post_data}" && [ -n "${response}" ]; then
+    _record_id=$(printf "%s\n" "${response}" | _egrep_o "\"record_id\": *[0-9]+" | cut -d : -f 2 | tr -d " " | _head_n 1)
+    _debug _record_id "${_record_id}"
+
+    if [ -z "$_record_id" ]; then
+      _err "Error adding the TXT record."
+      return 1
+    fi
+
+    _info "TXT record successfully added."
+    return 0
+  fi
+
+  return 1
+}
+
+# dns_nw_rm() - Remove TXT record
+# Usage: dns_nw_rm _acme-challenge.subdomain.domain.com "XyZ123..."
+dns_nw_rm() {
+  host="${1}"
+  txtvalue="${2}"
+
+  _debug host "${host}"
+  _debug txtvalue "${txtvalue}"
+
+  if ! _check_nw_api_creds; then
+    return 1
+  fi
+
+  _info "Using NocWorx (${NW_API_ENDPOINT})"
+  _debug "Calling: dns_nw_rm() '${host}'"
+
+  _debug "Detecting root zone"
+  if ! _get_root "${host}"; then
+    _err "Zone for domain does not exist."
+    return 1
+  fi
+  _debug _zone_id "${_zone_id}"
+  _debug _sub_domain "${_sub_domain}"
+  _debug _domain "${_domain}"
+
+  _parameters="?zone_id=${_zone_id}"
+
+  if _rest GET "dns-record" "${_parameters}" && [ -n "${response}" ]; then
+    response="$(echo "${response}" | tr -d "\n" | sed 's/^\[\(.*\)\]$/\1/' | sed -e 's/{"record_id":/|"record_id":/g' | sed 's/|/&{/g' | tr "|" "\n")"
+    _debug response "${response}"
+
+    record="$(echo "${response}" | _egrep_o "{.*\"host\": *\"${_sub_domain}\", *\"target\": *\"${txtvalue}\".*}")"
+    _debug record "${record}"
+
+    if [ "${record}" ]; then
+      _record_id=$(printf "%s\n" "${record}" | _egrep_o "\"record_id\": *[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ )
+      if [ "${_record_id}" ]; then
+        _debug _record_id "${_record_id}"
+
+        _rest DELETE "dns-record/${_record_id}"
+
+        _info "TXT record successfully deleted."
+        return 0
+      fi
+
+      return 1
+    fi
+
+    return 0
+  fi
+
+  return 1
+}
+
+_check_nw_api_creds() {
+  NW_API_TOKEN="${NW_API_TOKEN:-$(_readaccountconf_mutable NW_API_TOKEN)}"
+  NW_API_ENDPOINT="${NW_API_ENDPOINT:-$(_readaccountconf_mutable NW_API_ENDPOINT)}"
+
+  if [ -z "${NW_API_ENDPOINT}" ]; then
+    NW_API_ENDPOINT="https://portal.nexcess.net"
+  fi
+
+  if [ -z "${NW_API_TOKEN}" ]; then
+    _err "You have not defined your NW_API_TOKEN."
+    _err "Please create your token and try again."
+    _err "If you need to generate a new token, please visit one of the following URLs:"
+    _err "  - https://portal.nexcess.net/api-token"
+    _err "  - https://core.thermo.io/api-token"
+    _err "  - https://my.futurehosting.com/api-token"
+
+    return 1
+  fi
+
+  _saveaccountconf_mutable NW_API_TOKEN "${NW_API_TOKEN}"
+  _saveaccountconf_mutable NW_API_ENDPOINT "${NW_API_ENDPOINT}"
+}
+
+_get_root() {
+  domain="${1}"
+  i=2
+  p=1
+
+  if _rest GET "dns-zone"; then
+    response="$(echo "${response}" | tr -d "\n" | sed 's/^\[\(.*\)\]$/\1/' | sed -e 's/{"zone_id":/|"zone_id":/g' | sed 's/|/&{/g' | tr "|" "\n")"
+
+    _debug response "${response}"
+    while true; do
+      h=$(printf "%s" "${domain}" | cut -d . -f $i-100)
+      _debug h "${h}"
+      if [ -z "${h}" ]; then
+        #not valid
+        return 1
+      fi
+
+      hostedzone="$(echo "${response}" | _egrep_o "{.*\"domain\": *\"${h}\".*}")"
+      if [ "${hostedzone}" ]; then
+        _zone_id=$(printf "%s\n" "${hostedzone}" | _egrep_o "\"zone_id\": *[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ )
+        if [ "${_zone_id}" ]; then
+          _sub_domain=$(printf "%s" "${domain}" | cut -d . -f 1-${p})
+          _domain="${h}"
+          return 0
+        fi
+        return 1
+      fi
+      p=$i
+      i=$(_math "${i}" + 1)
+    done
+  fi
+  return 1
+}
+
+_rest() {
+  method="${1}"
+  ep="/${2}"
+  data="${3}"
+
+  _debug method "${method}"
+  _debug ep "${ep}"
+
+  export _H1="Accept: application/json"
+  export _H2="Content-Type: application/json"
+  export _H3="Api-Version: ${NW_API_VERSION}"
+  export _H4="User-Agent: NW-ACME-CLIENT"
+  export _H5="Authorization: Bearer ${NW_API_TOKEN}"
+
+  if [ "${method}" != "GET" ]; then
+    _debug data "${data}"
+    response="$(_post "${data}" "${NW_API_ENDPOINT}${ep}" "" "${method}")"
+  else
+    response="$(_get "${NW_API_ENDPOINT}${ep}${data}")"
+  fi
+
+  if [ "${?}" != "0" ]; then
+    _err "error ${ep}"
+    return 1
+  fi
+  _debug2 response "${response}"
+  return 0
+}

+ 217 - 0
dnsapi/dns_online.sh

@@ -0,0 +1,217 @@
+#!/usr/bin/env sh
+
+# Online API
+# https://console.online.net/en/api/
+#
+# Requires Online API key set in ONLINE_API_KEY
+
+########  Public functions #####################
+
+ONLINE_API="https://api.online.net/api/v1"
+
+#Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_online_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if ! _online_check_config; then
+    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 _real_dns_version "$_real_dns_version"
+
+  _info "Creating temporary zone version"
+  _online_create_temporary_zone_version
+  _info "Enabling temporary zone version"
+  _online_enable_zone "$_temporary_dns_version"
+
+  _info "Adding record"
+  _online_create_TXT_record "$_real_dns_version" "$_sub_domain" "$txtvalue"
+  _info "Disabling temporary version"
+  _online_enable_zone "$_real_dns_version"
+  _info "Destroying temporary version"
+  _online_destroy_zone "$_temporary_dns_version"
+
+  _info "Record added."
+  return 0
+}
+
+#fulldomain
+dns_online_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if ! _online_check_config; then
+    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 _real_dns_version "$_real_dns_version"
+
+  _debug "Getting txt records"
+  if ! _online_rest GET "domain/$_domain/version/active"; then
+    return 1
+  fi
+
+  rid=$(echo "$response" | _egrep_o "\"id\":[0-9]+,\"name\":\"$_sub_domain\",\"data\":\"\\\u0022$txtvalue\\\u0022\"" | cut -d ':' -f 2 | cut -d ',' -f 1)
+  _debug rid "$rid"
+  if [ -z "$rid" ]; then
+    return 1
+  fi
+
+  _info "Creating temporary zone version"
+  _online_create_temporary_zone_version
+  _info "Enabling temporary zone version"
+  _online_enable_zone "$_temporary_dns_version"
+
+  _info "Removing DNS record"
+  _online_rest DELETE "domain/$_domain/version/$_real_dns_version/zone/$rid"
+  _info "Disabling temporary version"
+  _online_enable_zone "$_real_dns_version"
+  _info "Destroying temporary version"
+  _online_destroy_zone "$_temporary_dns_version"
+
+  return 0
+}
+
+####################  Private functions below ##################################
+
+_online_check_config() {
+  ONLINE_API_KEY="${ONLINE_API_KEY:-$(_readaccountconf_mutable ONLINE_API_KEY)}"
+  if [ -z "$ONLINE_API_KEY" ]; then
+    _err "No API key specified for Online API."
+    _err "Create your key and export it as ONLINE_API_KEY"
+    return 1
+  fi
+  if ! _online_rest GET "domain/"; then
+    _err "Invalid API key specified for Online API."
+    return 1
+  fi
+
+  _saveaccountconf_mutable ONLINE_API_KEY "$ONLINE_API_KEY"
+
+  return 0
+}
+
+#_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
+
+    _online_rest GET "domain/$h/version/active"
+
+    if ! _contains "$response" "Domain not found" >/dev/null; then
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      _domain="$h"
+      _real_dns_version=$(echo "$response" | _egrep_o '"uuid_ref":.*' | cut -d ':' -f 2 | cut -d '"' -f 2)
+      return 0
+    fi
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+  _err "Unable to retrive DNS zone matching this domain"
+  return 1
+}
+
+# this function create a temporary zone version
+# as online.net does not allow updating an active version
+_online_create_temporary_zone_version() {
+
+  _online_rest POST "domain/$_domain/version" "name=acme.sh"
+  if [ "$?" != "0" ]; then
+    return 1
+  fi
+
+  _temporary_dns_version=$(echo "$response" | _egrep_o '"uuid_ref":.*' | cut -d ':' -f 2 | cut -d '"' -f 2)
+
+  # Creating a dummy record in this temporary version, because online.net doesn't accept enabling an empty version
+  _online_create_TXT_record "$_temporary_dns_version" "dummy.acme.sh" "dummy"
+
+  return 0
+}
+
+_online_destroy_zone() {
+  version_id=$1
+  _online_rest DELETE "domain/$_domain/version/$version_id"
+
+  if [ "$?" != "0" ]; then
+    return 1
+  fi
+  return 0
+}
+
+_online_enable_zone() {
+  version_id=$1
+  _online_rest PATCH "domain/$_domain/version/$version_id/enable"
+
+  if [ "$?" != "0" ]; then
+    return 1
+  fi
+  return 0
+}
+
+_online_create_TXT_record() {
+  version=$1
+  txt_name=$2
+  txt_value=$3
+
+  _online_rest POST "domain/$_domain/version/$version/zone" "type=TXT&name=$txt_name&data=%22$txt_value%22&ttl=60&priority=0"
+
+  # Note : the normal, expected response SHOULD be "Unknown method".
+  # this happens because the API HTTP response contains a Location: header, that redirect
+  # to an unknown online.net endpoint.
+  if [ "$?" != "0" ] || _contains "$response" "Unknown method" || _contains "$response" "\$ref"; then
+    return 0
+  else
+    _err "error $response"
+    return 1
+  fi
+}
+
+_online_rest() {
+  m=$1
+  ep="$2"
+  data="$3"
+  _debug "$ep"
+  _online_url="$ONLINE_API/$ep"
+  _debug2 _online_url "$_online_url"
+  export _H1="Authorization: Bearer $ONLINE_API_KEY"
+  export _H2="X-Pretty-JSON: 1"
+  if [ "$data" ] || [ "$m" != "GET" ]; then
+    _debug data "$data"
+    response="$(_post "$data" "$_online_url" "" "$m")"
+  else
+    response="$(_get "$_online_url")"
+  fi
+  if [ "$?" != "0" ] || _contains "$response" "invalid_grant" || _contains "$response" "Method not allowed"; then
+    _err "error $response"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}

+ 54 - 9
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
@@ -185,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')
+}

+ 164 - 0
dnsapi/dns_pointhq.sh

@@ -0,0 +1,164 @@
+#!/usr/bin/env sh
+
+#
+#PointHQ_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
+#
+#PointHQ_Email="xxxx@sss.com"
+
+PointHQ_Api="https://api.pointhq.com"
+
+########  Public functions #####################
+
+#Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_pointhq_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  PointHQ_Key="${PointHQ_Key:-$(_readaccountconf_mutable PointHQ_Key)}"
+  PointHQ_Email="${PointHQ_Email:-$(_readaccountconf_mutable PointHQ_Email)}"
+  if [ -z "$PointHQ_Key" ] || [ -z "$PointHQ_Email" ]; then
+    PointHQ_Key=""
+    PointHQ_Email=""
+    _err "You didn't specify a PointHQ API key and email yet."
+    _err "Please create the key and try again."
+    return 1
+  fi
+
+  if ! _contains "$PointHQ_Email" "@"; then
+    _err "It seems that the PointHQ_Email=$PointHQ_Email is not a valid email address."
+    _err "Please check and retry."
+    return 1
+  fi
+
+  #save the api key and email to the account conf file.
+  _saveaccountconf_mutable PointHQ_Key "$PointHQ_Key"
+  _saveaccountconf_mutable PointHQ_Email "$PointHQ_Email"
+
+  _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"
+  if _pointhq_rest POST "zones/$_domain/records" "{\"zone_record\": {\"name\":\"$_sub_domain\",\"record_type\":\"TXT\",\"data\":\"$txtvalue\",\"ttl\":3600}}"; then
+    if printf -- "%s" "$response" | grep "$fulldomain" >/dev/null; 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_pointhq_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  PointHQ_Key="${PointHQ_Key:-$(_readaccountconf_mutable PointHQ_Key)}"
+  PointHQ_Email="${PointHQ_Email:-$(_readaccountconf_mutable PointHQ_Email)}"
+  if [ -z "$PointHQ_Key" ] || [ -z "$PointHQ_Email" ]; then
+    PointHQ_Key=""
+    PointHQ_Email=""
+    _err "You didn't specify a PointHQ API key and email yet."
+    _err "Please create the key and try again."
+    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 txt records"
+  _pointhq_rest GET "zones/${_domain}/records?record_type=TXT&name=$_sub_domain"
+
+  if ! printf "%s" "$response" | grep "^\[" >/dev/null; then
+    _err "Error"
+    return 1
+  fi
+
+  if [ "$response" = "[]" ]; then
+    _info "No records to remove."
+  else
+    record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[^,]*" | cut -d : -f 2 | tr -d \" | head -n 1)
+    _debug "record_id" "$record_id"
+    if [ -z "$record_id" ]; then
+      _err "Can not get record id to remove."
+      return 1
+    fi
+    if ! _pointhq_rest DELETE "zones/$_domain/records/$record_id"; then
+      _err "Delete record error."
+      return 1
+    fi
+    _contains "$response" '"status":"OK"'
+  fi
+}
+
+####################  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)
+    _debug h "$h"
+    if [ -z "$h" ]; then
+      #not valid
+      return 1
+    fi
+
+    if ! _pointhq_rest GET "zones"; then
+      return 1
+    fi
+
+    if _contains "$response" "\"name\":\"$h\"" >/dev/null; then
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      _domain=$h
+      return 0
+    fi
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+  return 1
+}
+
+_pointhq_rest() {
+  m=$1
+  ep="$2"
+  data="$3"
+  _debug "$ep"
+
+  _pointhq_auth=$(printf "%s:%s" "$PointHQ_Email" "$PointHQ_Key" | _base64)
+
+  export _H1="Authorization: Basic $_pointhq_auth"
+  export _H2="Content-Type: application/json"
+  export _H3="Accept: application/json"
+
+  if [ "$m" != "GET" ]; then
+    _debug data "$data"
+    response="$(_post "$data" "$PointHQ_Api/$ep" "" "$m")"
+  else
+    response="$(_get "$PointHQ_Api/$ep")"
+  fi
+
+  if [ "$?" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}

+ 207 - 0
dnsapi/dns_rackspace.sh

@@ -0,0 +1,207 @@
+#!/usr/bin/env sh
+#
+#
+#RACKSPACE_Username=""
+#
+#RACKSPACE_Apikey=""
+
+RACKSPACE_Endpoint="https://dns.api.rackspacecloud.com/v1.0"
+
+# 20190213 - The name & id fields swapped in the API response; fix sed
+# 20190101 - Duplicating file for new pull request to dev branch
+# Original - tcocca:rackspace_dnsapi https://github.com/Neilpang/acme.sh/pull/1297
+
+########  Public functions #####################
+#Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_rackspace_add() {
+  fulldomain="$1"
+  _debug fulldomain="$fulldomain"
+  txtvalue="$2"
+  _debug txtvalue="$txtvalue"
+  _rackspace_check_auth || return 1
+  _rackspace_check_rootzone || return 1
+  _info "Creating TXT record."
+  if ! _rackspace_rest POST "$RACKSPACE_Tenant/domains/$_domain_id/records" "{\"records\":[{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"data\":\"$txtvalue\",\"ttl\":300}]}"; then
+    return 1
+  fi
+  _debug2 response "$response"
+  if ! _contains "$response" "$txtvalue" >/dev/null; then
+    _err "Could not add TXT record."
+    return 1
+  fi
+  return 0
+}
+
+#fulldomain txtvalue
+dns_rackspace_rm() {
+  fulldomain=$1
+  _debug fulldomain="$fulldomain"
+  txtvalue=$2
+  _debug txtvalue="$txtvalue"
+  _rackspace_check_auth || return 1
+  _rackspace_check_rootzone || return 1
+  _info "Checking for TXT record."
+  if ! _get_recordid "$_domain_id" "$fulldomain" "$txtvalue"; then
+    _err "Could not get TXT record id."
+    return 1
+  fi
+  if [ "$_dns_record_id" = "" ]; then
+    _err "TXT record not found."
+    return 1
+  fi
+  _info "Removing TXT record."
+  if ! _delete_txt_record "$_domain_id" "$_dns_record_id"; then
+    _err "Could not remove TXT record $_dns_record_id."
+  fi
+  return 0
+}
+
+####################  Private functions below ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+# _domain_id=sdjkglgdfewsdfg
+_get_root_zone() {
+  domain="$1"
+  i=2
+  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 ! _rackspace_rest GET "$RACKSPACE_Tenant/domains"; then
+      return 1
+    fi
+    _debug2 response "$response"
+    if _contains "$response" "\"name\":\"$h\"" >/dev/null; then
+      # Response looks like:
+      #   {"ttl":300,"accountId":12345,"id":1111111,"name":"example.com","emailAddress": ...<and so on>
+      _domain_id=$(echo "$response" | sed -n "s/^.*\"id\":\([^,]*\),\"name\":\"$h\",.*/\1/p")
+      _debug2 domain_id "$_domain_id"
+      if [ -n "$_domain_id" ]; then
+        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+        _domain=$h
+        return 0
+      fi
+      return 1
+    fi
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+  return 1
+}
+
+_get_recordid() {
+  domainid="$1"
+  fulldomain="$2"
+  txtvalue="$3"
+  if ! _rackspace_rest GET "$RACKSPACE_Tenant/domains/$domainid/records?name=$fulldomain&type=TXT"; then
+    return 1
+  fi
+  _debug response "$response"
+  if ! _contains "$response" "$txtvalue"; then
+    _dns_record_id=0
+    return 0
+  fi
+  _dns_record_id=$(echo "$response" | tr '{' "\n" | grep "\"data\":\"$txtvalue\"" | sed -n 's/^.*"id":"\([^"]*\)".*/\1/p')
+  _debug _dns_record_id "$_dns_record_id"
+  return 0
+}
+
+_delete_txt_record() {
+  domainid="$1"
+  _dns_record_id="$2"
+  if ! _rackspace_rest DELETE "$RACKSPACE_Tenant/domains/$domainid/records?id=$_dns_record_id"; then
+    return 1
+  fi
+  _debug response "$response"
+  if ! _contains "$response" "RUNNING"; then
+    return 1
+  fi
+  return 0
+}
+
+_rackspace_rest() {
+  m="$1"
+  ep="$2"
+  data="$3"
+  _debug ep "$ep"
+  export _H1="Accept: application/json"
+  export _H2="X-Auth-Token: $RACKSPACE_Token"
+  export _H3="X-Project-Id: $RACKSPACE_Tenant"
+  export _H4="Content-Type: application/json"
+  if [ "$m" != "GET" ]; then
+    _debug data "$data"
+    response="$(_post "$data" "$RACKSPACE_Endpoint/$ep" "" "$m")"
+    retcode=$?
+  else
+    _info "Getting $RACKSPACE_Endpoint/$ep"
+    response="$(_get "$RACKSPACE_Endpoint/$ep")"
+    retcode=$?
+  fi
+
+  if [ "$retcode" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}
+
+_rackspace_authorization() {
+  export _H1="Content-Type: application/json"
+  data="{\"auth\":{\"RAX-KSKEY:apiKeyCredentials\":{\"username\":\"$RACKSPACE_Username\",\"apiKey\":\"$RACKSPACE_Apikey\"}}}"
+  _debug data "$data"
+  response="$(_post "$data" "https://identity.api.rackspacecloud.com/v2.0/tokens" "" "POST")"
+  retcode=$?
+  _debug2 response "$response"
+  if [ "$retcode" != "0" ]; then
+    _err "Authentication failed."
+    return 1
+  fi
+  if _contains "$response" "token"; then
+    RACKSPACE_Token="$(echo "$response" | _normalizeJson | sed -n 's/^.*"token":{.*,"id":"\([^"]*\)",".*/\1/p')"
+    RACKSPACE_Tenant="$(echo "$response" | _normalizeJson | sed -n 's/^.*"token":{.*,"id":"\([^"]*\)"}.*/\1/p')"
+    _debug RACKSPACE_Token "$RACKSPACE_Token"
+    _debug RACKSPACE_Tenant "$RACKSPACE_Tenant"
+  fi
+  return 0
+}
+
+_rackspace_check_auth() {
+  # retrieve the rackspace creds
+  RACKSPACE_Username="${RACKSPACE_Username:-$(_readaccountconf_mutable RACKSPACE_Username)}"
+  RACKSPACE_Apikey="${RACKSPACE_Apikey:-$(_readaccountconf_mutable RACKSPACE_Apikey)}"
+  # check their vals for null
+  if [ -z "$RACKSPACE_Username" ] || [ -z "$RACKSPACE_Apikey" ]; then
+    RACKSPACE_Username=""
+    RACKSPACE_Apikey=""
+    _err "You didn't specify a Rackspace username and api key."
+    _err "Please set those values and try again."
+    return 1
+  fi
+  # save the username and api key to the account conf file.
+  _saveaccountconf_mutable RACKSPACE_Username "$RACKSPACE_Username"
+  _saveaccountconf_mutable RACKSPACE_Apikey "$RACKSPACE_Apikey"
+  if [ -z "$RACKSPACE_Token" ]; then
+    _info "Getting authorization token."
+    if ! _rackspace_authorization; then
+      _err "Can not get token."
+    fi
+  fi
+}
+
+_rackspace_check_rootzone() {
+  _debug "First detect the root zone"
+  if ! _get_root_zone "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+  _debug _domain_id "$_domain_id"
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+}

+ 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
+}

+ 22 - 39
dnsapi/dns_unoeuro.sh

@@ -50,34 +50,16 @@ dns_unoeuro_add() {
     _err "Error"
     return 1
   fi
+  _info "Adding record"
 
-  if ! _contains "$response" "$_sub_domain" >/dev/null; then
-    _info "Adding record"
-
-    if _uno_rest POST "my/products/$h/dns/records" "{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"data\":\"$txtvalue\",\"ttl\":120}"; then
-      if _contains "$response" "\"status\": 200" >/dev/null; then
-        _info "Added, OK"
-        return 0
-      else
-        _err "Add txt record error."
-        return 1
-      fi
-    fi
-    _err "Add txt record error."
-  else
-    _info "Updating record"
-    record_line_number=$(echo "$response" | grep -n "$_sub_domain" | cut -d : -f 1)
-    record_line_number=$(_math "$record_line_number" - 1)
-    record_id=$(echo "$response" | _head_n "$record_line_number" | _tail_n 1 1 | _egrep_o "[0-9]{1,}")
-    _debug "record_id" "$record_id"
-
-    _uno_rest PUT "my/products/$h/dns/records/$record_id" "{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"data\":\"$txtvalue\",\"ttl\":120}"
+  if _uno_rest POST "my/products/$h/dns/records" "{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"data\":\"$txtvalue\",\"ttl\":120}"; then
     if _contains "$response" "\"status\": 200" >/dev/null; then
-      _info "Updated, OK"
+      _info "Added, OK"
       return 0
+    else
+      _err "Add txt record error."
+      return 1
     fi
-    _err "Update error"
-    return 1
   fi
 }
 
@@ -122,23 +104,24 @@ dns_unoeuro_rm() {
   if ! _contains "$response" "$_sub_domain"; then
     _info "Don't need to remove."
   else
-    record_line_number=$(echo "$response" | grep -n "$_sub_domain" | cut -d : -f 1)
-    record_line_number=$(_math "$record_line_number" - 1)
-    record_id=$(echo "$response" | _head_n "$record_line_number" | _tail_n 1 1 | _egrep_o "[0-9]{1,}")
-    _debug "record_id" "$record_id"
-
-    if [ -z "$record_id" ]; then
-      _err "Can not get record id to remove."
-      return 1
-    fi
+    for record_line_number in $(echo "$response" | grep -n "$_sub_domain" | cut -d : -f 1); do
+      record_line_number=$(_math "$record_line_number" - 1)
+      _debug "record_line_number" "$record_line_number"
+      record_id=$(echo "$response" | _head_n "$record_line_number" | _tail_n 1 1 | _egrep_o "[0-9]{1,}")
+      _debug "record_id" "$record_id"
+
+      if [ -z "$record_id" ]; then
+        _err "Can not get record id to remove."
+        return 1
+      fi
 
-    if ! _uno_rest DELETE "my/products/$h/dns/records/$record_id"; then
-      _err "Delete record error."
-      return 1
-    fi
-    _contains "$response" "\"status\": 200"
+      if ! _uno_rest DELETE "my/products/$h/dns/records/$record_id"; then
+        _err "Delete record error."
+        return 1
+      fi
+      _contains "$response" "\"status\": 200"
+    done
   fi
-
 }
 
 ####################  Private functions below ##################################

+ 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
+}

Some files were not shown because too many files changed in this diff