Browse Source

Merge pull request #1 from Neilpang/master

Update from original
sjau 7 years ago
parent
commit
6e2669ed1d
59 changed files with 6787 additions and 349 deletions
  1. 2 2
      .github/ISSUE_TEMPLATE.md
  2. 0 5
      .github/PULL_REQUEST_TEMPLATE.md
  3. 47 54
      .travis.yml
  4. 63 0
      Dockerfile
  5. 121 32
      README.md
  6. 587 103
      acme.sh
  7. 118 0
      deploy/README.md
  8. 26 0
      deploy/apache.sh
  9. 64 0
      deploy/cpanel_uapi.sh
  10. 26 0
      deploy/dovecot.sh
  11. 114 0
      deploy/exim4.sh
  12. 108 0
      deploy/fritzbox.sh
  13. 26 0
      deploy/haproxy.sh
  14. 31 0
      deploy/keychain.sh
  15. 77 0
      deploy/kong.sh
  16. 26 0
      deploy/myapi.sh
  17. 26 0
      deploy/nginx.sh
  18. 26 0
      deploy/opensshd.sh
  19. 26 0
      deploy/pureftpd.sh
  20. 32 0
      deploy/strongswan.sh
  21. 100 0
      deploy/unifi.sh
  22. 110 0
      deploy/vsftpd.sh
  23. 415 3
      dnsapi/README.md
  24. 147 0
      dnsapi/dns_ad.sh
  25. 2 2
      dnsapi/dns_ali.sh
  26. 66 24
      dnsapi/dns_aws.sh
  27. 61 16
      dnsapi/dns_cf.sh
  28. 184 0
      dnsapi/dns_cloudns.sh
  29. 24 17
      dnsapi/dns_cx.sh
  30. 328 0
      dnsapi/dns_cyon.sh
  31. 205 0
      dnsapi/dns_dgon.sh
  32. 215 0
      dnsapi/dns_dnsimple.sh
  33. 148 0
      dnsapi/dns_do.sh
  34. 40 12
      dnsapi/dns_dp.sh
  35. 128 0
      dnsapi/dns_duckdns.sh
  36. 339 0
      dnsapi/dns_dyn.sh
  37. 228 0
      dnsapi/dns_dynu.sh
  38. 362 0
      dnsapi/dns_freedns.sh
  39. 123 0
      dnsapi/dns_gandi_livedns.sh
  40. 4 4
      dnsapi/dns_gd.sh
  41. 175 0
      dnsapi/dns_he.sh
  42. 102 0
      dnsapi/dns_infoblox.sh
  43. 355 0
      dnsapi/dns_inwx.sh
  44. 2 2
      dnsapi/dns_ispconfig.sh
  45. 95 0
      dnsapi/dns_knot.sh
  46. 14 9
      dnsapi/dns_lexicon.sh
  47. 183 0
      dnsapi/dns_linode.sh
  48. 40 9
      dnsapi/dns_lua.sh
  49. 36 7
      dnsapi/dns_me.sh
  50. 14 31
      dnsapi/dns_myapi.sh
  51. 193 0
      dnsapi/dns_namecom.sh
  52. 158 0
      dnsapi/dns_nsone.sh
  53. 1 1
      dnsapi/dns_nsupdate.sh
  54. 13 13
      dnsapi/dns_ovh.sh
  55. 3 3
      dnsapi/dns_pdns.sh
  56. 170 0
      dnsapi/dns_servercow.sh
  57. 202 0
      dnsapi/dns_unoeuro.sh
  58. 149 0
      dnsapi/dns_vscale.sh
  59. 107 0
      dnsapi/dns_yandex.sh

+ 2 - 2
.github/ISSUE_TEMPLATE.md

@@ -1,4 +1,6 @@
 <!--
+请确保已经更新到最新的代码, 然后贴上来 `--debug 2` 的调试输出. 没有调试输出,我帮不了你.
+如何调试 https://github.com/Neilpang/acme.sh/wiki/How-to-debug-acme.sh
 
 If it is a bug report:
 - make sure you are able to repro it on the latest released version. 
