Browse Source

Merge branch 'master' into dev

Pål Håland 7 years ago
parent
commit
aa875f1147

+ 9 - 15
.travis.yml

@@ -1,10 +1,14 @@
 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
@@ -18,20 +22,10 @@ addons:
 
 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 ACME_OPENSSL_BIN="/usr/local/openssl";
-      openssl version 2>&1 || true;
-      $ACME_OPENSSL_BIN version 2>&1 || true;
-      export PATH="$_old_path";
+      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
@@ -40,10 +34,10 @@ script:
   - 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 **/*.sh && echo "shellcheck OK" ; 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" ./letest.sh ; fi
+  - 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
 
 

+ 12 - 8
Dockerfile

@@ -1,22 +1,22 @@
-FROM alpine
+FROM alpine:3.6
 
 RUN apk update -f \
   && apk --no-cache add -f \
   openssl \
   curl \
-  netcat-openbsd
+  socat \
+  && rm -rf /var/cache/apk/*
 
 ENV LE_CONFIG_HOME /acme.sh
 
 ENV AUTO_UPGRADE 1
 
 #Install
-RUN mkdir -p /install_acme.sh/
 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)
-RUN rm -rf /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
+
+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 \
@@ -44,16 +44,20 @@ RUN for verb in help \
   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 \
- crond; tail -f /dev/null;\n \
+ trap \"echo stop && killall crond && exit 0\" SIGTERM SIGINT \n \
+ crond && while true; do sleep 1; done;\n \
 else \n \
- /root/.acme.sh/acme.sh --config-home /acme.sh \"\$@\"\n \
+ exec -- \"\$@\"\n \
 fi" >/entry.sh && chmod +x /entry.sh
 
+VOLUME /acme.sh
+
 ENTRYPOINT ["/entry.sh"]
 CMD ["--help"]

+ 134 - 86
README.md

@@ -1,6 +1,10 @@
 # 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.
+- Support ACME v1 and ACME v2
+- Support ACME v2 wildcard certs
 - Simple, powerful and very easy to use. You only need 3 minutes to learn it.
 - Bash, dash and sh compatible.
 - Simplest shell script for Let's Encrypt free certificate client.
@@ -8,6 +12,7 @@
 - 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 & smartest` shell script to automatically issue & renew the free certificates from Let's Encrypt.
 
@@ -20,7 +25,7 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
 
 # [中文说明](https://github.com/Neilpang/acme.sh/wiki/%E8%AF%B4%E6%98%8E)
 
-# Who are using **acme.sh**
+# Who:
 - [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))
@@ -32,6 +37,8 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
 - [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)
+- [CentOS Web Panel](http://centos-webpanel.com/)
+- [lnmp.org](https://lnmp.org/)
 - [more...](https://github.com/Neilpang/acme.sh/wiki/Blogs-and-tutorials)
 
 # Tested OS
@@ -59,7 +66,7 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
 |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
 
@@ -69,8 +76,9 @@ https://github.com/Neilpang/acmetest
 - Webroot mode
 - Standalone mode
 - Apache mode
-- Nginx mode ( Beta )
+- Nginx mode
 - DNS mode
+- [DNS alias mode](https://github.com/Neilpang/acme.sh/wiki/DNS-alias-mode)
 - [Stateless mode](https://github.com/Neilpang/acme.sh/wiki/Stateless-Mode)
 
 
@@ -124,7 +132,7 @@ Ok, you are ready to issue certs now.
 
 Show help message:
 
-```
+```sh
 root@v1:~# acme.sh -h
 ```
 
@@ -161,16 +169,16 @@ You must have at least one domain there.
 
 You must point and bind all the domains to the same webroot dir: `/home/wwwroot/example.com`.
 
-Generated/issued certs will be placed in `~/.acme.sh/example.com/`
+The certs will be placed in `~/.acme.sh/example.com/`
 
-The issued cert will be renewed automatically every **60** days.
+The certs will be renewed automatically every **60** days.
 
 More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert
 
 
-# 3. Install the issued cert to Apache/Nginx etc.
+# 3. Install the cert to Apache/Nginx etc.
 
-After you issue a cert, you probably want to install/copy the cert to your Apache/Nginx or other servers.
+After the cert is generated, you probably want to install/copy the cert to your Apache/Nginx or other servers.
 You **MUST** use this command to copy the certs to the target files, **DO NOT** use the certs files in **~/.acme.sh/** folder, they are for internal use only, the folder structure may change in the future.
 
 **Apache** example:
@@ -192,13 +200,15 @@ acme.sh --install-cert -d example.com \
 
 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.
+The ownership and permission info of existing files are preserved. You can pre-create the files to define the ownership and permission.
 
-Install/copy the issued cert/key to the production Apache or Nginx path.
+Install/copy the 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`.
 
 
+**Please take care:  The reloadcmd is very important. The cert can be automatically renewed, but, without a correct 'reloadcmd' the cert may not be flushed to your server(like nginx or apache), then your website will not be able to show renewed cert in 60 days.**
+
 # 4. Use Standalone server to issue cert
 
 **(requires you to be root/sudoer or have permission to listen on port 80 (TCP))**
@@ -212,38 +222,27 @@ acme.sh --issue --standalone -d example.com -d www.example.com -d cp.example.com
 More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert
 
 
-# 5. Use Standalone TLS server to issue cert
-
-**(requires you to be root/sudoer or have permission to listen on port 443 (TCP))**
-
-acme.sh supports `tls-sni-01` validation.
-
-Port `443` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again.
-
-```bash
-acme.sh --issue --tls -d example.com -d www.example.com -d cp.example.com
-```
-
-More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert
-
-
-# 6. Use Apache mode
+# 5. Use Apache mode
 
 **(requires you to be root/sudoer, since it is required to interact with Apache server)**
 
 If you are running a web server, Apache or Nginx, it is recommended to use the `Webroot mode`.
 
-Particularly, if you are running an Apache server, you should use Apache mode instead. This mode doesn't write any files to your web root folder.
+Particularly, if you are running an Apache server, you can use Apache mode instead. This mode doesn't write any files to your web root folder.
 
 Just set string "apache" as the second argument and it will force use of apache plugin automatically.
 
-```
+```sh
 acme.sh --issue --apache -d example.com -d www.example.com -d cp.example.com
 ```
 
+**This apache mode is only to issue the cert, it will not change your apache config files. 
+You will need to configure your website config files to use the cert by yourself.
+We don't want to mess your apache server, don't worry.**
+
 More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert
 
-# 7. Use Nginx mode
+# 6. Use Nginx mode
 
 **(requires you to be root/sudoer, since it is required to interact with Nginx server)**
 
@@ -257,44 +256,17 @@ It will configure nginx server automatically to verify the domain and then resto
 
 So, the config is not changed.
 
-```
+```sh
 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.
-
-```bash
-acme.sh --issue --dns -d example.com -d www.example.com -d cp.example.com
-```
-
-You should get an output like below:
-
-```
-Add the following txt record:
-Domain:_acme-challenge.example.com
-Txt value:9ihDbjYfTExAYeDs4DBUeuTo18KBzwvTEjUnSwd32-c
-
-Add the following txt record:
-Domain:_acme-challenge.www.example.com
-Txt value:9ihDbjxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-
-Please add those txt records to the domains. Waiting for the dns to take effect.
-```
-
-Then just rerun with `renew` argument:
-
-```bash
-acme.sh --renew -d example.com
-```
-
-Ok, it's finished.
+**This nginx mode is only to issue the cert, it will not change your nginx config files. 
+You will need to configure your website config files to use the cert by yourself.
+We don't want to mess your nginx server, don't worry.**
 
+More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert
 
-# 9. Automatic DNS API integration
+# 7. Automatic DNS API integration
 
 If your DNS provider supports API access, we can use that API to automatically issue the certs.
 
@@ -304,17 +276,14 @@ You don't have to do anything manually!
 
 1. CloudFlare.com API
 1. DNSPod.cn API
-1. DNSimple 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
@@ -329,6 +298,29 @@ You don't have to do anything manually!
 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)
+1. Namesilo (https://www.namesilo.com)
+1. InternetX autoDNS API (https://internetx.com)
+1. Azure DNS
+1. selectel.com(selectel.ru) DNS API
+1. zonomi.com DNS API
+1. DreamHost.com API
+1. DirectAdmin API
+
+
+And: 
+
+**lexicon DNS API: https://github.com/Neilpang/acme.sh/wiki/How-to-use-lexicon-dns-api
+   (DigitalOcean, DNSimple, DNSMadeEasy, DNSPark, EasyDNS, Namesilo, NS1, PointHQ, Rage4 and Vultr etc.)**
 
 
 **More APIs coming soon...**
@@ -337,14 +329,47 @@ If your DNS provider is not on the supported list above, you can write your own
 
 For more details: [How to use DNS API](dnsapi)
 
+# 8. Use DNS manual mode:
+
+If your dns provider doesn't support any api access, you can add the txt record by your hand.
 
-# 10. Issue ECC certificates
+```bash
+acme.sh --issue --dns -d example.com -d www.example.com -d cp.example.com
+```
+
+You should get an output like below:
+
+```sh
+Add the following txt record:
+Domain:_acme-challenge.example.com
+Txt value:9ihDbjYfTExAYeDs4DBUeuTo18KBzwvTEjUnSwd32-c
+
+Add the following txt record:
+Domain:_acme-challenge.www.example.com
+Txt value:9ihDbjxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+
+Please add those txt records to the domains. Waiting for the dns to take effect.
+```
+
+Then just rerun with `renew` argument:
+
+```bash
+acme.sh --renew -d example.com
+```
+
+Ok, it's done.
+
+**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.**
+
+**Please use dns api mode instead.**
+
+# 9. Issue ECC certificates
 
 `Let's Encrypt` can now issue **ECDSA** certificates.
 
 And we support them too!
 
-Just set the `length` parameter with a prefix `ec-`.
+Just set the `keylength` parameter with a prefix `ec-`.
 
 For example:
 
@@ -360,7 +385,7 @@ acme.sh --issue -w /home/wwwroot/example.com -d example.com --keylength ec-256
 acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com --keylength ec-256
 ```
 
-Please look at the last parameter above.
+Please look at the `keylength` parameter above.
 
 Valid values are:
 
@@ -369,36 +394,60 @@ Valid values are:
 3. **ec-521 (secp521r1,  "ECDSA P-521", which is not supported by Let's Encrypt yet.)**
 
 
-# 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.
+# 10. Issue Wildcard certificates
 
-However, you can also force to renew any cert:
+It's simple, just give a wildcard domain as the `-d` parameter.
 
+```sh
+acme.sh  --issue -d example.com  -d '*.example.com'  --dns dns_cf
 ```
+
+
+
+# 11. How to renew the certs
+
+No, you don't need to renew the certs manually. All the certs will be renewed automatically every **60** days.
+
+However, you can also force to renew a cert:
+
+```sh
 acme.sh --renew -d example.com --force
 ```
 
 or, for ECC cert:
 
-```
+```sh
 acme.sh --renew -d example.com --force --ecc
 ```
 
 
-# 12. How to upgrade `acme.sh`
+# 12. How to stop cert renewal
+
+To stop renewal of a cert, you can execute the following to remove the cert from the renewal list:
+
+```sh
+acme.sh --remove -d example.com [--ecc]
+```
+
+The cert/key file is not removed from the disk.
+
+You can remove the respective directory (e.g. `~/.acme.sh/example.com`) by yourself.
+
+
+# 13. How to upgrade `acme.sh`
 
 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:
 
-```
+```sh
 acme.sh --upgrade
 ```
 
 You can also enable auto upgrade:
 
-```
+```sh
 acme.sh --upgrade --auto-upgrade
 ```
 
@@ -406,31 +455,30 @@ Then **acme.sh** will be kept up to date automatically.
 
 Disable auto upgrade:
 
-```
+```sh
 acme.sh --upgrade --auto-upgrade 0
 ```
 
 
-# 13. Issue a cert from an existing CSR
+# 14. Issue a cert from an existing CSR
 
 https://github.com/Neilpang/acme.sh/wiki/Issue-a-cert-from-existing-CSR
 
 
-# 14. Under the Hood
+# 15. Under the Hood
 
 Speak ACME language using shell, directly to "Let's Encrypt".
 
 TODO:
 
 
-# 15. Acknowledgments
+# 16. 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
 
 
-# 16. License & Others
+# 17. License & Others
 
 License is GPLv3
 
@@ -439,7 +487,7 @@ 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.
 
 
-# 17. Donate
+# 18. Donate
 Your donation makes **acme.sh** better:
 
 1. PayPal/Alipay(支付宝)/Wechat(微信): [https://donate.acme.sh/](https://donate.acme.sh/)

File diff suppressed because it is too large
+ 376 - 173
acme.sh


+ 181 - 7
deploy/README.md

@@ -4,7 +4,9 @@ Before you can deploy your cert, you must [issue the cert first](https://github.
 
 Here are the scripts to deploy the certs/key to the server/services.
 
-## 1. Deploy the certs to your cpanel host.
+## 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.)
 
@@ -18,7 +20,7 @@ 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.
+## 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.
@@ -27,11 +29,150 @@ Currently supports Kong-v0.10.x.
 acme.sh --deploy -d ftp.example.com --deploy-hook kong
 ```
 
-## 3. Deploy the cert to remote server through SSH access.
+## 3. Deploy the cert to remote server through SSH access
+
+The ssh deploy plugin allows you to deploy certificates to a remote host
+using SSH command to connect to the remote server.  The ssh plugin is invoked
+with the following command...
+
+```sh
+acme.sh --deploy -d example.com --deploy-hook ssh
+```
+Prior to running this for the first time you must tell the plugin where
+and how to deploy the certificates.  This is done by exporting the following
+environment variables.  This is not required for subsequent runs as the
+values are stored by acme.sh in the domain configuration files.
+
+Required...
+```
+export DEPLOY_SSH_USER=username
+```
+Optional...
+```
+export DEPLOY_SSH_CMD=custom ssh command
+export DEPLOY_SSH_SERVER=url or ip address of remote host
+export DEPLOY_SSH_KEYFILE=filename for private key
+export DEPLOY_SSH_CERTFILE=filename for certificate file
+export DEPLOY_SSH_CAFILE=filename for intermediate CA file
+export DEPLOY_SSH_FULLCHAIN=filename for fullchain file
+export DEPLOY_SSH_REMOTE_CMD=command to execute on remote host
+export DEPLOY_SSH_BACKUP=yes or no
+```
+
+**DEPLOY_SSH_USER**
+Username at the remote host that SSH will login with. Note that
+SSH must be able to login to remote host without a password... SSH Keys
+must have been exchanged with the remote host. Validate and test that you
+can login to USER@URL from the host running acme.sh before using this script.
+
+The USER@URL at the remote server must also have has permissions to write to
+the target location of the certificate files and to execute any commands
+(e.g. to stop/start services).
+
+**DEPLOY_SSH_CMD**
+You can customize the ssh command used to connect to the remote host. For example
+if you need to connect to a specific port at the remote server you can set this
+to, for example, "ssh -p 22" or to use `sshpass` to provide password inline
+instead of exchanging ssh keys (this is not recommended, using keys is
+more secure).
+
+**DEPLOY_SSH_SERVER**
+URL or IP Address of the remote server.  If not provided then the domain
+name provided on the acme.sh --deploy command line is used.
+
+**DEPLOY_SSH_KEYFILE**
+Target filename for the private key issued by LetsEncrypt.
+
+**DEPLOY_SSH_CERTFILE**
+Target filename for the certificate issued by LetsEncrypt.
+If this is the same as the previous filename (for keyfile) then it is
+appended to the same file.
+
+**DEPLOY_SSH_CAFILE**
+Target filename for the CA intermediate certificate issued by LetsEncrypt.
+If this is the same as a previous filename (for keyfile or certfile) then
+it is appended to the same file.
+
+**DEPLOY_SSH_FULLCHAIN**
+Target filename for the fullchain certificate issued by LetsEncrypt.
+If this is the same as a previous filename (for keyfile, certfile or
+cafile) then it is appended to the same file.
+
+**DEPLOY_SSH_REMOTE_CMD**
+Command to execute on the remote server after copying any certificates.  This
+could be any additional command required for example to stop and restart
+the service.
+
+**DEPLOY_SSH_BACKUP**
+Before writing a certificate file to the remote server the existing
+certificate will be copied to a backup directory on the remote server.
+These are placed in a hidden directory in the home directory of the SSH
+user
+```sh
+~/.acme_ssh_deploy/[domain name]-backup-[timestamp]
+```
+Any backups older than 180 days will be deleted when new certificates
+are deployed.  This defaults to "yes" set to "no" to disable backup.
+
+###Examples using SSH deploy
+The following example illustrates deploying certificates to a QNAP NAS
+(tested with QTS version 4.2.3)
+
+```sh
+export DEPLOY_SSH_USER="admin"
+export DEPLOY_SSH_KEYFILE="/etc/stunnel/stunnel.pem"
+export DEPLOY_SSH_CERTFILE="/etc/stunnel/stunnel.pem"
+export DEPLOY_SSH_CAFILE="/etc/stunnel/uca.pem"
+export DEPLOY_SSH_REMOTE_CMD="/etc/init.d/stunnel.sh restart"
+
+acme.sh --deploy -d qnap.example.com --deploy-hook ssh
+```
+Note how in this example both the private key and certificate point to
+the same file.  This will result in the certificate being appended
+to the same file as the private key... a common requirement of several
+services.
 
-(TODO)
+The next example illustrates deploying certificates to a Unifi
+Controller (tested with version 5.4.11).
+
+```sh
+export DEPLOY_SSH_USER="root"
+export DEPLOY_SSH_KEYFILE="/var/lib/unifi/unifi.example.com.key"
+export DEPLOY_SSH_FULLCHAIN="/var/lib/unifi/unifi.example.com.cer"
+export DEPLOY_SSH_REMOTE_CMD="openssl pkcs12 -export \
+   -inkey /var/lib/unifi/unifi.example.com.key \
+   -in /var/lib/unifi/unifi.example.com.cer \
+   -out /var/lib/unifi/unifi.example.com.p12 \
+   -name ubnt -password pass:temppass \
+ && keytool -importkeystore -deststorepass aircontrolenterprise \
+   -destkeypass aircontrolenterprise \
+   -destkeystore /var/lib/unifi/keystore \
+   -srckeystore /var/lib/unifi/unifi.example.com.p12 \
+   -srcstoretype PKCS12 -srcstorepass temppass -alias ubnt -noprompt \
+ && service unifi restart"
+
+acme.sh --deploy -d unifi.example.com --deploy-hook ssh
+```
+In this example we execute several commands on the remote host
+after the certificate files have been copied... to generate a pkcs12 file
+compatible with Unifi, to import it into the Unifi keystore and then finally
+to restart the service.
+
+Note also that once the certificate is imported
+into the keystore the individual certificate files are no longer
+required. We could if we desired delete those files immediately. If we
+do that then we should disable backup at the remote host (as there are
+no files to backup -- they were erased during deployment). For example...
+```sh
+export DEPLOY_SSH_BACKUP=no
+# modify the end of the remote command...
+&& rm /var/lib/unifi/unifi.example.com.key \
+      /var/lib/unifi/unifi.example.com.cer \
+      /var/lib/unifi/unifi.example.com.p12 \
+&& service unifi restart
+```
 
-## 4. Deploy the cert to local vsftpd server.
+## 4. Deploy the cert to local vsftpd server
 
 ```sh
 acme.sh --deploy -d ftp.example.com --deploy-hook vsftpd
@@ -53,7 +194,7 @@ 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.
+## 5. Deploy the cert to local exim4 server
 
 ```sh
 acme.sh --deploy -d ftp.example.com --deploy-hook exim4
@@ -81,7 +222,40 @@ acme.sh --deploy -d ftp.example.com --deploy-hook exim4
 acme.sh --deploy -d ftp.example.com --deploy-hook keychain
 ```
 
-## 7. Deploy the cert to remote routeros
+## 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 separate 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
+
+## 10. Deploy the cert to remote routeros
 
 ```sh
 acme.sh --deploy -d ftp.example.com --deploy-hook routeros

+ 0 - 29
deploy/cpanel.sh

@@ -1,29 +0,0 @@
-#!/usr/bin/env sh
-
-#Here is the script to deploy the cert to your cpanel account by the cpanel APIs.
-
-#returns 0 means success, otherwise error.
-
-#export DEPLOY_CPANEL_USER=myusername
-#export DEPLOY_CPANEL_PASSWORD=PASSWORD
-
-########  Public functions #####################
-
-#domain keyfile certfile cafile fullchain
-cpanel_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
-
-}

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

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

+ 205 - 0
deploy/ssh.sh

@@ -0,0 +1,205 @@
+#!/usr/bin/env sh
+
+# Script to deploy certificates to remote server by SSH
+# Note that SSH must be able to login to remote host without a password...
+# SSH Keys must have been exchanged with the remote host.  Validate and
+# test that you can login to USER@SERVER from the host running acme.sh before
+# using this script.
+#
+# The following variables exported from environment will be used.
+# If not set then values previously saved in domain.conf file are used.
+#
+# Only a username is required.  All others are optional.
+#
+# The following examples are for QNAP NAS running QTS 4.2 
+# export DEPLOY_SSH_CMD=""  # defaults to ssh
+# export DEPLOY_SSH_USER="admin"  # required
+# export DEPLOY_SSH_SERVER="qnap"  # defaults to domain name
+# export DEPLOY_SSH_KEYFILE="/etc/stunnel/stunnel.pem"
+# export DEPLOY_SSH_CERTFILE="/etc/stunnel/stunnel.pem"
+# export DEPLOY_SSH_CAFILE="/etc/stunnel/uca.pem"
+# export DEPLOY_SSH_FULLCHAIN=""
+# export DEPLOY_SSH_REMOTE_CMD="/etc/init.d/stunnel.sh restart"
+# export DEPLOY_SSH_BACKUP=""  # yes or no, default to yes
+#
+########  Public functions #####################
+
+#domain keyfile certfile cafile fullchain
+ssh_deploy() {
+  _cdomain="$1"
+  _ckey="$2"
+  _ccert="$3"
+  _cca="$4"
+  _cfullchain="$5"
+  _cmdstr=""
+  _homedir='~'
+  _backupprefix="$_homedir/.acme_ssh_deploy/$_cdomain-backup"
+  _backupdir="$_backupprefix-$(_utc_date | tr ' ' '-')"
+
+  if [ -f "$DOMAIN_CONF" ]; then
+    # shellcheck disable=SC1090
+    . "$DOMAIN_CONF"
+  fi
+
+  _debug _cdomain "$_cdomain"
+  _debug _ckey "$_ckey"
+  _debug _ccert "$_ccert"
+  _debug _cca "$_cca"
+  _debug _cfullchain "$_cfullchain"
+
+  # USER is required to login by SSH to remote host.
+  if [ -z "$DEPLOY_SSH_USER" ]; then
+    if [ -z "$Le_Deploy_ssh_user" ]; then
+      _err "DEPLOY_SSH_USER not defined."
+      return 1
+    fi
+  else
+    Le_Deploy_ssh_user="$DEPLOY_SSH_USER"
+    _savedomainconf Le_Deploy_ssh_user "$Le_Deploy_ssh_user"
+  fi
+
+  # SERVER is optional. If not provided then use _cdomain
+  if [ -n "$DEPLOY_SSH_SERVER" ]; then
+    Le_Deploy_ssh_server="$DEPLOY_SSH_SERVER"
+    _savedomainconf Le_Deploy_ssh_server "$Le_Deploy_ssh_server"
+  elif [ -z "$Le_Deploy_ssh_server" ]; then
+    Le_Deploy_ssh_server="$_cdomain"
+  fi
+
+  # CMD is optional. If not provided then use ssh
+  if [ -n "$DEPLOY_SSH_CMD" ]; then
+    Le_Deploy_ssh_cmd="$DEPLOY_SSH_CMD"
+    _savedomainconf Le_Deploy_ssh_cmd "$Le_Deploy_ssh_cmd"
+  elif [ -z "$Le_Deploy_ssh_cmd" ]; then
+    Le_Deploy_ssh_cmd="ssh"
+  fi
+
+  # BACKUP is optional. If not provided then default to yes
+  if [ "$DEPLOY_SSH_BACKUP" = "no" ]; then
+    Le_Deploy_ssh_backup="no"
+  elif [ -z "$Le_Deploy_ssh_backup" ]; then
+    Le_Deploy_ssh_backup="yes"
+  fi
+  _savedomainconf Le_Deploy_ssh_backup "$Le_Deploy_ssh_backup"
+
+  _info "Deploy certificates to remote server $Le_Deploy_ssh_user@$Le_Deploy_ssh_server"
+
+  # KEYFILE is optional.
+  # If provided then private key will be copied to provided filename.
+  if [ -n "$DEPLOY_SSH_KEYFILE" ]; then
+    Le_Deploy_ssh_keyfile="$DEPLOY_SSH_KEYFILE"
+    _savedomainconf Le_Deploy_ssh_keyfile "$Le_Deploy_ssh_keyfile"
+  fi
+  if [ -n "$Le_Deploy_ssh_keyfile" ]; then
+    if [ "$Le_Deploy_ssh_backup" = "yes" ]; then
+      # backup file we are about to overwrite.
+      _cmdstr="$_cmdstr cp $Le_Deploy_ssh_keyfile $_backupdir >/dev/null;"
+    fi
+    # copy new certificate into file.
+    _cmdstr="$_cmdstr echo \"$(cat "$_ckey")\" > $Le_Deploy_ssh_keyfile;"
+    _info "will copy private key to remote file $Le_Deploy_ssh_keyfile"
+  fi
+
+  # CERTFILE is optional.
+  # If provided then private key will be copied or appended to provided filename.
+  if [ -n "$DEPLOY_SSH_CERTFILE" ]; then
+    Le_Deploy_ssh_certfile="$DEPLOY_SSH_CERTFILE"
+    _savedomainconf Le_Deploy_ssh_certfile "$Le_Deploy_ssh_certfile"
+  fi
+  if [ -n "$Le_Deploy_ssh_certfile" ]; then
+    _pipe=">"
+    if [ "$Le_Deploy_ssh_certfile" = "$Le_Deploy_ssh_keyfile" ]; then
+      # if filename is same as previous file then append.
+      _pipe=">>"
+    elif [ "$Le_Deploy_ssh_backup" = "yes" ]; then
+      # backup file we are about to overwrite.
+      _cmdstr="$_cmdstr cp $Le_Deploy_ssh_certfile $_backupdir >/dev/null;"
+    fi
+    # copy new certificate into file.
+    _cmdstr="$_cmdstr echo \"$(cat "$_ccert")\" $_pipe $Le_Deploy_ssh_certfile;"
+    _info "will copy certificate to remote file $Le_Deploy_ssh_certfile"
+  fi
+
+  # CAFILE is optional.
+  # If provided then CA intermediate certificate will be copied or appended to provided filename.
+  if [ -n "$DEPLOY_SSH_CAFILE" ]; then
+    Le_Deploy_ssh_cafile="$DEPLOY_SSH_CAFILE"
+    _savedomainconf Le_Deploy_ssh_cafile "$Le_Deploy_ssh_cafile"
+  fi
+  if [ -n "$Le_Deploy_ssh_cafile" ]; then
+    _pipe=">"
+    if [ "$Le_Deploy_ssh_cafile" = "$Le_Deploy_ssh_keyfile" ] \
+      || [ "$Le_Deploy_ssh_cafile" = "$Le_Deploy_ssh_certfile" ]; then
+      # if filename is same as previous file then append.
+      _pipe=">>"
+    elif [ "$Le_Deploy_ssh_backup" = "yes" ]; then
+      # backup file we are about to overwrite.
+      _cmdstr="$_cmdstr cp $Le_Deploy_ssh_cafile $_backupdir >/dev/null;"
+    fi
+    # copy new certificate into file.
+    _cmdstr="$_cmdstr echo \"$(cat "$_cca")\" $_pipe $Le_Deploy_ssh_cafile;"
+    _info "will copy CA file to remote file $Le_Deploy_ssh_cafile"
+  fi
+
+  # FULLCHAIN is optional.
+  # If provided then fullchain certificate will be copied or appended to provided filename.
+  if [ -n "$DEPLOY_SSH_FULLCHAIN" ]; then
+    Le_Deploy_ssh_fullchain="$DEPLOY_SSH_FULLCHAIN"
+    _savedomainconf Le_Deploy_ssh_fullchain "$Le_Deploy_ssh_fullchain"
+  fi
+  if [ -n "$Le_Deploy_ssh_fullchain" ]; then
+    _pipe=">"
+    if [ "$Le_Deploy_ssh_fullchain" = "$Le_Deploy_ssh_keyfile" ] \
+      || [ "$Le_Deploy_ssh_fullchain" = "$Le_Deploy_ssh_certfile" ] \
+      || [ "$Le_Deploy_ssh_fullchain" = "$Le_Deploy_ssh_cafile" ]; then
+      # if filename is same as previous file then append.
+      _pipe=">>"
+    elif [ "$Le_Deploy_ssh_backup" = "yes" ]; then
+      # backup file we are about to overwrite.
+      _cmdstr="$_cmdstr cp $Le_Deploy_ssh_fullchain $_backupdir >/dev/null;"
+    fi
+    # copy new certificate into file.
+    _cmdstr="$_cmdstr echo \"$(cat "$_cfullchain")\" $_pipe $Le_Deploy_ssh_fullchain;"
+    _info "will copy fullchain to remote file $Le_Deploy_ssh_fullchain"
+  fi
+
+  # REMOTE_CMD is optional.
+  # If provided then this command will be executed on remote host.
+  if [ -n "$DEPLOY_SSH_REMOTE_CMD" ]; then
+    Le_Deploy_ssh_remote_cmd="$DEPLOY_SSH_REMOTE_CMD"
+    _savedomainconf Le_Deploy_ssh_remote_cmd "$Le_Deploy_ssh_remote_cmd"
+  fi
+  if [ -n "$Le_Deploy_ssh_remote_cmd" ]; then
+    _cmdstr="$_cmdstr $Le_Deploy_ssh_remote_cmd;"
+    _info "Will execute remote command $Le_Deploy_ssh_remote_cmd"
+  fi
+
+  if [ -z "$_cmdstr" ]; then
+    _err "No remote commands to excute. Failed to deploy certificates to remote server"
+    return 1
+  elif [ "$Le_Deploy_ssh_backup" = "yes" ]; then
+    # run cleanup on the backup directory, erase all older
+    # than 180 days (15552000 seconds).
+    _cmdstr="{ now=\"\$(date -u +%s)\"; for fn in $_backupprefix*; \
+do if [ -d \"\$fn\" ] && [ \"\$(expr \$now - \$(date -ur \$fn +%s) )\" -ge \"15552000\" ]; \
+then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; done; }; $_cmdstr"
+    # Alternate version of above... _cmdstr="find $_backupprefix* -type d -mtime +180 2>/dev/null | xargs rm -rf; $_cmdstr"
+    # Create our backup directory for overwritten cert files.
+    _cmdstr="mkdir -p $_backupdir; $_cmdstr"
+    _info "Backup of old certificate files will be placed in remote directory $_backupdir"
+    _info "Backup directories erased after 180 days."
+  fi
+
+  _debug "Remote commands to execute: $_cmdstr"
+  _info "Submitting sequence of commands to remote server by ssh"
+  # quotations in bash cmd below intended.  Squash travis spellcheck error
+  # shellcheck disable=SC2029
+  $Le_Deploy_ssh_cmd -T "$Le_Deploy_ssh_user@$Le_Deploy_ssh_server" sh -c "'$_cmdstr'"
+  _ret="$?"
+
+  if [ "$_ret" != "0" ]; then
+    _err "Error code $_ret returned from $Le_Deploy_ssh_cmd"
+  fi
+
+  return $_ret
+}

+ 55 - 0
deploy/strongswan.sh

@@ -0,0 +1,55 @@
+#!/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"
+
+  _info "Using strongswan"
+
+  if [ -x /usr/sbin/ipsec ]; then
+    _ipsec=/usr/sbin/ipsec
+  elif [ -x /usr/sbin/strongswan ]; then
+    _ipsec=/usr/sbin/strongswan
+  elif [ -x /usr/local/sbin/ipsec ]; then
+    _ipsec=/usr/local/sbin/ipsec
+  else
+    _err "no strongswan or ipsec command is detected"
+    return 1
+  fi
+
+  _info _ipsec "$_ipsec"
+
+  _confdir=$($_ipsec --confdir)
+  if [ $? -ne 0 ] || [ -z "$_confdir" ]; then
+    _err "no strongswan --confdir is detected"
+    return 1
+  fi
+
+  _info _confdir "$_confdir"
+
+  _debug _cdomain "$_cdomain"
+  _debug _ckey "$_ckey"
+  _debug _ccert "$_ccert"
+  _debug _cca "$_cca"
+  _debug _cfullchain "$_cfullchain"
+
+  cat "$_ckey" >"${_confdir}/ipsec.d/private/$(basename "$_ckey")"
+  cat "$_ccert" >"${_confdir}/ipsec.d/certs/$(basename "$_ccert")"
+  cat "$_cca" >"${_confdir}/ipsec.d/cacerts/$(basename "$_cca")"
+  cat "$_cfullchain" >"${_confdir}/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
+
+}

+ 56 - 0
deploy/vault_cli.sh

@@ -0,0 +1,56 @@
+#!/usr/bin/env sh
+
+# Here is a script to deploy cert to hashicorp vault
+# (https://www.vaultproject.io/)
+# 
+# it requires the vault binary to be available in PATH, and the following
+# environment variables:
+# 
+# VAULT_PREFIX - this contains the prefix path in vault
+# VAULT_ADDR - vault requires this to find your vault server
+#
+# additionally, you need to ensure that VAULT_TOKEN is avialable or
+# `vault auth` has applied the appropriate authorization for the vault binary
+# to access the vault server
+
+#returns 0 means success, otherwise error.
+
+########  Public functions #####################
+
+#domain keyfile certfile cafile fullchain
+vault_cli_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"
+
+  # validate required env vars
+  if [ -z "$VAULT_PREFIX" ]; then
+    _err "VAULT_PREFIX needs to be defined (contains prefix path in vault)"
+    return 1
+  fi
+
+  if [ -z "$VAULT_ADDR" ]; then
+    _err "VAULT_ADDR needs to be defined (contains vault connection address)"
+    return 1
+  fi
+
+  VAULT_CMD=$(which vault)
+  if [ ! $? ]; then
+    _err "cannot find vault binary!"
+    return 1
+  fi
+
+  $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}/cert.pem" value=@"$_ccert" || return 1
+  $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}/cert.key" value=@"$_ckey" || return 1
+  $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}/fullchain.pem" value=@"$_cfullchain" || return 1
+
+}

+ 296 - 4
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:
@@ -354,7 +354,7 @@ acme.sh --issue --dns dns_gandi_livedns -d example.com -d www.example.com
 First, generate a TSIG key for updating the zone.
 
 ```
-keymgr tsig generate acme_key algorithm hmac-sha512 > /etc/knot/acme.key
+keymgr tsig generate -t acme_key hmac-sha512 > /etc/knot/acme.key
 ```
 
 Include this key in your knot configuration file.
@@ -409,10 +409,13 @@ 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/
+You need to set the HTTP API user ID and password credentials. See: https://www.cloudns.net/wiki/article/42/. For security reasons, it's recommended to use a sub user ID that only has access to the necessary zones, as a regular API user has access to your entire account.
 
 ```
-export CLOUDNS_AUTH_ID=XXXXX
+# Use this for a sub auth ID
+export CLOUDNS_SUB_AUTH_ID=XXXXX
+# Use this for a regular auth ID
+#export CLOUDNS_AUTH_ID=XXXXX
 export CLOUDNS_AUTH_PASSWORD="YYYYYYYYY"
 ```
 
@@ -420,6 +423,7 @@ 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
 
@@ -494,6 +498,293 @@ 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 (https://dns.he.net/) 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.
+
+## 35. Use Namesilo.com API
+
+You'll need to generate an API key at https://www.namesilo.com/account_api.php
+Optionally you may restrict the access to an IP range there.
+
+```
+export Namesilo_Key="xxxxxxxxxxxxxxxxxxxxxxxx"
+```
+
+And now you can issue certs with:
+
+```
+acme.sh --issue --dns dns_namesilo --dnssleep 900 -d example.com -d www.example.com
+```
+
+## 36. Use autoDNS (InternetX)
+
+[InternetX](https://www.internetx.com/) offers an [xml api](https://help.internetx.com/display/API/AutoDNS+XML-API)  with your standard login credentials, set them like so:
+
+```
+export AUTODNS_USER="yourusername"
+export AUTODNS_PASSWORD="password"
+export AUTODNS_CONTEXT="context"
+```
+
+Then you can issue your certificates with:
+
+```
+acme.sh --issue --dns dns_autodns -d example.com -d www.example.com
+```
+
+The `AUTODNS_USER`, `AUTODNS_PASSWORD` and `AUTODNS_CONTEXT` settings will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 37. Use Azure DNS
+
+You have to create a service principal first. See:[How to use Azure DNS](../../../wiki/How-to-use-Azure-DNS)
+
+```
+export AZUREDNS_SUBSCRIPTIONID="12345678-9abc-def0-1234-567890abcdef"
+export AZUREDNS_TENANTID="11111111-2222-3333-4444-555555555555"
+export AZUREDNS_APPID="3b5033b5-7a66-43a5-b3b9-a36b9e7c25ed"
+export AZUREDNS_CLIENTSECRET="1b0224ef-34d4-5af9-110f-77f527d561bd"
+```
+
+Then you can issue your certificates with:
+
+```
+acme.sh --issue --dns dns_azure -d example.com -d www.example.com
+```
+
+`AZUREDNS_SUBSCRIPTIONID`, `AZUREDNS_TENANTID`,`AZUREDNS_APPID` and `AZUREDNS_CLIENTSECRET` settings will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 38. Use selectel.com(selectel.ru) domain API to automatically issue cert
+
+First you need to login to your account to get your API key from: https://my.selectel.ru/profile/apikeys.
+
+```sh
+export SL_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
+
+```
+
+Ok, let's issue a cert now:
+```
+acme.sh --issue --dns dns_selectel -d example.com -d www.example.com
+```
+
+The `SL_Key` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 39. Use zonomi.com domain API to automatically issue cert
+
+First you need to login to your account to find your API key from: http://zonomi.com/app/dns/dyndns.jsp
+
+Your will find your api key in the example urls:
+
+```sh
+https://zonomi.com/app/dns/dyndns.jsp?host=example.com&api_key=1063364558943540954358668888888888
+```
+
+```sh
+export ZM_Key="1063364558943540954358668888888888"
+
+```
+
+Ok, let's issue a cert now:
+```
+acme.sh --issue --dns dns_zonomi -d example.com -d www.example.com
+```
+
+The `ZM_Key` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 40. Use DreamHost DNS API
+
+DNS API keys may be created at https://panel.dreamhost.com/?tree=home.api.
+Ensure the created key has add and remove privelages.
+
+```
+export DH_API_Key="<api key>"
+acme.sh --issue --dns dns_dreamhost -d example.com -d www.example.com
+```
+
+The 'DH_API_KEY' will be saved in `~/.acme.sh/account.conf` and will
+be reused when needed.
+
+## 41. Use DirectAdmin API
+The DirectAdmin interface has it's own Let's encrypt functionality, but this
+script can be used to generate certificates for names which are not hosted on
+DirectAdmin
+
+User must provide login data and URL to the DirectAdmin incl. port.
+You can create an user which only has access to
+
+- CMD_API_DNS_CONTROL
+- CMD_API_SHOW_DOMAINS
+
+By using the Login Keys function.
+See also https://www.directadmin.com/api.php and https://www.directadmin.com/features.php?id=1298
+
+```
+export DA_Api="https://remoteUser:remotePassword@da.domain.tld:8443"
+export DA_Api_Insecure=1
+```
+Set `DA_Api_Insecure` to 1 for insecure and 0 for secure -> difference is whether ssl cert is checked for validity (0) or whether it is just accepted (1)
+
+Ok, let's issue a cert now:
+```
+acme.sh --issue --dns dns_da -d example.com -d www.example.com
+```
+
+The `DA_Api` and `DA_Api_Insecure` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+
 # Use custom API
 
 If your API is not supported yet, you can write your own DNS API.
@@ -510,6 +801,7 @@ 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
 
 # Use lexicon DNS API
 

+ 31 - 16
dnsapi/dns_ali.sh

@@ -10,6 +10,8 @@ dns_ali_add() {
   fulldomain=$1
   txtvalue=$2
 
+  Ali_Key="${Ali_Key:-$(_readaccountconf_mutable Ali_Key)}"
+  Ali_Secret="${Ali_Secret:-$(_readaccountconf_mutable Ali_Secret)}"
   if [ -z "$Ali_Key" ] || [ -z "$Ali_Secret" ]; then
     Ali_Key=""
     Ali_Secret=""
@@ -18,8 +20,8 @@ dns_ali_add() {
   fi
 
   #save the api key and secret to the account conf file.
-  _saveaccountconf Ali_Key "$Ali_Key"
-  _saveaccountconf Ali_Secret "$Ali_Secret"
+  _saveaccountconf_mutable Ali_Key "$Ali_Key"
+  _saveaccountconf_mutable Ali_Secret "$Ali_Secret"
 
   _debug "First detect the root zone"
   if ! _get_root "$fulldomain"; then
@@ -32,6 +34,15 @@ dns_ali_add() {
 
 dns_ali_rm() {
   fulldomain=$1
+  txtvalue=$2
+  Ali_Key="${Ali_Key:-$(_readaccountconf_mutable Ali_Key)}"
+  Ali_Secret="${Ali_Secret:-$(_readaccountconf_mutable Ali_Secret)}"
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    return 1
+  fi
+
   _clean
 }
 
@@ -76,16 +87,14 @@ _ali_rest() {
     return 1
   fi
 
+  _debug2 response "$response"
   if [ -z "$2" ]; then
-    message="$(printf "%s" "$response" | _egrep_o "\"Message\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")"
-    if [ -n "$message" ]; then
+    message="$(echo "$response" | _egrep_o "\"Message\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")"
+    if [ "$message" ]; then
       _err "$message"
       return 1
     fi
   fi
-
-  _debug2 response "$response"
-  return 0
 }
 
 _ali_urlencode() {
@@ -112,12 +121,14 @@ _ali_nonce() {
 }
 
 _check_exist_query() {
+  _qdomain="$1"
+  _qsubdomain="$2"
   query=''
   query=$query'AccessKeyId='$Ali_Key
   query=$query'&Action=DescribeDomainRecords'
-  query=$query'&DomainName='$1
+  query=$query'&DomainName='$_qdomain
   query=$query'&Format=json'
-  query=$query'&RRKeyWord=_acme-challenge'
+  query=$query'&RRKeyWord='$_qsubdomain
   query=$query'&SignatureMethod=HMAC-SHA1'
   query=$query"&SignatureNonce=$(_ali_nonce)"
   query=$query'&SignatureVersion=1.0'
@@ -169,17 +180,21 @@ _describe_records_query() {
 }
 
 _clean() {
-  _check_exist_query "$_domain"
+  _check_exist_query "$_domain" "$_sub_domain"
   if ! _ali_rest "Check exist records" "ignore"; then
     return 1
   fi
 
-  records="$(echo "$response" -n | _egrep_o "\"RecordId\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")"
-  printf "%s" "$records" \
-    | while read -r record_id; do
-      _delete_record_query "$record_id"
-      _ali_rest "Delete record $record_id" "ignore"
-    done
+  record_id="$(echo "$response" | tr '{' "\n" | grep "$_sub_domain" | grep "$txtvalue" | tr "," "\n" | grep RecordId | cut -d '"' -f 4)"
+  _debug2 record_id "$record_id"
+
+  if [ -z "$record_id" ]; then
+    _debug "record not found, skip"
+  else
+    _delete_record_query "$record_id"
+    _ali_rest "Delete record $record_id" "ignore"
+  fi
+
 }
 
 _timestamp() {

+ 264 - 0
dnsapi/dns_autodns.sh

@@ -0,0 +1,264 @@
+#!/usr/bin/env sh
+# -*- mode: sh; tab-width: 2; indent-tabs-mode: s; coding: utf-8 -*-
+
+# This is the InternetX autoDNS xml api wrapper for acme.sh
+# Author: auerswald@gmail.com
+# Created: 2018-01-14
+#
+#     export AUTODNS_USER="username"
+#     export AUTODNS_PASSWORD="password"
+#     export AUTODNS_CONTEXT="context"
+#
+# Usage:
+#     acme.sh --issue --dns dns_autodns -d example.com
+
+AUTODNS_API="https://gateway.autodns.com"
+
+# Arguments:
+#   txtdomain
+#   txt
+dns_autodns_add() {
+  fulldomain="$1"
+  txtvalue="$2"
+
+  AUTODNS_USER="${AUTODNS_USER:-$(_readaccountconf_mutable AUTODNS_USER)}"
+  AUTODNS_PASSWORD="${AUTODNS_PASSWORD:-$(_readaccountconf_mutable AUTODNS_PASSWORD)}"
+  AUTODNS_CONTEXT="${AUTODNS_CONTEXT:-$(_readaccountconf_mutable AUTODNS_CONTEXT)}"
+
+  if [ -z "$AUTODNS_USER" ] || [ -z "$AUTODNS_CONTEXT" ] || [ -z "$AUTODNS_PASSWORD" ]; then
+    _err "You don't specify autodns user, password and context."
+    return 1
+  fi
+
+  _saveaccountconf_mutable AUTODNS_USER "$AUTODNS_USER"
+  _saveaccountconf_mutable AUTODNS_PASSWORD "$AUTODNS_PASSWORD"
+  _saveaccountconf_mutable AUTODNS_CONTEXT "$AUTODNS_CONTEXT"
+
+  _debug "First detect the root zone"
+
+  if ! _get_autodns_zone "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+
+  _debug _sub_domain "$_sub_domain"
+  _debug _zone "$_zone"
+  _debug _system_ns "$_system_ns"
+
+  _info "Adding TXT record"
+
+  autodns_response="$(_autodns_zone_update "$_zone" "$_sub_domain" "$txtvalue" "$_system_ns")"
+
+  if [ "$?" -eq "0" ]; then
+    _info "Added, OK"
+    return 0
+  fi
+
+  return 1
+}
+
+# Arguments:
+#   txtdomain
+#   txt
+dns_autodns_rm() {
+  fulldomain="$1"
+  txtvalue="$2"
+
+  AUTODNS_USER="${AUTODNS_USER:-$(_readaccountconf_mutable AUTODNS_USER)}"
+  AUTODNS_PASSWORD="${AUTODNS_PASSWORD:-$(_readaccountconf_mutable AUTODNS_PASSWORD)}"
+  AUTODNS_CONTEXT="${AUTODNS_CONTEXT:-$(_readaccountconf_mutable AUTODNS_CONTEXT)}"
+
+  if [ -z "$AUTODNS_USER" ] || [ -z "$AUTODNS_CONTEXT" ] || [ -z "$AUTODNS_PASSWORD" ]; then
+    _err "You don't specify autodns user, password and context."
+    return 1
+  fi
+
+  _debug "First detect the root zone"
+
+  if ! _get_autodns_zone "$fulldomain"; then
+    _err "zone not found"
+    return 1
+  fi
+
+  _debug _sub_domain "$_sub_domain"
+  _debug _zone "$_zone"
+  _debug _system_ns "$_system_ns"
+
+  _info "Delete TXT record"
+
+  autodns_response="$(_autodns_zone_cleanup "$_zone" "$_sub_domain" "$txtvalue" "$_system_ns")"
+
+  if [ "$?" -eq "0" ]; then
+    _info "Deleted, OK"
+    return 0
+  fi
+
+  return 1
+}
+
+####################  Private functions below ##################################
+
+# Arguments:
+#   fulldomain
+# Returns:
+#   _sub_domain=_acme-challenge.www
+#   _zone=domain.com
+#   _system_ns
+_get_autodns_zone() {
+  domain="$1"
+
+  i=2
+  p=1
+
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    _debug h "$h"
+
+    if [ -z "$h" ]; then
+      # not valid
+      return 1
+    fi
+
+    autodns_response="$(_autodns_zone_inquire "$h")"
+
+    if [ "$?" -ne "0" ]; then
+      _err "invalid domain"
+      return 1
+    fi
+
+    if _contains "$autodns_response" "<summary>1</summary>" >/dev/null; then
+      _zone="$(echo "$autodns_response" | _egrep_o '<name>[^<]*</name>' | cut -d '>' -f 2 | cut -d '<' -f 1)"
+      _system_ns="$(echo "$autodns_response" | _egrep_o '<system_ns>[^<]*</system_ns>' | cut -d '>' -f 2 | cut -d '<' -f 1)"
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      return 0
+    fi
+
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+
+  return 1
+}
+
+_build_request_auth_xml() {
+  printf "<auth>
+    <user>%s</user>
+    <password>%s</password>
+    <context>%s</context>
+  </auth>" "$AUTODNS_USER" "$AUTODNS_PASSWORD" "$AUTODNS_CONTEXT"
+}
+
+# Arguments:
+#   zone
+_build_zone_inquire_xml() {
+  printf "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
+  <request>
+    %s
+    <task>
+      <code>0205</code>
+      <view>
+        <children>1</children>
+        <limit>1</limit>
+      </view>
+      <where>
+        <key>name</key>
+        <operator>eq</operator>
+        <value>%s</value>
+      </where>
+    </task>
+  </request>" "$(_build_request_auth_xml)" "$1"
+}
+
+# Arguments:
+#   zone
+#   subdomain
+#   txtvalue
+#   system_ns
+_build_zone_update_xml() {
+  printf "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
+  <request>
+    %s
+    <task>
+      <code>0202001</code>
+      <default>
+        <rr_add>
+          <name>%s</name>
+          <ttl>600</ttl>
+          <type>TXT</type>
+          <value>%s</value>
+        </rr_add>
+      </default>
+      <zone>
+        <name>%s</name>
+        <system_ns>%s</system_ns>
+      </zone>
+    </task>
+  </request>" "$(_build_request_auth_xml)" "$2" "$3" "$1" "$4"
+}
+
+# Arguments:
+#   zone
+_autodns_zone_inquire() {
+  request_data="$(_build_zone_inquire_xml "$1")"
+  autodns_response="$(_autodns_api_call "$request_data")"
+  ret="$?"
+
+  printf "%s" "$autodns_response"
+  return "$ret"
+}
+
+# Arguments:
+#   zone
+#   subdomain
+#   txtvalue
+#   system_ns
+_autodns_zone_update() {
+  request_data="$(_build_zone_update_xml "$1" "$2" "$3" "$4")"
+  autodns_response="$(_autodns_api_call "$request_data")"
+  ret="$?"
+
+  printf "%s" "$autodns_response"
+  return "$ret"
+}
+
+# Arguments:
+#   zone
+#   subdomain
+#   txtvalue
+#   system_ns
+_autodns_zone_cleanup() {
+  request_data="$(_build_zone_update_xml "$1" "$2" "$3" "$4")"
+  # replace 'rr_add>' with 'rr_rem>' in request_data
+  request_data="$(printf -- "%s" "$request_data" | sed 's/rr_add>/rr_rem>/g')"
+  autodns_response="$(_autodns_api_call "$request_data")"
+  ret="$?"
+
+  printf "%s" "$autodns_response"
+  return "$ret"
+}
+
+# Arguments:
+#   request_data
+_autodns_api_call() {
+  request_data="$1"
+
+  _debug request_data "$request_data"
+
+  autodns_response="$(_post "$request_data" "$AUTODNS_API")"
+  ret="$?"
+
+  _debug autodns_response "$autodns_response"
+
+  if [ "$ret" -ne "0" ]; then
+    _err "error"
+    return 1
+  fi
+
+  if _contains "$autodns_response" "<type>success</type>" >/dev/null; then
+    _info "success"
+    printf "%s" "$autodns_response"
+    return 0
+  fi
+
+  return 1
+}

+ 115 - 18
dnsapi/dns_aws.sh

@@ -19,17 +19,25 @@ dns_aws_add() {
   fulldomain=$1
   txtvalue=$2
 
+  AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-$(_readaccountconf_mutable AWS_ACCESS_KEY_ID)}"
+  AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-$(_readaccountconf_mutable AWS_SECRET_ACCESS_KEY)}"
+
+  if [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ]; then
+    _use_container_role || _use_instance_role
+  fi
+
   if [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ]; then
     AWS_ACCESS_KEY_ID=""
     AWS_SECRET_ACCESS_KEY=""
     _err "You don't specify aws route53 api key id and and api key secret yet."
-    _err "Please create you key and try again. see $(__green $AWS_WIKI)"
+    _err "Please create your key and try again. see $(__green $AWS_WIKI)"
     return 1
   fi
 
-  if [ -z "$AWS_SESSION_TOKEN" ]; then
-    _saveaccountconf AWS_ACCESS_KEY_ID "$AWS_ACCESS_KEY_ID"
-    _saveaccountconf AWS_SECRET_ACCESS_KEY "$AWS_SECRET_ACCESS_KEY"
+  #save for future use, unless using a role which will be fetched as needed
+  if [ -z "$_using_role" ]; then
+    _saveaccountconf_mutable AWS_ACCESS_KEY_ID "$AWS_ACCESS_KEY_ID"
+    _saveaccountconf_mutable AWS_SECRET_ACCESS_KEY "$AWS_SECRET_ACCESS_KEY"
   fi
 
   _debug "First detect the root zone"
@@ -41,7 +49,26 @@ dns_aws_add() {
   _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>UPSERT</Action><ResourceRecordSet><Name>$fulldomain</Name><Type>TXT</Type><TTL>300</TTL><ResourceRecords><ResourceRecord><Value>\"$txtvalue\"</Value></ResourceRecord></ResourceRecords></ResourceRecordSet></Change></Changes></ChangeBatch></ChangeResourceRecordSetsRequest>"
+  _info "Geting existing records for $fulldomain"
+  if ! aws_rest GET "2013-04-01$_domain_id/rrset" "name=$fulldomain&type=TXT"; then
+    return 1
+  fi
+
+  if _contains "$response" "<Name>$fulldomain.</Name>"; then
+    _resource_record="$(echo "$response" | sed 's/<ResourceRecordSet>/"/g' | tr '"' "\n" | grep "<Name>$fulldomain.</Name>" | _egrep_o "<ResourceRecords.*</ResourceRecords>" | sed "s/<ResourceRecords>//" | sed "s#</ResourceRecords>##")"
+    _debug "_resource_record" "$_resource_record"
+  else
+    _debug "single new add"
+  fi
+
+  if [ "$_resource_record" ] && _contains "$response" "$txtvalue"; then
+    _info "The txt record already exists, skip"
+    return 0
+  fi
+
+  _debug "Adding records"
+
+  _aws_tmpl_xml="<ChangeResourceRecordSetsRequest xmlns=\"https://route53.amazonaws.com/doc/2013-04-01/\"><ChangeBatch><Changes><Change><Action>UPSERT</Action><ResourceRecordSet><Name>$fulldomain</Name><Type>TXT</Type><TTL>300</TTL><ResourceRecords>$_resource_record<ResourceRecord><Value>\"$txtvalue\"</Value></ResourceRecord></ResourceRecords></ResourceRecordSet></Change></Changes></ChangeBatch></ChangeResourceRecordSetsRequest>"
 
   if aws_rest POST "2013-04-01$_domain_id/rrset/" "" "$_aws_tmpl_xml" && _contains "$response" "ChangeResourceRecordSetsResponse"; then
     _info "txt record updated success."
@@ -56,6 +83,13 @@ dns_aws_rm() {
   fulldomain=$1
   txtvalue=$2
 
+  AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-$(_readaccountconf_mutable AWS_ACCESS_KEY_ID)}"
+  AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-$(_readaccountconf_mutable AWS_SECRET_ACCESS_KEY)}"
+
+  if [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ]; then
+    _use_container_role || _use_instance_role
+  fi
+
   _debug "First detect the root zone"
   if ! _get_root "$fulldomain"; then
     _err "invalid domain"
@@ -65,7 +99,20 @@ dns_aws_rm() {
   _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>"
+  _info "Geting existing records for $fulldomain"
+  if ! aws_rest GET "2013-04-01$_domain_id/rrset" "name=$fulldomain&type=TXT"; then
+    return 1
+  fi
+
+  if _contains "$response" "<Name>$fulldomain.</Name>"; then
+    _resource_record="$(echo "$response" | sed 's/<ResourceRecordSet>/"/g' | tr '"' "\n" | grep "<Name>$fulldomain.</Name>" | _egrep_o "<ResourceRecords.*</ResourceRecords>" | sed "s/<ResourceRecords>//" | sed "s#</ResourceRecords>##")"
+    _debug "_resource_record" "$_resource_record"
+  else
+    _debug "no records exists, skip"
+    return 0
+  fi
+
+  _aws_tmpl_xml="<ChangeResourceRecordSetsRequest xmlns=\"https://route53.amazonaws.com/doc/2013-04-01/\"><ChangeBatch><Changes><Change><Action>DELETE</Action><ResourceRecordSet><ResourceRecords>$_resource_record</ResourceRecords><Name>$fulldomain.</Name><Type>TXT</Type><TTL>300</TTL></ResourceRecordSet></Change></Changes></ChangeBatch></ChangeResourceRecordSetsRequest>"
 
   if aws_rest POST "2013-04-01$_domain_id/rrset/" "" "$_aws_tmpl_xml" && _contains "$response" "ChangeResourceRecordSetsResponse"; then
     _info "txt record deleted success."
@@ -84,9 +131,9 @@ _get_root() {
   p=1
 
   if aws_rest GET "2013-04-01/hostedzone"; then
-    _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"
@@ -102,23 +149,23 @@ _get_root() {
           fi
         fi
         #not valid
+        _err "Invalid domain"
         return 1
       fi
 
       if _contains "$response" "<Name>$h.</Name>"; then
-        hostedzone="$(echo "$response" | sed 's/<HostedZone>/#&/g' | tr '#' '\n' | _egrep_o "<HostedZone><Id>[^<]*<.Id><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)
@@ -127,6 +174,55 @@ _get_root() {
   return 1
 }
 
+_use_container_role() {
+  # automatically set if running inside ECS
+  if [ -z "$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" ]; then
+    _debug "No ECS environment variable detected"
+    return 1
+  fi
+  _use_metadata "169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"
+}
+
+_use_instance_role() {
+  _url="http://169.254.169.254/latest/meta-data/iam/security-credentials/"
+  _debug "_url" "$_url"
+  if ! _get "$_url" true 1 | _head_n 1 | grep -Fq 200; then
+    _debug "Unable to fetch IAM role from instance metadata"
+    return 1
+  fi
+  _aws_role=$(_get "$_url" "" 1)
+  _debug "_aws_role" "$_aws_role"
+  _use_metadata "$_url$_aws_role"
+}
+
+_use_metadata() {
+  _aws_creds="$(
+    _get "$1" "" 1 \
+      | _normalizeJson \
+      | tr '{,}' '\n' \
+      | while read -r _line; do
+        _key="$(echo "${_line%%:*}" | tr -d '"')"
+        _value="${_line#*:}"
+        _debug3 "_key" "$_key"
+        _secure_debug3 "_value" "$_value"
+        case "$_key" in
+          AccessKeyId) echo "AWS_ACCESS_KEY_ID=$_value" ;;
+          SecretAccessKey) echo "AWS_SECRET_ACCESS_KEY=$_value" ;;
+          Token) echo "AWS_SESSION_TOKEN=$_value" ;;
+        esac
+      done \
+        | paste -sd' ' -
+  )"
+  _secure_debug "_aws_creds" "$_aws_creds"
+
+  if [ -z "$_aws_creds" ]; then
+    return 1
+  fi
+
+  eval "$_aws_creds"
+  _using_role=true
+}
+
 #method uri qstr data
 aws_rest() {
   mtd="$1"
@@ -208,7 +304,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)"
@@ -232,6 +328,7 @@ aws_rest() {
   fi
 
   _ret="$?"
+  _debug2 response "$response"
   if [ "$_ret" = "0" ]; then
     if _contains "$response" "<ErrorResponse"; then
       _err "Response error:$response"

+ 343 - 0
dnsapi/dns_azure.sh

@@ -0,0 +1,343 @@
+#!/usr/bin/env sh
+
+WIKI="https://github.com/Neilpang/acme.sh/wiki/How-to-use-Azure-DNS"
+
+########  Public functions #####################
+
+# Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+# Used to add txt record
+#
+# Ref: https://docs.microsoft.com/en-us/rest/api/dns/recordsets/createorupdate
+#
+dns_azure_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  AZUREDNS_SUBSCRIPTIONID="${AZUREDNS_SUBSCRIPTIONID:-$(_readaccountconf_mutable AZUREDNS_SUBSCRIPTIONID)}"
+  AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}"
+  AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}"
+  AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}"
+
+  if [ -z "$AZUREDNS_SUBSCRIPTIONID" ]; then
+    AZUREDNS_SUBSCRIPTIONID=""
+    AZUREDNS_TENANTID=""
+    AZUREDNS_APPID=""
+    AZUREDNS_CLIENTSECRET=""
+    _err "You didn't specify the Azure Subscription ID "
+    return 1
+  fi
+
+  if [ -z "$AZUREDNS_TENANTID" ]; then
+    AZUREDNS_SUBSCRIPTIONID=""
+    AZUREDNS_TENANTID=""
+    AZUREDNS_APPID=""
+    AZUREDNS_CLIENTSECRET=""
+    _err "You didn't specify the Azure Tenant ID "
+    return 1
+  fi
+
+  if [ -z "$AZUREDNS_APPID" ]; then
+    AZUREDNS_SUBSCRIPTIONID=""
+    AZUREDNS_TENANTID=""
+    AZUREDNS_APPID=""
+    AZUREDNS_CLIENTSECRET=""
+    _err "You didn't specify the Azure App ID"
+    return 1
+  fi
+
+  if [ -z "$AZUREDNS_CLIENTSECRET" ]; then
+    AZUREDNS_SUBSCRIPTIONID=""
+    AZUREDNS_TENANTID=""
+    AZUREDNS_APPID=""
+    AZUREDNS_CLIENTSECRET=""
+    _err "You didn't specify the Azure Client Secret"
+    return 1
+  fi
+  #save account details to account conf file.
+  _saveaccountconf_mutable AZUREDNS_SUBSCRIPTIONID "$AZUREDNS_SUBSCRIPTIONID"
+  _saveaccountconf_mutable AZUREDNS_TENANTID "$AZUREDNS_TENANTID"
+  _saveaccountconf_mutable AZUREDNS_APPID "$AZUREDNS_APPID"
+  _saveaccountconf_mutable AZUREDNS_CLIENTSECRET "$AZUREDNS_CLIENTSECRET"
+
+  accesstoken=$(_azure_getaccess_token "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET")
+
+  if ! _get_root "$fulldomain" "$AZUREDNS_SUBSCRIPTIONID" "$accesstoken"; then
+    _err "invalid domain"
+    return 1
+  fi
+  _debug _domain_id "$_domain_id"
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  acmeRecordURI="https://management.azure.com$(printf '%s' "$_domain_id" | sed 's/\\//g')/TXT/$_sub_domain?api-version=2017-09-01"
+  _debug "$acmeRecordURI"
+  # Get existing TXT record
+  _azure_rest GET "$acmeRecordURI" "" "$accesstoken"
+  values="{\"value\":[\"$txtvalue\"]}"
+  timestamp="$(_time)"
+  if [ "$_code" = "200" ]; then
+    vlist="$(echo "$response" | _egrep_o "\"value\"\s*:\s*\[\s*\"[^\"]*\"\s*]" | cut -d : -f 2 | tr -d "[]\"")"
+    _debug "existing TXT found"
+    _debug "$vlist"
+    existingts="$(echo "$response" | _egrep_o "\"acmetscheck\"\s*:\s*\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d "\"")"
+    if [ -z "$existingts" ]; then
+      # the record was not created by acme.sh. Copy the exisiting entires
+      existingts=$timestamp
+    fi
+    _diff="$(_math "$timestamp - $existingts")"
+    _debug "existing txt age: $_diff"
+    # only use recently added records and discard if older than 2 hours because they are probably orphaned
+    if [ "$_diff" -lt 7200 ]; then
+      _debug "existing txt value: $vlist"
+      for v in $vlist; do
+        values="$values ,{\"value\":[\"$v\"]}"
+      done
+    fi
+  fi
+  # Add the txtvalue TXT Record
+  body="{\"properties\":{\"metadata\":{\"acmetscheck\":\"$timestamp\"},\"TTL\":10, \"TXTRecords\":[$values]}}"
+  _azure_rest PUT "$acmeRecordURI" "$body" "$accesstoken"
+  if [ "$_code" = "200" ] || [ "$_code" = '201' ]; then
+    _info "validation value added"
+    return 0
+  else
+    _err "error adding validation value ($_code)"
+    return 1
+  fi
+}
+
+# Usage: fulldomain txtvalue
+# Used to remove the txt record after validation
+#
+# Ref: https://docs.microsoft.com/en-us/rest/api/dns/recordsets/delete
+#
+dns_azure_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  AZUREDNS_SUBSCRIPTIONID="${AZUREDNS_SUBSCRIPTIONID:-$(_readaccountconf_mutable AZUREDNS_SUBSCRIPTIONID)}"
+  AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}"
+  AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}"
+  AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}"
+
+  if [ -z "$AZUREDNS_SUBSCRIPTIONID" ]; then
+    AZUREDNS_SUBSCRIPTIONID=""
+    AZUREDNS_TENANTID=""
+    AZUREDNS_APPID=""
+    AZUREDNS_CLIENTSECRET=""
+    _err "You didn't specify the Azure Subscription ID "
+    return 1
+  fi
+
+  if [ -z "$AZUREDNS_TENANTID" ]; then
+    AZUREDNS_SUBSCRIPTIONID=""
+    AZUREDNS_TENANTID=""
+    AZUREDNS_APPID=""
+    AZUREDNS_CLIENTSECRET=""
+    _err "You didn't specify the Azure Tenant ID "
+    return 1
+  fi
+
+  if [ -z "$AZUREDNS_APPID" ]; then
+    AZUREDNS_SUBSCRIPTIONID=""
+    AZUREDNS_TENANTID=""
+    AZUREDNS_APPID=""
+    AZUREDNS_CLIENTSECRET=""
+    _err "You didn't specify the Azure App ID"
+    return 1
+  fi
+
+  if [ -z "$AZUREDNS_CLIENTSECRET" ]; then
+    AZUREDNS_SUBSCRIPTIONID=""
+    AZUREDNS_TENANTID=""
+    AZUREDNS_APPID=""
+    AZUREDNS_CLIENTSECRET=""
+    _err "You didn't specify the Azure Client Secret"
+    return 1
+  fi
+
+  accesstoken=$(_azure_getaccess_token "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET")
+
+  if ! _get_root "$fulldomain" "$AZUREDNS_SUBSCRIPTIONID" "$accesstoken"; then
+    _err "invalid domain"
+    return 1
+  fi
+  _debug _domain_id "$_domain_id"
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  acmeRecordURI="https://management.azure.com$(printf '%s' "$_domain_id" | sed 's/\\//g')/TXT/$_sub_domain?api-version=2017-09-01"
+  _debug "$acmeRecordURI"
+  # Get existing TXT record
+  _azure_rest GET "$acmeRecordURI" "" "$accesstoken"
+  timestamp="$(_time)"
+  if [ "$_code" = "200" ]; then
+    vlist="$(echo "$response" | _egrep_o "\"value\"\s*:\s*\[\s*\"[^\"]*\"\s*]" | cut -d : -f 2 | tr -d "[]\"" | grep -v "$txtvalue")"
+    values=""
+    comma=""
+    for v in $vlist; do
+      values="$values$comma{\"value\":[\"$v\"]}"
+      comma=","
+    done
+    if [ -z "$values" ]; then
+      # No values left remove record
+      _debug "removing validation record completely $acmeRecordURI"
+      _azure_rest DELETE "$acmeRecordURI" "" "$accesstoken"
+      if [ "$_code" = "200" ] || [ "$_code" = '204' ]; then
+        _info "validation record removed"
+      else
+        _err "error removing validation record ($_code)"
+        return 1
+      fi
+    else
+      # Remove only txtvalue from the TXT Record
+      body="{\"properties\":{\"metadata\":{\"acmetscheck\":\"$timestamp\"},\"TTL\":10, \"TXTRecords\":[$values]}}"
+      _azure_rest PUT "$acmeRecordURI" "$body" "$accesstoken"
+      if [ "$_code" = "200" ] || [ "$_code" = '201' ]; then
+        _info "validation value removed"
+        return 0
+      else
+        _err "error removing validation value ($_code)"
+        return 1
+      fi
+    fi
+  fi
+}
+
+###################  Private functions below ##################################
+
+_azure_rest() {
+  m=$1
+  ep="$2"
+  data="$3"
+  accesstoken="$4"
+
+  MAX_REQUEST_RETRY_TIMES=5
+  _request_retry_times=0
+  while [ "${_request_retry_times}" -lt "$MAX_REQUEST_RETRY_TIMES" ]; do
+    _debug3 _request_retry_times "$_request_retry_times"
+    export _H1="authorization: Bearer $accesstoken"
+    export _H2="accept: application/json"
+    export _H3="Content-Type: application/json"
+    # clear headers from previous request to avoid getting wrong http code on timeouts
+    :>"$HTTP_HEADER"
+    _debug "$ep"
+    if [ "$m" != "GET" ]; then
+      _secure_debug2 "data $data"
+      response="$(_post "$data" "$ep" "" "$m")"
+    else
+      response="$(_get "$ep")"
+    fi
+    _ret="$?"
+    _secure_debug2 "response $response"
+    _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
+    _debug "http response code $_code"
+    if [ "$_code" = "401" ]; then
+      # we have an invalid access token set to expired
+      _saveaccountconf_mutable AZUREDNS_TOKENVALIDTO "0"
+      _err "access denied make sure your Azure settings are correct. See $WIKI"
+      return 1
+    fi
+    # See https://docs.microsoft.com/en-us/azure/architecture/best-practices/retry-service-specific#general-rest-and-retry-guidelines for retryable HTTP codes
+    if [ "$_ret" != "0" ] || [ -z "$_code" ] || [ "$_code" = "408" ] || [ "$_code" = "500" ] || [ "$_code" = "503" ] || [ "$_code" = "504" ]; then
+      _request_retry_times="$(_math "$_request_retry_times" + 1)"
+      _info "REST call error $_code retrying $ep in $_request_retry_times s"
+      _sleep "$_request_retry_times"
+      continue
+    fi
+    break
+  done
+  if [ "$_request_retry_times" = "$MAX_REQUEST_RETRY_TIMES" ]; then
+    _err "Error Azure REST called was retried $MAX_REQUEST_RETRY_TIMES times."
+    _err "Calling $ep failed."
+    return 1
+  fi
+  response="$(echo "$response" | _normalizeJson)"
+  return 0
+}
+
+## Ref: https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-oauth-service-to-service#request-an-access-token
+_azure_getaccess_token() {
+  tenantID=$1
+  clientID=$2
+  clientSecret=$3
+
+  accesstoken="${AZUREDNS_BEARERTOKEN:-$(_readaccountconf_mutable AZUREDNS_BEARERTOKEN)}"
+  expires_on="${AZUREDNS_TOKENVALIDTO:-$(_readaccountconf_mutable AZUREDNS_TOKENVALIDTO)}"
+
+  # can we reuse the bearer token?
+  if [ -n "$accesstoken" ] && [ -n "$expires_on" ]; then
+    if [ "$(_time)" -lt "$expires_on" ]; then
+      # brearer token is still valid - reuse it
+      _debug "reusing bearer token"
+      printf "%s" "$accesstoken"
+      return 0
+    else
+      _debug "bearer token expired"
+    fi
+  fi
+  _debug "getting new bearer token"
+
+  export _H1="accept: application/json"
+  export _H2="Content-Type: application/x-www-form-urlencoded"
+
+  body="resource=$(printf "%s" 'https://management.core.windows.net/' | _url_encode)&client_id=$(printf "%s" "$clientID" | _url_encode)&client_secret=$(printf "%s" "$clientSecret" | _url_encode)&grant_type=client_credentials"
+  _secure_debug2 "data $body"
+  response="$(_post "$body" "https://login.microsoftonline.com/$tenantID/oauth2/token" "" "POST")"
+  _ret="$?"
+  _secure_debug2 "response $response"
+  response="$(echo "$response" | _normalizeJson)"
+  accesstoken=$(echo "$response" | _egrep_o "\"access_token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
+  expires_on=$(echo "$response" | _egrep_o "\"expires_on\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
+
+  if [ -z "$accesstoken" ]; then
+    _err "no acccess token received. Check your Azure settings see $WIKI"
+    return 1
+  fi
+  if [ "$_ret" != "0" ]; then
+    _err "error $response"
+    return 1
+  fi
+  _saveaccountconf_mutable AZUREDNS_BEARERTOKEN "$accesstoken"
+  _saveaccountconf_mutable AZUREDNS_TOKENVALIDTO "$expires_on"
+  printf "%s" "$accesstoken"
+  return 0
+}
+
+_get_root() {
+  domain=$1
+  subscriptionId=$2
+  accesstoken=$3
+  i=2
+  p=1
+
+  ## Ref: https://docs.microsoft.com/en-us/rest/api/dns/zones/list
+  ## returns up to 100 zones in one response therefore handling more results is not not implemented
+  ## (ZoneListResult with  continuation token for the next page of results)
+  ## Per https://docs.microsoft.com/en-us/azure/azure-subscription-service-limits#dns-limits you are limited to 100 Zone/subscriptions anyways
+  ##
+  _azure_rest GET "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.Network/dnszones?api-version=2017-09-01" "" "$accesstoken"
+  # Find matching domain name is Json response
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    _debug2 "Checking domain: $h"
+    if [ -z "$h" ]; then
+      #not valid
+      _err "Invalid domain"
+      return 1
+    fi
+
+    if _contains "$response" "\"name\":\"$h\"" >/dev/null; then
+      _domain_id=$(echo "$response" | _egrep_o "\{\"id\":\"[^\"]*$h\"" | head -n 1 | cut -d : -f 2 | tr -d \")
+      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
+}

+ 31 - 28
dnsapi/dns_cf.sh

@@ -19,8 +19,8 @@ dns_cf_add() {
   if [ -z "$CF_Key" ] || [ -z "$CF_Email" ]; then
     CF_Key=""
     CF_Email=""
-    _err "You don't specify cloudflare api key and email yet."
-    _err "Please create you key and try again."
+    _err "You didn't specify a cloudflare api key and email yet."
+    _err "Please create the key and try again."
     return 1
   fi
 
@@ -51,33 +51,36 @@ dns_cf_add() {
     return 1
   fi
 
-  count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2)
-  _debug count "$count"
-  if [ "$count" = "0" ]; then
-    _info "Adding record"
-    if _cf_rest POST "zones/$_domain_id/dns_records" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":120}"; then
-      if printf -- "%s" "$response" | grep "$fulldomain" >/dev/null; then
-        _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_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | head -n 1)
-    _debug "record_id" "$record_id"
-
-    _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, OK"
+  # For wildcard cert, the main root domain and the wildcard domain have the same txt subdomain name, so
+  # we can not use updating anymore.
+  #  count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2)
+  #  _debug count "$count"
+  #  if [ "$count" = "0" ]; then
+  _info "Adding record"
+  if _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, OK"
       return 0
+    else
+      _err "Add txt record error."
+      return 1
     fi
-    _err "Update error"
-    return 1
   fi
+  _err "Add txt record error."
+  return 1
+  #  else
+  #    _info "Updating record"
+  #    record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | head -n 1)
+  #    _debug "record_id" "$record_id"
+  #
+  #    _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, OK"
+  #      return 0
+  #    fi
+  #    _err "Update error"
+  #    return 1
+  #  fi
 
 }
 
@@ -91,8 +94,8 @@ dns_cf_rm() {
   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."
+    _err "You didn't specify a cloudflare api key and email yet."
+    _err "Please create the key and try again."
     return 1
   fi
 

+ 59 - 45
dnsapi/dns_cloudns.sh

@@ -4,6 +4,7 @@
 # Repository: https://github.com/ClouDNS/acme.sh/
 
 #CLOUDNS_AUTH_ID=XXXXX
+#CLOUDNS_SUB_AUTH_ID=XXXXX
 #CLOUDNS_AUTH_PASSWORD="YYYYYYYYY"
 CLOUDNS_API="https://api.cloudns.net"
 
@@ -25,30 +26,18 @@ dns_cloudns_add() {
 
   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."
+  _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."
 
   return 0
 }
@@ -71,22 +60,32 @@ dns_cloudns_rm() {
 
   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"
+  _dns_cloudns_http_api_call "dns/records.json" "domain-name=$zone&host=$host&type=TXT"
+  if ! _contains "$response" "\"id\":"; then
+    return 1
+  fi
 
-  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
+  for i in $(echo "$response" | tr '{' "\n" | grep "$record"); do
+    record_id=$(echo "$i" | tr ',' "\n" | grep -E '^"id"' | sed -re 's/^\"id\"\:\"([0-9]+)\"$/\1/g')
+
+    if [ ! -z "$record_id" ]; then
+      _debug zone "$zone"
+      _debug host "$host"
+      _debug record "$record"
+      _debug record_id "$record_id"
+
+      _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."
+      else
+        _info "Deleted."
+      fi
     fi
-    _info "Deleted."
-  fi
+  done
+
   return 0
 }
 
@@ -96,8 +95,20 @@ _dns_cloudns_init_check() {
     return 0
   fi
 
-  if [ -z "$CLOUDNS_AUTH_ID" ]; then
-    _err "CLOUDNS_AUTH_ID is not configured"
+  CLOUDNS_AUTH_ID="${CLOUDNS_AUTH_ID:-$(_readaccountconf_mutable CLOUDNS_AUTH_ID)}"
+  CLOUDNS_SUB_AUTH_ID="${CLOUDNS_SUB_AUTH_ID:-$(_readaccountconf_mutable CLOUDNS_SUB_AUTH_ID)}"
+  CLOUDNS_AUTH_PASSWORD="${CLOUDNS_AUTH_PASSWORD:-$(_readaccountconf_mutable CLOUDNS_AUTH_PASSWORD)}"
+  if [ -z "$CLOUDNS_AUTH_ID$CLOUDNS_SUB_AUTH_ID" ] || [ -z "$CLOUDNS_AUTH_PASSWORD" ]; then
+    CLOUDNS_AUTH_ID=""
+    CLOUDNS_SUB_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" ] && [ -z "$CLOUDNS_SUB_AUTH_ID" ]; then
+    _err "CLOUDNS_AUTH_ID or CLOUDNS_SUB_AUTH_ID is not configured"
     return 1
   fi
 
@@ -113,6 +124,11 @@ _dns_cloudns_init_check() {
     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_SUB_AUTH_ID "$CLOUDNS_SUB_AUTH_ID"
+  _saveaccountconf_mutable CLOUDNS_AUTH_PASSWORD "$CLOUDNS_AUTH_PASSWORD"
+
   CLOUDNS_INIT_CHECK_COMPLETED=1
 
   return 0
@@ -141,30 +157,28 @@ _dns_cloudns_get_zone_name() {
   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_SUB_AUTH_ID "$CLOUDNS_SUB_AUTH_ID"
   _debug CLOUDNS_AUTH_PASSWORD "$CLOUDNS_AUTH_PASSWORD"
 
+  if [ ! -z "$CLOUDNS_SUB_AUTH_ID" ]; then
+    auth_user="sub-auth-id=$CLOUDNS_SUB_AUTH_ID"
+  else
+    auth_user="auth-id=$CLOUDNS_AUTH_ID"
+  fi
+
   if [ -z "$2" ]; then
-    data="auth-id=$CLOUDNS_AUTH_ID&auth-password=$CLOUDNS_AUTH_PASSWORD"
+    data="$auth_user&auth-password=$CLOUDNS_AUTH_PASSWORD"
   else
-    data="auth-id=$CLOUDNS_AUTH_ID&auth-password=$CLOUDNS_AUTH_PASSWORD&$2"
+    data="$auth_user&auth-password=$CLOUDNS_AUTH_PASSWORD&$2"
   fi
 
   response="$(_get "$CLOUDNS_API/$method?$data")"
 
-  _debug2 response "$response"
+  _debug response "$response"
 
   return 0
 }

+ 5 - 39
dnsapi/dns_cx.sh

@@ -36,33 +36,18 @@ dns_cx_add() {
     return 1
   fi
 
-  existing_records "$_domain" "$_sub_domain"
-  _debug count "$count"
-  if [ "$?" != "0" ]; then
-    _err "Error get existing records."
-    return 1
-  fi
-
-  if [ "$count" = "0" ]; then
-    add_record "$_domain" "$_sub_domain" "$txtvalue"
-  else
-    update_record "$_domain" "$_sub_domain" "$txtvalue"
-  fi
-
-  if [ "$?" = "0" ]; then
-    return 0
-  fi
-  return 1
+  add_record "$_domain" "$_sub_domain" "$txtvalue"
 }
 
-#fulldomain
+#fulldomain txtvalue
 dns_cx_rm() {
   fulldomain=$1
+  txtvalue=$2
   REST_API="$CX_Api"
   if _get_root "$fulldomain"; then
     record_id=""
-    existing_records "$_domain" "$_sub_domain"
-    if ! [ "$record_id" = "" ]; then
+    existing_records "$_domain" "$_sub_domain" "$txtvalue"
+    if [ "$record_id" ]; then
       _rest DELETE "record/$record_id/$_domain_id" "{}"
       _info "Deleted record ${fulldomain}"
     fi
@@ -77,7 +62,6 @@ 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
@@ -89,7 +73,6 @@ existing_records() {
   fi
 
   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 \" | _head_n 1)
     _debug record_id "$record_id"
     return 0
@@ -114,23 +97,6 @@ add_record() {
   return 0
 }
 
-#update the txt record
-#Usage: root sub txtvalue
-update_record() {
-  root=$1
-  sub=$2
-  txtvalue=$3
-  fulldomain="$sub.$root"
-
-  _info "Updating record"
-
-  if _rest PUT "record/$record_id" "{\"domain_id\": $_domain_id, \"host\":\"$_sub_domain\", \"value\":\"$txtvalue\", \"type\":\"TXT\",\"ttl\":600, \"line_id\":1}"; then
-    return 0
-  fi
-
-  return 1
-}
-
 ####################  Private functions below ##################################
 #_acme-challenge.www.domain.com
 #returns

+ 184 - 0
dnsapi/dns_da.sh

@@ -0,0 +1,184 @@
+#!/usr/bin/env sh
+# -*- mode: sh; tab-width: 2; indent-tabs-mode: s; coding: utf-8 -*-
+# vim: et ts=2 sw=2
+#
+# DirectAdmin 1.41.0 API
+# The DirectAdmin interface has it's own Let's encrypt functionality, but this
+# script can be used to generate certificates for names which are not hosted on
+# DirectAdmin
+#
+# User must provide login data and URL to DirectAdmin incl. port.
+# You can create login key, by using the Login Keys function
+# ( https://da.example.com:8443/CMD_LOGIN_KEYS ), which only has access to 
+# - CMD_API_DNS_CONTROL
+# - CMD_API_SHOW_DOMAINS
+#
+# See also https://www.directadmin.com/api.php and
+# https://www.directadmin.com/features.php?id=1298
+#
+# Report bugs to https://github.com/TigerP/acme.sh/issues
+#
+# Values to export:
+# export DA_Api="https://remoteUser:remotePassword@da.example.com:8443"
+# export DA_Api_Insecure=1
+#
+# Set DA_Api_Insecure to 1 for insecure and 0 for secure -> difference is
+# whether ssl cert is checked for validity (0) or whether it is just accepted
+# (1)
+#
+########  Public functions #####################
+
+# Usage: dns_myapi_add  _acme-challenge.www.example.com  "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+# Used to add txt record
+dns_da_add() {
+  fulldomain="${1}"
+  txtvalue="${2}"
+  _debug "Calling: dns_da_add() '${fulldomain}' '${txtvalue}'"
+  _DA_credentials && _DA_getDomainInfo && _DA_addTxt
+}
+
+# Usage: dns_da_rm  _acme-challenge.www.example.com  "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+# Used to remove the txt record after validation
+dns_da_rm() {
+  fulldomain="${1}"
+  txtvalue="${2}"
+  _debug "Calling: dns_da_rm() '${fulldomain}' '${txtvalue}'"
+  _DA_credentials && _DA_getDomainInfo && _DA_rmTxt
+}
+
+####################  Private functions below ##################################
+# Usage: _DA_credentials
+# It will check if the needed settings are available
+_DA_credentials() {
+  DA_Api="${DA_Api:-$(_readaccountconf_mutable DA_Api)}"
+  DA_Api_Insecure="${DA_Api_Insecure:-$(_readaccountconf_mutable DA_Api_Insecure)}"
+  if [ -z "${DA_Api}" ] || [ -z "${DA_Api_Insecure}" ]; then
+    DA_Api=""
+    DA_Api_Insecure=""
+    _err "You haven't specified the DirectAdmin Login data, URL and whether you want check the DirectAdmin SSL cert. Please try again."
+    return 1
+  else
+    _saveaccountconf_mutable DA_Api "${DA_Api}"
+    _saveaccountconf_mutable DA_Api_Insecure "${DA_Api_Insecure}"
+    # Set whether curl should use secure or insecure mode
+    export HTTPS_INSECURE="${DA_Api_Insecure}"
+  fi
+}
+
+# Usage: _get_root _acme-challenge.www.example.com
+# Split the full domain to a domain and subdomain
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=example.com
+_get_root() {
+  domain=$1
+  i=2
+  p=1
+  # Get a list of all the domains
+  # response will contain "list[]=example.com&list[]=example.org"
+  _da_api CMD_API_SHOW_DOMAINS "" "${domain}"
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    _debug h "$h"
+    if [ -z "$h" ]; then
+      # not valid
+      _debug "The given domain $h is not valid"
+      return 1
+    fi
+    if _contains "$response" "$h" >/dev/null; then
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      _domain=$h
+      return 0
+    fi
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+  _debug "Stop on 100"
+  return 1
+}
+
+# Usage: _da_api CMD_API_* data example.com
+# Use the DirectAdmin API and check the result
+# returns
+#  response="error=0&text=Result text&details="
+_da_api() {
+  cmd=$1
+  data=$2
+  domain=$3
+  _debug "$domain; $data"
+  response="$(_post "$data" "$DA_Api/$cmd" "" "POST")"
+
+  if [ "$?" != "0" ]; then
+    _err "error $cmd"
+    return 1
+  fi
+  _debug response "$response"
+
+  case "${cmd}" in
+    CMD_API_DNS_CONTROL)
+      # Parse the result in general
+      # error=0&text=Records Deleted&details=
+      # error=1&text=Cannot View Dns Record&details=No domain provided
+      err_field="$(_getfield "$response" 1 '&')"
+      txt_field="$(_getfield "$response" 2 '&')"
+      details_field="$(_getfield "$response" 3 '&')"
+      error="$(_getfield "$err_field" 2 '=')"
+      text="$(_getfield "$txt_field" 2 '=')"
+      details="$(_getfield "$details_field" 2 '=')"
+      _debug "error: ${error}, text: ${text}, details: ${details}"
+      if [ "$error" != "0" ]; then
+        _err "error $response"
+        return 1
+      fi
+      ;;
+    CMD_API_SHOW_DOMAINS) ;;
+  esac
+  return 0
+}
+
+# Usage: _DA_getDomainInfo
+# Get the root zone if possible
+_DA_getDomainInfo() {
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  else
+    _debug "The root domain: $_domain"
+    _debug "The sub domain: $_sub_domain"
+  fi
+  return 0
+}
+
+# Usage: _DA_addTxt
+# Use the API to add a record
+_DA_addTxt() {
+  curData="domain=${_domain}&action=add&type=TXT&name=${_sub_domain}&value=\"${txtvalue}\""
+  _debug "Calling _DA_addTxt: '${curData}' '${DA_Api}/CMD_API_DNS_CONTROL'"
+  _da_api CMD_API_DNS_CONTROL "${curData}" "${_domain}"
+  _debug "Result of _DA_addTxt: '$response'"
+  if _contains "${response}" 'error=0'; then
+    _debug "Add TXT succeeded"
+    return 0
+  fi
+  _debug "Add TXT failed"
+  return 1
+}
+
+# Usage: _DA_rmTxt
+# Use the API to remove a record
+_DA_rmTxt() {
+  curData="domain=${_domain}&action=select&txtrecs0=name=${_sub_domain}&amp;value=\"${txtvalue}\""
+  _debug "Calling _DA_rmTxt: '${curData}' '${DA_Api}/CMD_API_DNS_CONTROL'"
+  if _da_api CMD_API_DNS_CONTROL "${curData}" "${_domain}"; then
+    _debug "Result of _DA_rmTxt: '$response'"
+  else
+    _err "Result of _DA_rmTxt: '$response'"
+  fi
+  if _contains "${response}" 'error=0'; then
+    _debug "RM TXT succeeded"
+    return 0
+  fi
+  _debug "RM TXT failed"
+  return 1
+}

+ 25 - 5
dnsapi/dns_dgon.sh

@@ -20,12 +20,22 @@
 dns_dgon_add() {
   fulldomain="$(echo "$1" | _lower_case)"
   txtvalue=$2
+
+  DO_API_KEY="${DO_API_KEY:-$(_readaccountconf_mutable DO_API_KEY)}"
+  # Check if API Key Exist
+  if [ -z "$DO_API_KEY" ]; then
+    DO_API_KEY=""
+    _err "You did not specify DigitalOcean API key."
+    _err "Please export DO_API_KEY and try again."
+    return 1
+  fi
+
   _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"
+  _saveaccountconf_mutable DO_API_KEY "$DO_API_KEY"
 
   ## split the domain for DO API
   if ! _get_base_domain "$fulldomain"; then
@@ -39,7 +49,7 @@ dns_dgon_add() {
   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'"}'
+  PBODY='{"type":"TXT","name":"'$_sub_domain'","data":"'$txtvalue'","ttl":120}'
 
   _debug PURL "$PURL"
   _debug PBODY "$PBODY"
@@ -65,6 +75,16 @@ dns_dgon_add() {
 dns_dgon_rm() {
   fulldomain="$(echo "$1" | _lower_case)"
   txtvalue=$2
+
+  DO_API_KEY="${DO_API_KEY:-$(_readaccountconf_mutable DO_API_KEY)}"
+  # Check if API Key Exist
+  if [ -z "$DO_API_KEY" ]; then
+    DO_API_KEY=""
+    _err "You did not specify DigitalOcean API key."
+    _err "Please export DO_API_KEY and try again."
+    return 1
+  fi
+
   _info "Using digitalocean dns validation - remove record"
   _debug fulldomain "$fulldomain"
   _debug txtvalue "$txtvalue"
@@ -92,11 +112,11 @@ dns_dgon_rm() {
     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\"")"
+    record="$(echo "$domain_list" | _egrep_o "\"id\"\s*\:\s*\"*[0-9]+\"*[^}]*\"name\"\s*\:\s*\"$_sub_domain\"[^}]*\"data\"\s*\:\s*\"$txtvalue\"")"
     ## 3) check record and get next page
     if [ -z "$record" ]; then
       ## find the next page if we dont have a match
-      nextpage="$(echo "$domain_list" | _egrep_o "\"links\".*" | _egrep_o "\"next\".*" | _egrep_o "http.*page\=\d+")"
+      nextpage="$(echo "$domain_list" | _egrep_o "\"links\".*" | _egrep_o "\"next\".*" | _egrep_o "http.*page\=[0-9]+")"
       if [ -z "$nextpage" ]; then
         _err "no record and no nextpage in digital ocean DNS removal"
         return 1
@@ -108,7 +128,7 @@ dns_dgon_rm() {
   done
 
   ## we found the record
-  rec_id="$(echo "$record" | _egrep_o "id\"\s*\:\s*\"*\d+" | _egrep_o "\d+")"
+  rec_id="$(echo "$record" | _egrep_o "id\"\s*\:\s*\"*[0-9]+" | _egrep_o "[0-9]+")"
   _debug rec_id "$rec_id"
 
   ## delete the record

+ 10 - 72
dnsapi/dns_dp.sh

@@ -15,6 +15,8 @@ dns_dp_add() {
   fulldomain=$1
   txtvalue=$2
 
+  DP_Id="${DP_Id:-$(_readaccountconf_mutable DP_Id)}"
+  DP_Key="${DP_Key:-$(_readaccountconf_mutable DP_Key)}"
   if [ -z "$DP_Id" ] || [ -z "$DP_Key" ]; then
     DP_Id=""
     DP_Key=""
@@ -24,8 +26,8 @@ dns_dp_add() {
   fi
 
   #save the api key and email to the account conf file.
-  _saveaccountconf DP_Id "$DP_Id"
-  _saveaccountconf DP_Key "$DP_Key"
+  _saveaccountconf_mutable DP_Id "$DP_Id"
+  _saveaccountconf_mutable DP_Key "$DP_Key"
 
   _debug "First detect the root zone"
   if ! _get_root "$fulldomain"; then
@@ -33,24 +35,18 @@ dns_dp_add() {
     return 1
   fi
 
-  existing_records "$_domain" "$_sub_domain"
-  _debug count "$count"
-  if [ "$?" != "0" ]; then
-    _err "Error get existing records."
-    return 1
-  fi
+  add_record "$_domain" "$_sub_domain" "$txtvalue"
 
-  if [ "$count" = "0" ]; then
-    add_record "$_domain" "$_sub_domain" "$txtvalue"
-  else
-    update_record "$_domain" "$_sub_domain" "$txtvalue"
-  fi
 }
 
 #fulldomain txtvalue
 dns_dp_rm() {
   fulldomain=$1
   txtvalue=$2
+
+  DP_Id="${DP_Id:-$(_readaccountconf_mutable DP_Id)}"
+  DP_Key="${DP_Key:-$(_readaccountconf_mutable DP_Key)}"
+
   _debug "First detect the root zone"
   if ! _get_root "$fulldomain"; then
     _err "invalid domain"
@@ -83,37 +79,6 @@ dns_dp_rm() {
 
 }
 
-#usage:  root  sub
-#return if the sub record already exists.
-#echos the existing records count.
-# '0' means doesn't exist
-existing_records() {
-  _debug "Getting txt records"
-  root=$1
-  sub=$2
-
-  if ! _rest POST "Record.List" "login_token=$DP_Id,$DP_Key&domain_id=$_domain_id&sub_domain=$_sub_domain"; then
-    return 1
-  fi
-
-  if _contains "$response" 'No records'; then
-    count=0
-    return 0
-  fi
-
-  if _contains "$response" "Action completed successful"; then
-    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."
-    return 1
-  fi
-
-  count=0
-}
-
 #add the txt record.
 #usage: root  sub  txtvalue
 add_record() {
@@ -128,34 +93,7 @@ add_record() {
     return 1
   fi
 
-  if _contains "$response" "Action completed successful"; then
-
-    return 0
-  fi
-
-  return 1 #error
-}
-
-#update the txt record
-#Usage: root sub txtvalue
-update_record() {
-  root=$1
-  sub=$2
-  txtvalue=$3
-  fulldomain="$sub.$root"
-
-  _info "Updating record"
-
-  if ! _rest POST "Record.Modify" "login_token=$DP_Id,$DP_Key&format=json&domain_id=$_domain_id&sub_domain=$_sub_domain&record_type=TXT&value=$txtvalue&record_line=默认&record_id=$record_id"; then
-    return 1
-  fi
-
-  if _contains "$response" "Action completed successful"; then
-
-    return 0
-  fi
-
-  return 1 #error
+  _contains "$response" "Action completed successful" || _contains "$response" "Domain record already exists"
 }
 
 ####################  Private functions below ##################################

+ 97 - 0
dnsapi/dns_dreamhost.sh

@@ -0,0 +1,97 @@
+#!/usr/bin/env sh
+
+#Author: RhinoLance
+#Report Bugs here: https://github.com/RhinoLance/acme.sh
+#
+
+#define the api endpoint
+DH_API_ENDPOINT="https://api.dreamhost.com/"
+querystring=""
+
+########  Public functions #####################
+
+#Usage: dns_myapi_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_dreamhost_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if ! validate "$fulldomain" "$txtvalue"; then
+    return 1
+  fi
+
+  querystring="key=$DH_API_KEY&cmd=dns-add_record&record=$fulldomain&type=TXT&value=$txtvalue"
+  if ! submit "$querystring"; then
+    return 1
+  fi
+
+  return 0
+}
+
+#Usage: fulldomain txtvalue
+#Remove the txt record after validation.
+dns_dreamhost_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if ! validate "$fulldomain" "$txtvalue"; then
+    return 1
+  fi
+
+  querystring="key=$DH_API_KEY&cmd=dns-remove_record&record=$fulldomain&type=TXT&value=$txtvalue"
+  if ! submit "$querystring"; then
+    return 1
+  fi
+
+  return 0
+}
+
+####################  Private functions below ##################################
+
+#send the command to the api endpoint.
+submit() {
+  querystring=$1
+
+  url="$DH_API_ENDPOINT?$querystring"
+
+  _debug url "$url"
+
+  if ! response="$(_get "$url")"; then
+    _err "Error <$1>"
+    return 1
+  fi
+
+  if [ -z "$2" ]; then
+    message="$(echo "$response" | _egrep_o "\"Message\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")"
+    if [ -n "$message" ]; then
+      _err "$message"
+      return 1
+    fi
+  fi
+
+  _debug response "$response"
+
+  return 0
+}
+
+#check that we have a valid API Key
+validate() {
+  fulldomain=$1
+  txtvalue=$2
+
+  _info "Using dreamhost"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  #retrieve the API key from the environment variable if it exists, otherwise look for a saved key.
+  DH_API_KEY="${DH_API_KEY:-$(_readaccountconf_mutable DH_API_KEY)}"
+
+  if [ -z "$DH_API_KEY" ]; then
+    DH_API_KEY=""
+    _err "You didn't specify the DreamHost api key yet (export DH_API_KEY=\"<api key>\")"
+    _err "Please login to your control panel, create a key and try again."
+    return 1
+  fi
+
+  #save the api key to the account conf file.
+  _saveaccountconf_mutable DH_API_KEY "$DH_API_KEY"
+}

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

+ 22 - 10
dnsapi/dns_dynu.sh

@@ -122,18 +122,30 @@ dns_dynu_rm() {
 # _domain_name=domain.com
 _get_root() {
   domain=$1
-  if ! _dynu_rest GET "dns/getroot/$domain"; then
-    return 1
-  fi
+  i=2
+  p=1
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    _debug h "$h"
+    if [ -z "$h" ]; then
+      #not valid
+      return 1
+    fi
 
-  if ! _contains "$response" "domain_name"; then
-    _debug "Domain name not found."
-    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
 
-  _domain_name=$(printf "%s" "$response" | tr -d "{}" | cut -d , -f 1 | cut -d : -f 2 | cut -d '"' -f 2)
-  _node=$(printf "%s" "$response" | tr -d "{}" | cut -d , -f 3 | cut -d : -f 2 | cut -d '"' -f 2)
-  return 0
 }
 
 _get_recordid() {

+ 64 - 113
dnsapi/dns_freedns.sh

@@ -53,6 +53,9 @@ dns_freedns_add() {
   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
@@ -71,18 +74,9 @@ dns_freedns_add() {
       return 1
     fi
 
-    # Now convert the tables in the HTML to CSV.  This litte gem from
-    # http://stackoverflow.com/questions/1403087/how-can-i-convert-an-html-table-to-csv
-    subdomain_csv="$(echo "$htmlpage" \
-      | grep -i -e '</\?TABLE\|</\?TD\|</\?TR\|</\?TH' \
-      | sed 's/^[\ \t]*//g' \
-      | tr -d '\n' \
-      | sed 's/<\/TR[^>]*>/\n/Ig' \
-      | sed 's/<\/\?\(TABLE\|TR\)[^>]*>//Ig' \
-      | sed 's/^<T[DH][^>]*>\|<\/\?T[DH][^>]*>$//Ig' \
-      | sed 's/<\/T[DH][^>]*><T[DH][^>]*>/,/Ig' \
-      | grep 'edit.php?' \
-      | grep "$top_domain")"
+    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")"
+    _debug3 "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
@@ -90,55 +84,27 @@ dns_freedns_add() {
 
     # Now we have to read through this table and extract the data we need
     lines="$(echo "$subdomain_csv" | wc -l)"
-    nl='
-'
     i=0
     found=0
+    DNSdomainid=""
     while [ "$i" -lt "$lines" ]; do
       i="$(_math "$i" + 1)"
-      line="$(echo "$subdomain_csv" | cut -d "$nl" -f "$i")"
-      tmp="$(echo "$line" | cut -d ',' -f 1)"
-      if [ $found = 0 ] && _startswith "$tmp" "<td>$top_domain"; then
+      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" | cut -d ',' -f 2 | sed 's/^.*domain_id=//;s/>.*//')"
+        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" | cut -d ',' -f 2 | sed 's/^[^>]*>//;s/<\/a>.*//')"
-        DNStype="$(echo "$line" | cut -d ',' -f 3)"
-        if [ "$DNSname" = "$fulldomain" ] && [ "$DNStype" = "TXT" ]; then
-          DNSdataid="$(echo "$line" | cut -d ',' -f 2 | sed 's/^.*data_id=//;s/>.*//')"
-          # 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" | cut -d ',' -f 4 | sed 's/^[^&quot;]*&quot;//;s/&quot;.*//;s/<\/td>.*//')"
-          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
+        break
       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
@@ -150,34 +116,10 @@ dns_freedns_add() {
     _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)
-      _freedns_delete_txt_record "$FREEDNS_COOKIE" "$DNSdataid"
-      if [ "$?" = "0" ]; 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
+  # Add in new TXT record with the value provided
+  _debug "Adding TXT record for $fulldomain, $txtvalue"
+  _freedns_add_txt_record "$FREEDNS_COOKIE" "$DNSdomainid" "$sub_domain" "$txtvalue"
+  return $?
 }
 
 #Usage: fulldomain txtvalue
