Browse Source

Merge branch 'dev' of https://github.com/Neilpang/acme.sh into ssh-deploy

David Kerr 7 years ago
parent
commit
98b8bfb3fa

+ 8 - 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,21 +22,10 @@ addons:
 
 install:
   - if [ "$TRAVIS_OS_NAME" = 'osx' ]; then
-      brew update && brew install openssl socat;
-      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";
-    else sudo apt-get install socat;
+      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
@@ -44,7 +37,7 @@ script:
   - 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
 
 

+ 1 - 1
Dockerfile

@@ -1,4 +1,4 @@
-FROM alpine
+FROM alpine:3.6
 
 RUN apk update -f \
   && apk --no-cache add -f \

+ 108 - 66
README.md

@@ -3,6 +3,8 @@
 [![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.
@@ -23,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))
@@ -72,7 +74,7 @@ https://github.com/Neilpang/acmetest
 - Webroot mode
 - Standalone mode
 - Apache mode
-- Nginx mode ( Beta )
+- Nginx mode
 - DNS mode
 - [Stateless mode](https://github.com/Neilpang/acme.sh/wiki/Stateless-Mode)
 
@@ -127,7 +129,7 @@ Ok, you are ready to issue certs now.
 
 Show help message:
 
-```
+```sh
 root@v1:~# acme.sh -h
 ```
 
@@ -164,16 +166,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:
@@ -195,13 +197,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 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))**
@@ -236,14 +240,18 @@ More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert
 
 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
@@ -260,47 +268,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:
+**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.**
 
-```
-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.
-
-**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.**
+More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert
 
-# 9. Automatic DNS API integration
+# 8. Automatic DNS API integration
 
 If your DNS provider supports API access, we can use that API to automatically issue the certs.
 
@@ -339,21 +317,62 @@ You don't have to do anything manually!
 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
 
 
 And: 
 
-1. lexicon DNS API: https://github.com/Neilpang/acme.sh/wiki/How-to-use-lexicon-dns-api
-   (DigitalOcean, DNSimple, DNSMadeEasy, DNSPark, EasyDNS, Namesilo, NS1, PointHQ, Rage4 and Vultr etc.)
+**lexicon DNS API: https://github.com/Neilpang/acme.sh/wiki/How-to-use-lexicon-dns-api
+   (DigitalOcean, DNSimple, DNSMadeEasy, DNSPark, EasyDNS, Namesilo, NS1, PointHQ, Rage4 and Vultr etc.)**
 
 
-   
 **More APIs coming soon...**
 
 If your DNS provider is not on the supported list above, you can write your own DNS API script easily. If you do, please consider submitting a [Pull Request](https://github.com/Neilpang/acme.sh/pulls) and contribute it to the project.
 
 For more details: [How to use DNS API](dnsapi)
 
+# 9. Use DNS manual mode:
+
+If your dns provider doesn't support any api access, you can add the txt record by your hand.
+
+```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.**
 
 # 10. Issue ECC certificates
 
@@ -361,7 +380,7 @@ For more details: [How to use DNS API](dnsapi)
 
 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:
 
@@ -377,7 +396,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:
 
@@ -386,36 +405,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.
+# 11. 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
 ```
+
+
+
+# 12. How to renew the certs
+
+No, you don't need to renew the certs manually. All the certs will be renewed automatically every **60** days.
+
+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`
+# 13. How to stop cert renewal
+
+To stop renewal of a cert, you can execute the following to remove the cert from the renewal list:
+
+```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.
+
+
+# 14. 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
 ```
 
@@ -423,31 +466,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
+# 15. Issue a cert from an existing CSR
 
 https://github.com/Neilpang/acme.sh/wiki/Issue-a-cert-from-existing-CSR
 
 
-# 14. Under the Hood
+# 16. Under the Hood
 
 Speak ACME language using shell, directly to "Let's Encrypt".
 
 TODO:
 
 
-# 15. Acknowledgments
+# 17. 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
+# 18. License & Others
 
 License is GPLv3
 
@@ -456,7 +498,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
+# 19. 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
+ 405 - 196
acme.sh


+ 28 - 5
deploy/strongswan.sh

@@ -16,17 +16,40 @@ strongswan_deploy() {
   _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" >"/etc/ipsec.d/private/$(basename "$_ckey")"
-  cat "$_ccert" >"/etc/ipsec.d/certs/$(basename "$_ccert")"
-  cat "$_cca" >"/etc/ipsec.d/cacerts/$(basename "$_cca")"
-  cat "$_cfullchain" >"/etc/ipsec.d/cacerts/$(basename "$_cfullchain")"
+  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
+  $_ipsec reload
 
 }

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

+ 159 - 4
dnsapi/README.md

@@ -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"
 ```
 
@@ -512,7 +515,7 @@ acme.sh --issue --dns dns_nsone -d example.com -d www.example.com
 export DuckDNS_Token="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
 ```
 
-Please note that since DuckDNS uses StartSSL as their cert provider, thus 
+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
@@ -585,7 +588,7 @@ For issues, please report to https://github.com/non7top/acme.sh/issues.
 
 ## 31. Use Hurricane Electric
 
-Hurricane Electric doesn't have an API so just set your login credentials like so:
+Hurricane Electric (https://dns.he.net/) doesn't have an API so just set your login credentials like so:
 
 ```
 export HE_Username="yourusername"
@@ -602,6 +605,158 @@ The `HE_Username` and `HE_Password` settings will be saved in `~/.acme.sh/accoun
 
 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.
+
 # Use custom API
 
 If your API is not supported yet, you can write your own 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
+}

+ 85 - 7
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_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_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,7 +131,6 @@ _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"
@@ -128,6 +174,37 @@ _get_root() {
   return 1
 }
 
+_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
+    _err "Unable to fetch IAM role from AWS instance metadata."
+    return
+  fi
+  _aws_role=$(_get "$_url" "" 1)
+  _debug "_aws_role" "$_aws_role"
+  _aws_creds="$(
+    _get "$_url$_aws_role" "" 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"
+  eval "$_aws_creds"
+  _using_role=true
+}
+
 #method uri qstr data
 aws_rest() {
   mtd="$1"
@@ -233,6 +310,7 @@ aws_rest() {
   fi
 
   _ret="$?"
+  _debug2 response "$response"
   if [ "$_ret" = "0" ]; then
     if _contains "$response" "<ErrorResponse"; then
       _err "Response error:$response"

+ 249 - 0
dnsapi/dns_azure.sh

@@ -0,0 +1,249 @@
+#!/usr/bin/env sh
+
+########  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"
+  body="{\"properties\": {\"TTL\": 3600, \"TXTRecords\": [{\"value\": [\"$txtvalue\"]}]}}"
+  _azure_rest PUT "$acmeRecordURI" "$body" "$accesstoken"
+  if [ "$_code" = "200" ] || [ "$_code" = '201' ]; then
+    _info "validation record added"
+  else
+    _err "error adding validation record ($_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"
+  body="{\"properties\": {\"TTL\": 3600, \"TXTRecords\": [{\"value\": [\"$txtvalue\"]}]}}"
+  _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
+}
+
+###################  Private functions below ##################################
+
+_azure_rest() {
+  m=$1
+  ep="$2"
+  data="$3"
+  accesstoken="$4"
+
+  export _H1="authorization: Bearer $accesstoken"
+  export _H2="accept: application/json"
+  export _H3="Content-Type: application/json"
+
+  _debug "$ep"
+  if [ "$m" != "GET" ]; then
+    _debug data "$data"
+    response="$(_post "$data" "$ep" "" "$m")"
+  else
+    response="$(_get "$ep")"
+  fi
+  _debug2 response "$response"
+
+  _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
+  _debug2 "http response code $_code"
+
+  if [ "$?" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+  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
+
+  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"
+  _debug data "$body"
+  response="$(_post "$body" "https://login.windows.net/$TENANTID/oauth2/token" "" "POST")"
+  accesstoken=$(echo "$response" | _egrep_o "\"access_token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
+  _debug2 "response $response"
+
+  if [ -z "$accesstoken" ]; then
+    _err "no acccess token received"
+    return 1
+  fi
+  if [ "$?" != "0" ]; then
+    _err "error $response"
+    return 1
+  fi
+  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
+}

+ 27 - 24
dnsapi/dns_cf.sh

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

+ 16 - 5
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"
 
@@ -97,17 +98,19 @@ _dns_cloudns_init_check() {
   fi
 
   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" ] || [ -z "$CLOUDNS_AUTH_PASSWORD" ]; then
+  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" ]; then
-    _err "CLOUDNS_AUTH_ID is not configured"
+  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
 
@@ -125,6 +128,7 @@ _dns_cloudns_init_check() {
 
   #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
@@ -168,12 +172,19 @@ _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")"

+ 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

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

+ 32 - 45
dnsapi/dns_freedns.sh

@@ -53,6 +53,8 @@ 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
@@ -61,7 +63,6 @@ dns_freedns_add() {
   attempts=2
   while [ "$attempts" -gt "0" ]; do
     attempts="$(_math "$attempts" - 1)"
-
     htmlpage="$(_freedns_retrieve_subdomain_page "$FREEDNS_COOKIE")"
     if [ "$?" != "0" ]; then
       if [ "$using_cached_cookies" = "true" ]; then
@@ -70,19 +71,11 @@ dns_freedns_add() {
       fi
       return 1
     fi
+    _debug2 htmlpage "$htmlpage"
+
+    subdomain_csv="$(echo "$htmlpage" | tr -d "\n\r" | _egrep_o '<form .*</form>' | sed 's/<tr>/@<tr>/g' | tr '@' '\n' | grep edit.php | grep "$top_domain")"
+    _debug2 subdomain_csv "$subdomain_csv"
 
-    # 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")"
     # 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,30 +83,32 @@ 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
     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)"
+        DNSname="$(echo "$line" | _egrep_o 'edit.php.*</a>' | cut -d '>' -f 2 | cut -d '<' -f 1)"
+        _debug2 DNSname "$DNSname"
+        DNStype="$(echo "$line" | sed 's/<td/@<td/g' | tr '@' '\n' | sed -n '4p' | cut -d '>' -f 2 | cut -d '<' -f 1)"
+        _debug2 DNStype "$DNStype"
         if [ "$DNSname" = "$fulldomain" ] && [ "$DNStype" = "TXT" ]; then
-          DNSdataid="$(echo "$line" | cut -d ',' -f 2 | sed 's/^.*data_id=//;s/>.*//')"
+          DNSdataid="$(echo "$line" | _egrep_o 'data_id=.*' | cut -d = -f 2 | cut -d '>' -f 1)"
           # Now get current value for the TXT record.  This method may
           # not produce accurate results as the value field is truncated
           # on this webpage. To get full value we would need to load
           # another page. However we don't really need this so long as
           # there is only one TXT record for the acme challenge subdomain.
-          DNSvalue="$(echo "$line" | cut -d ',' -f 4 | sed 's/^[^&quot;]*&quot;//;s/&quot;.*//;s/<\/td>.*//')"
+          DNSvalue="$(echo "$line" | sed 's/<td/@<td/g' | tr '@' '\n' | sed -n '5p' | cut -d '>' -f 2 | cut -d '<' -f 1)"
+          _debug2 DNSvalue "$DNSvalue"
           if [ $found != 0 ]; then
             break
             # we are breaking out of the loop at the first match of DNS name
@@ -169,8 +164,7 @@ dns_freedns_add() {
       return 0
     else
       # Delete the old TXT record (with the wrong value)
-      _freedns_delete_txt_record "$FREEDNS_COOKIE" "$DNSdataid"
-      if [ "$?" = "0" ]; then
+      if _freedns_delete_txt_record "$FREEDNS_COOKIE" "$DNSdataid"; then
         # And add in new TXT record with the value provided
         _freedns_add_txt_record "$FREEDNS_COOKIE" "$DNSdomainid" "$sub_domain" "$txtvalue"
       fi
@@ -210,18 +204,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")"
+    _debug2 subdomain_csv "$subdomain_csv"
+
     # The above beauty ends with striping out rows that do not have an
     # href to edit.php and do not have the domain name we are looking for.
     # So all we should be left with is CSV of table of subdomains we are
@@ -229,19 +214,21 @@ 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
     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)"
+      line="$(echo "$subdomain_csv" | sed -n "${i}p")"
+      _debug2 line "$line"
+      DNSname="$(echo "$line" | _egrep_o 'edit.php.*</a>' | cut -d '>' -f 2 | cut -d '<' -f 1)"
+      _debug2 DNSname "$DNSname"
+      DNStype="$(echo "$line" | sed 's/<td/@<td/g' | tr '@' '\n' | sed -n '4p' | cut -d '>' -f 2 | cut -d '<' -f 1)"
+      _debug2 DNStype "$DNStype"
       if [ "$DNSname" = "$fulldomain" ] && [ "$DNStype" = "TXT" ]; then
-        DNSdataid="$(echo "$line" | 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"
+        DNSdataid="$(echo "$line" | _egrep_o 'data_id=.*' | cut -d = -f 2 | cut -d '>' -f 1)"
+        _debug2 DNSdataid "$DNSdataid"
+        DNSvalue="$(echo "$line" | sed 's/<td/@<td/g' | tr '@' '\n' | sed -n '5p' | cut -d '>' -f 2 | cut -d '<' -f 1)"
+        _debug2 DNSvalue "$DNSvalue"
         #     if [ "$DNSvalue" = "$txtvalue" ]; then
         # Testing value match fails.  Website is truncating the value
         # field. So for now we will assume that there is only one TXT

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

+ 34 - 51
dnsapi/dns_he.sh

@@ -19,14 +19,16 @@ dns_he_add() {
   _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 HE_Username "$HE_Username"
-  _saveaccountconf HE_Password "$HE_Password"
+  _saveaccountconf_mutable HE_Username "$HE_Username"
+  _saveaccountconf_mutable HE_Password "$HE_Password"
 
   # Fills in the $_zone_id
   _find_zone "$_full_domain" || return 1
@@ -62,7 +64,8 @@ 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."
@@ -72,17 +75,19 @@ dns_he_rm() {
   body="$body&hosted_dns_zoneid=$_zone_id"
   body="$body&menu=edit_zone"
   body="$body&hosted_dns_editzone="
-  domain_regex="$(echo "$_full_domain" | sed 's/\./\\./g')" # escape dots
-  _record_id=$(_post "$body" "https://dns.he.net/" \
-    | tr -d '\n' \
-    | _egrep_o "data=\"&quot;${_txt_value}&quot;([^>]+>){6}[^<]+<[^;]+;deleteRecord\('[0-9]+','${domain_regex}','TXT'\)" \
-    | _egrep_o "[0-9]+','${domain_regex}','TXT'\)$" \
-    | _egrep_o "^[0-9]+"
-  )
-  # The series of egreps above could have been done a bit shorter but
-  #  I wanted to double-check whether it's the correct record (in case
-  #  HE changes their website somehow).
 
+  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"
@@ -105,41 +110,26 @@ dns_he_rm() {
 
 ########################## PRIVATE FUNCTIONS ###########################
 
-#-- _find_zone() -------------------------------------------------------
-# Returns the most specific zone found in administration interface.
-#
-# Example:
-#
-# _find_zone first.second.third.co.uk
-#
-# ... will return the first zone that exists in admin out of these:
-# - "first.second.third.co.uk"
-# - "second.third.co.uk"
-# - "third.co.uk"
-# - "co.uk" <-- unlikely
-# - "uk"    <-'
-#
-# (another approach would be something like this:
-#   https://github.com/hlandau/acme/blob/master/_doc/dns.hook
-#   - that's better if there are multiple pages. It's so much simpler.
-# )
-
 _find_zone() {
-
   _domain="$1"
-
   body="email=${HE_Username}&pass=${HE_Password}"
-  _matches=$(_post "$body" "https://dns.he.net/" \
-    | _egrep_o "delete_dom.*name=\"[^\"]+\" value=\"[0-9]+"
-  )
+  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" | sed -n 3p)"
+  _debug2 _matches "$_matches"
   # Zone names and zone IDs are in same order
-  _zone_ids=$(echo "$_matches" | cut -d '"' -f 5)
-  _zone_names=$(echo "$_matches" | cut -d '"' -f 3)
+  _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
@@ -153,17 +143,10 @@ _find_zone() {
 
     _debug "Looking for zone \"${_attempted_zone}\""
 
-    # Take care of "." and only match whole lines. Note that grep -F
-    # cannot be used because there's no way to make it match whole
-    # lines.
-    regex="^$(echo "$_attempted_zone" | sed 's/\./\\./g')$"
-    line_num=$(echo "$_zone_names" \
-      | grep -n "$regex" \
-      | cut -d : -f 1
-    )
-
-    if [ -n "$line_num" ]; then
-      _zone_id=$(echo "$_zone_ids" | sed "${line_num}q;d")
+    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

+ 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

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

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

+ 3 - 3
dnsapi/dns_nsone.sh

@@ -59,10 +59,10 @@ dns_nsone_add() {
     _err "Add txt record error."
   else
     _info "Updating record"
-    record_id=$(printf "%s\n" "$response" | _egrep_o "\"domain\":\"$fulldomain.\",[^{]*\"type\":\"TXT\",\"id\":\"[^,]*\"" | _head_n 1 | cut -d: -f7 | cut -d, -f1)
-    _debug "record_id" "$record_id"
+    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\"]}],\"type\": \"TXT\",\"domain\":\"$fulldomain\",\"zone\": \"$_domain\"}"
+    _nsone_rest POST "zones/$_domain/$fulldomain/TXT" "{\"answers\": [{\"answer\": [\"$txtvalue\"]},{\"answer\": $prev_txt}],\"type\": \"TXT\",\"domain\":\"$fulldomain\",\"zone\": \"$_domain\"}"
     if [ "$?" = "0" ] && _contains "$response" "$fulldomain"; then
       _info "Updated!"
       #todo: check if the record takes effect

+ 73 - 50
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"
@@ -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"

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

+ 8 - 9
dnsapi/dns_yandex.sh

@@ -16,9 +16,8 @@ dns_yandex_add() {
   _PDD_credentials || return 1
   export _H1="PddToken: $PDD_Token"
 
-  curDomain=$(_PDD_get_domain "$fulldomain")
+  _PDD_get_domain "$fulldomain"
   _debug "Found suitable domain in pdd: $curDomain"
-  curSubdomain="$(echo "${fulldomain}" | sed -e "s@.${curDomain}\$@@")"
   curData="domain=${curDomain}&type=TXT&subdomain=${curSubdomain}&ttl=360&content=${txtvalue}"
   curUri="https://pddimp.yandex.ru/api2/admin/dns/add"
   curResult="$(_post "${curData}" "${curUri}")"
@@ -34,9 +33,8 @@ dns_yandex_rm() {
   record_id=$(pdd_get_record_id "${fulldomain}")
   _debug "Result: $record_id"
 
-  curDomain=$(_PDD_get_domain "$fulldomain")
+  _PDD_get_domain "$fulldomain"
   _debug "Found suitable domain in pdd: $curDomain"
-  curSubdomain="$(echo "${fulldomain}" | sed -e "s@.${curDomain}\$@@")"
 
   curUri="https://pddimp.yandex.ru/api2/admin/dns/del"
   curData="domain=${curDomain}&record_id=${record_id}"
@@ -61,7 +59,7 @@ _PDD_get_domain() {
       __last=1
     fi
 
-    __all_domains="$__all_domains $(echo "$res1" | sed -e "s@,@\n@g" | grep '"name"' | cut -d: -f2 | sed -e 's@"@@g')"
+    __all_domains="$__all_domains $(echo "$res1" | tr "," "\n" | grep '"name"' | cut -d: -f2 | sed -e 's@"@@g')"
 
     __page=$(_math $__page + 1)
   done
@@ -72,8 +70,10 @@ _PDD_get_domain() {
     _debug "finding zone for domain $__t"
     for d in $__all_domains; do
       if [ "$d" = "$__t" ]; then
-        echo "$__t"
-        return
+        p=$(_math $k - 1)
+        curSubdomain="$(echo "$fulldomain" | cut -d . -f "1-$p")"
+        curDomain="$__t"
+        return 0
       fi
     done
     k=$(_math $k + 1)
@@ -96,9 +96,8 @@ _PDD_credentials() {
 pdd_get_record_id() {
   fulldomain="${1}"
 
-  curDomain=$(_PDD_get_domain "$fulldomain")
+  _PDD_get_domain "$fulldomain"
   _debug "Found suitable domain in pdd: $curDomain"
-  curSubdomain="$(echo "${fulldomain}" | sed -e "s@.${curDomain}\$@@")"
 
   curUri="https://pddimp.yandex.ru/api2/admin/dns/list?domain=${curDomain}"
   curResult="$(_get "${curUri}" | _normalizeJson)"

+ 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