@@ -8,13 +10,11 @@ You can install the latest version by: `acme.sh --upgrade`
 - Refer to the [WIKI](https://wiki.acme.sh).
 - Debug info [Debug](https://github.com/Neilpang/acme.sh/wiki/How-to-debug-acme.sh).
 
-
 -->
 
 Steps to reproduce
 ------------------
 
-
 Debug log
 -----------------
 

+ 0 - 5
.github/PULL_REQUEST_TEMPLATE.md

@@ -1,14 +1,9 @@
 <!--
 
-
-
 Do NOT send pull request to `master` branch.
 
-
 Please send to `dev` branch instead.
 
-
 Any PR to `master` branch will NOT be merged.
 
-
 -->

+ 47 - 54
.travis.yml

@@ -1,54 +1,47 @@
-language: shell
-sudo: required
-
-os:
-  - linux
-  - osx
-
-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 
-      brew update && brew install openssl;
-      brew info openssl;
-      ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/;
-      ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/;
-      ln -s /usr/local/Cellar/openssl/1.0.2j/bin/openssl /usr/local/openssl;
-      _old_path="$PATH";
-      echo "PATH=$PATH";
-      export PATH="";
-      export OPENSSL_BIN="/usr/local/openssl";
-      openssl version 2>&1 || true;
-      $OPENSSL_BIN version 2>&1 || true;
-      export PATH="$_old_path";
-    fi
-  
-script:
-  - echo "TEST_LOCAL=$TEST_LOCAL"
-  - echo "NGROK_TOKEN=$(echo "$NGROK_TOKEN" | wc -c)"
-  - which 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 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 SC2021,SC2126,SC2034 **/*.sh && echo "shellcheck OK" ; fi
-  - cd ..
-  - git clone https://github.com/Neilpang/acmetest.git && cp -r acme.sh acmetest/ && cd acmetest
-  - if [[ "$TRAVIS_OS_NAME" == "linux" ]] && [[ "$NGROK_TOKEN" ]]; then sudo NGROK_TOKEN="$NGROK_TOKEN" ./letest.sh ; fi
-  - if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$NGROK_TOKEN" ]]; then sudo NGROK_TOKEN="$NGROK_TOKEN" OPENSSL_BIN="$OPENSSL_BIN" ./letest.sh ; fi
-
-
-matrix:
-  fast_finish: true
-  
-  
+language: shell
+sudo: required
+dist: trusty
+
+os:
+  - linux
+  - osx
+
+services:
+  - docker
+
+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
+      brew update && brew install socat;
+      export PATH="/usr/local/opt/openssl@1.1/bin:$PATH" ;
+    fi
+
+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 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
+  - cd ..
+  - git clone https://github.com/Neilpang/acmetest.git && cp -r acme.sh acmetest/ && cd acmetest
+  - 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
+  
+  

+ 63 - 0
Dockerfile

@@ -0,0 +1,63 @@
+FROM alpine:3.6
+
+RUN apk update -f \
+  && apk --no-cache add -f \
+  openssl \
+  curl \
+  socat \
+  && rm -rf /var/cache/apk/*
+
+ENV LE_CONFIG_HOME /acme.sh
+
+ENV AUTO_UPGRADE 1
+
+#Install
+ADD ./ /install_acme.sh/
+RUN cd /install_acme.sh && ([ -f /install_acme.sh/acme.sh ] && /install_acme.sh/acme.sh --install || curl https://get.acme.sh | sh) && rm -rf /install_acme.sh/
+
+
+RUN ln -s  /root/.acme.sh/acme.sh  /usr/local/bin/acme.sh && crontab -l | grep acme.sh | sed 's#> /dev/null##' | crontab -
+
+RUN for verb in help \ 
+  version \
+  install \
+  uninstall \
+  upgrade \
+  issue \
+  signcsr \
+  deploy \
+  install-cert \
+  renew \
+  renew-all \
+  revoke \
+  remove \
+  list \
+  showcsr \
+  install-cronjob \
+  uninstall-cronjob \
+  cron \
+  toPkcs \
+  toPkcs8 \
+  update-account \
+  register-account \
+  create-account-key \
+  create-domain-key \
+  createCSR \
+  deactivate \
+  deactivate-account \
+  ; do \
+    printf -- "%b" "#!/usr/bin/env sh\n/root/.acme.sh/acme.sh --${verb} --config-home /acme.sh \"\$@\"" >/usr/local/bin/--${verb} && chmod +x /usr/local/bin/--${verb} \
+  ; done
+
+RUN printf "%b" '#!'"/usr/bin/env sh\n \
+if [ \"\$1\" = \"daemon\" ];  then \n \
+ trap \"echo stop && killall crond && exit 0\" SIGTERM SIGINT \n \
+ crond && while true; do sleep 1; done;\n \
+else \n \
+ exec -- \"\$@\"\n \
+fi" >/entry.sh && chmod +x /entry.sh
+
+VOLUME /acme.sh
+
+ENTRYPOINT ["/entry.sh"]
+CMD ["--help"]

+ 121 - 32
README.md

@@ -1,4 +1,6 @@
 # An ACME Shell script: acme.sh [![Build Status](https://travis-ci.org/Neilpang/acme.sh.svg?branch=master)](https://travis-ci.org/Neilpang/acme.sh)
+
+[![Join the chat at https://gitter.im/acme-sh/Lobby](https://badges.gitter.im/acme-sh/Lobby.svg)](https://gitter.im/acme-sh/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
 - An ACME protocol client written purely in Shell (Unix shell) language.
 - Full ACME protocol implementation.
 - Simple, powerful and very easy to use. You only need 3 minutes to learn it.
@@ -7,14 +9,33 @@
 - Purely written in Shell with no dependencies on python or the official Let's Encrypt client.
 - Just one script to issue, renew and install your certificates automatically.
 - DOES NOT require `root/sudoer` access.
+- Docker friendly
+- IPv6 support
 
-It's probably the `easiest&smallest&smartest` shell script to automatically issue & renew the free certificates from Let's Encrypt.
+It's probably the `easiest & smartest` shell script to automatically issue & renew the free certificates from Let's Encrypt.
 
 Wiki: https://github.com/Neilpang/acme.sh/wiki
 
+For Docker Fans: [acme.sh :two_hearts: Docker ](https://github.com/Neilpang/acme.sh/wiki/Run-acme.sh-in-docker)
+
+Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
+
 
 # [中文说明](https://github.com/Neilpang/acme.sh/wiki/%E8%AF%B4%E6%98%8E)
 
+# Who are using **acme.sh**
+- [FreeBSD.org](https://blog.crashed.org/letsencrypt-in-freebsd-org/)
+- [ruby-china.org](https://ruby-china.org/topics/31983)
+- [Proxmox](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x_and_newer))
+- [pfsense](https://github.com/pfsense/FreeBSD-ports/pull/89)
+- [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)
+- [splynx](https://forum.splynx.com/t/free-ssl-cert-for-splynx-lets-encrypt/297)
+- [archlinux](https://aur.archlinux.org/packages/acme.sh-git/)
+- [opnsense.org](https://github.com/opnsense/plugins/tree/master/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient)
+- [more...](https://github.com/Neilpang/acme.sh/wiki/Blogs-and-tutorials)
 
 # Tested OS
 
@@ -39,8 +60,9 @@ Wiki: https://github.com/Neilpang/acme.sh/wiki
 |17|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/Neilpang/acme.sh/wiki/How-to-run-on-OpenWRT)
 |18|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/solaris.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|SunOS/Solaris
 |19|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/gentoo-stage3-amd64.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Gentoo Linux
+|20|[![Build Status](https://travis-ci.org/Neilpang/acme.sh.svg?branch=master)](https://travis-ci.org/Neilpang/acme.sh)|Mac OSX
 
-For all build statuses, check our [daily build project](https://github.com/Neilpang/acmetest):
+For all build statuses, check our [weekly build project](https://github.com/Neilpang/acmetest):
 
 https://github.com/Neilpang/acmetest
 
@@ -50,7 +72,9 @@ https://github.com/Neilpang/acmetest
 - Webroot mode
 - Standalone mode
 - Apache mode
+- Nginx mode ( Beta )
 - DNS mode
+- [Stateless mode](https://github.com/Neilpang/acme.sh/wiki/Stateless-Mode)
 
 
 # 1. How to install
@@ -115,13 +139,25 @@ root@v1:~# acme.sh -h
 acme.sh --issue -d example.com -w /home/wwwroot/example.com
 ```
 
+or:
+
+```bash
+acme.sh --issue -d example.com -w /home/username/public_html
+```
+
+or:
+
+```bash
+acme.sh --issue -d example.com -w /var/www/html
+```
+
 **Example 2:** Multiple domains in the same cert.
 
 ```bash
 acme.sh --issue -d example.com -d www.example.com -d cp.example.com -w /home/wwwroot/example.com
 ```
 
-The parameter `/home/wwwroot/example.com` is the web root folder. You **MUST** have `write access` to this folder.
+The parameter `/home/wwwroot/example.com` or `/home/username/public_html` or `/var/www/html` is the web root folder where you host your website files. You **MUST** have `write access` to this folder.
 
 Second argument **"example.com"** is the main domain you want to issue the cert for.
 You must have at least one domain there.
@@ -142,26 +178,28 @@ You **MUST** use this command to copy the certs to the target files, **DO NOT**
 
 **Apache** example:
 ```bash
-acme.sh --installcert -d example.com \
---certpath      /path/to/certfile/in/apache/cert.pem  \
---keypath       /path/to/keyfile/in/apache/key.pem  \
---fullchainpath /path/to/fullchain/certfile/apache/fullchain.pem \
---reloadcmd     "service apache2 restart"
+acme.sh --install-cert -d example.com \
+--cert-file      /path/to/certfile/in/apache/cert.pem  \
+--key-file       /path/to/keyfile/in/apache/key.pem  \
+--fullchain-file /path/to/fullchain/certfile/apache/fullchain.pem \
+--reloadcmd     "service apache2 force-reload"
 ```
 
 **Nginx** example:
 ```bash
-acme.sh --installcert -d example.com \
---keypath       /path/to/keyfile/in/nginx/key.pem  \
---fullchainpath /path/to/fullchain/nginx/cert.pem \
---reloadcmd     "service nginx restart"
+acme.sh --install-cert -d example.com \
+--key-file       /path/to/keyfile/in/nginx/key.pem  \
+--fullchain-file /path/to/fullchain/nginx/cert.pem \
+--reloadcmd     "service nginx force-reload"
 ```
 
 Only the domain is required, all the other parameters are optional.
 
+The ownership and permission info of existing files are preserved. You may want to precreate the files to have defined ownership and permission.
+
 Install/copy the issued cert/key to the production Apache or Nginx path.
 
-The cert will be `renewed every **60** days by default` (which is configurable). Once the cert is renewed, the Apache/Nginx service will be restarted automatically by the command: `service apache2 restart` or `service nginx restart`.
+The cert will be renewed every **60** days by default (which is configurable). Once the cert is renewed, the Apache/Nginx service will be reloaded automatically by the command: `service apache2 force-reload` or `service nginx force-reload`.
 
 
 # 4. Use Standalone server to issue cert
@@ -208,8 +246,27 @@ acme.sh --issue --apache -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
 
+# 7. Use Nginx mode
+
+**(requires you to be root/sudoer, since it is required to interact with Nginx server)**
+
+If you are running a web server, Apache or Nginx, it is recommended to use the `Webroot mode`.
+
+Particularly, if you are running an nginx server, you can use nginx mode instead. This mode doesn't write any files to your web root folder.
+
+Just set string "nginx" as the second argument.
+
+It will configure nginx server automatically to verify the domain and then restore the nginx config to the original version.
+
+So, the config is not changed.
 
-# 7. Use DNS mode:
+```
+acme.sh --issue --nginx -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
+
+# 8. Use DNS mode:
 
 Support the `dns-01` challenge.
 
@@ -239,8 +296,11 @@ acme.sh --renew -d example.com
 
 Ok, it's finished.
 
+**Take care, this is dns manual mode, it can not be renewed automatically. you will have to add a new txt record to your domain by your hand when you renew your cert.**
 
-# 8. Automatic DNS API integration
+**Please use dns api mode instead.**
+
+# 9. Automatic DNS API integration
 
 If your DNS provider supports API access, we can use that API to automatically issue the certs.
 
@@ -252,17 +312,45 @@ You don't have to do anything manually!
 1. DNSPod.cn API
 1. CloudXNS.com API
 1. GoDaddy.com API
-1. OVH, kimsufi, soyoustart and runabove API
-1. AWS Route 53
 1. PowerDNS.com API
-1. 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.)
+1. OVH, kimsufi, soyoustart and runabove API
+1. nsupdate API
 1. LuaDNS.com API
 1. DNSMadeEasy.com API
-1. nsupdate API
+1. AWS Route 53
 1. aliyun.com(阿里云) API
 1. ISPConfig 3.1 API
+1. Alwaysdata.com API
+1. Linode.com API
+1. FreeDNS (https://freedns.afraid.org/)
+1. cyon.ch
+1. Domain-Offensive/Resellerinterface/Domainrobot API
+1. Gandi LiveDNS API
+1. Knot DNS API
+1. DigitalOcean API (native)
+1. ClouDNS.net API
+1. Infoblox NIOS API (https://www.infoblox.com/)
+1. VSCALE (https://vscale.io/)
+1. Dynu API (https://www.dynu.com)
+1. DNSimple API
+1. NS1.com API
+1. DuckDNS.org API
+1. Name.com API
+1. Dyn Managed DNS API
+1. Yandex PDD API (https://pdd.yandex.ru)
+1. Hurricane Electric DNS service (https://dns.he.net)
+1. UnoEuro API (https://www.unoeuro.com/)
+1. INWX (https://www.inwx.de/)
+1. Servercow (https://servercow.de)
+
+
+And: 
 
+1. 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.)
+
+
+   
 **More APIs coming soon...**
 
 If your DNS provider is not on the supported list above, you can write your own DNS API script easily. If you do, please consider submitting a [Pull Request](https://github.com/Neilpang/acme.sh/pulls) and contribute it to the project.
@@ -270,7 +358,7 @@ 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)
 
 
-# 9. Issue ECC certificates
+# 10. Issue ECC certificates
 
 `Let's Encrypt` can now issue **ECDSA** certificates.
 
@@ -280,7 +368,7 @@ Just set the `length` parameter with a prefix `ec-`.
 
 For example:
 
-### Single domain ECC cerfiticate
+### Single domain ECC certificate
 
 ```bash
 acme.sh --issue -w /home/wwwroot/example.com -d example.com --keylength ec-256
@@ -301,7 +389,7 @@ Valid values are:
 3. **ec-521 (secp521r1,  "ECDSA P-521", which is not supported by Let's Encrypt yet.)**
 
 
-# 10. How to renew the issued certs
+# 11. How to renew the issued certs
 
 No, you don't need to renew the certs manually. All the certs will be renewed automatically every **60** days.
 
@@ -318,9 +406,9 @@ acme.sh --renew -d example.com --force --ecc
 ```
 
 
-# 11. How to upgrade `acme.sh`
+# 12. How to upgrade `acme.sh`
 
-acme.sh is in constant developement, so it's strongly recommended to use the latest code.
+acme.sh is in constant development, so it's strongly recommended to use the latest code.
 
 You can update acme.sh to the latest code:
 
@@ -343,26 +431,26 @@ acme.sh --upgrade --auto-upgrade 0
 ```
 
 
-# 12. Issue a cert from an existing CSR
+# 13. Issue a cert from an existing CSR
 
 https://github.com/Neilpang/acme.sh/wiki/Issue-a-cert-from-existing-CSR
 
 
-# Under the Hood
+# 14. Under the Hood
 
 Speak ACME language using shell, directly to "Let's Encrypt".
 
 TODO:
 
 
-# Acknowledgments
+# 15. Acknowledgments
 
 1. Acme-tiny: https://github.com/diafygi/acme-tiny
 2. ACME protocol: https://github.com/ietf-wg-acme/acme
 3. Certbot: https://github.com/certbot/certbot
 
 
-# License & Others
+# 16. License & Others
 
 License is GPLv3
 
@@ -371,8 +459,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.
 
 
-# Donate
-
-1. PayPal: donate@acme.sh
+# 17. 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
+ 587 - 103
acme.sh


+ 118 - 0
deploy/README.md

@@ -0,0 +1,118 @@
+# Using deploy api
+
+Before you can deploy your cert, you must [issue the cert first](https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert).
+
+Here are the scripts to deploy the certs/key to the server/services.
+
+## 1. Deploy the certs to your cpanel host
+
+If you want to deploy using cpanel UAPI see 7.
+
+(cpanel deploy hook is not finished yet, this is just an example.)
+
+
+
+Then you can deploy now:
+
+```sh
+export DEPLOY_CPANEL_USER=myusername
+export DEPLOY_CPANEL_PASSWORD=PASSWORD
+acme.sh --deploy -d example.com --deploy-hook cpanel
+```
+
+## 2. Deploy ssl cert on kong proxy engine based on api
+
+Before you can deploy your cert, you must [issue the cert first](https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert).
+Currently supports Kong-v0.10.x.
+
+```sh
+acme.sh --deploy -d ftp.example.com --deploy-hook kong
+```
+
+## 3. Deploy the cert to remote server through SSH access
+
+(TODO)
+
+## 4. Deploy the cert to local vsftpd server
+
+```sh
+acme.sh --deploy -d ftp.example.com --deploy-hook vsftpd
+```
+
+The default vsftpd conf file is `/etc/vsftpd.conf`,  if your vsftpd conf is not in the default location, you can specify one:
+
+```sh
+export DEPLOY_VSFTPD_CONF="/etc/vsftpd.conf"
+
+acme.sh --deploy -d ftp.example.com --deploy-hook vsftpd
+```
+
+The default command to restart vsftpd server is `service vsftpd restart`, if it doesn't work, you can specify one:
+
+```sh
+export DEPLOY_VSFTPD_RELOAD="/etc/init.d/vsftpd restart"
+
+acme.sh --deploy -d ftp.example.com --deploy-hook vsftpd
+```
+
+## 5. Deploy the cert to local exim4 server
+
+```sh
+acme.sh --deploy -d ftp.example.com --deploy-hook exim4
+```
+
+The default exim4 conf file is `/etc/exim/exim.conf`,  if your exim4 conf is not in the default location, you can specify one:
+
+```sh
+export DEPLOY_EXIM4_CONF="/etc/exim4/exim4.conf.template"
+
+acme.sh --deploy -d ftp.example.com --deploy-hook exim4
+```
+
+The default command to restart exim4 server is `service exim4 restart`, if it doesn't work, you can specify one:
+
+```sh
+export DEPLOY_EXIM4_RELOAD="/etc/init.d/exim4 restart"
+
+acme.sh --deploy -d ftp.example.com --deploy-hook exim4
+```
+
+## 6. Deploy the cert to OSX Keychain
+
+```sh
+acme.sh --deploy -d ftp.example.com --deploy-hook keychain
+```
+
+## 7. Deploy to cpanel host using UAPI
+
+This hook is using UAPI and works in cPanel & WHM version 56 or newer.
+```
+acme.sh  --deploy  -d example.com  --deploy-hook cpanel_uapi
+```
+DEPLOY_CPANEL_USER is required only if you run the script as root and it should contain cpanel username.
+```sh
+export DEPLOY_CPANEL_USER=username
+acme.sh  --deploy  -d example.com  --deploy-hook cpanel_uapi
+```
+Please note, that the cpanel_uapi hook will deploy only the first domain when your certificate will automatically renew. Therefore you should issue a separete certificate for each domain. 
+
+## 8. Deploy the cert to your FRITZ!Box router
+
+You must specify the credentials that have administrative privileges on the FRITZ!Box in order to deploy the certificate, plus the URL of your FRITZ!Box, through the following environment variables:
+```sh
+$ export DEPLOY_FRITZBOX_USERNAME=my_username
+$ export DEPLOY_FRITZBOX_PASSWORD=the_password
+$ export DEPLOY_FRITZBOX_URL=https://fritzbox.example.com
+```
+
+After the first deployment, these values will be stored in your $HOME/.acme.sh/account.conf. You may now deploy the certificate like this:
+
+```sh
+acme.sh --deploy -d fritzbox.example.com --deploy-hook fritzbox
+```
+
+## 9. Deploy the cert to strongswan
+
+```sh
+acme.sh --deploy -d ftp.example.com --deploy-hook strongswan
+```

+ 26 - 0
deploy/apache.sh

@@ -0,0 +1,26 @@
+#!/usr/bin/env sh
+
+#Here is a script to deploy cert to apache server.
+
+#returns 0 means success, otherwise error.
+
+########  Public functions #####################
+
+#domain keyfile certfile cafile fullchain
+apache_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"
+
+  _err "Deploy cert to apache server, Not implemented yet"
+  return 1
+
+}

+ 64 - 0
deploy/cpanel_uapi.sh

@@ -0,0 +1,64 @@
+#!/usr/bin/env sh
+# 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
+
+#export DEPLOY_CPANEL_USER=myusername
+
+########  Public functions #####################
+
+#domain keyfile certfile cafile fullchain
+
+cpanel_uapi_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 ! _exists uapi; then
+    _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\");")
+
+  _debug _cert "$_cert"
+  _debug _key "$_key"
+
+  if [ "$(id -u)" = 0 ]; then
+    if [ -z "$DEPLOY_CPANEL_USER" ]; then
+      _err "It seems that you are root, please define the target user name: export DEPLOY_CPANEL_USER=username"
+      return 1
+    fi
+    _savedomainconf DEPLOY_CPANEL_USER "$DEPLOY_CPANEL_USER"
+    _response=$(uapi --user="$DEPLOY_CPANEL_USER" SSL install_ssl domain="$_cdomain" cert="$_cert" key="$_key")
+  else
+    _response=$(uapi SSL install_ssl domain="$_cdomain" cert="$_cert" key="$_key")
+  fi
+  error_response="status: 0"
+  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
+}

+ 26 - 0
deploy/dovecot.sh

@@ -0,0 +1,26 @@
+#!/usr/bin/env sh
+
+#Here is a script to deploy cert to dovecot server.
+
+#returns 0 means success, otherwise error.
+
+########  Public functions #####################
+
+#domain keyfile certfile cafile fullchain
+dovecot_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"
+
+  _err "Not implemented yet"
+  return 1
+
+}

+ 114 - 0
deploy/exim4.sh

@@ -0,0 +1,114 @@
+#!/usr/bin/env sh
+
+#Here is a script to deploy cert to exim4 server.
+
+#returns 0 means success, otherwise error.
+
+#DEPLOY_EXIM4_CONF="/etc/exim/exim.conf"
+#DEPLOY_EXIM4_RELOAD="service exim4 restart"
+
+########  Public functions #####################
+
+#domain keyfile certfile cafile fullchain
+exim4_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"
+
+  _ssl_path="/etc/acme.sh/exim4"
+  if ! mkdir -p "$_ssl_path"; then
+    _err "Can not create folder:$_ssl_path"
+    return 1
+  fi
+
+  _info "Copying key and cert"
+  _real_key="$_ssl_path/exim4.key"
+  if ! cat "$_ckey" >"$_real_key"; then
+    _err "Error: write key file to: $_real_key"
+    return 1
+  fi
+  _real_fullchain="$_ssl_path/exim4.pem"
+  if ! cat "$_cfullchain" >"$_real_fullchain"; then
+    _err "Error: write key file to: $_real_fullchain"
+    return 1
+  fi
+
+  DEFAULT_EXIM4_RELOAD="service exim4 restart"
+  _reload="${DEPLOY_EXIM4_RELOAD:-$DEFAULT_EXIM4_RELOAD}"
+
+  if [ -z "$IS_RENEW" ]; then
+    DEFAULT_EXIM4_CONF="/etc/exim/exim.conf"
+    if [ ! -f "$DEFAULT_EXIM4_CONF" ]; then
+      DEFAULT_EXIM4_CONF="/etc/exim4/exim4.conf.template"
+    fi
+    _exim4_conf="${DEPLOY_EXIM4_CONF:-$DEFAULT_EXIM4_CONF}"
+    _debug _exim4_conf "$_exim4_conf"
+    if [ ! -f "$_exim4_conf" ]; then
+      if [ -z "$DEPLOY_EXIM4_CONF" ]; then
+        _err "exim4 conf is not found, please define DEPLOY_EXIM4_CONF"
+        return 1
+      else
+        _err "It seems that the specified exim4 conf is not valid, please check."
+        return 1
+      fi
+    fi
+    if [ ! -w "$_exim4_conf" ]; then
+      _err "The file $_exim4_conf is not writable, please change the permission."
+      return 1
+    fi
+    _backup_conf="$DOMAIN_BACKUP_PATH/exim4.conf.bak"
+    _info "Backup $_exim4_conf to $_backup_conf"
+    cp "$_exim4_conf" "$_backup_conf"
+
+    _info "Modify exim4 conf: $_exim4_conf"
+    if _setopt "$_exim4_conf" "tls_certificate" "=" "$_real_fullchain" \
+      && _setopt "$_exim4_conf" "tls_privatekey" "=" "$_real_key"; then
+      _info "Set config success!"
+    else
+      _err "Config exim4 server error, please report bug to us."
+      _info "Restoring exim4 conf"
+      if cat "$_backup_conf" >"$_exim4_conf"; then
+        _info "Restore conf success"
+        eval "$_reload"
+      else
+        _err "Oops, error restore exim4 conf, please report bug to us."
+      fi
+      return 1
+    fi
+  fi
+
+  _info "Run reload: $_reload"
+  if eval "$_reload"; then
+    _info "Reload success!"
+    if [ "$DEPLOY_EXIM4_CONF" ]; then
+      _savedomainconf DEPLOY_EXIM4_CONF "$DEPLOY_EXIM4_CONF"
+    else
+      _cleardomainconf DEPLOY_EXIM4_CONF
+    fi
+    if [ "$DEPLOY_EXIM4_RELOAD" ]; then
+      _savedomainconf DEPLOY_EXIM4_RELOAD "$DEPLOY_EXIM4_RELOAD"
+    else
+      _cleardomainconf DEPLOY_EXIM4_RELOAD
+    fi
+    return 0
+  else
+    _err "Reload error, restoring"
+    if cat "$_backup_conf" >"$_exim4_conf"; then
+      _info "Restore conf success"
+      eval "$_reload"
+    else
+      _err "Oops, error restore exim4 conf, please report bug to us."
+    fi
+    return 1
+  fi
+  return 0
+
+}

+ 108 - 0
deploy/fritzbox.sh

@@ -0,0 +1,108 @@
+#!/usr/bin/env sh
+
+#Here is a script to deploy cert to an AVM FRITZ!Box router.
+
+#returns 0 means success, otherwise error.
+
+#DEPLOY_FRITZBOX_USERNAME="username"
+#DEPLOY_FRITZBOX_PASSWORD="password"
+#DEPLOY_FRITZBOX_URL="https://fritz.box"
+
+# Kudos to wikrie at Github for his FRITZ!Box update script:
+# https://gist.github.com/wikrie/f1d5747a714e0a34d0582981f7cb4cfb
+
+########  Public functions #####################
+
+#domain keyfile certfile cafile fullchain
+fritzbox_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 ! _exists iconv; then
+    _err "iconv not found"
+    return 1
+  fi
+
+  _fritzbox_username="${DEPLOY_FRITZBOX_USERNAME}"
+  _fritzbox_password="${DEPLOY_FRITZBOX_PASSWORD}"
+  _fritzbox_url="${DEPLOY_FRITZBOX_URL}"
+
+  _debug _fritzbox_url "$_fritzbox_url"
+  _debug _fritzbox_username "$_fritzbox_username"
+  _secure_debug _fritzbox_password "$_fritzbox_password"
+  if [ -z "$_fritzbox_username" ]; then
+    _err "FRITZ!Box username is not found, please define DEPLOY_FRITZBOX_USERNAME."
+    return 1
+  fi
+  if [ -z "$_fritzbox_password" ]; then
+    _err "FRITZ!Box password is not found, please define DEPLOY_FRITZBOX_PASSWORD."
+    return 1
+  fi
+  if [ -z "$_fritzbox_url" ]; then
+    _err "FRITZ!Box url is not found, please define DEPLOY_FRITZBOX_URL."
+    return 1
+  fi
+
+  _saveaccountconf DEPLOY_FRITZBOX_USERNAME "${_fritzbox_username}"
+  _saveaccountconf DEPLOY_FRITZBOX_PASSWORD "${_fritzbox_password}"
+  _saveaccountconf DEPLOY_FRITZBOX_URL "${_fritzbox_url}"
+
+  # Do not check for a valid SSL certificate, because initially the cert is not valid, so it could not install the LE generated certificate
+  export HTTPS_INSECURE=1
+
+  _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}')"
+  _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
+    _err "Logging in to the FRITZ!Box failed. Please check username, password and URL."
+    return 1
+  fi
+
+  _info "Generate form POST request"
+  _post_request="$(_mktemp)"
+  _post_boundary="---------------------------$(date +%Y%m%d%H%M%S)"
+  # _CERTPASSWORD_ is unset because Let's Encrypt certificates don't have a password. But if they ever do, here's the place to use it!
+  _CERTPASSWORD_=
+  {
+    printf -- "--"
+    printf -- "%s\r\n" "${_post_boundary}"
+    printf "Content-Disposition: form-data; name=\"sid\"\r\n\r\n%s\r\n" "${_fritzbox_sid}"
+    printf -- "--"
+    printf -- "%s\r\n" "${_post_boundary}"
+    printf "Content-Disposition: form-data; name=\"BoxCertPassword\"\r\n\r\n%s\r\n" "${_CERTPASSWORD_}"
+    printf -- "--"
+    printf -- "%s\r\n" "${_post_boundary}"
+    printf "Content-Disposition: form-data; name=\"BoxCertImportFile\"; filename=\"BoxCert.pem\"\r\n"
+    printf "Content-Type: application/octet-stream\r\n\r\n"
+    cat "${_ckey}" "${_cfullchain}"
+    printf "\r\n"
+    printf -- "--"
+    printf -- "%s--" "${_post_boundary}"
+  } >>"${_post_request}"
+
+  _info "Upload certificate to the FRITZ!Box"
+
+  export _H1="Content-type: multipart/form-data boundary=${_post_boundary}"
+  _post "$(cat "${_post_request}")" "${_fritzbox_url}/cgi-bin/firmwarecfg" | grep SSL
+
+  retval=$?
+  if [ $retval = 0 ]; then
+    _info "Upload successful"
+  else
+    _err "Upload failed"
+  fi
+  rm "${_post_request}"
+
+  return $retval
+}

+ 26 - 0
deploy/haproxy.sh

@@ -0,0 +1,26 @@
+#!/usr/bin/env sh
+
+#Here is a script to deploy cert to haproxy server.
+
+#returns 0 means success, otherwise error.
+
+########  Public functions #####################
+
+#domain keyfile certfile cafile fullchain
+haproxy_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"
+
+  _err "deploy cert to haproxy server, Not implemented yet"
+  return 1
+
+}

+ 31 - 0
deploy/keychain.sh

@@ -0,0 +1,31 @@
+#!/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
+keychain_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"
+
+  /usr/bin/security import "$_ckey" -k "/Library/Keychains/System.keychain"
+  /usr/bin/security import "$_ccert" -k "/Library/Keychains/System.keychain"
+  /usr/bin/security import "$_cca" -k "/Library/Keychains/System.keychain"
+  /usr/bin/security import "$_cfullchain" -k "/Library/Keychains/System.keychain"
+
+  return 0
+}

+ 77 - 0
deploy/kong.sh

@@ -0,0 +1,77 @@
+#!/usr/bin/env sh
+# If certificate already exist it will update only cert and key not touching other parameter
+# If certificate  doesn't exist it will only upload cert and key and not set other parameter
+# Note that we deploy full chain
+# Written by Geoffroi Genot <ggenot@voxbone.com>
+
+########  Public functions #####################
+
+#domain keyfile certfile cafile fullchain
+kong_deploy() {
+  _cdomain="$1"
+  _ckey="$2"
+  _ccert="$3"
+  _cca="$4"
+  _cfullchain="$5"
+  _info "Deploying certificate on Kong instance"
+  if [ -z "$KONG_URL" ]; then
+    _debug "KONG_URL Not set, using default http://localhost:8001"
+    KONG_URL="http://localhost:8001"
+  fi
+
+  _debug _cdomain "$_cdomain"
+  _debug _ckey "$_ckey"
+  _debug _ccert "$_ccert"
+  _debug _cca "$_cca"
+  _debug _cfullchain "$_cfullchain"
+
+  #Get ssl_uuid linked to the domain
+  ssl_uuid=$(_get "$KONG_URL/certificates/$_cdomain" | _normalizeJson | _egrep_o '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
+  if [ -z "$ssl_uuid" ]; then
+    _debug "Unable to get Kong ssl_uuid for domain $_cdomain"
+    _debug "Make sure that KONG_URL is correctly configured"
+    _debug "Make sure that a Kong certificate match the sni"
+    _debug "Kong url: $KONG_URL"
+    _info "No existing certificate, creating..."
+    #return 1
+  fi
+  #Save kong url if it's succesful (First run case)
+  _saveaccountconf KONG_URL "$KONG_URL"
+  #Generate DEIM
+  delim="-----MultipartDelimiter$(date "+%s%N")"
+  nl="\015\012"
+  #Set Header
+  _H1="Content-Type: multipart/form-data; boundary=$delim"
+  #Generate data for request (Multipart/form-data with mixed content)
+  if [ -z "$ssl_uuid" ]; then
+    #set sni to domain
+    content="--$delim${nl}Content-Disposition: form-data; name=\"snis\"${nl}${nl}$_cdomain"
+  fi
+  #add key
+  content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"key\"; filename=\"$(basename "$_ckey")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")"
+  #Add cert
+  content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"cert\"; filename=\"$(basename "$_cfullchain")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_cfullchain")"
+  #Close multipart
+  content="$content${nl}--$delim--${nl}"
+  #Convert CRLF
+  content=$(printf %b "$content")
+  #DEBUG
+  _debug header "$_H1"
+  _debug content "$content"
+  #Check if sslcreated (if not => POST else => PATCH)
+
+  if [ -z "$ssl_uuid" ]; then
+    #Post certificate to Kong
+    response=$(_post "$content" "$KONG_URL/certificates" "" "POST")
+  else
+    #patch
+    response=$(_post "$content" "$KONG_URL/certificates/$ssl_uuid" "" "PATCH")
+  fi
+  if ! [ "$(echo "$response" | _egrep_o "created_at")" = "created_at" ]; then
+    _err "An error occurred with cert upload. Check response:"
+    _err "$response"
+    return 1
+  fi
+  _debug response "$response"
+  _info "Certificate successfully deployed"
+}

+ 26 - 0
deploy/myapi.sh

@@ -0,0 +1,26 @@
+#!/usr/bin/env sh
+
+#Here is a script to deploy cert to mysqld server.
+
+#returns 0 means success, otherwise error.
+
+########  Public functions #####################
+
+#domain keyfile certfile cafile fullchain
+mysqld_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"
+
+  _err "deploy cert to mysqld server, Not implemented yet"
+  return 1
+
+}

+ 26 - 0
deploy/nginx.sh

@@ -0,0 +1,26 @@
+#!/usr/bin/env sh
+
+#Here is a script to deploy cert to nginx server.
+
+#returns 0 means success, otherwise error.
+
+########  Public functions #####################
+
+#domain keyfile certfile cafile fullchain
+nginx_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"
+
+  _err "deploy cert to nginx server, Not implemented yet"
+  return 1
+
+}

+ 26 - 0
deploy/opensshd.sh

@@ -0,0 +1,26 @@
+#!/usr/bin/env sh
+
+#Here is a script to deploy cert to opensshd server.
+
+#returns 0 means success, otherwise error.
+
+########  Public functions #####################
+
+#domain keyfile certfile cafile fullchain
+opensshd_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"
+
+  _err "deploy cert to opensshd server, Not implemented yet"
+  return 1
+
+}

+ 26 - 0
deploy/pureftpd.sh

@@ -0,0 +1,26 @@
+#!/usr/bin/env sh
+
+#Here is a script to deploy cert to pureftpd server.
+
+#returns 0 means success, otherwise error.
+
+########  Public functions #####################
+
+#domain keyfile certfile cafile fullchain
+pureftpd_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"
+
+  _err "deploy cert to pureftpd server, Not implemented yet"
+  return 1
+
+}

+ 32 - 0
deploy/strongswan.sh

@@ -0,0 +1,32 @@
+#!/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
+strongswan_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"
+
+  cat "$_ckey" >"/etc/ipsec.d/private/$(basename "$_ckey")"
+  cat "$_ccert" >"/etc/ipsec.d/certs/$(basename "$_ccert")"
+  cat "$_cca" >"/etc/ipsec.d/cacerts/$(basename "$_cca")"
+  cat "$_cfullchain" >"/etc/ipsec.d/cacerts/$(basename "$_cfullchain")"
+
+  ipsec reload
+
+}

+ 100 - 0
deploy/unifi.sh

@@ -0,0 +1,100 @@
+#!/usr/bin/env sh
+
+#Here is a script to deploy cert to unifi server.
+
+#returns 0 means success, otherwise error.
+
+#DEPLOY_UNIFI_KEYSTORE="/usr/lib/unifi/data/keystore"
+#DEPLOY_UNIFI_KEYPASS="aircontrolenterprise"
+#DEPLOY_UNIFI_RELOAD="service unifi restart"
+
+########  Public functions #####################
+
+#domain keyfile certfile cafile fullchain
+unifi_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 ! _exists keytool; then
+    _err "keytool not found"
+    return 1
+  fi
+
+  DEFAULT_UNIFI_KEYSTORE="/usr/lib/unifi/data/keystore"
+  _unifi_keystore="${DEPLOY_UNIFI_KEYSTORE:-$DEFAULT_UNIFI_KEYSTORE}"
+  DEFAULT_UNIFI_KEYPASS="aircontrolenterprise"
+  _unifi_keypass="${DEPLOY_UNIFI_KEYPASS:-$DEFAULT_UNIFI_KEYPASS}"
+  DEFAULT_UNIFI_RELOAD="service unifi restart"
+  _reload="${DEPLOY_UNIFI_RELOAD:-$DEFAULT_UNIFI_RELOAD}"
+
+  _debug _unifi_keystore "$_unifi_keystore"
+  if [ ! -f "$_unifi_keystore" ]; then
+    if [ -z "$DEPLOY_UNIFI_KEYSTORE" ]; then
+      _err "unifi keystore is not found, please define DEPLOY_UNIFI_KEYSTORE"
+      return 1
+    else
+      _err "It seems that the specified unifi keystore is not valid, please check."
+      return 1
+    fi
+  fi
+  if [ ! -w "$_unifi_keystore" ]; then
+    _err "The file $_unifi_keystore is not writable, please change the permission."
+    return 1
+  fi
+
+  _info "Generate import pkcs12"
+  _import_pkcs12="$(_mktemp)"
+  _toPkcs "$_import_pkcs12" "$_ckey" "$_ccert" "$_cca" "$_unifi_keypass" unifi root
+  if [ "$?" != "0" ]; then
+    _err "Oops, error creating import pkcs12, please report bug to us."
+    return 1
+  fi
+
+  _info "Modify unifi keystore: $_unifi_keystore"
+  if keytool -importkeystore \
+    -deststorepass "$_unifi_keypass" -destkeypass "$_unifi_keypass" -destkeystore "$_unifi_keystore" \
+    -srckeystore "$_import_pkcs12" -srcstoretype PKCS12 -srcstorepass "$_unifi_keypass" \
+    -alias unifi -noprompt; then
+    _info "Import keystore success!"
+    rm "$_import_pkcs12"
+  else
+    _err "Import unifi keystore error, please report bug to us."
+    rm "$_import_pkcs12"
+    return 1
+  fi
+
+  _info "Run reload: $_reload"
+  if eval "$_reload"; then
+    _info "Reload success!"
+    if [ "$DEPLOY_UNIFI_KEYSTORE" ]; then
+      _savedomainconf DEPLOY_UNIFI_KEYSTORE "$DEPLOY_UNIFI_KEYSTORE"
+    else
+      _cleardomainconf DEPLOY_UNIFI_KEYSTORE
+    fi
+    if [ "$DEPLOY_UNIFI_KEYPASS" ]; then
+      _savedomainconf DEPLOY_UNIFI_KEYPASS "$DEPLOY_UNIFI_KEYPASS"
+    else
+      _cleardomainconf DEPLOY_UNIFI_KEYPASS
+    fi
+    if [ "$DEPLOY_UNIFI_RELOAD" ]; then
+      _savedomainconf DEPLOY_UNIFI_RELOAD "$DEPLOY_UNIFI_RELOAD"
+    else
+      _cleardomainconf DEPLOY_UNIFI_RELOAD
+    fi
+    return 0
+  else
+    _err "Reload error"
+    return 1
+  fi
+  return 0
+
+}

+ 110 - 0
deploy/vsftpd.sh

@@ -0,0 +1,110 @@
+#!/usr/bin/env sh
+
+#Here is a script to deploy cert to vsftpd server.
+
+#returns 0 means success, otherwise error.
+
+#DEPLOY_VSFTPD_CONF="/etc/vsftpd.conf"
+#DEPLOY_VSFTPD_RELOAD="service vsftpd restart"
+
+########  Public functions #####################
+
+#domain keyfile certfile cafile fullchain
+vsftpd_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"
+
+  _ssl_path="/etc/acme.sh/vsftpd"
+  if ! mkdir -p "$_ssl_path"; then
+    _err "Can not create folder:$_ssl_path"
+    return 1
+  fi
+
+  _info "Copying key and cert"
+  _real_key="$_ssl_path/vsftpd.key"
+  if ! cat "$_ckey" >"$_real_key"; then
+    _err "Error: write key file to: $_real_key"
+    return 1
+  fi
+  _real_fullchain="$_ssl_path/vsftpd.chain.pem"
+  if ! cat "$_cfullchain" >"$_real_fullchain"; then
+    _err "Error: write key file to: $_real_fullchain"
+    return 1
+  fi
+
+  DEFAULT_VSFTPD_RELOAD="service vsftpd restart"
+  _reload="${DEPLOY_VSFTPD_RELOAD:-$DEFAULT_VSFTPD_RELOAD}"
+
+  if [ -z "$IS_RENEW" ]; then
+    DEFAULT_VSFTPD_CONF="/etc/vsftpd.conf"
+    _vsftpd_conf="${DEPLOY_VSFTPD_CONF:-$DEFAULT_VSFTPD_CONF}"
+    if [ ! -f "$_vsftpd_conf" ]; then
+      if [ -z "$DEPLOY_VSFTPD_CONF" ]; then
+        _err "vsftpd conf is not found, please define DEPLOY_VSFTPD_CONF"
+        return 1
+      else
+        _err "It seems that the specified vsftpd conf is not valid, please check."
+        return 1
+      fi
+    fi
+    if [ ! -w "$_vsftpd_conf" ]; then
+      _err "The file $_vsftpd_conf is not writable, please change the permission."
+      return 1
+    fi
+    _backup_conf="$DOMAIN_BACKUP_PATH/vsftpd.conf.bak"
+    _info "Backup $_vsftpd_conf to $_backup_conf"
+    cp "$_vsftpd_conf" "$_backup_conf"
+
+    _info "Modify vsftpd conf: $_vsftpd_conf"
+    if _setopt "$_vsftpd_conf" "rsa_cert_file" "=" "$_real_fullchain" \
+      && _setopt "$_vsftpd_conf" "rsa_private_key_file" "=" "$_real_key" \
+      && _setopt "$_vsftpd_conf" "ssl_enable" "=" "YES"; then
+      _info "Set config success!"
+    else
+      _err "Config vsftpd server error, please report bug to us."
+      _info "Restoring vsftpd conf"
+      if cat "$_backup_conf" >"$_vsftpd_conf"; then
+        _info "Restore conf success"
+        eval "$_reload"
+      else
+        _err "Oops, error restore vsftpd conf, please report bug to us."
+      fi
+      return 1
+    fi
+  fi
+
+  _info "Run reload: $_reload"
+  if eval "$_reload"; then
+    _info "Reload success!"
+    if [ "$DEPLOY_VSFTPD_CONF" ]; then
+      _savedomainconf DEPLOY_VSFTPD_CONF "$DEPLOY_VSFTPD_CONF"
+    else
+      _cleardomainconf DEPLOY_VSFTPD_CONF
+    fi
+    if [ "$DEPLOY_VSFTPD_RELOAD" ]; then
+      _savedomainconf DEPLOY_VSFTPD_RELOAD "$DEPLOY_VSFTPD_RELOAD"
+    else
+      _cleardomainconf DEPLOY_VSFTPD_RELOAD
+    fi
+    return 0
+  else
+    _err "Reload error, restoring"
+    if cat "$_backup_conf" >"$_vsftpd_conf"; then
+      _info "Restore conf success"
+      eval "$_reload"
+    else
+      _err "Oops, error restore vsftpd conf, please report bug to us."
+    fi
+    return 1
+  fi
+  return 0
+}

+ 415 - 3
dnsapi/README.md

@@ -140,7 +140,7 @@ Finally, make the DNS server and update Key available to `acme.sh`
 
 ```
 export NSUPDATE_SERVER="dns.example.com"
-export NSUPDATE_KEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=="
+export NSUPDATE_KEY="/path/to/your/nsupdate.key"
 ```
 
 Ok, let's issue a cert now:
@@ -240,7 +240,418 @@ acme.sh --issue --dns dns_ispconfig -d example.com -d www.example.com
 
 The `ISPC_User`, `ISPC_Password`, `ISPC_Api`and `ISPC_Api_Insecure` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
 
-# 13. Use custom API
+## 13. Use Alwaysdata domain API
+
+First you need to login to your Alwaysdata account to get your API Key.
+
+```sh
+export AD_API_KEY="myalwaysdataapikey"
+```
+
+Ok, let's issue a cert now:
+
+```sh
+acme.sh --issue --dns dns_ad -d example.com -d www.example.com
+```
+
+The `AD_API_KEY` will be saved in `~/.acme.sh/account.conf` and will be reused
+when needed.
+
+## 14. Use Linode domain 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.
+
+```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.
+
+Ok, let's issue a cert now:
+
+```sh
+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.
+
+## 15. Use FreeDNS
+
+FreeDNS (https://freedns.afraid.org/) 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 FreeDNS website to read the HTML and posting updates as HTTP.  The plugin needs to know your
+userid and password for the FreeDNS website.
+
+```sh
+export FREEDNS_User="..."
+export FREEDNS_Password="..."
+```
+
+You need only provide this the first time you run the acme.sh client with FreeDNS validation and then again
+whenever you change your password at the FreeDNS site.  The acme.sh FreeDNS plugin does not store your userid
+or password but rather saves an authentication token returned by FreeDNS in `~/.acme.sh/account.conf` and
+reuses that when needed.
+
+Now you can issue a certificate.
+
+```sh
+acme.sh --issue --dns dns_freedns -d example.com -d www.example.com
+```
+
+Note that you cannot use acme.sh automatic DNS validation for FreeDNS public domains or for a subdomain that
+you create under a FreeDNS public domain.  You must own the top level domain in order to automatically
+validate with acme.sh at FreeDNS.
+
+## 16. Use cyon.ch
+
+You only need to set your cyon.ch login credentials.
+If you also have 2 Factor Authentication (OTP) enabled, you need to set your secret token too and have `oathtool` installed.
+
+```
+export CY_Username="your_cyon_username"
+export CY_Password="your_cyon_password"
+export CY_OTP_Secret="your_otp_secret" # Only required if using 2FA
+```
+
+To issue a cert:
+```
+acme.sh --issue --dns dns_cyon -d example.com -d www.example.com
+```
+
+The `CY_Username`, `CY_Password` and `CY_OTP_Secret` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 17. Use Domain-Offensive/Resellerinterface/Domainrobot API
+
+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"
+export DO_PW="cdfkjl3n2"
+```
+
+Ok, let's issue a cert now:
+```
+acme.sh --issue --dns dns_do -d example.com -d www.example.com
+```
+
+## 18. Use Gandi LiveDNS API
+
+You must enable the new Gandi LiveDNS API first and the create your api key, See: http://doc.livedns.gandi.net/
+
+```
+export GANDI_LIVEDNS_KEY="fdmlfsdklmfdkmqsdfk"
+```
+
+Ok, let's issue a cert now:
+```
+acme.sh --issue --dns dns_gandi_livedns -d example.com -d www.example.com
+```
+
+## 19. Use Knot (knsupdate) DNS API to automatically issue cert
+
+First, generate a TSIG key for updating the zone.
+
+```
+keymgr tsig generate acme_key algorithm hmac-sha512 > /etc/knot/acme.key
+```
+
+Include this key in your knot configuration file.
+
+```
+include: /etc/knot/acme.key
+```
+
+Next, configure your zone to allow dynamic updates.
+
+Dynamic updates for the zone are allowed via proper ACL rule with the `update` action. For in-depth instructions, please see [Knot DNS's documentation](https://www.knot-dns.cz/documentation/).
+
+```
+acl:
+  - id: acme_acl
+    address: 192.168.1.0/24
+    key: acme_key
+    action: update
+
+zone:
+  - domain: example.com
+    file: example.com.zone
+    acl: acme_acl
+```
+
+Finally, make the DNS server and TSIG Key available to `acme.sh`
+
+```
+export KNOT_SERVER="dns.example.com"
+export KNOT_KEY=`grep \# /etc/knot/acme.key | cut -d' ' -f2`
+```
+
+Ok, let's issue a cert now:
+```
+acme.sh --issue --dns dns_knot -d example.com -d www.example.com
+```
+
+The `KNOT_SERVER` and `KNOT_KEY` settings will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 20. Use DigitalOcean API (native)
+
+You need to obtain a read and write capable API key from your DigitalOcean account. See: https://www.digitalocean.com/help/api/
+
+```
+export DO_API_KEY="75310dc4ca779ac39a19f6355db573b49ce92ae126553ebd61ac3a3ae34834cc"
+```
+
+Ok, let's issue a cert now:
+```
+acme.sh --issue --dns dns_dgon -d example.com -d www.example.com
+```
+
+## 21. Use ClouDNS.net API
+
+You need to set the HTTP API user ID and password credentials. See: https://www.cloudns.net/wiki/article/42/
+
+```
+export CLOUDNS_AUTH_ID=XXXXX
+export CLOUDNS_AUTH_PASSWORD="YYYYYYYYY"
+```
+
+Ok, let's issue a cert now:
+```
+acme.sh --issue --dns dns_cloudns -d example.com -d www.example.com
+```
+The `CLOUDNS_AUTH_ID` and `CLOUDNS_AUTH_PASSWORD` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 22. Use Infoblox API
+
+First you need to create/obtain API credentials on your Infoblox appliance.
+
+```
+export Infoblox_Creds="username:password"
+export Infoblox_Server="ip or fqdn of infoblox appliance"
+```
+
+Ok, let's issue a cert now:
+```
+acme.sh --issue --dns dns_infoblox -d example.com -d www.example.com
+```
+
+Note: This script will automatically create and delete the ephemeral txt record.
+The `Infoblox_Creds` and `Infoblox_Server` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+
+## 23. Use VSCALE API
+
+First you need to create/obtain API tokens on your [settings panel](https://vscale.io/panel/settings/tokens/).
+
+```
+VSCALE_API_KEY="sdfsdfsdfljlbjkljlkjsdfoiwje"
+```
+
+Ok, let's issue a cert now:
+```
+acme.sh --issue --dns dns_vscale -d example.com -d www.example.com
+```
+
+##  24. Use Dynu API
+
+First you need to create/obtain API credentials from your Dynu account. See: https://www.dynu.com/resources/api/documentation
+
+```
+export Dynu_ClientId="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
+export Dynu_Secret="yyyyyyyyyyyyyyyyyyyyyyyyy"
+```
+
+Ok, let's issue a cert now:
+```
+acme.sh --issue --dns dns_dynu -d example.com -d www.example.com
+```
+
+The `Dynu_ClientId` and `Dynu_Secret` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 25. Use DNSimple API
+
+First you need to login to your DNSimple account and generate a new oauth token.
+
+https://dnsimple.com/a/{your account id}/account/access_tokens
+
+Note that this is an _account_ token and not a user token. The account token is
+needed to infer the `account_id` used in requests. A user token will not be able
+to determine the correct account to use.
+
+```
+export DNSimple_OAUTH_TOKEN="sdfsdfsdfljlbjkljlkjsdfoiwje"
+```
+
+To issue the cert just specify the `dns_dnsimple` API.
+
+```
+acme.sh --issue --dns dns_dnsimple -d example.com
+```
+
+The `DNSimple_OAUTH_TOKEN` will be saved in `~/.acme.sh/account.conf` and will
+be reused when needed.
+
+If you have any issues with this integration please report them to
+https://github.com/pho3nixf1re/acme.sh/issues.
+
+## 26. Use NS1.com API
+
+```
+export NS1_Key="fdmlfsdklmfdkmqsdfk"
+```
+
+Ok, let's issue a cert now:
+```
+acme.sh --issue --dns dns_nsone -d example.com -d www.example.com
+```
+
+## 27. Use DuckDNS.org API
+
+```
+export DuckDNS_Token="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
+```
+
+Please note that since DuckDNS uses StartSSL as their cert provider, thus 
+--insecure may need to be used when issuing certs:
+```
+acme.sh --insecure --issue --dns dns_duckdns -d mydomain.duckdns.org
+```
+
+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.
+
+```
+export Namecom_Username="testuser"
+export Namecom_Token="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+```
+
+And now you can issue certs with:
+
+```
+acme.sh --issue --dns dns_namecom -d example.com -d www.example.com
+```
+
+For issues, please report to https://github.com/raidenii/acme.sh/issues.
+
+## 29. Use Dyn Managed DNS API to automatically issue cert
+
+First, login to your Dyn Managed DNS account: https://portal.dynect.net/login/
+
+It is recommended to add a new user specific for API access.
+
+The minimum "Zones & Records Permissions" required are:
+```
+RecordAdd
+RecordUpdate
+RecordDelete
+RecordGet
+ZoneGet
+ZoneAddNode
+ZoneRemoveNode
+ZonePublish
+```
+
+Pass the API user credentials to the environment:
+```
+export DYN_Customer="customer"
+export DYN_Username="apiuser"
+export DYN_Password="secret"
+```
+
+Ok, let's issue a cert now:
+```
+acme.sh --issue --dns dns_dyn -d example.com -d www.example.com
+```
+
+The `DYN_Customer`, `DYN_Username` and `DYN_Password` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 30. Use pdd.yandex.ru API
+
+```
+export PDD_Token="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+```
+
+Follow these instructions to get the token for your domain https://tech.yandex.com/domain/doc/concepts/access-docpage/
+```
+acme.sh --issue --dns dns_yandex -d mydomain.example.org
+```
+
+For issues, please report to https://github.com/non7top/acme.sh/issues.
+
+## 31. Use Hurricane Electric
+
+Hurricane Electric doesn't have an API so just set your login credentials like so:
+
+```
+export HE_Username="yourusername"
+export HE_Password="password"
+```
+
+Then you can issue your certificate:
+
+```
+acme.sh --issue --dns dns_he -d example.com -d www.example.com
+```
+
+The `HE_Username` and `HE_Password` settings will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+Please report any issues to https://github.com/angel333/acme.sh or to <me@ondrejsimek.com>.
+
+## 32. Use UnoEuro API to automatically issue cert
+
+First you need to login to your UnoEuro account to get your API key.
+
+```
+export UNO_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
+export UNO_User="UExxxxxx"
+```
+
+Ok, let's issue a cert now:
+```
+acme.sh --issue --dns dns_unoeuro -d example.com -d www.example.com
+```
+
+The `UNO_Key` and `UNO_User` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 33. Use INWX
+
+[INWX](https://www.inwx.de/) offers an [xmlrpc api](https://www.inwx.de/de/help/apidoc)  with your standard login credentials, set them like so:
+
+```
+export INWX_User="yourusername"
+export INWX_Password="password"
+```
+
+Then you can issue your certificates with:
+
+```
+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.
+
+## 34. User Servercow API v1
+
+Create a new user from the servercow control center. Don't forget to activate **DNS API** for this user.
+
+```
+export SERVERCOW_API_Username=username
+export SERVERCOW_API_Password=password
+```
+
+Now you cann issue a cert:
+
+```
+acme.sh --issue --dns dns_servercow -d example.com -d www.example.com
+```
+Both, `SERVERCOW_API_Username` and `SERVERCOW_API_Password` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+# Use custom API
 
 If your API is not supported yet, you can write your own DNS API.
 
@@ -256,7 +667,8 @@ acme.sh --issue --dns dns_myapi -d example.com -d www.example.com
 
 For more details, please check our sample script: [dns_myapi.sh](dns_myapi.sh)
 
+See:  https://github.com/Neilpang/acme.sh/wiki/DNS-API-Dev-Guide
 
-## 14. Use lexicon DNS API
+# Use lexicon DNS API
 
 https://github.com/Neilpang/acme.sh/wiki/How-to-use-lexicon-dns-api

+ 147 - 0
dnsapi/dns_ad.sh

@@ -0,0 +1,147 @@
+#!/usr/bin/env sh
+
+#
+#AD_API_KEY="sdfsdfsdfljlbjkljlkjsdfoiwje"
+
+#This is the Alwaysdata api wrapper for acme.sh
+#
+#Author: Paul Koppen
+#Report Bugs here: https://github.com/wpk-/acme.sh
+
+AD_API_URL="https://$AD_API_KEY:@api.alwaysdata.com/v1"
+
+########  Public functions #####################
+
+#Usage: dns_myapi_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_ad_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if [ -z "$AD_API_KEY" ]; then
+    AD_API_KEY=""
+    _err "You didn't specify the AD api key yet."
+    _err "Please create you key and try again."
+    return 1
+  fi
+
+  _saveaccountconf AD_API_KEY "$AD_API_KEY"
+
+  _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"
+
+  _ad_tmpl_json="{\"domain\":$_domain_id,\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":\"$txtvalue\"}"
+
+  if _ad_rest POST "record/" "$_ad_tmpl_json" && [ -z "$response" ]; then
+    _info "txt record updated success."
+    return 0
+  fi
+
+  return 1
+}
+
+#fulldomain txtvalue
+dns_ad_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  _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"
+  _ad_rest GET "record/?domain=$_domain_id&name=$_sub_domain"
+
+  if [ -n "$response" ]; then
+    record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\s*[0-9]+" | 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 _ad_rest DELETE "record/$record_id/" && [ -z "$response" ]; then
+      _info "txt record deleted success."
+      return 0
+    fi
+    _debug response "$response"
+    return 1
+  fi
+
+  return 1
+}
+
+####################  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 _ad_rest GET "domain/"; then
+    response="$(echo "$response" | tr -d "\n" | sed 's/{/\n&/g')"
+    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 "{.*\"name\":\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 uri qstr data
+_ad_rest() {
+  mtd="$1"
+  ep="$2"
+  data="$3"
+
+  _debug mtd "$mtd"
+  _debug ep "$ep"
+
+  export _H1="Accept: application/json"
+  export _H2="Content-Type: application/json"
+
+  if [ "$mtd" != "GET" ]; then
+    # both POST and DELETE.
+    _debug data "$data"
+    response="$(_post "$data" "$AD_API_URL/$ep" "" "$mtd")"
+  else
+    response="$(_get "$AD_API_URL/$ep")"
+  fi
+
+  if [ "$?" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}

+ 2 - 2
dnsapi/dns_ali.sh

@@ -35,7 +35,7 @@ dns_ali_rm() {
   _clean
 }
 
-####################  Private functions bellow ##################################
+####################  Private functions below ##################################
 
 _get_root() {
   domain=$1
@@ -67,7 +67,7 @@ _get_root() {
 }
 
 _ali_rest() {
-  signature=$(printf "%s" "GET&%2F&$(_ali_urlencode "$query")" | _hmac "sha1" "$(_hex "$Ali_Secret&")" | _base64)
+  signature=$(printf "%s" "GET&%2F&$(_ali_urlencode "$query")" | _hmac "sha1" "$(printf "%s" "$Ali_Secret&" | _hex_dump | tr -d " ")" | _base64)
   signature=$(_ali_urlencode "$signature")
   url="$Ali_API?$query&Signature=$signature"
 

+ 66 - 24
dnsapi/dns_aws.sh

@@ -27,8 +27,10 @@ dns_aws_add() {
     return 1
   fi
 
-  _saveaccountconf AWS_ACCESS_KEY_ID "$AWS_ACCESS_KEY_ID"
-  _saveaccountconf AWS_SECRET_ACCESS_KEY "$AWS_SECRET_ACCESS_KEY"
+  if [ -z "$AWS_SESSION_TOKEN" ]; then
+    _saveaccountconf AWS_ACCESS_KEY_ID "$AWS_ACCESS_KEY_ID"
+    _saveaccountconf AWS_SECRET_ACCESS_KEY "$AWS_SECRET_ACCESS_KEY"
+  fi
 
   _debug "First detect the root zone"
   if ! _get_root "$fulldomain"; then
@@ -42,20 +44,39 @@ 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><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 sucess."
+    _info "txt record updated success."
     return 0
   fi
 
   return 1
 }
 
-#fulldomain
+#fulldomain txtvalue
 dns_aws_rm() {
   fulldomain=$1
+  txtvalue=$2
+
+  _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"
+
+  _aws_tmpl_xml="<ChangeResourceRecordSetsRequest xmlns=\"https://route53.amazonaws.com/doc/2013-04-01/\"><ChangeBatch><Changes><Change><Action>DELETE</Action><ResourceRecordSet><ResourceRecords><ResourceRecord><Value>\"$txtvalue\"</Value></ResourceRecord></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."
+    return 0
+  fi
+
+  return 1
 
 }
 
-####################  Private functions bellow ##################################
+####################  Private functions below ##################################
 
 _get_root() {
   domain=$1
@@ -66,25 +87,39 @@ _get_root() {
     _debug "response" "$response"
     while true; do
       h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+      _debug2 "Checking domain: $h"
       if [ -z "$h" ]; then
+        if _contains "$response" "<IsTruncated>true</IsTruncated>" && _contains "$response" "<NextMarker>"; then
+          _debug "IsTruncated"
+          _nextMarker="$(echo "$response" | _egrep_o "<NextMarker>.*</NextMarker>" | cut -d '>' -f 2 | cut -d '<' -f 1)"
+          _debug "NextMarker" "$_nextMarker"
+          if aws_rest GET "2013-04-01/hostedzone" "marker=$_nextMarker"; then
+            _debug "Truncated request OK"
+            i=2
+            p=1
+            continue
+          else
+            _err "Truncated request error."
+          fi
+        fi
         #not valid
+        _err "Invalid domain"
         return 1
       fi
 
       if _contains "$response" "<Name>$h.</Name>"; then
-        hostedzone="$(echo "$response" | _egrep_o "<HostedZone>.*<Name>$h.</Name>.*</HostedZone>")"
+        hostedzone="$(echo "$response" | sed 's/<HostedZone>/#&/g' | tr '#' '\n' | _egrep_o "<HostedZone><Id>[^<]*<.Id><Name>$h.<.Name>.*<PrivateZone>false<.PrivateZone>.*<.HostedZone>")"
         _debug hostedzone "$hostedzone"
-        if [ -z "$hostedzone" ]; then
-          _err "Error, can not get hostedzone."
+        if [ "$hostedzone" ]; then
+          _domain_id=$(printf "%s\n" "$hostedzone" | _egrep_o "<Id>.*<.Id>" | head -n 1 | _egrep_o ">.*<" | tr -d "<>")
+          if [ "$_domain_id" ]; then
+            _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+            _domain=$h
+            return 0
+          fi
+          _err "Can not find domain id: $h"
           return 1
         fi
-        _domain_id=$(printf "%s\n" "$hostedzone" | _egrep_o "<Id>.*</Id>" | head -n 1 | _egrep_o ">.*<" | 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)
@@ -116,13 +151,17 @@ aws_rest() {
 
   #RequestDate="20161120T141056Z" ##############
 
-  _H1="x-amz-date: $RequestDate"
+  export _H1="x-amz-date: $RequestDate"
 
   aws_host="$AWS_HOST"
   CanonicalHeaders="host:$aws_host\nx-amz-date:$RequestDate\n"
-  _debug2 CanonicalHeaders "$CanonicalHeaders"
-
   SignedHeaders="host;x-amz-date"
+  if [ -n "$AWS_SESSION_TOKEN" ]; then
+    export _H3="x-amz-security-token: $AWS_SESSION_TOKEN"
+    CanonicalHeaders="${CanonicalHeaders}x-amz-security-token:$AWS_SESSION_TOKEN\n"
+    SignedHeaders="${SignedHeaders};x-amz-security-token"
+  fi
+  _debug2 CanonicalHeaders "$CanonicalHeaders"
   _debug2 SignedHeaders "$SignedHeaders"
 
   RequestPayload="$data"
@@ -156,10 +195,10 @@ aws_rest() {
 
   #kSecret="wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY" ############################
 
-  _debug2 kSecret "$kSecret"
+  _secure_debug2 kSecret "$kSecret"
 
-  kSecretH="$(_hex "$kSecret")"
-  _debug2 kSecretH "$kSecretH"
+  kSecretH="$(printf "%s" "$kSecret" | _hex_dump | tr -d " ")"
+  _secure_debug2 kSecretH "$kSecretH"
 
   kDateH="$(printf "$RequestDateOnly%s" | _hmac "$Hash" "$kSecretH" hex)"
   _debug2 kDateH "$kDateH"
@@ -170,7 +209,7 @@ aws_rest() {
   kServiceH="$(printf "$Service%s" | _hmac "$Hash" "$kRegionH" hex)"
   _debug2 kServiceH "$kServiceH"
 
-  kSigningH="$(printf "aws4_request%s" | _hmac "$Hash" "$kServiceH" hex)"
+  kSigningH="$(printf "%s" "aws4_request" | _hmac "$Hash" "$kServiceH" hex)"
   _debug2 kSigningH "$kSigningH"
 
   signature="$(printf "$StringToSign%s" | _hmac "$Hash" "$kSigningH" hex)"
@@ -179,10 +218,13 @@ aws_rest() {
   Authorization="$Algorithm Credential=$AWS_ACCESS_KEY_ID/$CredentialScope, SignedHeaders=$SignedHeaders, Signature=$signature"
   _debug2 Authorization "$Authorization"
 
-  _H3="Authorization: $Authorization"
-  _debug _H3 "$_H3"
+  _H2="Authorization: $Authorization"
+  _debug _H2 "$_H2"
 
   url="$AWS_URL/$ep"
+  if [ "$qsr" ]; then
+    url="$AWS_URL/$ep?$qsr"
+  fi
 
   if [ "$mtd" = "GET" ]; then
     response="$(_get "$url")"

+ 61 - 16
dnsapi/dns_cf.sh

@@ -14,6 +14,8 @@ dns_cf_add() {
   fulldomain=$1
   txtvalue=$2
 
+  CF_Key="${CF_Key:-$(_readaccountconf_mutable CF_Key)}"
+  CF_Email="${CF_Email:-$(_readaccountconf_mutable CF_Email)}"
   if [ -z "$CF_Key" ] || [ -z "$CF_Email" ]; then
     CF_Key=""
     CF_Email=""
@@ -29,8 +31,8 @@ dns_cf_add() {
   fi
 
   #save the api key and email to the account conf file.
-  _saveaccountconf CF_Key "$CF_Key"
-  _saveaccountconf CF_Email "$CF_Email"
+  _saveaccountconf_mutable CF_Key "$CF_Key"
+  _saveaccountconf_mutable CF_Email "$CF_Email"
 
   _debug "First detect the root zone"
   if ! _get_root "$fulldomain"; then
@@ -55,9 +57,7 @@ dns_cf_add() {
     _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
-        _info "Added, sleeping 10 seconds"
-        sleep 10
-        #todo: check if the record takes effect
+        _info "Added, OK"
         return 0
       else
         _err "Add txt record error."
@@ -72,9 +72,7 @@ dns_cf_add() {
 
     _cf_rest PUT "zones/$_domain_id/dns_records/$record_id" "{\"id\":\"$record_id\",\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"zone_id\":\"$_domain_id\",\"zone_name\":\"$_domain\"}"
     if [ "$?" = "0" ]; then
-      _info "Updated, sleeping 10 seconds"
-      sleep 10
-      #todo: check if the record takes effect
+      _info "Updated, OK"
       return 0
     fi
     _err "Update error"
@@ -83,13 +81,59 @@ dns_cf_add() {
 
 }
 
-#fulldomain
+#fulldomain txtvalue
 dns_cf_rm() {
   fulldomain=$1
+  txtvalue=$2
+
+  CF_Key="${CF_Key:-$(_readaccountconf_mutable CF_Key)}"
+  CF_Email="${CF_Email:-$(_readaccountconf_mutable CF_Email)}"
+  if [ -z "$CF_Key" ] || [ -z "$CF_Email" ]; then
+    CF_Key=""
+    CF_Email=""
+    _err "You don't specify cloudflare api key and email yet."
+    _err "Please create you 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 _domain_id "$_domain_id"
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  _debug "Getting txt records"
+  _cf_rest GET "zones/${_domain_id}/dns_records?type=TXT&name=$fulldomain&content=$txtvalue"
+
+  if ! printf "%s" "$response" | grep \"success\":true >/dev/null; then
+    _err "Error"
+    return 1
+  fi
+
+  count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2)
+  _debug count "$count"
+  if [ "$count" = "0" ]; then
+    _info "Don't need 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 ! _cf_rest DELETE "zones/$_domain_id/dns_records/$record_id"; then
+      _err "Delete record error."
+      return 1
+    fi
+    _contains "$response" '"success":true'
+  fi
 
 }
 
-####################  Private functions bellow ##################################
+####################  Private functions below ##################################
 #_acme-challenge.www.domain.com
 #returns
 # _sub_domain=_acme-challenge.www
@@ -101,6 +145,7 @@ _get_root() {
   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
@@ -110,8 +155,8 @@ _get_root() {
       return 1
     fi
 
-    if printf "%s" "$response" | grep "\"name\":\"$h\"" >/dev/null; then
-      _domain_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | head -n 1 | cut -d : -f 2 | tr -d \")
+    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
@@ -131,11 +176,11 @@ _cf_rest() {
   data="$3"
   _debug "$ep"
 
-  _H1="X-Auth-Email: $CF_Email"
-  _H2="X-Auth-Key: $CF_Key"
-  _H3="Content-Type: application/json"
+  export _H1="X-Auth-Email: $CF_Email"
+  export _H2="X-Auth-Key: $CF_Key"
+  export _H3="Content-Type: application/json"
 
-  if [ "$data" ]; then
+  if [ "$m" != "GET" ]; then
     _debug data "$data"
     response="$(_post "$data" "$CF_Api/$ep" "" "$m")"
   else

+ 184 - 0
dnsapi/dns_cloudns.sh

@@ -0,0 +1,184 @@
+#!/usr/bin/env sh
+
+# Author: Boyan Peychev <boyan at cloudns dot net>
+# Repository: https://github.com/ClouDNS/acme.sh/
+
+#CLOUDNS_AUTH_ID=XXXXX
+#CLOUDNS_AUTH_PASSWORD="YYYYYYYYY"
+CLOUDNS_API="https://api.cloudns.net"
+
+########  Public functions #####################
+
+#Usage: dns_cloudns_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_cloudns_add() {
+  _info "Using cloudns"
+
+  if ! _dns_cloudns_init_check; then
+    return 1
+  fi
+
+  zone="$(_dns_cloudns_get_zone_name "$1")"
+  if [ -z "$zone" ]; then
+    _err "Missing DNS zone at ClouDNS. Please log into your control panel and create the required DNS zone for the initial setup."
+    return 1
+  fi
+
+  host="$(echo "$1" | sed "s/\.$zone\$//")"
+  record=$2
+  record_id=$(_dns_cloudns_get_record_id "$zone" "$host")
+
+  _debug zone "$zone"
+  _debug host "$host"
+  _debug record "$record"
+  _debug record_id "$record_id"
+
+  if [ -z "$record_id" ]; then
+    _info "Adding the TXT record for $1"
+    _dns_cloudns_http_api_call "dns/add-record.json" "domain-name=$zone&record-type=TXT&host=$host&record=$record&ttl=60"
+    if ! _contains "$response" "\"status\":\"Success\""; then
+      _err "Record cannot be added."
+      return 1
+    fi
+    _info "Added."
+  else
+    _info "Updating the TXT record for $1"
+    _dns_cloudns_http_api_call "dns/mod-record.json" "domain-name=$zone&record-id=$record_id&record-type=TXT&host=$host&record=$record&ttl=60"
+    if ! _contains "$response" "\"status\":\"Success\""; then
+      _err "The TXT record for $1 cannot be updated."
+      return 1
+    fi
+    _info "Updated."
+  fi
+
+  return 0
+}
+
+#Usage: dns_cloudns_rm   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_cloudns_rm() {
+  _info "Using cloudns"
+
+  if ! _dns_cloudns_init_check; then
+    return 1
+  fi
+
+  if [ -z "$zone" ]; then
+    zone="$(_dns_cloudns_get_zone_name "$1")"
+    if [ -z "$zone" ]; then
+      _err "Missing DNS zone at ClouDNS. Please log into your control panel and create the required DNS zone for the initial setup."
+      return 1
+    fi
+  fi
+
+  host="$(echo "$1" | sed "s/\.$zone\$//")"
+  record=$2
+  record_id=$(_dns_cloudns_get_record_id "$zone" "$host")
+
+  _debug zone "$zone"
+  _debug host "$host"
+  _debug record "$record"
+  _debug record_id "$record_id"
+
+  if [ ! -z "$record_id" ]; then
+    _info "Deleting the TXT record for $1"
+    _dns_cloudns_http_api_call "dns/delete-record.json" "domain-name=$zone&record-id=$record_id"
+    if ! _contains "$response" "\"status\":\"Success\""; then
+      _err "The TXT record for $1 cannot be deleted."
+      return 1
+    fi
+    _info "Deleted."
+  fi
+  return 0
+}
+
+####################  Private functions below ##################################
+_dns_cloudns_init_check() {
+  if [ ! -z "$CLOUDNS_INIT_CHECK_COMPLETED" ]; then
+    return 0
+  fi
+
+  CLOUDNS_AUTH_ID="${CLOUDNS_AUTH_ID:-$(_readaccountconf_mutable CLOUDNS_AUTH_ID)}"
+  CLOUDNS_AUTH_PASSWORD="${CLOUDNS_AUTH_PASSWORD:-$(_readaccountconf_mutable CLOUDNS_AUTH_PASSWORD)}"
+  if [ -z "$CLOUDNS_AUTH_ID" ] || [ -z "$CLOUDNS_AUTH_PASSWORD" ]; then
+    CLOUDNS_AUTH_ID=""
+    CLOUDNS_AUTH_PASSWORD=""
+    _err "You don't specify cloudns api id and password yet."
+    _err "Please create you id and password and try again."
+    return 1
+  fi
+
+  if [ -z "$CLOUDNS_AUTH_ID" ]; then
+    _err "CLOUDNS_AUTH_ID is not configured"
+    return 1
+  fi
+
+  if [ -z "$CLOUDNS_AUTH_PASSWORD" ]; then
+    _err "CLOUDNS_AUTH_PASSWORD is not configured"
+    return 1
+  fi
+
+  _dns_cloudns_http_api_call "dns/login.json" ""
+
+  if ! _contains "$response" "\"status\":\"Success\""; then
+    _err "Invalid CLOUDNS_AUTH_ID or CLOUDNS_AUTH_PASSWORD. Please check your login credentials."
+    return 1
+  fi
+
+  #save the api id and password to the account conf file.
+  _saveaccountconf_mutable CLOUDNS_AUTH_ID "$CLOUDNS_AUTH_ID"
+  _saveaccountconf_mutable CLOUDNS_AUTH_PASSWORD "$CLOUDNS_AUTH_PASSWORD"
+
+  CLOUDNS_INIT_CHECK_COMPLETED=1
+
+  return 0
+}
+
+_dns_cloudns_get_zone_name() {
+  i=2
+  while true; do
+    zoneForCheck=$(printf "%s" "$1" | cut -d . -f $i-100)
+
+    if [ -z "$zoneForCheck" ]; then
+      return 1
+    fi
+
+    _debug zoneForCheck "$zoneForCheck"
+
+    _dns_cloudns_http_api_call "dns/get-zone-info.json" "domain-name=$zoneForCheck"
+
+    if ! _contains "$response" "\"status\":\"Failed\""; then
+      echo "$zoneForCheck"
+      return 0
+    fi
+
+    i=$(_math "$i" + 1)
+  done
+  return 1
+}
+
+_dns_cloudns_get_record_id() {
+  _dns_cloudns_http_api_call "dns/records.json" "domain-name=$1&host=$2&type=TXT"
+  if _contains "$response" "\"id\":"; then
+    echo "$response" | cut -d '"' -f 2
+    return 0
+  fi
+  return 1
+}
+
+_dns_cloudns_http_api_call() {
+  method=$1
+
+  _debug CLOUDNS_AUTH_ID "$CLOUDNS_AUTH_ID"
+  _debug CLOUDNS_AUTH_PASSWORD "$CLOUDNS_AUTH_PASSWORD"
+
+  if [ -z "$2" ]; then
+    data="auth-id=$CLOUDNS_AUTH_ID&auth-password=$CLOUDNS_AUTH_PASSWORD"
+  else
+    data="auth-id=$CLOUDNS_AUTH_ID&auth-password=$CLOUDNS_AUTH_PASSWORD&$2"
+  fi
+
+  response="$(_get "$CLOUDNS_API/$method?$data")"
+
+  _debug2 response "$response"
+
+  return 0
+}

+ 24 - 17
dnsapi/dns_cx.sh

@@ -58,7 +58,15 @@ dns_cx_add() {
 #fulldomain
 dns_cx_rm() {
   fulldomain=$1
-
+  REST_API="$CX_Api"
+  if _get_root "$fulldomain"; then
+    record_id=""
+    existing_records "$_domain" "$_sub_domain"
+    if ! [ "$record_id" = "" ]; then
+      _rest DELETE "record/$record_id/$_domain_id" "{}"
+      _info "Deleted record ${fulldomain}"
+    fi
+  fi
 }
 
 #usage:  root  sub
@@ -69,12 +77,12 @@ existing_records() {
   _debug "Getting txt records"
   root=$1
   sub=$2
-
+  count=0
   if ! _rest GET "record/$_domain_id?:domain_id?host_id=0&offset=0&row_num=100"; then
     return 1
   fi
-  count=0
-  seg=$(printf "%s\n" "$response" | _egrep_o "{[^\{]*host\":\"$_sub_domain\"[^\}]*\}")
+
+  seg=$(printf "%s\n" "$response" | _egrep_o '"record_id":[^{]*host":"'"$_sub_domain"'"[^}]*\}')
   _debug seg "$seg"
   if [ -z "$seg" ]; then
     return 0
@@ -82,7 +90,7 @@ existing_records() {
 
   if printf "%s" "$response" | grep '"type":"TXT"' >/dev/null; then
     count=1
-    record_id=$(printf "%s\n" "$seg" | _egrep_o "\"record_id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")
+    record_id=$(printf "%s\n" "$seg" | _egrep_o '"record_id":"[^"]*"' | cut -d : -f 2 | tr -d \" | _head_n 1)
     _debug record_id "$record_id"
     return 0
   fi
@@ -123,7 +131,7 @@ update_record() {
   return 1
 }
 
-####################  Private functions bellow ##################################
+####################  Private functions below ##################################
 #_acme-challenge.www.domain.com
 #returns
 # _sub_domain=_acme-challenge.www
@@ -147,9 +155,9 @@ _get_root() {
     fi
 
     if _contains "$response" "$h."; then
-      seg=$(printf "%s" "$response" | _egrep_o "\{[^\{]*\"$h\.\"[^\}]*\}")
+      seg=$(printf "%s\n" "$response" | _egrep_o '"id":[^{]*"'"$h"'."[^}]*}')
       _debug seg "$seg"
-      _domain_id=$(printf "%s" "$seg" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")
+      _domain_id=$(printf "%s\n" "$seg" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")
       _debug _domain_id "$_domain_id"
       if [ "$_domain_id" ]; then
         _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
@@ -170,7 +178,7 @@ _get_root() {
 _rest() {
   m=$1
   ep="$2"
-  _debug "$ep"
+  _debug ep "$ep"
   url="$REST_API/$ep"
   _debug url "$url"
 
@@ -185,10 +193,10 @@ _rest() {
   hmac=$(printf "%s" "$sec" | _digest md5 hex)
   _debug hmac "$hmac"
 
-  _H1="API-KEY: $CX_Key"
-  _H2="API-REQUEST-DATE: $cdate"
-  _H3="API-HMAC: $hmac"
-  _H4="Content-Type: application/json"
+  export _H1="API-KEY: $CX_Key"
+  export _H2="API-REQUEST-DATE: $cdate"
+  export _H3="API-HMAC: $hmac"
+  export _H4="Content-Type: application/json"
 
   if [ "$data" ]; then
     response="$(_post "$data" "$url" "" "$m")"
@@ -201,8 +209,7 @@ _rest() {
     return 1
   fi
   _debug2 response "$response"
-  if ! _contains "$response" '"message":"success"'; then
-    return 1
-  fi
-  return 0
+
+  _contains "$response" '"code":1'
+
 }

+ 328 - 0
dnsapi/dns_cyon.sh

@@ -0,0 +1,328 @@
+#!/usr/bin/env sh
+
+########
+# Custom cyon.ch DNS API for use with [acme.sh](https://github.com/Neilpang/acme.sh)
+#
+# Usage: acme.sh --issue --dns dns_cyon -d www.domain.com
+#
+# Dependencies:
+# -------------
+# - oathtool (When using 2 Factor Authentication)
+#
+# Issues:
+# -------
+# Any issues / questions / suggestions can be posted here:
+# https://github.com/noplanman/cyon-api/issues
+#
+# Author: Armando Lüscher <armando@noplanman.ch>
+########
+
+dns_cyon_add() {
+  _cyon_load_credentials \
+    && _cyon_load_parameters "$@" \
+    && _cyon_print_header "add" \
+    && _cyon_login \
+    && _cyon_change_domain_env \
+    && _cyon_add_txt \
+    && _cyon_logout
+}
+
+dns_cyon_rm() {
+  _cyon_load_credentials \
+    && _cyon_load_parameters "$@" \
+    && _cyon_print_header "delete" \
+    && _cyon_login \
+    && _cyon_change_domain_env \
+    && _cyon_delete_txt \
+    && _cyon_logout
+}
+
+#########################
+### PRIVATE FUNCTIONS ###
+#########################
+
+_cyon_load_credentials() {
+  # Convert loaded password to/from base64 as needed.
+  if [ "${CY_Password_B64}" ]; then
+    CY_Password="$(printf "%s" "${CY_Password_B64}" | _dbase64 "multiline")"
+  elif [ "${CY_Password}" ]; then
+    CY_Password_B64="$(printf "%s" "${CY_Password}" | _base64)"
+  fi
+
+  if [ -z "${CY_Username}" ] || [ -z "${CY_Password}" ]; then
+    # Dummy entries to satisfy script checker.
+    CY_Username=""
+    CY_Password=""
+    CY_OTP_Secret=""
+
+    _err ""
+    _err "You haven't set your cyon.ch login credentials yet."
+    _err "Please set the required cyon environment variables."
+    _err ""
+    return 1
+  fi
+
+  # Save the login credentials to the account.conf file.
+  _debug "Save credentials to account.conf"
+  _saveaccountconf CY_Username "${CY_Username}"
+  _saveaccountconf CY_Password_B64 "$CY_Password_B64"
+  if [ ! -z "${CY_OTP_Secret}" ]; then
+    _saveaccountconf CY_OTP_Secret "$CY_OTP_Secret"
+  else
+    _clearaccountconf CY_OTP_Secret
+  fi
+}
+
+_cyon_is_idn() {
+  _idn_temp="$(printf "%s" "${1}" | tr -d "0-9a-zA-Z.,-_")"
+  _idn_temp2="$(printf "%s" "${1}" | grep -o "xn--")"
+  [ "$_idn_temp" ] || [ "$_idn_temp2" ]
+}
+
+_cyon_load_parameters() {
+  # Read the required parameters to add the TXT entry.
+  # shellcheck disable=SC2018,SC2019
+  fulldomain="$(printf "%s" "${1}" | tr "A-Z" "a-z")"
+  fulldomain_idn="${fulldomain}"
+
+  # Special case for IDNs, as cyon needs a domain environment change,
+  # which uses the "pretty" instead of the punycode version.
+  if _cyon_is_idn "${fulldomain}"; then
+    if ! _exists idn; then
+      _err "Please install idn to process IDN names."
+      _err ""
+      return 1
+    fi
+
+    fulldomain="$(idn -u "${fulldomain}")"
+    fulldomain_idn="$(idn -a "${fulldomain}")"
+  fi
+
+  _debug fulldomain "${fulldomain}"
+  _debug fulldomain_idn "${fulldomain_idn}"
+
+  txtvalue="${2}"
+  _debug txtvalue "${txtvalue}"
+
+  # This header is required for curl calls.
+  _H1="X-Requested-With: XMLHttpRequest"
+  export _H1
+}
+
+_cyon_print_header() {
+  if [ "${1}" = "add" ]; then
+    _info ""
+    _info "+---------------------------------------------+"
+    _info "| Adding DNS TXT entry to your cyon.ch domain |"
+    _info "+---------------------------------------------+"
+    _info ""
+    _info "  * Full Domain: ${fulldomain}"
+    _info "  * TXT Value:   ${txtvalue}"
+    _info ""
+  elif [ "${1}" = "delete" ]; then
+    _info ""
+    _info "+-------------------------------------------------+"
+    _info "| Deleting DNS TXT entry from your cyon.ch domain |"
+    _info "+-------------------------------------------------+"
+    _info ""
+    _info "  * Full Domain: ${fulldomain}"
+    _info ""
+  fi
+}
+
+_cyon_get_cookie_header() {
+  printf "Cookie: %s" "$(grep "cyon=" "$HTTP_HEADER" | grep "^Set-Cookie:" | _tail_n 1 | _egrep_o 'cyon=[^;]*;' | tr -d ';')"
+}
+
+_cyon_login() {
+  _info "  - Logging in..."
+
+  username_encoded="$(printf "%s" "${CY_Username}" | _url_encode)"
+  password_encoded="$(printf "%s" "${CY_Password}" | _url_encode)"
+
+  login_url="https://my.cyon.ch/auth/index/dologin-async"
+  login_data="$(printf "%s" "username=${username_encoded}&password=${password_encoded}&pathname=%2F")"
+
+  login_response="$(_post "$login_data" "$login_url")"
+  _debug login_response "${login_response}"
+
+  # Bail if login fails.
+  if [ "$(printf "%s" "${login_response}" | _cyon_get_response_success)" != "success" ]; then
+    _err "    $(printf "%s" "${login_response}" | _cyon_get_response_message)"
+    _err ""
+    return 1
+  fi
+
+  _info "    success"
+
+  # NECESSARY!! Load the main page after login, to get the new cookie.
+  _H2="$(_cyon_get_cookie_header)"
+  export _H2
+
+  _get "https://my.cyon.ch/" >/dev/null
+
+  # todo: instead of just checking if the env variable is defined, check if we actually need to do a 2FA auth request.
+
+  # 2FA authentication with OTP?
+  if [ ! -z "${CY_OTP_Secret}" ]; then
+    _info "  - Authorising with OTP code..."
+
+    if ! _exists oathtool; then
+      _err "Please install oathtool to use 2 Factor Authentication."
+      _err ""
+      return 1
+    fi
+
+    # Get OTP code with the defined secret.
+    otp_code="$(oathtool --base32 --totp "${CY_OTP_Secret}" 2>/dev/null)"
+
+    login_otp_url="https://my.cyon.ch/auth/multi-factor/domultifactorauth-async"
+    login_otp_data="totpcode=${otp_code}&pathname=%2F&rememberme=0"
+
+    login_otp_response="$(_post "$login_otp_data" "$login_otp_url")"
+    _debug login_otp_response "${login_otp_response}"
+
+    # Bail if OTP authentication fails.
+    if [ "$(printf "%s" "${login_otp_response}" | _cyon_get_response_success)" != "success" ]; then
+      _err "    $(printf "%s" "${login_otp_response}" | _cyon_get_response_message)"
+      _err ""
+      return 1
+    fi
+
+    _info "    success"
+  fi
+
+  _info ""
+}
+
+_cyon_logout() {
+  _info "  - Logging out..."
+
+  _get "https://my.cyon.ch/auth/index/dologout" >/dev/null
+
+  _info "    success"
+  _info ""
+}
+
+_cyon_change_domain_env() {
+  _info "  - Changing domain environment..."
+
+  # Get the "example.com" part of the full domain name.
+  domain_env="$(printf "%s" "${fulldomain}" | sed -E -e 's/.*\.(.*\..*)$/\1/')"
+  _debug "Changing domain environment to ${domain_env}"
+
+  gloo_item_key="$(_get "https://my.cyon.ch/domain/" | tr '\n' ' ' | sed -E -e "s/.*data-domain=\"${domain_env}\"[^<]*data-itemkey=\"([^\"]*).*/\1/")"
+  _debug gloo_item_key "${gloo_item_key}"
+
+  domain_env_url="https://my.cyon.ch/user/environment/setdomain/d/${domain_env}/gik/${gloo_item_key}"
+
+  domain_env_response="$(_get "${domain_env_url}")"
+  _debug domain_env_response "${domain_env_response}"
+
+  if ! _cyon_check_if_2fa_missed "${domain_env_response}"; then return 1; fi
+
+  domain_env_success="$(printf "%s" "${domain_env_response}" | _egrep_o '"authenticated":\w*' | cut -d : -f 2)"
+
+  # Bail if domain environment change fails.
+  if [ "${domain_env_success}" != "true" ]; then
+    _err "    $(printf "%s" "${domain_env_response}" | _cyon_get_response_message)"
+    _err ""
+    return 1
+  fi
+
+  _info "    success"
+  _info ""
+}
+
+_cyon_add_txt() {
+  _info "  - Adding DNS TXT entry..."
+
+  add_txt_url="https://my.cyon.ch/domain/dnseditor/add-record-async"
+  add_txt_data="zone=${fulldomain_idn}.&ttl=900&type=TXT&value=${txtvalue}"
+
+  add_txt_response="$(_post "$add_txt_data" "$add_txt_url")"
+  _debug add_txt_response "${add_txt_response}"
+
+  if ! _cyon_check_if_2fa_missed "${add_txt_response}"; then return 1; fi
+
+  add_txt_message="$(printf "%s" "${add_txt_response}" | _cyon_get_response_message)"
+  add_txt_status="$(printf "%s" "${add_txt_response}" | _cyon_get_response_status)"
+
+  # Bail if adding TXT entry fails.
+  if [ "${add_txt_status}" != "true" ]; then
+    _err "    ${add_txt_message}"
+    _err ""
+    return 1
+  fi
+
+  _info "    success (TXT|${fulldomain_idn}.|${txtvalue})"
+  _info ""
+}
+
+_cyon_delete_txt() {
+  _info "  - Deleting DNS TXT entry..."
+
+  list_txt_url="https://my.cyon.ch/domain/dnseditor/list-async"
+
+  list_txt_response="$(_get "${list_txt_url}" | sed -e 's/data-hash/\\ndata-hash/g')"
+  _debug list_txt_response "${list_txt_response}"
+
+  if ! _cyon_check_if_2fa_missed "${list_txt_response}"; then return 1; fi
+
+  # Find and delete all acme challenge entries for the $fulldomain.
+  _dns_entries="$(printf "%b\n" "${list_txt_response}" | sed -n 's/data-hash=\\"\([^"]*\)\\" data-identifier=\\"\([^"]*\)\\".*/\1 \2/p')"
+
+  printf "%s" "${_dns_entries}" | while read -r _hash _identifier; do
+    dns_type="$(printf "%s" "$_identifier" | cut -d'|' -f1)"
+    dns_domain="$(printf "%s" "$_identifier" | cut -d'|' -f2)"
+
+    if [ "${dns_type}" != "TXT" ] || [ "${dns_domain}" != "${fulldomain_idn}." ]; then
+      continue
+    fi
+
+    hash_encoded="$(printf "%s" "${_hash}" | _url_encode)"
+    identifier_encoded="$(printf "%s" "${_identifier}" | _url_encode)"
+
+    delete_txt_url="https://my.cyon.ch/domain/dnseditor/delete-record-async"
+    delete_txt_data="$(printf "%s" "hash=${hash_encoded}&identifier=${identifier_encoded}")"
+
+    delete_txt_response="$(_post "$delete_txt_data" "$delete_txt_url")"
+    _debug delete_txt_response "${delete_txt_response}"
+
+    if ! _cyon_check_if_2fa_missed "${delete_txt_response}"; then return 1; fi
+
+    delete_txt_message="$(printf "%s" "${delete_txt_response}" | _cyon_get_response_message)"
+    delete_txt_status="$(printf "%s" "${delete_txt_response}" | _cyon_get_response_status)"
+
+    # Skip if deleting TXT entry fails.
+    if [ "${delete_txt_status}" != "true" ]; then
+      _err "    ${delete_txt_message} (${_identifier})"
+    else
+      _info "    success (${_identifier})"
+    fi
+  done
+
+  _info "    done"
+  _info ""
+}
+
+_cyon_get_response_message() {
+  _egrep_o '"message":"[^"]*"' | cut -d : -f 2 | tr -d '"'
+}
+
+_cyon_get_response_status() {
+  _egrep_o '"status":\w*' | cut -d : -f 2
+}
+
+_cyon_get_response_success() {
+  _egrep_o '"onSuccess":"[^"]*"' | cut -d : -f 2 | tr -d '"'
+}
+
+_cyon_check_if_2fa_missed() {
+  # Did we miss the 2FA?
+  if test "${1#*multi_factor_form}" != "${1}"; then
+    _err "    Missed OTP authentication!"
+    _err ""
+    return 1
+  fi
+}

+ 205 - 0
dnsapi/dns_dgon.sh

@@ -0,0 +1,205 @@
+#!/usr/bin/env sh
+
+## Will be called by acme.sh to add the txt record to your api system.
+## returns 0 means success, otherwise error.
+
+## Author: thewer <github at thewer.com>
+## GitHub: https://github.com/gitwer/acme.sh
+
+##
+## Environment Variables Required:
+##
+## DO_API_KEY="75310dc4ca779ac39a19f6355db573b49ce92ae126553ebd61ac3a3ae34834cc"
+##
+
+#####################  Public functions  #####################
+
+## Create the text record for validation.
+## Usage: fulldomain txtvalue
+## EG: "_acme-challenge.www.other.domain.com" "XKrxpRBosdq0HG9i01zxXp5CPBs"
+dns_dgon_add() {
+  fulldomain="$(echo "$1" | _lower_case)"
+  txtvalue=$2
+  _info "Using digitalocean dns validation - add record"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  ## save the env vars (key and domain split location) for later automated use
+  _saveaccountconf DO_API_KEY "$DO_API_KEY"
+
+  ## split the domain for DO API
+  if ! _get_base_domain "$fulldomain"; then
+    _err "domain not found in your account for addition"
+    return 1
+  fi
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  ## Set the header with our post type and key auth key
+  export _H1="Content-Type: application/json"
+  export _H2="Authorization: Bearer $DO_API_KEY"
+  PURL='https://api.digitalocean.com/v2/domains/'$_domain'/records'
+  PBODY='{"type":"TXT","name":"'$_sub_domain'","data":"'$txtvalue'"}'
+
+  _debug PURL "$PURL"
+  _debug PBODY "$PBODY"
+
+  ## the create request - post
+  ## args: BODY, URL, [need64, httpmethod]
+  response="$(_post "$PBODY" "$PURL")"
+
+  ## check response
+  if [ "$?" != "0" ]; then
+    _err "error in response: $response"
+    return 1
+  fi
+  _debug2 response "$response"
+
+  ## finished correctly
+  return 0
+}
+
+## Remove the txt record after validation.
+## Usage: fulldomain txtvalue
+## EG: "_acme-challenge.www.other.domain.com" "XKrxpRBosdq0HG9i01zxXp5CPBs"
+dns_dgon_rm() {
+  fulldomain="$(echo "$1" | _lower_case)"
+  txtvalue=$2
+  _info "Using digitalocean dns validation - remove record"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  ## split the domain for DO API
+  if ! _get_base_domain "$fulldomain"; then
+    _err "domain not found in your account for removal"
+    return 1
+  fi
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  ## Set the header with our post type and key auth key
+  export _H1="Content-Type: application/json"
+  export _H2="Authorization: Bearer $DO_API_KEY"
+  ## get URL for the list of domains
+  ## 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
+    ## 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*\"*\d+\"*[^}]*\"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\=\d+")"
+      if [ -z "$nextpage" ]; then
+        _err "no record and no nextpage in digital ocean DNS removal"
+        return 1
+      fi
+      _debug2 nextpage "$nextpage"
+      GURL="$nextpage"
+    fi
+    ## we break out of the loop when we have a record
+  done
+
+  ## we found the record
+  rec_id="$(echo "$record" | _egrep_o "id\"\s*\:\s*\"*\d+" | _egrep_o "\d+")"
+  _debug rec_id "$rec_id"
+
+  ## 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"
+
+  ## finished correctly
+  return 0
+}
+
+#####################  Private functions below  #####################
+
+## Split the domain provided into the "bade domain" and the "start prefix".
+## This function searches for the longest subdomain in your account
+## for the full domain given and splits it into the base domain (zone)
+## and the prefix/record to be added/removed
+## USAGE: fulldomain
+## EG: "_acme-challenge.two.three.four.domain.com"
+## returns
+## _sub_domain="_acme-challenge.two"
+## _domain="three.four.domain.com" *IF* zone "three.four.domain.com" exists
+## if only "domain.com" exists it will return
+## _sub_domain="_acme-challenge.two.three.four"
+## _domain="domain.com"
+_get_base_domain() {
+  # args
+  fulldomain="$(echo "$1" | tr '[:upper:]' '[:lower:]')"
+  _debug fulldomain "$fulldomain"
+
+  # domain max legal length = 253
+  MAX_DOM=255
+
+  ## get a list of domains for the account to check thru
+  ## Set the headers
+  export _H1="Content-Type: application/json"
+  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)
+  DOMURL="https://api.digitalocean.com/v2/domains"
+
+  ## get the domain list (DO gives basically a full XFER!)
+  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"
+      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
+    fi
+    ## increment cut point $i
+    i=$(_math $i + 1)
+  done
+
+  ## we went through the entire domain zone list and dint find one that matched
+  ## doesnt look like we can add in the record
+  _err "domain not found in DigitalOcean account, but we should never get here"
+  return 1
+}

