Browse Source

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

Ne-Lexa 6 years ago
parent
commit
f90bf756fb
16 changed files with 1160 additions and 52 deletions
  1. 1 0
      Dockerfile
  2. 6 2
      README.md
  3. 74 24
      acme.sh
  4. 37 1
      deploy/README.md
  5. 8 4
      deploy/vault_cli.sh
  6. 121 8
      dnsapi/README.md
  7. 1 1
      dnsapi/dns_azure.sh
  8. 29 7
      dnsapi/dns_cf.sh
  9. 2 2
      dnsapi/dns_cx.sh
  10. 168 0
      dnsapi/dns_exoscale.sh
  11. 3 1
      dnsapi/dns_he.sh
  12. 109 0
      dnsapi/dns_hostingde.sh
  13. 185 0
      dnsapi/dns_linode_v4.sh
  14. 210 0
      dnsapi/dns_mydnsjp.sh
  15. 181 0
      dnsapi/dns_neodigit.sh
  16. 25 2
      dnsapi/dns_nsupdate.sh

+ 1 - 0
Dockerfile

@@ -4,6 +4,7 @@ RUN apk update -f \
   && apk --no-cache add -f \
   openssl \
   coreutils \
+  bind-tools \
   curl \
   socat \
   && rm -rf /var/cache/apk/*

+ 6 - 2
README.md

@@ -35,7 +35,7 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
 - [discourse.org](https://meta.discourse.org/t/setting-up-lets-encrypt/40709)
 - [Centminmod](https://centminmod.com/letsencrypt-acmetool-https.html)
 - [splynx](https://forum.splynx.com/t/free-ssl-cert-for-splynx-lets-encrypt/297)
-- [archlinux](https://aur.archlinux.org/packages/acme.sh-git/)
+- [archlinux](https://www.archlinux.org/packages/community/any/acme.sh)
 - [opnsense.org](https://github.com/opnsense/plugins/tree/master/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient)
 - [CentOS Web Panel](http://centos-webpanel.com/)
 - [lnmp.org](https://lnmp.org/)
@@ -327,9 +327,13 @@ You don't have to do anything manually!
 1. netcup DNS API (https://www.netcup.de)
 1. GratisDNS.dk (https://gratisdns.dk)
 1. Namecheap API (https://www.namecheap.com/)
+1. MyDNS.JP API (https://www.mydns.jp/)
+1. hosting.de (https://www.hosting.de)
+1. Neodigit.net API (https://www.neodigit.net)
+1. Exoscale.com API (https://www.exoscale.com/)
 1. Internet.bs API (https://internetbs.net/)
 
-And: 
+And:
 
 **lexicon DNS API: https://github.com/Neilpang/acme.sh/wiki/How-to-use-lexicon-dns-api
    (DigitalOcean, DNSimple, DNSMadeEasy, DNSPark, EasyDNS, Namesilo, NS1, PointHQ, Rage4 and Vultr etc.)**

+ 74 - 24
acme.sh

@@ -37,6 +37,7 @@ VTYPE_HTTP="http-01"
 VTYPE_DNS="dns-01"
 VTYPE_TLS="tls-sni-01"
 VTYPE_TLS2="tls-sni-02"
+VTYPE_ALPN="tls-alpn-01"
 
 LOCAL_ANY_ADDRESS="0.0.0.0"
 
@@ -48,6 +49,7 @@ NO_VALUE="no"
 
 W_TLS="tls"
 W_DNS="dns"
+W_ALPN="alpn"
 DNS_ALIAS_PREFIX="="
 
 MODE_STATELESS="stateless"
@@ -124,23 +126,19 @@ if [ -t 1 ]; then
 fi
 
 __green() {
-  if [ "$__INTERACTIVE${ACME_NO_COLOR}" = "1" -o "${ACME_FORCE_COLOR}" = "1" ]; then
-    printf '\033[1;31;32m'
+  if [ "${__INTERACTIVE}${ACME_NO_COLOR:-0}" = "10" -o "${ACME_FORCE_COLOR}" = "1" ]; then
+    printf '\033[1;31;32m%b\033[0m' "$1"
+    return
   fi
   printf -- "%b" "$1"
-  if [ "$__INTERACTIVE${ACME_NO_COLOR}" = "1" -o "${ACME_FORCE_COLOR}" = "1" ]; then
-    printf '\033[0m'
-  fi
 }
 
 __red() {
-  if [ "$__INTERACTIVE${ACME_NO_COLOR}" = "1" -o "${ACME_FORCE_COLOR}" = "1" ]; then
-    printf '\033[1;31;40m'
+  if [ "${__INTERACTIVE}${ACME_NO_COLOR:-0}" = "10" -o "${ACME_FORCE_COLOR}" = "1" ]; then
+    printf '\033[1;31;40m%b\033[0m' "$1"
+    return
   fi
   printf -- "%b" "$1"
-  if [ "$__INTERACTIVE${ACME_NO_COLOR}" = "1" -o "${ACME_FORCE_COLOR}" = "1" ]; then
-    printf '\033[0m'
-  fi
 }
 
 _printargs() {
@@ -1050,7 +1048,7 @@ _idn() {
   fi
 }
 
-#_createcsr  cn  san_list  keyfile csrfile conf
+#_createcsr  cn  san_list  keyfile csrfile conf acmeValidationv1
 _createcsr() {
   _debug _createcsr
   domain="$1"
@@ -1058,6 +1056,7 @@ _createcsr() {
   csrkey="$3"
   csr="$4"
   csrconf="$5"
+  acmeValidationv1="$6"
   _debug2 domain "$domain"
   _debug2 domainlist "$domainlist"
   _debug2 csrkey "$csrkey"
@@ -1066,7 +1065,9 @@ _createcsr() {
 
   printf "[ req_distinguished_name ]\n[ req ]\ndistinguished_name = req_distinguished_name\nreq_extensions = v3_req\n[ v3_req ]\n\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment" >"$csrconf"
 
-  if [ -z "$domainlist" ] || [ "$domainlist" = "$NO_VALUE" ]; then
+  if [ "$acmeValidationv1" ]; then
+    printf -- "\nsubjectAltName=DNS:$domainlist" >>"$csrconf"
+  elif [ -z "$domainlist" ] || [ "$domainlist" = "$NO_VALUE" ]; then
     #single domain
     _info "Single domain" "$domain"
     printf -- "\nsubjectAltName=DNS:$domain" >>"$csrconf"
@@ -1088,6 +1089,10 @@ _createcsr() {
     printf -- "\nbasicConstraints = CA:FALSE\n1.3.6.1.5.5.7.1.24=DER:30:03:02:01:05" >>"$csrconf"
   fi
 
+  if [ "$acmeValidationv1" ]; then
+    printf "\n1.3.6.1.5.5.7.1.30.1=critical,DER:04:20:${acmeValidationv1}" >>"${csrconf}"
+  fi
+
   _csr_cn="$(_idn "$domain")"
   _debug2 _csr_cn "$_csr_cn"
   if _contains "$(uname -a)" "MINGW"; then
@@ -1138,12 +1143,17 @@ _readSubjectAltNamesFromCSR() {
 
   if _contains "$_dnsAltnames," "DNS:$_csrsubj,"; then
     _debug "AltNames contains subject"
-    _dnsAltnames="$(printf "%s" "$_dnsAltnames," | sed "s/DNS:$_csrsubj,//g")"
+    _excapedAlgnames="$(echo "$_dnsAltnames" | tr '*' '#')"
+    _debug _excapedAlgnames "$_excapedAlgnames"
+    _escapedSubject="$(echo "$_csrsubj" | tr '*' '#')"
+    _debug _escapedSubject "$_escapedSubject"
+    _dnsAltnames="$(echo "$_excapedAlgnames," | sed "s/DNS:$_escapedSubject,//g" | tr '#' '*' | sed "s/,\$//g")"
+    _debug _dnsAltnames "$_dnsAltnames"
   else
     _debug "AltNames doesn't contain subject"
   fi
 
-  printf "%s" "$_dnsAltnames" | sed "s/DNS://g"
+  echo "$_dnsAltnames" | sed "s/DNS://g"
 }
 
 #_csrfile
@@ -1520,7 +1530,8 @@ _calcjwk() {
     JWK_HEADERPLACE_PART1='{"nonce": "'
     JWK_HEADERPLACE_PART2='", "alg": "ES'$__ECC_KEY_LEN'"'
   else
-    _err "Only RSA or EC key is supported."
+    _err "Only RSA or EC key is supported. keyfile=$keyfile"
+    _debug2 "$(cat "$keyfile")"
     return 1
   fi
 
@@ -2105,7 +2116,7 @@ _sleep() {
   fi
 }
 
-# _starttlsserver  san_a  san_b port content _ncaddr
+# _starttlsserver  san_a  san_b port content _ncaddr acmeValidationv1
 _starttlsserver() {
   _info "Starting tls server."
   san_a="$1"
@@ -2113,10 +2124,12 @@ _starttlsserver() {
   port="$3"
   content="$4"
   opaddr="$5"
+  acmeValidationv1="$6"
 
   _debug san_a "$san_a"
   _debug san_b "$san_b"
   _debug port "$port"
+  _debug acmeValidationv1 "$acmeValidationv1"
 
   #create key TLS_KEY
   if ! _createkey "2048" "$TLS_KEY"; then
@@ -2129,7 +2142,7 @@ _starttlsserver() {
   if [ "$san_b" ]; then
     alt="$alt,$san_b"
   fi
-  if ! _createcsr "tls.acme.sh" "$alt" "$TLS_KEY" "$TLS_CSR" "$TLS_CONF"; then
+  if ! _createcsr "tls.acme.sh" "$alt" "$TLS_KEY" "$TLS_CSR" "$TLS_CONF" "$acmeValidationv1"; then
     _err "Create tls validation csr error."
     return 1
   fi
@@ -2155,6 +2168,10 @@ _starttlsserver() {
     __S_OPENSSL="$__S_OPENSSL -6"
   fi
 
+  if [ "$acmeValidationv1" ]; then
+    __S_OPENSSL="$__S_OPENSSL -alpn acme-tls/1"
+  fi
+
   _debug "$__S_OPENSSL"
   if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then
     $__S_OPENSSL -tlsextdebug &
@@ -2843,7 +2860,7 @@ _isRealNginxConf() {
         _skip_ssl=1
         for _listen_i in $(echo "$_seg_n" | tr "\t" ' ' | grep "^ *listen" | tr -d " "); do
           if [ "$_listen_i" ]; then
-            if [ "$(echo "$_listen_i" | _egrep_o "listen.*ssl[ |;]")" ]; then
+            if [ "$(echo "$_listen_i" | _egrep_o "listen.*ssl")" ]; then
               _debug2 "$_listen_i is ssl"
             else
               _debug2 "$_listen_i is plain text"
@@ -2925,6 +2942,7 @@ _clearupdns() {
     _debug txt "$txt"
     if [ "$keyauthorization" = "$STATE_VERIFIED" ]; then
       _debug "$d is already verified, skip $vtype."
+      _alias_index="$(_math "$_alias_index" + 1)"
       continue
     fi
 
@@ -3064,8 +3082,8 @@ _on_before_issue() {
         _savedomainconf "Le_HTTPPort" "$Le_HTTPPort"
       fi
       _checkport="$Le_HTTPPort"
-    elif [ "$_currentRoot" = "$W_TLS" ]; then
-      _info "Standalone tls mode."
+    elif [ "$_currentRoot" = "$W_TLS" ] || [ "$_currentRoot" = "$W_ALPN" ]; then
+      _info "Standalone tls/alpn mode."
       if [ -z "$Le_TLSPort" ]; then
         Le_TLSPort=443
       else
@@ -3691,6 +3709,10 @@ $_authorizations_map"
         fi
       fi
 
+      if [ "$_currentRoot" = "$W_ALPN" ]; then
+        vtype="$VTYPE_ALPN"
+      fi
+
       if [ "$ACME_VERSION" = "2" ]; then
         response="$(echo "$_authorizations_map" | grep "^$d," | sed "s/$d,//")"
         _debug2 "response" "$response"
@@ -3775,6 +3797,7 @@ $_authorizations_map"
       _debug d "$d"
       if [ "$keyauthorization" = "$STATE_VERIFIED" ]; then
         _debug "$d is already verified, skip $vtype."
+        _alias_index="$(_math "$_alias_index" + 1)"
         continue
       fi
 
@@ -4003,6 +4026,16 @@ $_authorizations_map"
         _on_issue_err "$_post_hook" "$vlist"
         return 1
       fi
+    elif [ "$vtype" = "$VTYPE_ALPN" ]; then
+      acmevalidationv1="$(printf "%s" "$keyauthorization" | _digest "sha256" "hex")"
+      _debug acmevalidationv1 "$acmevalidationv1"
+      if ! _starttlsserver "$d" "" "$Le_TLSPort" "$keyauthorization" "$_ncaddr" "$acmevalidationv1"; then
+        _err "Start tls server error."
+        _clearupwebbroot "$_currentRoot" "$removelevel" "$token"
+        _clearup
+        _on_issue_err "$_post_hook" "$vlist"
+        return 1
+      fi
     fi
 
     if ! __trigger_validation "$uri" "$keyauthorization"; then
@@ -4602,7 +4635,8 @@ deploy() {
 
   _initpath "$_d" "$_isEcc"
   if [ ! -d "$DOMAIN_PATH" ]; then
-    _err "Domain is not valid:'$_d'"
+    _err "The domain '$_d' is not a cert name. You must use the cert name to specify the cert to install."
+    _err "Can not find path:'$DOMAIN_PATH'"
     return 1
   fi
 
@@ -4629,7 +4663,8 @@ installcert() {
 
   _initpath "$_main_domain" "$_isEcc"
   if [ ! -d "$DOMAIN_PATH" ]; then
-    _err "Domain is not valid:'$_main_domain'"
+    _err "The domain '$_main_domain' is not a cert name. You must use the cert name to specify the cert to install."
+    _err "Can not find path:'$DOMAIN_PATH'"
     return 1
   fi
 
@@ -5463,6 +5498,7 @@ Parameters:
   --output-insecure                 Output all the sensitive messages. By default all the credentials/sensitive messages are hidden from the output/debug/log for secure.
   --webroot, -w  /path/to/webroot   Specifies the web root folder for web root mode.
   --standalone                      Use standalone mode.
+  --alpn                            Use standalone alpn mode.
   --stateless                       Use stateless mode, see: $_STATELESS_WIKI
   --apache                          Use apache mode.
   --dns [dns_cf|dns_dp|dns_cx|/path/to/api/file]   Use dns mode or dns api.
@@ -5474,7 +5510,7 @@ Parameters:
   --log-level 1|2                   Specifies the log level, default is 1.
   --syslog [0|3|6|7]                Syslog level, 0: disable syslog, 3: error, 6: info, 7: debug.
 
-  These parameters are to install the cert to nginx/apache or anyother server after issue/renew a cert:
+  These parameters are to install the cert to nginx/apache or any other server after issue/renew a cert:
 
   --cert-file                       After issue/renew, the cert will be copied to this path.
   --key-file                        After issue/renew, the key will be copied to this path.
@@ -5485,7 +5521,7 @@ Parameters:
 
   --server SERVER                   ACME Directory Resource URI. (default: https://acme-v01.api.letsencrypt.org/directory)
   --accountconf                     Specifies a customized account config file.
-  --home                            Specifies the home dir for $PROJECT_NAME .
+  --home                            Specifies the home dir for $PROJECT_NAME.
   --cert-home                       Specifies the home dir to save all the certs, only valid for '--install' command.
   --config-home                     Specifies the home dir to save all the configurations.
   --useragent                       Specifies the user agent string. it will be saved for future use too.
@@ -5493,6 +5529,7 @@ Parameters:
   --accountkey                      Specifies the account key path, only valid for the '--install' command.
   --days                            Specifies the days to renew the cert when using '--issue' command. The max value is $MAX_RENEW days.
   --httpport                        Specifies the standalone listening port. Only valid if the server is behind a reverse proxy or load balancer.
+  --tlsport                         Specifies the standalone tls listening port. Only valid if the server is behind a reverse proxy or load balancer.
   --local-address                   Specifies the standalone/tls server listening address, in case you have multiple ip addresses.
   --listraw                         Only used for '--list' command, list the certs in raw format.
   --stopRenewOnError, -se           Only valid for '--renew-all' command. Stop if one cert has error in renewal.
@@ -5817,6 +5854,14 @@ _process() {
           _webroot="$_webroot,$wvalue"
         fi
         ;;
+      --alpn)
+        wvalue="$W_ALPN"
+        if [ -z "$_webroot" ]; then
+          _webroot="$wvalue"
+        else
+          _webroot="$_webroot,$wvalue"
+        fi
+        ;;
       --stateless)
         wvalue="$MODE_STATELESS"
         if [ -z "$_webroot" ]; then
@@ -5941,6 +5986,11 @@ _process() {
         Le_HTTPPort="$_httpport"
         shift
         ;;
+      --tlsport)
+        _tlsport="$2"
+        Le_TLSPort="$_tlsport"
+        shift
+        ;;
       --listraw)
         _listraw="raw"
         ;;

+ 37 - 1
deploy/README.md

@@ -295,4 +295,40 @@ You can then deploy the certificate as follows
 
 ```sh
 acme.sh --deploy -d www.mydomain.com --deploy-hook gitlab
-```
+```
+
+## 12. Deploy your cert to Hashicorp Vault
+
+```sh
+export VAULT_PREFIX="acme"
+```
+
+You can then deploy the certificate as follows
+
+```sh
+acme.sh --deploy -d www.mydomain.com --deploy-hook vault_cli
+```
+
+Your certs will be saved in Vault using this structure:
+
+```sh
+vault write "${VAULT_PREFIX}/${domain}/cert.pem"      value=@"..."
+vault write "${VAULT_PREFIX}/${domain}/cert.key"      value=@"..."
+vault write "${VAULT_PREFIX}/${domain}/chain.pem"     value=@"..."
+vault write "${VAULT_PREFIX}/${domain}/fullchain.pem" value=@"..."
+```
+
+You might be using Fabio load balancer (which can get certs from
+Vault). It needs a bit different structure of your certs in Vault. It
+gets certs only from keys that were saved in `prefix/domain`, like this:
+
+```bash
+vault write <PREFIX>/www.domain.com cert=@cert.pem key=@key.pem
+```
+
+If you want to save certs in Vault this way just set "FABIO" env
+variable to anything (ex: "1") before running `acme.sh`:
+
+```sh
+export FABIO="1"
+```

+ 8 - 4
deploy/vault_cli.sh

@@ -49,9 +49,13 @@ vault_cli_deploy() {
     return 1
   fi
 
-  $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}/cert.pem" value=@"$_ccert" || return 1
-  $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}/cert.key" value=@"$_ckey" || return 1
-  $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}/chain.pem" value=@"$_cca" || return 1
-  $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}/fullchain.pem" value=@"$_cfullchain" || return 1
+  if [ -n "$FABIO" ]; then
+    $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}" cert=@"$_cfullchain" key=@"$_ckey" || return 1
+  else
+    $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}/cert.pem" value=@"$_ccert" || return 1
+    $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}/cert.key" value=@"$_ckey" || return 1
+    $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}/chain.pem" value=@"$_cca" || return 1
+    $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}/fullchain.pem" value=@"$_cfullchain" || return 1
+  fi
 
 }

+ 121 - 8
dnsapi/README.md

@@ -6,7 +6,7 @@ https://github.com/Neilpang/acme.sh/wiki/DNS-alias-mode
 
 ## 1. Use CloudFlare domain API to automatically issue cert
 
-First you need to login to your CloudFlare account to get your API key.
+First you need to login to your CloudFlare account to get your [API key](https://dash.cloudflare.com/profile). 
 
 ```
 export CF_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
@@ -146,13 +146,17 @@ Finally, make the DNS server and update Key available to `acme.sh`
 export NSUPDATE_SERVER="dns.example.com"
 export NSUPDATE_KEY="/path/to/your/nsupdate.key"
 ```
+and optionally (depending on DNS server)
+```
+export NSUPDATE_ZONE="example.com"
+```
 
 Ok, let's issue a cert now:
 ```
 acme.sh --issue --dns dns_nsupdate -d example.com -d www.example.com
 ```
 
-The `NSUPDATE_SERVER` and `NSUPDATE_KEY` settings will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+The `NSUPDATE_SERVER`, `NSUPDATE_KEY`, and `NSUPDATE_ZONE` settings will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
 
 
 ## 8. Use LuaDNS domain API
@@ -263,16 +267,26 @@ when needed.
 
 ## 14. Use Linode domain API
 
+The tokens created in the classic manager and cloud manager are incompatible
+with one another. While the classic manager makes an all or nothing API, the
+newer cloud manager interface promises to produce API keys with a finer
+permission system. However, either way works just fine.
+
+### Classic Manager ###
+
+Classic Manager: https://manager.linode.com/profile/api
+
 First you need to login to your Linode account to get your API Key.
-[https://manager.linode.com/profile/api](https://manager.linode.com/profile/api)
 
-Then add an API key with label *ACME* and copy the new key.
+Then add an API key with label *ACME* and copy the new key into the following
+command.
 
 ```sh
 export LINODE_API_KEY="..."
 ```
 
-Due to the reload time of any changes in the DNS records, we have to use the `dnssleep` option to wait at least 15 minutes for the changes to take effect.
+Due to the reload time of any changes in the DNS records, we have to use the
+`dnssleep` option to wait at least 15 minutes for the changes to take effect.
 
 Ok, let's issue a cert now:
 
@@ -280,7 +294,35 @@ Ok, let's issue a cert now:
 acme.sh --issue --dns dns_linode --dnssleep 900 -d example.com -d www.example.com
 ```
 
-The `LINODE_API_KEY` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+The `LINODE_API_KEY` will be saved in `~/.acme.sh/account.conf` and will be
+reused when needed.
+
+### Cloud Manager ###
+
+Cloud Manager: https://cloud.linode.com/profile/tokens
+
+First you need to login to your Linode account to get your API Key.
+
+   1. Click on "Add a Personal Access Token".
+   2. Give the new key a "Label" (we recommend *ACME*)
+   3. Give it Read/Write access to "Domains"
+   4. "Submit" and copy the new key into the `LINODE_V4_API_KEY` command below.
+
+```sh
+export LINODE_V4_API_KEY="..."
+```
+
+Due to the reload time of any changes in the DNS records, we have to use the
+`dnssleep` option to wait at least 15 minutes for the changes to take effect.
+
+Ok, let's issue a cert now:
+
+```sh
+acme.sh --issue --dns dns_linode_v4 --dnssleep 900 -d example.com -d www.example.com
+```
+
+The `LINODE_V4_API_KEY` will be saved in `~/.acme.sh/account.conf` and will be
+reused when needed.
 
 ## 15. Use FreeDNS
 
@@ -454,7 +496,7 @@ The `Infoblox_Creds` and `Infoblox_Server` will be saved in `~/.acme.sh/account.
 First you need to create/obtain API tokens on your [settings panel](https://vscale.io/panel/settings/tokens/).
 
 ```
-VSCALE_API_KEY="sdfsdfsdfljlbjkljlkjsdfoiwje"
+export VSCALE_API_KEY="sdfsdfsdfljlbjkljlkjsdfoiwje"
 ```
 
 Ok, let's issue a cert now:
@@ -1014,7 +1056,78 @@ Now you can issue a certificate.
 acme.sh --issue --dns dns_namecheap -d example.com -d *.example.com
 ```
 
-## 54. Use Internet.bs
+## 54. Use MyDNS.JP API
+
+First, register to MyDNS.JP and get MasterID and Password.
+
+```
+export MYDNSJP_MasterID=MasterID
+export MYDNSJP_Password=Password
+```
+
+To issue a certificate:
+
+```
+acme.sh --issue --dns dns_mydnsjp -d example.com -d www.example.com
+```
+The `MYDNSJP_MasterID` and `MYDNSJP_Password` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 55. Use hosting.de API
+
+Create an API key in your hosting.de account here: https://secure.hosting.de
+
+The key needs the following rights:
+- DNS_ZONES_EDIT
+- DNS_ZONES_LIST
+
+Set your API Key and endpoint:
+
+```
+export HOSTINGDE_APIKEY='xxx'
+export HOSTINGDE_ENDPOINT='https://secure.hosting.de'
+```
+
+The plugin can also be used for the http.net API. http.net customers have to set endpoint to https://partner.http.net.
+
+Ok, let's issue a cert now:
+```
+acme.sh --issue --dns dns_hostingde -d example.com -d *.example.com
+```
+
+The hosting.de API key and endpoint will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 56. Use Neodigit.net API
+
+```
+export NEODIGIT_API_TOKEN="eXJxTkdUVUZmcHQ3QWJackQ4ZGlMejRDSklRYmo5VG5zcFFKK2thYnE0WnVnNnMy"
+```
+
+Ok, let's issue a cert now:
+```
+acme.sh --issue --dns dns_neodigit -d example.com -d www.example.com
+```
+
+Neodigit API Token will be saved in `~/.acme.sh/account.conf` and will be used when needed.
+
+## 57. Use Exoscale API
+
+Create an API key and secret key in the Exoscale account section
+
+Set your API and secret key:
+
+```
+export EXOSCALE_API_KEY='xxx'
+export EXOSCALE_SECRET_KEY='xxx'
+```
+
+Now, let's issue a cert:
+```
+acme.sh --issue --dns dns_netcup -d example.com -d www.example.com
+```
+
+The `EXOSCALE_API_KEY` and `EXOSCALE_SECRET_KEY` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 58. Use Internet.bs
 
 First you need to create/obtain API credentials on your Internet.bs (https://internetbs.net) account. Go to the "Get my API Key" section in the "My Domains" section.
 

+ 1 - 1
dnsapi/dns_azure.sh

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

+ 29 - 7
dnsapi/dns_cf.sh

@@ -34,6 +34,9 @@ dns_cf_add() {
   _saveaccountconf_mutable CF_Key "$CF_Key"
   _saveaccountconf_mutable CF_Email "$CF_Email"
 
+  _DOMAIN_CF_ZONES_CACHE_NAME_="$(echo "${CF_Email}_CF_ZONES_" | tr '@.' '__')"
+  _cleardomainconf "$_DOMAIN_CF_ZONES_CACHE_NAME_"
+
   _debug "First detect the root zone"
   if ! _get_root "$fulldomain"; then
     _err "invalid domain"
@@ -58,9 +61,12 @@ dns_cf_add() {
   #  if [ "$count" = "0" ]; then
   _info "Adding record"
   if _cf_rest POST "zones/$_domain_id/dns_records" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":120}"; then
-    if printf -- "%s" "$response" | grep "$fulldomain" >/dev/null; then
+    if _contains "$response" "$fulldomain"; then
       _info "Added, OK"
       return 0
+    elif _contains "$response" "The record already exists"; then
+      _info "Already exists, OK"
+      return 0
     else
       _err "Add txt record error."
       return 1
@@ -99,11 +105,16 @@ dns_cf_rm() {
     return 1
   fi
 
+  _DOMAIN_CF_ZONES_CACHE_NAME_="$(echo "${CF_Email}_CF_ZONES_" | tr '@.' '__')"
+
   _debug "First detect the root zone"
   if ! _get_root "$fulldomain"; then
+    _cleardomainconf "$_DOMAIN_CF_ZONES_CACHE_NAME_"
     _err "invalid domain"
     return 1
   fi
+  _cleardomainconf "$_DOMAIN_CF_ZONES_CACHE_NAME_"
+
   _debug _domain_id "$_domain_id"
   _debug _sub_domain "$_sub_domain"
   _debug _domain "$_domain"
@@ -143,6 +154,21 @@ dns_cf_rm() {
 # _domain=domain.com
 # _domain_id=sdjkglgdfewsdfg
 _get_root() {
+
+  _cf_zones="$(_readdomainconf "$_DOMAIN_CF_ZONES_CACHE_NAME_")"
+  _debug2 "_cf_zones" "$_cf_zones"
+  if [ -z "$_cf_zones" ]; then
+    _debug "$_DOMAIN_CF_ZONES_CACHE_NAME_ is none, so get it."
+    if ! _cf_rest GET "zones"; then
+      return 1
+    fi
+    _cf_zones="$response"
+    _savedomainconf "$_DOMAIN_CF_ZONES_CACHE_NAME_" "$(echo "$_cf_zones" | _base64)"
+  else
+    _debug "$_DOMAIN_CF_ZONES_CACHE_NAME_ found"
+    _cf_zones="$(echo "$_cf_zones" | _dbase64)"
+  fi
+
   domain=$1
   i=2
   p=1
@@ -154,12 +180,8 @@ _get_root() {
       return 1
     fi
 
-    if ! _cf_rest GET "zones?name=$h"; then
-      return 1
-    fi
-
-    if _contains "$response" "\"name\":\"$h\"" >/dev/null; then
-      _domain_id=$(printf "%s\n" "$response" | _egrep_o "\[.\"id\":\"[^\"]*\"" | head -n 1 | cut -d : -f 2 | tr -d \")
+    if _contains "$_cf_zones" "\"name\":\"$h\"" >/dev/null; then
+      _domain_id=$(echo "$_cf_zones" | tr '{' "\n" | grep "\"name\":\"$h\"" | _egrep_o "^\"id\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
       if [ "$_domain_id" ]; then
         _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
         _domain=$h

+ 2 - 2
dnsapi/dns_cx.sh

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

+ 168 - 0
dnsapi/dns_exoscale.sh

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

+ 3 - 1
dnsapi/dns_he.sh

@@ -92,7 +92,9 @@ dns_he_rm() {
     return 1
   fi
   # Remove the record
-  body="email=${HE_Username}&pass=${HE_Password}"
+  username_encoded="$(printf "%s" "${HE_Username}" | _url_encode)"
+  password_encoded="$(printf "%s" "${HE_Password}" | _url_encode)"
+  body="email=${username_encoded}&pass=${password_encoded}"
   body="$body&menu=edit_zone"
   body="$body&hosted_dns_zoneid=$_zone_id"
   body="$body&hosted_dns_recordid=$_record_id"

+ 109 - 0
dnsapi/dns_hostingde.sh

@@ -0,0 +1,109 @@
+#!/usr/bin/env sh
+
+# hosting.de API
+
+# Values to export:
+# export HOSTINGDE_ENDPOINT='https://secure.hosting.de'
+# export HOSTINGDE_APIKEY='xxxxx'
+
+########  Public functions #####################
+
+dns_hostingde_add() {
+  fulldomain="${1}"
+  txtvalue="${2}"
+  _debug "Calling: _hostingde_addRecord() '${fulldomain}' '${txtvalue}'"
+  _hostingde_apiKey && _hostingde_getZoneConfig && _hostingde_addRecord
+}
+
+dns_hostingde_rm() {
+  fulldomain="${1}"
+  txtvalue="${2}"
+  _debug "Calling: _hostingde_removeRecord() '${fulldomain}' '${txtvalue}'"
+  _hostingde_apiKey && _hostingde_getZoneConfig && _hostingde_removeRecord
+}
+
+#################### own Private functions below ##################################
+
+_hostingde_apiKey() {
+  HOSTINGDE_APIKEY="${HOSTINGDE_APIKEY:-$(_readaccountconf_mutable HOSTINGDE_APIKEY)}"
+  if [ -z "$HOSTINGDE_APIKEY" ] || [ -z "$HOSTINGDE_ENDPOINT" ]; then
+    HOSTINGDE_APIKEY=""
+    HOSTINGDE_ENDPOINT=""
+    _err "You haven't specified hosting.de API key or endpoint yet."
+    _err "Please create your key and try again."
+    return 1
+  fi
+
+  _saveaccountconf_mutable HOSTINGDE_APIKEY "$HOSTINGDE_APIKEY"
+  _saveaccountconf_mutable HOSTINGDE_ENDPOINT "$HOSTINGDE_ENDPOINT"
+}
+
+_hostingde_getZoneConfig() {
+  _info "Getting ZoneConfig"
+  curZone="${fulldomain#*.}"
+  returnCode=1
+  while _contains "${curZone}" "\\."; do
+    curData="{\"filter\":{\"field\":\"zoneName\",\"value\":\"${curZone}\"},\"limit\":1,\"authToken\":\"${HOSTINGDE_APIKEY}\"}"
+    curResult="$(_post "${curData}" "${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneConfigsFind")"
+    _debug "Calling zoneConfigsFind: '${curData}' '${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneConfigsFind'"
+    _debug "Result of zoneConfigsFind: '$curResult'"
+    if _contains "${curResult}" '"status": "error"'; then
+      if _contains "${curResult}" '"code": 10109'; then
+        _err "The API-Key is invalid or could not be found"
+      else
+        _err "UNKNOWN API ERROR"
+      fi
+      returnCode=1
+      break
+    fi
+    if _contains "${curResult}" '"totalEntries": 1'; then
+      _info "Retrieved zone data."
+      _debug "Zone data: '${curResult}'"
+
+      # read ZoneConfigId for later update
+      zoneConfigId=$(echo "${curResult}" | _egrep_o '"id":.*' | cut -d ':' -f 2 | cut -d '"' -f 2)
+      _debug "zoneConfigId '${zoneConfigId}'"
+      returnCode=0
+      break
+    fi
+    curZone="${curZone#*.}"
+  done
+  if [ $returnCode -ne 0 ]; then
+    _info "ZoneEnd reached, Zone ${curZone} not found in hosting.de API"
+  fi
+  return $returnCode
+}
+
+_hostingde_addRecord() {
+  _info "Adding record to zone"
+  curData="{\"authToken\":\"${HOSTINGDE_APIKEY}\",\"zoneConfig\":{\"id\":\"${zoneConfigId}\"},\"recordsToAdd\":[{\"name\":\"${fulldomain}\",\"type\":\"TXT\",\"content\":\"\\\"${txtvalue}\\\"\",\"ttl\":3600}]}"
+  curResult="$(_post "${curData}" "${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneUpdate")"
+  _debug "Calling zoneUpdate: '${curData}' '${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneUpdate'"
+  _debug "Result of zoneUpdate: '$curResult'"
+  if _contains "${curResult}" '"status": "error"'; then
+    if _contains "${curResult}" '"code": 10109'; then
+      _err "The API-Key is invalid or could not be found"
+    else
+      _err "UNKNOWN API ERROR"
+    fi
+    return 1
+  fi
+  return 0
+}
+
+_hostingde_removeRecord() {
+  _info "Removing record from zone"
+  curData="{\"authToken\":\"${HOSTINGDE_APIKEY}\",\"zoneConfig\":{\"id\":\"${zoneConfigId}\"},\"recordsToDelete\":[{\"name\":\"${fulldomain}\",\"type\":\"TXT\",\"content\":\"\\\"${txtvalue}\\\"\"}]}"
+  curResult="$(_post "${curData}" "${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneUpdate")"
+  _debug "Calling zoneUpdate: '${curData}' '${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneUpdate'"
+  _debug "Result of zoneUpdate: '$curResult'"
+  if _contains "${curResult}" '"status": "error"'; then
+    if _contains "${curResult}" '"code": 10109'; then
+      _err "The API-Key is invalid or could not be found"
+    else
+      _err "UNKNOWN API ERROR"
+    fi
+    return 1
+  fi
+  return 0
+}

+ 185 - 0
dnsapi/dns_linode_v4.sh

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

+ 210 - 0
dnsapi/dns_mydnsjp.sh

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

+ 181 - 0
dnsapi/dns_neodigit.sh

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

+ 25 - 2
dnsapi/dns_nsupdate.sh

@@ -13,12 +13,24 @@ dns_nsupdate_add() {
   _saveaccountconf NSUPDATE_SERVER "${NSUPDATE_SERVER}"
   _saveaccountconf NSUPDATE_SERVER_PORT "${NSUPDATE_SERVER_PORT}"
   _saveaccountconf NSUPDATE_KEY "${NSUPDATE_KEY}"
+  _saveaccountconf NSUPDATE_ZONE "${NSUPDATE_ZONE}"
   _info "adding ${fulldomain}. 60 in txt \"${txtvalue}\""
-  nsupdate -k "${NSUPDATE_KEY}" <<EOF
+  [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_1" ] && nsdebug="-d"
+  [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_2" ] && nsdebug="-D"
+  if [ -z "${NSUPDATE_ZONE}" ]; then
+    nsupdate -k "${NSUPDATE_KEY}" $nsdebug <<EOF
 server ${NSUPDATE_SERVER}  ${NSUPDATE_SERVER_PORT} 
 update add ${fulldomain}. 60 in txt "${txtvalue}"
 send
 EOF
+  else
+    nsupdate -k "${NSUPDATE_KEY}" $nsdebug <<EOF
+server ${NSUPDATE_SERVER}  ${NSUPDATE_SERVER_PORT}
+zone ${NSUPDATE_ZONE}.
+update add ${fulldomain}. 60 in txt "${txtvalue}"
+send
+EOF
+  fi
   if [ $? -ne 0 ]; then
     _err "error updating domain"
     return 1
@@ -34,11 +46,22 @@ dns_nsupdate_rm() {
   [ -n "${NSUPDATE_SERVER}" ] || NSUPDATE_SERVER="localhost"
   [ -n "${NSUPDATE_SERVER_PORT}" ] || NSUPDATE_SERVER_PORT=53
   _info "removing ${fulldomain}. txt"
-  nsupdate -k "${NSUPDATE_KEY}" <<EOF
+  [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_1" ] && nsdebug="-d"
+  [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_2" ] && nsdebug="-D"
+  if [ -z "${NSUPDATE_ZONE}" ]; then
+    nsupdate -k "${NSUPDATE_KEY}" $nsdebug <<EOF
 server ${NSUPDATE_SERVER}  ${NSUPDATE_SERVER_PORT} 
 update delete ${fulldomain}. txt
 send
 EOF
+  else
+    nsupdate -k "${NSUPDATE_KEY}" $nsdebug <<EOF
+server ${NSUPDATE_SERVER}  ${NSUPDATE_SERVER_PORT}
+zone ${NSUPDATE_ZONE}.
+update delete ${fulldomain}. txt
+send
+EOF
+  fi
   if [ $? -ne 0 ]; then
     _err "error updating domain"
     return 1