Browse Source

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

# Conflicts:
#	README.md
#	dnsapi/README.md
Ne-Lexa 6 years ago
parent
commit
412f85b665

+ 35 - 15
README.md

@@ -70,11 +70,16 @@ For all build statuses, check our [weekly build project](https://github.com/Neil
 
 https://github.com/Neilpang/acmetest
 
+# Supported CA
+
+- Letsencrypt.org CA(default)
+- [BuyPass.com CA](https://github.com/Neilpang/acme.sh/wiki/BuyPass.com-CA)
 
 # Supported modes
 
 - Webroot mode
 - Standalone mode
+- Standalone tls-alpn mode
 - Apache mode
 - Nginx mode
 - DNS mode
@@ -221,8 +226,20 @@ acme.sh --issue --standalone -d example.com -d www.example.com -d cp.example.com
 
 More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert
 
+# 5. Use Standalone ssl server to issue cert
+
+**(requires you to be root/sudoer or have permission to listen on port 443 (TCP))**
+
+Port `443` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again.
+
+```bash
+acme.sh --issue --alpn -d example.com -d www.example.com -d cp.example.com
+```
+
+More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert
+
 
-# 5. Use Apache mode
+# 6. Use Apache mode
 
 **(requires you to be root/sudoer, since it is required to interact with Apache server)**
 
@@ -242,7 +259,7 @@ We don't want to mess your apache server, don't worry.**
 
 More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert
 
-# 6. Use Nginx mode
+# 7. Use Nginx mode
 
 **(requires you to be root/sudoer, since it is required to interact with Nginx server)**
 
@@ -266,7 +283,7 @@ We don't want to mess your nginx server, don't worry.**
 
 More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert
 
-# 7. Automatic DNS API integration
+# 8. Automatic DNS API integration
 
 If your DNS provider supports API access, we can use that API to automatically issue the certs.
 
@@ -331,7 +348,10 @@ You don't have to do anything manually!
 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/)
+1. PointDNS API (https://pointhq.com/)
+1. Active24.cz API (https://www.active24.cz/)
+1. do.de API (https://www.do.de/)
+1. internetbs.net API (https://internetbs.net/)
 
 And:
 
@@ -345,7 +365,7 @@ If your DNS provider is not on the supported list above, you can write your own
 
 For more details: [How to use DNS API](dnsapi)
 
-# 8. Use DNS manual mode:
+# 9. Use DNS manual mode:
 
 See: https://github.com/Neilpang/acme.sh/wiki/dns-manual-mode first.
 
@@ -381,7 +401,7 @@ Ok, it's done.
 
 **Please use dns api mode instead.**
 
-# 9. Issue ECC certificates
+# 10. Issue ECC certificates
 
 `Let's Encrypt` can now issue **ECDSA** certificates.
 
@@ -413,7 +433,7 @@ Valid values are:
 
 
 
-# 10. Issue Wildcard certificates
+# 11. Issue Wildcard certificates
 
 It's simple, just give a wildcard domain as the `-d` parameter.
 
@@ -423,7 +443,7 @@ acme.sh  --issue -d example.com  -d '*.example.com'  --dns dns_cf
 
 
 
-# 11. How to renew the certs
+# 12. How to renew the certs
 
 No, you don't need to renew the certs manually. All the certs will be renewed automatically every **60** days.
 
@@ -440,7 +460,7 @@ acme.sh --renew -d example.com --force --ecc
 ```
 
 
-# 12. How to stop cert renewal
+# 13. How to stop cert renewal
 
 To stop renewal of a cert, you can execute the following to remove the cert from the renewal list:
 
@@ -453,7 +473,7 @@ The cert/key file is not removed from the disk.
 You can remove the respective directory (e.g. `~/.acme.sh/example.com`) by yourself.
 
 
-# 13. How to upgrade `acme.sh`
+# 14. How to upgrade `acme.sh`
 
 acme.sh is in constant development, so it's strongly recommended to use the latest code.
 
@@ -478,25 +498,25 @@ acme.sh --upgrade --auto-upgrade 0
 ```
 
 
-# 14. Issue a cert from an existing CSR
+# 15. Issue a cert from an existing CSR
 
 https://github.com/Neilpang/acme.sh/wiki/Issue-a-cert-from-existing-CSR
 
 
-# 15. Under the Hood
+# 16. Under the Hood
 
 Speak ACME language using shell, directly to "Let's Encrypt".
 
 TODO:
 
 
-# 16. Acknowledgments
+# 17. Acknowledgments
 
 1. Acme-tiny: https://github.com/diafygi/acme-tiny
 2. ACME protocol: https://github.com/ietf-wg-acme/acme
 
 
-# 17. License & Others
+# 18. License & Others
 
 License is GPLv3
 
@@ -505,7 +525,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.
 
 
-# 18. Donate
+# 19. Donate
 Your donation makes **acme.sh** better:
 
 1. PayPal/Alipay(支付宝)/Wechat(微信): [https://donate.acme.sh/](https://donate.acme.sh/)

+ 36 - 71
acme.sh

@@ -1,6 +1,6 @@
 #!/usr/bin/env sh
 
-VER=2.8.0
+VER=2.8.1
 
 PROJECT_NAME="acme.sh"
 
@@ -35,19 +35,16 @@ _OLD_STAGE_CA_HOST="https://acme-staging.api.letsencrypt.org"
 
 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"
 
-MAX_RENEW=60
+DEFAULT_RENEW=60
 
 DEFAULT_DNS_SLEEP=120
 
 NO_VALUE="no"
 
-W_TLS="tls"
 W_DNS="dns"
 W_ALPN="alpn"
 DNS_ALIAS_PREFIX="="
@@ -1090,7 +1087,7 @@ _createcsr() {
   fi
 
   if [ "$acmeValidationv1" ]; then
-    printf "\n1.3.6.1.5.5.7.1.30.1=critical,DER:04:20:${acmeValidationv1}" >>"${csrconf}"
+    printf "\n1.3.6.1.5.5.7.1.31=critical,DER:04:20:${acmeValidationv1}" >>"${csrconf}"
   fi
 
   _csr_cn="$(_idn "$domain")"
@@ -1875,11 +1872,7 @@ _send_signed_request() {
     sig="$(printf "%s" "$_sig_t" | _url_replace)"
     _debug3 sig "$sig"
 
-    if [ "$ACME_VERSION" = "2" ]; then
-      body="{\"protected\": \"$protected64\", \"payload\": \"$payload64\", \"signature\": \"$sig\"}"
-    else
-      body="{\"header\": $JWK_HEADER, \"protected\": \"$protected64\", \"payload\": \"$payload64\", \"signature\": \"$sig\"}"
-    fi
+    body="{\"protected\": \"$protected64\", \"payload\": \"$payload64\", \"signature\": \"$sig\"}"
     _debug3 body "$body"
 
     response="$(_post "$body" "$url" "$needbase64" "POST" "$__request_conent_type")"
@@ -2926,7 +2919,10 @@ _clearup() {
 
 _clearupdns() {
   _debug "_clearupdns"
-  if [ "$dnsadded" != 1 ] || [ -z "$vlist" ]; then
+  _debug "dnsadded" "$dnsadded"
+  _debug "vlist" "$vlist"
+  #dnsadded is "0" or "1" means dns-01 method was used for at least one domain
+  if [ -z "$dnsadded" ] || [ -z "$vlist" ]; then
     _debug "skip dns."
     return
   fi
@@ -3082,8 +3078,8 @@ _on_before_issue() {
         _savedomainconf "Le_HTTPPort" "$Le_HTTPPort"
       fi
       _checkport="$Le_HTTPPort"
-    elif [ "$_currentRoot" = "$W_TLS" ] || [ "$_currentRoot" = "$W_ALPN" ]; then
-      _info "Standalone tls/alpn mode."
+    elif [ "$_currentRoot" = "$W_ALPN" ]; then
+      _info "Standalone alpn mode."
       if [ -z "$Le_TLSPort" ]; then
         Le_TLSPort=443
       else
@@ -3443,15 +3439,17 @@ __get_domain_new_authz() {
 
 #uri keyAuthorization
 __trigger_validation() {
-  _debug2 "tigger domain validation."
+  _debug2 "Trigger domain validation."
   _t_url="$1"
   _debug2 _t_url "$_t_url"
   _t_key_authz="$2"
   _debug2 _t_key_authz "$_t_key_authz"
+  _t_vtype="$3"
+  _debug2 _t_vtype "$_t_vtype"
   if [ "$ACME_VERSION" = "2" ]; then
     _send_signed_request "$_t_url" "{\"keyAuthorization\": \"$_t_key_authz\"}"
   else
-    _send_signed_request "$_t_url" "{\"resource\": \"challenge\", \"keyAuthorization\": \"$_t_key_authz\"}"
+    _send_signed_request "$_t_url" "{\"resource\": \"challenge\", \"type\": \"$_t_vtype\", \"keyAuthorization\": \"$_t_key_authz\"}"
   fi
 }
 
@@ -3654,7 +3652,7 @@ issue() {
       _authorizations_map=""
       for _authz_url in $(echo "$_authorizations_seg" | tr ',' ' '); do
         _debug2 "_authz_url" "$_authz_url"
-        if ! response="$(_get "$_authz_url")"; then
+        if ! _send_signed_request "$_authz_url"; then
           _err "get to authz error."
           _err "_authorizations_seg" "$_authorizations_seg"
           _err "_authz_url" "$_authz_url"
@@ -3701,14 +3699,6 @@ $_authorizations_map"
         vtype="$VTYPE_DNS"
       fi
 
-      if [ "$_currentRoot" = "$W_TLS" ]; then
-        if [ "$ACME_VERSION" = "2" ]; then
-          vtype="$VTYPE_TLS2"
-        else
-          vtype="$VTYPE_TLS"
-        fi
-      fi
-
       if [ "$_currentRoot" = "$W_ALPN" ]; then
         vtype="$VTYPE_ALPN"
       fi
@@ -3861,8 +3851,8 @@ $_authorizations_map"
         )
 
         if [ "$?" != "0" ]; then
-          _clearup
           _on_issue_err "$_post_hook" "$vlist"
+          _clearup
           return 1
         fi
         dnsadded='1'
@@ -3873,8 +3863,8 @@ $_authorizations_map"
       _savedomainconf "Le_Vlist" "$vlist"
       _debug "Dns record not added yet, so, save to $DOMAIN_CONF and exit."
       _err "Please add the TXT records to the domains, and re-run with --renew."
-      _clearup
       _on_issue_err "$_post_hook"
+      _clearup
       return 1
     fi
 
@@ -3908,7 +3898,7 @@ $_authorizations_map"
       continue
     fi
 
-    _info "Verifying:$d"
+    _info "Verifying: $d"
     _debug "d" "$d"
     _debug "keyauthorization" "$keyauthorization"
     _debug "uri" "$uri"
@@ -3992,40 +3982,6 @@ $_authorizations_map"
         fi
 
       fi
-
-    elif [ "$vtype" = "$VTYPE_TLS" ]; then
-      #create A
-      #_hash_A="$(printf "%s" $token | _digest "sha256" "hex" )"
-      #_debug2 _hash_A "$_hash_A"
-      #_x="$(echo $_hash_A | cut -c 1-32)"
-      #_debug2 _x "$_x"
-      #_y="$(echo $_hash_A | cut -c 33-64)"
-      #_debug2 _y "$_y"
-      #_SAN_A="$_x.$_y.token.acme.invalid"
-      #_debug2 _SAN_A "$_SAN_A"
-
-      #create B
-      _hash_B="$(printf "%s" "$keyauthorization" | _digest "sha256" "hex")"
-      _debug2 _hash_B "$_hash_B"
-      _x="$(echo "$_hash_B" | cut -c 1-32)"
-      _debug2 _x "$_x"
-      _y="$(echo "$_hash_B" | cut -c 33-64)"
-      _debug2 _y "$_y"
-
-      #_SAN_B="$_x.$_y.ka.acme.invalid"
-
-      _SAN_B="$_x.$_y.acme.invalid"
-      _debug2 _SAN_B "$_SAN_B"
-
-      _ncaddr="$(_getfield "$_local_addr" "$_ncIndex")"
-      _ncIndex="$(_math "$_ncIndex" + 1)"
-      if ! _starttlsserver "$_SAN_B" "$_SAN_A" "$Le_TLSPort" "$keyauthorization" "$_ncaddr"; then
-        _err "Start tls server error."
-        _clearupwebbroot "$_currentRoot" "$removelevel" "$token"
-        _clearup
-        _on_issue_err "$_post_hook" "$vlist"
-        return 1
-      fi
     elif [ "$vtype" = "$VTYPE_ALPN" ]; then
       acmevalidationv1="$(printf "%s" "$keyauthorization" | _digest "sha256" "hex")"
       _debug acmevalidationv1 "$acmevalidationv1"
@@ -4038,7 +3994,7 @@ $_authorizations_map"
       fi
     fi
 
-    if ! __trigger_validation "$uri" "$keyauthorization"; then
+    if ! __trigger_validation "$uri" "$keyauthorization" "$vtype"; then
       _err "$d:Can not get challenge: $response"
       _clearupwebbroot "$_currentRoot" "$removelevel" "$token"
       _clearup
@@ -4047,7 +4003,7 @@ $_authorizations_map"
     fi
 
     if [ "$code" ] && [ "$code" != '202' ]; then
-      if [ "$ACME_VERSION" = "2" ] && [ "$code" = '200' ]; then
+      if [ "$code" = '200' ]; then
         _debug "trigger validation code: $code"
       else
         _err "$d:Challenge error: $response"
@@ -4076,7 +4032,11 @@ $_authorizations_map"
       _debug "sleep 2 secs to verify"
       sleep 2
       _debug "checking"
-      response="$(_get "$uri")"
+      if [ "$ACME_VERSION" = "2" ]; then
+        _send_signed_request "$uri"
+      else
+        response="$(_get "$uri")"
+      fi
       if [ "$?" != "0" ]; then
         _err "$d:Verify error:$response"
         _clearupwebbroot "$_currentRoot" "$removelevel" "$token"
@@ -4152,13 +4112,16 @@ $_authorizations_map"
     fi
     Le_LinkCert="$(echo "$response" | tr -d '\r\n' | _egrep_o '"certificate" *: *"[^"]*"' | cut -d '"' -f 4)"
 
-    if ! _get "$Le_LinkCert" >"$CERT_PATH"; then
+    _tempSignedResponse="$response"
+    if ! _send_signed_request "$Le_LinkCert" "" "needbase64"; then
       _err "Sign failed, can not download cert:$Le_LinkCert."
       _err "$response"
       _on_issue_err "$_post_hook"
       return 1
     fi
 
+    echo "$response" | _dbase64 "multiline" >"$CERT_PATH"
+
     if [ "$(grep -- "$BEGIN_CERT" "$CERT_PATH" | wc -l)" -gt "1" ]; then
       _debug "Found cert chain"
       cat "$CERT_PATH" >"$CERT_FULLCHAIN_PATH"
@@ -4168,6 +4131,7 @@ $_authorizations_map"
       _end_n="$(_math $_end_n + 1)"
       sed -n "${_end_n},9999p" "$CERT_FULLCHAIN_PATH" >"$CA_CERT_PATH"
     fi
+    response="$_tempSignedResponse"
   else
     if ! _send_signed_request "${ACME_NEW_ORDER}" "{\"resource\": \"$ACME_NEW_ORDER_RES\", \"csr\": \"$der\"}" "needbase64"; then
       _err "Sign failed. $response"
@@ -4238,7 +4202,8 @@ $_authorizations_map"
       while [ "$_link_issuer_retry" -lt "$_MAX_ISSUER_RETRY" ]; do
         _debug _link_issuer_retry "$_link_issuer_retry"
         if [ "$ACME_VERSION" = "2" ]; then
-          if _get "$Le_LinkIssuer" >"$CA_CERT_PATH"; then
+          if _send_signed_request "$Le_LinkIssuer"; then
+            echo "$response" >"$CA_CERT_PATH"
             break
           fi
         else
@@ -4274,8 +4239,8 @@ $_authorizations_map"
   Le_CertCreateTimeStr=$(date -u)
   _savedomainconf "Le_CertCreateTimeStr" "$Le_CertCreateTimeStr"
 
-  if [ -z "$Le_RenewalDays" ] || [ "$Le_RenewalDays" -lt "0" ] || [ "$Le_RenewalDays" -gt "$MAX_RENEW" ]; then
-    Le_RenewalDays="$MAX_RENEW"
+  if [ -z "$Le_RenewalDays" ] || [ "$Le_RenewalDays" -lt "0" ]; then
+    Le_RenewalDays="$DEFAULT_RENEW"
   else
     _savedomainconf "Le_RenewalDays" "$Le_RenewalDays"
   fi
@@ -4964,7 +4929,7 @@ _deactivate() {
 
     authzUri="$_authorizations_seg"
     _debug2 "authzUri" "$authzUri"
-    if ! response="$(_get "$authzUri")"; then
+    if ! _send_signed_request "$authzUri"; then
       _err "get to authz error."
       _err "_authorizations_seg" "$_authorizations_seg"
       _err "authzUri" "$authzUri"
@@ -5527,7 +5492,7 @@ Parameters:
   --useragent                       Specifies the user agent string. it will be saved for future use too.
   --accountemail                    Specifies the account email, only valid for the '--install' and '--update-account' command.
   --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.
+  --days                            Specifies the days to renew the cert when using '--issue' command. The default value is $DEFAULT_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.

+ 49 - 0
deploy/README.md

@@ -332,3 +332,52 @@ variable to anything (ex: "1") before running `acme.sh`:
 ```sh
 export FABIO="1"
 ```
+
+## 13. Deploy your certificate to Qiniu.com
+
+使用 acme.sh 部署到七牛之前,需要确保部署的域名已打开 HTTPS 功能,您可以访问[融合 CDN - 域名管理](https://portal.qiniu.com/cdn/domain) 设置。
+另外还需要先导出 AK/SK 环境变量,您可以访问[密钥管理](https://portal.qiniu.com/user/key) 获得。
+
+```sh
+$ export QINIU_AK="foo"
+$ export QINIU_SK="bar"
+```
+
+完成准备工作之后,您就可以通过下面的命令开始部署 SSL 证书到七牛上:
+
+```sh
+$ acme.sh --deploy -d example.com --deploy-hook qiniu
+```
+
+假如您部署的证书为泛域名证书,您还需要设置 `QINIU_CDN_DOMAIN` 变量,指定实际需要部署的域名:
+
+```sh
+$ export QINIU_CDN_DOMAIN="cdn.example.com"
+$ acme.sh --deploy -d example.com --deploy-hook qiniu
+```
+
+### English version
+
+You should create AccessKey/SecretKey pair in https://portal.qiniu.com/user/key 
+before deploying your certificate, and please ensure you have enabled HTTPS for
+your domain name. You can enable it in https://portal.qiniu.com/cdn/domain.
+
+```sh
+$ export QINIU_AK="foo"
+$ export QINIU_SK="bar"
+```
+
+then you can deploy certificate by following command:
+
+```sh
+$ acme.sh --deploy -d example.com --deploy-hook qiniu
+```
+
+(Optional), If you are using wildcard certificate,
+you may need export `QINIU_CDN_DOMAIN` to specify which domain
+you want to update:
+
+```sh
+$ export QINIU_CDN_DOMAIN="cdn.example.com"
+$ acme.sh --deploy -d example.com --deploy-hook qiniu
+```

+ 92 - 0
deploy/qiniu.sh

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

+ 50 - 4
dnsapi/README.md

@@ -1122,21 +1122,67 @@ export EXOSCALE_SECRET_KEY='xxx'
 
 Now, let's issue a cert:
 ```
-acme.sh --issue --dns dns_netcup -d example.com -d www.example.com
+acme.sh --issue --dns dns_exoscale -d example.com -d www.example.com
 ```
 
 The `EXOSCALE_API_KEY` and `EXOSCALE_SECRET_KEY` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
 
-## 58. Use Internet.bs
+## 58. Using PointHQ API to issue certs
+
+Log into [PointHQ account management](https://app.pointhq.com/profile) and copy the API key from the page there.
+
+```export PointHQ_Key="apikeystringgoeshere"
+exportPointHQ_Email="accountemail@yourdomain.com"
+```
+
+You can then issue certs by using:
+```acme.sh --issue --dns dns_pointhq -d example.com -d www.example.com
+```
+
+## 59. Use Active24 API
 
-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.
+Create an API token in the Active24 account section, documentation on https://faq.active24.com/cz/790131-REST-API-rozhran%C3%AD.
 
+Set your API token:
+
+```
+export ACTIVE24_Token='xxx'
+```
+
+Now, let's issue a cert, set `dnssleep` for propagation new DNS record:
+```
+acme.sh --issue --dns dns_active24 -d example.com -d www.example.com --dnssleep 1000
+```
+
+The `ACTIVE24_Token` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 60. Use do.de API
+
+Create an API token in your do.de account.
+
+Set your API token:
+```
+export DO_LETOKEN='FmD408PdqT1E269gUK57'
+```
+
+To issue a certificate run:
+```
+acme.sh --issue --dns dns_doapi -d example.com -d *.example.com
+```
+
+The API token will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 61. Use internetbs.net API
+
+Create an API token in your internetbs.net account.
+
+Set your API token:
 ```
 export INTERNETBS_API_KEY="..."
 export INTERNETBS_API_PASSWORD="..."
 ```
 
-Ok, let's issue a cert now:
+To issue a certificate run:
 ```
 acme.sh --issue --dns dns_internetbs -d example.com -d www.example.com
 ```

+ 141 - 0
dnsapi/dns_active24.sh

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

+ 10 - 29
dnsapi/dns_cf.sh

@@ -19,8 +19,8 @@ dns_cf_add() {
   if [ -z "$CF_Key" ] || [ -z "$CF_Email" ]; then
     CF_Key=""
     CF_Email=""
-    _err "You didn't specify a cloudflare api key and email yet."
-    _err "Please create the key and try again."
+    _err "You didn't specify a Cloudflare api key and email yet."
+    _err "You can get yours from here https://dash.cloudflare.com/profile."
     return 1
   fi
 
@@ -34,9 +34,6 @@ 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"
@@ -100,21 +97,16 @@ dns_cf_rm() {
   if [ -z "$CF_Key" ] || [ -z "$CF_Email" ]; then
     CF_Key=""
     CF_Email=""
-    _err "You didn't specify a cloudflare api key and email yet."
-    _err "Please create the key and try again."
+    _err "You didn't specify a Cloudflare api key and email yet."
+    _err "You can get yours from here https://dash.cloudflare.com/profile."
     return 1
   fi
 
-  _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"
@@ -154,21 +146,6 @@ 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
@@ -180,8 +157,12 @@ _get_root() {
       return 1
     fi
 
-    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 ! _cf_rest GET "zones?name=$h"; then
+      return 1
+    fi
+
+    if _contains "$response" "\"name\":\"$h\"" >/dev/null; then
+      _domain_id=$(echo "$response" | _egrep_o "\[.\"id\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
       if [ "$_domain_id" ]; then
         _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
         _domain=$h

+ 89 - 64
dnsapi/dns_dgon.sh

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

+ 1 - 1
dnsapi/dns_dnsimple.sh

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

+ 59 - 0
dnsapi/dns_doapi.sh

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

+ 1 - 1
dnsapi/dns_dp.sh

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

+ 13 - 13
dnsapi/dns_dynu.sh

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

+ 57 - 5
dnsapi/dns_gandi_livedns.sh

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

+ 42 - 4
dnsapi/dns_hostingde.sh

@@ -59,9 +59,22 @@ _hostingde_getZoneConfig() {
     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)
+      zoneConfigName=$(echo "${curResult}" | _egrep_o '"name":.*' | cut -d ':' -f 2 | cut -d '"' -f 2)
+      zoneConfigType=$(echo "${curResult}" | grep -v "FindZoneConfigsResult" | _egrep_o '"type":.*' | cut -d ':' -f 2 | cut -d '"' -f 2)
+      zoneConfigExpire=$(echo "${curResult}" | _egrep_o '"expire":.*' | cut -d ':' -f 2 | cut -d '"' -f 2 | cut -d ',' -f 1)
+      zoneConfigNegativeTtl=$(echo "${curResult}" | _egrep_o '"negativeTtl":.*' | cut -d ':' -f 2 | cut -d '"' -f 2 | cut -d ',' -f 1)
+      zoneConfigRefresh=$(echo "${curResult}" | _egrep_o '"refresh":.*' | cut -d ':' -f 2 | cut -d '"' -f 2 | cut -d ',' -f 1)
+      zoneConfigRetry=$(echo "${curResult}" | _egrep_o '"retry":.*' | cut -d ':' -f 2 | cut -d '"' -f 2 | cut -d ',' -f 1)
+      zoneConfigTtl=$(echo "${curResult}" | _egrep_o '"ttl":.*' | cut -d ':' -f 2 | cut -d '"' -f 2 | cut -d ',' -f 1)
+      zoneConfigDnsServerGroupId=$(echo "${curResult}" | _egrep_o '"dnsServerGroupId":.*' | cut -d ':' -f 2 | cut -d '"' -f 2)
+      zoneConfigEmailAddress=$(echo "${curResult}" | _egrep_o '"emailAddress":.*' | cut -d ':' -f 2 | cut -d '"' -f 2)
+      zoneConfigDnsSecMode=$(echo "${curResult}" | _egrep_o '"dnsSecMode":.*' | cut -d ':' -f 2 | cut -d '"' -f 2)
+      if [ "${zoneConfigType}" != "NATIVE" ]; then
+        _err "Zone is not native"
+        returnCode=1
+        break
+      fi
       _debug "zoneConfigId '${zoneConfigId}'"
       returnCode=0
       break
@@ -74,9 +87,27 @@ _hostingde_getZoneConfig() {
   return $returnCode
 }
 
+_hostingde_getZoneStatus() {
+  _debug "Checking Zone status"
+  curData="{\"filter\":{\"field\":\"zoneConfigId\",\"value\":\"${zoneConfigId}\"},\"limit\":1,\"authToken\":\"${HOSTINGDE_APIKEY}\"}"
+  curResult="$(_post "${curData}" "${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zonesFind")"
+  _debug "Calling zonesFind '${curData}' '${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zonesFind'"
+  _debug "Result of zonesFind '$curResult'"
+  zoneStatus=$(echo "${curResult}" | grep -v success | _egrep_o '"status":.*' | cut -d ':' -f 2 | cut -d '"' -f 2)
+  _debug "zoneStatus '${zoneStatus}'"
+  return 0
+}
+
 _hostingde_addRecord() {
   _info "Adding record to zone"
-  curData="{\"authToken\":\"${HOSTINGDE_APIKEY}\",\"zoneConfig\":{\"id\":\"${zoneConfigId}\"},\"recordsToAdd\":[{\"name\":\"${fulldomain}\",\"type\":\"TXT\",\"content\":\"\\\"${txtvalue}\\\"\",\"ttl\":3600}]}"
+  _hostingde_getZoneStatus
+  _debug "Result of zoneStatus: '${zoneStatus}'"
+  while [ "${zoneStatus}" != "active" ]; do
+    _sleep 5
+    _hostingde_getZoneStatus
+    _debug "Result of zoneStatus: '${zoneStatus}'"
+  done
+  curData="{\"authToken\":\"${HOSTINGDE_APIKEY}\",\"zoneConfig\":{\"id\":\"${zoneConfigId}\",\"name\":\"${zoneConfigName}\",\"type\":\"${zoneConfigType}\",\"dnsServerGroupId\":\"${zoneConfigDnsServerGroupId}\",\"dnsSecMode\":\"${zoneConfigDnsSecMode}\",\"emailAddress\":\"${zoneConfigEmailAddress}\",\"soaValues\":{\"expire\":${zoneConfigExpire},\"negativeTtl\":${zoneConfigNegativeTtl},\"refresh\":${zoneConfigRefresh},\"retry\":${zoneConfigRetry},\"ttl\":${zoneConfigTtl}}},\"recordsToAdd\":[{\"name\":\"${fulldomain}\",\"type\":\"TXT\",\"content\":\"\\\"${txtvalue}\\\"\",\"ttl\":3600}]}"
   curResult="$(_post "${curData}" "${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneUpdate")"
   _debug "Calling zoneUpdate: '${curData}' '${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneUpdate'"
   _debug "Result of zoneUpdate: '$curResult'"
@@ -93,7 +124,14 @@ _hostingde_addRecord() {
 
 _hostingde_removeRecord() {
   _info "Removing record from zone"
-  curData="{\"authToken\":\"${HOSTINGDE_APIKEY}\",\"zoneConfig\":{\"id\":\"${zoneConfigId}\"},\"recordsToDelete\":[{\"name\":\"${fulldomain}\",\"type\":\"TXT\",\"content\":\"\\\"${txtvalue}\\\"\"}]}"
+  _hostingde_getZoneStatus
+  _debug "Result of zoneStatus: '$zoneStatus'"
+  while [ "$zoneStatus" != "active" ]; do
+    _sleep 5
+    _hostingde_getZoneStatus
+    _debug "Result of zoneStatus: '$zoneStatus'"
+  done
+  curData="{\"authToken\":\"${HOSTINGDE_APIKEY}\",\"zoneConfig\":{\"id\":\"${zoneConfigId}\",\"name\":\"${zoneConfigName}\",\"type\":\"${zoneConfigType}\",\"dnsServerGroupId\":\"${zoneConfigDnsServerGroupId}\",\"dnsSecMode\":\"${zoneConfigDnsSecMode}\",\"emailAddress\":\"${zoneConfigEmailAddress}\",\"soaValues\":{\"expire\":${zoneConfigExpire},\"negativeTtl\":${zoneConfigNegativeTtl},\"refresh\":${zoneConfigRefresh},\"retry\":${zoneConfigRetry},\"ttl\":${zoneConfigTtl}}},\"recordsToDelete\":[{\"name\":\"${fulldomain}\",\"type\":\"TXT\",\"content\":\"\\\"${txtvalue}\\\"\"}]}"
   curResult="$(_post "${curData}" "${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneUpdate")"
   _debug "Calling zoneUpdate: '${curData}' '${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneUpdate'"
   _debug "Result of zoneUpdate: '$curResult'"

+ 2 - 1
dnsapi/dns_inwx.sh

@@ -158,7 +158,8 @@ _inwx_login() {
   export _H1
 
   #https://github.com/inwx/php-client/blob/master/INWX/Domrobot.php#L71
-  if _contains "$response" "tfa"; then
+  if _contains "$response" "<member><name>code</name><value><int>1000</int></value></member>" \
+    && _contains "$response" "<member><name>tfa</name><value><string>GOOGLE-AUTH</string></value></member>"; then
     if [ -z "$INWX_Shared_Secret" ]; then
       _err "Mobile TAN detected."
       _err "Please define a shared secret."

+ 2 - 2
dnsapi/dns_linode_v4.sh

@@ -8,7 +8,7 @@ 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() {
+dns_linode_v4_add() {
   fulldomain="${1}"
   txtvalue="${2}"
 
@@ -51,7 +51,7 @@ dns_linode_add() {
 }
 
 #Usage: dns_linode_rm   _acme-challenge.www.domain.com
-dns_linode_rm() {
+dns_linode_v4_rm() {
   fulldomain="${1}"
 
   if ! _Linode_API; then

+ 68 - 9
dnsapi/dns_loopia.sh

@@ -38,8 +38,8 @@ dns_loopia_add() {
 
   _info "Adding record"
 
-  _loopia_add_record "$_domain" "$_sub_domain"
-  _loopia_update_record "$_domain" "$_sub_domain" "$txtvalue"
+  _loopia_add_sub_domain "$_domain" "$_sub_domain"
+  _loopia_add_record "$_domain" "$_sub_domain" "$txtvalue"
 
 }
 
@@ -96,6 +96,37 @@ dns_loopia_rm() {
 
 ####################  Private functions below ##################################
 
+_loopia_get_records() {
+  domain=$1
+  sub_domain=$2
+
+  xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
+  <methodCall>
+    <methodName>getZoneRecords</methodName>
+    <params>
+      <param>
+        <value><string>%s</string></value>
+      </param>
+      <param>
+        <value><string>%s</string></value>
+      </param>
+      <param>
+        <value><string>%s</string></value>
+      </param>
+      <param>
+        <value><string>%s</string></value>
+      </param>
+    </params>
+  </methodCall>' $LOOPIA_User $LOOPIA_Password "$domain" "$sub_domain")
+
+  response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")"
+  if ! _contains "$response" "<array>"; then
+    _err "Error"
+    return 1
+  fi
+  return 0
+}
+
 _get_root() {
   domain=$1
   _debug "get root"
@@ -137,14 +168,14 @@ _get_root() {
 
 }
 
-_loopia_update_record() {
+_loopia_add_record() {
   domain=$1
   sub_domain=$2
   txtval=$3
 
   xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
   <methodCall>
-    <methodName>updateZoneRecord</methodName>
+    <methodName>addZoneRecord</methodName>
     <params>
       <param>
         <value><string>%s</string></value>
@@ -176,10 +207,6 @@ _loopia_update_record() {
             <name>rdata</name>
             <value><string>%s</string></value>
           </member>
-          <member>
-            <name>record_id</name>
-            <value><int>0</int></value>
-          </member>
         </struct>
       </param>
     </params>
@@ -194,10 +221,42 @@ _loopia_update_record() {
   return 0
 }
 
-_loopia_add_record() {
+_sub_domain_exists() {
   domain=$1
   sub_domain=$2
 
+  xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
+  <methodCall>
+    <methodName>getSubdomains</methodName>
+    <params>
+      <param>
+        <value><string>%s</string></value>
+      </param>
+      <param>
+        <value><string>%s</string></value>
+      </param>
+      <param>
+        <value><string>%s</string></value>
+      </param>
+    </params>
+  </methodCall>' $LOOPIA_User $LOOPIA_Password "$domain")
+
+  response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")"
+
+  if _contains "$response" "$sub_domain"; then
+    return 0
+  fi
+  return 1
+}
+
+_loopia_add_sub_domain() {
+  domain=$1
+  sub_domain=$2
+
+  if _sub_domain_exists "$domain" "$sub_domain"; then
+    return 0
+  fi
+
   xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
   <methodCall>
     <methodName>addSubdomain</methodName>

+ 75 - 21
dnsapi/dns_namecheap.sh

@@ -3,16 +3,15 @@
 # Namecheap API
 # https://www.namecheap.com/support/api/intro.aspx
 #
-# Requires Namecheap API key set in NAMECHEAP_API_KEY, NAMECHEAP_SOURCEIP and NAMECHEAP_USERNAME set as environment variable
+# Requires Namecheap API key set in 
+#NAMECHEAP_API_KEY, 
+#NAMECHEAP_USERNAME,
+#NAMECHEAP_SOURCEIP 
 # Due to Namecheap's API limitation all the records of your domain will be read and re applied, make sure to have a backup of your records you could apply if any issue would arise.
 
 ########  Public functions #####################
 
-if [ "$STAGE" -eq 1 ]; then
-  NAMECHEAP_API="https://api.sandbox.namecheap.com/xml.response"
-else
-  NAMECHEAP_API="https://api.namecheap.com/xml.response"
-fi
+NAMECHEAP_API="https://api.namecheap.com/xml.response"
 
 #Usage: dns_namecheap_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
 dns_namecheap_add() {
@@ -144,7 +143,7 @@ _namecheap_set_publicip() {
 _namecheap_post() {
   command=$1
   data="ApiUser=${NAMECHEAP_USERNAME}&ApiKey=${NAMECHEAP_API_KEY}&ClientIp=${_publicip}&UserName=${NAMECHEAP_USERNAME}&Command=${command}"
-
+  _debug2 "_namecheap_post data" "$data"
   response="$(_post "$data" "$NAMECHEAP_API" "" "POST")"
   _debug2 response "$response"
 
@@ -161,12 +160,12 @@ _namecheap_parse_host() {
   _host=$1
   _debug _host "$_host"
 
-  _hostid=$(echo "$_host" | _egrep_o '\sHostId="[^"]*' | cut -d '"' -f 2)
-  _hostname=$(echo "$_host" | _egrep_o '\sName="[^"]*' | cut -d '"' -f 2)
-  _hosttype=$(echo "$_host" | _egrep_o '\sType="[^"]*' | cut -d '"' -f 2)
-  _hostaddress=$(echo "$_host" | _egrep_o '\sAddress="[^"]*' | cut -d '"' -f 2)
-  _hostmxpref=$(echo "$_host" | _egrep_o '\sMXPref="[^"]*' | cut -d '"' -f 2)
-  _hostttl=$(echo "$_host" | _egrep_o '\sTTL="[^"]*' | cut -d '"' -f 2)
+  _hostid=$(echo "$_host" | _egrep_o ' HostId="[^"]*' | cut -d '"' -f 2)
+  _hostname=$(echo "$_host" | _egrep_o ' Name="[^"]*' | cut -d '"' -f 2)
+  _hosttype=$(echo "$_host" | _egrep_o ' Type="[^"]*' | cut -d '"' -f 2)
+  _hostaddress=$(echo "$_host" | _egrep_o ' Address="[^"]*' | cut -d '"' -f 2)
+  _hostmxpref=$(echo "$_host" | _egrep_o ' MXPref="[^"]*' | cut -d '"' -f 2)
+  _hostttl=$(echo "$_host" | _egrep_o ' TTL="[^"]*' | cut -d '"' -f 2)
 
   _debug hostid "$_hostid"
   _debug hostname "$_hostname"
@@ -199,9 +198,12 @@ _namecheap_check_config() {
 _set_namecheap_TXT() {
   subdomain=$2
   txt=$3
-  tld=$(echo "$1" | cut -d '.' -f 2)
-  sld=$(echo "$1" | cut -d '.' -f 1)
-  request="namecheap.domains.dns.getHosts&SLD=$sld&TLD=$tld"
+
+  if ! _namecheap_set_tld_sld "$1"; then
+    return 1
+  fi
+
+  request="namecheap.domains.dns.getHosts&SLD=${_sld}&TLD=${_tld}"
 
   if ! _namecheap_post "$request"; then
     _err "$error"
@@ -221,6 +223,12 @@ _set_namecheap_TXT() {
   while read -r host; do
     if _contains "$host" "<host"; then
       _namecheap_parse_host "$host"
+      _debug2 _hostname "_hostname"
+      _debug2 _hosttype "_hosttype"
+      _debug2 _hostaddress "_hostaddress"
+      _debug2 _hostmxpref "_hostmxpref"
+      _hostaddress="$(printf "%s" "$_hostaddress" | _url_encode)"
+      _debug2 "encoded _hostaddress" "_hostaddress"
       _namecheap_add_host "$_hostname" "$_hosttype" "$_hostaddress" "$_hostmxpref" "$_hostttl"
     fi
   done <<EOT
@@ -231,7 +239,7 @@ EOT
 
   _debug hostrequestfinal "$_hostrequest"
 
-  request="namecheap.domains.dns.setHosts&SLD=${sld}&TLD=${tld}${_hostrequest}"
+  request="namecheap.domains.dns.setHosts&SLD=${_sld}&TLD=${_tld}${_hostrequest}"
 
   if ! _namecheap_post "$request"; then
     _err "$error"
@@ -244,9 +252,12 @@ EOT
 _del_namecheap_TXT() {
   subdomain=$2
   txt=$3
-  tld=$(echo "$1" | cut -d '.' -f 2)
-  sld=$(echo "$1" | cut -d '.' -f 1)
-  request="namecheap.domains.dns.getHosts&SLD=$sld&TLD=$tld"
+
+  if ! _namecheap_set_tld_sld "$1"; then
+    return 1
+  fi
+
+  request="namecheap.domains.dns.getHosts&SLD=${_sld}&TLD=${_tld}"
 
   if ! _namecheap_post "$request"; then
     _err "$error"
@@ -272,6 +283,7 @@ _del_namecheap_TXT() {
         _debug "TXT entry found"
         found=1
       else
+        _hostaddress="$(printf "%s" "$_hostaddress" | _url_encode)"
         _namecheap_add_host "$_hostname" "$_hosttype" "$_hostaddress" "$_hostmxpref" "$_hostttl"
       fi
     fi
@@ -286,7 +298,7 @@ EOT
 
   _debug hostrequestfinal "$_hostrequest"
 
-  request="namecheap.domains.dns.setHosts&SLD=${sld}&TLD=${tld}${_hostrequest}"
+  request="namecheap.domains.dns.setHosts&SLD=${_sld}&TLD=${_tld}${_hostrequest}"
 
   if ! _namecheap_post "$request"; then
     _err "$error"
@@ -306,3 +318,45 @@ _namecheap_add_host() {
   _hostindex=$(_math "$_hostindex" + 1)
   _hostrequest=$(printf '%s&HostName%d=%s&RecordType%d=%s&Address%d=%s&MXPref%d=%d&TTL%d=%d' "$_hostrequest" "$_hostindex" "$1" "$_hostindex" "$2" "$_hostindex" "$3" "$_hostindex" "$4" "$_hostindex" "$5")
 }
+
+_namecheap_set_tld_sld() {
+  domain=$1
+  _tld=""
+  _sld=""
+
+  i=2
+
+  while true; do
+
+    _tld=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    _debug tld "$_tld"
+
+    if [ -z "$_tld" ]; then
+      _debug "invalid tld"
+      return 1
+    fi
+
+    j=$(_math "$i" - 1)
+
+    _sld=$(printf "%s" "$domain" | cut -d . -f 1-"$j")
+    _debug sld "$_sld"
+
+    if [ -z "$_sld" ]; then
+      _debug "invalid sld"
+      return 1
+    fi
+
+    request="namecheap.domains.dns.getHosts&SLD=$_sld&TLD=$_tld"
+
+    if ! _namecheap_post "$request"; then
+      _debug "sld($_sld)/tld($_tld) not found"
+    else
+      _debug "sld($_sld)/tld($_tld) found"
+      return 0
+    fi
+
+    i=$(_math "$i" + 1)
+
+  done
+
+}

+ 164 - 0
dnsapi/dns_pointhq.sh

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