+ 215 - 0
dnsapi/dns_dnsimple.sh

@@ -0,0 +1,215 @@
+#!/usr/bin/env sh
+
+# DNSimple domain api
+# https://github.com/pho3nixf1re/acme.sh/issues
+#
+# This is your oauth token which can be acquired on the account page. Please
+# note that this must be an _account_ token and not a _user_ token.
+# https://dnsimple.com/a/<your account id>/account/access_tokens
+# DNSimple_OAUTH_TOKEN="sdfsdfsdfljlbjkljlkjsdfoiwje"
+
+DNSimple_API="https://api.dnsimple.com/v2"
+
+########  Public functions #####################
+
+# Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_dnsimple_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if [ -z "$DNSimple_OAUTH_TOKEN" ]; then
+    DNSimple_OAUTH_TOKEN=""
+    _err "You have not set the dnsimple oauth token yet."
+    _err "Please visit https://dnsimple.com/user to generate it."
+    return 1
+  fi
+
+  # save the oauth token for later
+  _saveaccountconf DNSimple_OAUTH_TOKEN "$DNSimple_OAUTH_TOKEN"
+
+  if ! _get_account_id; then
+    _err "failed to retrive account id"
+    return 1
+  fi
+
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+
+  _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!"
+      return 0
+    fi
+
+    _err "Update error"
+    return 1
+  fi
+}
+
+# fulldomain
+dns_dnsimple_rm() {
+  fulldomain=$1
+
+  if ! _get_account_id; then
+    _err "failed to retrive account id"
+    return 1
+  fi
+
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+
+  _get_records "$_account_id" "$_domain" "$_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
+  fi
+
+  _err "failed to remove record" "$_record_id"
+  return 1
+
+}
+
+####################  Private functions bellow ##################################
+# _acme-challenge.www.domain.com
+# returns
+#   _sub_domain=_acme-challenge.www
+#   _domain=domain.com
+_get_root() {
+  domain=$1
+  i=2
+  previous=1
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    if [ -z "$h" ]; then
+      # not valid
+      return 1
+    fi
+
+    if ! _dnsimple_rest GET "$_account_id/zones/$h"; then
+      return 1
+    fi
+
+    if _contains "$response" 'not found'; then
+      _debug "$h not found"
+    else
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$previous)
+      _domain="$h"
+
+      _debug _domain "$_domain"
+      _debug _sub_domain "$_sub_domain"
+
+      return 0
+    fi
+
+    previous="$i"
+    i=$(_math "$i" + 1)
+  done
+  return 1
+}
+
+# returns _account_id
+_get_account_id() {
+  _debug "retrive account id"
+  if ! _dnsimple_rest GET "whoami"; then
+    return 1
+  fi
+
+  if _contains "$response" "\"account\":null"; then
+    _err "no account associated with this token"
+    return 1
+  fi
+
+  if _contains "$response" "timeout"; then
+    _err "timeout retrieving account id"
+    return 1
+  fi
+
+  _account_id=$(printf "%s" "$response" | _egrep_o "\"id\":[^,]*,\"email\":" | cut -d: -f2 | cut -d, -f1)
+  _debug _account_id "$_account_id"
+
+  return 0
+}
+
+# returns
+#   _records
+#   _records_count
+_get_records() {
+  account_id=$1
+  domain=$2
+  sub_domain=$3
+
+  _debug "fetching txt records"
+  _dnsimple_rest GET "$account_id/zones/$domain/records?per_page=100"
+
+  if ! _contains "$response" "\"id\":"; then
+    _err "failed to retrieve records"
+    return 1
+  fi
+
+  _records_count=$(printf "%s" "$response" | _egrep_o "\"name\":\"$sub_domain\"" | wc -l | _egrep_o "[0-9]+")
+  _records=$response
+  _debug _records_count "$_records_count"
+}
+
+# returns _record_id
+_extract_record_id() {
+  _record_id=$(printf "%s" "$_records" | _egrep_o "\"id\":[^,]*,\"zone_id\":\"[^,]*\",\"parent_id\":null,\"name\":\"$_sub_domain\"" | cut -d: -f2 | cut -d, -f1)
+  _debug "_record_id" "$_record_id"
+}
+
+# returns response
+_dnsimple_rest() {
+  method=$1
+  path="$2"
+  data="$3"
+  request_url="$DNSimple_API/$path"
+  _debug "$path"
+
+  export _H1="Accept: application/json"
+  export _H2="Authorization: Bearer $DNSimple_OAUTH_TOKEN"
+
+  if [ "$data" ] || [ "$method" = "DELETE" ]; then
+    _H1="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
+}