@@ -210,18 +152,9 @@ dns_freedns_rm() {
       return 1
     fi
 
-    # Now convert the tables in the HTML to CSV.  This litte gem from
-    # http://stackoverflow.com/questions/1403087/how-can-i-convert-an-html-table-to-csv
-    subdomain_csv="$(echo "$htmlpage" \
-      | grep -i -e '</\?TABLE\|</\?TD\|</\?TR\|</\?TH' \
-      | sed 's/^[\ \t]*//g' \
-      | tr -d '\n' \
-      | sed 's/<\/TR[^>]*>/\n/Ig' \
-      | sed 's/<\/\?\(TABLE\|TR\)[^>]*>//Ig' \
-      | sed 's/^<T[DH][^>]*>\|<\/\?T[DH][^>]*>$//Ig' \
-      | sed 's/<\/T[DH][^>]*><T[DH][^>]*>/,/Ig' \
-      | grep 'edit.php?' \
-      | grep "$fulldomain")"
+    subdomain_csv="$(echo "$htmlpage" | tr -d "\n\r" | _egrep_o '<form .*</form>' | sed 's/<tr>/@<tr>/g' | tr '@' '\n' | grep edit.php | grep "$fulldomain")"
+    _debug3 "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
@@ -229,35 +162,53 @@ dns_freedns_rm() {
 
     # Now we have to read through this table and extract the data we need
     lines="$(echo "$subdomain_csv" | wc -l)"
-    nl='
-'
     i=0
     found=0
+    DNSdataid=""
     while [ "$i" -lt "$lines" ]; do
       i="$(_math "$i" + 1)"
-      line="$(echo "$subdomain_csv" | cut -d "$nl" -f "$i")"
-      DNSname="$(echo "$line" | cut -d ',' -f 2 | sed 's/^[^>]*>//;s/<\/a>.*//')"
-      DNStype="$(echo "$line" | cut -d ',' -f 3)"
-      if [ "$DNSname" = "$fulldomain" ] && [ "$DNStype" = "TXT" ]; then
-        DNSdataid="$(echo "$line" | cut -d ',' -f 2 | sed 's/^.*data_id=//;s/>.*//')"
-        DNSvalue="$(echo "$line" | cut -d ',' -f 4 | sed 's/^[^&quot;]*&quot;//;s/&quot;.*//;s/<\/td>.*//')"
-        _debug "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
+      line="$(echo "$subdomain_csv" | sed -n "${i}p")"
+      _debug3 "line: $line"
+      DNSname="$(echo "$line" | _egrep_o 'edit.php.*</a>' | cut -d '>' -f 2 | cut -d '<' -f 1)"
+      _debug2 "DNSname: $DNSname"
+      if [ "$DNSname" = "$fulldomain" ]; then
+        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 [ "$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)"
+          if _startswith "$DNSvalue" "&quot;"; then
+            # remove the quotation from the start
+            DNSvalue="$(echo "$DNSvalue" | cut -c 7-)"
+          fi
+          if _endswith "$DNSvalue" "..."; then
+            # value was truncated, remove the dot dot dot from the end
+            DNSvalue="$(echo "$DNSvalue" | sed 's/...$//')"
+          elif _endswith "$DNSvalue" "&quot;"; then
+            # else remove the closing quotation from the end
+            DNSvalue="$(echo "$DNSvalue" | sed 's/......$//')"
+          fi
+          _debug2 "DNSvalue: $DNSvalue"
+
+          if [ -n "$DNSdataid" ] && _startswith "$txtvalue" "$DNSvalue"; then
+            # Found a match. But note... Website is truncating the
+            # value field so we are only testing that part that is not 
+            # truncated.  This should be accurate enough.
+            _debug "Deleting TXT record for $fulldomain, $txtvalue"
+            _freedns_delete_txt_record "$FREEDNS_COOKIE" "$DNSdataid"
+            return $?
+          fi
+
+        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"
+  _debug3 "$subdomain_csv"
+  _info "Cannot delete TXT record for $fulldomain, $txtvalue. Does not exist at FreeDNS"
   return 0
 }
 
@@ -285,7 +236,7 @@ _freedns_login() {
 
   # if cookies is not empty then logon successful
   if [ -z "$cookies" ]; then
-    _debug "$htmlpage"
+    _debug3 "htmlpage: $htmlpage"
     _err "FreeDNS login failed for user $username. Check $HTTP_HEADER file"
     return 1
   fi
@@ -314,7 +265,7 @@ _freedns_retrieve_subdomain_page() {
     return 1
   fi
 
-  _debug2 "$htmlpage"
+  _debug3 "htmlpage: $htmlpage"
 
   printf "%s" "$htmlpage"
   return 0
@@ -328,7 +279,7 @@ _freedns_add_txt_record() {
   domain_id="$2"
   subdomain="$3"
   value="$(printf '%s' "$4" | _url_encode)"
-  url="http://freedns.afraid.org/subdomain/save.php?step=2"
+  url="https://freedns.afraid.org/subdomain/save.php?step=2"
 
   htmlpage="$(_post "type=TXT&domain_id=$domain_id&subdomain=$subdomain&address=%22$value%22&send=Save%21" "$url")"
 
@@ -336,17 +287,17 @@ _freedns_add_txt_record() {
     _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"
+    _debug3 "htmlpage: $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"
+    _debug3 "htmlpage: $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"
+  _debug3 "htmlpage: $htmlpage"
   _info "Added acme challenge TXT record for $fulldomain at FreeDNS"
   return 0
 }
@@ -365,7 +316,7 @@ _freedns_delete_txt_record() {
     _err "FreeDNS failed to delete TXT record for $data_id bad RC from _get"
     return 1
   elif ! _contains "$htmlheader" "200 OK"; then
-    _debug "$htmlheader"
+    _debug2 "htmlheader: $htmlheader"
     _err "FreeDNS failed to delete TXT record $data_id"
     return 1
   fi

+ 2 - 2
dnsapi/dns_gandi_livedns.sh

@@ -11,7 +11,7 @@
 #
 ########  Public functions #####################
 
-GANDI_LIVEDNS_API="https://dns.beta.gandi.net/api/v5"
+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() {
@@ -37,7 +37,7 @@ dns_gandi_livedns_add() {
   _debug sub_domain "$_sub_domain"
 
   _gandi_livedns_rest PUT "domains/$_domain/records/$_sub_domain/TXT" "{\"rrset_ttl\": 300, \"rrset_values\":[\"$txtvalue\"]}" \
-    && _contains "$response" '{"message": "Zone Record Created"}' \
+    && _contains "$response" '{"message": "DNS Record Created"}' \
     && _info "Add $(__green "success")"
 }
 

+ 64 - 3
dnsapi/dns_gd.sh

@@ -15,6 +15,8 @@ dns_gd_add() {
   fulldomain=$1
   txtvalue=$2
 
+  GD_Key="${GD_Key:-$(_readaccountconf_mutable GD_Key)}"
+  GD_Secret="${GD_Secret:-$(_readaccountconf_mutable GD_Secret)}"
   if [ -z "$GD_Key" ] || [ -z "$GD_Secret" ]; then
     GD_Key=""
     GD_Secret=""
@@ -24,8 +26,8 @@ dns_gd_add() {
   fi
 
   #save the api key and email to the account conf file.
-  _saveaccountconf GD_Key "$GD_Key"
-  _saveaccountconf GD_Secret "$GD_Secret"
+  _saveaccountconf_mutable GD_Key "$GD_Key"
+  _saveaccountconf_mutable GD_Secret "$GD_Secret"
 
   _debug "First detect the root zone"
   if ! _get_root "$fulldomain"; then
@@ -36,8 +38,27 @@ dns_gd_add() {
   _debug _sub_domain "$_sub_domain"
   _debug _domain "$_domain"
 
+  _debug "Getting existing records"
+  if ! _gd_rest GET "domains/$_domain/records/TXT/$_sub_domain"; then
+    return 1
+  fi
+
+  if _contains "$response" "$txtvalue"; then
+    _info "The record is existing, skip"
+    return 0
+  fi
+
+  _add_data="{\"data\":\"$txtvalue\"}"
+  for t in $(echo "$response" | tr '{' "\n" | grep "\"name\":\"$_sub_domain\"" | tr ',' "\n" | grep '"data"' | cut -d : -f 2); do
+    _debug2 t "$t"
+    if [ "$t" ]; then
+      _add_data="$_add_data,{\"data\":$t}"
+    fi
+  done
+  _debug2 _add_data "$_add_data"
+
   _info "Adding record"
-  if _gd_rest PUT "domains/$_domain/records/TXT/$_sub_domain" "[{\"data\":\"$txtvalue\"}]"; then
+  if _gd_rest PUT "domains/$_domain/records/TXT/$_sub_domain" "[$_add_data]"; then
     if [ "$response" = "{}" ]; then
       _info "Added, sleeping 10 seconds"
       _sleep 10
@@ -56,7 +77,47 @@ dns_gd_add() {
 #fulldomain
 dns_gd_rm() {
   fulldomain=$1
+  txtvalue=$2
+
+  GD_Key="${GD_Key:-$(_readaccountconf_mutable GD_Key)}"
+  GD_Secret="${GD_Secret:-$(_readaccountconf_mutable GD_Secret)}"
+
+  _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 existing records"
+  if ! _gd_rest GET "domains/$_domain/records/TXT/$_sub_domain"; then
+    return 1
+  fi
+
+  if ! _contains "$response" "$txtvalue"; then
+    _info "The record is not existing, skip"
+    return 0
+  fi
+
+  _add_data=""
+  for t in $(echo "$response" | tr '{' "\n" | grep "\"name\":\"$_sub_domain\"" | tr ',' "\n" | grep '"data"' | cut -d : -f 2); do
+    _debug2 t "$t"
+    if [ "$t" ] && [ "$t" != "\"$txtvalue\"" ]; then
+      if [ "$_add_data" ]; then
+        _add_data="$_add_data,{\"data\":$t}"
+      else
+        _add_data="{\"data\":$t}"
+      fi
+    fi
+  done
+  if [ -z "$_add_data" ]; then
+    _add_data="{\"data\":\"\"}"
+  fi
+  _debug2 _add_data "$_add_data"
 
+  _gd_rest PUT "domains/$_domain/records/TXT/$_sub_domain" "[$_add_data]"
 }
 
 ####################  Private functions below ##################################

+ 158 - 0
dnsapi/dns_he.sh

@@ -0,0 +1,158 @@
+#!/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"
+
+  HE_Username="${HE_Username:-$(_readaccountconf_mutable HE_Username)}"
+  HE_Password="${HE_Password:-$(_readaccountconf_mutable HE_Password)}"
+  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_mutable HE_Username "$HE_Username"
+  _saveaccountconf_mutable 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"
+  HE_Username="${HE_Username:-$(_readaccountconf_mutable HE_Username)}"
+  HE_Password="${HE_Password:-$(_readaccountconf_mutable HE_Password)}"
+  # 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="
+
+  response="$(_post "$body" "https://dns.he.net/")"
+  _debug2 "response" "$response"
+  if ! _contains "$response" "$_txt_value"; then
+    _debug "The txt record is not found, just skip"
+    return 0
+  fi
+  _record_id="$(echo "$response" | tr -d "#" | sed "s/<tr/#<tr/g" | tr -d "\n" | tr "#" "\n" | grep "$_full_domain" | grep '"dns_tr"' | grep "$_txt_value" | cut -d '"' -f 4)"
+  _debug2 _record_id "$_record_id"
+  if [ -z "$_record_id" ]; then
+    _err "Can not find record id"
+    return 1
+  fi
+  # 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() {
+  _domain="$1"
+  body="email=${HE_Username}&pass=${HE_Password}"
+  response="$(_post "$body" "https://dns.he.net/")"
+  _debug2 response "$response"
+  _table="$(echo "$response" | tr -d "#" | sed "s/<table/#<table/g" | tr -d "\n" | tr "#" "\n" | grep 'id="domains_table"')"
+  _debug2 _table "$_table"
+  _matches="$(echo "$_table" | sed "s/<tr/#<tr/g" | tr "#" "\n" | grep 'alt="edit"' | tr -d " " | sed "s/<td/#<td/g" | tr "#" "\n" | grep 'hosted_dns_zoneid')"
+  _debug2 _matches "$_matches"
+  # Zone names and zone IDs are in same order
+  _zone_ids=$(echo "$_matches" | _egrep_o "hosted_dns_zoneid=[0-9]*&" | cut -d = -f 2 | tr -d '&')
+  _zone_names=$(echo "$_matches" | _egrep_o "name=.*onclick" | cut -d '"' -f 2)
+  _debug2 "These are the zones on this HE account:"
+  _debug2 "$_zone_names"
+  _debug2 "And these are their respective IDs:"
+  _debug2 "$_zone_ids"
+  if [ -z "$_zone_names" ] || [ -z "$_zone_ids" ]; then
+    _err "Can not get zone names."
+    return 1
+  fi
+  # Walk through all possible zone names
+  _strip_counter=1
+  while true; do
+    _attempted_zone=$(echo "$_domain" | cut -d . -f ${_strip_counter}-)
+
+    # All possible zone names have been tried
+    if [ -z "$_attempted_zone" ]; then
+      _err "No zone for domain \"$_domain\" found."
+      return 1
+    fi
+
+    _debug "Looking for zone \"${_attempted_zone}\""
+
+    line_num="$(echo "$_zone_names" | grep -n "$_attempted_zone" | cut -d : -f 1)"
+
+    if [ "$line_num" ]; then
+      _zone_id=$(echo "$_zone_ids" | sed -n "${line_num}p")
+      _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:

+ 17 - 12
dnsapi/dns_infoblox.sh

@@ -9,7 +9,7 @@ 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"
+  baseurlnObject="https://$Infoblox_Server/wapi/v2.2.2/record:txt?name=$fulldomain&text=$txtvalue&view=$Infoblox_View"
 
   _info "Using Infoblox API"
   _debug fulldomain "$fulldomain"
@@ -19,14 +19,19 @@ dns_infoblox_add() {
   if [ -z "$Infoblox_Creds" ] || [ -z "$Infoblox_Server" ]; then
     Infoblox_Creds=""
     Infoblox_Server=""
-    _err "You didn't specify the credentials or server yet (Infoblox_Creds and Infoblox_Server)."
-    _err "Please set them via EXPORT ([username:password] and [ip or hostname]) and try again."
+    _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)
@@ -36,10 +41,10 @@ dns_infoblox_add() {
   export _H2="Authorization: Basic $Infoblox_CredsEncoded"
 
   ## Add the challenge record to the Infoblox grid member
-  result=$(_post "" "$baseurlnObject" "" "POST")
+  result="$(_post "" "$baseurlnObject" "" "POST")"
 
   ## Let's see if we get something intelligible back from the unit
-  if echo "$result" | egrep 'record:txt/.*:.*/default'; then
+  if [ "$(echo "$result" | _egrep_o "record:txt/.*:.*/$Infoblox_View")" ]; then
     _info "Successfully created the txt record"
     return 0
   else
@@ -61,25 +66,25 @@ dns_infoblox_rm() {
   _debug txtvalue "$txtvalue"
 
   ## Base64 encode the credentials
-  Infoblox_CredsEncoded=$(printf "%b" "$Infoblox_Creds" | _base64)
+  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&_return_type=xml-pretty"
-  result=$(_get "$baseurlnObject")
+  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 'record:txt/.*:.*/default'; then
+  if [ "$(echo "$result" | _egrep_o "record:txt/.*:.*/$Infoblox_View")" ]; then
     ## Extract the object reference
-    objRef=$(printf "%b" "$result" | _egrep_o 'record:txt/.*:.*/default')
+    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")
+    rmResult="$(_post "" "$objRmUrl" "" "DELETE")"
     ## Let's see if that worked
-    if echo "$rmResult" | egrep 'record:txt/.*:.*/default'; then
+    if [ "$(echo "$rmResult" | _egrep_o "record:txt/.*:.*/$Infoblox_View")" ]; then
       _info "Successfully deleted $objRef"
       return 0
     else

+ 311 - 0
dnsapi/dns_inwx.sh

@@ -0,0 +1,311 @@
+#!/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"
+
+  _info "Adding record"
+  _inwx_add_record "$_domain" "$_sub_domain" "$txtvalue"
+
+}
+
+#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 ! _contains "$response" "Command completed successfully"; 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
+}

+ 0 - 1
dnsapi/dns_ispconfig.sh

@@ -2,7 +2,6 @@
 
 # ISPConfig 3.1 API
 # User must provide login data and URL to the ISPConfig installation incl. port. The remote user in ISPConfig must have access to:
-# - DNS zone Functions
 # - DNS txt Functions
 
 # Report bugs to https://github.com/sjau/acme.sh

+ 2 - 2
dnsapi/dns_linode.sh

@@ -68,7 +68,7 @@ dns_linode_rm() {
   _parameters="&DomainID=$_domain_id"
 
   if _rest GET "domain.resource.list" "$_parameters" && [ -n "$response" ]; then
-    response="$(echo "$response" | tr -d "\n" | sed 's/{/\n&/g')"
+    response="$(echo "$response" | tr -d "\n" | tr '{' "|" | sed 's/|/&{/g' | tr "|" "\n")"
 
     resource="$(echo "$response" | _egrep_o "{.*\"NAME\":\s*\"$_sub_domain\".*}")"
     if [ "$resource" ]; then
@@ -128,7 +128,7 @@ _get_root() {
   p=1
 
   if _rest GET "domain.list"; then
-    response="$(echo "$response" | tr -d "\n" | sed 's/{/\n&/g')"
+    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"

+ 17 - 37
dnsapi/dns_lua.sh

@@ -8,7 +8,6 @@
 #LUA_Email="user@luadns.net"
 
 LUA_Api="https://api.luadns.com/v1"
-LUA_auth=$(printf "%s" "$LUA_Email:$LUA_Key" | _base64)
 
 ########  Public functions #####################
 
@@ -17,6 +16,10 @@ dns_lua_add() {
   fulldomain=$1
   txtvalue=$2
 
+  LUA_Key="${LUA_Key:-$(_readaccountconf_mutable LUA_Key)}"
+  LUA_Email="${LUA_Email:-$(_readaccountconf_mutable LUA_Email)}"
+  LUA_auth=$(printf "%s" "$LUA_Email:$LUA_Key" | _base64)
+
   if [ -z "$LUA_Key" ] || [ -z "$LUA_Email" ]; then
     LUA_Key=""
     LUA_Email=""
@@ -26,8 +29,8 @@ dns_lua_add() {
   fi
 
   #save the api key and email to the account conf file.
-  _saveaccountconf LUA_Key "$LUA_Key"
-  _saveaccountconf LUA_Email "$LUA_Email"
+  _saveaccountconf_mutable LUA_Key "$LUA_Key"
+  _saveaccountconf_mutable LUA_Email "$LUA_Email"
 
   _debug "First detect the root zone"
   if ! _get_root "$fulldomain"; then
@@ -38,50 +41,27 @@ dns_lua_add() {
   _debug _sub_domain "$_sub_domain"
   _debug _domain "$_domain"
 
-  _debug "Getting txt records"
-  _LUA_rest GET "zones/${_domain_id}/records"
-
-  if ! _contains "$response" "\"id\":"; then
-    _err "Error"
-    return 1
-  fi
-
-  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 _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 "\"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" ] && _contains "$response" "updated_at"; then
-      _info "Updated!"
+  _info "Adding record"
+  if _LUA_rest POST "zones/$_domain_id/records" "{\"type\":\"TXT\",\"name\":\"$fulldomain.\",\"content\":\"$txtvalue\",\"ttl\":120}"; 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
-    _err "Update error"
-    return 1
   fi
-
 }
 
 #fulldomain
 dns_lua_rm() {
   fulldomain=$1
   txtvalue=$2
+
+  LUA_Key="${LUA_Key:-$(_readaccountconf_mutable LUA_Key)}"
+  LUA_Email="${LUA_Email:-$(_readaccountconf_mutable LUA_Email)}"
+  LUA_auth=$(printf "%s" "$LUA_Email:$LUA_Key" | _base64)
   _debug "First detect the root zone"
   if ! _get_root "$fulldomain"; then
     _err "invalid domain"

+ 9 - 27
dnsapi/dns_me.sh

@@ -43,34 +43,16 @@ dns_me_add() {
     return 1
   fi
 
-  count=$(printf "%s\n" "$response" | _egrep_o "\"totalRecords\":[^,]*" | cut -d : -f 2)
-  _debug count "$count"
-  if [ "$count" = "0" ]; then
-    _info "Adding record"
-    if _me_rest POST "$_domain_id/records/" "{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":\"$txtvalue\",\"gtdLocation\":\"DEFAULT\",\"ttl\":120}"; then
-      if printf -- "%s" "$response" | grep \"id\": >/dev/null; 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 "\"id\":[^,]*" | cut -d : -f 2 | head -n 1)
-    _debug "record_id" "$record_id"
-
-    _me_rest PUT "$_domain_id/records/$record_id/" "{\"id\":\"$record_id\",\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":\"$txtvalue\",\"gtdLocation\":\"DEFAULT\",\"ttl\":120}"
-    if [ "$?" = "0" ]; then
-      _info "Updated"
+  _info "Adding record"
+  if _me_rest POST "$_domain_id/records/" "{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":\"$txtvalue\",\"gtdLocation\":\"DEFAULT\",\"ttl\":120}"; then
+    if printf -- "%s" "$response" | grep \"id\": >/dev/null; then
+      _info "Added"
       #todo: check if the record takes effect
       return 0
+    else
+      _err "Add txt record error."
+      return 1
     fi
-    _err "Update error"
-    return 1
   fi
 
 }
@@ -96,7 +78,7 @@ dns_me_rm() {
   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)
+    record_id=$(printf "%s\n" "$response" | _egrep_o ",\"value\":\"..$txtvalue..\",\"id\":[^,]*" | cut -d : -f 3 | head -n 1)
     _debug "record_id" "$record_id"
     if [ -z "$record_id" ]; then
       _err "Can not get record id to remove."
@@ -152,7 +134,7 @@ _me_rest() {
   data="$3"
   _debug "$ep"
 
-  cdate=$(date -u +"%a, %d %b %Y %T %Z")
+  cdate=$(LANG=C date -u +"%a, %d %b %Y %T %Z")
   hmac=$(printf "%s" "$cdate" | _hmac sha1 "$(printf "%s" "$ME_Secret" | _hex_dump | tr -d " ")" hex)
 
   export _H1="x-dnsme-apiKey: $ME_Key"

+ 166 - 0
dnsapi/dns_namecom.sh

@@ -0,0 +1,166 @@
+#!/usr/bin/env sh
+
+#Author: RaidenII
+#Created 06/28/2017
+#Updated 03/01/2018, rewrote to support name.com API v4
+#Utilize name.com API to finish dns-01 verifications.
+########  Public functions #####################
+
+Namecom_API="https://api.name.com/v4"
+
+#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."
+    return 1
+  fi
+
+  # Add TXT record.
+  _namecom_addtxt_json="{\"host\":\"$_sub_domain\",\"type\":\"TXT\",\"answer\":\"$txtvalue\",\"ttl\":\"300\"}"
+  if _namecom_rest POST "domains/$_domain/records" "$_namecom_addtxt_json"; then
+    _retvalue=$(printf "%s\n" "$response" | _egrep_o "\"$_sub_domain\"")
+    if [ "$_retvalue" ]; then
+      _info "Successfully added TXT record, ready for validation."
+      return 0
+    else
+      _err "Unable to add the DNS record."
+      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."
+    return 1
+  fi
+
+  # Get the record id.
+  if _namecom_rest GET "domains/$_domain/records"; then
+    _record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[0-9]+,\"domainName\":\"$_domain\",\"host\":\"$_sub_domain\",\"fqdn\":\"$fulldomain.\",\"type\":\"TXT\",\"answer\":\"$txtvalue\"" | cut -d \" -f 3 | _egrep_o [0-9]+)
+    _debug record_id "$_record_id"
+    if [ "$_record_id" ]; then
+      _info "Successfully retrieved the record id for ACME challenge."
+    else
+      _err "Unable to retrieve the record id."
+      return 1
+    fi
+  fi
+
+  # Remove the DNS record using record id.
+  if _namecom_rest DELETE "domains/$_domain/records/$_record_id"; then
+    _info "Successfully removed the TXT record."
+    return 0
+  else
+    _err "Unable to delete record id."
+    return 1
+  fi
+}
+
+####################  Private functions below ##################################
+_namecom_rest() {
+  method=$1
+  param=$2
+  data=$3
+
+  export _H1="Authorization: Basic $_namecom_auth"
+  export _H2="Content-Type: application/json"
+
+  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() {
+  # Auth string
+  # Name.com API v4 uses http basic auth to authenticate
+  # need to convert the token for http auth
+  _namecom_auth=$(printf "%s:%s" "$Namecom_Username" "$Namecom_Token" | base64)
+
+  if _namecom_rest GET "hello"; then
+    retcode=$(printf "%s\n" "$response" | _egrep_o "\"username\"\:\"$Namecom_Username\"")
+    if [ "$retcode" ]; then
+      _info "Successfully logged in."
+    else
+      _err "Logging in failed."
+      return 1
+    fi
+  fi
+}
+
+_namecom_get_root() {
+  domain=$1
+  i=2
+  p=1
+
+  if ! _namecom_rest GET "domains"; 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
+}

+ 137 - 0
dnsapi/dns_namesilo.sh

@@ -0,0 +1,137 @@
+#!/usr/bin/env sh
+
+#Author: meowthink
+#Created 01/14/2017
+#Utilize namesilo.com API to finish dns-01 verifications.
+
+Namesilo_API="https://www.namesilo.com/api"
+
+########  Public functions #####################
+
+#Usage: dns_myapi_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_namesilo_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if [ -z "$Namesilo_Key" ]; then
+    Namesilo_Key=""
+    _err "API token for namesilo.com is missing."
+    _err "Please specify that in your environment variable."
+    return 1
+  fi
+
+  #save the api key and email to the account conf file.
+  _saveaccountconf Namesilo_Key "$Namesilo_Key"
+
+  if ! _get_root "$fulldomain"; then
+    _err "Unable to find domain specified."
+    return 1
+  fi
+
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  _debug txtvalue "$txtvalue"
+  if _namesilo_rest GET "dnsAddRecord?version=1&type=xml&key=$Namesilo_Key&domain=$_domain&rrtype=TXT&rrhost=$_sub_domain&rrvalue=$txtvalue"; then
+    retcode=$(printf "%s\n" "$response" | _egrep_o "<code>300")
+    if [ "$retcode" ]; then
+      _info "Successfully added TXT record, ready for validation."
+      return 0
+    else
+      _err "Unable to add the DNS record."
+      return 1
+    fi
+  fi
+}
+
+#Usage: fulldomain txtvalue
+#Remove the txt record after validation.
+dns_namesilo_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if ! _get_root "$fulldomain"; then
+    _err "Unable to find domain specified."
+    return 1
+  fi
+
+  # Get the record id.
+  if _namesilo_rest GET "dnsListRecords?version=1&type=xml&key=$Namesilo_Key&domain=$_domain"; then
+    retcode=$(printf "%s\n" "$response" | _egrep_o "<code>300")
+    if [ "$retcode" ]; then
+      _record_id=$(printf "%s\n" "$response" | _egrep_o "<record_id>([^<]*)</record_id><type>TXT</type><host>$fulldomain</host>" | _egrep_o "<record_id>([^<]*)</record_id>" | sed -r "s/<record_id>([^<]*)<\/record_id>/\1/" | tail -n 1)
+      _debug record_id "$_record_id"
+      _info "Successfully retrieved the record id for ACME challenge."
+    else
+      _err "Unable to retrieve the record id."
+      return 1
+    fi
+  fi
+
+  # Remove the DNS record using record id.
+  if _namesilo_rest GET "dnsDeleteRecord?version=1&type=xml&key=$Namesilo_Key&domain=$_domain&rrid=$_record_id"; then
+    retcode=$(printf "%s\n" "$response" | _egrep_o "<code>300")
+    if [ "$retcode" ]; then
+      _info "Successfully removed the TXT record."
+      return 0
+    else
+      _err "Unable to remove the DNS record."
+      return 1
+    fi
+  fi
+}
+
+####################  Private functions below ##################################
+
+# _acme-challenge.www.domain.com
+# returns
+#  _sub_domain=_acme-challenge.www
+#  _domain=domain.com
+_get_root() {
+  domain=$1
+  i=2
+  p=1
+
+  if ! _namesilo_rest GET "listDomains?version=1&type=xml&key=$Namesilo_Key"; 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
+}
+
+_namesilo_rest() {
+  method=$1
+  param=$2
+  data=$3
+
+  if [ "$method" != "GET" ]; then
+    response="$(_post "$data" "$Namesilo_API/$param" "" "$method")"
+  else
+    response="$(_get "$Namesilo_API/$param")"
+  fi
+
+  if [ "$?" != "0" ]; then
+    _err "error $param"
+    return 1
+  fi
+
+  _debug2 response "$response"
+  return 0
+}

+ 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"
+    prev_txt=$(printf "%s\n" "$response" | _egrep_o "\"domain\":\"$fulldomain\",\"short_answers\":\[\"[^,]*\]" | _head_n 1 | cut -d: -f3 | cut -d, -f1)
+    _debug "prev_txt" "$prev_txt"
+
+    _nsone_rest POST "zones/$_domain/$fulldomain/TXT" "{\"answers\": [{\"answer\": [\"$txtvalue\"]},{\"answer\": $prev_txt}],\"type\": \"TXT\",\"domain\":\"$fulldomain\",\"zone\": \"$_domain\"}"
+    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
+}

+ 74 - 51
dnsapi/dns_ovh.sh

@@ -78,12 +78,9 @@ _ovh_get_api() {
   esac
 }
 
-########  Public functions #####################
-
-#Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
-dns_ovh_add() {
-  fulldomain=$1
-  txtvalue=$2
+_initAuth() {
+  OVH_AK="${OVH_AK:-$(_readaccountconf_mutable OVH_AK)}"
+  OVH_AS="${OVH_AS:-$(_readaccountconf_mutable OVH_AS)}"
 
   if [ -z "$OVH_AK" ] || [ -z "$OVH_AS" ]; then
     OVH_AK=""
@@ -93,21 +90,26 @@ dns_ovh_add() {
     return 1
   fi
 
-  #save the api key and email to the account conf file.
-  _saveaccountconf OVH_AK "$OVH_AK"
-  _saveaccountconf OVH_AS "$OVH_AS"
+  if [ "$OVH_AK" != "$(_readaccountconf OVH_AK)" ]; then
+    _info "It seems that your ovh key is changed, let's clear consumer key first."
+    _clearaccountconf OVH_CK
+  fi
+  _saveaccountconf_mutable OVH_AK "$OVH_AK"
+  _saveaccountconf_mutable OVH_AS "$OVH_AS"
 
+  OVH_END_POINT="${OVH_END_POINT:-$(_readaccountconf_mutable OVH_END_POINT)}"
   if [ -z "$OVH_END_POINT" ]; then
     OVH_END_POINT="ovh-eu"
   fi
   _info "Using OVH endpoint: $OVH_END_POINT"
   if [ "$OVH_END_POINT" != "ovh-eu" ]; then
-    _saveaccountconf OVH_END_POINT "$OVH_END_POINT"
+    _saveaccountconf_mutable OVH_END_POINT "$OVH_END_POINT"
   fi
 
   OVH_API="$(_ovh_get_api $OVH_END_POINT)"
   _debug OVH_API "$OVH_API"
 
+  OVH_CK="${OVH_CK:-$(_readaccountconf_mutable OVH_CK)}"
   if [ -z "$OVH_CK" ]; then
     _info "OVH consumer key is empty, Let's get one:"
     if ! _ovh_authentication; then
@@ -119,14 +121,26 @@ dns_ovh_add() {
 
   _info "Checking authentication"
 
-  response="$(_ovh_rest GET "domain")"
-  if _contains "$response" "INVALID_CREDENTIAL"; then
+  if ! _ovh_rest GET "domain" || _contains "$response" "INVALID_CREDENTIAL"; then
     _err "The consumer key is invalid: $OVH_CK"
     _err "Please retry to create a new one."
     _clearaccountconf OVH_CK
     return 1
   fi
   _info "Consumer key is ok."
+  return 0
+}
+
+########  Public functions #####################
+
+#Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_ovh_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if ! _initAuth; then
+    return 1
+  fi
 
   _debug "First detect the root zone"
   if ! _get_root "$fulldomain"; then
@@ -137,49 +151,58 @@ dns_ovh_add() {
   _debug _sub_domain "$_sub_domain"
   _debug _domain "$_domain"
 
-  _debug "Getting txt records"
-  _ovh_rest GET "domain/zone/$_domain/record?fieldType=TXT&subDomain=$_sub_domain"
-
-  if _contains "$response" '\[\]' || _contains "$response" "This service does not exist"; then
-    _info "Adding record"
-    if _ovh_rest POST "domain/zone/$_domain/record" "{\"fieldType\":\"TXT\",\"subDomain\":\"$_sub_domain\",\"target\":\"$txtvalue\",\"ttl\":60}"; then
-      if _contains "$response" "$txtvalue"; then
-        _ovh_rest POST "domain/zone/$_domain/refresh"
-        _debug "Refresh:$response"
-        _info "Added, sleeping 10 seconds"
-        sleep 10
-        return 0
-      fi
-    fi
-    _err "Add txt record error."
-  else
-    _info "Updating record"
-    record_id=$(printf "%s" "$response" | tr -d "[]" | cut -d , -f 1)
-    if [ -z "$record_id" ]; then
-      _err "Can not get record id."
-      return 1
-    fi
-    _debug "record_id" "$record_id"
-
-    if _ovh_rest PUT "domain/zone/$_domain/record/$record_id" "{\"target\":\"$txtvalue\",\"subDomain\":\"$_sub_domain\",\"ttl\":60}"; then
-      if _contains "$response" "null"; then
-        _ovh_rest POST "domain/zone/$_domain/refresh"
-        _debug "Refresh:$response"
-        _info "Updated, sleeping 10 seconds"
-        sleep 10
-        return 0
-      fi
+  _info "Adding record"
+  if _ovh_rest POST "domain/zone/$_domain/record" "{\"fieldType\":\"TXT\",\"subDomain\":\"$_sub_domain\",\"target\":\"$txtvalue\",\"ttl\":60}"; then
+    if _contains "$response" "$txtvalue"; then
+      _ovh_rest POST "domain/zone/$_domain/refresh"
+      _debug "Refresh:$response"
+      _info "Added, sleep 10 seconds."
+      _sleep 10
+      return 0
     fi
-    _err "Update error"
-    return 1
   fi
+  _err "Add txt record error."
+  return 1
 
 }
 
 #fulldomain
 dns_ovh_rm() {
   fulldomain=$1
+  txtvalue=$2
+
+  if ! _initAuth; then
+    return 1
+  fi
 
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+  _debug "Getting txt records"
+  if ! _ovh_rest GET "domain/zone/$_domain/record?fieldType=TXT&subDomain=$_sub_domain"; then
+    return 1
+  fi
+
+  for rid in $(echo "$response" | tr '][,' '   '); do
+    _debug rid "$rid"
+    if ! _ovh_rest GET "domain/zone/$_domain/record/$rid"; then
+      return 1
+    fi
+    if _contains "$response" "\"target\":\"$txtvalue\""; then
+      _debug "Found txt id:$rid"
+      if ! _ovh_rest DELETE "domain/zone/$_domain/record/$rid"; then
+        return 1
+      fi
+      return 0
+    fi
+  done
+
+  return 1
 }
 
 ####################  Private functions below ##################################
@@ -191,7 +214,7 @@ _ovh_authentication() {
   _H3=""
   _H4=""
 
-  _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'"}'
+  _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/*"},{"method": "DELETE","path": "/domain/zone/*/record/*"}],"redirection":"'$ovh_success'"}'
 
   response="$(_post "$_ovhdata" "$OVH_API/auth/credential")"
   _debug3 response "$response"
@@ -238,7 +261,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
@@ -279,15 +302,15 @@ _ovh_rest() {
   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
+  if [ "$data" ] || [ "$m" = "POST" ] || [ "$m" = "PUT" ] || [ "$m" = "DELETE" ]; then
     _debug data "$data"
     response="$(_post "$data" "$_ovh_url" "" "$m")"
   else
     response="$(_get "$_ovh_url")"
   fi
 
-  if [ "$?" != "0" ]; then
-    _err "error $ep"
+  if [ "$?" != "0" ] || _contains "$response" "INVALID_CREDENTIAL"; then
+    _err "error $response"
     return 1
   fi
   _debug2 response "$response"

+ 10 - 7
dnsapi/dns_pdns.sh

@@ -90,7 +90,7 @@ set_record() {
   full=$2
   txtvalue=$3
 
-  if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root." "{\"rrsets\": [{\"changetype\": \"REPLACE\", \"name\": \"$full.\", \"type\": \"TXT\", \"ttl\": $PDNS_Ttl, \"records\": [{\"name\": \"$full.\", \"type\": \"TXT\", \"content\": \"\\\"$txtvalue\\\"\", \"disabled\": false, \"ttl\": $PDNS_Ttl}]}]}"; then
+  if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root" "{\"rrsets\": [{\"changetype\": \"REPLACE\", \"name\": \"$full.\", \"type\": \"TXT\", \"ttl\": $PDNS_Ttl, \"records\": [{\"name\": \"$full.\", \"type\": \"TXT\", \"content\": \"\\\"$txtvalue\\\"\", \"disabled\": false, \"ttl\": $PDNS_Ttl}]}]}"; then
     _err "Set txt record error."
     return 1
   fi
@@ -107,7 +107,7 @@ rm_record() {
   root=$1
   full=$2
 
-  if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root." "{\"rrsets\": [{\"changetype\": \"DELETE\", \"name\": \"$full.\", \"type\": \"TXT\"}]}"; then
+  if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root" "{\"rrsets\": [{\"changetype\": \"DELETE\", \"name\": \"$full.\", \"type\": \"TXT\"}]}"; then
     _err "Delete txt record error."
     return 1
   fi
@@ -122,7 +122,7 @@ rm_record() {
 notify_slaves() {
   root=$1
 
-  if ! _pdns_rest "PUT" "/api/v1/servers/$PDNS_ServerId/zones/$root./notify"; then
+  if ! _pdns_rest "PUT" "/api/v1/servers/$PDNS_ServerId/zones/$root/notify"; then
     _err "Notify slaves error."
     return 1
   fi
@@ -144,15 +144,18 @@ _get_root() {
 
   while true; do
     h=$(printf "%s" "$domain" | cut -d . -f $i-100)
-    if [ -z "$h" ]; then
-      return 1
-    fi
 
     if _contains "$_zones_response" "\"name\": \"$h.\""; then
-      _domain="$h"
+      _domain="$h."
+      if [ -z "$h" ]; then
+        _domain="=2E"
+      fi
       return 0
     fi
 
+    if [ -z "$h" ]; then
+      return 1
+    fi
     i=$(_math $i + 1)
   done
   _debug "$domain not found"

+ 161 - 0
dnsapi/dns_selectel.sh

@@ -0,0 +1,161 @@
+#!/usr/bin/env sh
+
+#
+#SL_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
+#
+
+SL_Api="https://api.selectel.ru/domains/v1"
+
+########  Public functions #####################
+
+#Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_selectel_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  SL_Key="${SL_Key:-$(_readaccountconf_mutable SL_Key)}"
+
+  if [ -z "$SL_Key" ]; then
+    SL_Key=""
+    _err "You don't specify selectel.ru api key yet."
+    _err "Please create you key and try again."
+    return 1
+  fi
+
+  #save the api key to the account conf file.
+  _saveaccountconf_mutable SL_Key "$SL_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"
+
+  _info "Adding record"
+  if _sl_rest POST "/$_domain_id/records/" "{\"type\": \"TXT\", \"ttl\": 60, \"name\": \"$fulldomain\", \"content\": \"$txtvalue\"}"; then
+    if _contains "$response" "$txtvalue" || _contains "$response" "record_already_exists"; then
+      _info "Added, OK"
+      return 0
+    fi
+  fi
+  _err "Add txt record error."
+  return 1
+}
+
+#fulldomain txtvalue
+dns_selectel_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  SL_Key="${SL_Key:-$(_readaccountconf_mutable SL_Key)}"
+
+  if [ -z "$SL_Key" ]; then
+    SL_Key=""
+    _err "You don't specify slectel api key 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"
+  _sl_rest GET "/${_domain_id}/records/"
+
+  if ! _contains "$response" "$txtvalue"; then
+    _err "Txt record not found"
+    return 1
+  fi
+
+  _record_seg="$(echo "$response" | _egrep_o "\"content\" *: *\"$txtvalue\"[^}]*}")"
+  _debug2 "_record_seg" "$_record_seg"
+  if [ -z "$_record_seg" ]; then
+    _err "can not find _record_seg"
+    return 1
+  fi
+
+  _record_id="$(echo "$_record_seg" | tr "," "\n" | tr "}" "\n" | tr -d " " | grep "\"id\"" | cut -d : -f 2)"
+  _debug2 "_record_id" "$_record_id"
+  if [ -z "$_record_id" ]; then
+    _err "can not find _record_id"
+    return 1
+  fi
+
+  if ! _sl_rest DELETE "/$_domain_id/records/$_record_id"; then
+    _err "Delete record error."
+    return 1
+  fi
+  return 0
+}
+
+####################  Private functions below ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+# _domain_id=sdjkglgdfewsdfg
+_get_root() {
+  domain=$1
+
+  if ! _sl_rest GET "/"; then
+    return 1
+  fi
+
+  i=2
+  p=1
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    _debug h "$h"
+    if [ -z "$h" ]; then
+      #not valid
+      return 1
+    fi
+
+    if _contains "$response" "\"name\": \"$h\","; then
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      _domain=$h
+      _debug "Getting domain id for $h"
+      if ! _sl_rest GET "/$h"; then
+        return 1
+      fi
+      _domain_id="$(echo "$response" | tr "," "\n" | tr "}" "\n" | tr -d " " | grep "\"id\":" | cut -d : -f 2)"
+      return 0
+    fi
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+  return 1
+}
+
+_sl_rest() {
+  m=$1
+  ep="$2"
+  data="$3"
+  _debug "$ep"
+
+  export _H1="X-Token: $SL_Key"
+  export _H2="Content-Type: application/json"
+
+  if [ "$m" != "GET" ]; then
+    _debug data "$data"
+    response="$(_post "$data" "$SL_Api/$ep" "" "$m")"
+  else
+    response="$(_get "$SL_Api/$ep")"
+  fi
+
+  if [ "$?" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}

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

+ 106 - 0
dnsapi/dns_yandex.sh

@@ -0,0 +1,106 @@
+#!/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"
+
+  _PDD_get_domain "$fulldomain"
+  _debug "Found suitable domain in pdd: $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"
+
+  _PDD_get_domain "$fulldomain"
+  _debug "Found suitable domain in pdd: $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)"
+    _debug2 "res1" "$res1"
+    __found="$(echo "$res1" | sed -n -e 's#.* "found": \([^,]*\),.*#\1#p')"
+    _debug "found: $__found results on page"
+    if [ "$__found" -lt 20 ]; then
+      _debug "last page: $__page"
+      __last=1
+    fi
+
+    __all_domains="$__all_domains $(echo "$res1" | tr "," "\n" | 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
+        p=$(_math $k - 1)
+        curSubdomain="$(echo "$fulldomain" | cut -d . -f "1-$p")"
+        curDomain="$__t"
+        return 0
+      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}"
+
+  _PDD_get_domain "$fulldomain"
+  _debug "Found suitable domain in pdd: $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'
+}

+ 85 - 0
dnsapi/dns_zonomi.sh

@@ -0,0 +1,85 @@
+#!/usr/bin/env sh
+
+#
+#ZM_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
+#
+#https://zonomi.com dns api
+
+ZM_Api="https://zonomi.com/app/dns/dyndns.jsp"
+
+########  Public functions #####################
+
+#Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_zonomi_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  ZM_Key="${ZM_Key:-$(_readaccountconf_mutable ZM_Key)}"
+
+  if [ -z "$ZM_Key" ]; then
+    ZM_Key=""
+    _err "You don't specify zonomi api key yet."
+    _err "Please create your key and try again."
+    return 1
+  fi
+
+  #save the api key to the account conf file.
+  _saveaccountconf_mutable ZM_Key "$ZM_Key"
+
+  _info "Get existing txt records for $fulldomain"
+  if ! _zm_request "action=QUERY&name=$fulldomain"; then
+    _err "error"
+    return 1
+  fi
+
+  if _contains "$response" "<record"; then
+    _debug "get and update records"
+    _qstr="action[1]=SET&type[1]=TXT&name[1]=$fulldomain&value[1]=$txtvalue"
+    _qindex=2
+    for t in $(echo "$response" | tr -d "\r\n" | _egrep_o '<action.*</action>' | tr "<" "\n" | grep record | grep 'type="TXT"' | cut -d '"' -f 6); do
+      _debug2 t "$t"
+      _qstr="$_qstr&action[$_qindex]=SET&type[$_qindex]=TXT&name[$_qindex]=$fulldomain&value[$_qindex]=$t"
+      _qindex="$(_math "$_qindex" + 1)"
+    done
+    _zm_request "$_qstr"
+  else
+    _debug "Just add record"
+    _zm_request "action=SET&type=TXT&name=$fulldomain&value=$txtvalue"
+  fi
+
+}
+
+#fulldomain txtvalue
+dns_zonomi_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  ZM_Key="${ZM_Key:-$(_readaccountconf_mutable ZM_Key)}"
+  if [ -z "$ZM_Key" ]; then
+    ZM_Key=""
+    _err "You don't specify zonomi api key yet."
+    _err "Please create your key and try again."
+    return 1
+  fi
+
+  _zm_request "action=DELETE&type=TXT&name=$fulldomain"
+
+}
+
+####################  Private functions below ##################################
+#qstr
+_zm_request() {
+  qstr="$1"
+
+  _debug2 "qstr" "$qstr"
+
+  _zm_url="$ZM_Api?api_key=$ZM_Key&$qstr"
+  _debug2 "_zm_url" "$_zm_url"
+  response="$(_get "$_zm_url")"
+
+  if [ "$?" != "0" ]; then
+    return 1
+  fi
+  _debug2 response "$response"
+  _contains "$response" "<is_ok>OK:"
+}

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