Browse Source

Merge branch 'dev' into dev

linux-insideDE 7 years ago
parent
commit
dc267663a7
15 changed files with 915 additions and 103 deletions
  1. 4 1
      README.md
  2. 22 22
      acme.sh
  3. 20 0
      deploy/README.md
  4. 8 10
      deploy/cpanel_uapi.sh
  5. 34 2
      deploy/haproxy.sh
  6. 3 3
      deploy/ssh.sh
  7. 63 2
      dnsapi/README.md
  8. 7 7
      dnsapi/dns_aws.sh
  9. 161 0
      dnsapi/dns_dpi.sh
  10. 358 0
      dnsapi/dns_euserv.sh
  11. 167 0
      dnsapi/dns_gcloud.sh
  12. 4 0
      dnsapi/dns_gd.sh
  13. 2 2
      dnsapi/dns_ispconfig.sh
  14. 40 15
      dnsapi/dns_lexicon.sh
  15. 22 39
      dnsapi/dns_unoeuro.sh

+ 4 - 1
README.md

@@ -33,7 +33,7 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
 - [webfaction](https://community.webfaction.com/questions/19988/using-letsencrypt)
 - [Loadbalancer.org](https://www.loadbalancer.org/blog/loadbalancer-org-with-lets-encrypt-quick-and-dirty)
 - [discourse.org](https://meta.discourse.org/t/setting-up-lets-encrypt/40709)
-- [Centminmod](http://centminmod.com/letsencrypt-acmetool-https.html)
+- [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/)
 - [opnsense.org](https://github.com/opnsense/plugins/tree/master/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient)
@@ -320,6 +320,9 @@ You don't have to do anything manually!
 1. Loopia.se API
 1. acme-dns (https://github.com/joohoi/acme-dns)
 1. TELE3 (https://www.tele3.cz)
+1. EUSERV.EU (https://www.euserv.eu)
+1. DNSPod.com API (https://www.dnspod.com)
+1. Google Cloud DNS API
 1. netcup DNS API (https://www.netcup.de)
 
 And: 

+ 22 - 22
acme.sh

@@ -1,6 +1,6 @@
 #!/usr/bin/env sh
 
-VER=2.7.9
+VER=2.8.0
 
 PROJECT_NAME="acme.sh"
 
@@ -1327,6 +1327,7 @@ createDomainKey() {
     if _createkey "$_cdl" "$CERT_KEY_PATH"; then
       _savedomainconf Le_Keylength "$_cdl"
       _info "The domain key is here: $(__green $CERT_KEY_PATH)"
+      return 0
     fi
   else
     if [ "$IS_RENEW" ]; then
@@ -1374,17 +1375,17 @@ _url_replace() {
 }
 
 _time2str() {
-  #Linux
-  if date -u -d@"$1" 2>/dev/null; then
+  #BSD
+  if date -u -r "$1" 2>/dev/null; then
     return
   fi
 
-  #BSD
-  if date -u -r "$1" 2>/dev/null; then
+  #Linux
+  if date -u -d@"$1" 2>/dev/null; then
     return
   fi
 
-  #Soaris
+  #Solaris
   if _exists adb; then
     _t_s_a=$(echo "0t${1}=Y" | adb)
     echo "$_t_s_a"
@@ -1607,7 +1608,7 @@ _inithttp() {
 
 }
 
-# body  url [needbase64] [POST|PUT] [ContentType]
+# body  url [needbase64] [POST|PUT|DELETE] [ContentType]
 _post() {
   body="$1"
   _post_url="$2"
@@ -1795,15 +1796,13 @@ _send_signed_request() {
     return 1
   fi
 
-  if [ "$ACME_VERSION" = "2" ]; then
-    __request_conent_type="$CONTENT_TYPE_JSON"
-  else
-    __request_conent_type=""
-  fi
+  __request_conent_type="$CONTENT_TYPE_JSON"
+
   payload64=$(printf "%s" "$payload" | _base64 | _url_replace)
   _debug3 payload64 "$payload64"
 
-  MAX_REQUEST_RETRY_TIMES=5
+  MAX_REQUEST_RETRY_TIMES=20
+  _sleep_retry_sec=1
   _request_retry_times=0
   while [ "${_request_retry_times}" -lt "$MAX_REQUEST_RETRY_TIMES" ]; do
     _request_retry_times=$(_math "$_request_retry_times" + 1)
@@ -1897,9 +1896,10 @@ _send_signed_request() {
       _debug3 _body "$_body"
     fi
 
-    if _contains "$_body" "JWS has invalid anti-replay nonce"; then
-      _info "It seems the CA server is busy now, let's wait and retry."
-      _sleep 5
+    if _contains "$_body" "JWS has invalid anti-replay nonce" || _contains "$_body" "JWS has an invalid anti-replay nonce"; then
+      _info "It seems the CA server is busy now, let's wait and retry. Sleeping $_sleep_retry_sec seconds."
+      _CACHED_NONCE=""
+      _sleep $_sleep_retry_sec
       continue
     fi
     break
@@ -4676,19 +4676,19 @@ _installcert() {
     if [ -f "$_real_cert" ] && [ ! "$IS_RENEW" ]; then
       cp "$_real_cert" "$_backup_path/cert.bak"
     fi
-    cat "$CERT_PATH" >"$_real_cert"
+    cat "$CERT_PATH" >"$_real_cert" || return 1
   fi
 
   if [ "$_real_ca" ]; then
     _info "Installing CA to:$_real_ca"
     if [ "$_real_ca" = "$_real_cert" ]; then
       echo "" >>"$_real_ca"
-      cat "$CA_CERT_PATH" >>"$_real_ca"
+      cat "$CA_CERT_PATH" >>"$_real_ca" || return 1
     else
       if [ -f "$_real_ca" ] && [ ! "$IS_RENEW" ]; then
         cp "$_real_ca" "$_backup_path/ca.bak"
       fi
-      cat "$CA_CERT_PATH" >"$_real_ca"
+      cat "$CA_CERT_PATH" >"$_real_ca" || return 1
     fi
   fi
 
@@ -4698,9 +4698,9 @@ _installcert() {
       cp "$_real_key" "$_backup_path/key.bak"
     fi
     if [ -f "$_real_key" ]; then
-      cat "$CERT_KEY_PATH" >"$_real_key"
+      cat "$CERT_KEY_PATH" >"$_real_key" || return 1
     else
-      cat "$CERT_KEY_PATH" >"$_real_key"
+      cat "$CERT_KEY_PATH" >"$_real_key" || return 1
       chmod 600 "$_real_key"
     fi
   fi
@@ -4710,7 +4710,7 @@ _installcert() {
     if [ -f "$_real_fullchain" ] && [ ! "$IS_RENEW" ]; then
       cp "$_real_fullchain" "$_backup_path/fullchain.bak"
     fi
-    cat "$CERT_FULLCHAIN_PATH" >"$_real_fullchain"
+    cat "$CERT_FULLCHAIN_PATH" >"$_real_fullchain" || return 1
   fi
 
   if [ "$_reload_cmd" ]; then

+ 20 - 0
deploy/README.md

@@ -255,3 +255,23 @@ acme.sh --deploy -d fritzbox.example.com --deploy-hook fritzbox
 ```sh
 acme.sh --deploy -d ftp.example.com --deploy-hook strongswan
 ```
+
+## 10. Deploy the cert to HAProxy
+
+You must specify the path where you want the concatenated key and certificate chain written.
+```sh
+export DEPLOY_HAPROXY_PEM_PATH=/etc/haproxy
+```
+
+You may optionally define the command to reload HAProxy. The value shown below will be used as the default if you don't set this environment variable.
+
+```sh
+export DEPLOY_HAPROXY_RELOAD="/usr/sbin/service haproxy restart"
+```
+
+You can then deploy the certificate as follows
+```sh
+acme.sh --deploy -d haproxy.example.com --deploy-hook haproxy
+```
+
+The path for the PEM file will be stored with the domain configuration and will be available when renewing, so that deploy will happen automatically when renewed.

+ 8 - 10
deploy/cpanel_uapi.sh

@@ -2,8 +2,12 @@
 # Here is the script to deploy the cert to your cpanel using the cpanel API.
 # Uses command line uapi.  --user option is needed only if run as root.
 # Returns 0 when success.
-# Written by Santeri Kannisto <santeri.kannisto@2globalnomads.info>
-# Public domain, 2017
+#
+# Please note that I am no longer using Github. If you want to report an issue
+# or contact me, visit https://forum.webseodesigners.com/web-design-seo-and-hosting-f16/
+#
+# Written by Santeri Kannisto <santeri.kannisto@webseodesigners.com>
+# Public domain, 2017-2018
 
 #export DEPLOY_CPANEL_USER=myusername
 
@@ -28,15 +32,9 @@ cpanel_uapi_deploy() {
     _err "The command uapi is not found."
     return 1
   fi
-  if ! _exists php; then
-    _err "The command php is not found."
-    return 1
-  fi
   # read cert and key files and urlencode both
-  _certstr=$(cat "$_ccert")
-  _keystr=$(cat "$_ckey")
-  _cert=$(php -r "echo urlencode(\"$_certstr\");")
-  _key=$(php -r "echo urlencode(\"$_keystr\");")
+  _cert=$(_url_encode <"$_ccert")
+  _key=$(_url_encode <"$_ckey")
 
   _debug _cert "$_cert"
   _debug _key "$_key"

+ 34 - 2
deploy/haproxy.sh

@@ -20,7 +20,39 @@ haproxy_deploy() {
   _debug _cca "$_cca"
   _debug _cfullchain "$_cfullchain"
 
-  _err "deploy cert to haproxy server, Not implemented yet"
-  return 1
+  # handle reload preference
+  DEFAULT_HAPROXY_RELOAD="/usr/sbin/service haproxy restart"
+  if [ -z "${DEPLOY_HAPROXY_RELOAD}" ]; then
+    _reload="${DEFAULT_HAPROXY_RELOAD}"
+    _cleardomainconf DEPLOY_HAPROXY_RELOAD
+  else
+    _reload="${DEPLOY_HAPROXY_RELOAD}"
+    _savedomainconf DEPLOY_HAPROXY_RELOAD "$DEPLOY_HAPROXY_RELOAD"
+  fi
+  _savedomainconf DEPLOY_HAPROXY_PEM_PATH "$DEPLOY_HAPROXY_PEM_PATH"
+
+  # work out the path where the PEM file should go
+  _pem_path="${DEPLOY_HAPROXY_PEM_PATH}"
+  if [ -z "$_pem_path" ]; then
+    _err "Path to save PEM file not found. Please define DEPLOY_HAPROXY_PEM_PATH."
+    return 1
+  fi
+  _pem_full_path="$_pem_path/$_cdomain.pem"
+  _info "Full path to PEM $_pem_full_path"
+
+  # combine the key and fullchain into a single pem and install
+  cat "$_cfullchain" "$_ckey" >"$_pem_full_path"
+  chmod 600 "$_pem_full_path"
+  _info "Certificate successfully deployed"
+
+  # restart HAProxy
+  _info "Run reload: $_reload"
+  if eval "$_reload"; then
+    _info "Reload success!"
+    return 0
+  else
+    _err "Reload error"
+    return 1
+  fi
 
 }

+ 3 - 3
deploy/ssh.sh

@@ -11,7 +11,7 @@
 #
 # Only a username is required.  All others are optional.
 #
-# The following examples are for QNAP NAS running QTS 4.2 
+# The following examples are for QNAP NAS running QTS 4.2
 # export DEPLOY_SSH_CMD=""  # defaults to ssh
 # export DEPLOY_SSH_USER="admin"  # required
 # export DEPLOY_SSH_SERVER="qnap"  # defaults to domain name
@@ -101,7 +101,7 @@ ssh_deploy() {
   fi
 
   # CERTFILE is optional.
-  # If provided then private key will be copied or appended to provided filename.
+  # If provided then certificate will be copied or appended to provided filename.
   if [ -n "$DEPLOY_SSH_CERTFILE" ]; then
     Le_Deploy_ssh_certfile="$DEPLOY_SSH_CERTFILE"
     _savedomainconf Le_Deploy_ssh_certfile "$Le_Deploy_ssh_certfile"
@@ -190,7 +190,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
     _info "Backup directories erased after 180 days."
   fi
 
-  _debug "Remote commands to execute: $_cmdstr"
+  _secure_debug "Remote commands to execute: " "$_cmdstr"
   _info "Submitting sequence of commands to remote server by ssh"
   # quotations in bash cmd below intended.  Squash travis spellcheck error
   # shellcheck disable=SC2029

+ 63 - 2
dnsapi/README.md

@@ -876,9 +876,69 @@ acme.sh --issue --dns dns_tele3 -d example.com -d *.example.com
 ```
 
 The TELE3_Key and TELE3_Secret will be saved in ~/.acme.sh/account.conf and will be reused when needed.
-## 47. Use netcup DNS API to automatically issue cert
 
-First you need to login to your CCP account to get your API Key and API Password.
+## 47. Use Euserv.eu API
+
+First you need to login to your euserv.eu account and activate your API Administration (API Verwaltung).
+[https://support.euserv.com](https://support.euserv.com)
+
+Once you've activate, login to your API Admin Interface and create an API account.
+Please specify the scope (active groups: domain) and assign the allowed IPs.
+
+```
+export EUSERV_Username="99999.user123"
+export EUSERV_Password="Asbe54gHde"
+```
+
+Ok, let's issue a cert now: (Be aware to use the `--insecure` flag, cause euserv.eu is still using self-signed certificates!)
+```
+acme.sh --issue --dns dns_euserv -d example.com -d *.example.com --insecure
+```
+
+The `EUSERV_Username` and `EUSERV_Password` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+Please report any issues to https://github.com/initit/acme.sh or to <github@initit.de>
+
+## 48. Use DNSPod.com domain API to automatically issue cert
+
+First you need to get your API Key and ID by this [get-the-user-token](https://www.dnspod.com/docs/info.html#get-the-user-token).
+
+```
+export DPI_Id="1234"
+export DPI_Key="sADDsdasdgdsf"
+```
+
+Ok, let's issue a cert now:
+```
+acme.sh --issue --dns dns_dpi -d example.com -d www.example.com
+```
+
+The `DPI_Id` and `DPI_Key` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
+## 49. Use Google Cloud DNS API to automatically issue cert
+
+First you need to authenticate to gcloud.
+
+```
+gcloud init
+```
+
+**The `dns_gcloud` script uses the active gcloud configuration and credentials.**
+There is no logic inside `dns_gcloud` to override the project and other settings.
+If needed, create additional [gcloud configurations](https://cloud.google.com/sdk/gcloud/reference/topic/configurations).
+You can change the configuration being used without *activating* it; simply set the `CLOUDSDK_ACTIVE_CONFIG_NAME` environment variable.
+
+To issue a certificate you can:
+```
+export CLOUDSDK_ACTIVE_CONFIG_NAME=default  # see the note above
+acme.sh --issue --dns dns_gcloud -d example.com -d '*.example.com'
+```
+
+`dns_gcloud` also supports [DNS alias mode](https://github.com/Neilpang/acme.sh/wiki/DNS-alias-mode).
+
+## 50. Use netcup DNS API to automatically issue cert
+
+First you need to login in your CCP account to get your API Key and API Password.
 ```
 export NC_Apikey="<Apikey>"
 export NC_Apipw="<Apipassword>"
@@ -891,6 +951,7 @@ acme.sh --issue --dns dns_netcup -d example.com -d www.example.com
 ```
 
 The `NC_Apikey`,`NC_Apipw` and `NC_CID` will be saved in `~/.acme.sh/account.conf` and will be reused when needed.
+
 # Use custom API
 
 If your API is not supported yet, you can write your own DNS API.

+ 7 - 7
dnsapi/dns_aws.sh

@@ -29,7 +29,7 @@ dns_aws_add() {
   if [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ]; then
     AWS_ACCESS_KEY_ID=""
     AWS_SECRET_ACCESS_KEY=""
-    _err "You don't specify aws route53 api key id and and api key secret yet."
+    _err "You haven't specifed the aws route53 api key id and and api key secret yet."
     _err "Please create your key and try again. see $(__green $AWS_WIKI)"
     return 1
   fi
@@ -62,7 +62,7 @@ dns_aws_add() {
   fi
 
   if [ "$_resource_record" ] && _contains "$response" "$txtvalue"; then
-    _info "The txt record already exists, skip"
+    _info "The TXT record already exists. Skipping."
     return 0
   fi
 
@@ -71,7 +71,7 @@ dns_aws_add() {
   _aws_tmpl_xml="<ChangeResourceRecordSetsRequest xmlns=\"https://route53.amazonaws.com/doc/2013-04-01/\"><ChangeBatch><Changes><Change><Action>UPSERT</Action><ResourceRecordSet><Name>$fulldomain</Name><Type>TXT</Type><TTL>300</TTL><ResourceRecords>$_resource_record<ResourceRecord><Value>\"$txtvalue\"</Value></ResourceRecord></ResourceRecords></ResourceRecordSet></Change></Changes></ChangeBatch></ChangeResourceRecordSetsRequest>"
 
   if aws_rest POST "2013-04-01$_domain_id/rrset/" "" "$_aws_tmpl_xml" && _contains "$response" "ChangeResourceRecordSetsResponse"; then
-    _info "txt record updated success."
+    _info "TXT record updated successfully."
     return 0
   fi
 
@@ -99,7 +99,7 @@ dns_aws_rm() {
   _debug _sub_domain "$_sub_domain"
   _debug _domain "$_domain"
 
-  _info "Geting existing records for $fulldomain"
+  _info "Getting existing records for $fulldomain"
   if ! aws_rest GET "2013-04-01$_domain_id/rrset" "name=$fulldomain&type=TXT"; then
     return 1
   fi
@@ -108,14 +108,14 @@ dns_aws_rm() {
     _resource_record="$(echo "$response" | sed 's/<ResourceRecordSet>/"/g' | tr '"' "\n" | grep "<Name>$fulldomain.</Name>" | _egrep_o "<ResourceRecords.*</ResourceRecords>" | sed "s/<ResourceRecords>//" | sed "s#</ResourceRecords>##")"
     _debug "_resource_record" "$_resource_record"
   else
-    _debug "no records exists, skip"
+    _debug "no records exist, skip"
     return 0
   fi
 
   _aws_tmpl_xml="<ChangeResourceRecordSetsRequest xmlns=\"https://route53.amazonaws.com/doc/2013-04-01/\"><ChangeBatch><Changes><Change><Action>DELETE</Action><ResourceRecordSet><ResourceRecords>$_resource_record</ResourceRecords><Name>$fulldomain.</Name><Type>TXT</Type><TTL>300</TTL></ResourceRecordSet></Change></Changes></ChangeBatch></ChangeResourceRecordSetsRequest>"
 
   if aws_rest POST "2013-04-01$_domain_id/rrset/" "" "$_aws_tmpl_xml" && _contains "$response" "ChangeResourceRecordSetsResponse"; then
-    _info "txt record deleted success."
+    _info "TXT record deleted successfully."
     return 0
   fi
 
@@ -163,7 +163,7 @@ _get_root() {
             _domain=$h
             return 0
           fi
-          _err "Can not find domain id: $h"
+          _err "Can't find domain with id: $h"
           return 1
         fi
       fi

+ 161 - 0
dnsapi/dns_dpi.sh

@@ -0,0 +1,161 @@
+#!/usr/bin/env sh
+
+# Dnspod.com Domain api
+#
+#DPI_Id="1234"
+#
+#DPI_Key="sADDsdasdgdsf"
+
+REST_API="https://api.dnspod.com"
+
+########  Public functions #####################
+
+#Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_dpi_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  DPI_Id="${DPI_Id:-$(_readaccountconf_mutable DPI_Id)}"
+  DPI_Key="${DPI_Key:-$(_readaccountconf_mutable DPI_Key)}"
+  if [ -z "$DPI_Id" ] || [ -z "$DPI_Key" ]; then
+    DPI_Id=""
+    DPI_Key=""
+    _err "You don't specify dnspod api key and key id yet."
+    _err "Please create you key and try again."
+    return 1
+  fi
+
+  #save the api key and email to the account conf file.
+  _saveaccountconf_mutable DPI_Id "$DPI_Id"
+  _saveaccountconf_mutable DPI_Key "$DPI_Key"
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+
+  add_record "$_domain" "$_sub_domain" "$txtvalue"
+
+}
+
+#fulldomain txtvalue
+dns_dpi_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  DPI_Id="${DPI_Id:-$(_readaccountconf_mutable DPI_Id)}"
+  DPI_Key="${DPI_Key:-$(_readaccountconf_mutable DPI_Key)}"
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+
+  if ! _rest POST "Record.List" "user_token=$DPI_Id,$DPI_Key&format=json&domain_id=$_domain_id&sub_domain=$_sub_domain"; then
+    _err "Record.Lis error."
+    return 1
+  fi
+
+  if _contains "$response" 'No records'; then
+    _info "Don't need to remove."
+    return 0
+  fi
+
+  record_id=$(echo "$response" | _egrep_o '{[^{]*"value":"'"$txtvalue"'"' | cut -d , -f 1 | cut -d : -f 2 | tr -d \")
+  _debug record_id "$record_id"
+  if [ -z "$record_id" ]; then
+    _err "Can not get record id."
+    return 1
+  fi
+
+  if ! _rest POST "Record.Remove" "user_token=$DPI_Id,$DPI_Key&format=json&domain_id=$_domain_id&record_id=$record_id"; then
+    _err "Record.Remove error."
+    return 1
+  fi
+
+  _contains "$response" "Action completed successful"
+
+}
+
+#add the txt record.
+#usage: root  sub  txtvalue
+add_record() {
+  root=$1
+  sub=$2
+  txtvalue=$3
+  fulldomain="$sub.$root"
+
+  _info "Adding record"
+
+  if ! _rest POST "Record.Create" "user_token=$DPI_Id,$DPI_Key&format=json&domain_id=$_domain_id&sub_domain=$_sub_domain&record_type=TXT&value=$txtvalue&record_line=default"; then
+    return 1
+  fi
+
+  _contains "$response" "Action completed successful" || _contains "$response" "Domain record already exists"
+}
+
+####################  Private functions below ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+# _domain_id=sdjkglgdfewsdfg
+_get_root() {
+  domain=$1
+  i=2
+  p=1
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    if [ -z "$h" ]; then
+      #not valid
+      return 1
+    fi
+
+    if ! _rest POST "Domain.Info" "user_token=$DPI_Id,$DPI_Key&format=json&domain=$h"; then
+      return 1
+    fi
+
+    if _contains "$response" "Action completed successful"; then
+      _domain_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")
+      _debug _domain_id "$_domain_id"
+      if [ "$_domain_id" ]; then
+        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+        _debug _sub_domain "$_sub_domain"
+        _domain="$h"
+        _debug _domain "$_domain"
+        return 0
+      fi
+      return 1
+    fi
+    p="$i"
+    i=$(_math "$i" + 1)
+  done
+  return 1
+}
+
+#Usage: method  URI  data
+_rest() {
+  m="$1"
+  ep="$2"
+  data="$3"
+  _debug "$ep"
+  url="$REST_API/$ep"
+
+  _debug url "$url"
+
+  if [ "$m" = "GET" ]; then
+    response="$(_get "$url" | tr -d '\r')"
+  else
+    _debug2 data "$data"
+    response="$(_post "$data" "$url" | tr -d '\r')"
+  fi
+
+  if [ "$?" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}

+ 358 - 0
dnsapi/dns_euserv.sh

@@ -0,0 +1,358 @@
+#!/usr/bin/env sh
+
+#This is the euserv.eu api wrapper for acme.sh
+#
+#Author: Michael Brueckner
+#Report Bugs: https://www.github.com/initit/acme.sh  or  mbr@initit.de
+
+#
+#EUSERV_Username="username"
+#
+#EUSERV_Password="password"
+#
+# Dependencies:
+# -------------
+# - none -
+
+EUSERV_Api="https://api.euserv.net"
+
+########  Public functions #####################
+
+#Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_euserv_add() {
+  fulldomain="$(echo "$1" | _lower_case)"
+  txtvalue=$2
+
+  EUSERV_Username="${EUSERV_Username:-$(_readaccountconf_mutable EUSERV_Username)}"
+  EUSERV_Password="${EUSERV_Password:-$(_readaccountconf_mutable EUSERV_Password)}"
+  if [ -z "$EUSERV_Username" ] || [ -z "$EUSERV_Password" ]; then
+    EUSERV_Username=""
+    EUSERV_Password=""
+    _err "You don't specify euserv user and password yet."
+    _err "Please create your key and try again."
+    return 1
+  fi
+
+  #save the user and email to the account conf file.
+  _saveaccountconf_mutable EUSERV_Username "$EUSERV_Username"
+  _saveaccountconf_mutable EUSERV_Password "$EUSERV_Password"
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+  _debug "_sub_domain" "$_sub_domain"
+  _debug "_domain" "$_domain"
+  _info "Adding record"
+  if ! _euserv_add_record "$_domain" "$_sub_domain" "$txtvalue"; then
+    return 1
+  fi
+
+}
+
+#fulldomain txtvalue
+dns_euserv_rm() {
+
+  fulldomain="$(echo "$1" | _lower_case)"
+  txtvalue=$2
+
+  EUSERV_Username="${EUSERV_Username:-$(_readaccountconf_mutable EUSERV_Username)}"
+  EUSERV_Password="${EUSERV_Password:-$(_readaccountconf_mutable EUSERV_Password)}"
+  if [ -z "$EUSERV_Username" ] || [ -z "$EUSERV_Password" ]; then
+    EUSERV_Username=""
+    EUSERV_Password=""
+    _err "You don't specify euserv user and password yet."
+    _err "Please create your key and try again."
+    return 1
+  fi
+
+  #save the user and email to the account conf file.
+  _saveaccountconf_mutable EUSERV_Username "$EUSERV_Username"
+  _saveaccountconf_mutable EUSERV_Password "$EUSERV_Password"
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+  _debug "_sub_domain" "$_sub_domain"
+  _debug "_domain" "$_domain"
+
+  _debug "Getting txt records"
+
+  xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
+  <methodCall>
+    <methodName>domain.dns_get_active_records</methodName>
+    <params>
+      <param>
+       <value>
+         <struct>
+           <member>
+             <name>login</name>
+             <value>
+               <string>%s</string>
+             </value>
+            </member>
+            <member>
+              <name>password</name>
+              <value>
+                <string>%s</string>
+              </value>
+            </member>
+            <member>
+              <name>domain_id</name>
+              <value>
+                <int>%s</int>
+              </value>
+            </member>
+          </struct>
+        </value>
+      </param>
+    </params>
+  </methodCall>' "$EUSERV_Username" "$EUSERV_Password" "$_euserv_domain_id")
+
+  export _H1="Content-Type: text/xml"
+  response="$(_post "$xml_content" "$EUSERV_Api" "" "POST")"
+
+  if ! _contains "$response" "<member><name>status</name><value><i4>100</i4></value></member>"; then
+    _err "Error could not get txt records"
+    _debug "xml_content" "$xml_content"
+    _debug "response" "$response"
+    return 1
+  fi
+
+  if ! echo "$response" | grep '>dns_record_content<.*>'"$txtvalue"'<' >/dev/null; then
+    _info "Do not need to delete record"
+  else
+    # find XML block where txtvalue is in. The record_id is allways prior this line!
+    _endLine=$(echo "$response" | grep -n '>dns_record_content<.*>'"$txtvalue"'<' | cut -d ':' -f 1)
+    # record_id is the last <name> Tag with a number before the row _endLine, identified by </name><value><struct> 
+    _record_id=$(echo "$response" | sed -n '1,'"$_endLine"'p' | grep '</name><value><struct>' | _tail_n 1 | sed 's/.*<name>\([0-9]*\)<\/name>.*/\1/')
+    _info "Deleting record"
+    _euserv_delete_record "$_record_id"
+  fi
+
+}
+
+####################  Private functions below ##################################
+
+_get_root() {
+  domain=$1
+  _debug "get root"
+
+  # Just to read the domain_orders once
+
+  domain=$1
+  i=2
+  p=1
+
+  if ! _euserv_get_domain_orders; then
+    return 1
+  fi
+
+  # Get saved response with domain_orders
+  response="$_euserv_domain_orders"
+
+  while true; do
+    h=$(echo "$domain" | cut -d . -f $i-100)
+    _debug h "$h"
+    if [ -z "$h" ]; then
+      #not valid
+      return 1
+    fi
+
+    if _contains "$response" "$h"; then
+      _sub_domain=$(echo "$domain" | cut -d . -f 1-$p)
+      _domain="$h"
+      if ! _euserv_get_domain_id "$_domain"; then
+        _err "invalid domain"
+        return 1
+      fi
+      return 0
+    fi
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+
+  return 1
+}
+
+_euserv_get_domain_orders() {
+  # returns: _euserv_domain_orders
+
+  _debug "get domain_orders"
+
+  xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
+  <methodCall>
+    <methodName>domain.get_domain_orders</methodName>
+    <params>
+      <param>
+        <value>
+          <struct>
+            <member>
+              <name>login</name>
+              <value><string>%s</string></value>
+            </member>
+            <member>
+              <name>password</name>
+              <value><string>%s</string></value>
+            </member>
+          </struct>
+        </value>
+      </param>
+    </params>
+  </methodCall>' "$EUSERV_Username" "$EUSERV_Password")
+
+  export _H1="Content-Type: text/xml"
+  response="$(_post "$xml_content" "$EUSERV_Api" "" "POST")"
+
+  if ! _contains "$response" "<member><name>status</name><value><i4>100</i4></value></member>"; then
+    _err "Error could not get domain orders"
+    _debug "xml_content" "$xml_content"
+    _debug "response" "$response"
+    return 1
+  fi
+
+  # save response to reduce API calls
+  _euserv_domain_orders="$response"
+  return 0
+}
+
+_euserv_get_domain_id() {
+  # returns: _euserv_domain_id
+  domain=$1
+  _debug "get domain_id"
+
+  # find line where the domain name is within the $response
+  _startLine=$(echo "$_euserv_domain_orders" | grep -n '>domain_name<.*>'"$domain"'<' | cut -d ':' -f 1)
+  # next occurency of domain_id after the domain_name is the correct one
+  _euserv_domain_id=$(echo "$_euserv_domain_orders" | sed -n "$_startLine"',$p' | grep '>domain_id<' | _head_n 1 | sed 's/.*<i4>\([0-9]*\)<\/i4>.*/\1/')
+
+  if [ -z "$_euserv_domain_id" ]; then
+    _err "Could not find domain_id for domain $domain"
+    _debug "_euserv_domain_orders" "$_euserv_domain_orders"
+    return 1
+  fi
+
+  return 0
+}
+
+_euserv_delete_record() {
+  record_id=$1
+  xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
+  <methodCall>
+    <methodName>domain.dns_delete_record</methodName>
+    <params>
+      <param>
+       <value>
+         <struct>
+           <member>
+             <name>login</name>
+             <value>
+               <string>%s</string>
+             </value>
+            </member>
+            <member>
+              <name>password</name>
+              <value>
+                <string>%s</string>
+              </value>
+            </member>
+            <member>
+              <name>dns_record_id</name>
+              <value>
+                <int>%s</int>
+              </value>
+            </member>
+          </struct>
+        </value>
+      </param>
+    </params>
+  </methodCall>' "$EUSERV_Username" "$EUSERV_Password" "$record_id")
+
+  export _H1="Content-Type: text/xml"
+  response="$(_post "$xml_content" "$EUSERV_Api" "" "POST")"
+
+  if ! _contains "$response" "<member><name>status</name><value><i4>100</i4></value></member>"; then
+    _err "Error deleting record"
+    _debug "xml_content" "$xml_content"
+    _debug "response" "$response"
+    return 1
+  fi
+
+  return 0
+
+}
+
+_euserv_add_record() {
+  domain=$1
+  sub_domain=$2
+  txtval=$3
+
+  xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
+  <methodCall>
+  <methodName>domain.dns_create_record</methodName>
+  <params>
+   <param>
+    <value>
+     <struct>
+      <member>
+       <name>login</name>
+       <value>
+        <string>%s</string>
+       </value>
+      </member>
+      <member>
+       <name>password</name>
+       <value>
+        <string>%s</string></value>
+      </member>
+      <member>
+       <name>domain_id</name>
+       <value>
+        <int>%s</int>
+       </value>
+      </member>
+      <member>
+       <name>dns_record_subdomain</name>
+       <value>
+        <string>%s</string>
+       </value>
+      </member>
+      <member>
+       <name>dns_record_type</name>
+       <value>
+        <string>TXT</string>
+       </value>
+      </member>
+      <member>
+       <name>dns_record_value</name>
+       <value>
+        <string>%s</string>
+       </value>
+      </member>
+      <member>
+       <name>dns_record_ttl</name>
+       <value>
+        <int>300</int>
+       </value>
+      </member>
+     </struct>
+    </value>
+   </param>
+  </params>
+  </methodCall>' "$EUSERV_Username" "$EUSERV_Password" "$_euserv_domain_id" "$sub_domain" "$txtval")
+
+  export _H1="Content-Type: text/xml"
+  response="$(_post "$xml_content" "$EUSERV_Api" "" "POST")"
+
+  if ! _contains "$response" "<member><name>status</name><value><i4>100</i4></value></member>"; then
+    _err "Error could not create record"
+    _debug "xml_content" "$xml_content"
+    _debug "response" "$response"
+    return 1
+  fi
+
+  return 0
+}

+ 167 - 0
dnsapi/dns_gcloud.sh

@@ -0,0 +1,167 @@
+#!/usr/bin/env sh
+
+# Author: Janos Lenart <janos@lenart.io>
+
+########  Public functions #####################
+
+# Usage: dns_gcloud_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_gcloud_add() {
+  fulldomain=$1
+  txtvalue=$2
+  _info "Using gcloud"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  _dns_gcloud_find_zone || return $?
+
+  # Add an extra RR
+  _dns_gcloud_start_tr || return $?
+  _dns_gcloud_get_rrdatas || return $?
+  echo "$rrdatas" | _dns_gcloud_remove_rrs || return $?
+  printf "%s\n%s\n" "$rrdatas" "\"$txtvalue\"" | grep -v '^$' | _dns_gcloud_add_rrs || return $?
+  _dns_gcloud_execute_tr || return $?
+
+  _info "$fulldomain record added"
+}
+
+# Usage: dns_gcloud_rm   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+# Remove the txt record after validation.
+dns_gcloud_rm() {
+  fulldomain=$1
+  txtvalue=$2
+  _info "Using gcloud"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  _dns_gcloud_find_zone || return $?
+
+  # Remove one RR
+  _dns_gcloud_start_tr || return $?
+  _dns_gcloud_get_rrdatas || return $?
+  echo "$rrdatas" | _dns_gcloud_remove_rrs || return $?
+  echo "$rrdatas" | grep -F -v "\"$txtvalue\"" | _dns_gcloud_add_rrs || return $?
+  _dns_gcloud_execute_tr || return $?
+
+  _info "$fulldomain record added"
+}
+
+####################  Private functions below ##################################
+
+_dns_gcloud_start_tr() {
+  if ! trd=$(mktemp -d); then
+    _err "_dns_gcloud_start_tr: failed to create temporary directory"
+    return 1
+  fi
+  tr="$trd/tr.yaml"
+  _debug tr "$tr"
+
+  if ! gcloud dns record-sets transaction start \
+    --transaction-file="$tr" \
+    --zone="$managedZone"; then
+    rm -r "$trd"
+    _err "_dns_gcloud_start_tr: failed to execute transaction"
+    return 1
+  fi
+}
+
+_dns_gcloud_execute_tr() {
+  if ! gcloud dns record-sets transaction execute \
+    --transaction-file="$tr" \
+    --zone="$managedZone"; then
+    _debug tr "$(cat "$tr")"
+    rm -r "$trd"
+    _err "_dns_gcloud_execute_tr: failed to execute transaction"
+    return 1
+  fi
+  rm -r "$trd"
+
+  for i in $(seq 1 120); do
+    if gcloud dns record-sets changes list \
+      --zone="$managedZone" \
+      --filter='status != done' \
+      | grep -q '^.*'; then
+      _info "_dns_gcloud_execute_tr: waiting for transaction to be comitted ($i/120)..."
+      sleep 5
+    else
+      return 0
+    fi
+  done
+
+  _err "_dns_gcloud_execute_tr: transaction is still pending after 10 minutes"
+  rm -r "$trd"
+  return 1
+}
+
+_dns_gcloud_remove_rrs() {
+  if ! xargs --no-run-if-empty gcloud dns record-sets transaction remove \
+    --name="$fulldomain." \
+    --ttl="$ttl" \
+    --type=TXT \
+    --zone="$managedZone" \
+    --transaction-file="$tr"; then
+    _debug tr "$(cat "$tr")"
+    rm -r "$trd"
+    _err "_dns_gcloud_remove_rrs: failed to remove RRs"
+    return 1
+  fi
+}
+
+_dns_gcloud_add_rrs() {
+  ttl=60
+  if ! xargs --no-run-if-empty gcloud dns record-sets transaction add \
+    --name="$fulldomain." \
+    --ttl="$ttl" \
+    --type=TXT \
+    --zone="$managedZone" \
+    --transaction-file="$tr"; then
+    _debug tr "$(cat "$tr")"
+    rm -r "$trd"
+    _err "_dns_gcloud_add_rrs: failed to add RRs"
+    return 1
+  fi
+}
+
+_dns_gcloud_find_zone() {
+  # Prepare a filter that matches zones that are suiteable for this entry.
+  # For example, _acme-challenge.something.domain.com might need to go into something.domain.com or domain.com;
+  # this function finds the longest postfix that has a managed zone.
+  part="$fulldomain"
+  filter="dnsName=( "
+  while [ "$part" != "" ]; do
+    filter="$filter$part. "
+    part="$(echo "$part" | sed 's/[^.]*\.*//')"
+  done
+  filter="$filter)"
+  _debug filter "$filter"
+
+  # List domains and find the longest match (in case of some levels of delegation)
+  if ! match=$(gcloud dns managed-zones list \
+    --format="value(name, dnsName)" \
+    --filter="$filter" \
+    | while read -r dnsName name; do
+      printf "%s\t%s\t%s\n" "${#dnsName}" "$dnsName" "$name"
+    done \
+    | sort -n -r | _head_n 1 | cut -f2,3 | grep '^.*'); then
+    _err "_dns_gcloud_find_zone: Can't find a matching managed zone! Perhaps wrong project or gcloud credentials?"
+    return 1
+  fi
+
+  dnsName=$(echo "$match" | cut -f2)
+  _debug dnsName "$dnsName"
+  managedZone=$(echo "$match" | cut -f1)
+  _debug managedZone "$managedZone"
+}
+
+_dns_gcloud_get_rrdatas() {
+  if ! rrdatas=$(gcloud dns record-sets list \
+    --zone="$managedZone" \
+    --name="$fulldomain." \
+    --type=TXT \
+    --format="value(ttl,rrdatas)"); then
+    _err "_dns_gcloud_get_rrdatas: Failed to list record-sets"
+    rm -r "$trd"
+    return 1
+  fi
+  ttl=$(echo "$rrdatas" | cut -f1)
+  rrdatas=$(echo "$rrdatas" | cut -f2 | sed 's/","/"\n"/g')
+}

+ 4 - 0
dnsapi/dns_gd.sh

@@ -168,5 +168,9 @@ _gd_rest() {
     return 1
   fi
   _debug2 response "$response"
+  if _contains "$response" "UNABLE_TO_AUTHENTICATE"; then
+    _err "It seems that your api key or secret is not correct."
+    return 1
+  fi
   return 0
 }

+ 2 - 2
dnsapi/dns_ispconfig.sh

@@ -128,7 +128,7 @@ _ISPC_addTxt() {
   curSerial="$(date +%s)"
   curStamp="$(date +'%F %T')"
   params="\"server_id\":\"${server_id}\",\"zone\":\"${zone}\",\"name\":\"${fulldomain}.\",\"type\":\"txt\",\"data\":\"${txtvalue}\",\"aux\":\"0\",\"ttl\":\"3600\",\"active\":\"y\",\"stamp\":\"${curStamp}\",\"serial\":\"${curSerial}\""
-  curData="{\"session_id\":\"${sessionID}\",\"client_id\":\"${client_id}\",\"params\":{${params}}}"
+  curData="{\"session_id\":\"${sessionID}\",\"client_id\":\"${client_id}\",\"params\":{${params}},\"update_serial\":true}"
   curResult="$(_post "${curData}" "${ISPC_Api}?dns_txt_add")"
   _debug "Calling _ISPC_addTxt: '${curData}' '${ISPC_Api}?dns_txt_add'"
   _debug "Result of _ISPC_addTxt: '$curResult'"
@@ -160,7 +160,7 @@ _ISPC_rmTxt() {
       *)
         unset IFS
         _info "Retrieved Record ID."
-        curData="{\"session_id\":\"${sessionID}\",\"primary_id\":\"${record_id}\"}"
+        curData="{\"session_id\":\"${sessionID}\",\"primary_id\":\"${record_id}\",\"update_serial\":true}"
         curResult="$(_post "${curData}" "${ISPC_Api}?dns_txt_delete")"
         _debug "Calling _ISPC_rmTxt: '${curData}' '${ISPC_Api}?dns_txt_delete'"
         _debug "Result of _ISPC_rmTxt: '$curResult'"

+ 40 - 15
dnsapi/dns_lexicon.sh

@@ -7,20 +7,13 @@ lexicon_cmd="lexicon"
 
 wiki="https://github.com/Neilpang/acme.sh/wiki/How-to-use-lexicon-dns-api"
 
-########  Public functions #####################
-
-#Usage: add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
-dns_lexicon_add() {
-  fulldomain=$1
-  txtvalue=$2
-
-  domain=$(printf "%s" "$fulldomain" | cut -d . -f 2-999)
-
+_lexicon_init() {
   if ! _exists "$lexicon_cmd"; then
     _err "Please install $lexicon_cmd first: $wiki"
     return 1
   fi
 
+  PROVIDER="${PROVIDER:-$(_readdomainconf PROVIDER)}"
   if [ -z "$PROVIDER" ]; then
     PROVIDER=""
     _err "Please define env PROVIDER first: $wiki"
@@ -33,46 +26,78 @@ dns_lexicon_add() {
   # e.g. busybox-ash does not know [:upper:]
   # shellcheck disable=SC2018,SC2019
   Lx_name=$(echo LEXICON_"${PROVIDER}"_USERNAME | tr 'a-z' 'A-Z')
+  eval "$Lx_name=\${$Lx_name:-$(_readaccountconf_mutable "$Lx_name")}"
   Lx_name_v=$(eval echo \$"$Lx_name")
   _secure_debug "$Lx_name" "$Lx_name_v"
   if [ "$Lx_name_v" ]; then
-    _saveaccountconf "$Lx_name" "$Lx_name_v"
+    _saveaccountconf_mutable "$Lx_name" "$Lx_name_v"
     eval export "$Lx_name"
   fi
 
   # shellcheck disable=SC2018,SC2019
   Lx_token=$(echo LEXICON_"${PROVIDER}"_TOKEN | tr 'a-z' 'A-Z')
+  eval "$Lx_token=\${$Lx_token:-$(_readaccountconf_mutable "$Lx_token")}"
   Lx_token_v=$(eval echo \$"$Lx_token")
   _secure_debug "$Lx_token" "$Lx_token_v"
   if [ "$Lx_token_v" ]; then
-    _saveaccountconf "$Lx_token" "$Lx_token_v"
+    _saveaccountconf_mutable "$Lx_token" "$Lx_token_v"
     eval export "$Lx_token"
   fi
 
   # shellcheck disable=SC2018,SC2019
   Lx_password=$(echo LEXICON_"${PROVIDER}"_PASSWORD | tr 'a-z' 'A-Z')
+  eval "$Lx_password=\${$Lx_password:-$(_readaccountconf_mutable "$Lx_password")}"
   Lx_password_v=$(eval echo \$"$Lx_password")
   _secure_debug "$Lx_password" "$Lx_password_v"
   if [ "$Lx_password_v" ]; then
-    _saveaccountconf "$Lx_password" "$Lx_password_v"
+    _saveaccountconf_mutable "$Lx_password" "$Lx_password_v"
     eval export "$Lx_password"
   fi
 
   # shellcheck disable=SC2018,SC2019
   Lx_domaintoken=$(echo LEXICON_"${PROVIDER}"_DOMAINTOKEN | tr 'a-z' 'A-Z')
+  eval "$Lx_domaintoken=\${$Lx_domaintoken:-$(_readaccountconf_mutable "$Lx_domaintoken")}"
   Lx_domaintoken_v=$(eval echo \$"$Lx_domaintoken")
   _secure_debug "$Lx_domaintoken" "$Lx_domaintoken_v"
   if [ "$Lx_domaintoken_v" ]; then
+    _saveaccountconf_mutable "$Lx_domaintoken" "$Lx_domaintoken_v"
     eval export "$Lx_domaintoken"
-    _saveaccountconf "$Lx_domaintoken" "$Lx_domaintoken_v"
   fi
+}
+
+########  Public functions #####################
+
+#Usage: dns_lexicon_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_lexicon_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  if ! _lexicon_init; then
+    return 1
+  fi
+
+  domain=$(printf "%s" "$fulldomain" | cut -d . -f 2-999)
+
+  _secure_debug LEXICON_OPTS "$LEXICON_OPTS"
+  _savedomainconf LEXICON_OPTS "$LEXICON_OPTS"
 
-  $lexicon_cmd "$PROVIDER" create "${domain}" TXT --name="_acme-challenge.${domain}." --content="${txtvalue}"
+  # shellcheck disable=SC2086
+  $lexicon_cmd "$PROVIDER" $LEXICON_OPTS create "${domain}" TXT --name="_acme-challenge.${domain}." --content="${txtvalue}"
 
 }
 
-#fulldomain
+#Usage: dns_lexicon_rm   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
 dns_lexicon_rm() {
   fulldomain=$1
+  txtvalue=$2
+
+  if ! _lexicon_init; then
+    return 1
+  fi
+
+  domain=$(printf "%s" "$fulldomain" | cut -d . -f 2-999)
+
+  # shellcheck disable=SC2086
+  $lexicon_cmd "$PROVIDER" $LEXICON_OPTS delete "${domain}" TXT --name="_acme-challenge.${domain}." --content="${txtvalue}"
 
 }

+ 22 - 39
dnsapi/dns_unoeuro.sh

@@ -50,34 +50,16 @@ dns_unoeuro_add() {
     _err "Error"
     return 1
   fi
+  _info "Adding record"
 
-  if ! _contains "$response" "$_sub_domain" >/dev/null; then
-    _info "Adding record"
-
-    if _uno_rest POST "my/products/$h/dns/records" "{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"data\":\"$txtvalue\",\"ttl\":120}"; then
-      if _contains "$response" "\"status\": 200" >/dev/null; then
-        _info "Added, OK"
-        return 0
-      else
-        _err "Add txt record error."
-        return 1
-      fi
-    fi
-    _err "Add txt record error."
-  else
-    _info "Updating record"
-    record_line_number=$(echo "$response" | grep -n "$_sub_domain" | cut -d : -f 1)
-    record_line_number=$(_math "$record_line_number" - 1)
-    record_id=$(echo "$response" | _head_n "$record_line_number" | _tail_n 1 1 | _egrep_o "[0-9]{1,}")
-    _debug "record_id" "$record_id"
-
-    _uno_rest PUT "my/products/$h/dns/records/$record_id" "{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"data\":\"$txtvalue\",\"ttl\":120}"
+  if _uno_rest POST "my/products/$h/dns/records" "{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"data\":\"$txtvalue\",\"ttl\":120}"; then
     if _contains "$response" "\"status\": 200" >/dev/null; then
-      _info "Updated, OK"
+      _info "Added, OK"
       return 0
+    else
+      _err "Add txt record error."
+      return 1
     fi
-    _err "Update error"
-    return 1
   fi
 }
 
@@ -122,23 +104,24 @@ dns_unoeuro_rm() {
   if ! _contains "$response" "$_sub_domain"; then
     _info "Don't need to remove."
   else
-    record_line_number=$(echo "$response" | grep -n "$_sub_domain" | cut -d : -f 1)
-    record_line_number=$(_math "$record_line_number" - 1)
-    record_id=$(echo "$response" | _head_n "$record_line_number" | _tail_n 1 1 | _egrep_o "[0-9]{1,}")
-    _debug "record_id" "$record_id"
-
-    if [ -z "$record_id" ]; then
-      _err "Can not get record id to remove."
-      return 1
-    fi
+    for record_line_number in $(echo "$response" | grep -n "$_sub_domain" | cut -d : -f 1); do
+      record_line_number=$(_math "$record_line_number" - 1)
+      _debug "record_line_number" "$record_line_number"
+      record_id=$(echo "$response" | _head_n "$record_line_number" | _tail_n 1 1 | _egrep_o "[0-9]{1,}")
+      _debug "record_id" "$record_id"
+
+      if [ -z "$record_id" ]; then
+        _err "Can not get record id to remove."
+        return 1
+      fi
 
-    if ! _uno_rest DELETE "my/products/$h/dns/records/$record_id"; then
-      _err "Delete record error."
-      return 1
-    fi
-    _contains "$response" "\"status\": 200"
+      if ! _uno_rest DELETE "my/products/$h/dns/records/$record_id"; then
+        _err "Delete record error."
+        return 1
+      fi
+      _contains "$response" "\"status\": 200"
+    done
   fi
-
 }
 
 ####################  Private functions below ##################################