+ 148 - 0
dnsapi/dns_do.sh

@@ -0,0 +1,148 @@
+#!/usr/bin/env sh
+
+# DNS API for Domain-Offensive / Resellerinterface / Domainrobot
+
+# Report bugs at https://github.com/seidler2547/acme.sh/issues
+
+# set these environment variables to match your customer ID and password:
+# DO_PID="KD-1234567"
+# DO_PW="cdfkjl3n2"
+
+DO_URL="https://soap.resellerinterface.de/"
+
+########  Public functions #####################
+
+#Usage: dns_myapi_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_do_add() {
+  fulldomain=$1
+  txtvalue=$2
+  if _dns_do_authenticate; then
+    _info "Adding TXT record to ${_domain} as ${fulldomain}"
+    _dns_do_soap createRR origin "${_domain}" name "${fulldomain}" type TXT data "${txtvalue}" ttl 300
+    if _contains "${response}" '>success<'; then
+      return 0
+    fi
+    _err "Could not create resource record, check logs"
+  fi
+  return 1
+}
+
+#fulldomain
+dns_do_rm() {
+  fulldomain=$1
+  if _dns_do_authenticate; then
+    if _dns_do_list_rrs; then
+      _dns_do_had_error=0
+      for _rrid in ${_rr_list}; do
+        _info "Deleting resource record $_rrid for $_domain"
+        _dns_do_soap deleteRR origin "${_domain}" rrid "${_rrid}"
+        if ! _contains "${response}" '>success<'; then
+          _dns_do_had_error=1
+          _err "Could not delete resource record for ${_domain}, id ${_rrid}"
+        fi
+      done
+      return $_dns_do_had_error
+    fi
+  fi
+  return 1
+}
+
+####################  Private functions below ##################################
+_dns_do_authenticate() {
+  _info "Authenticating as ${DO_PID}"
+  _dns_do_soap authPartner partner "${DO_PID}" password "${DO_PW}"
+  if _contains "${response}" '>success<'; then
+    _get_root "$fulldomain"
+    _debug "_domain $_domain"
+    return 0
+  else
+    _err "Authentication failed, are DO_PID and DO_PW set correctly?"
+  fi
+  return 1
+}
+
+_dns_do_list_rrs() {
+  _dns_do_soap getRRList origin "${_domain}"
+  if ! _contains "${response}" 'SOAP-ENC:Array'; then
+    _err "getRRList origin ${_domain} failed"
+    return 1
+  fi
+  _rr_list="$(echo "${response}" \
+    | tr -d "\n\r\t" \
+    | sed -e 's/<item xsi:type="ns2:Map">/\n/g' \
+    | grep ">$(_regexcape "$fulldomain")</value>" \
+    | sed -e 's/<\/item>/\n/g' \
+    | grep '>id</key><value' \
+    | _egrep_o '>[0-9]{1,16}<' \
+    | tr -d '><')"
+  [ "${_rr_list}" ]
+}
+
+_dns_do_soap() {
+  func="$1"
+  shift
+  # put the parameters to xml
+  body="<tns:${func} xmlns:tns=\"${DO_URL}\">"
+  while [ "$1" ]; do
+    _k="$1"
+    shift
+    _v="$1"
+    shift
+    body="$body<$_k>$_v</$_k>"
+  done
+  body="$body</tns:${func}>"
+  _debug2 "SOAP request ${body}"
+
+  # build SOAP XML
+  _xml='<?xml version="1.0" encoding="UTF-8"?>
+<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
+  <env:Body>'"$body"'</env:Body>
+</env:Envelope>'
+
+  # set SOAP headers
+  export _H1="SOAPAction: ${DO_URL}#${func}"
+
+  if ! response="$(_post "${_xml}" "${DO_URL}")"; then
+    _err "Error <$1>"
+    return 1
+  fi
+  _debug2 "SOAP response $response"
+
+  # retrieve cookie header
+  _H2="$(_egrep_o 'Cookie: [^;]+' <"$HTTP_HEADER" | _head_n 1)"
+  export _H2
+
+  return 0
+}
+
+_get_root() {
+  domain=$1
+  i=1
+
+  _dns_do_soap getDomainList
+  _all_domains="$(echo "${response}" \
+    | tr -d "\n\r\t " \
+    | _egrep_o 'domain</key><value[^>]+>[^<]+' \
+    | sed -e 's/^domain<\/key><value[^>]*>//g')"
+
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    if [ -z "$h" ]; then
+      return 1
+    fi
+
+    if _contains "${_all_domains}" "^$(_regexcape "$h")\$"; then
+      _domain="$h"
+      return 0
+    fi
+
+    i=$(_math $i + 1)
+  done
+  _debug "$domain not found"
+
+  return 1
+}
+
+_regexcape() {
+  echo "$1" | sed -e 's/\([]\.$*^[]\)/\\\1/g'
+}

+ 40 - 12
dnsapi/dns_dp.sh

@@ -6,9 +6,8 @@
 #
 #DP_Key="sADDsdasdgdsf"
 
-DP_Api="https://dnsapi.cn"
+REST_API="https://dnsapi.cn"
 
-#REST_API
 ########  Public functions #####################
 
 #Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
@@ -24,8 +23,6 @@ dns_dp_add() {
     return 1
   fi
 
-  REST_API="$DP_Api"
-
   #save the api key and email to the account conf file.
   _saveaccountconf DP_Id "$DP_Id"
   _saveaccountconf DP_Key "$DP_Key"
@@ -50,9 +47,39 @@ dns_dp_add() {
   fi
 }
 
-#fulldomain
+#fulldomain txtvalue
 dns_dp_rm() {
   fulldomain=$1
+  txtvalue=$2
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+
+  if ! _rest POST "Record.List" "login_token=$DP_Id,$DP_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" "login_token=$DP_Id,$DP_Key&format=json&domain_id=$_domain_id&record_id=$record_id"; then
+    _err "Record.Remove error."
+    return 1
+  fi
+
+  _contains "$response" "Action completed successful"
 
 }
 
@@ -75,8 +102,9 @@ existing_records() {
   fi
 
   if _contains "$response" "Action completed successful"; then
-    count=$(printf "%s" "$response" | grep '<type>TXT</type>' | wc -l)
+    count=$(printf "%s" "$response" | grep -c '<type>TXT</type>' | tr -d ' ')
     record_id=$(printf "%s" "$response" | grep '^<id>' | tail -1 | cut -d '>' -f 2 | cut -d '<' -f 1)
+    _debug record_id "$record_id"
     return 0
   else
     _err "get existing records error."
@@ -130,7 +158,7 @@ update_record() {
   return 1 #error
 }
 
-####################  Private functions bellow ##################################
+####################  Private functions below ##################################
 #_acme-challenge.www.domain.com
 #returns
 # _sub_domain=_acme-challenge.www
@@ -171,7 +199,7 @@ _get_root() {
 
 #Usage: method  URI  data
 _rest() {
-  m=$1
+  m="$1"
   ep="$2"
   data="$3"
   _debug "$ep"
@@ -179,11 +207,11 @@ _rest() {
 
   _debug url "$url"
 
-  if [ "$data" ]; then
-    _debug2 data "$data"
-    response="$(_post "$data" "$url")"
+  if [ "$m" = "GET" ]; then
+    response="$(_get "$url" | tr -d '\r')"
   else
-    response="$(_get "$url")"
+    _debug2 data "$data"
+    response="$(_post "$data" "$url" | tr -d '\r')"
   fi
 
   if [ "$?" != "0" ]; then

+ 128 - 0
dnsapi/dns_duckdns.sh

@@ -0,0 +1,128 @@
+#!/usr/bin/env sh
+
+#Created by RaidenII, to use DuckDNS's API to add/remove text records
+#06/27/2017
+
+# Pass credentials before "acme.sh --issue --dns dns_duckdns ..."
+# --
+# export DuckDNS_Token="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
+# --
+#
+# Due to the fact that DuckDNS uses StartSSL as cert provider, --insecure may need to be used with acme.sh
+
+DuckDNS_API="https://www.duckdns.org/update"
+
+########  Public functions #####################
+
+#Usage: dns_duckdns_add _acme-challenge.domain.duckdns.org "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_duckdns_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  DuckDNS_Token="${DuckDNS_Token:-$(_readaccountconf_mutable DuckDNS_Token)}"
+  if [ -z "$DuckDNS_Token" ]; then
+    _err "You must export variable: DuckDNS_Token"
+    _err "The token for your DuckDNS account is necessary."
+    _err "You can look it up in your DuckDNS account."
+    return 1
+  fi
+
+  # Now save the credentials.
+  _saveaccountconf_mutable DuckDNS_Token "$DuckDNS_Token"
+
+  # Unfortunately, DuckDNS does not seems to support lookup domain through API
+  # So I assume your credentials (which are your domain and token) are correct
+  # If something goes wrong, we will get a KO response from DuckDNS
+
+  if ! _duckdns_get_domain; then
+    return 1
+  fi
+
+  # Now add the TXT record to DuckDNS
+  _info "Trying to add TXT record"
+  if _duckdns_rest GET "domains=$_duckdns_domain&token=$DuckDNS_Token&txt=$txtvalue"; then
+    if [ "$response" = "OK" ]; then
+      _info "TXT record has been successfully added to your DuckDNS domain."
+      _info "Note that all subdomains under this domain uses the same TXT record."
+      return 0
+    else
+      _err "Errors happened during adding the TXT record, response=$response"
+      return 1
+    fi
+  else
+    _err "Errors happened during adding the TXT record."
+    return 1
+  fi
+}
+
+#Usage: fulldomain txtvalue
+#Remove the txt record after validation.
+dns_duckdns_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  DuckDNS_Token="${DuckDNS_Token:-$(_readaccountconf_mutable DuckDNS_Token)}"
+  if [ -z "$DuckDNS_Token" ]; then
+    _err "You must export variable: DuckDNS_Token"
+    _err "The token for your DuckDNS account is necessary."
+    _err "You can look it up in your DuckDNS account."
+    return 1
+  fi
+
+  if ! _duckdns_get_domain; then
+    return 1
+  fi
+
+  # Now remove the TXT record from DuckDNS
+  _info "Trying to remove TXT record"
+  if _duckdns_rest GET "domains=$_duckdns_domain&token=$DuckDNS_Token&txt=&clear=true"; then
+    if [ "$response" = "OK" ]; then
+      _info "TXT record has been successfully removed from your DuckDNS domain."
+      return 0
+    else
+      _err "Errors happened during removing the TXT record, response=$response"
+      return 1
+    fi
+  else
+    _err "Errors happened during removing the TXT record."
+    return 1
+  fi
+}
+
+####################  Private functions below ##################################
+
+#fulldomain=_acme-challenge.domain.duckdns.org
+#returns
+# _duckdns_domain=domain
+_duckdns_get_domain() {
+
+  # We'll extract the domain/username from full domain
+  _duckdns_domain="$(printf "%s" "$fulldomain" | _lower_case | _egrep_o '[.][^.][^.]*[.]duckdns.org' | cut -d . -f 2)"
+
+  if [ -z "$_duckdns_domain" ]; then
+    _err "Error extracting the domain."
+    return 1
+  fi
+
+  return 0
+}
+
+#Usage: method URI
+_duckdns_rest() {
+  method=$1
+  param="$2"
+  _debug param "$param"
+  url="$DuckDNS_API?$param"
+  _debug url "$url"
+
+  # DuckDNS uses GET to update domain info
+  if [ "$method" = "GET" ]; then
+    response="$(_get "$url")"
+  else
+    _err "Unsupported method"
+    return 1
+  fi
+
+  _debug2 response "$response"
+  return 0
+}

+ 339 - 0
dnsapi/dns_dyn.sh

@@ -0,0 +1,339 @@
+#!/usr/bin/env sh
+#
+# Dyn.com Domain API
+#
+# Author: Gerd Naschenweng
+# https://github.com/magicdude4eva
+#
+# Dyn Managed DNS API
+# https://help.dyn.com/dns-api-knowledge-base/
+#
+# It is recommended to add a "Dyn Managed DNS" user specific for API access.
+# The "Zones & Records Permissions" required by this script are:
+# --
+# RecordAdd
+# RecordUpdate
+# RecordDelete
+# RecordGet
+# ZoneGet
+# ZoneAddNode
+# ZoneRemoveNode
+# ZonePublish
+# --
+#
+# Pass credentials before "acme.sh --issue --dns dns_dyn ..."
+# --
+# export DYN_Customer="customer"
+# export DYN_Username="apiuser"
+# export DYN_Password="secret"
+# --
+
+DYN_API="https://api.dynect.net/REST"
+
+#REST_API
+########  Public functions #####################
+
+#Usage: add  _acme-challenge.www.domain.com   "Challenge-code"
+dns_dyn_add() {
+  fulldomain="$1"
+  txtvalue="$2"
+
+  DYN_Customer="${DYN_Customer:-$(_readaccountconf_mutable DYN_Customer)}"
+  DYN_Username="${DYN_Username:-$(_readaccountconf_mutable DYN_Username)}"
+  DYN_Password="${DYN_Password:-$(_readaccountconf_mutable DYN_Password)}"
+  if [ -z "$DYN_Customer" ] || [ -z "$DYN_Username" ] || [ -z "$DYN_Password" ]; then
+    DYN_Customer=""
+    DYN_Username=""
+    DYN_Password=""
+    _err "You must export variables: DYN_Customer, DYN_Username and DYN_Password"
+    return 1
+  fi
+
+  #save the config variables to the account conf file.
+  _saveaccountconf_mutable DYN_Customer "$DYN_Customer"
+  _saveaccountconf_mutable DYN_Username "$DYN_Username"
+  _saveaccountconf_mutable DYN_Password "$DYN_Password"
+
+  if ! _dyn_get_authtoken; then
+    return 1
+  fi
+
+  if [ -z "$_dyn_authtoken" ]; then
+    _dyn_end_session
+    return 1
+  fi
+
+  if ! _dyn_get_zone; then
+    _dyn_end_session
+    return 1
+  fi
+
+  if ! _dyn_add_record; then
+    _dyn_end_session
+    return 1
+  fi
+
+  if ! _dyn_publish_zone; then
+    _dyn_end_session
+    return 1
+  fi
+
+  _dyn_end_session
+
+  return 0
+}
+
+#Usage: fulldomain txtvalue
+#Remove the txt record after validation.
+dns_dyn_rm() {
+  fulldomain="$1"
+  txtvalue="$2"
+
+  DYN_Customer="${DYN_Customer:-$(_readaccountconf_mutable DYN_Customer)}"
+  DYN_Username="${DYN_Username:-$(_readaccountconf_mutable DYN_Username)}"
+  DYN_Password="${DYN_Password:-$(_readaccountconf_mutable DYN_Password)}"
+  if [ -z "$DYN_Customer" ] || [ -z "$DYN_Username" ] || [ -z "$DYN_Password" ]; then
+    DYN_Customer=""
+    DYN_Username=""
+    DYN_Password=""
+    _err "You must export variables: DYN_Customer, DYN_Username and DYN_Password"
+    return 1
+  fi
+
+  if ! _dyn_get_authtoken; then
+    return 1
+  fi
+
+  if [ -z "$_dyn_authtoken" ]; then
+    _dyn_end_session
+    return 1
+  fi
+
+  if ! _dyn_get_zone; then
+    _dyn_end_session
+    return 1
+  fi
+
+  if ! _dyn_get_record_id; then
+    _dyn_end_session
+    return 1
+  fi
+
+  if [ -z "$_dyn_record_id" ]; then
+    _dyn_end_session
+    return 1
+  fi
+
+  if ! _dyn_rm_record; then
+    _dyn_end_session
+    return 1
+  fi
+
+  if ! _dyn_publish_zone; then
+    _dyn_end_session
+    return 1
+  fi
+
+  _dyn_end_session
+
+  return 0
+}
+
+####################  Private functions below ##################################
+
+#get Auth-Token
+_dyn_get_authtoken() {
+
+  _info "Start Dyn API Session"
+
+  data="{\"customer_name\":\"$DYN_Customer\", \"user_name\":\"$DYN_Username\", \"password\":\"$DYN_Password\"}"
+  dyn_url="$DYN_API/Session/"
+  method="POST"
+
+  _debug data "$data"
+  _debug dyn_url "$dyn_url"
+
+  export _H1="Content-Type: application/json"
+
+  response="$(_post "$data" "$dyn_url" "" "$method")"
+  sessionstatus="$(printf "%s\n" "$response" | _egrep_o '"status" *: *"[^"]*' | _head_n 1 | sed 's#^"status" *: *"##')"
+
+  _debug response "$response"
+  _debug sessionstatus "$sessionstatus"
+
+  if [ "$sessionstatus" = "success" ]; then
+    _dyn_authtoken="$(printf "%s\n" "$response" | _egrep_o '"token" *: *"[^"]*' | _head_n 1 | sed 's#^"token" *: *"##')"
+    _info "Token received"
+    _debug _dyn_authtoken "$_dyn_authtoken"
+    return 0
+  fi
+
+  _dyn_authtoken=""
+  _err "get token failed"
+  return 1
+}
+
+#fulldomain=_acme-challenge.www.domain.com
+#returns
+# _dyn_zone=domain.com
+_dyn_get_zone() {
+  i=2
+  while true; do
+    domain="$(printf "%s" "$fulldomain" | cut -d . -f "$i-100")"
+    if [ -z "$domain" ]; then
+      break
+    fi
+
+    dyn_url="$DYN_API/Zone/$domain/"
+
+    export _H1="Auth-Token: $_dyn_authtoken"
+    export _H2="Content-Type: application/json"
+
+    response="$(_get "$dyn_url" "" "")"
+    sessionstatus="$(printf "%s\n" "$response" | _egrep_o '"status" *: *"[^"]*' | _head_n 1 | sed 's#^"status" *: *"##')"
+
+    _debug dyn_url "$dyn_url"
+    _debug response "$response"
+    _debug sessionstatus "$sessionstatus"
+
+    if [ "$sessionstatus" = "success" ]; then
+      _dyn_zone="$domain"
+      return 0
+    fi
+    i=$(_math "$i" + 1)
+  done
+
+  _dyn_zone=""
+  _err "get zone failed"
+  return 1
+}
+
+#add TXT record
+_dyn_add_record() {
+
+  _info "Adding TXT record"
+
+  data="{\"rdata\":{\"txtdata\":\"$txtvalue\"},\"ttl\":\"300\"}"
+  dyn_url="$DYN_API/TXTRecord/$_dyn_zone/$fulldomain/"
+  method="POST"
+
+  export _H1="Auth-Token: $_dyn_authtoken"
+  export _H2="Content-Type: application/json"
+
+  response="$(_post "$data" "$dyn_url" "" "$method")"
+  sessionstatus="$(printf "%s\n" "$response" | _egrep_o '"status" *: *"[^"]*' | _head_n 1 | sed 's#^"status" *: *"##')"
+
+  _debug response "$response"
+  _debug sessionstatus "$sessionstatus"
+
+  if [ "$sessionstatus" = "success" ]; then
+    _info "TXT Record successfully added"
+    return 0
+  fi
+
+  _err "add TXT record failed"
+  return 1
+}
+
+#publish the zone
+_dyn_publish_zone() {
+
+  _info "Publishing zone"
+
+  data="{\"publish\":\"true\"}"
+  dyn_url="$DYN_API/Zone/$_dyn_zone/"
+  method="PUT"
+
+  export _H1="Auth-Token: $_dyn_authtoken"
+  export _H2="Content-Type: application/json"
+
+  response="$(_post "$data" "$dyn_url" "" "$method")"
+  sessionstatus="$(printf "%s\n" "$response" | _egrep_o '"status" *: *"[^"]*' | _head_n 1 | sed 's#^"status" *: *"##')"
+
+  _debug response "$response"
+  _debug sessionstatus "$sessionstatus"
+
+  if [ "$sessionstatus" = "success" ]; then
+    _info "Zone published"
+    return 0
+  fi
+
+  _err "publish zone failed"
+  return 1
+}
+
+#get record_id of TXT record so we can delete the record
+_dyn_get_record_id() {
+
+  _info "Getting record_id of TXT record"
+
+  dyn_url="$DYN_API/TXTRecord/$_dyn_zone/$fulldomain/"
+
+  export _H1="Auth-Token: $_dyn_authtoken"
+  export _H2="Content-Type: application/json"
+
+  response="$(_get "$dyn_url" "" "")"
+  sessionstatus="$(printf "%s\n" "$response" | _egrep_o '"status" *: *"[^"]*' | _head_n 1 | sed 's#^"status" *: *"##')"
+
+  _debug response "$response"
+  _debug sessionstatus "$sessionstatus"
+
+  if [ "$sessionstatus" = "success" ]; then
+    _dyn_record_id="$(printf "%s\n" "$response" | _egrep_o "\"data\" *: *\[\"/REST/TXTRecord/$_dyn_zone/$fulldomain/[^\"]*" | _head_n 1 | sed "s#^\"data\" *: *\[\"/REST/TXTRecord/$_dyn_zone/$fulldomain/##")"
+    _debug _dyn_record_id "$_dyn_record_id"
+    return 0
+  fi
+
+  _dyn_record_id=""
+  _err "getting record_id failed"
+  return 1
+}
+
+#delete TXT record
+_dyn_rm_record() {
+
+  _info "Deleting TXT record"
+
+  dyn_url="$DYN_API/TXTRecord/$_dyn_zone/$fulldomain/$_dyn_record_id/"
+  method="DELETE"
+
+  _debug dyn_url "$dyn_url"
+
+  export _H1="Auth-Token: $_dyn_authtoken"
+  export _H2="Content-Type: application/json"
+
+  response="$(_post "" "$dyn_url" "" "$method")"
+  sessionstatus="$(printf "%s\n" "$response" | _egrep_o '"status" *: *"[^"]*' | _head_n 1 | sed 's#^"status" *: *"##')"
+
+  _debug response "$response"
+  _debug sessionstatus "$sessionstatus"
+
+  if [ "$sessionstatus" = "success" ]; then
+    _info "TXT record successfully deleted"
+    return 0
+  fi
+
+  _err "delete TXT record failed"
+  return 1
+}
+
+#logout
+_dyn_end_session() {
+
+  _info "End Dyn API Session"
+
+  dyn_url="$DYN_API/Session/"
+  method="DELETE"
+
+  _debug dyn_url "$dyn_url"
+
+  export _H1="Auth-Token: $_dyn_authtoken"
+  export _H2="Content-Type: application/json"
+
+  response="$(_post "" "$dyn_url" "" "$method")"
+
+  _debug response "$response"
+
+  _dyn_authtoken=""
+  return 0
+}

+ 228 - 0
dnsapi/dns_dynu.sh

@@ -0,0 +1,228 @@
+#!/usr/bin/env sh
+
+#Client ID
+#Dynu_ClientId="0b71cae7-a099-4f6b-8ddf-94571cdb760d"
+#
+#Secret
+#Dynu_Secret="aCUEY4BDCV45KI8CSIC3sp2LKQ9"
+#
+#Token
+Dynu_Token=""
+#
+#Endpoint
+Dynu_EndPoint="https://api.dynu.com/v1"
+#
+#Author: Dynu Systems, Inc.
+#Report Bugs here: https://github.com/shar0119/acme.sh
+#
+########  Public functions #####################
+
+#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_dynu_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if [ -z "$Dynu_ClientId" ] || [ -z "$Dynu_Secret" ]; then
+    Dynu_ClientId=""
+    Dynu_Secret=""
+    _err "Dynu client id and secret is not specified."
+    _err "Please create you API client id and secret and try again."
+    return 1
+  fi
+
+  #save the client id and secret to the account conf file.
+  _saveaccountconf Dynu_ClientId "$Dynu_ClientId"
+  _saveaccountconf Dynu_Secret "$Dynu_Secret"
+
+  if [ -z "$Dynu_Token" ]; then
+    _info "Getting Dynu token."
+    if ! _dynu_authentication; then
+      _err "Can not get token."
+    fi
+  fi
+
+  _debug "Detect root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "Invalid domain."
+    return 1
+  fi
+
+  _debug _node "$_node"
+  _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
+    return 1
+  fi
+
+  if ! _contains "$response" "text_data"; then
+    _err "Could not add TXT record."
+    return 1
+  fi
+
+  return 0
+}
+
+#Usage: rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_dynu_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if [ -z "$Dynu_ClientId" ] || [ -z "$Dynu_Secret" ]; then
+    Dynu_ClientId=""
+    Dynu_Secret=""
+    _err "Dynu client id and secret is not specified."
+    _err "Please create you API client id and secret and try again."
+    return 1
+  fi
+
+  #save the client id and secret to the account conf file.
+  _saveaccountconf Dynu_ClientId "$Dynu_ClientId"
+  _saveaccountconf Dynu_Secret "$Dynu_Secret"
+
+  if [ -z "$Dynu_Token" ]; then
+    _info "Getting Dynu token."
+    if ! _dynu_authentication; then
+      _err "Can not get token."
+    fi
+  fi
+
+  _debug "Detect root zone."
+  if ! _get_root "$fulldomain"; then
+    _err "Invalid domain."
+    return 1
+  fi
+
+  _debug _node "$_node"
+  _debug _domain_name "$_domain_name"
+
+  _info "Checking for TXT record."
+  if ! _get_recordid "$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 "$_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
+# _node=_acme-challenge.www
+# _domain_name=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 ! _dynu_rest GET "dns/get/$h"; then
+      return 1
+    fi
+
+    if _contains "$response" "\"name\":\"$h\"" >/dev/null; then
+      _domain_name=$h
+      _node=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      return 0
+    fi
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+  return 1
+
+}
+
+_get_recordid() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if ! _dynu_rest GET "dns/record/get?hostname=$fulldomain&rrtype=TXT"; then
+    return 1
+  fi
+
+  if ! _contains "$response" "$txtvalue"; then
+    _dns_record_id=0
+    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)
+
+  return 0
+}
+
+_delete_txt_record() {
+  _dns_record_id=$1
+
+  if ! _dynu_rest GET "dns/record/delete/$_dns_record_id"; then
+    return 1
+  fi
+
+  if ! _contains "$response" "true"; then
+    return 1
+  fi
+
+  return 0
+}
+
+_dynu_rest() {
+  m=$1
+  ep="$2"
+  data="$3"
+  _debug "$ep"
+
+  export _H1="Authorization: Bearer $Dynu_Token"
+  export _H2="Content-Type: application/json"
+
+  if [ "$data" ]; then
+    _debug data "$data"
+    response="$(_post "$data" "$Dynu_EndPoint/$ep" "" "$m")"
+  else
+    _info "Getting $Dynu_EndPoint/$ep"
+    response="$(_get "$Dynu_EndPoint/$ep")"
+  fi
+
+  if [ "$?" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}
+
+_dynu_authentication() {
+  realm="$(printf "%s" "$Dynu_ClientId:$Dynu_Secret" | _base64)"
+
+  export _H1="Authorization: Basic $realm"
+  export _H2="Content-Type: application/json"
+
+  response="$(_get "$Dynu_EndPoint/oauth2/token")"
+  if [ "$?" != "0" ]; then
+    _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)
+  fi
+  if _contains "$Dynu_Token" "null"; then
+    Dynu_Token=""
+  fi
+
+  _debug2 response "$response"
+  return 0
+}

+ 362 - 0
dnsapi/dns_freedns.sh

@@ -0,0 +1,362 @@
+#!/usr/bin/env sh
+
+#This file name is "dns_freedns.sh"
+#So, here must be a method dns_freedns_add()
+#Which will be called by acme.sh to add the txt record to your api system.
+#returns 0 means success, otherwise error.
+#
+#Author: David Kerr
+#Report Bugs here: https://github.com/dkerr64/acme.sh
+#
+########  Public functions #####################
+
+# Export FreeDNS userid and password in following variables...
+#  FREEDNS_User=username
+#  FREEDNS_Password=password
+# login cookie is saved in acme account config file so userid / pw
+# need to be set only when changed.
+
+#Usage: dns_freedns_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_freedns_add() {
+  fulldomain="$1"
+  txtvalue="$2"
+
+  _info "Add TXT record using FreeDNS"
+  _debug "fulldomain: $fulldomain"
+  _debug "txtvalue: $txtvalue"
+
+  if [ -z "$FREEDNS_User" ] || [ -z "$FREEDNS_Password" ]; then
+    FREEDNS_User=""
+    FREEDNS_Password=""
+    if [ -z "$FREEDNS_COOKIE" ]; then
+      _err "You did not specify the FreeDNS username and password yet."
+      _err "Please export as FREEDNS_User / FREEDNS_Password and try again."
+      return 1
+    fi
+    using_cached_cookies="true"
+  else
+    FREEDNS_COOKIE="$(_freedns_login "$FREEDNS_User" "$FREEDNS_Password")"
+    if [ -z "$FREEDNS_COOKIE" ]; then
+      return 1
+    fi
+    using_cached_cookies="false"
+  fi
+
+  _debug "FreeDNS login cookies: $FREEDNS_COOKIE (cached = $using_cached_cookies)"
+
+  _saveaccountconf FREEDNS_COOKIE "$FREEDNS_COOKIE"
+
+  # split our full domain name into two parts...
+  i="$(echo "$fulldomain" | tr '.' ' ' | wc -w)"
+  i="$(_math "$i" - 1)"
+  top_domain="$(echo "$fulldomain" | cut -d. -f "$i"-100)"
+  i="$(_math "$i" - 1)"
+  sub_domain="$(echo "$fulldomain" | cut -d. -f -"$i")"
+
+  _debug top_domain "$top_domain"
+  _debug sub_domain "$sub_domain"
+  # Sometimes FreeDNS does not return the subdomain page but rather
+  # returns a page regarding becoming a premium member.  This usually
+  # happens after a period of inactivity.  Immediately trying again
+  # returns the correct subdomain page.  So, we will try twice to
+  # load the page and obtain our domain ID
+  attempts=2
+  while [ "$attempts" -gt "0" ]; do
+    attempts="$(_math "$attempts" - 1)"
+    htmlpage="$(_freedns_retrieve_subdomain_page "$FREEDNS_COOKIE")"
+    if [ "$?" != "0" ]; then
+      if [ "$using_cached_cookies" = "true" ]; then
+        _err "Has your FreeDNS username and password changed?  If so..."
+        _err "Please export as FREEDNS_User / FREEDNS_Password and try again."
+      fi
+      return 1
+    fi
+    _debug2 htmlpage "$htmlpage"
+
+    subdomain_csv="$(echo "$htmlpage" | tr -d "\n\r" | _egrep_o '<form .*</form>' | sed 's/<tr>/@<tr>/g' | tr '@' '\n' | grep edit.php | grep "$top_domain")"
+    _debug2 subdomain_csv "$subdomain_csv"
+
+    # The above beauty ends with striping out rows that do not have an
+    # href to edit.php and do not have the top domain we are looking for.
+    # So all we should be left with is CSV of table of subdomains we are
+    # interested in.
+
+    # Now we have to read through this table and extract the data we need
+    lines="$(echo "$subdomain_csv" | wc -l)"
+    i=0
+    found=0
+    while [ "$i" -lt "$lines" ]; do
+      i="$(_math "$i" + 1)"
+      line="$(echo "$subdomain_csv" | sed -n "${i}p")"
+      _debug2 line "$line"
+      if [ $found = 0 ] && _contains "$line" "<td>$top_domain</td>"; then
+        # this line will contain DNSdomainid for the top_domain
+        DNSdomainid="$(echo "$line" | _egrep_o "edit_domain_id *= *.*>" | cut -d = -f 2 | cut -d '>' -f 1)"
+        _debug2 DNSdomainid "$DNSdomainid"
+        found=1
+      else
+        # lines contain DNS records for all subdomains
+        DNSname="$(echo "$line" | _egrep_o 'edit.php.*</a>' | cut -d '>' -f 2 | cut -d '<' -f 1)"
+        _debug2 DNSname "$DNSname"
+        DNStype="$(echo "$line" | sed 's/<td/@<td/g' | tr '@' '\n' | sed -n '4p' | cut -d '>' -f 2 | cut -d '<' -f 1)"
+        _debug2 DNStype "$DNStype"
+        if [ "$DNSname" = "$fulldomain" ] && [ "$DNStype" = "TXT" ]; then
+          DNSdataid="$(echo "$line" | _egrep_o 'data_id=.*' | cut -d = -f 2 | cut -d '>' -f 1)"
+          # Now get current value for the TXT record.  This method may
+          # not produce accurate results as the value field is truncated
+          # on this webpage. To get full value we would need to load
+          # another page. However we don't really need this so long as
+          # there is only one TXT record for the acme challenge subdomain.
+          DNSvalue="$(echo "$line" | sed 's/<td/@<td/g' | tr '@' '\n' | sed -n '5p' | cut -d '>' -f 2 | cut -d '<' -f 1)"
+          _debug2 DNSvalue "$DNSvalue"
+          if [ $found != 0 ]; then
+            break
+            # we are breaking out of the loop at the first match of DNS name
+            # and DNS type (if we are past finding the domainid). This assumes
+            # that there is only ever one TXT record for the LetsEncrypt/acme
+            # challenge subdomain.  This seems to be a reasonable assumption
+            # as the acme client deletes the TXT record on successful validation.
+          fi
+        else
+          DNSname=""
+          DNStype=""
+        fi
+      fi
+    done
+
+    _debug "DNSname: $DNSname DNStype: $DNStype DNSdomainid: $DNSdomainid DNSdataid: $DNSdataid"
+    _debug "DNSvalue: $DNSvalue"
+
+    if [ -z "$DNSdomainid" ]; then
+      # If domain ID is empty then something went wrong (top level
+      # domain not found at FreeDNS).
+      if [ "$attempts" = "0" ]; then
+        # exhausted maximum retry attempts
+        _debug "$htmlpage"
+        _debug "$subdomain_csv"
+        _err "Domain $top_domain not found at FreeDNS"
+        return 1
+      fi
+    else
+      # break out of the 'retry' loop... we have found our domain ID
+      break
+    fi
+    _info "Domain $top_domain not found at FreeDNS"
+    _info "Retry loading subdomain page ($attempts attempts remaining)"
+  done
+
+  if [ -z "$DNSdataid" ]; then
+    # If data ID is empty then specific subdomain does not exist yet, need
+    # to create it this should always be the case as the acme client
+    # deletes the entry after domain is validated.
+    _freedns_add_txt_record "$FREEDNS_COOKIE" "$DNSdomainid" "$sub_domain" "$txtvalue"
+    return $?
+  else
+    if [ "$txtvalue" = "$DNSvalue" ]; then
+      # if value in TXT record matches value requested then DNS record
+      # does not need to be updated. But...
+      # Testing value match fails.  Website is truncating the value field.
+      # So for now we will always go down the else path.  Though in theory
+      # should never come here anyway as the acme client deletes
+      # the TXT record on successful validation, so we should not even
+      # have found a TXT record !!
+      _info "No update necessary for $fulldomain at FreeDNS"
+      return 0
+    else
+      # Delete the old TXT record (with the wrong value)
+      if _freedns_delete_txt_record "$FREEDNS_COOKIE" "$DNSdataid"; then
+        # And add in new TXT record with the value provided
+        _freedns_add_txt_record "$FREEDNS_COOKIE" "$DNSdomainid" "$sub_domain" "$txtvalue"
+      fi
+      return $?
+    fi
+  fi
+  return 0
+}
+
+#Usage: fulldomain txtvalue
+#Remove the txt record after validation.
+dns_freedns_rm() {
+  fulldomain="$1"
+  txtvalue="$2"
+
+  _info "Delete TXT record using FreeDNS"
+  _debug "fulldomain: $fulldomain"
+  _debug "txtvalue: $txtvalue"
+
+  # Need to read cookie from conf file again in case new value set
+  # during login to FreeDNS when TXT record was created.
+  # acme.sh does not have a _readaccountconf() function
+  FREEDNS_COOKIE="$(_read_conf "$ACCOUNT_CONF_PATH" "FREEDNS_COOKIE")"
+  _debug "FreeDNS login cookies: $FREEDNS_COOKIE"
+
+  # Sometimes FreeDNS does not return the subdomain page but rather
+  # returns a page regarding becoming a premium member.  This usually
+  # happens after a period of inactivity.  Immediately trying again
+  # returns the correct subdomain page.  So, we will try twice to
+  # load the page and obtain our TXT record.
+  attempts=2
+  while [ "$attempts" -gt "0" ]; do
+    attempts="$(_math "$attempts" - 1)"
+
+    htmlpage="$(_freedns_retrieve_subdomain_page "$FREEDNS_COOKIE")"
+    if [ "$?" != "0" ]; then
+      return 1
+    fi
+
+    subdomain_csv="$(echo "$htmlpage" | tr -d "\n\r" | _egrep_o '<form .*</form>' | sed 's/<tr>/@<tr>/g' | tr '@' '\n' | grep edit.php | grep "$fulldomain")"
+    _debug2 subdomain_csv "$subdomain_csv"
+
+    # The above beauty ends with striping out rows that do not have an
+    # href to edit.php and do not have the domain name we are looking for.
+    # So all we should be left with is CSV of table of subdomains we are
+    # interested in.
+
+    # Now we have to read through this table and extract the data we need
+    lines="$(echo "$subdomain_csv" | wc -l)"
+    i=0
+    found=0
+    while [ "$i" -lt "$lines" ]; do
+      i="$(_math "$i" + 1)"
+      line="$(echo "$subdomain_csv" | sed -n "${i}p")"
+      _debug2 line "$line"
+      DNSname="$(echo "$line" | _egrep_o 'edit.php.*</a>' | cut -d '>' -f 2 | cut -d '<' -f 1)"
+      _debug2 DNSname "$DNSname"
+      DNStype="$(echo "$line" | sed 's/<td/@<td/g' | tr '@' '\n' | sed -n '4p' | cut -d '>' -f 2 | cut -d '<' -f 1)"
+      _debug2 DNStype "$DNStype"
+      if [ "$DNSname" = "$fulldomain" ] && [ "$DNStype" = "TXT" ]; then
+        DNSdataid="$(echo "$line" | _egrep_o 'data_id=.*' | cut -d = -f 2 | cut -d '>' -f 1)"
+        _debug2 DNSdataid "$DNSdataid"
+        DNSvalue="$(echo "$line" | sed 's/<td/@<td/g' | tr '@' '\n' | sed -n '5p' | cut -d '>' -f 2 | cut -d '<' -f 1)"
+        _debug2 DNSvalue "$DNSvalue"
+        #     if [ "$DNSvalue" = "$txtvalue" ]; then
+        # Testing value match fails.  Website is truncating the value
+        # field. So for now we will assume that there is only one TXT
+        # field for the sub domain and just delete it. Currently this
+        # is a safe assumption.
+        _freedns_delete_txt_record "$FREEDNS_COOKIE" "$DNSdataid"
+        return $?
+        #     fi
+      fi
+    done
+  done
+
+  # If we get this far we did not find a match (after two attempts)
+  # Not necessarily an error, but log anyway.
+  _debug2 "$subdomain_csv"
+  _info "Cannot delete TXT record for $fulldomain/$txtvalue. Does not exist at FreeDNS"
+  return 0
+}
+
+####################  Private functions below ##################################
+
+# usage: _freedns_login username password
+# print string "cookie=value" etc.
+# returns 0 success
+_freedns_login() {
+  export _H1="Accept-Language:en-US"
+  username="$1"
+  password="$2"
+  url="https://freedns.afraid.org/zc.php?step=2"
+
+  _debug "Login to FreeDNS as user $username"
+
+  htmlpage="$(_post "username=$(printf '%s' "$username" | _url_encode)&password=$(printf '%s' "$password" | _url_encode)&submit=Login&action=auth" "$url")"
+
+  if [ "$?" != "0" ]; then
+    _err "FreeDNS login failed for user $username bad RC from _post"
+    return 1
+  fi
+
+  cookies="$(grep -i '^Set-Cookie.*dns_cookie.*$' "$HTTP_HEADER" | _head_n 1 | tr -d "\r\n" | cut -d " " -f 2)"
+
+  # if cookies is not empty then logon successful
+  if [ -z "$cookies" ]; then
+    _debug "$htmlpage"
+    _err "FreeDNS login failed for user $username. Check $HTTP_HEADER file"
+    return 1
+  fi
+
+  printf "%s" "$cookies"
+  return 0
+}
+
+# usage _freedns_retrieve_subdomain_page login_cookies
+# echo page retrieved (html)
+# returns 0 success
+_freedns_retrieve_subdomain_page() {
+  export _H1="Cookie:$1"
+  export _H2="Accept-Language:en-US"
+  url="https://freedns.afraid.org/subdomain/"
+
+  _debug "Retrieve subdomain page from FreeDNS"
+
+  htmlpage="$(_get "$url")"
+
+  if [ "$?" != "0" ]; then
+    _err "FreeDNS retrieve subdomains failed bad RC from _get"
+    return 1
+  elif [ -z "$htmlpage" ]; then
+    _err "FreeDNS returned empty subdomain page"
+    return 1
+  fi
+
+  _debug2 "$htmlpage"
+
+  printf "%s" "$htmlpage"
+  return 0
+}
+
+# usage _freedns_add_txt_record login_cookies domain_id subdomain value
+# returns 0 success
+_freedns_add_txt_record() {
+  export _H1="Cookie:$1"
+  export _H2="Accept-Language:en-US"
+  domain_id="$2"
+  subdomain="$3"
+  value="$(printf '%s' "$4" | _url_encode)"
+  url="http://freedns.afraid.org/subdomain/save.php?step=2"
+
+  htmlpage="$(_post "type=TXT&domain_id=$domain_id&subdomain=$subdomain&address=%22$value%22&send=Save%21" "$url")"
+
+  if [ "$?" != "0" ]; then
+    _err "FreeDNS failed to add TXT record for $subdomain bad RC from _post"
+    return 1
+  elif ! grep "200 OK" "$HTTP_HEADER" >/dev/null; then
+    _debug "$htmlpage"
+    _err "FreeDNS failed to add TXT record for $subdomain. Check $HTTP_HEADER file"
+    return 1
+  elif _contains "$htmlpage" "security code was incorrect"; then
+    _debug "$htmlpage"
+    _err "FreeDNS failed to add TXT record for $subdomain as FreeDNS requested security code"
+    _err "Note that you cannot use automatic DNS validation for FreeDNS public domains"
+    return 1
+  fi
+
+  _debug2 "$htmlpage"
+  _info "Added acme challenge TXT record for $fulldomain at FreeDNS"
+  return 0
+}
+
+# usage _freedns_delete_txt_record login_cookies data_id
+# returns 0 success
+_freedns_delete_txt_record() {
+  export _H1="Cookie:$1"
+  export _H2="Accept-Language:en-US"
+  data_id="$2"
+  url="https://freedns.afraid.org/subdomain/delete2.php"
+
+  htmlheader="$(_get "$url?data_id%5B%5D=$data_id&submit=delete+selected" "onlyheader")"
+
+  if [ "$?" != "0" ]; then
+    _err "FreeDNS failed to delete TXT record for $data_id bad RC from _get"
+    return 1
+  elif ! _contains "$htmlheader" "200 OK"; then
+    _debug "$htmlheader"
+    _err "FreeDNS failed to delete TXT record $data_id"
+    return 1
+  fi
+
+  _info "Deleted acme challenge TXT record for $fulldomain at FreeDNS"
+  return 0
+}

+ 123 - 0
dnsapi/dns_gandi_livedns.sh

@@ -0,0 +1,123 @@
+#!/usr/bin/env sh
+
+# Gandi LiveDNS v5 API
+# http://doc.livedns.gandi.net/
+# currently under beta
+#
+# Requires GANDI API KEY set in GANDI_LIVEDNS_KEY set as environment variable
+#
+#Author: Frédéric Crozat <fcrozat@suse.com>
+#Report Bugs here: https://github.com/fcrozat/acme.sh
+#
+########  Public functions #####################
+
+GANDI_LIVEDNS_API="https://dns.api.gandi.net/api/v5"
+
+#Usage: dns_gandi_livedns_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_gandi_livedns_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if [ -z "$GANDI_LIVEDNS_KEY" ]; then
+    _err "No API key specified for Gandi LiveDNS."
+    _err "Create your key and export it as GANDI_LIVEDNS_KEY"
+    return 1
+  fi
+
+  _saveaccountconf GANDI_LIVEDNS_KEY "$GANDI_LIVEDNS_KEY"
+
+  _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"
+
+  _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")"
+}
+
+#Usage: fulldomain txtvalue
+#Remove the txt record after validation.
+dns_gandi_livedns_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+
+  _debug fulldomain "$fulldomain"
+  _debug domain "$_domain"
+  _debug sub_domain "$_sub_domain"
+
+  _gandi_livedns_rest DELETE "domains/$_domain/records/$_sub_domain/TXT" ""
+
+}
+
+####################  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 ! _gandi_livedns_rest GET "domains/$h"; then
+      return 1
+    fi
+
+    if _contains "$response" '"code": 401'; then
+      _err "$response"
+      return 1
+    elif _contains "$response" '"code": 404'; 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
+}
+
+_gandi_livedns_rest() {
+  m=$1
+  ep="$2"
+  data="$3"
+  _debug "$ep"
+
+  export _H1="Content-Type: application/json"
+  export _H2="X-Api-Key: $GANDI_LIVEDNS_KEY"
+
+  if [ "$m" = "GET" ]; then
+    response="$(_get "$GANDI_LIVEDNS_API/$ep")"
+  else
+    _debug data "$data"
+    response="$(_post "$data" "$GANDI_LIVEDNS_API/$ep" "" "$m")"
+  fi
+
+  if [ "$?" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}

+ 4 - 4
dnsapi/dns_gd.sh

@@ -40,7 +40,7 @@ dns_gd_add() {
   if _gd_rest PUT "domains/$_domain/records/TXT/$_sub_domain" "[{\"data\":\"$txtvalue\"}]"; then
     if [ "$response" = "{}" ]; then
       _info "Added, sleeping 10 seconds"
-      sleep 10
+      _sleep 10
       #todo: check if the record takes effect
       return 0
     else
@@ -59,7 +59,7 @@ dns_gd_rm() {
 
 }
 
-####################  Private functions bellow ##################################
+####################  Private functions below ##################################
 #_acme-challenge.www.domain.com
 #returns
 # _sub_domain=_acme-challenge.www
@@ -98,8 +98,8 @@ _gd_rest() {
   data="$3"
   _debug "$ep"
 
-  _H1="Authorization: sso-key $GD_Key:$GD_Secret"
-  _H2="Content-Type: application/json"
+  export _H1="Authorization: sso-key $GD_Key:$GD_Secret"
+  export _H2="Content-Type: application/json"
 
   if [ "$data" ]; then
     _debug data "$data"

+ 175 - 0
dnsapi/dns_he.sh

@@ -0,0 +1,175 @@
+#!/usr/bin/env sh
+
+########################################################################
+# Hurricane Electric hook script for acme.sh
+#
+# Environment variables:
+#
+#  - $HE_Username  (your dns.he.net username)
+#  - $HE_Password  (your dns.he.net password)
+#
+# Author: Ondrej Simek <me@ondrejsimek.com>
+# Git repo: https://github.com/angel333/acme.sh
+
+#-- dns_he_add() - Add TXT record --------------------------------------
+# Usage: dns_he_add _acme-challenge.subdomain.domain.com "XyZ123..."
+
+dns_he_add() {
+  _full_domain=$1
+  _txt_value=$2
+  _info "Using DNS-01 Hurricane Electric hook"
+
+  if [ -z "$HE_Username" ] || [ -z "$HE_Password" ]; then
+    HE_Username=
+    HE_Password=
+    _err "No auth details provided. Please set user credentials using the \$HE_Username and \$HE_Password envoronment variables."
+    return 1
+  fi
+  _saveaccountconf HE_Username "$HE_Username"
+  _saveaccountconf HE_Password "$HE_Password"
+
+  # 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}"
+  body="$body&account="
+  body="$body&menu=edit_zone"
+  body="$body&Type=TXT"
+  body="$body&hosted_dns_zoneid=$_zone_id"
+  body="$body&hosted_dns_recordid="
+  body="$body&hosted_dns_editzone=1"
+  body="$body&Priority="
+  body="$body&Name=$_full_domain"
+  body="$body&Content=$_txt_value"
+  body="$body&TTL=300"
+  body="$body&hosted_dns_editrecord=Submit"
+  response="$(_post "$body" "https://dns.he.net/")"
+  exit_code="$?"
+  if [ "$exit_code" -eq 0 ]; then
+    _info "TXT record added successfully."
+  else
+    _err "Couldn't add the TXT record."
+  fi
+  _debug2 response "$response"
+  return "$exit_code"
+}
+
+#-- dns_he_rm() - Remove TXT record ------------------------------------
+# Usage: dns_he_rm _acme-challenge.subdomain.domain.com "XyZ123..."
+
+dns_he_rm() {
+  _full_domain=$1
+  _txt_value=$2
+  _info "Cleaning up after DNS-01 Hurricane Electric hook"
+
+  # fills in the $_zone_id
+  _find_zone "$_full_domain" || return 1
+  _debug "Zone id \"$_zone_id\" will be used."
+
+  # Find the record id to clean
+  body="email=${HE_Username}&pass=${HE_Password}"
+  body="$body&hosted_dns_zoneid=$_zone_id"
+  body="$body&menu=edit_zone"
+  body="$body&hosted_dns_editzone="
+  domain_regex="$(echo "$_full_domain" | sed 's/\./\\./g')" # escape dots
+  _record_id=$(_post "$body" "https://dns.he.net/" \
+    | tr -d '\n' \
+    | _egrep_o "data=\"&quot;${_txt_value}&quot;([^>]+>){6}[^<]+<[^;]+;deleteRecord\('[0-9]+','${domain_regex}','TXT'\)" \
+    | _egrep_o "[0-9]+','${domain_regex}','TXT'\)$" \
+    | _egrep_o "^[0-9]+"
+  )
+  # The series of egreps above could have been done a bit shorter but
+  #  I wanted to double-check whether it's the correct record (in case
+  #  HE changes their website somehow).
+
+  # Remove the record
+  body="email=${HE_Username}&pass=${HE_Password}"
+  body="$body&menu=edit_zone"
+  body="$body&hosted_dns_zoneid=$_zone_id"
+  body="$body&hosted_dns_recordid=$_record_id"
+  body="$body&hosted_dns_editzone=1"
+  body="$body&hosted_dns_delrecord=1"
+  body="$body&hosted_dns_delconfirm=delete"
+  _post "$body" "https://dns.he.net/" \
+    | grep '<div id="dns_status" onClick="hideThis(this);">Successfully removed record.</div>' \
+      >/dev/null
+  exit_code="$?"
+  if [ "$exit_code" -eq 0 ]; then
+    _info "Record removed successfully."
+  else
+    _err "Could not clean (remove) up the record. Please go to HE administration interface and clean it by hand."
+    return "$exit_code"
+  fi
+}
+
+########################## PRIVATE FUNCTIONS ###########################
+
+#-- _find_zone() -------------------------------------------------------
+# Returns the most specific zone found in administration interface.
+#
+# Example:
+#
+# _find_zone first.second.third.co.uk
+#
+# ... will return the first zone that exists in admin out of these:
+# - "first.second.third.co.uk"
+# - "second.third.co.uk"
+# - "third.co.uk"
+# - "co.uk" <-- unlikely
+# - "uk"    <-'
+#
+# (another approach would be something like this:
+#   https://github.com/hlandau/acme/blob/master/_doc/dns.hook
+#   - that's better if there are multiple pages. It's so much simpler.
+# )
+
+_find_zone() {
+
+  _domain="$1"
+
+  body="email=${HE_Username}&pass=${HE_Password}"
+  _matches=$(_post "$body" "https://dns.he.net/" \
+    | _egrep_o "delete_dom.*name=\"[^\"]+\" value=\"[0-9]+"
+  )
+  # Zone names and zone IDs are in same order
+  _zone_ids=$(echo "$_matches" | cut -d '"' -f 5)
+  _zone_names=$(echo "$_matches" | cut -d '"' -f 3)
+  _debug2 "These are the zones on this HE account:"
+  _debug2 "$_zone_names"
+  _debug2 "And these are their respective IDs:"
+  _debug2 "$_zone_ids"
+
+  # Walk through all possible zone names
+  _strip_counter=1
+  while true; do
+    _attempted_zone=$(echo "$_domain" | cut -d . -f ${_strip_counter}-)
+
+    # All possible zone names have been tried
+    if [ -z "$_attempted_zone" ]; then
+      _err "No zone for domain \"$_domain\" found."
+      return 1
+    fi
+
+    _debug "Looking for zone \"${_attempted_zone}\""
+
+    # Take care of "." and only match whole lines. Note that grep -F
+    # cannot be used because there's no way to make it match whole
+    # lines.
+    regex="^$(echo "$_attempted_zone" | sed 's/\./\\./g')$"
+    line_num=$(echo "$_zone_names" \
+      | grep -n "$regex" \
+      | cut -d : -f 1
+    )
+
+    if [ -n "$line_num" ]; then
+      _zone_id=$(echo "$_zone_ids" | sed "${line_num}q;d")
+      _debug "Found relevant zone \"$_attempted_zone\" with id \"$_zone_id\" - will be used for domain \"$_domain\"."
+      return 0
+    fi
+
+    _debug "Zone \"$_attempted_zone\" doesn't exist, let's try a less specific zone."
+    _strip_counter=$(_math "$_strip_counter" + 1)
+  done
+}
+# vim: et:ts=2:sw=2:

+ 102 - 0
dnsapi/dns_infoblox.sh

@@ -0,0 +1,102 @@
+#!/usr/bin/env sh
+
+## Infoblox API integration by Jason Keller and Elijah Tenai
+##
+## Report any bugs via https://github.com/jasonkeller/acme.sh
+
+dns_infoblox_add() {
+
+  ## Nothing to see here, just some housekeeping
+  fulldomain=$1
+  txtvalue=$2
+  baseurlnObject="https://$Infoblox_Server/wapi/v2.2.2/record:txt?name=$fulldomain&text=$txtvalue&view=$Infoblox_View"
+
+  _info "Using Infoblox API"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  ## Check for the credentials
+  if [ -z "$Infoblox_Creds" ] || [ -z "$Infoblox_Server" ]; then
+    Infoblox_Creds=""
+    Infoblox_Server=""
+    _err "You didn't specify the credentials, server or infoblox view yet (Infoblox_Creds, Infoblox_Server and Infoblox_View)."
+    _err "Please set them via EXPORT ([username:password], [ip or hostname]) and try again."
+    return 1
+  fi
+
+  if [ -z "$Infoblox_View" ]; then
+    Infoblox_View="default"
+  fi
+
+  ## Save the credentials to the account file
+  _saveaccountconf Infoblox_Creds "$Infoblox_Creds"
+  _saveaccountconf Infoblox_Server "$Infoblox_Server"
+  _saveaccountconf Infoblox_View "$Infoblox_View"
+
+  ## Base64 encode the credentials
+  Infoblox_CredsEncoded=$(printf "%b" "$Infoblox_Creds" | _base64)
+
+  ## Construct the HTTP Authorization header
+  export _H1="Accept-Language:en-US"
+  export _H2="Authorization: Basic $Infoblox_CredsEncoded"
+
+  ## Add the challenge record to the Infoblox grid member
+  result="$(_post "" "$baseurlnObject" "" "POST")"
+
+  ## Let's see if we get something intelligible back from the unit
+  if [ "$(echo "$result" | _egrep_o "record:txt/.*:.*/$Infoblox_View")" ]; then
+    _info "Successfully created the txt record"
+    return 0
+  else
+    _err "Error encountered during record addition"
+    _err "$result"
+    return 1
+  fi
+
+}
+
+dns_infoblox_rm() {
+
+  ## Nothing to see here, just some housekeeping
+  fulldomain=$1
+  txtvalue=$2
+
+  _info "Using Infoblox API"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  ## Base64 encode the credentials
+  Infoblox_CredsEncoded="$(printf "%b" "$Infoblox_Creds" | _base64)"
+
+  ## Construct the HTTP Authorization header
+  export _H1="Accept-Language:en-US"
+  export _H2="Authorization: Basic $Infoblox_CredsEncoded"
+
+  ## Does the record exist?  Let's check.
+  baseurlnObject="https://$Infoblox_Server/wapi/v2.2.2/record:txt?name=$fulldomain&text=$txtvalue&view=$Infoblox_View&_return_type=xml-pretty"
+  result="$(_get "$baseurlnObject")"
+
+  ## Let's see if we get something intelligible back from the grid
+  if [ "$(echo "$result" | _egrep_o "record:txt/.*:.*/$Infoblox_View")" ]; then
+    ## Extract the object reference
+    objRef="$(printf "%b" "$result" | _egrep_o "record:txt/.*:.*/$Infoblox_View")"
+    objRmUrl="https://$Infoblox_Server/wapi/v2.2.2/$objRef"
+    ## Delete them! All the stale records!
+    rmResult="$(_post "" "$objRmUrl" "" "DELETE")"
+    ## Let's see if that worked
+    if [ "$(echo "$rmResult" | _egrep_o "record:txt/.*:.*/$Infoblox_View")" ]; then
+      _info "Successfully deleted $objRef"
+      return 0
+    else
+      _err "Error occurred during txt record delete"
+      _err "$rmResult"
+      return 1
+    fi
+  else
+    _err "Record to delete didn't match an existing record"
+    _err "$result"
+    return 1
+  fi
+}
+
+####################  Private functions below ##################################

+ 355 - 0
dnsapi/dns_inwx.sh

@@ -0,0 +1,355 @@
+#!/usr/bin/env sh
+
+#
+#INWX_User="username"
+#
+#INWX_Password="password"
+
+INWX_Api="https://api.domrobot.com/xmlrpc/"
+
+########  Public functions #####################
+
+#Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_inwx_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  INWX_User="${INWX_User:-$(_readaccountconf_mutable INWX_User)}"
+  INWX_Password="${INWX_Password:-$(_readaccountconf_mutable INWX_Password)}"
+  if [ -z "$INWX_User" ] || [ -z "$INWX_Password" ]; then
+    INWX_User=""
+    INWX_Password=""
+    _err "You don't specify inwx 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 INWX_User "$INWX_User"
+  _saveaccountconf_mutable INWX_Password "$INWX_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>nameserver.info</methodName>
+  <params>
+   <param>
+    <value>
+     <struct>
+      <member>
+       <name>domain</name>
+       <value>
+        <string>%s</string>
+       </value>
+      </member>
+      <member>
+       <name>type</name>
+       <value>
+        <string>TXT</string>
+       </value>
+      </member>
+      <member>
+       <name>name</name>
+       <value>
+        <string>%s</string>
+       </value>
+      </member>
+     </struct>
+    </value>
+   </param>
+  </params>
+  </methodCall>' "$_domain" "$_sub_domain")
+  response="$(_post "$xml_content" "$INWX_Api" "" "POST")"
+
+  if ! printf "%s" "$response" | grep "Command completed successfully" >/dev/null; then
+    _err "Error could net get txt records"
+    return 1
+  fi
+
+  if ! printf "%s" "$response" | grep "count" >/dev/null; then
+    _info "Adding record"
+    _inwx_add_record "$_domain" "$_sub_domain" "$txtvalue"
+  else
+    _record_id=$(printf '%s' "$response" | _egrep_o '.*(<member><name>record){1}(.*)([0-9]+){1}' | _egrep_o '<name>id<\/name><value><int>[0-9]+' | _egrep_o '[0-9]+')
+    _info "Updating record"
+    _inwx_update_record "$_record_id" "$txtvalue"
+  fi
+
+}
+
+#fulldomain txtvalue
+dns_inwx_rm() {
+
+  fulldomain=$1
+  txtvalue=$2
+
+  INWX_User="${INWX_User:-$(_readaccountconf_mutable INWX_User)}"
+  INWX_Password="${INWX_Password:-$(_readaccountconf_mutable INWX_Password)}"
+  if [ -z "$INWX_User" ] || [ -z "$INWX_Password" ]; then
+    INWX_User=""
+    INWX_Password=""
+    _err "You don't specify inwx 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 INWX_User "$INWX_User"
+  _saveaccountconf_mutable INWX_Password "$INWX_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>nameserver.info</methodName>
+  <params>
+   <param>
+    <value>
+     <struct>
+      <member>
+       <name>domain</name>
+       <value>
+        <string>%s</string>
+       </value>
+      </member>
+      <member>
+       <name>type</name>
+       <value>
+        <string>TXT</string>
+       </value>
+      </member>
+      <member>
+       <name>name</name>
+       <value>
+        <string>%s</string>
+       </value>
+      </member>
+     </struct>
+    </value>
+   </param>
+  </params>
+  </methodCall>' "$_domain" "$_sub_domain")
+  response="$(_post "$xml_content" "$INWX_Api" "" "POST")"
+
+  if ! printf "%s" "$response" | grep "Command completed successfully" >/dev/null; then
+    _err "Error could not get txt records"
+    return 1
+  fi
+
+  if ! printf "%s" "$response" | grep "count" >/dev/null; then
+    _info "Do not need to delete record"
+  else
+    _record_id=$(printf '%s' "$response" | _egrep_o '.*(<member><name>record){1}(.*)([0-9]+){1}' | _egrep_o '<name>id<\/name><value><int>[0-9]+' | _egrep_o '[0-9]+')
+    _info "Deleting record"
+    _inwx_delete_record "$_record_id"
+  fi
+
+}
+
+####################  Private functions below ##################################
+
+_inwx_login() {
+
+  xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
+  <methodCall>
+  <methodName>account.login</methodName>
+  <params>
+   <param>
+    <value>
+     <struct>
+      <member>
+       <name>user</name>
+       <value>
+        <string>%s</string>
+       </value>
+      </member>
+      <member>
+       <name>pass</name>
+       <value>
+        <string>%s</string>
+       </value>
+      </member>
+     </struct>
+    </value>
+   </param>
+  </params>
+  </methodCall>' $INWX_User $INWX_Password)
+
+  response="$(_post "$xml_content" "$INWX_Api" "" "POST")"
+
+  printf "Cookie: %s" "$(grep "domrobot=" "$HTTP_HEADER" | grep "^Set-Cookie:" | _tail_n 1 | _egrep_o 'domrobot=[^;]*;' | tr -d ';')"
+
+}
+
+_get_root() {
+  domain=$1
+  _debug "get root"
+
+  domain=$1
+  i=2
+  p=1
+
+  _H1=$(_inwx_login)
+  export _H1
+  xml_content='<?xml version="1.0" encoding="UTF-8"?>
+  <methodCall>
+  <methodName>nameserver.list</methodName>
+  </methodCall>'
+
+  response="$(_post "$xml_content" "$INWX_Api" "" "POST")"
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    _debug h "$h"
+    if [ -z "$h" ]; then
+      #not valid
+      return 1
+    fi
+
+    if _contains "$response" "$h"; then
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      _domain="$h"
+      return 0
+    fi
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+  return 1
+
+}
+
+_inwx_delete_record() {
+  record_id=$1
+  xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
+  <methodCall>
+  <methodName>nameserver.deleteRecord</methodName>
+  <params>
+   <param>
+    <value>
+     <struct>
+      <member>
+       <name>id</name>
+       <value>
+        <int>%s</int>
+       </value>
+      </member>
+     </struct>
+    </value>
+   </param>
+  </params>
+  </methodCall>' "$record_id")
+
+  response="$(_post "$xml_content" "$INWX_Api" "" "POST")"
+
+  if ! printf "%s" "$response" | grep "Command completed successfully" >/dev/null; then
+    _err "Error"
+    return 1
+  fi
+  return 0
+
+}
+
+_inwx_update_record() {
+  record_id=$1
+  txtval=$2
+  xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
+  <methodCall>
+  <methodName>nameserver.updateRecord</methodName>
+  <params>
+   <param>
+    <value>
+     <struct>
+      <member>
+       <name>content</name>
+       <value>
+        <string>%s</string>
+       </value>
+      </member>
+      <member>
+       <name>id</name>
+       <value>
+        <int>%s</int>
+       </value>
+      </member>
+     </struct>
+    </value>
+   </param>
+  </params>
+  </methodCall>' "$txtval" "$record_id")
+
+  response="$(_post "$xml_content" "$INWX_Api" "" "POST")"
+
+  if ! printf "%s" "$response" | grep "Command completed successfully" >/dev/null; then
+    _err "Error"
+    return 1
+  fi
+  return 0
+
+}
+
+_inwx_add_record() {
+
+  domain=$1
+  sub_domain=$2
+  txtval=$3
+
+  xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
+  <methodCall>
+  <methodName>nameserver.createRecord</methodName>
+  <params>
+   <param>
+    <value>
+     <struct>
+      <member>
+       <name>domain</name>
+       <value>
+        <string>%s</string>
+       </value>
+      </member>
+      <member>
+       <name>type</name>
+       <value>
+        <string>TXT</string>
+       </value>
+      </member>
+      <member>
+       <name>content</name>
+       <value>
+        <string>%s</string>
+       </value>
+      </member>
+      <member>
+       <name>name</name>
+       <value>
+        <string>%s</string>
+       </value>
+      </member>
+     </struct>
+    </value>
+   </param>
+  </params>
+  </methodCall>' "$domain" "$txtval" "$sub_domain")
+
+  response="$(_post "$xml_content" "$INWX_Api" "" "POST")"
+
+  if ! printf "%s" "$response" | grep "Command completed successfully" >/dev/null; then
+    _err "Error"
+    return 1
+  fi
+  return 0
+}

+ 2 - 2
dnsapi/dns_ispconfig.sh

@@ -30,7 +30,7 @@ dns_ispconfig_rm() {
   _ISPC_credentials && _ISPC_login && _ISPC_rmTxt
 }
 
-####################  Private functions bellow ##################################
+####################  Private functions below ##################################
 
 _ISPC_credentials() {
   if [ -z "${ISPC_User}" ] || [ -z "$ISPC_Password" ] || [ -z "${ISPC_Api}" ] || [ -z "${ISPC_Api_Insecure}" ]; then
@@ -46,7 +46,7 @@ _ISPC_credentials() {
     _saveaccountconf ISPC_Api "${ISPC_Api}"
     _saveaccountconf ISPC_Api_Insecure "${ISPC_Api_Insecure}"
     # Set whether curl should use secure or insecure mode
-    HTTPS_INSECURE="${ISPC_Api_Insecure}"
+    export HTTPS_INSECURE="${ISPC_Api_Insecure}"
   fi
 }
 

+ 95 - 0
dnsapi/dns_knot.sh

@@ -0,0 +1,95 @@
+#!/usr/bin/env sh
+
+########  Public functions #####################
+
+#Usage: dns_knot_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_knot_add() {
+  fulldomain=$1
+  txtvalue=$2
+  _checkKey || return 1
+  [ -n "${KNOT_SERVER}" ] || KNOT_SERVER="localhost"
+  # save the dns server and key to the account.conf file.
+  _saveaccountconf KNOT_SERVER "${KNOT_SERVER}"
+  _saveaccountconf KNOT_KEY "${KNOT_KEY}"
+
+  if ! _get_root "$fulldomain"; then
+    _err "Domain does not exist."
+    return 1
+  fi
+
+  _info "Adding ${fulldomain}. 60 TXT \"${txtvalue}\""
+
+  knsupdate -y "${KNOT_KEY}" <<EOF
+server ${KNOT_SERVER}
+zone ${_domain}.
+update add ${fulldomain}. 60 TXT "${txtvalue}"
+send
+quit
+EOF
+
+  if [ $? -ne 0 ]; then
+    _err "Error updating domain."
+    return 1
+  fi
+
+  _info "Domain TXT record successfully added."
+  return 0
+}
+
+#Usage: dns_knot_rm   _acme-challenge.www.domain.com
+dns_knot_rm() {
+  fulldomain=$1
+  _checkKey || return 1
+  [ -n "${KNOT_SERVER}" ] || KNOT_SERVER="localhost"
+
+  if ! _get_root "$fulldomain"; then
+    _err "Domain does not exist."
+    return 1
+  fi
+
+  _info "Removing ${fulldomain}. TXT"
+
+  knsupdate -y "${KNOT_KEY}" <<EOF
+server ${KNOT_SERVER}
+zone ${_domain}.
+update del ${fulldomain}. TXT
+send
+quit
+EOF
+
+  if [ $? -ne 0 ]; then
+    _err "error updating domain"
+    return 1
+  fi
+
+  _info "Domain TXT record successfully deleted."
+  return 0
+}
+
+####################  Private functions below ##################################
+# _acme-challenge.www.domain.com
+# returns
+# _domain=domain.com
+_get_root() {
+  domain=$1
+  i="$(echo "$fulldomain" | tr '.' ' ' | wc -w)"
+  i=$(_math "$i" - 1)
+
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
+    if [ -z "$h" ]; then
+      return 1
+    fi
+    _domain="$h"
+    return 0
+  done
+  _debug "$domain not found"
+  return 1
+}
+
+_checkKey() {
+  if [ -z "${KNOT_KEY}" ]; then
+    _err "You must specify a TSIG key to authenticate the request."
+    return 1
+  fi
+}

+ 14 - 9
dnsapi/dns_lexicon.sh

@@ -2,7 +2,7 @@
 
 # dns api wrapper of lexicon for acme.sh
 
-lexicon_url="https://github.com/AnalogJ/lexicon"
+# https://github.com/AnalogJ/lexicon
 lexicon_cmd="lexicon"
 
 wiki="https://github.com/Neilpang/acme.sh/wiki/How-to-use-lexicon-dns-api"
@@ -30,33 +30,38 @@ dns_lexicon_add() {
   _savedomainconf PROVIDER "$PROVIDER"
   export PROVIDER
 
-  Lx_name=$(echo LEXICON_"${PROVIDER}"_USERNAME | tr '[a-z]' '[A-Z]')
+  # e.g. busybox-ash does not know [:upper:]
+  # shellcheck disable=SC2018,SC2019
+  Lx_name=$(echo LEXICON_"${PROVIDER}"_USERNAME | tr 'a-z' 'A-Z')
   Lx_name_v=$(eval echo \$"$Lx_name")
-  _debug "$Lx_name" "$Lx_name_v"
+  _secure_debug "$Lx_name" "$Lx_name_v"
   if [ "$Lx_name_v" ]; then
     _saveaccountconf "$Lx_name" "$Lx_name_v"
     eval export "$Lx_name"
   fi
 
-  Lx_token=$(echo LEXICON_"${PROVIDER}"_TOKEN | tr '[a-z]' '[A-Z]')
+  # shellcheck disable=SC2018,SC2019
+  Lx_token=$(echo LEXICON_"${PROVIDER}"_TOKEN | tr 'a-z' 'A-Z')
   Lx_token_v=$(eval echo \$"$Lx_token")
-  _debug "$Lx_token" "$Lx_token_v"
+  _secure_debug "$Lx_token" "$Lx_token_v"
   if [ "$Lx_token_v" ]; then
     _saveaccountconf "$Lx_token" "$Lx_token_v"
     eval export "$Lx_token"
   fi
 
-  Lx_password=$(echo LEXICON_"${PROVIDER}"_PASSWORD | tr '[a-z]' '[A-Z]')
+  # shellcheck disable=SC2018,SC2019
+  Lx_password=$(echo LEXICON_"${PROVIDER}"_PASSWORD | tr 'a-z' 'A-Z')
   Lx_password_v=$(eval echo \$"$Lx_password")
-  _debug "$Lx_password" "$Lx_password_v"
+  _secure_debug "$Lx_password" "$Lx_password_v"
   if [ "$Lx_password_v" ]; then
     _saveaccountconf "$Lx_password" "$Lx_password_v"
     eval export "$Lx_password"
   fi
 
-  Lx_domaintoken=$(echo LEXICON_"${PROVIDER}"_DOMAINTOKEN | tr '[a-z]' '[A-Z]')
+  # shellcheck disable=SC2018,SC2019
+  Lx_domaintoken=$(echo LEXICON_"${PROVIDER}"_DOMAINTOKEN | tr 'a-z' 'A-Z')
   Lx_domaintoken_v=$(eval echo \$"$Lx_domaintoken")
-  _debug "$Lx_domaintoken" "$Lx_domaintoken_v"
+  _secure_debug "$Lx_domaintoken" "$Lx_domaintoken_v"
   if [ "$Lx_domaintoken_v" ]; then
     eval export "$Lx_domaintoken"
     _saveaccountconf "$Lx_domaintoken" "$Lx_domaintoken_v"

+ 183 - 0
dnsapi/dns_linode.sh

@@ -0,0 +1,183 @@
+#!/usr/bin/env sh
+
+#Author: Philipp Grosswiler <philipp.grosswiler@swiss-design.net>
+
+LINODE_API_URL="https://api.linode.com/?api_key=$LINODE_API_KEY&api_action="
+
+########  Public functions #####################
+
+#Usage: dns_linode_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_linode_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"
+
+  _parameters="&DomainID=$_domain_id&Type=TXT&Name=$_sub_domain&Target=$txtvalue"
+
+  if _rest GET "domain.resource.create" "$_parameters" && [ -n "$response" ]; then
+    _resource_id=$(printf "%s\n" "$response" | _egrep_o "\"ResourceID\":\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_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"
+
+  _parameters="&DomainID=$_domain_id"
+
+  if _rest GET "domain.resource.list" "$_parameters" && [ -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 "\"RESOURCEID\":\s*[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ )
+      if [ "$_resource_id" ]; then
+        _debug _resource_id "$_resource_id"
+
+        _parameters="&DomainID=$_domain_id&ResourceID=$_resource_id"
+
+        if _rest GET "domain.resource.delete" "$_parameters" && [ -n "$response" ]; then
+          _resource_id=$(printf "%s\n" "$response" | _egrep_o "\"ResourceID\":\s*[0-9]+" | cut -d : -f 2 | tr -d " " | _head_n 1)
+          _debug _resource_id "$_resource_id"
+
+          if [ -z "$_resource_id" ]; then
+            _err "Error deleting the domain resource."
+            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_API_KEY" ]; then
+    LINODE_API_KEY=""
+
+    _err "You didn't specify the Linode API key yet."
+    _err "Please create your key and try again."
+
+    return 1
+  fi
+
+  _saveaccountconf LINODE_API_KEY "$LINODE_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 "domain.list"; 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 "\"DOMAINID\":\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"
+
+  if [ "$mtd" != "GET" ]; then
+    # both POST and DELETE.
+    _debug data "$data"
+    response="$(_post "$data" "$LINODE_API_URL$ep" "" "$mtd")"
+  else
+    response="$(_get "$LINODE_API_URL$ep$data")"
+  fi
+
+  if [ "$?" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}

+ 40 - 9
dnsapi/dns_lua.sh

@@ -46,12 +46,12 @@ dns_lua_add() {
     return 1
   fi
 
-  count=$(printf "%s\n" "$response" | _egrep_o "\"name\":\"$fulldomain\"" | wc -l)
+  count=$(printf "%s\n" "$response" | _egrep_o "\"name\":\"$fulldomain.\",\"type\":\"TXT\"" | wc -l | tr -d " ")
   _debug count "$count"
   if [ "$count" = "0" ]; then
     _info "Adding record"
     if _LUA_rest POST "zones/$_domain_id/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"
         #todo: check if the record takes effect
         return 0
@@ -63,11 +63,11 @@ dns_lua_add() {
     _err "Add txt record error."
   else
     _info "Updating record"
-    record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[^,]*,\"name\":\"$fulldomain.\",\"type\":\"TXT\"" | cut -d: -f2 | cut -d, -f1)
+    record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[^,]*,\"name\":\"$fulldomain.\",\"type\":\"TXT\"" | _head_n 1 | cut -d: -f2 | cut -d, -f1)
     _debug "record_id" "$record_id"
 
-    _LUA_rest PUT "zones/$_domain_id/records/$record_id" "{\"id\":\"$record_id\",\"type\":\"TXT\",\"name\":\"$fulldomain.\",\"content\":\"$txtvalue\",\"zone_id\":\"$_domain_id\",\"ttl\":120}"
-    if [ "$?" = "0" ]; then
+    _LUA_rest PUT "zones/$_domain_id/records/$record_id" "{\"id\":$record_id,\"type\":\"TXT\",\"name\":\"$fulldomain.\",\"content\":\"$txtvalue\",\"zone_id\":$_domain_id,\"ttl\":120}"
+    if [ "$?" = "0" ] && _contains "$response" "updated_at"; then
       _info "Updated!"
       #todo: check if the record takes effect
       return 0
@@ -81,10 +81,39 @@ dns_lua_add() {
 #fulldomain
 dns_lua_rm() {
   fulldomain=$1
+  txtvalue=$2
+  _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"
+  _LUA_rest GET "zones/${_domain_id}/records"
+
+  count=$(printf "%s\n" "$response" | _egrep_o "\"name\":\"$fulldomain.\",\"type\":\"TXT\"" | wc -l | tr -d " ")
+  _debug count "$count"
+  if [ "$count" = "0" ]; then
+    _info "Don't need to remove."
+  else
+    record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[^,]*,\"name\":\"$fulldomain.\",\"type\":\"TXT\"" | _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 ! _LUA_rest DELETE "/zones/$_domain_id/records/$record_id"; then
+      _err "Delete record error."
+      return 1
+    fi
+    _contains "$response" "$record_id"
+  fi
 }
 
-####################  Private functions bellow ##################################
+####################  Private functions below ##################################
 #_acme-challenge.www.domain.com
 #returns
 # _sub_domain=_acme-challenge.www
@@ -99,6 +128,7 @@ _get_root() {
   fi
   while true; do
     h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    _debug h "$h"
     if [ -z "$h" ]; then
       #not valid
       return 1
@@ -106,6 +136,7 @@ _get_root() {
 
     if _contains "$response" "\"name\":\"$h\""; then
       _domain_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[^,]*,\"name\":\"$h\"" | cut -d : -f 2 | cut -d , -f 1)
+      _debug _domain_id "$_domain_id"
       if [ "$_domain_id" ]; then
         _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
         _domain="$h"
@@ -125,9 +156,9 @@ _LUA_rest() {
   data="$3"
   _debug "$ep"
 
-  _H1="Accept: application/json"
-  _H2="Authorization: Basic $LUA_auth"
-  if [ "$data" ]; then
+  export _H1="Accept: application/json"
+  export _H2="Authorization: Basic $LUA_auth"
+  if [ "$m" != "GET" ]; then
     _debug data "$data"
     response="$(_post "$data" "$LUA_Api/$ep" "" "$m")"
   else

+ 36 - 7
dnsapi/dns_me.sh

@@ -78,10 +78,39 @@ dns_me_add() {
 #fulldomain
 dns_me_rm() {
   fulldomain=$1
+  txtvalue=$2
+  _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"
+  _me_rest GET "${_domain_id}/records?recordName=$_sub_domain&type=TXT"
 
+  count=$(printf "%s\n" "$response" | _egrep_o "\"totalRecords\":[^,]*" | cut -d : -f 2)
+  _debug count "$count"
+  if [ "$count" = "0" ]; then
+    _info "Don't need to remove."
+  else
+    record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[^,]*" | cut -d : -f 2 | 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 ! _me_rest DELETE "$_domain_id/records/$record_id"; then
+      _err "Delete record error."
+      return 1
+    fi
+    _contains "$response" ''
+  fi
 }
 
-####################  Private functions bellow ##################################
+####################  Private functions below ##################################
 #_acme-challenge.www.domain.com
 #returns
 # _sub_domain=_acme-challenge.www
@@ -103,7 +132,7 @@ _get_root() {
     fi
 
     if _contains "$response" "\"name\":\"$h\""; then
-      _domain_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[^,]*" | head -n 1 | cut -d : -f 2)
+      _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"
@@ -124,13 +153,13 @@ _me_rest() {
   _debug "$ep"
 
   cdate=$(date -u +"%a, %d %b %Y %T %Z")
-  hmac=$(printf "%s" "$cdate" | _hmac sha1 "$(_hex "$ME_Secret")" hex)
+  hmac=$(printf "%s" "$cdate" | _hmac sha1 "$(printf "%s" "$ME_Secret" | _hex_dump | tr -d " ")" hex)
 
-  _H1="x-dnsme-apiKey: $ME_Key"
-  _H2="x-dnsme-requestDate: $cdate"
-  _H3="x-dnsme-hmac: $hmac"
+  export _H1="x-dnsme-apiKey: $ME_Key"
+  export _H2="x-dnsme-requestDate: $cdate"
+  export _H3="x-dnsme-hmac: $hmac"
 
-  if [ "$data" ]; then
+  if [ "$m" != "GET" ]; then
     _debug data "$data"
     response="$(_post "$data" "$ME_Api/$ep" "" "$m")"
   else

+ 14 - 31
dnsapi/dns_myapi.sh

@@ -5,48 +5,31 @@
 #So, here must be a method   dns_myapi_add()
 #Which will be called by acme.sh to add the txt record to your api system.
 #returns 0 means success, otherwise error.
-
+#
+#Author: Neilpang
+#Report Bugs here: https://github.com/Neilpang/acme.sh
+#
 ########  Public functions #####################
 
 #Usage: dns_myapi_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
 dns_myapi_add() {
   fulldomain=$1
   txtvalue=$2
+  _info "Using myapi"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
   _err "Not implemented!"
   return 1
 }
 
-#fulldomain
+#Usage: fulldomain txtvalue
+#Remove the txt record after validation.
 dns_myapi_rm() {
   fulldomain=$1
-
-}
-
-####################  Private functions bellow ##################################
-_info() {
-  if [ -z "$2" ]; then
-    echo "[$(date)] $1"
-  else
-    echo "[$(date)] $1='$2'"
-  fi
-}
-
-_err() {
-  _info "$@" >&2
-  return 1
-}
-
-_debug() {
-  if [ -z "$DEBUG" ]; then
-    return
-  fi
-  _err "$@"
-  return 0
+  txtvalue=$2
+  _info "Using myapi"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
 }
 
-_debug2() {
-  if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then
-    _debug "$@"
-  fi
-  return
-}
+####################  Private functions below ##################################

+ 193 - 0
dnsapi/dns_namecom.sh

@@ -0,0 +1,193 @@
+#!/usr/bin/env sh
+
+#Author: RaidneII
+#Created 06/28/2017
+#Utilize name.com API to finish dns-01 verifications.
+########  Public functions #####################
+
+Namecom_API="https://api.name.com/api"
+
+#Usage: dns_namecom_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_namecom_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  # First we need name.com credentials.
+  if [ -z "$Namecom_Username" ]; then
+    Namecom_Username=""
+    _err "Username for name.com is missing."
+    _err "Please specify that in your environment variable."
+    return 1
+  fi
+
+  if [ -z "$Namecom_Token" ]; then
+    Namecom_Token=""
+    _err "API token for name.com is missing."
+    _err "Please specify that in your environment variable."
+    return 1
+  fi
+
+  # Save them in configuration.
+  _saveaccountconf Namecom_Username "$Namecom_Username"
+  _saveaccountconf Namecom_Token "$Namecom_Token"
+
+  # Login in using API
+  if ! _namecom_login; then
+    return 1
+  fi
+
+  # Find domain in domain list.
+  if ! _namecom_get_root "$fulldomain"; then
+    _err "Unable to find domain specified."
+    _namecom_logout
+    return 1
+  fi
+
+  # Add TXT record.
+  _namecom_addtxt_json="{\"hostname\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":\"300\",\"priority\":\"10\"}"
+  if _namecom_rest POST "dns/create/$_domain" "$_namecom_addtxt_json"; then
+    retcode=$(printf "%s\n" "$response" | _egrep_o "\"code\":100")
+    if [ "$retcode" ]; then
+      _info "Successfully added TXT record, ready for validation."
+      _namecom_logout
+      return 0
+    else
+      _err "Unable to add the DNS record."
+      _namecom_logout
+      return 1
+    fi
+  fi
+}
+
+#Usage: fulldomain txtvalue
+#Remove the txt record after validation.
+dns_namecom_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if ! _namecom_login; then
+    return 1
+  fi
+
+  # Find domain in domain list.
+  if ! _namecom_get_root "$fulldomain"; then
+    _err "Unable to find domain specified."
+    _namecom_logout
+    return 1
+  fi
+
+  # Get the record id.
+  if _namecom_rest GET "dns/list/$_domain"; then
+    retcode=$(printf "%s\n" "$response" | _egrep_o "\"code\":100")
+    if [ "$retcode" ]; then
+      _record_id=$(printf "%s\n" "$response" | _egrep_o "\"record_id\":\"[0-9]+\",\"name\":\"$fulldomain\",\"type\":\"TXT\"" | cut -d \" -f 4)
+      _debug record_id "$_record_id"
+      _info "Successfully retrieved the record id for ACME challenge."
+    else
+      _err "Unable to retrieve the record id."
+      _namecom_logout
+      return 1
+    fi
+  fi
+
+  # Remove the DNS record using record id.
+  _namecom_rmtxt_json="{\"record_id\":\"$_record_id\"}"
+  if _namecom_rest POST "dns/delete/$_domain" "$_namecom_rmtxt_json"; then
+    retcode=$(printf "%s\n" "$response" | _egrep_o "\"code\":100")
+    if [ "$retcode" ]; then
+      _info "Successfully removed the TXT record."
+      _namecom_logout
+      return 0
+    else
+      _err "Unable to remove the DNS record."
+      _namecom_logout
+      return 1
+    fi
+  fi
+}
+
+####################  Private functions below ##################################
+_namecom_rest() {
+  method=$1
+  param=$2
+  data=$3
+
+  export _H1="Content-Type: application/json"
+  export _H2="Api-Session-Token: $sessionkey"
+  if [ "$method" != "GET" ]; then
+    response="$(_post "$data" "$Namecom_API/$param" "" "$method")"
+  else
+    response="$(_get "$Namecom_API/$param")"
+  fi
+
+  if [ "$?" != "0" ]; then
+    _err "error $param"
+    return 1
+  fi
+
+  _debug2 response "$response"
+  return 0
+}
+
+_namecom_login() {
+  namecom_login_json="{\"username\":\"$Namecom_Username\",\"api_token\":\"$Namecom_Token\"}"
+
+  if _namecom_rest POST "login" "$namecom_login_json"; then
+    retcode=$(printf "%s\n" "$response" | _egrep_o "\"code\":100")
+    if [ "$retcode" ]; then
+      _info "Successfully logged in. Fetching session token..."
+      sessionkey=$(printf "%s\n" "$response" | _egrep_o "\"session_token\":\".+" | cut -d \" -f 4)
+      if [ ! -z "$sessionkey" ]; then
+        _debug sessionkey "$sessionkey"
+        _info "Session key obtained."
+      else
+        _err "Unable to get session key."
+        return 1
+      fi
+    else
+      _err "Logging in failed."
+      return 1
+    fi
+  fi
+}
+
+_namecom_logout() {
+  if _namecom_rest GET "logout"; then
+    retcode=$(printf "%s\n" "$response" | _egrep_o "\"code\":100")
+    if [ "$retcode" ]; then
+      _info "Successfully logged out."
+    else
+      _err "Error logging out."
+      return 1
+    fi
+  fi
+}
+
+_namecom_get_root() {
+  domain=$1
+  i=2
+  p=1
+
+  if ! _namecom_rest GET "domain/list"; then
+    return 1
+  fi
+
+  # Need to exclude the last field (tld)
+  numfields=$(echo "$domain" | _egrep_o "\." | wc -l)
+  while [ $i -le "$numfields" ]; do
+    host=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    _debug host "$host"
+    if [ -z "$host" ]; then
+      return 1
+    fi
+
+    if _contains "$response" "$host"; then
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      _domain="$host"
+      return 0
+    fi
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+  return 1
+}

+ 158 - 0
dnsapi/dns_nsone.sh

@@ -0,0 +1,158 @@
+#!/usr/bin/env sh
+
+# bug reports to dev@1e.ca
+
+#
+#NS1_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
+#
+
+NS1_Api="https://api.nsone.net/v1"
+
+########  Public functions #####################
+
+#Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_nsone_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if [ -z "$NS1_Key" ]; then
+    NS1_Key=""
+    _err "You didn't specify nsone dns api key yet."
+    _err "Please create you key and try again."
+    return 1
+  fi
+
+  #save the api key and email to the account conf file.
+  _saveaccountconf NS1_Key "$NS1_Key"
+
+  _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"
+  _nsone_rest GET "zones/${_domain}"
+
+  if ! _contains "$response" "\"records\":"; then
+    _err "Error"
+    return 1
+  fi
+
+  count=$(printf "%s\n" "$response" | _egrep_o "\"domain\":\"$fulldomain\",[^{]*\"type\":\"TXT\"" | wc -l | tr -d " ")
+  _debug count "$count"
+  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 _contains "$response" "$fulldomain"; then
+        _info "Added"
+        #todo: check if the record takes effect
+        return 0
+      else
+        _err "Add txt record error."
+        return 1
+      fi
+    fi
+    _err "Add txt record error."
+  else
+    _info "Updating record"
+    record_id=$(printf "%s\n" "$response" | _egrep_o "\"domain\":\"$fulldomain.\",[^{]*\"type\":\"TXT\",\"id\":\"[^,]*\"" | _head_n 1 | cut -d: -f7 | cut -d, -f1)
+    _debug "record_id" "$record_id"
+
+    _nsone_rest POST "zones/$_domain/$fulldomain/TXT" "{\"answers\": [{\"answer\": [\"$txtvalue\"]}],\"type\": \"TXT\",\"domain\":\"$fulldomain\",\"zone\": \"$_domain\"}"
+    if [ "$?" = "0" ] && _contains "$response" "$fulldomain"; then
+      _info "Updated!"
+      #todo: check if the record takes effect
+      return 0
+    fi
+    _err "Update error"
+    return 1
+  fi
+
+}
+
+#fulldomain
+dns_nsone_rm() {
+  fulldomain=$1
+  txtvalue=$2
+  _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"
+  _nsone_rest GET "zones/${_domain}/$fulldomain/TXT"
+
+  count=$(printf "%s\n" "$response" | _egrep_o "\"domain\":\"$fulldomain\",.*\"type\":\"TXT\"" | wc -l | tr -d " ")
+  _debug count "$count"
+  if [ "$count" = "0" ]; then
+    _info "Don't need to remove."
+  else
+    if ! _nsone_rest DELETE "zones/${_domain}/$fulldomain/TXT"; then
+      _err "Delete record error."
+      return 1
+    fi
+    _contains "$response" ""
+  fi
+}
+
+####################  Private functions below ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+# _domain_id=sdjkglgdfewsdfg
+_get_root() {
+  domain=$1
+  i=2
+  p=1
+  if ! _nsone_rest GET "zones"; then
+    return 1
+  fi
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    _debug h "$h"
+    if [ -z "$h" ]; then
+      #not valid
+      return 1
+    fi
+
+    if _contains "$response" "\"zone\":\"$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
+}
+
+_nsone_rest() {
+  m=$1
+  ep="$2"
+  data="$3"
+  _debug "$ep"
+
+  export _H1="Accept: application/json"
+  export _H2="X-NSONE-Key: $NS1_Key"
+  if [ "$m" != "GET" ]; then
+    _debug data "$data"
+    response="$(_post "$data" "$NS1_Api/$ep" "" "$m")"
+  else
+    response="$(_get "$NS1_Api/$ep")"
+  fi
+
+  if [ "$?" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}

+ 1 - 1
dnsapi/dns_nsupdate.sh

@@ -44,7 +44,7 @@ EOF
   return 0
 }
 
-####################  Private functions bellow ##################################
+####################  Private functions below ##################################
 
 _checkKeyFile() {
   if [ -z "${NSUPDATE_KEY}" ]; then

+ 13 - 13
dnsapi/dns_ovh.sh

@@ -1,6 +1,6 @@
 #!/usr/bin/env sh
 
-#Applcation Key
+#Application Key
 #OVH_AK="sdfsdfsdfljlbjkljlkjsdfoiwje"
 #
 #Application Secret
@@ -14,7 +14,7 @@
 #'ovh-eu'
 OVH_EU='https://eu.api.ovh.com/1.0'
 
-#'ovh-ca': 
+#'ovh-ca':
 OVH_CA='https://ca.api.ovh.com/1.0'
 
 #'kimsufi-eu'
@@ -119,7 +119,7 @@ dns_ovh_add() {
 
   _info "Checking authentication"
 
-  response="$(_ovh_rest GET "domain/")"
+  response="$(_ovh_rest GET "domain")"
   if _contains "$response" "INVALID_CREDENTIAL"; then
     _err "The consumer key is invalid: $OVH_CK"
     _err "Please retry to create a new one."
@@ -182,7 +182,7 @@ dns_ovh_rm() {
 
 }
 
-####################  Private functions bellow ##################################
+####################  Private functions below ##################################
 
 _ovh_authentication() {
 
@@ -191,7 +191,7 @@ _ovh_authentication() {
   _H3=""
   _H4=""
 
-  _ovhdata='{"accessRules": [{"method": "GET","path": "/*"},{"method": "POST","path": "/*"},{"method": "PUT","path": "/*"},{"method": "DELETE","path": "/*"}],"redirection":"'$ovh_success'"}'
+  _ovhdata='{"accessRules": [{"method": "GET","path": "/auth/time"},{"method": "GET","path": "/domain"},{"method": "GET","path": "/domain/zone/*"},{"method": "GET","path": "/domain/zone/*/record"},{"method": "POST","path": "/domain/zone/*/record"},{"method": "POST","path": "/domain/zone/*/refresh"},{"method": "PUT","path": "/domain/zone/*/record/*"}],"redirection":"'$ovh_success'"}'
 
   response="$(_post "$_ovhdata" "$OVH_API/auth/credential")"
   _debug3 response "$response"
@@ -207,7 +207,7 @@ _ovh_authentication() {
     _err "Unable to get consumerKey"
     return 1
   fi
-  _debug consumerKey "$consumerKey"
+  _secure_debug consumerKey "$consumerKey"
 
   OVH_CK="$consumerKey"
   _saveaccountconf OVH_CK "$OVH_CK"
@@ -238,7 +238,7 @@ _get_root() {
       return 1
     fi
 
-    if ! _contains "$response" "This service does not exist" >/dev/null; then
+    if ! _contains "$response" "This service does not exist" >/dev/null && ! _contains "$response" "NOT_GRANTED_CALL" >/dev/null; then
       _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
       _domain="$h"
       return 0
@@ -269,16 +269,16 @@ _ovh_rest() {
   _ovh_t="$(_ovh_timestamp)"
   _debug2 _ovh_t "$_ovh_t"
   _ovh_p="$OVH_AS+$OVH_CK+$m+$_ovh_url+$data+$_ovh_t"
-  _debug _ovh_p "$_ovh_p"
+  _secure_debug _ovh_p "$_ovh_p"
   _ovh_hex="$(printf "%s" "$_ovh_p" | _digest sha1 hex)"
   _debug2 _ovh_hex "$_ovh_hex"
 
-  _H1="X-Ovh-Application: $OVH_AK"
-  _H2="X-Ovh-Signature: \$1\$$_ovh_hex"
+  export _H1="X-Ovh-Application: $OVH_AK"
+  export _H2="X-Ovh-Signature: \$1\$$_ovh_hex"
   _debug2 _H2 "$_H2"
-  _H3="X-Ovh-Timestamp: $_ovh_t"
-  _H4="X-Ovh-Consumer: $OVH_CK"
-  _H5="Content-Type: application/json;charset=utf-8"
+  export _H3="X-Ovh-Timestamp: $_ovh_t"
+  export _H4="X-Ovh-Consumer: $OVH_CK"
+  export _H5="Content-Type: application/json;charset=utf-8"
   if [ "$data" ] || [ "$m" = "POST" ] || [ "$m" = "PUT" ]; then
     _debug data "$data"
     response="$(_post "$data" "$_ovh_url" "" "$m")"

+ 3 - 3
dnsapi/dns_pdns.sh

@@ -1,6 +1,6 @@
 #!/usr/bin/env sh
 
-#PowerDNS Emdedded API
+#PowerDNS Embedded API
 #https://doc.powerdns.com/md/httpapi/api_spec/
 #
 #PDNS_Url="http://ns.example.com:8081"
@@ -130,7 +130,7 @@ notify_slaves() {
   return 0
 }
 
-####################  Private functions bellow ##################################
+####################  Private functions below ##################################
 #_acme-challenge.www.domain.com
 #returns
 # _domain=domain.com
@@ -165,7 +165,7 @@ _pdns_rest() {
   ep=$2
   data=$3
 
-  _H1="X-API-Key: $PDNS_Token"
+  export _H1="X-API-Key: $PDNS_Token"
 
   if [ ! "$method" = "GET" ]; then
     _debug data "$data"

+ 170 - 0
dnsapi/dns_servercow.sh

@@ -0,0 +1,170 @@
+#!/usr/bin/env sh
+
+##########
+# Custom servercow.de DNS API v1 for use with [acme.sh](https://github.com/Neilpang/acme.sh)
+#
+# Usage:
+# export SERVERCOW_API_Username=username
+# export SERVERCOW_API_Password=password
+# acme.sh --issue -d example.com --dns dns_servercow
+#
+# Issues:
+# Any issues / questions / suggestions can be posted here:
+# https://github.com/jhartlep/servercow-dns-api/issues
+#
+# Author: Jens Hartlep
+##########
+
+SERVERCOW_API="https://api.servercow.de/dns/v1/domains"
+
+# Usage dns_servercow_add _acme-challenge.www.domain.com "abcdefghijklmnopqrstuvwxyz"
+dns_servercow_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  _info "Using servercow"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  SERVERCOW_API_Username="${SERVERCOW_API_Username:-$(_readaccountconf_mutable SERVERCOW_API_Username)}"
+  SERVERCOW_API_Password="${SERVERCOW_API_Password:-$(_readaccountconf_mutable SERVERCOW_API_Password)}"
+  if [ -z "$SERVERCOW_API_Username" ] || [ -z "$SERVERCOW_API_Password" ]; then
+    SERVERCOW_API_Username=""
+    SERVERCOW_API_Password=""
+    _err "You don't specify servercow api username and password yet."
+    _err "Please create your username and password and try again."
+    return 1
+  fi
+
+  # save the credentials to the account conf file
+  _saveaccountconf_mutable SERVERCOW_API_Username "$SERVERCOW_API_Username"
+  _saveaccountconf_mutable SERVERCOW_API_Password "$SERVERCOW_API_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 _servercow_api POST "$_domain" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":20}"; 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_servercow_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  _info "Using servercow"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$fulldomain"
+
+  SERVERCOW_API_Username="${SERVERCOW_API_Username:-$(_readaccountconf_mutable SERVERCOW_API_Username)}"
+  SERVERCOW_API_Password="${SERVERCOW_API_Password:-$(_readaccountconf_mutable SERVERCOW_API_Password)}"
+  if [ -z "$SERVERCOW_API_Username" ] || [ -z "$SERVERCOW_API_Password" ]; then
+    SERVERCOW_API_Username=""
+    SERVERCOW_API_Password=""
+    _err "You don't specify servercow api username and password yet."
+    _err "Please create your username and 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 _servercow_api DELETE "$_domain" "{\"type\":\"TXT\",\"name\":\"$fulldomain\"}"; then
+    if printf -- "%s" "$response" | grep "ok" >/dev/null; then
+      _info "Deleted, OK"
+      _contains "$response" '"message":"ok"'
+    else
+      _err "delete txt record error."
+      return 1
+    fi
+  fi
+
+}
+
+####################  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
+
+  while true; do
+    _domain=$(printf "%s" "$fulldomain" | cut -d . -f $i-100)
+
+    _debug _domain "$_domain"
+    if [ -z "$_domain" ]; then
+      # not valid
+      return 1
+    fi
+
+    if ! _servercow_api GET "$_domain"; then
+      return 1
+    fi
+
+    if ! _contains "$response" '"error":"no such domain in user context"' >/dev/null; then
+      _sub_domain=$(printf "%s" "$fulldomain" | cut -d . -f 1-$p)
+      if [ -z "$_sub_domain" ]; then
+        # not valid
+        return 1
+      fi
+
+      return 0
+    fi
+
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+
+  return 1
+}
+
+_servercow_api() {
+  method=$1
+  domain=$2
+  data="$3"
+
+  export _H1="Content-Type: application/json"
+  export _H2="X-Auth-Username: $SERVERCOW_API_Username"
+  export _H3="X-Auth-Password: $SERVERCOW_API_Password"
+
+  if [ "$method" != "GET" ]; then
+    _debug data "$data"
+    response="$(_post "$data" "$SERVERCOW_API/$domain" "" "$method")"
+  else
+    response="$(_get "$SERVERCOW_API/$domain")"
+  fi
+
+  if [ "$?" != "0" ]; then
+    _err "error $domain"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}

+ 202 - 0
dnsapi/dns_unoeuro.sh

@@ -0,0 +1,202 @@
+#!/usr/bin/env sh
+
+#
+#UNO_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
+#
+#UNO_User="UExxxxxx"
+
+Uno_Api="https://api.unoeuro.com/1"
+
+########  Public functions #####################
+
+#Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_unoeuro_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  UNO_Key="${UNO_Key:-$(_readaccountconf_mutable UNO_Key)}"
+  UNO_User="${UNO_User:-$(_readaccountconf_mutable UNO_User)}"
+  if [ -z "$UNO_Key" ] || [ -z "$UNO_User" ]; then
+    UNO_Key=""
+    UNO_User=""
+    _err "You haven't specified a UnoEuro api key and account yet."
+    _err "Please create your key and try again."
+    return 1
+  fi
+
+  if ! _contains "$UNO_User" "UE"; then
+    _err "It seems that the UNO_User=$UNO_User is not a valid username."
+    _err "Please check and retry."
+    return 1
+  fi
+
+  #save the api key and email to the account conf file.
+  _saveaccountconf_mutable UNO_Key "$UNO_Key"
+  _saveaccountconf_mutable UNO_User "$UNO_User"
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+  _debug _domain_id "$_domain_id"
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  _debug "Getting txt records"
+  _uno_rest GET "my/products/$h/dns/records"
+
+  if ! _contains "$response" "\"status\": 200" >/dev/null; then
+    _err "Error"
+    return 1
+  fi
+
+  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 _contains "$response" "\"status\": 200" >/dev/null; then
+      _info "Updated, OK"
+      return 0
+    fi
+    _err "Update error"
+    return 1
+  fi
+}
+
+#fulldomain txtvalue
+dns_unoeuro_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  UNO_Key="${UNO_Key:-$(_readaccountconf_mutable UNO_Key)}"
+  UNO_User="${UNO_User:-$(_readaccountconf_mutable UNO_User)}"
+  if [ -z "$UNO_Key" ] || [ -z "$UNO_User" ]; then
+    UNO_Key=""
+    UNO_User=""
+    _err "You haven't specified a UnoEuro api key and account yet."
+    _err "Please create your key and try again."
+    return 1
+  fi
+
+  if ! _contains "$UNO_User" "UE"; then
+    _err "It seems that the UNO_User=$UNO_User is not a valid username."
+    _err "Please check and retry."
+    return 1
+  fi
+
+  _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"
+  _uno_rest GET "my/products/$h/dns/records"
+
+  if ! _contains "$response" "\"status\": 200"; then
+    _err "Error"
+    return 1
+  fi
+
+  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
+
+    if ! _uno_rest DELETE "my/products/$h/dns/records/$record_id"; then
+      _err "Delete record error."
+      return 1
+    fi
+    _contains "$response" "\"status\": 200"
+  fi
+
+}
+
+####################  Private functions below ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+# _domain_id=sdjkglgdfewsdfg
+_get_root() {
+  domain=$1
+  i=2
+  p=1
+  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 ! _uno_rest GET "my/products/$h/dns/records"; then
+      return 1
+    fi
+
+    if _contains "$response" "\"status\": 200"; then
+      _domain_id=$h
+      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
+}
+
+_uno_rest() {
+  m=$1
+  ep="$2"
+  data="$3"
+  _debug "$ep"
+
+  export _H1="Content-Type: application/json"
+
+  if [ "$m" != "GET" ]; then
+    _debug data "$data"
+    response="$(_post "$data" "$Uno_Api/$UNO_User/$UNO_Key/$ep" "" "$m")"
+  else
+    response="$(_get "$Uno_Api/$UNO_User/$UNO_Key/$ep")"
+  fi
+
+  if [ "$?" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}

+ 149 - 0
dnsapi/dns_vscale.sh

@@ -0,0 +1,149 @@
+#!/usr/bin/env sh
+
+#This is the vscale.io api wrapper for acme.sh
+#
+#Author: Alex Loban
+#Report Bugs here: https://github.com/LAV45/acme.sh
+
+#VSCALE_API_KEY="sdfsdfsdfljlbjkljlkjsdfoiwje"
+VSCALE_API_URL="https://api.vscale.io/v1"
+
+########  Public functions #####################
+
+#Usage: dns_myapi_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_vscale_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if [ -z "$VSCALE_API_KEY" ]; then
+    VSCALE_API_KEY=""
+    _err "You didn't specify the VSCALE api key yet."
+    _err "Please create you key and try again."
+    return 1
+  fi
+
+  _saveaccountconf VSCALE_API_KEY "$VSCALE_API_KEY"
+
+  _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"
+
+  _vscale_tmpl_json="{\"type\":\"TXT\",\"name\":\"$_sub_domain.$_domain\",\"content\":\"$txtvalue\"}"
+
+  if _vscale_rest POST "domains/$_domain_id/records/" "$_vscale_tmpl_json"; then
+    response=$(printf "%s\n" "$response" | _egrep_o "{\"error\": \".+\"" | cut -d : -f 2)
+    if [ -z "$response" ]; then
+      _info "txt record updated success."
+      return 0
+    fi
+  fi
+
+  return 1
+}
+
+#fulldomain txtvalue
+dns_vscale_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  _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"
+  _vscale_rest GET "domains/$_domain_id/records/"
+
+  if [ -n "$response" ]; then
+    record_id=$(printf "%s\n" "$response" | _egrep_o "\"TXT\", \"id\": [0-9]+, \"name\": \"$_sub_domain.$_domain\"" | cut -d : -f 2 | tr -d ", \"name\"")
+    _debug record_id "$record_id"
+    if [ -z "$record_id" ]; then
+      _err "Can not get record id to remove."
+      return 1
+    fi
+    if _vscale_rest DELETE "domains/$_domain_id/records/$record_id" && [ -z "$response" ]; then
+      _info "txt record deleted success."
+      return 0
+    fi
+    _debug response "$response"
+    return 1
+  fi
+
+  return 1
+}
+
+####################  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 _vscale_rest GET "domains/"; then
+    response="$(echo "$response" | tr -d "\n" | sed 's/{/\n&/g')"
+    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 "{.*\"name\":\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 uri qstr data
+_vscale_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="X-Token: ${VSCALE_API_KEY}"
+
+  if [ "$mtd" != "GET" ]; then
+    # both POST and DELETE.
+    _debug data "$data"
+    response="$(_post "$data" "$VSCALE_API_URL/$ep" "" "$mtd")"
+  else
+    response="$(_get "$VSCALE_API_URL/$ep")"
+  fi
+
+  if [ "$?" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}

+ 107 - 0
dnsapi/dns_yandex.sh

@@ -0,0 +1,107 @@
+#!/usr/bin/env sh
+# Author: non7top@gmail.com
+# 07 Jul 2017
+# report bugs at https://github.com/non7top/acme.sh
+
+# Values to export:
+# export PDD_Token="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+
+########  Public functions #####################
+
+#Usage: dns_myapi_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_yandex_add() {
+  fulldomain="${1}"
+  txtvalue="${2}"
+  _debug "Calling: dns_yandex_add() '${fulldomain}' '${txtvalue}'"
+  _PDD_credentials || return 1
+  export _H1="PddToken: $PDD_Token"
+
+  curDomain=$(_PDD_get_domain "$fulldomain")
+  _debug "Found suitable domain in pdd: $curDomain"
+  curSubdomain="$(echo "${fulldomain}" | sed -e "s@.${curDomain}\$@@")"
+  curData="domain=${curDomain}&type=TXT&subdomain=${curSubdomain}&ttl=360&content=${txtvalue}"
+  curUri="https://pddimp.yandex.ru/api2/admin/dns/add"
+  curResult="$(_post "${curData}" "${curUri}")"
+  _debug "Result: $curResult"
+}
+
+#Usage: dns_myapi_rm   _acme-challenge.www.domain.com
+dns_yandex_rm() {
+  fulldomain="${1}"
+  _debug "Calling: dns_yandex_rm() '${fulldomain}'"
+  _PDD_credentials || return 1
+  export _H1="PddToken: $PDD_Token"
+  record_id=$(pdd_get_record_id "${fulldomain}")
+  _debug "Result: $record_id"
+
+  curDomain=$(_PDD_get_domain "$fulldomain")
+  _debug "Found suitable domain in pdd: $curDomain"
+  curSubdomain="$(echo "${fulldomain}" | sed -e "s@.${curDomain}\$@@")"
+
+  curUri="https://pddimp.yandex.ru/api2/admin/dns/del"
+  curData="domain=${curDomain}&record_id=${record_id}"
+  curResult="$(_post "${curData}" "${curUri}")"
+  _debug "Result: $curResult"
+}
+
+####################  Private functions below ##################################
+
+_PDD_get_domain() {
+  fulldomain="${1}"
+  __page=1
+  __last=0
+  while [ $__last -eq 0 ]; do
+    uri1="https://pddimp.yandex.ru/api2/admin/domain/domains?page=${__page}&on_page=20"
+    res1=$(_get "$uri1" | _normalizeJson)
+    #_debug "$res1"
+    __found=$(echo "$res1" | sed -n -e 's#.* "found": \([^,]*\),.*#\1#p')
+    _debug "found: $__found results on page"
+    if [ "$__found" -lt 20 ]; then
+      _debug "last page: $__page"
+      __last=1
+    fi
+
+    __all_domains="$__all_domains $(echo "$res1" | sed -e "s@,@\n@g" | grep '"name"' | cut -d: -f2 | sed -e 's@"@@g')"
+
+    __page=$(_math $__page + 1)
+  done
+
+  k=2
+  while [ $k -lt 10 ]; do
+    __t=$(echo "$fulldomain" | cut -d . -f $k-100)
+    _debug "finding zone for domain $__t"
+    for d in $__all_domains; do
+      if [ "$d" = "$__t" ]; then
+        echo "$__t"
+        return
+      fi
+    done
+    k=$(_math $k + 1)
+  done
+  _err "No suitable domain found in your account"
+  return 1
+}
+
+_PDD_credentials() {
+  if [ -z "${PDD_Token}" ]; then
+    PDD_Token=""
+    _err "You need to export PDD_Token=xxxxxxxxxxxxxxxxx"
+    _err "You can get it at https://pddimp.yandex.ru/api2/admin/get_token"
+    return 1
+  else
+    _saveaccountconf PDD_Token "${PDD_Token}"
+  fi
+}
+
+pdd_get_record_id() {
+  fulldomain="${1}"
+
+  curDomain=$(_PDD_get_domain "$fulldomain")
+  _debug "Found suitable domain in pdd: $curDomain"
+  curSubdomain="$(echo "${fulldomain}" | sed -e "s@.${curDomain}\$@@")"
+
+  curUri="https://pddimp.yandex.ru/api2/admin/dns/list?domain=${curDomain}"
+  curResult="$(_get "${curUri}" | _normalizeJson)"
+  _debug "Result: $curResult"
+  echo "$curResult" | _egrep_o "{[^{]*\"content\":[^{]*\"subdomain\":\"${curSubdomain}\"" | sed -n -e 's#.* "record_id": \(.*\),[^,]*#\1#p'
+}

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