Browse Source

更新到https

liuyuqi-dellpc 3 years ago
parent
commit
d840e3a284
85 changed files with 2898 additions and 1338 deletions
  1. 1 1
      .github/FUNDING.yml
  2. 3 3
      .github/ISSUE_TEMPLATE.md
  3. 1 1
      .github/PULL_REQUEST_TEMPLATE.md
  4. 15 7
      Dockerfile
  5. 128 64
      README.md
  6. 437 294
      acme.sh
  7. 1 1
      deploy/README.md
  8. 167 18
      deploy/cpanel_uapi.sh
  9. 3 2
      deploy/docker.sh
  10. 2 2
      deploy/exim4.sh
  11. 32 20
      deploy/fritzbox.sh
  12. 11 8
      deploy/gcore_cdn.sh
  13. 40 32
      deploy/haproxy.sh
  14. 3 3
      deploy/kong.sh
  15. 15 4
      deploy/mailcow.sh
  16. 20 16
      deploy/qiniu.sh
  17. 115 23
      deploy/routeros.sh
  18. 137 39
      deploy/ssh.sh
  19. 167 53
      deploy/unifi.sh
  20. 8 8
      deploy/vault_cli.sh
  21. 3 3
      deploy/vsftpd.sh
  22. 1 1
      dnsapi/README.md
  23. 51 12
      dnsapi/dns_acmedns.sh
  24. 1 0
      dnsapi/dns_ali.sh
  25. 63 40
      dnsapi/dns_aws.sh
  26. 109 79
      dnsapi/dns_azure.sh
  27. 50 9
      dnsapi/dns_cf.sh
  28. 27 3
      dnsapi/dns_cloudns.sh
  29. 3 3
      dnsapi/dns_conoha.sh
  30. 18 18
      dnsapi/dns_cyon.sh
  31. 18 18
      dnsapi/dns_da.sh
  32. 2 2
      dnsapi/dns_ddnss.sh
  33. 11 18
      dnsapi/dns_desec.sh
  34. 7 8
      dnsapi/dns_dgon.sh
  35. 12 12
      dnsapi/dns_do.sh
  36. 3 3
      dnsapi/dns_doapi.sh
  37. 7 7
      dnsapi/dns_dp.sh
  38. 8 8
      dnsapi/dns_dpi.sh
  39. 11 7
      dnsapi/dns_duckdns.sh
  40. 5 5
      dnsapi/dns_durabledns.sh
  41. 4 0
      dnsapi/dns_dynu.sh
  42. 1 1
      dnsapi/dns_euserv.sh
  43. 11 11
      dnsapi/dns_freedns.sh
  44. 8 8
      dnsapi/dns_gandi_livedns.sh
  45. 14 11
      dnsapi/dns_gcloud.sh
  46. 41 17
      dnsapi/dns_gd.sh
  47. 4 4
      dnsapi/dns_he.sh
  48. 4 4
      dnsapi/dns_hexonet.sh
  49. 17 8
      dnsapi/dns_infoblox.sh
  50. 74 10
      dnsapi/dns_inwx.sh
  51. 64 46
      dnsapi/dns_ispconfig.sh
  52. 1 1
      dnsapi/dns_kinghost.sh
  53. 4 2
      dnsapi/dns_knot.sh
  54. 13 3
      dnsapi/dns_lexicon.sh
  55. 7 6
      dnsapi/dns_linode_v4.sh
  56. 54 30
      dnsapi/dns_loopia.sh
  57. 2 2
      dnsapi/dns_me.sh
  58. 2 2
      dnsapi/dns_myapi.sh
  59. 3 1
      dnsapi/dns_mydevil.sh
  60. 1 15
      dnsapi/dns_mydnsjp.sh
  61. 12 8
      dnsapi/dns_namecheap.sh
  62. 1 1
      dnsapi/dns_namesilo.sh
  63. 3 7
      dnsapi/dns_nederhost.sh
  64. 4 4
      dnsapi/dns_netcup.sh
  65. 1 1
      dnsapi/dns_nsd.sh
  66. 2 2
      dnsapi/dns_nsupdate.sh
  67. 95 47
      dnsapi/dns_one.sh
  68. 12 9
      dnsapi/dns_openprovider.sh
  69. 46 42
      dnsapi/dns_ovh.sh
  70. 7 6
      dnsapi/dns_pdns.sh
  71. 5 4
      dnsapi/dns_rackspace.sh
  72. 69 5
      dnsapi/dns_regru.sh
  73. 2 2
      dnsapi/dns_selectel.sh
  74. 35 9
      dnsapi/dns_servercow.sh
  75. 12 9
      dnsapi/dns_ultra.sh
  76. 2 8
      dnsapi/dns_unoeuro.sh
  77. 1 1
      dnsapi/dns_vscale.sh
  78. 17 19
      dnsapi/dns_vultr.sh
  79. 69 57
      dnsapi/dns_yandex.sh
  80. 2 2
      dnsapi/dns_zone.sh
  81. 46 40
      notify/mail.sh
  82. 1 1
      notify/mailgun.sh
  83. 9 1
      notify/sendgrid.sh
  84. 393 9
      notify/smtp.sh
  85. 7 7
      notify/xmpp.sh

+ 1 - 1
.github/FUNDING.yml

@@ -3,7 +3,7 @@
 github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
 patreon: # Replace with a single Patreon username
 open_collective: acmesh
-ko_fi: # Replace with a single Ko-fi username
+ko_fi: neilpang
 tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
 community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
 liberapay: # Replace with a single Liberapay username

+ 3 - 3
.github/ISSUE_TEMPLATE.md

@@ -2,15 +2,15 @@
 我很忙, 每天可能只有 几秒钟 时间看你的 issue, 如果不按照我的要求写 issue, 你可能不会得到任何回复, 石沉大海.
 
 请确保已经更新到最新的代码, 然后贴上来 `--debug 2` 的调试输出. 没有调试信息. 我做不了什么.
-如何调试 https://github.com/Neilpang/acme.sh/wiki/How-to-debug-acme.sh
+如何调试 https://github.com/acmesh-official/acme.sh/wiki/How-to-debug-acme.sh
 
 If it is a bug report:
-- make sure you are able to repro it on the latest released version. 
+- make sure you are able to repro it on the latest released version.
 You can install the latest version by: `acme.sh --upgrade`
 
 - Search the existing issues.
 - Refer to the [WIKI](https://wiki.acme.sh).
-- Debug info [Debug](https://github.com/Neilpang/acme.sh/wiki/How-to-debug-acme.sh).
+- Debug info [Debug](https://github.com/acmesh-official/acme.sh/wiki/How-to-debug-acme.sh).
 
 -->
 

+ 1 - 1
.github/PULL_REQUEST_TEMPLATE.md

@@ -3,7 +3,7 @@
 Please send to `dev` branch instead.
 Any PR to `master` branch will NOT be merged.
 
-2. For dns api support, read this guide first: https://github.com/Neilpang/acme.sh/wiki/DNS-API-Dev-Guide
+2. For dns api support, read this guide first: https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide
 You will NOT get any review without passing this guide.  You also need to fix the CI errors.
 
 -->

+ 15 - 7
Dockerfile

@@ -1,23 +1,27 @@
-FROM alpine:3.10
+FROM alpine:3.15
 
-RUN apk update -f \
-  && apk --no-cache add -f \
+RUN apk --no-cache add -f \
   openssl \
+  openssh-client \
   coreutils \
   bind-tools \
   curl \
+  sed \
   socat \
   tzdata \
   oath-toolkit-oathtool \
   tar \
-  && rm -rf /var/cache/apk/*
+  libidn \
+  jq
 
 ENV LE_CONFIG_HOME /acme.sh
 
-ENV AUTO_UPGRADE 1
+ARG AUTO_UPGRADE=1
+
+ENV AUTO_UPGRADE $AUTO_UPGRADE
 
 #Install
-ADD ./ /install_acme.sh/
+COPY ./ /install_acme.sh/
 RUN cd /install_acme.sh && ([ -f /install_acme.sh/acme.sh ] && /install_acme.sh/acme.sh --install || curl https://get.acme.sh | sh) && rm -rf /install_acme.sh/
 
 
@@ -37,6 +41,7 @@ RUN for verb in help \
   revoke \
   remove \
   list \
+  info \
   showcsr \
   install-cronjob \
   uninstall-cronjob \
@@ -51,6 +56,8 @@ RUN for verb in help \
   deactivate \
   deactivate-account \
   set-notify \
+  set-default-ca \
+  set-default-chain \
   ; do \
     printf -- "%b" "#!/usr/bin/env sh\n/root/.acme.sh/acme.sh --${verb} --config-home /acme.sh \"\$@\"" >/usr/local/bin/--${verb} && chmod +x /usr/local/bin/--${verb} \
   ; done
@@ -58,7 +65,8 @@ RUN for verb in help \
 RUN printf "%b" '#!'"/usr/bin/env sh\n \
 if [ \"\$1\" = \"daemon\" ];  then \n \
  trap \"echo stop && killall crond && exit 0\" SIGTERM SIGINT \n \
- crond && while true; do sleep 1; done;\n \
+ crond && sleep infinity &\n \
+ wait \n \
 else \n \
  exec -- \"\$@\"\n \
 fi" >/entry.sh && chmod +x /entry.sh

+ 128 - 64
README.md

@@ -1,35 +1,55 @@
-# An ACME Shell script: acme.sh [![Build Status](https://travis-ci.org/Neilpang/acme.sh.svg?branch=master)](https://travis-ci.org/Neilpang/acme.sh)
+# An ACME Shell script: acme.sh 
+
+[![FreeBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml)
+[![OpenBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml)
+[![NetBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml)
+[![MacOS](https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml)
+[![Ubuntu](https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml)
+[![Windows](https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml)
+[![Solaris](https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml)
+[![DragonFlyBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml)
+
+
+![Shellcheck](https://github.com/acmesh-official/acme.sh/workflows/Shellcheck/badge.svg)
+![PebbleStrict](https://github.com/acmesh-official/acme.sh/workflows/PebbleStrict/badge.svg)
+![DockerHub](https://github.com/acmesh-official/acme.sh/workflows/Build%20DockerHub/badge.svg)
+
+
+<a href="https://opencollective.com/acmesh" alt="Financial Contributors on Open Collective"><img src="https://opencollective.com/acmesh/all/badge.svg?label=financial+contributors" /></a> 
+[![Join the chat at https://gitter.im/acme-sh/Lobby](https://badges.gitter.im/acme-sh/Lobby.svg)](https://gitter.im/acme-sh/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[![Docker stars](https://img.shields.io/docker/stars/neilpang/acme.sh.svg)](https://hub.docker.com/r/neilpang/acme.sh "Click to view the image on Docker Hub")
+[![Docker pulls](https://img.shields.io/docker/pulls/neilpang/acme.sh.svg)](https://hub.docker.com/r/neilpang/acme.sh "Click to view the image on Docker Hub")
+
+
 
-<img src="https://opencollective.com/acmesh/tiers/backers/badge.svg?label=backer&color=brightgreen" /> [![Join the chat at https://gitter.im/acme-sh/Lobby](https://badges.gitter.im/acme-sh/Lobby.svg)](https://gitter.im/acme-sh/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
 - An ACME protocol client written purely in Shell (Unix shell) language.
 - Full ACME protocol implementation.
-- Support ACME v1 and ACME v2
-- Support ACME v2 wildcard certs
+- Support ECDSA certs
+- Support SAN and wildcard certs
 - Simple, powerful and very easy to use. You only need 3 minutes to learn it.
 - Bash, dash and sh compatible.
-- Simplest shell script for Let's Encrypt free certificate client.
-- Purely written in Shell with no dependencies on python or the official Let's Encrypt client.
+- Purely written in Shell with no dependencies on python.
 - Just one script to issue, renew and install your certificates automatically.
 - DOES NOT require `root/sudoer` access.
-- Docker friendly
-- IPv6 support
+- Docker ready
+- IPv6 ready
 - Cron job notifications for renewal or error etc.
 
-It's probably the `easiest & smartest` shell script to automatically issue & renew the free certificates from Let's Encrypt.
+It's probably the `easiest & smartest` shell script to automatically issue & renew the free certificates.
 
-Wiki: https://github.com/Neilpang/acme.sh/wiki
+Wiki: https://github.com/acmesh-official/acme.sh/wiki
 
-For Docker Fans: [acme.sh :two_hearts: Docker ](https://github.com/Neilpang/acme.sh/wiki/Run-acme.sh-in-docker)
+For Docker Fans: [acme.sh :two_hearts: Docker ](https://github.com/acmesh-official/acme.sh/wiki/Run-acme.sh-in-docker)
 
 Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
 
 
-# [中文说明](https://github.com/Neilpang/acme.sh/wiki/%E8%AF%B4%E6%98%8E)
+# [中文说明](https://github.com/acmesh-official/acme.sh/wiki/%E8%AF%B4%E6%98%8E)
 
 # Who:
 - [FreeBSD.org](https://blog.crashed.org/letsencrypt-in-freebsd-org/)
 - [ruby-china.org](https://ruby-china.org/topics/31983)
-- [Proxmox](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x_and_newer))
+- [Proxmox](https://pve.proxmox.com/wiki/Certificate_Management)
 - [pfsense](https://github.com/pfsense/FreeBSD-ports/pull/89)
 - [webfaction](https://community.webfaction.com/questions/19988/using-letsencrypt)
 - [Loadbalancer.org](https://www.loadbalancer.org/blog/loadbalancer-org-with-lets-encrypt-quick-and-dirty)
@@ -40,42 +60,50 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
 - [opnsense.org](https://github.com/opnsense/plugins/tree/master/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient)
 - [CentOS Web Panel](http://centos-webpanel.com/)
 - [lnmp.org](https://lnmp.org/)
-- [more...](https://github.com/Neilpang/acme.sh/wiki/Blogs-and-tutorials)
+- [more...](https://github.com/acmesh-official/acme.sh/wiki/Blogs-and-tutorials)
 
 # Tested OS
 
 | NO | Status| Platform|
 |----|-------|---------|
-|1|[![](https://neilpang.github.io/acmetest/status/ubuntu-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)| Ubuntu
-|2|[![](https://neilpang.github.io/acmetest/status/debian-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)| Debian
-|3|[![](https://neilpang.github.io/acmetest/status/centos-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|CentOS
-|4|[![](https://neilpang.github.io/acmetest/status/windows-cygwin.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Windows (cygwin with curl, openssl and crontab included)
-|5|[![](https://neilpang.github.io/acmetest/status/freebsd.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|FreeBSD
-|6|[![](https://neilpang.github.io/acmetest/status/pfsense.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|pfsense
-|7|[![](https://neilpang.github.io/acmetest/status/opensuse-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|openSUSE
-|8|[![](https://neilpang.github.io/acmetest/status/alpine-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Alpine Linux (with curl)
-|9|[![](https://neilpang.github.io/acmetest/status/base-archlinux.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Archlinux
-|10|[![](https://neilpang.github.io/acmetest/status/fedora-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|fedora
-|11|[![](https://neilpang.github.io/acmetest/status/kalilinux-kali-linux-docker.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Kali Linux
-|12|[![](https://neilpang.github.io/acmetest/status/oraclelinux-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Oracle Linux
-|13|[![](https://neilpang.github.io/acmetest/status/proxmox.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)| Proxmox https://pve.proxmox.com/wiki/HTTPSCertificateConfiguration#Let.27s_Encrypt_using_acme.sh
-|14|-----| Cloud Linux  https://github.com/Neilpang/le/issues/111
-|15|[![](https://neilpang.github.io/acmetest/status/openbsd.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|OpenBSD
-|16|[![](https://neilpang.github.io/acmetest/status/mageia.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Mageia
-|17|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/Neilpang/acme.sh/wiki/How-to-run-on-OpenWRT)
-|18|[![](https://neilpang.github.io/acmetest/status/solaris.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|SunOS/Solaris
-|19|[![](https://neilpang.github.io/acmetest/status/gentoo-stage3-amd64.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Gentoo Linux
-|20|[![Build Status](https://travis-ci.org/Neilpang/acme.sh.svg?branch=master)](https://travis-ci.org/Neilpang/acme.sh)|Mac OSX
-
-For all build statuses, check our [weekly build project](https://github.com/Neilpang/acmetest):
-
-https://github.com/Neilpang/acmetest
+|1|[![MacOS](https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml)|Mac OSX
+|2|[![Windows](https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml)|Windows (cygwin with curl, openssl and crontab included)
+|3|[![FreeBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml)|FreeBSD
+|4|[![Solaris](https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml)|Solaris
+|5|[![Ubuntu](https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml)| Ubuntu
+|6|NA|pfsense
+|7|[![OpenBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml)|OpenBSD
+|8|[![NetBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml)|NetBSD
+|9|[![DragonFlyBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml)|DragonFlyBSD
+|10|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)| Debian
+|11|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|CentOS
+|12|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|openSUSE
+|13|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Alpine Linux (with curl)
+|14|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Archlinux
+|15|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|fedora
+|16|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Kali Linux
+|17|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Oracle Linux
+|18|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Mageia
+|19|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Gentoo Linux
+|10|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|ClearLinux
+|11|-----| Cloud Linux  https://github.com/acmesh-official/acme.sh/issues/111
+|22|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/acmesh-official/acme.sh/wiki/How-to-run-on-OpenWRT)
+|23|[![](https://acmesh-official.github.io/acmetest/status/proxmox.svg)](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management)
+
+
+Check our [testing project](https://github.com/acmesh-official/acmetest):
+
+https://github.com/acmesh-official/acmetest
 
 # Supported CA
 
-- Letsencrypt.org CA(default)
-- [BuyPass.com CA](https://github.com/Neilpang/acme.sh/wiki/BuyPass.com-CA)
+- [ZeroSSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/ZeroSSL.com-CA)(default)
+- Letsencrypt.org CA
+- [BuyPass.com CA](https://github.com/acmesh-official/acme.sh/wiki/BuyPass.com-CA)
+- [SSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/SSL.com-CA)
+- [Google.com Public CA](https://github.com/acmesh-official/acme.sh/wiki/Google-Public-CA)
 - [Pebble strict Mode](https://github.com/letsencrypt/pebble)
+- Any other [RFC8555](https://tools.ietf.org/html/rfc8555)-compliant CA
 
 # Supported modes
 
@@ -85,24 +113,24 @@ https://github.com/Neilpang/acmetest
 - Apache mode
 - Nginx mode
 - DNS mode
-- [DNS alias mode](https://github.com/Neilpang/acme.sh/wiki/DNS-alias-mode)
-- [Stateless mode](https://github.com/Neilpang/acme.sh/wiki/Stateless-Mode)
+- [DNS alias mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-alias-mode)
+- [Stateless mode](https://github.com/acmesh-official/acme.sh/wiki/Stateless-Mode)
 
 
 # 1. How to install
 
 ### 1. Install online
 
-Check this project: https://github.com/Neilpang/get.acme.sh
+Check this project: https://github.com/acmesh-official/get.acme.sh
 
 ```bash
-curl https://get.acme.sh | sh
+curl https://get.acme.sh | sh -s email=my@example.com
 ```
 
 Or:
 
 ```bash
-wget -O -  https://get.acme.sh | sh
+wget -O -  https://get.acme.sh | sh -s email=my@example.com
 ```
 
 
@@ -111,14 +139,14 @@ wget -O -  https://get.acme.sh | sh
 Clone this project and launch installation:
 
 ```bash
-git clone https://github.com/Neilpang/acme.sh.git
+git clone https://github.com/acmesh-official/acme.sh.git
 cd ./acme.sh
-./acme.sh --install
+./acme.sh --install -m my@example.com
 ```
 
 You `don't have to be root` then, although `it is recommended`.
 
-Advanced Installation: https://github.com/Neilpang/acme.sh/wiki/How-to-install
+Advanced Installation: https://github.com/acmesh-official/acme.sh/wiki/How-to-install
 
 The installer will perform 3 actions:
 
@@ -180,7 +208,7 @@ The certs will be placed in `~/.acme.sh/example.com/`
 
 The certs will be renewed automatically every **60** days.
 
-More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert
+More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
 
 
 # 3. Install the cert to Apache/Nginx etc.
@@ -226,7 +254,7 @@ Port `80` (TCP) **MUST** be free to listen on, otherwise you will be prompted to
 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
+More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
 
 # 5. Use Standalone ssl server to issue cert
 
@@ -238,14 +266,14 @@ Port `443` (TCP) **MUST** be free to listen on, otherwise you will be prompted t
 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
+More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
 
 
 # 6. Use Apache mode
 
 **(requires you to be root/sudoer, since it is required to interact with Apache server)**
 
-If you are running a web server, Apache or Nginx, it is recommended to use the `Webroot mode`.
+If you are running a web server, it is recommended to use the `Webroot mode`.
 
 Particularly, if you are running an Apache server, you can use Apache mode instead. This mode doesn't write any files to your web root folder.
 
@@ -257,15 +285,15 @@ acme.sh --issue --apache -d example.com -d www.example.com -d cp.example.com
 
 **This apache mode is only to issue the cert, it will not change your apache config files.
 You will need to configure your website config files to use the cert by yourself.
-We don't want to mess your apache server, don't worry.**
+We don't want to mess with your apache server, don't worry.**
 
-More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert
+More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
 
 # 7. Use Nginx mode
 
 **(requires you to be root/sudoer, since it is required to interact with Nginx server)**
 
-If you are running a web server, Apache or Nginx, it is recommended to use the `Webroot mode`.
+If you are running a web server, it is recommended to use the `Webroot mode`.
 
 Particularly, if you are running an nginx server, you can use nginx mode instead. This mode doesn't write any files to your web root folder.
 
@@ -281,9 +309,9 @@ acme.sh --issue --nginx -d example.com -d www.example.com -d cp.example.com
 
 **This nginx mode is only to issue the cert, it will not change your nginx config files.
 You will need to configure your website config files to use the cert by yourself.
-We don't want to mess your nginx server, don't worry.**
+We don't want to mess with your nginx server, don't worry.**
 
-More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert
+More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
 
 # 8. Automatic DNS API integration
 
@@ -293,13 +321,13 @@ You don't have to do anything manually!
 
 ### Currently acme.sh supports most of the dns providers:
 
-https://github.com/Neilpang/acme.sh/wiki/dnsapi
+https://github.com/acmesh-official/acme.sh/wiki/dnsapi
 
 # 9. Use DNS manual mode:
 
-See: https://github.com/Neilpang/acme.sh/wiki/dns-manual-mode first.
+See: https://github.com/acmesh-official/acme.sh/wiki/dns-manual-mode first.
 
-If your dns provider doesn't support any api access, you can add the txt record by your hand.
+If your dns provider doesn't support any api access, you can add the txt record by hand.
 
 ```bash
 acme.sh --issue --dns -d example.com -d www.example.com -d cp.example.com
@@ -430,12 +458,12 @@ acme.sh --upgrade --auto-upgrade 0
 
 # 15. Issue a cert from an existing CSR
 
-https://github.com/Neilpang/acme.sh/wiki/Issue-a-cert-from-existing-CSR
+https://github.com/acmesh-official/acme.sh/wiki/Issue-a-cert-from-existing-CSR
 
 
 # 16. Send notifications in cronjob
 
-https://github.com/Neilpang/acme.sh/wiki/notify
+https://github.com/acmesh-official/acme.sh/wiki/notify
 
 
 # 17. Under the Hood
@@ -451,13 +479,49 @@ TODO:
 2. ACME protocol: https://github.com/ietf-wg-acme/acme
 
 
+## Contributors
+
+### Code Contributors
+
+This project exists thanks to all the people who contribute.
+<a href="https://github.com/acmesh-official/acme.sh/graphs/contributors"><img src="https://opencollective.com/acmesh/contributors.svg?width=890&button=false" /></a>
+
+### Financial Contributors
+
+Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/acmesh/contribute)]
+
+#### Individuals
+
+<a href="https://opencollective.com/acmesh"><img src="https://opencollective.com/acmesh/individuals.svg?width=890"></a>
+
+#### Organizations
+
+Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/acmesh/contribute)]
+
+<a href="https://opencollective.com/acmesh/organization/0/website"><img src="https://opencollective.com/acmesh/organization/0/avatar.svg"></a>
+<a href="https://opencollective.com/acmesh/organization/1/website"><img src="https://opencollective.com/acmesh/organization/1/avatar.svg"></a>
+<a href="https://opencollective.com/acmesh/organization/2/website"><img src="https://opencollective.com/acmesh/organization/2/avatar.svg"></a>
+<a href="https://opencollective.com/acmesh/organization/3/website"><img src="https://opencollective.com/acmesh/organization/3/avatar.svg"></a>
+<a href="https://opencollective.com/acmesh/organization/4/website"><img src="https://opencollective.com/acmesh/organization/4/avatar.svg"></a>
+<a href="https://opencollective.com/acmesh/organization/5/website"><img src="https://opencollective.com/acmesh/organization/5/avatar.svg"></a>
+<a href="https://opencollective.com/acmesh/organization/6/website"><img src="https://opencollective.com/acmesh/organization/6/avatar.svg"></a>
+<a href="https://opencollective.com/acmesh/organization/7/website"><img src="https://opencollective.com/acmesh/organization/7/avatar.svg"></a>
+<a href="https://opencollective.com/acmesh/organization/8/website"><img src="https://opencollective.com/acmesh/organization/8/avatar.svg"></a>
+<a href="https://opencollective.com/acmesh/organization/9/website"><img src="https://opencollective.com/acmesh/organization/9/avatar.svg"></a>
+
+
+#### Sponsors
+
+[![quantumca-acmesh-logo](https://user-images.githubusercontent.com/8305679/183255712-634ee1db-bb61-4c03-bca0-bacce99e078c.svg)](https://www.quantumca.com.cn/?__utm_source=acmesh-donation)
+
+
 # 19. License & Others
 
 License is GPLv3
 
 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.
+[Issues](https://github.com/acmesh-official/acme.sh/issues) and [pull requests](https://github.com/acmesh-official/acme.sh/pulls) are welcome.
 
 
 # 20. Donate
@@ -465,4 +529,4 @@ Your donation makes **acme.sh** better:
 
 1. PayPal/Alipay(支付宝)/Wechat(微信): [https://donate.acme.sh/](https://donate.acme.sh/)
 
-[Donate List](https://github.com/Neilpang/acme.sh/wiki/Donate-list)
+[Donate List](https://github.com/acmesh-official/acme.sh/wiki/Donate-list)

File diff suppressed because it is too large
+ 437 - 294
acme.sh


+ 1 - 1
deploy/README.md

@@ -2,5 +2,5 @@
 
 deploy hook usage:
 
-https://github.com/Neilpang/acme.sh/wiki/deployhooks
+https://github.com/acmesh-official/acme.sh/wiki/deployhooks
 

+ 167 - 18
deploy/cpanel_uapi.sh

@@ -3,18 +3,29 @@
 # Uses command line uapi.  --user option is needed only if run as root.
 # Returns 0 when success.
 #
+# Configure DEPLOY_CPANEL_AUTO_<...> options to enable or restrict automatic
+# detection of deployment targets through UAPI (if not set, defaults below are used.)
+# - ENABLED : 'true' for multi-site / wildcard capability; otherwise single-site mode.
+# - NOMATCH : 'true' to allow deployment to sites that do not match the certificate.
+# - INCLUDE : Comma-separated list - sites must match this field.
+# - EXCLUDE : Comma-separated list - sites must NOT match this field.
+# INCLUDE/EXCLUDE both support non-lexical, glob-style matches using '*'
+#
 # 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
+#
+# export DEPLOY_CPANEL_USER=myusername
+# export DEPLOY_CPANEL_AUTO_ENABLED='true'
+# export DEPLOY_CPANEL_AUTO_NOMATCH='false'
+# export DEPLOY_CPANEL_AUTO_INCLUDE='*'
+# export DEPLOY_CPANEL_AUTO_EXCLUDE=''
 
 ########  Public functions #####################
 
 #domain keyfile certfile cafile fullchain
-
 cpanel_uapi_deploy() {
   _cdomain="$1"
   _ckey="$2"
@@ -22,6 +33,9 @@ cpanel_uapi_deploy() {
   _cca="$4"
   _cfullchain="$5"
 
+  # re-declare vars inherited from acme.sh but not passed to make ShellCheck happy
+  : "${Le_Alt:=""}"
+
   _debug _cdomain "$_cdomain"
   _debug _ckey "$_ckey"
   _debug _ccert "$_ccert"
@@ -32,31 +46,166 @@ cpanel_uapi_deploy() {
     _err "The command uapi is not found."
     return 1
   fi
+
+  # declare useful constants
+  uapi_error_response='status: 0'
+
   # read cert and key files and urlencode both
   _cert=$(_url_encode <"$_ccert")
   _key=$(_url_encode <"$_ckey")
 
-  _debug _cert "$_cert"
-  _debug _key "$_key"
+  _debug2 _cert "$_cert"
+  _debug2 _key "$_key"
 
   if [ "$(id -u)" = 0 ]; then
-    if [ -z "$DEPLOY_CPANEL_USER" ]; then
+    _getdeployconf DEPLOY_CPANEL_USER
+    # fallback to _readdomainconf for old installs
+    if [ -z "${DEPLOY_CPANEL_USER:=$(_readdomainconf DEPLOY_CPANEL_USER)}" ]; then
       _err "It seems that you are root, please define the target user name: export DEPLOY_CPANEL_USER=username"
       return 1
     fi
-    _savedomainconf DEPLOY_CPANEL_USER "$DEPLOY_CPANEL_USER"
-    _response=$(uapi --user="$DEPLOY_CPANEL_USER" SSL install_ssl domain="$_cdomain" cert="$_cert" key="$_key")
-  else
-    _response=$(uapi SSL install_ssl domain="$_cdomain" cert="$_cert" key="$_key")
+    _debug DEPLOY_CPANEL_USER "$DEPLOY_CPANEL_USER"
+    _savedeployconf DEPLOY_CPANEL_USER "$DEPLOY_CPANEL_USER"
+
+    _uapi_user="$DEPLOY_CPANEL_USER"
   fi
-  error_response="status: 0"
-  if test "${_response#*$error_response}" != "$_response"; then
-    _err "Error in deploying certificate:"
-    _err "$_response"
-    return 1
+
+  # Load all AUTO envars and set defaults - see above for usage
+  __cpanel_initautoparam ENABLED 'true'
+  __cpanel_initautoparam NOMATCH 'false'
+  __cpanel_initautoparam INCLUDE '*'
+  __cpanel_initautoparam EXCLUDE ''
+
+  # Auto mode
+  if [ "$DEPLOY_CPANEL_AUTO_ENABLED" = "true" ]; then
+    # call API for site config
+    _response=$(uapi DomainInfo list_domains)
+    # exit if error in response
+    if [ -z "$_response" ] || [ "${_response#*"$uapi_error_response"}" != "$_response" ]; then
+      _err "Error in deploying certificate - cannot retrieve sitelist:"
+      _err "\n$_response"
+      return 1
+    fi
+
+    # parse response to create site list
+    sitelist=$(__cpanel_parse_response "$_response")
+    _debug "UAPI sites found: $sitelist"
+
+    # filter sitelist using configured domains
+    # skip if NOMATCH is "true"
+    if [ "$DEPLOY_CPANEL_AUTO_NOMATCH" = "true" ]; then
+      _debug "DEPLOY_CPANEL_AUTO_NOMATCH is true"
+      _info "UAPI nomatch mode is enabled - Will not validate sites are valid for the certificate"
+    else
+      _debug "DEPLOY_CPANEL_AUTO_NOMATCH is false"
+      d="$(echo "${Le_Alt}," | sed -e "s/^$_cdomain,//" -e "s/,$_cdomain,/,/")"
+      d="$(echo "$_cdomain,$d" | tr ',' '\n' | sed -e 's/\./\\./g' -e 's/\*/\[\^\.\]\*/g')"
+      sitelist="$(echo "$sitelist" | grep -ix "$d")"
+      _debug2 "Matched UAPI sites: $sitelist"
+    fi
+
+    # filter sites that do not match $DEPLOY_CPANEL_AUTO_INCLUDE
+    _info "Applying sitelist filter DEPLOY_CPANEL_AUTO_INCLUDE: $DEPLOY_CPANEL_AUTO_INCLUDE"
+    sitelist="$(echo "$sitelist" | grep -ix "$(echo "$DEPLOY_CPANEL_AUTO_INCLUDE" | tr ',' '\n' | sed -e 's/\./\\./g' -e 's/\*/\.\*/g')")"
+    _debug2 "Remaining sites: $sitelist"
+
+    # filter sites that match $DEPLOY_CPANEL_AUTO_EXCLUDE
+    _info "Applying sitelist filter DEPLOY_CPANEL_AUTO_EXCLUDE: $DEPLOY_CPANEL_AUTO_EXCLUDE"
+    sitelist="$(echo "$sitelist" | grep -vix "$(echo "$DEPLOY_CPANEL_AUTO_EXCLUDE" | tr ',' '\n' | sed -e 's/\./\\./g' -e 's/\*/\.\*/g')")"
+    _debug2 "Remaining sites: $sitelist"
+
+    # counter for success / failure check
+    successes=0
+    if [ -n "$sitelist" ]; then
+      sitetotal="$(echo "$sitelist" | wc -l)"
+      _debug "$sitetotal sites to deploy"
+    else
+      sitetotal=0
+      _debug "No sites to deploy"
+    fi
+
+    # for each site: call uapi to publish cert and log result. Only return failure if all fail
+    for site in $sitelist; do
+      # call uapi to publish cert, check response for errors and log them.
+      if [ -n "$_uapi_user" ]; then
+        _response=$(uapi --user="$_uapi_user" SSL install_ssl domain="$site" cert="$_cert" key="$_key")
+      else
+        _response=$(uapi SSL install_ssl domain="$site" cert="$_cert" key="$_key")
+      fi
+      if [ "${_response#*"$uapi_error_response"}" != "$_response" ]; then
+        _err "Error in deploying certificate to $site:"
+        _err "$_response"
+      else
+        successes=$((successes + 1))
+        _debug "$_response"
+        _info "Succcessfully deployed to $site"
+      fi
+    done
+
+    # Raise error if all updates fail
+    if [ "$sitetotal" -gt 0 ] && [ "$successes" -eq 0 ]; then
+      _err "Could not deploy to any of $sitetotal sites via UAPI"
+      _debug "successes: $successes, sitetotal: $sitetotal"
+      return 1
+    fi
+
+    _info "Successfully deployed certificate to $successes of $sitetotal sites via UAPI"
+    return 0
+  else
+    # "classic" mode - will only try to deploy to the primary domain; will not check UAPI first
+    if [ -n "$_uapi_user" ]; then
+      _response=$(uapi --user="$_uapi_user" SSL install_ssl domain="$_cdomain" cert="$_cert" key="$_key")
+    else
+      _response=$(uapi SSL install_ssl domain="$_cdomain" cert="$_cert" key="$_key")
+    fi
+
+    if [ "${_response#*"$uapi_error_response"}" != "$_response" ]; then
+      _err "Error in deploying certificate:"
+      _err "$_response"
+      return 1
+    fi
+
+    _debug response "$_response"
+    _info "Certificate successfully deployed"
+    return 0
   fi
+}
+
+########  Private functions #####################
+
+# Internal utility to process YML from UAPI - only looks at main_domain and sub_domains
+#[response]
+__cpanel_parse_response() {
+  if [ $# -gt 0 ]; then resp="$*"; else resp="$(cat)"; fi
+
+  echo "$resp" |
+    sed -En \
+      -e 's/\r$//' \
+      -e 's/^( *)([_.[:alnum:]]+) *: *(.*)/\1,\2,\3/p' \
+      -e 's/^( *)- (.*)/\1,-,\2/p' |
+    awk -F, '{
+      level = length($1)/2;
+      section[level] = $2;
+      for (i in section) {if (i > level) {delete section[i]}}
+      if (length($3) > 0) {
+        prefix="";
+        for (i=0; i < level; i++)
+          { prefix = (prefix)(section[i])("/") }
+        printf("%s%s=%s\n", prefix, $2, $3);
+      }
+    }' |
+    sed -En -e 's/^result\/data\/(main_domain|sub_domains\/-)=(.*)$/\2/p'
+}
+
+# Load parameter by prefix+name - fallback to default if not set, and save to config
+#pname pdefault
+__cpanel_initautoparam() {
+  pname="$1"
+  pdefault="$2"
+  pkey="DEPLOY_CPANEL_AUTO_$pname"
 
-  _debug response "$_response"
-  _info "Certificate successfully deployed"
-  return 0
+  _getdeployconf "$pkey"
+  [ -n "$(eval echo "\"\$$pkey\"")" ] || eval "$pkey=\"$pdefault\""
+  _debug2 "$pkey" "$(eval echo "\"\$$pkey\"")"
+  _savedeployconf "$pkey" "$(eval echo "\"\$$pkey\"")"
 }

+ 3 - 2
deploy/docker.sh

@@ -8,7 +8,7 @@
 #DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE="/path/to/fullchain.pem"
 #DEPLOY_DOCKER_CONTAINER_RELOAD_CMD="service nginx force-reload"
 
-_DEPLOY_DOCKER_WIKI="https://github.com/Neilpang/acme.sh/wiki/deploy-to-docker-containers"
+_DEPLOY_DOCKER_WIKI="https://github.com/acmesh-official/acme.sh/wiki/deploy-to-docker-containers"
 
 _DOCKER_HOST_DEFAULT="/var/run/docker.sock"
 
@@ -91,7 +91,7 @@ docker_deploy() {
   _getdeployconf DEPLOY_DOCKER_CONTAINER_RELOAD_CMD
   _debug2 DEPLOY_DOCKER_CONTAINER_RELOAD_CMD "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD"
   if [ "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD" ]; then
-    _savedeployconf DEPLOY_DOCKER_CONTAINER_RELOAD_CMD "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD"
+    _savedeployconf DEPLOY_DOCKER_CONTAINER_RELOAD_CMD "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD" "base64"
   fi
 
   _cid="$(_get_id "$DEPLOY_DOCKER_CONTAINER_LABEL")"
@@ -275,6 +275,7 @@ _check_curl_version() {
 
   if [ "$_major$_minor" -lt "740" ]; then
     _err "curl v$_cversion doesn't support unit socket"
+    _err "Please upgrade to curl 7.40 or later."
     return 1
   fi
   if [ "$_major$_minor" -lt "750" ]; then

+ 2 - 2
deploy/exim4.sh

@@ -69,8 +69,8 @@ exim4_deploy() {
     cp "$_exim4_conf" "$_backup_conf"
 
     _info "Modify exim4 conf: $_exim4_conf"
-    if _setopt "$_exim4_conf" "tls_certificate" "=" "$_real_fullchain" \
-      && _setopt "$_exim4_conf" "tls_privatekey" "=" "$_real_key"; then
+    if _setopt "$_exim4_conf" "tls_certificate" "=" "$_real_fullchain" &&
+      _setopt "$_exim4_conf" "tls_privatekey" "=" "$_real_key"; then
       _info "Set config success!"
     else
       _err "Config exim4 server error, please report bug to us."

+ 32 - 20
deploy/fritzbox.sh

@@ -28,47 +28,59 @@ fritzbox_deploy() {
   _debug _cfullchain "$_cfullchain"
 
   if ! _exists iconv; then
-    if ! _exists perl; then
-      _err "iconv or perl not found"
-      return 1
+    if ! _exists uconv; then
+      if ! _exists perl; then
+        _err "iconv or uconv or perl not found"
+        return 1
+      fi
     fi
   fi
 
-  _fritzbox_username="${DEPLOY_FRITZBOX_USERNAME}"
-  _fritzbox_password="${DEPLOY_FRITZBOX_PASSWORD}"
-  _fritzbox_url="${DEPLOY_FRITZBOX_URL}"
+  # Clear traces of incorrectly stored values
+  _clearaccountconf DEPLOY_FRITZBOX_USERNAME
+  _clearaccountconf DEPLOY_FRITZBOX_PASSWORD
+  _clearaccountconf DEPLOY_FRITZBOX_URL
 
-  _debug _fritzbox_url "$_fritzbox_url"
-  _debug _fritzbox_username "$_fritzbox_username"
-  _secure_debug _fritzbox_password "$_fritzbox_password"
-  if [ -z "$_fritzbox_username" ]; then
+  # Read config from saved values or env
+  _getdeployconf DEPLOY_FRITZBOX_USERNAME
+  _getdeployconf DEPLOY_FRITZBOX_PASSWORD
+  _getdeployconf DEPLOY_FRITZBOX_URL
+
+  _debug DEPLOY_FRITZBOX_URL "$DEPLOY_FRITZBOX_URL"
+  _debug DEPLOY_FRITZBOX_USERNAME "$DEPLOY_FRITZBOX_USERNAME"
+  _secure_debug DEPLOY_FRITZBOX_PASSWORD "$DEPLOY_FRITZBOX_PASSWORD"
+
+  if [ -z "$DEPLOY_FRITZBOX_USERNAME" ]; then
     _err "FRITZ!Box username is not found, please define DEPLOY_FRITZBOX_USERNAME."
     return 1
   fi
-  if [ -z "$_fritzbox_password" ]; then
+  if [ -z "$DEPLOY_FRITZBOX_PASSWORD" ]; then
     _err "FRITZ!Box password is not found, please define DEPLOY_FRITZBOX_PASSWORD."
     return 1
   fi
-  if [ -z "$_fritzbox_url" ]; then
+  if [ -z "$DEPLOY_FRITZBOX_URL" ]; then
     _err "FRITZ!Box url is not found, please define DEPLOY_FRITZBOX_URL."
     return 1
   fi
 
-  _saveaccountconf DEPLOY_FRITZBOX_USERNAME "${_fritzbox_username}"
-  _saveaccountconf DEPLOY_FRITZBOX_PASSWORD "${_fritzbox_password}"
-  _saveaccountconf DEPLOY_FRITZBOX_URL "${_fritzbox_url}"
+  # Save current values
+  _savedeployconf DEPLOY_FRITZBOX_USERNAME "$DEPLOY_FRITZBOX_USERNAME"
+  _savedeployconf DEPLOY_FRITZBOX_PASSWORD "$DEPLOY_FRITZBOX_PASSWORD"
+  _savedeployconf DEPLOY_FRITZBOX_URL "$DEPLOY_FRITZBOX_URL"
 
   # Do not check for a valid SSL certificate, because initially the cert is not valid, so it could not install the LE generated certificate
   export HTTPS_INSECURE=1
 
   _info "Log in to the FRITZ!Box"
-  _fritzbox_challenge="$(_get "${_fritzbox_url}/login_sid.lua" | sed -e 's/^.*<Challenge>//' -e 's/<\/Challenge>.*$//')"
+  _fritzbox_challenge="$(_get "${DEPLOY_FRITZBOX_URL}/login_sid.lua" | sed -e 's/^.*<Challenge>//' -e 's/<\/Challenge>.*$//')"
   if _exists iconv; then
-    _fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${_fritzbox_password}" | iconv -f ASCII -t UTF16LE | md5sum | awk '{print $1}')"
+    _fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | iconv -f ASCII -t UTF16LE | _digest md5 hex)"
+  elif _exists uconv; then
+    _fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | uconv -f ASCII -t UTF16LE | _digest md5 hex)"
   else
-    _fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${_fritzbox_password}" | perl -p -e 'use Encode qw/encode/; print encode("UTF-16LE","$_"); $_="";' | md5sum | awk '{print $1}')"
+    _fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | perl -p -e 'use Encode qw/encode/; print encode("UTF-16LE","$_"); $_="";' | _digest md5 hex)"
   fi
-  _fritzbox_sid="$(_get "${_fritzbox_url}/login_sid.lua?sid=0000000000000000&username=${_fritzbox_username}&response=${_fritzbox_challenge}-${_fritzbox_hash}" | sed -e 's/^.*<SID>//' -e 's/<\/SID>.*$//')"
+  _fritzbox_sid="$(_get "${DEPLOY_FRITZBOX_URL}/login_sid.lua?sid=0000000000000000&username=${DEPLOY_FRITZBOX_USERNAME}&response=${_fritzbox_challenge}-${_fritzbox_hash}" | sed -e 's/^.*<SID>//' -e 's/<\/SID>.*$//')"
 
   if [ -z "${_fritzbox_sid}" ] || [ "${_fritzbox_sid}" = "0000000000000000" ]; then
     _err "Logging in to the FRITZ!Box failed. Please check username, password and URL."
@@ -100,7 +112,7 @@ fritzbox_deploy() {
   _info "Upload certificate to the FRITZ!Box"
 
   export _H1="Content-type: multipart/form-data boundary=${_post_boundary}"
-  _post "$(cat "${_post_request}")" "${_fritzbox_url}/cgi-bin/firmwarecfg" | grep SSL
+  _post "$(cat "${_post_request}")" "${DEPLOY_FRITZBOX_URL}/cgi-bin/firmwarecfg" | grep SSL
 
   retval=$?
   if [ $retval = 0 ]; then

+ 11 - 8
deploy/gcore_cdn.sh

@@ -56,9 +56,9 @@ gcore_cdn_deploy() {
   _request="{\"username\":\"$Le_Deploy_gcore_cdn_username\",\"password\":\"$Le_Deploy_gcore_cdn_password\"}"
   _debug _request "$_request"
   export _H1="Content-Type:application/json"
-  _response=$(_post "$_request" "https://api.gcdn.co/auth/signin")
+  _response=$(_post "$_request" "https://api.gcdn.co/auth/jwt/login")
   _debug _response "$_response"
-  _regex=".*\"token\":\"\([-._0-9A-Za-z]*\)\".*$"
+  _regex=".*\"access\":\"\([-._0-9A-Za-z]*\)\".*$"
   _debug _regex "$_regex"
   _token=$(echo "$_response" | sed -n "s/$_regex/\1/p")
   _debug _token "$_token"
@@ -72,20 +72,23 @@ gcore_cdn_deploy() {
   export _H2="Authorization:Token $_token"
   _response=$(_get "https://api.gcdn.co/resources")
   _debug _response "$_response"
-  _regex=".*(\"id\".*?\"cname\":\"$_cdomain\".*?})"
+  _regex="\"primary_resource\":null},"
+  _debug _regex "$_regex"
+  _response=$(echo "$_response" | sed "s/$_regex/$_regex\n/g")
+  _debug _response "$_response"
   _regex="^.*\"cname\":\"$_cdomain\".*$"
   _debug _regex "$_regex"
-  _resource=$(echo "$_response" | sed 's/},{/},\n{/g' | _egrep_o "$_regex")
+  _resource=$(echo "$_response" | _egrep_o "$_regex")
   _debug _resource "$_resource"
-  _regex=".*\"id\":\([0-9]*\),.*$"
+  _regex=".*\"id\":\([0-9]*\).*$"
   _debug _regex "$_regex"
   _resourceId=$(echo "$_resource" | sed -n "s/$_regex/\1/p")
   _debug _resourceId "$_resourceId"
-  _regex=".*\"sslData\":\([0-9]*\)}.*$"
+  _regex=".*\"sslData\":\([0-9]*\).*$"
   _debug _regex "$_regex"
   _sslDataOld=$(echo "$_resource" | sed -n "s/$_regex/\1/p")
   _debug _sslDataOld "$_sslDataOld"
-  _regex=".*\"originGroup\":\([0-9]*\),.*$"
+  _regex=".*\"originGroup\":\([0-9]*\).*$"
   _debug _regex "$_regex"
   _originGroup=$(echo "$_resource" | sed -n "s/$_regex/\1/p")
   _debug _originGroup "$_originGroup"
@@ -101,7 +104,7 @@ gcore_cdn_deploy() {
   _debug _request "$_request"
   _response=$(_post "$_request" "https://api.gcdn.co/sslData")
   _debug _response "$_response"
-  _regex=".*\"id\":\([0-9]*\),.*$"
+  _regex=".*\"id\":\([0-9]*\).*$"
   _debug _regex "$_regex"
   _sslDataAdd=$(echo "$_response" | sed -n "s/$_regex/\1/p")
   _debug _sslDataAdd "$_sslDataAdd"

+ 40 - 32
deploy/haproxy.sh

@@ -54,11 +54,6 @@ haproxy_deploy() {
   DEPLOY_HAPROXY_ISSUER_DEFAULT="no"
   DEPLOY_HAPROXY_RELOAD_DEFAULT="true"
 
-  if [ -f "${DOMAIN_CONF}" ]; then
-    # shellcheck disable=SC1090
-    . "${DOMAIN_CONF}"
-  fi
-
   _debug _cdomain "${_cdomain}"
   _debug _ckey "${_ckey}"
   _debug _ccert "${_ccert}"
@@ -66,6 +61,8 @@ haproxy_deploy() {
   _debug _cfullchain "${_cfullchain}"
 
   # PEM_PATH is optional. If not provided then assume "${DEPLOY_HAPROXY_PEM_PATH_DEFAULT}"
+  _getdeployconf DEPLOY_HAPROXY_PEM_PATH
+  _debug2 DEPLOY_HAPROXY_PEM_PATH "${DEPLOY_HAPROXY_PEM_PATH}"
   if [ -n "${DEPLOY_HAPROXY_PEM_PATH}" ]; then
     Le_Deploy_haproxy_pem_path="${DEPLOY_HAPROXY_PEM_PATH}"
     _savedomainconf Le_Deploy_haproxy_pem_path "${Le_Deploy_haproxy_pem_path}"
@@ -82,6 +79,8 @@ haproxy_deploy() {
   fi
 
   # PEM_NAME is optional. If not provided then assume "${DEPLOY_HAPROXY_PEM_NAME_DEFAULT}"
+  _getdeployconf DEPLOY_HAPROXY_PEM_NAME
+  _debug2 DEPLOY_HAPROXY_PEM_NAME "${DEPLOY_HAPROXY_PEM_NAME}"
   if [ -n "${DEPLOY_HAPROXY_PEM_NAME}" ]; then
     Le_Deploy_haproxy_pem_name="${DEPLOY_HAPROXY_PEM_NAME}"
     _savedomainconf Le_Deploy_haproxy_pem_name "${Le_Deploy_haproxy_pem_name}"
@@ -90,6 +89,8 @@ haproxy_deploy() {
   fi
 
   # BUNDLE is optional. If not provided then assume "${DEPLOY_HAPROXY_BUNDLE_DEFAULT}"
+  _getdeployconf DEPLOY_HAPROXY_BUNDLE
+  _debug2 DEPLOY_HAPROXY_BUNDLE "${DEPLOY_HAPROXY_BUNDLE}"
   if [ -n "${DEPLOY_HAPROXY_BUNDLE}" ]; then
     Le_Deploy_haproxy_bundle="${DEPLOY_HAPROXY_BUNDLE}"
     _savedomainconf Le_Deploy_haproxy_bundle "${Le_Deploy_haproxy_bundle}"
@@ -98,6 +99,8 @@ haproxy_deploy() {
   fi
 
   # ISSUER is optional. If not provided then assume "${DEPLOY_HAPROXY_ISSUER_DEFAULT}"
+  _getdeployconf DEPLOY_HAPROXY_ISSUER
+  _debug2 DEPLOY_HAPROXY_ISSUER "${DEPLOY_HAPROXY_ISSUER}"
   if [ -n "${DEPLOY_HAPROXY_ISSUER}" ]; then
     Le_Deploy_haproxy_issuer="${DEPLOY_HAPROXY_ISSUER}"
     _savedomainconf Le_Deploy_haproxy_issuer "${Le_Deploy_haproxy_issuer}"
@@ -106,6 +109,8 @@ haproxy_deploy() {
   fi
 
   # RELOAD is optional. If not provided then assume "${DEPLOY_HAPROXY_RELOAD_DEFAULT}"
+  _getdeployconf DEPLOY_HAPROXY_RELOAD
+  _debug2 DEPLOY_HAPROXY_RELOAD "${DEPLOY_HAPROXY_RELOAD}"
   if [ -n "${DEPLOY_HAPROXY_RELOAD}" ]; then
     Le_Deploy_haproxy_reload="${DEPLOY_HAPROXY_RELOAD}"
     _savedomainconf Le_Deploy_haproxy_reload "${Le_Deploy_haproxy_reload}"
@@ -190,7 +195,7 @@ haproxy_deploy() {
     _info "Updating OCSP stapling info"
     _debug _ocsp "${_ocsp}"
     _info "Extracting OCSP URL"
-    _ocsp_url=$(openssl x509 -noout -ocsp_uri -in "${_pem}")
+    _ocsp_url=$(${ACME_OPENSSL_BIN:-openssl} x509 -noout -ocsp_uri -in "${_pem}")
     _debug _ocsp_url "${_ocsp_url}"
 
     # Only process OCSP if URL was present
@@ -203,38 +208,41 @@ haproxy_deploy() {
       # Only process the certificate if we have a .issuer file
       if [ -r "${_issuer}" ]; then
         # Check if issuer cert is also a root CA cert
-        _subjectdn=$(openssl x509 -in "${_issuer}" -subject -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10)
+        _subjectdn=$(${ACME_OPENSSL_BIN:-openssl} x509 -in "${_issuer}" -subject -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10)
         _debug _subjectdn "${_subjectdn}"
-        _issuerdn=$(openssl x509 -in "${_issuer}" -issuer -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10)
+        _issuerdn=$(${ACME_OPENSSL_BIN:-openssl} x509 -in "${_issuer}" -issuer -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10)
         _debug _issuerdn "${_issuerdn}"
         _info "Requesting OCSP response"
-        # Request the OCSP response from the issuer and store it
+        # If the issuer is a CA cert then our command line has "-CAfile" added
         if [ "${_subjectdn}" = "${_issuerdn}" ]; then
-          # If the issuer is a CA cert then our command line has "-CAfile" added
-          openssl ocsp \
-            -issuer "${_issuer}" \
-            -cert "${_pem}" \
-            -url "${_ocsp_url}" \
-            -header Host "${_ocsp_host}" \
-            -respout "${_ocsp}" \
-            -verify_other "${_issuer}" \
-            -no_nonce \
-            -CAfile "${_issuer}" \
-            | grep -q "${_pem}: good"
-          _ret=$?
+          _cafile_argument="-CAfile \"${_issuer}\""
         else
-          # Issuer is not a root CA so no "-CAfile" option
-          openssl ocsp \
-            -issuer "${_issuer}" \
-            -cert "${_pem}" \
-            -url "${_ocsp_url}" \
-            -header Host "${_ocsp_host}" \
-            -respout "${_ocsp}" \
-            -verify_other "${_issuer}" \
-            -no_nonce \
-            | grep -q "${_pem}: good"
-          _ret=$?
+          _cafile_argument=""
         fi
+        _debug _cafile_argument "${_cafile_argument}"
+        # if OpenSSL/LibreSSL is v1.1 or above, the format for the -header option has changed
+        _openssl_version=$(${ACME_OPENSSL_BIN:-openssl} version | cut -d' ' -f2)
+        _debug _openssl_version "${_openssl_version}"
+        _openssl_major=$(echo "${_openssl_version}" | cut -d '.' -f1)
+        _openssl_minor=$(echo "${_openssl_version}" | cut -d '.' -f2)
+        if [ "${_openssl_major}" -eq "1" ] && [ "${_openssl_minor}" -ge "1" ] || [ "${_openssl_major}" -ge "2" ]; then
+          _header_sep="="
+        else
+          _header_sep=" "
+        fi
+        # Request the OCSP response from the issuer and store it
+        _openssl_ocsp_cmd="${ACME_OPENSSL_BIN:-openssl} ocsp \
+          -issuer \"${_issuer}\" \
+          -cert \"${_pem}\" \
+          -url \"${_ocsp_url}\" \
+          -header Host${_header_sep}\"${_ocsp_host}\" \
+          -respout \"${_ocsp}\" \
+          -verify_other \"${_issuer}\" \
+          ${_cafile_argument} \
+          | grep -q \"${_pem}: good\""
+        _debug _openssl_ocsp_cmd "${_openssl_ocsp_cmd}"
+        eval "${_openssl_ocsp_cmd}"
+        _ret=$?
       else
         # Non fatal: No issuer file was present so no OCSP stapling file created
         _err "OCSP stapling in use but no .issuer file was present"

+ 3 - 3
deploy/kong.sh

@@ -1,6 +1,6 @@
 #!/usr/bin/env sh
-# If certificate already exist it will update only cert and key not touching other parameter
-# If certificate  doesn't exist it will only upload cert and key and not set other parameter
+# If certificate already exists it will update only cert and key, not touching other parameters
+# If certificate doesn't exist it will only upload cert and key, and not set other parameters
 # Note that we deploy full chain
 # Written by Geoffroi Genot <ggenot@voxbone.com>
 
@@ -45,7 +45,7 @@ kong_deploy() {
   #Generate data for request (Multipart/form-data with mixed content)
   if [ -z "$ssl_uuid" ]; then
     #set sni to domain
-    content="--$delim${nl}Content-Disposition: form-data; name=\"snis\"${nl}${nl}$_cdomain"
+    content="--$delim${nl}Content-Disposition: form-data; name=\"snis[]\"${nl}${nl}$_cdomain"
   fi
   #add key
   content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"key\"; filename=\"$(basename "$_ckey")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")"

+ 15 - 4
deploy/mailcow.sh

@@ -20,14 +20,25 @@ mailcow_deploy() {
   _debug _cca "$_cca"
   _debug _cfullchain "$_cfullchain"
 
-  _mailcow_path="${DEPLOY_MAILCOW_PATH}"
+  _getdeployconf DEPLOY_MAILCOW_PATH
+  _getdeployconf DEPLOY_MAILCOW_RELOAD
 
-  if [ -z "$_mailcow_path" ]; then
+  _debug DEPLOY_MAILCOW_PATH "$DEPLOY_MAILCOW_PATH"
+  _debug DEPLOY_MAILCOW_RELOAD "$DEPLOY_MAILCOW_RELOAD"
+
+  if [ -z "$DEPLOY_MAILCOW_PATH" ]; then
     _err "Mailcow path is not found, please define DEPLOY_MAILCOW_PATH."
     return 1
   fi
 
-  _ssl_path="${_mailcow_path}/data/assets/ssl/"
+  _savedeployconf DEPLOY_MAILCOW_PATH "$DEPLOY_MAILCOW_PATH"
+  [ -n "$DEPLOY_MAILCOW_RELOAD" ] && _savedeployconf DEPLOY_MAILCOW_RELOAD "$DEPLOY_MAILCOW_RELOAD"
+
+  _ssl_path="$DEPLOY_MAILCOW_PATH"
+  if [ -f "$DEPLOY_MAILCOW_PATH/generate_config.sh" ]; then
+    _ssl_path="$DEPLOY_MAILCOW_PATH/data/assets/ssl/"
+  fi
+
   if [ ! -d "$_ssl_path" ]; then
     _err "Cannot find mailcow ssl path: $_ssl_path"
     return 1
@@ -46,7 +57,7 @@ mailcow_deploy() {
     return 1
   fi
 
-  DEFAULT_MAILCOW_RELOAD="cd ${_mailcow_path} && docker-compose restart postfix-mailcow dovecot-mailcow nginx-mailcow"
+  DEFAULT_MAILCOW_RELOAD="docker restart \$(docker ps --quiet --filter name=nginx-mailcow --filter name=dovecot-mailcow --filter name=postfix-mailcow)"
   _reload="${DEPLOY_MAILCOW_RELOAD:-$DEFAULT_MAILCOW_RELOAD}"
 
   _info "Run reload: $_reload"

+ 20 - 16
deploy/qiniu.sh

@@ -1,11 +1,13 @@
 #!/usr/bin/env sh
 
-# Script to create certificate to qiniu.com 
+# 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"
+# If you have more than one domain, just
+# export QINIU_CDN_DOMAIN="cdn1.example.com cdn2.example.com"
 
 QINIU_API_BASE="https://api.qiniu.com"
 
@@ -51,7 +53,7 @@ qiniu_deploy() {
   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")
+  sslcert_response=$(_post "$sslcerl_body" "$QINIU_API_BASE$sslcert_path" 0 "POST" "application/json" | _dbase64)
 
   if ! _contains "$sslcert_response" "certID"; then
     _err "Error in creating certificate:"
@@ -67,21 +69,23 @@ qiniu_deploy() {
   _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"
+  for domain in $QINIU_CDN_DOMAIN; do
+    update_path="/domain/$domain/httpsconf"
+    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)
+
+    if _contains "$update_response" "error"; then
+      _err "Error in updating domain $domain httpsconf:"
+      _err "$update_response"
+      return 1
+    fi
+
+    _debug update_response "$update_response"
+    _info "Domain $domain certificate has been deployed successfully"
+  done
 
   return 0
 }

+ 115 - 23
deploy/routeros.sh

@@ -23,6 +23,7 @@
 # ```sh
 # export ROUTER_OS_USERNAME=certuser
 # export ROUTER_OS_HOST=router.example.com
+# export ROUTER_OS_PORT=22
 #
 # acme.sh --deploy -d ftp.example.com --deploy-hook routeros
 # ```
@@ -48,6 +49,16 @@
 # One optional thing to do as well is to create a script that updates
 # all the required services and run that script in a single command.
 #
+# To adopt parameters to `scp` and/or `ssh` set the optional
+# `ROUTER_OS_SSH_CMD` and `ROUTER_OS_SCP_CMD` variables accordingly,
+# see ssh(1) and scp(1) for parameters to those commands.
+#
+# Example:
+# ```ssh
+# export ROUTER_OS_SSH_CMD="ssh -i /acme.sh/.ssh/router.example.com -o UserKnownHostsFile=/acme.sh/.ssh/known_hosts"
+# export ROUTER_OS_SCP_CMD="scp -i /acme.sh/.ssh/router.example.com -o UserKnownHostsFile=/acme.sh/.ssh/known_hosts"
+# ````
+#
 # returns 0 means success, otherwise error.
 
 ########  Public functions #####################
@@ -59,6 +70,7 @@ routeros_deploy() {
   _ccert="$3"
   _cca="$4"
   _cfullchain="$5"
+  _err_code=0
 
   _debug _cdomain "$_cdomain"
   _debug _ckey "$_ckey"
@@ -66,46 +78,126 @@ routeros_deploy() {
   _debug _cca "$_cca"
   _debug _cfullchain "$_cfullchain"
 
+  _getdeployconf ROUTER_OS_HOST
+
   if [ -z "$ROUTER_OS_HOST" ]; then
     _debug "Using _cdomain as ROUTER_OS_HOST, please set if not correct."
     ROUTER_OS_HOST="$_cdomain"
   fi
 
+  _getdeployconf ROUTER_OS_USERNAME
+
   if [ -z "$ROUTER_OS_USERNAME" ]; then
     _err "Need to set the env variable ROUTER_OS_USERNAME"
     return 1
   fi
 
+  _getdeployconf ROUTER_OS_PORT
+
+  if [ -z "$ROUTER_OS_PORT" ]; then
+    _debug "Using default port 22 as ROUTER_OS_PORT, please set if not correct."
+    ROUTER_OS_PORT=22
+  fi
+
+  _getdeployconf ROUTER_OS_SSH_CMD
+
+  if [ -z "$ROUTER_OS_SSH_CMD" ]; then
+    _debug "Use default ssh setup."
+    ROUTER_OS_SSH_CMD="ssh -p $ROUTER_OS_PORT"
+  fi
+
+  _getdeployconf ROUTER_OS_SCP_CMD
+
+  if [ -z "$ROUTER_OS_SCP_CMD" ]; then
+    _debug "USe default scp setup."
+    ROUTER_OS_SCP_CMD="scp -P $ROUTER_OS_PORT"
+  fi
+
+  _getdeployconf ROUTER_OS_ADDITIONAL_SERVICES
+
   if [ -z "$ROUTER_OS_ADDITIONAL_SERVICES" ]; then
     _debug "Not enabling additional services"
     ROUTER_OS_ADDITIONAL_SERVICES=""
   fi
 
-  _info "Trying to push key '$_ckey' to router"
-  scp "$_ckey" "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST:$_cdomain.key"
-  _info "Trying to push cert '$_cfullchain' to router"
-  scp "$_cfullchain" "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST:$_cdomain.cer"
-  DEPLOY_SCRIPT_CMD="/system script add name=\"LE Cert Deploy - $_cdomain\" owner=admin policy=ftp,read,write,password,sensitive 
-source=\"## generated by routeros deploy script in acme.sh
-\n/certificate remove [ find name=$_cdomain.cer_0 ]
-\n/certificate remove [ find name=$_cdomain.cer_1 ]
-\ndelay 1
-\n/certificate import file-name=$_cdomain.cer passphrase=\\\"\\\"
-\n/certificate import file-name=$_cdomain.key passphrase=\\\"\\\"
-\ndelay 1
-\n/file remove $_cdomain.cer
-\n/file remove $_cdomain.key
-\ndelay 2
-\n/ip service set www-ssl certificate=$_cdomain.cer_0
-\n$ROUTER_OS_ADDITIONAL_SERVICES
+  _savedeployconf ROUTER_OS_HOST "$ROUTER_OS_HOST"
+  _savedeployconf ROUTER_OS_USERNAME "$ROUTER_OS_USERNAME"
+  _savedeployconf ROUTER_OS_PORT "$ROUTER_OS_PORT"
+  _savedeployconf ROUTER_OS_SSH_CMD "$ROUTER_OS_SSH_CMD"
+  _savedeployconf ROUTER_OS_SCP_CMD "$ROUTER_OS_SCP_CMD"
+  _savedeployconf ROUTER_OS_ADDITIONAL_SERVICES "$ROUTER_OS_ADDITIONAL_SERVICES"
+
+  # push key to routeros
+  if ! _scp_certificate "$_ckey" "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST:$_cdomain.key"; then
+    return $_err_code
+  fi
+
+  # push certificate chain to routeros
+  if ! _scp_certificate "$_cfullchain" "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST:$_cdomain.cer"; then
+    return $_err_code
+  fi
+
+  DEPLOY_SCRIPT_CMD="/system script add name=\"LE Cert Deploy - $_cdomain\" owner=$ROUTER_OS_USERNAME \
+comment=\"generated by routeros deploy script in acme.sh\" \
+source=\"/certificate remove [ find name=$_cdomain.cer_0 ];\
+\n/certificate remove [ find name=$_cdomain.cer_1 ];\
+\n/certificate remove [ find name=$_cdomain.cer_2 ];\
+\ndelay 1;\
+\n/certificate import file-name=$_cdomain.cer passphrase=\\\"\\\";\
+\n/certificate import file-name=$_cdomain.key passphrase=\\\"\\\";\
+\ndelay 1;\
+\n/file remove $_cdomain.cer;\
+\n/file remove $_cdomain.key;\
+\ndelay 2;\
+\n/ip service set www-ssl certificate=$_cdomain.cer_0;\
+\n$ROUTER_OS_ADDITIONAL_SERVICES;\
 \n\"
 "
-  # shellcheck disable=SC2029
-  ssh "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST" "$DEPLOY_SCRIPT_CMD"
-  # shellcheck disable=SC2029
-  ssh "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST" "/system script run \"LE Cert Deploy - $_cdomain\""
-  # shellcheck disable=SC2029
-  ssh "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST" "/system script remove \"LE Cert Deploy - $_cdomain\""
+
+  if ! _ssh_remote_cmd "$DEPLOY_SCRIPT_CMD"; then
+    return $_err_code
+  fi
+
+  if ! _ssh_remote_cmd "/system script run \"LE Cert Deploy - $_cdomain\""; then
+    return $_err_code
+  fi
+
+  if ! _ssh_remote_cmd "/system script remove \"LE Cert Deploy - $_cdomain\""; then
+    return $_err_code
+  fi
 
   return 0
 }
+
+# inspired by deploy/ssh.sh
+_ssh_remote_cmd() {
+  _cmd="$1"
+  _secure_debug "Remote commands to execute: $_cmd"
+  _info "Submitting sequence of commands to routeros"
+  # quotations in bash cmd below intended.  Squash travis spellcheck error
+  # shellcheck disable=SC2029
+  $ROUTER_OS_SSH_CMD "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST" "$_cmd"
+  _err_code="$?"
+
+  if [ "$_err_code" != "0" ]; then
+    _err "Error code $_err_code returned from routeros"
+  fi
+
+  return $_err_code
+}
+
+_scp_certificate() {
+  _src="$1"
+  _dst="$2"
+  _secure_debug "scp '$_src' to '$_dst'"
+  _info "Push key '$_src' to routeros"
+
+  $ROUTER_OS_SCP_CMD "$_src" "$_dst"
+  _err_code="$?"
+
+  if [ "$_err_code" != "0" ]; then
+    _err "Error code $_err_code returned from scp"
+  fi
+
+  return $_err_code
+}

+ 137 - 39
deploy/ssh.sh

@@ -12,7 +12,7 @@
 # Only a username is required.  All others are optional.
 #
 # The following examples are for QNAP NAS running QTS 4.2
-# export DEPLOY_SSH_CMD=""  # defaults to ssh
+# export DEPLOY_SSH_CMD=""  # defaults to "ssh -T"
 # export DEPLOY_SSH_USER="admin"  # required
 # export DEPLOY_SSH_SERVER="qnap"  # defaults to domain name
 # export DEPLOY_SSH_KEYFILE="/etc/stunnel/stunnel.pem"
@@ -20,7 +20,9 @@
 # export DEPLOY_SSH_CAFILE="/etc/stunnel/uca.pem"
 # export DEPLOY_SSH_FULLCHAIN=""
 # export DEPLOY_SSH_REMOTE_CMD="/etc/init.d/stunnel.sh restart"
-# export DEPLOY_SSH_BACKUP=""  # yes or no, default to yes
+# export DEPLOY_SSH_BACKUP=""  # yes or no, default to yes or previously saved value
+# export DEPLOY_SSH_BACKUP_PATH=".acme_ssh_deploy"  # path on remote system. Defaults to .acme_ssh_deploy
+# export DEPLOY_SSH_MULTI_CALL=""  # yes or no, default to no or previously saved value
 #
 ########  Public functions #####################
 
@@ -31,15 +33,7 @@ ssh_deploy() {
   _ccert="$3"
   _cca="$4"
   _cfullchain="$5"
-  _cmdstr=""
-  _homedir='~'
-  _backupprefix="$_homedir/.acme_ssh_deploy/$_cdomain-backup"
-  _backupdir="$_backupprefix-$(_utc_date | tr ' ' '-')"
-
-  if [ -f "$DOMAIN_CONF" ]; then
-    # shellcheck disable=SC1090
-    . "$DOMAIN_CONF"
-  fi
+  _deploy_ssh_servers=""
 
   _debug _cdomain "$_cdomain"
   _debug _ckey "$_ckey"
@@ -48,6 +42,8 @@ ssh_deploy() {
   _debug _cfullchain "$_cfullchain"
 
   # USER is required to login by SSH to remote host.
+  _getdeployconf DEPLOY_SSH_USER
+  _debug2 DEPLOY_SSH_USER "$DEPLOY_SSH_USER"
   if [ -z "$DEPLOY_SSH_USER" ]; then
     if [ -z "$Le_Deploy_ssh_user" ]; then
       _err "DEPLOY_SSH_USER not defined."
@@ -59,6 +55,8 @@ ssh_deploy() {
   fi
 
   # SERVER is optional. If not provided then use _cdomain
+  _getdeployconf DEPLOY_SSH_SERVER
+  _debug2 DEPLOY_SSH_SERVER "$DEPLOY_SSH_SERVER"
   if [ -n "$DEPLOY_SSH_SERVER" ]; then
     Le_Deploy_ssh_server="$DEPLOY_SSH_SERVER"
     _savedomainconf Le_Deploy_ssh_server "$Le_Deploy_ssh_server"
@@ -67,25 +65,91 @@ ssh_deploy() {
   fi
 
   # CMD is optional. If not provided then use ssh
+  _getdeployconf DEPLOY_SSH_CMD
+  _debug2 DEPLOY_SSH_CMD "$DEPLOY_SSH_CMD"
   if [ -n "$DEPLOY_SSH_CMD" ]; then
     Le_Deploy_ssh_cmd="$DEPLOY_SSH_CMD"
     _savedomainconf Le_Deploy_ssh_cmd "$Le_Deploy_ssh_cmd"
   elif [ -z "$Le_Deploy_ssh_cmd" ]; then
-    Le_Deploy_ssh_cmd="ssh"
+    Le_Deploy_ssh_cmd="ssh -T"
   fi
 
-  # BACKUP is optional. If not provided then default to yes
+  # BACKUP is optional. If not provided then default to previously saved value or yes.
+  _getdeployconf DEPLOY_SSH_BACKUP
+  _debug2 DEPLOY_SSH_BACKUP "$DEPLOY_SSH_BACKUP"
   if [ "$DEPLOY_SSH_BACKUP" = "no" ]; then
     Le_Deploy_ssh_backup="no"
-  elif [ -z "$Le_Deploy_ssh_backup" ]; then
+  elif [ -z "$Le_Deploy_ssh_backup" ] || [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
     Le_Deploy_ssh_backup="yes"
   fi
   _savedomainconf Le_Deploy_ssh_backup "$Le_Deploy_ssh_backup"
 
+  # BACKUP_PATH is optional. If not provided then default to previously saved value or .acme_ssh_deploy
+  _getdeployconf DEPLOY_SSH_BACKUP_PATH
+  _debug2 DEPLOY_SSH_BACKUP_PATH "$DEPLOY_SSH_BACKUP_PATH"
+  if [ -n "$DEPLOY_SSH_BACKUP_PATH" ]; then
+    Le_Deploy_ssh_backup_path="$DEPLOY_SSH_BACKUP_PATH"
+  elif [ -z "$Le_Deploy_ssh_backup_path" ]; then
+    Le_Deploy_ssh_backup_path=".acme_ssh_deploy"
+  fi
+  _savedomainconf Le_Deploy_ssh_backup_path "$Le_Deploy_ssh_backup_path"
+
+  # MULTI_CALL is optional. If not provided then default to previously saved
+  # value (which may be undefined... equivalent to "no").
+  _getdeployconf DEPLOY_SSH_MULTI_CALL
+  _debug2 DEPLOY_SSH_MULTI_CALL "$DEPLOY_SSH_MULTI_CALL"
+  if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
+    Le_Deploy_ssh_multi_call="yes"
+    _savedomainconf Le_Deploy_ssh_multi_call "$Le_Deploy_ssh_multi_call"
+  elif [ "$DEPLOY_SSH_MULTI_CALL" = "no" ]; then
+    Le_Deploy_ssh_multi_call=""
+    _cleardomainconf Le_Deploy_ssh_multi_call
+  fi
+
+  _deploy_ssh_servers=$Le_Deploy_ssh_server
+  for Le_Deploy_ssh_server in $_deploy_ssh_servers; do
+    _ssh_deploy
+  done
+}
+
+_ssh_deploy() {
+  _err_code=0
+  _cmdstr=""
+  _backupprefix=""
+  _backupdir=""
+
   _info "Deploy certificates to remote server $Le_Deploy_ssh_user@$Le_Deploy_ssh_server"
+  if [ "$Le_Deploy_ssh_multi_call" = "yes" ]; then
+    _info "Using MULTI_CALL mode... Required commands sent in multiple calls to remote host"
+  else
+    _info "Required commands batched and sent in single call to remote host"
+  fi
+
+  if [ "$Le_Deploy_ssh_backup" = "yes" ]; then
+    _backupprefix="$Le_Deploy_ssh_backup_path/$_cdomain-backup"
+    _backupdir="$_backupprefix-$(_utc_date | tr ' ' '-')"
+    # run cleanup on the backup directory, erase all older
+    # than 180 days (15552000 seconds).
+    _cmdstr="{ now=\"\$(date -u +%s)\"; for fn in $_backupprefix*; \
+do if [ -d \"\$fn\" ] && [ \"\$(expr \$now - \$(date -ur \$fn +%s) )\" -ge \"15552000\" ]; \
+then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; done; }; $_cmdstr"
+    # Alternate version of above... _cmdstr="find $_backupprefix* -type d -mtime +180 2>/dev/null | xargs rm -rf; $_cmdstr"
+    # Create our backup directory for overwritten cert files.
+    _cmdstr="mkdir -p $_backupdir; $_cmdstr"
+    _info "Backup of old certificate files will be placed in remote directory $_backupdir"
+    _info "Backup directories erased after 180 days."
+    if [ "$Le_Deploy_ssh_multi_call" = "yes" ]; then
+      if ! _ssh_remote_cmd "$_cmdstr"; then
+        return $_err_code
+      fi
+      _cmdstr=""
+    fi
+  fi
 
   # KEYFILE is optional.
   # If provided then private key will be copied to provided filename.
+  _getdeployconf DEPLOY_SSH_KEYFILE
+  _debug2 DEPLOY_SSH_KEYFILE "$DEPLOY_SSH_KEYFILE"
   if [ -n "$DEPLOY_SSH_KEYFILE" ]; then
     Le_Deploy_ssh_keyfile="$DEPLOY_SSH_KEYFILE"
     _savedomainconf Le_Deploy_ssh_keyfile "$Le_Deploy_ssh_keyfile"
@@ -98,10 +162,18 @@ ssh_deploy() {
     # copy new certificate into file.
     _cmdstr="$_cmdstr echo \"$(cat "$_ckey")\" > $Le_Deploy_ssh_keyfile;"
     _info "will copy private key to remote file $Le_Deploy_ssh_keyfile"
+    if [ "$Le_Deploy_ssh_multi_call" = "yes" ]; then
+      if ! _ssh_remote_cmd "$_cmdstr"; then
+        return $_err_code
+      fi
+      _cmdstr=""
+    fi
   fi
 
   # CERTFILE is optional.
   # If provided then certificate will be copied or appended to provided filename.
+  _getdeployconf DEPLOY_SSH_CERTFILE
+  _debug2 DEPLOY_SSH_CERTFILE "$DEPLOY_SSH_CERTFILE"
   if [ -n "$DEPLOY_SSH_CERTFILE" ]; then
     Le_Deploy_ssh_certfile="$DEPLOY_SSH_CERTFILE"
     _savedomainconf Le_Deploy_ssh_certfile "$Le_Deploy_ssh_certfile"
@@ -118,18 +190,26 @@ ssh_deploy() {
     # copy new certificate into file.
     _cmdstr="$_cmdstr echo \"$(cat "$_ccert")\" $_pipe $Le_Deploy_ssh_certfile;"
     _info "will copy certificate to remote file $Le_Deploy_ssh_certfile"
+    if [ "$Le_Deploy_ssh_multi_call" = "yes" ]; then
+      if ! _ssh_remote_cmd "$_cmdstr"; then
+        return $_err_code
+      fi
+      _cmdstr=""
+    fi
   fi
 
   # CAFILE is optional.
   # If provided then CA intermediate certificate will be copied or appended to provided filename.
+  _getdeployconf DEPLOY_SSH_CAFILE
+  _debug2 DEPLOY_SSH_CAFILE "$DEPLOY_SSH_CAFILE"
   if [ -n "$DEPLOY_SSH_CAFILE" ]; then
     Le_Deploy_ssh_cafile="$DEPLOY_SSH_CAFILE"
     _savedomainconf Le_Deploy_ssh_cafile "$Le_Deploy_ssh_cafile"
   fi
   if [ -n "$Le_Deploy_ssh_cafile" ]; then
     _pipe=">"
-    if [ "$Le_Deploy_ssh_cafile" = "$Le_Deploy_ssh_keyfile" ] \
-      || [ "$Le_Deploy_ssh_cafile" = "$Le_Deploy_ssh_certfile" ]; then
+    if [ "$Le_Deploy_ssh_cafile" = "$Le_Deploy_ssh_keyfile" ] ||
+      [ "$Le_Deploy_ssh_cafile" = "$Le_Deploy_ssh_certfile" ]; then
       # if filename is same as previous file then append.
       _pipe=">>"
     elif [ "$Le_Deploy_ssh_backup" = "yes" ]; then
@@ -139,19 +219,27 @@ ssh_deploy() {
     # copy new certificate into file.
     _cmdstr="$_cmdstr echo \"$(cat "$_cca")\" $_pipe $Le_Deploy_ssh_cafile;"
     _info "will copy CA file to remote file $Le_Deploy_ssh_cafile"
+    if [ "$Le_Deploy_ssh_multi_call" = "yes" ]; then
+      if ! _ssh_remote_cmd "$_cmdstr"; then
+        return $_err_code
+      fi
+      _cmdstr=""
+    fi
   fi
 
   # FULLCHAIN is optional.
   # If provided then fullchain certificate will be copied or appended to provided filename.
+  _getdeployconf DEPLOY_SSH_FULLCHAIN
+  _debug2 DEPLOY_SSH_FULLCHAIN "$DEPLOY_SSH_FULLCHAIN"
   if [ -n "$DEPLOY_SSH_FULLCHAIN" ]; then
     Le_Deploy_ssh_fullchain="$DEPLOY_SSH_FULLCHAIN"
     _savedomainconf Le_Deploy_ssh_fullchain "$Le_Deploy_ssh_fullchain"
   fi
   if [ -n "$Le_Deploy_ssh_fullchain" ]; then
     _pipe=">"
-    if [ "$Le_Deploy_ssh_fullchain" = "$Le_Deploy_ssh_keyfile" ] \
-      || [ "$Le_Deploy_ssh_fullchain" = "$Le_Deploy_ssh_certfile" ] \
-      || [ "$Le_Deploy_ssh_fullchain" = "$Le_Deploy_ssh_cafile" ]; then
+    if [ "$Le_Deploy_ssh_fullchain" = "$Le_Deploy_ssh_keyfile" ] ||
+      [ "$Le_Deploy_ssh_fullchain" = "$Le_Deploy_ssh_certfile" ] ||
+      [ "$Le_Deploy_ssh_fullchain" = "$Le_Deploy_ssh_cafile" ]; then
       # if filename is same as previous file then append.
       _pipe=">>"
     elif [ "$Le_Deploy_ssh_backup" = "yes" ]; then
@@ -161,10 +249,18 @@ ssh_deploy() {
     # copy new certificate into file.
     _cmdstr="$_cmdstr echo \"$(cat "$_cfullchain")\" $_pipe $Le_Deploy_ssh_fullchain;"
     _info "will copy fullchain to remote file $Le_Deploy_ssh_fullchain"
+    if [ "$Le_Deploy_ssh_multi_call" = "yes" ]; then
+      if ! _ssh_remote_cmd "$_cmdstr"; then
+        return $_err_code
+      fi
+      _cmdstr=""
+    fi
   fi
 
   # REMOTE_CMD is optional.
   # If provided then this command will be executed on remote host.
+  _getdeployconf DEPLOY_SSH_REMOTE_CMD
+  _debug2 DEPLOY_SSH_REMOTE_CMD "$DEPLOY_SSH_REMOTE_CMD"
   if [ -n "$DEPLOY_SSH_REMOTE_CMD" ]; then
     Le_Deploy_ssh_remote_cmd="$DEPLOY_SSH_REMOTE_CMD"
     _savedomainconf Le_Deploy_ssh_remote_cmd "$Le_Deploy_ssh_remote_cmd"
@@ -172,34 +268,36 @@ ssh_deploy() {
   if [ -n "$Le_Deploy_ssh_remote_cmd" ]; then
     _cmdstr="$_cmdstr $Le_Deploy_ssh_remote_cmd;"
     _info "Will execute remote command $Le_Deploy_ssh_remote_cmd"
+    if [ "$Le_Deploy_ssh_multi_call" = "yes" ]; then
+      if ! _ssh_remote_cmd "$_cmdstr"; then
+        return $_err_code
+      fi
+      _cmdstr=""
+    fi
   fi
 
-  if [ -z "$_cmdstr" ]; then
-    _err "No remote commands to excute. Failed to deploy certificates to remote server"
-    return 1
-  elif [ "$Le_Deploy_ssh_backup" = "yes" ]; then
-    # run cleanup on the backup directory, erase all older
-    # than 180 days (15552000 seconds).
-    _cmdstr="{ now=\"\$(date -u +%s)\"; for fn in $_backupprefix*; \
-do if [ -d \"\$fn\" ] && [ \"\$(expr \$now - \$(date -ur \$fn +%s) )\" -ge \"15552000\" ]; \
-then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; done; }; $_cmdstr"
-    # Alternate version of above... _cmdstr="find $_backupprefix* -type d -mtime +180 2>/dev/null | xargs rm -rf; $_cmdstr"
-    # Create our backup directory for overwritten cert files.
-    _cmdstr="mkdir -p $_backupdir; $_cmdstr"
-    _info "Backup of old certificate files will be placed in remote directory $_backupdir"
-    _info "Backup directories erased after 180 days."
+  # if commands not all sent in multiple calls then all commands sent in a single SSH call now...
+  if [ -n "$_cmdstr" ]; then
+    if ! _ssh_remote_cmd "$_cmdstr"; then
+      return $_err_code
+    fi
   fi
+  return 0
+}
 
-  _secure_debug "Remote commands to execute: " "$_cmdstr"
+#cmd
+_ssh_remote_cmd() {
+  _cmd="$1"
+  _secure_debug "Remote commands to execute: $_cmd"
   _info "Submitting sequence of commands to remote server by ssh"
   # quotations in bash cmd below intended.  Squash travis spellcheck error
   # shellcheck disable=SC2029
-  $Le_Deploy_ssh_cmd -T "$Le_Deploy_ssh_user@$Le_Deploy_ssh_server" sh -c "'$_cmdstr'"
-  _ret="$?"
+  $Le_Deploy_ssh_cmd "$Le_Deploy_ssh_user@$Le_Deploy_ssh_server" sh -c "'$_cmd'"
+  _err_code="$?"
 
-  if [ "$_ret" != "0" ]; then
-    _err "Error code $_ret returned from $Le_Deploy_ssh_cmd"
+  if [ "$_err_code" != "0" ]; then
+    _err "Error code $_err_code returned from ssh"
   fi
 
-  return $_ret
+  return $_err_code
 }

+ 167 - 53
deploy/unifi.sh

@@ -1,12 +1,43 @@
 #!/usr/bin/env sh
 
-#Here is a script to deploy cert to unifi server.
+# Here is a script to deploy cert on a Unifi Controller or Cloud Key device.
+# It supports:
+#   - self-hosted Unifi Controller
+#   - Unifi Cloud Key (Gen1/2/2+)
+#   - Unifi Cloud Key running UnifiOS (v2.0.0+, Gen2/2+ only)
+# Please report bugs to https://github.com/acmesh-official/acme.sh/issues/3359
 
 #returns 0 means success, otherwise error.
 
+# The deploy-hook automatically detects standard Unifi installations
+# for each of the supported environments. Most users should not need
+# to set any of these variables, but if you are running a self-hosted
+# Controller with custom locations, set these as necessary before running
+# the deploy hook. (Defaults shown below.)
+#
+# Settings for Unifi Controller:
+# Location of Java keystore or unifi.keystore.jks file:
 #DEPLOY_UNIFI_KEYSTORE="/usr/lib/unifi/data/keystore"
+# Keystore password (built into Unifi Controller, not a user-set password):
 #DEPLOY_UNIFI_KEYPASS="aircontrolenterprise"
+# Command to restart Unifi Controller:
 #DEPLOY_UNIFI_RELOAD="service unifi restart"
+#
+# Settings for Unifi Cloud Key Gen1 (nginx admin pages):
+# Directory where cloudkey.crt and cloudkey.key live:
+#DEPLOY_UNIFI_CLOUDKEY_CERTDIR="/etc/ssl/private"
+# Command to restart maintenance pages and Controller
+# (same setting as above, default is updated when running on Cloud Key Gen1):
+#DEPLOY_UNIFI_RELOAD="service nginx restart && service unifi restart"
+#
+# Settings for UnifiOS (Cloud Key Gen2):
+# Directory where unifi-core.crt and unifi-core.key live:
+#DEPLOY_UNIFI_CORE_CONFIG="/data/unifi-core/config/"
+# Command to restart unifi-core:
+#DEPLOY_UNIFI_RELOAD="systemctl restart unifi-core"
+#
+# At least one of DEPLOY_UNIFI_KEYSTORE, DEPLOY_UNIFI_CLOUDKEY_CERTDIR,
+# or DEPLOY_UNIFI_CORE_CONFIG must exist to receive the deployed certs.
 
 ########  Public functions #####################
 
@@ -24,77 +55,160 @@ unifi_deploy() {
   _debug _cca "$_cca"
   _debug _cfullchain "$_cfullchain"
 
-  if ! _exists keytool; then
-    _err "keytool not found"
-    return 1
-  fi
+  _getdeployconf DEPLOY_UNIFI_KEYSTORE
+  _getdeployconf DEPLOY_UNIFI_KEYPASS
+  _getdeployconf DEPLOY_UNIFI_CLOUDKEY_CERTDIR
+  _getdeployconf DEPLOY_UNIFI_CORE_CONFIG
+  _getdeployconf DEPLOY_UNIFI_RELOAD
+
+  _debug2 DEPLOY_UNIFI_KEYSTORE "$DEPLOY_UNIFI_KEYSTORE"
+  _debug2 DEPLOY_UNIFI_KEYPASS "$DEPLOY_UNIFI_KEYPASS"
+  _debug2 DEPLOY_UNIFI_CLOUDKEY_CERTDIR "$DEPLOY_UNIFI_CLOUDKEY_CERTDIR"
+  _debug2 DEPLOY_UNIFI_CORE_CONFIG "$DEPLOY_UNIFI_CORE_CONFIG"
+  _debug2 DEPLOY_UNIFI_RELOAD "$DEPLOY_UNIFI_RELOAD"
+
+  # Space-separated list of environments detected and installed:
+  _services_updated=""
+
+  # Default reload commands accumulated as we auto-detect environments:
+  _reload_cmd=""
+
+  # Unifi Controller environment (self hosted or any Cloud Key) --
+  # auto-detect by file /usr/lib/unifi/data/keystore:
+  _unifi_keystore="${DEPLOY_UNIFI_KEYSTORE:-/usr/lib/unifi/data/keystore}"
+  if [ -f "$_unifi_keystore" ]; then
+    _info "Installing certificate for Unifi Controller (Java keystore)"
+    _debug _unifi_keystore "$_unifi_keystore"
+    if ! _exists keytool; then
+      _err "keytool not found"
+      return 1
+    fi
+    if [ ! -w "$_unifi_keystore" ]; then
+      _err "The file $_unifi_keystore is not writable, please change the permission."
+      return 1
+    fi
+
+    _unifi_keypass="${DEPLOY_UNIFI_KEYPASS:-aircontrolenterprise}"
 
-  DEFAULT_UNIFI_KEYSTORE="/usr/lib/unifi/data/keystore"
-  _unifi_keystore="${DEPLOY_UNIFI_KEYSTORE:-$DEFAULT_UNIFI_KEYSTORE}"
-  DEFAULT_UNIFI_KEYPASS="aircontrolenterprise"
-  _unifi_keypass="${DEPLOY_UNIFI_KEYPASS:-$DEFAULT_UNIFI_KEYPASS}"
-  DEFAULT_UNIFI_RELOAD="service unifi restart"
-  _reload="${DEPLOY_UNIFI_RELOAD:-$DEFAULT_UNIFI_RELOAD}"
-
-  _debug _unifi_keystore "$_unifi_keystore"
-  if [ ! -f "$_unifi_keystore" ]; then
-    if [ -z "$DEPLOY_UNIFI_KEYSTORE" ]; then
-      _err "unifi keystore is not found, please define DEPLOY_UNIFI_KEYSTORE"
+    _debug "Generate import pkcs12"
+    _import_pkcs12="$(_mktemp)"
+    _toPkcs "$_import_pkcs12" "$_ckey" "$_ccert" "$_cca" "$_unifi_keypass" unifi root
+    # shellcheck disable=SC2181
+    if [ "$?" != "0" ]; then
+      _err "Error generating pkcs12. Please re-run with --debug and report a bug."
       return 1
+    fi
+
+    _debug "Import into keystore: $_unifi_keystore"
+    if keytool -importkeystore \
+      -deststorepass "$_unifi_keypass" -destkeypass "$_unifi_keypass" -destkeystore "$_unifi_keystore" \
+      -srckeystore "$_import_pkcs12" -srcstoretype PKCS12 -srcstorepass "$_unifi_keypass" \
+      -alias unifi -noprompt; then
+      _debug "Import keystore success!"
+      rm "$_import_pkcs12"
     else
-      _err "It seems that the specified unifi keystore is not valid, please check."
+      _err "Error importing into Unifi Java keystore."
+      _err "Please re-run with --debug and report a bug."
+      rm "$_import_pkcs12"
       return 1
     fi
+
+    if systemctl -q is-active unifi; then
+      _reload_cmd="${_reload_cmd:+$_reload_cmd && }service unifi restart"
+    fi
+    _services_updated="${_services_updated} unifi"
+    _info "Install Unifi Controller certificate success!"
+  elif [ "$DEPLOY_UNIFI_KEYSTORE" ]; then
+    _err "The specified DEPLOY_UNIFI_KEYSTORE='$DEPLOY_UNIFI_KEYSTORE' is not valid, please check."
+    return 1
   fi
-  if [ ! -w "$_unifi_keystore" ]; then
-    _err "The file $_unifi_keystore is not writable, please change the permission."
+
+  # Cloud Key environment (non-UnifiOS -- nginx serves admin pages) --
+  # auto-detect by file /etc/ssl/private/cloudkey.key:
+  _cloudkey_certdir="${DEPLOY_UNIFI_CLOUDKEY_CERTDIR:-/etc/ssl/private}"
+  if [ -f "${_cloudkey_certdir}/cloudkey.key" ]; then
+    _info "Installing certificate for Cloud Key Gen1 (nginx admin pages)"
+    _debug _cloudkey_certdir "$_cloudkey_certdir"
+    if [ ! -w "$_cloudkey_certdir" ]; then
+      _err "The directory $_cloudkey_certdir is not writable; please check permissions."
+      return 1
+    fi
+    # Cloud Key expects to load the keystore from /etc/ssl/private/unifi.keystore.jks.
+    # Normally /usr/lib/unifi/data/keystore is a symlink there (so the keystore was
+    # updated above), but if not, we don't know how to handle this installation:
+    if ! cmp -s "$_unifi_keystore" "${_cloudkey_certdir}/unifi.keystore.jks"; then
+      _err "Unsupported Cloud Key configuration: keystore not found at '${_cloudkey_certdir}/unifi.keystore.jks'"
+      return 1
+    fi
+
+    cat "$_cfullchain" >"${_cloudkey_certdir}/cloudkey.crt"
+    cat "$_ckey" >"${_cloudkey_certdir}/cloudkey.key"
+    (cd "$_cloudkey_certdir" && tar -cf cert.tar cloudkey.crt cloudkey.key unifi.keystore.jks)
+
+    if systemctl -q is-active nginx; then
+      _reload_cmd="${_reload_cmd:+$_reload_cmd && }service nginx restart"
+    fi
+    _info "Install Cloud Key Gen1 certificate success!"
+    _services_updated="${_services_updated} nginx"
+  elif [ "$DEPLOY_UNIFI_CLOUDKEY_CERTDIR" ]; then
+    _err "The specified DEPLOY_UNIFI_CLOUDKEY_CERTDIR='$DEPLOY_UNIFI_CLOUDKEY_CERTDIR' is not valid, please check."
     return 1
   fi
 
-  _info "Generate import pkcs12"
-  _import_pkcs12="$(_mktemp)"
-  _toPkcs "$_import_pkcs12" "$_ckey" "$_ccert" "$_cca" "$_unifi_keypass" unifi root
-  if [ "$?" != "0" ]; then
-    _err "Oops, error creating import pkcs12, please report bug to us."
+  # UnifiOS environment -- auto-detect by /data/unifi-core/config/unifi-core.key:
+  _unifi_core_config="${DEPLOY_UNIFI_CORE_CONFIG:-/data/unifi-core/config}"
+  if [ -f "${_unifi_core_config}/unifi-core.key" ]; then
+    _info "Installing certificate for UnifiOS"
+    _debug _unifi_core_config "$_unifi_core_config"
+    if [ ! -w "$_unifi_core_config" ]; then
+      _err "The directory $_unifi_core_config is not writable; please check permissions."
+      return 1
+    fi
+
+    cat "$_cfullchain" >"${_unifi_core_config}/unifi-core.crt"
+    cat "$_ckey" >"${_unifi_core_config}/unifi-core.key"
+
+    if systemctl -q is-active unifi-core; then
+      _reload_cmd="${_reload_cmd:+$_reload_cmd && }systemctl restart unifi-core"
+    fi
+    _info "Install UnifiOS certificate success!"
+    _services_updated="${_services_updated} unifi-core"
+  elif [ "$DEPLOY_UNIFI_CORE_CONFIG" ]; then
+    _err "The specified DEPLOY_UNIFI_CORE_CONFIG='$DEPLOY_UNIFI_CORE_CONFIG' is not valid, please check."
     return 1
   fi
 
-  _info "Modify unifi keystore: $_unifi_keystore"
-  if keytool -importkeystore \
-    -deststorepass "$_unifi_keypass" -destkeypass "$_unifi_keypass" -destkeystore "$_unifi_keystore" \
-    -srckeystore "$_import_pkcs12" -srcstoretype PKCS12 -srcstorepass "$_unifi_keypass" \
-    -alias unifi -noprompt; then
-    _info "Import keystore success!"
-    rm "$_import_pkcs12"
-  else
-    _err "Import unifi keystore error, please report bug to us."
-    rm "$_import_pkcs12"
+  if [ -z "$_services_updated" ]; then
+    # None of the Unifi environments were auto-detected, so no deployment has occurred
+    # (and none of DEPLOY_UNIFI_{KEYSTORE,CLOUDKEY_CERTDIR,CORE_CONFIG} were set).
+    _err "Unable to detect Unifi environment in standard location."
+    _err "(This deploy hook must be run on the Unifi device, not a remote machine.)"
+    _err "For non-standard Unifi installations, set DEPLOY_UNIFI_KEYSTORE,"
+    _err "DEPLOY_UNIFI_CLOUDKEY_CERTDIR, and/or DEPLOY_UNIFI_CORE_CONFIG as appropriate."
     return 1
   fi
 
-  _info "Run reload: $_reload"
-  if eval "$_reload"; then
+  _reload_cmd="${DEPLOY_UNIFI_RELOAD:-$_reload_cmd}"
+  if [ -z "$_reload_cmd" ]; then
+    _err "Certificates were installed for services:${_services_updated},"
+    _err "but none appear to be active. Please set DEPLOY_UNIFI_RELOAD"
+    _err "to a command that will restart the necessary services."
+    return 1
+  fi
+  _info "Reload services (this may take some time): $_reload_cmd"
+  if eval "$_reload_cmd"; then
     _info "Reload success!"
-    if [ "$DEPLOY_UNIFI_KEYSTORE" ]; then
-      _savedomainconf DEPLOY_UNIFI_KEYSTORE "$DEPLOY_UNIFI_KEYSTORE"
-    else
-      _cleardomainconf DEPLOY_UNIFI_KEYSTORE
-    fi
-    if [ "$DEPLOY_UNIFI_KEYPASS" ]; then
-      _savedomainconf DEPLOY_UNIFI_KEYPASS "$DEPLOY_UNIFI_KEYPASS"
-    else
-      _cleardomainconf DEPLOY_UNIFI_KEYPASS
-    fi
-    if [ "$DEPLOY_UNIFI_RELOAD" ]; then
-      _savedomainconf DEPLOY_UNIFI_RELOAD "$DEPLOY_UNIFI_RELOAD"
-    else
-      _cleardomainconf DEPLOY_UNIFI_RELOAD
-    fi
-    return 0
   else
     _err "Reload error"
     return 1
   fi
-  return 0
 
+  # Successful, so save all (non-default) config:
+  _savedeployconf DEPLOY_UNIFI_KEYSTORE "$DEPLOY_UNIFI_KEYSTORE"
+  _savedeployconf DEPLOY_UNIFI_KEYPASS "$DEPLOY_UNIFI_KEYPASS"
+  _savedeployconf DEPLOY_UNIFI_CLOUDKEY_CERTDIR "$DEPLOY_UNIFI_CLOUDKEY_CERTDIR"
+  _savedeployconf DEPLOY_UNIFI_CORE_CONFIG "$DEPLOY_UNIFI_CORE_CONFIG"
+  _savedeployconf DEPLOY_UNIFI_RELOAD "$DEPLOY_UNIFI_RELOAD"
+
+  return 0
 }

+ 8 - 8
deploy/vault_cli.sh

@@ -2,10 +2,10 @@
 
 # Here is a script to deploy cert to hashicorp vault
 # (https://www.vaultproject.io/)
-# 
+#
 # it requires the vault binary to be available in PATH, and the following
 # environment variables:
-# 
+#
 # VAULT_PREFIX - this contains the prefix path in vault
 # VAULT_ADDR - vault requires this to find your vault server
 #
@@ -43,19 +43,19 @@ vault_cli_deploy() {
     return 1
   fi
 
-  VAULT_CMD=$(which vault)
+  VAULT_CMD=$(command -v vault)
   if [ ! $? ]; then
     _err "cannot find vault binary!"
     return 1
   fi
 
   if [ -n "$FABIO" ]; then
-    $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}" cert=@"$_cfullchain" key=@"$_ckey" || return 1
+    $VAULT_CMD kv put "${VAULT_PREFIX}/${_cdomain}" cert=@"$_cfullchain" key=@"$_ckey" || return 1
   else
-    $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}/cert.pem" value=@"$_ccert" || return 1
-    $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}/cert.key" value=@"$_ckey" || return 1
-    $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}/chain.pem" value=@"$_cca" || return 1
-    $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}/fullchain.pem" value=@"$_cfullchain" || return 1
+    $VAULT_CMD kv put "${VAULT_PREFIX}/${_cdomain}/cert.pem" value=@"$_ccert" || return 1
+    $VAULT_CMD kv put "${VAULT_PREFIX}/${_cdomain}/cert.key" value=@"$_ckey" || return 1
+    $VAULT_CMD kv put "${VAULT_PREFIX}/${_cdomain}/chain.pem" value=@"$_cca" || return 1
+    $VAULT_CMD kv put "${VAULT_PREFIX}/${_cdomain}/fullchain.pem" value=@"$_cfullchain" || return 1
   fi
 
 }

+ 3 - 3
deploy/vsftpd.sh

@@ -65,9 +65,9 @@ vsftpd_deploy() {
     cp "$_vsftpd_conf" "$_backup_conf"
 
     _info "Modify vsftpd conf: $_vsftpd_conf"
-    if _setopt "$_vsftpd_conf" "rsa_cert_file" "=" "$_real_fullchain" \
-      && _setopt "$_vsftpd_conf" "rsa_private_key_file" "=" "$_real_key" \
-      && _setopt "$_vsftpd_conf" "ssl_enable" "=" "YES"; then
+    if _setopt "$_vsftpd_conf" "rsa_cert_file" "=" "$_real_fullchain" &&
+      _setopt "$_vsftpd_conf" "rsa_private_key_file" "=" "$_real_key" &&
+      _setopt "$_vsftpd_conf" "ssl_enable" "=" "YES"; then
       _info "Set config success!"
     else
       _err "Config vsftpd server error, please report bug to us."

+ 1 - 1
dnsapi/README.md

@@ -2,5 +2,5 @@
 DNS api usage:
 
 
-https://github.com/Neilpang/acme.sh/wiki/dnsapi
+https://github.com/acmesh-official/acme.sh/wiki/dnsapi
 

+ 51 - 12
dnsapi/dns_acmedns.sh

@@ -1,31 +1,70 @@
 #!/usr/bin/env sh
 #
 #Author: Wolfgang Ebner
-#Report Bugs here: https://github.com/webner/acme.sh
+#Author: Sven Neubuaer
+#Report Bugs here: https://github.com/dampfklon/acme.sh
+#
+# Usage:
+# export ACMEDNS_BASE_URL="https://auth.acme-dns.io"
+#
+# You can optionally define an already existing account:
+#
+# export ACMEDNS_USERNAME="<username>"
+# export ACMEDNS_PASSWORD="<password>"
+# export ACMEDNS_SUBDOMAIN="<subdomain>"
 #
 ########  Public functions #####################
 
 #Usage: dns_acmedns_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+# Used to add txt record
 dns_acmedns_add() {
   fulldomain=$1
   txtvalue=$2
   _info "Using acme-dns"
-  _debug fulldomain "$fulldomain"
-  _debug txtvalue "$txtvalue"
+  _debug "fulldomain $fulldomain"
+  _debug "txtvalue $txtvalue"
 
-  ACMEDNS_UPDATE_URL="${ACMEDNS_UPDATE_URL:-$(_readaccountconf_mutable ACMEDNS_UPDATE_URL)}"
+  #for compatiblity from account conf
   ACMEDNS_USERNAME="${ACMEDNS_USERNAME:-$(_readaccountconf_mutable ACMEDNS_USERNAME)}"
+  _clearaccountconf_mutable ACMEDNS_USERNAME
   ACMEDNS_PASSWORD="${ACMEDNS_PASSWORD:-$(_readaccountconf_mutable ACMEDNS_PASSWORD)}"
+  _clearaccountconf_mutable ACMEDNS_PASSWORD
   ACMEDNS_SUBDOMAIN="${ACMEDNS_SUBDOMAIN:-$(_readaccountconf_mutable ACMEDNS_SUBDOMAIN)}"
+  _clearaccountconf_mutable ACMEDNS_SUBDOMAIN
+
+  ACMEDNS_BASE_URL="${ACMEDNS_BASE_URL:-$(_readdomainconf ACMEDNS_BASE_URL)}"
+  ACMEDNS_USERNAME="${ACMEDNS_USERNAME:-$(_readdomainconf ACMEDNS_USERNAME)}"
+  ACMEDNS_PASSWORD="${ACMEDNS_PASSWORD:-$(_readdomainconf ACMEDNS_PASSWORD)}"
+  ACMEDNS_SUBDOMAIN="${ACMEDNS_SUBDOMAIN:-$(_readdomainconf ACMEDNS_SUBDOMAIN)}"
+
+  if [ "$ACMEDNS_BASE_URL" = "" ]; then
+    ACMEDNS_BASE_URL="https://auth.acme-dns.io"
+  fi
+
+  ACMEDNS_UPDATE_URL="$ACMEDNS_BASE_URL/update"
+  ACMEDNS_REGISTER_URL="$ACMEDNS_BASE_URL/register"
 
-  if [ "$ACMEDNS_UPDATE_URL" = "" ]; then
-    ACMEDNS_UPDATE_URL="https://auth.acme-dns.io/update"
+  if [ -z "$ACMEDNS_USERNAME" ] || [ -z "$ACMEDNS_PASSWORD" ]; then
+    response="$(_post "" "$ACMEDNS_REGISTER_URL" "" "POST")"
+    _debug response "$response"
+    ACMEDNS_USERNAME=$(echo "$response" | sed -n 's/^{.*\"username\":[ ]*\"\([^\"]*\)\".*}/\1/p')
+    _debug "received username: $ACMEDNS_USERNAME"
+    ACMEDNS_PASSWORD=$(echo "$response" | sed -n 's/^{.*\"password\":[ ]*\"\([^\"]*\)\".*}/\1/p')
+    _debug "received password: $ACMEDNS_PASSWORD"
+    ACMEDNS_SUBDOMAIN=$(echo "$response" | sed -n 's/^{.*\"subdomain\":[ ]*\"\([^\"]*\)\".*}/\1/p')
+    _debug "received subdomain: $ACMEDNS_SUBDOMAIN"
+    ACMEDNS_FULLDOMAIN=$(echo "$response" | sed -n 's/^{.*\"fulldomain\":[ ]*\"\([^\"]*\)\".*}/\1/p')
+    _info "##########################################################"
+    _info "# Create $fulldomain CNAME $ACMEDNS_FULLDOMAIN DNS entry #"
+    _info "##########################################################"
+    _info "Press enter to continue... "
+    read -r _
   fi
 
-  _saveaccountconf_mutable ACMEDNS_UPDATE_URL "$ACMEDNS_UPDATE_URL"
-  _saveaccountconf_mutable ACMEDNS_USERNAME "$ACMEDNS_USERNAME"
-  _saveaccountconf_mutable ACMEDNS_PASSWORD "$ACMEDNS_PASSWORD"
-  _saveaccountconf_mutable ACMEDNS_SUBDOMAIN "$ACMEDNS_SUBDOMAIN"
+  _savedomainconf ACMEDNS_BASE_URL "$ACMEDNS_BASE_URL"
+  _savedomainconf ACMEDNS_USERNAME "$ACMEDNS_USERNAME"
+  _savedomainconf ACMEDNS_PASSWORD "$ACMEDNS_PASSWORD"
+  _savedomainconf ACMEDNS_SUBDOMAIN "$ACMEDNS_SUBDOMAIN"
 
   export _H1="X-Api-User: $ACMEDNS_USERNAME"
   export _H2="X-Api-Key: $ACMEDNS_PASSWORD"
@@ -48,8 +87,8 @@ dns_acmedns_rm() {
   fulldomain=$1
   txtvalue=$2
   _info "Using acme-dns"
-  _debug fulldomain "$fulldomain"
-  _debug txtvalue "$txtvalue"
+  _debug "fulldomain $fulldomain"
+  _debug "txtvalue $txtvalue"
 }
 
 ####################  Private functions below ##################################

+ 1 - 0
dnsapi/dns_ali.sh

@@ -181,6 +181,7 @@ _describe_records_query() {
 
 _clean() {
   _check_exist_query "$_domain" "$_sub_domain"
+  # do not correct grammar here
   if ! _ali_rest "Check exist records" "ignore"; then
     return 1
   fi

+ 63 - 40
dnsapi/dns_aws.sh

@@ -6,11 +6,13 @@
 #AWS_SECRET_ACCESS_KEY="xxxxxxx"
 
 #This is the Amazon Route53 api wrapper for acme.sh
+#All `_sleep` commands are included to avoid Route53 throttling, see
+#https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DNSLimitations.html#limits-api-requests
 
 AWS_HOST="route53.amazonaws.com"
 AWS_URL="https://$AWS_HOST"
 
-AWS_WIKI="https://github.com/Neilpang/acme.sh/wiki/How-to-use-Amazon-Route53-API"
+AWS_WIKI="https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Amazon-Route53-API"
 
 ########  Public functions #####################
 
@@ -21,6 +23,7 @@ dns_aws_add() {
 
   AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-$(_readaccountconf_mutable AWS_ACCESS_KEY_ID)}"
   AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-$(_readaccountconf_mutable AWS_SECRET_ACCESS_KEY)}"
+  AWS_DNS_SLOWRATE="${AWS_DNS_SLOWRATE:-$(_readaccountconf_mutable AWS_DNS_SLOWRATE)}"
 
   if [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ]; then
     _use_container_role || _use_instance_role
@@ -29,7 +32,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 haven't specifed the aws route53 api key id and and api key secret yet."
+    _err "You haven't specified 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
@@ -38,11 +41,13 @@ dns_aws_add() {
   if [ -z "$_using_role" ]; then
     _saveaccountconf_mutable AWS_ACCESS_KEY_ID "$AWS_ACCESS_KEY_ID"
     _saveaccountconf_mutable AWS_SECRET_ACCESS_KEY "$AWS_SECRET_ACCESS_KEY"
+    _saveaccountconf_mutable AWS_DNS_SLOWRATE "$AWS_DNS_SLOWRATE"
   fi
 
   _debug "First detect the root zone"
   if ! _get_root "$fulldomain"; then
     _err "invalid domain"
+    _sleep 1
     return 1
   fi
   _debug _domain_id "$_domain_id"
@@ -51,6 +56,7 @@ dns_aws_add() {
 
   _info "Getting existing records for $fulldomain"
   if ! aws_rest GET "2013-04-01$_domain_id/rrset" "name=$fulldomain&type=TXT"; then
+    _sleep 1
     return 1
   fi
 
@@ -63,6 +69,7 @@ dns_aws_add() {
 
   if [ "$_resource_record" ] && _contains "$response" "$txtvalue"; then
     _info "The TXT record already exists. Skipping."
+    _sleep 1
     return 0
   fi
 
@@ -72,9 +79,16 @@ dns_aws_add() {
 
   if aws_rest POST "2013-04-01$_domain_id/rrset/" "" "$_aws_tmpl_xml" && _contains "$response" "ChangeResourceRecordSetsResponse"; then
     _info "TXT record updated successfully."
+    if [ -n "$AWS_DNS_SLOWRATE" ]; then
+      _info "Slow rate activated: sleeping for $AWS_DNS_SLOWRATE seconds"
+      _sleep "$AWS_DNS_SLOWRATE"
+    else
+      _sleep 1
+    fi
+
     return 0
   fi
-
+  _sleep 1
   return 1
 }
 
@@ -85,6 +99,7 @@ dns_aws_rm() {
 
   AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-$(_readaccountconf_mutable AWS_ACCESS_KEY_ID)}"
   AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-$(_readaccountconf_mutable AWS_SECRET_ACCESS_KEY)}"
+  AWS_DNS_SLOWRATE="${AWS_DNS_SLOWRATE:-$(_readaccountconf_mutable AWS_DNS_SLOWRATE)}"
 
   if [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ]; then
     _use_container_role || _use_instance_role
@@ -93,6 +108,7 @@ dns_aws_rm() {
   _debug "First detect the root zone"
   if ! _get_root "$fulldomain"; then
     _err "invalid domain"
+    _sleep 1
     return 1
   fi
   _debug _domain_id "$_domain_id"
@@ -101,6 +117,7 @@ dns_aws_rm() {
 
   _info "Getting existing records for $fulldomain"
   if ! aws_rest GET "2013-04-01$_domain_id/rrset" "name=$fulldomain&type=TXT"; then
+    _sleep 1
     return 1
   fi
 
@@ -109,6 +126,7 @@ dns_aws_rm() {
     _debug "_resource_record" "$_resource_record"
   else
     _debug "no records exist, skip"
+    _sleep 1
     return 0
   fi
 
@@ -116,9 +134,16 @@ dns_aws_rm() {
 
   if aws_rest POST "2013-04-01$_domain_id/rrset/" "" "$_aws_tmpl_xml" && _contains "$response" "ChangeResourceRecordSetsResponse"; then
     _info "TXT record deleted successfully."
+    if [ -n "$AWS_DNS_SLOWRATE" ]; then
+      _info "Slow rate activated: sleeping for $AWS_DNS_SLOWRATE seconds"
+      _sleep "$AWS_DNS_SLOWRATE"
+    else
+      _sleep 1
+    fi
+
     return 0
   fi
-
+  _sleep 1
   return 1
 
 }
@@ -127,34 +152,23 @@ dns_aws_rm() {
 
 _get_root() {
   domain=$1
-  i=2
+  i=1
   p=1
 
-  if aws_rest GET "2013-04-01/hostedzone"; then
-    while true; do
-      h=$(printf "%s" "$domain" | cut -d . -f $i-100)
-      _debug2 "Checking domain: $h"
-      if [ -z "$h" ]; then
-        if _contains "$response" "<IsTruncated>true</IsTruncated>" && _contains "$response" "<NextMarker>"; then
-          _debug "IsTruncated"
-          _nextMarker="$(echo "$response" | _egrep_o "<NextMarker>.*</NextMarker>" | cut -d '>' -f 2 | cut -d '<' -f 1)"
-          _debug "NextMarker" "$_nextMarker"
-          if aws_rest GET "2013-04-01/hostedzone" "marker=$_nextMarker"; then
-            _debug "Truncated request OK"
-            i=2
-            p=1
-            continue
-          else
-            _err "Truncated request error."
-          fi
-        fi
-        #not valid
-        _err "Invalid domain"
-        return 1
-      fi
+  # iterate over names (a.b.c.d -> b.c.d -> c.d -> d)
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    _debug "Checking domain: $h"
+    if [ -z "$h" ]; then
+      _error "invalid domain"
+      return 1
+    fi
 
+    # iterate over paginated result for list_hosted_zones
+    aws_rest GET "2013-04-01/hostedzone"
+    while true; do
       if _contains "$response" "<Name>$h.</Name>"; then
-        hostedzone="$(echo "$response" | sed 's/<HostedZone>/#&/g' | tr '#' '\n' | _egrep_o "<HostedZone><Id>[^<]*<.Id><Name>$h.<.Name>.*<PrivateZone>false<.PrivateZone>.*<.HostedZone>")"
+        hostedzone="$(echo "$response" | tr -d '\n' | sed 's/<HostedZone>/#&/g' | tr '#' '\n' | _egrep_o "<HostedZone><Id>[^<]*<.Id><Name>$h.<.Name>.*<PrivateZone>false<.PrivateZone>.*<.HostedZone>")"
         _debug hostedzone "$hostedzone"
         if [ "$hostedzone" ]; then
           _domain_id=$(printf "%s\n" "$hostedzone" | _egrep_o "<Id>.*<.Id>" | head -n 1 | _egrep_o ">.*<" | tr -d "<>")
@@ -167,10 +181,19 @@ _get_root() {
           return 1
         fi
       fi
-      p=$i
-      i=$(_math "$i" + 1)
+      if _contains "$response" "<IsTruncated>true</IsTruncated>" && _contains "$response" "<NextMarker>"; then
+        _debug "IsTruncated"
+        _nextMarker="$(echo "$response" | _egrep_o "<NextMarker>.*</NextMarker>" | cut -d '>' -f 2 | cut -d '<' -f 1)"
+        _debug "NextMarker" "$_nextMarker"
+      else
+        break
+      fi
+      _debug "Checking domain: $h - Next Page "
+      aws_rest GET "2013-04-01/hostedzone" "marker=$_nextMarker"
     done
-  fi
+    p=$i
+    i=$(_math "$i" + 1)
+  done
   return 1
 }
 
@@ -197,21 +220,21 @@ _use_instance_role() {
 
 _use_metadata() {
   _aws_creds="$(
-    _get "$1" "" 1 \
-      | _normalizeJson \
-      | tr '{,}' '\n' \
-      | while read -r _line; do
+    _get "$1" "" 1 |
+      _normalizeJson |
+      tr '{,}' '\n' |
+      while read -r _line; do
         _key="$(echo "${_line%%:*}" | tr -d '"')"
         _value="${_line#*:}"
         _debug3 "_key" "$_key"
         _secure_debug3 "_value" "$_value"
         case "$_key" in
-          AccessKeyId) echo "AWS_ACCESS_KEY_ID=$_value" ;;
-          SecretAccessKey) echo "AWS_SECRET_ACCESS_KEY=$_value" ;;
-          Token) echo "AWS_SESSION_TOKEN=$_value" ;;
+        AccessKeyId) echo "AWS_ACCESS_KEY_ID=$_value" ;;
+        SecretAccessKey) echo "AWS_SECRET_ACCESS_KEY=$_value" ;;
+        Token) echo "AWS_SESSION_TOKEN=$_value" ;;
         esac
-      done \
-        | paste -sd' ' -
+      done |
+      paste -sd' ' -
   )"
   _secure_debug "_aws_creds" "$_aws_creds"
 

+ 109 - 79
dnsapi/dns_azure.sh

@@ -1,6 +1,6 @@
 #!/usr/bin/env sh
 
-WIKI="https://github.com/Neilpang/acme.sh/wiki/How-to-use-Azure-DNS"
+WIKI="https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Azure-DNS"
 
 ########  Public functions #####################
 
@@ -9,57 +9,72 @@ WIKI="https://github.com/Neilpang/acme.sh/wiki/How-to-use-Azure-DNS"
 #
 # Ref: https://docs.microsoft.com/en-us/rest/api/dns/recordsets/createorupdate
 #
+
 dns_azure_add() {
   fulldomain=$1
   txtvalue=$2
 
   AZUREDNS_SUBSCRIPTIONID="${AZUREDNS_SUBSCRIPTIONID:-$(_readaccountconf_mutable AZUREDNS_SUBSCRIPTIONID)}"
-  AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}"
-  AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}"
-  AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}"
-
   if [ -z "$AZUREDNS_SUBSCRIPTIONID" ]; then
     AZUREDNS_SUBSCRIPTIONID=""
     AZUREDNS_TENANTID=""
     AZUREDNS_APPID=""
     AZUREDNS_CLIENTSECRET=""
-    _err "You didn't specify the Azure Subscription ID "
+    _err "You didn't specify the Azure Subscription ID"
     return 1
   fi
+  #save subscription id to account conf file.
+  _saveaccountconf_mutable AZUREDNS_SUBSCRIPTIONID "$AZUREDNS_SUBSCRIPTIONID"
 
-  if [ -z "$AZUREDNS_TENANTID" ]; then
-    AZUREDNS_SUBSCRIPTIONID=""
-    AZUREDNS_TENANTID=""
-    AZUREDNS_APPID=""
-    AZUREDNS_CLIENTSECRET=""
-    _err "You didn't specify the Azure Tenant ID "
-    return 1
-  fi
+  AZUREDNS_MANAGEDIDENTITY="${AZUREDNS_MANAGEDIDENTITY:-$(_readaccountconf_mutable AZUREDNS_MANAGEDIDENTITY)}"
+  if [ "$AZUREDNS_MANAGEDIDENTITY" = true ]; then
+    _info "Using Azure managed identity"
+    #save managed identity as preferred authentication method, clear service principal credentials from conf file.
+    _saveaccountconf_mutable AZUREDNS_MANAGEDIDENTITY "$AZUREDNS_MANAGEDIDENTITY"
+    _saveaccountconf_mutable AZUREDNS_TENANTID ""
+    _saveaccountconf_mutable AZUREDNS_APPID ""
+    _saveaccountconf_mutable AZUREDNS_CLIENTSECRET ""
+  else
+    _info "You didn't ask to use Azure managed identity, checking service principal credentials"
+    AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}"
+    AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}"
+    AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}"
+
+    if [ -z "$AZUREDNS_TENANTID" ]; then
+      AZUREDNS_SUBSCRIPTIONID=""
+      AZUREDNS_TENANTID=""
+      AZUREDNS_APPID=""
+      AZUREDNS_CLIENTSECRET=""
+      _err "You didn't specify the Azure Tenant ID "
+      return 1
+    fi
 
-  if [ -z "$AZUREDNS_APPID" ]; then
-    AZUREDNS_SUBSCRIPTIONID=""
-    AZUREDNS_TENANTID=""
-    AZUREDNS_APPID=""
-    AZUREDNS_CLIENTSECRET=""
-    _err "You didn't specify the Azure App ID"
-    return 1
-  fi
+    if [ -z "$AZUREDNS_APPID" ]; then
+      AZUREDNS_SUBSCRIPTIONID=""
+      AZUREDNS_TENANTID=""
+      AZUREDNS_APPID=""
+      AZUREDNS_CLIENTSECRET=""
+      _err "You didn't specify the Azure App ID"
+      return 1
+    fi
 
-  if [ -z "$AZUREDNS_CLIENTSECRET" ]; then
-    AZUREDNS_SUBSCRIPTIONID=""
-    AZUREDNS_TENANTID=""
-    AZUREDNS_APPID=""
-    AZUREDNS_CLIENTSECRET=""
-    _err "You didn't specify the Azure Client Secret"
-    return 1
+    if [ -z "$AZUREDNS_CLIENTSECRET" ]; then
+      AZUREDNS_SUBSCRIPTIONID=""
+      AZUREDNS_TENANTID=""
+      AZUREDNS_APPID=""
+      AZUREDNS_CLIENTSECRET=""
+      _err "You didn't specify the Azure Client Secret"
+      return 1
+    fi
+
+    #save account details to account conf file, don't opt in for azure manages identity check.
+    _saveaccountconf_mutable AZUREDNS_MANAGEDIDENTITY "false"
+    _saveaccountconf_mutable AZUREDNS_TENANTID "$AZUREDNS_TENANTID"
+    _saveaccountconf_mutable AZUREDNS_APPID "$AZUREDNS_APPID"
+    _saveaccountconf_mutable AZUREDNS_CLIENTSECRET "$AZUREDNS_CLIENTSECRET"
   fi
-  #save account details to account conf file.
-  _saveaccountconf_mutable AZUREDNS_SUBSCRIPTIONID "$AZUREDNS_SUBSCRIPTIONID"
-  _saveaccountconf_mutable AZUREDNS_TENANTID "$AZUREDNS_TENANTID"
-  _saveaccountconf_mutable AZUREDNS_APPID "$AZUREDNS_APPID"
-  _saveaccountconf_mutable AZUREDNS_CLIENTSECRET "$AZUREDNS_CLIENTSECRET"
 
-  accesstoken=$(_azure_getaccess_token "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET")
+  accesstoken=$(_azure_getaccess_token "$AZUREDNS_MANAGEDIDENTITY" "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET")
 
   if ! _get_root "$fulldomain" "$AZUREDNS_SUBSCRIPTIONID" "$accesstoken"; then
     _err "invalid domain"
@@ -116,10 +131,6 @@ dns_azure_rm() {
   txtvalue=$2
 
   AZUREDNS_SUBSCRIPTIONID="${AZUREDNS_SUBSCRIPTIONID:-$(_readaccountconf_mutable AZUREDNS_SUBSCRIPTIONID)}"
-  AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}"
-  AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}"
-  AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}"
-
   if [ -z "$AZUREDNS_SUBSCRIPTIONID" ]; then
     AZUREDNS_SUBSCRIPTIONID=""
     AZUREDNS_TENANTID=""
@@ -129,34 +140,44 @@ dns_azure_rm() {
     return 1
   fi
 
-  if [ -z "$AZUREDNS_TENANTID" ]; then
-    AZUREDNS_SUBSCRIPTIONID=""
-    AZUREDNS_TENANTID=""
-    AZUREDNS_APPID=""
-    AZUREDNS_CLIENTSECRET=""
-    _err "You didn't specify the Azure Tenant ID "
-    return 1
-  fi
+  AZUREDNS_MANAGEDIDENTITY="${AZUREDNS_MANAGEDIDENTITY:-$(_readaccountconf_mutable AZUREDNS_MANAGEDIDENTITY)}"
+  if [ "$AZUREDNS_MANAGEDIDENTITY" = true ]; then
+    _info "Using Azure managed identity"
+  else
+    _info "You didn't ask to use Azure managed identity, checking service principal credentials"
+    AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}"
+    AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}"
+    AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}"
+
+    if [ -z "$AZUREDNS_TENANTID" ]; then
+      AZUREDNS_SUBSCRIPTIONID=""
+      AZUREDNS_TENANTID=""
+      AZUREDNS_APPID=""
+      AZUREDNS_CLIENTSECRET=""
+      _err "You didn't specify the Azure Tenant ID "
+      return 1
+    fi
 
-  if [ -z "$AZUREDNS_APPID" ]; then
-    AZUREDNS_SUBSCRIPTIONID=""
-    AZUREDNS_TENANTID=""
-    AZUREDNS_APPID=""
-    AZUREDNS_CLIENTSECRET=""
-    _err "You didn't specify the Azure App ID"
-    return 1
-  fi
+    if [ -z "$AZUREDNS_APPID" ]; then
+      AZUREDNS_SUBSCRIPTIONID=""
+      AZUREDNS_TENANTID=""
+      AZUREDNS_APPID=""
+      AZUREDNS_CLIENTSECRET=""
+      _err "You didn't specify the Azure App ID"
+      return 1
+    fi
 
-  if [ -z "$AZUREDNS_CLIENTSECRET" ]; then
-    AZUREDNS_SUBSCRIPTIONID=""
-    AZUREDNS_TENANTID=""
-    AZUREDNS_APPID=""
-    AZUREDNS_CLIENTSECRET=""
-    _err "You didn't specify the Azure Client Secret"
-    return 1
+    if [ -z "$AZUREDNS_CLIENTSECRET" ]; then
+      AZUREDNS_SUBSCRIPTIONID=""
+      AZUREDNS_TENANTID=""
+      AZUREDNS_APPID=""
+      AZUREDNS_CLIENTSECRET=""
+      _err "You didn't specify the Azure Client Secret"
+      return 1
+    fi
   fi
 
-  accesstoken=$(_azure_getaccess_token "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET")
+  accesstoken=$(_azure_getaccess_token "$AZUREDNS_MANAGEDIDENTITY" "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET")
 
   if ! _get_root "$fulldomain" "$AZUREDNS_SUBSCRIPTIONID" "$accesstoken"; then
     _err "invalid domain"
@@ -172,7 +193,7 @@ dns_azure_rm() {
   _azure_rest GET "$acmeRecordURI" "" "$accesstoken"
   timestamp="$(_time)"
   if [ "$_code" = "200" ]; then
-    vlist="$(echo "$response" | _egrep_o "\"value\"\\s*:\\s*\\[\\s*\"[^\"]*\"\\s*]" | cut -d : -f 2 | tr -d "[]\"" | grep -v "$txtvalue")"
+    vlist="$(echo "$response" | _egrep_o "\"value\"\\s*:\\s*\\[\\s*\"[^\"]*\"\\s*]" | cut -d : -f 2 | tr -d "[]\"" | grep -v -- "$txtvalue")"
     values=""
     comma=""
     for v in $vlist; do
@@ -220,7 +241,7 @@ _azure_rest() {
     export _H2="accept: application/json"
     export _H3="Content-Type: application/json"
     # clear headers from previous request to avoid getting wrong http code on timeouts
-    :>"$HTTP_HEADER"
+    : >"$HTTP_HEADER"
     _debug "$ep"
     if [ "$m" != "GET" ]; then
       _secure_debug2 "data $data"
@@ -258,9 +279,10 @@ _azure_rest() {
 
 ## Ref: https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-oauth-service-to-service#request-an-access-token
 _azure_getaccess_token() {
-  tenantID=$1
-  clientID=$2
-  clientSecret=$3
+  managedIdentity=$1
+  tenantID=$2
+  clientID=$3
+  clientSecret=$4
 
   accesstoken="${AZUREDNS_BEARERTOKEN:-$(_readaccountconf_mutable AZUREDNS_BEARERTOKEN)}"
   expires_on="${AZUREDNS_TOKENVALIDTO:-$(_readaccountconf_mutable AZUREDNS_TOKENVALIDTO)}"
@@ -278,17 +300,25 @@ _azure_getaccess_token() {
   fi
   _debug "getting new bearer token"
 
-  export _H1="accept: application/json"
-  export _H2="Content-Type: application/x-www-form-urlencoded"
-
-  body="resource=$(printf "%s" 'https://management.core.windows.net/' | _url_encode)&client_id=$(printf "%s" "$clientID" | _url_encode)&client_secret=$(printf "%s" "$clientSecret" | _url_encode)&grant_type=client_credentials"
-  _secure_debug2 "data $body"
-  response="$(_post "$body" "https://login.microsoftonline.com/$tenantID/oauth2/token" "" "POST")"
-  _ret="$?"
-  _secure_debug2 "response $response"
-  response="$(echo "$response" | _normalizeJson)"
-  accesstoken=$(echo "$response" | _egrep_o "\"access_token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
-  expires_on=$(echo "$response" | _egrep_o "\"expires_on\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
+  if [ "$managedIdentity" = true ]; then
+    # https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http
+    export _H1="Metadata: true"
+    response="$(_get http://169.254.169.254/metadata/identity/oauth2/token\?api-version=2018-02-01\&resource=https://management.azure.com/)"
+    response="$(echo "$response" | _normalizeJson)"
+    accesstoken=$(echo "$response" | _egrep_o "\"access_token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
+    expires_on=$(echo "$response" | _egrep_o "\"expires_on\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
+  else
+    export _H1="accept: application/json"
+    export _H2="Content-Type: application/x-www-form-urlencoded"
+    body="resource=$(printf "%s" 'https://management.core.windows.net/' | _url_encode)&client_id=$(printf "%s" "$clientID" | _url_encode)&client_secret=$(printf "%s" "$clientSecret" | _url_encode)&grant_type=client_credentials"
+    _secure_debug2 "data $body"
+    response="$(_post "$body" "https://login.microsoftonline.com/$tenantID/oauth2/token" "" "POST")"
+    _ret="$?"
+    _secure_debug2 "response $response"
+    response="$(echo "$response" | _normalizeJson)"
+    accesstoken=$(echo "$response" | _egrep_o "\"access_token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
+    expires_on=$(echo "$response" | _egrep_o "\"expires_on\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
+  fi
 
   if [ -z "$accesstoken" ]; then
     _err "no acccess token received. Check your Azure settings see $WIKI"

+ 50 - 9
dnsapi/dns_cf.sh

@@ -7,6 +7,7 @@
 
 #CF_Token="xxxx"
 #CF_Account_ID="xxxx"
+#CF_Zone_ID="xxxx"
 
 CF_Api="https://api.cloudflare.com/client/v4"
 
@@ -19,12 +20,21 @@ dns_cf_add() {
 
   CF_Token="${CF_Token:-$(_readaccountconf_mutable CF_Token)}"
   CF_Account_ID="${CF_Account_ID:-$(_readaccountconf_mutable CF_Account_ID)}"
+  CF_Zone_ID="${CF_Zone_ID:-$(_readaccountconf_mutable CF_Zone_ID)}"
   CF_Key="${CF_Key:-$(_readaccountconf_mutable CF_Key)}"
   CF_Email="${CF_Email:-$(_readaccountconf_mutable CF_Email)}"
 
   if [ "$CF_Token" ]; then
-    _saveaccountconf_mutable CF_Token "$CF_Token"
-    _saveaccountconf_mutable CF_Account_ID "$CF_Account_ID"
+    if [ "$CF_Zone_ID" ]; then
+      _savedomainconf CF_Token "$CF_Token"
+      _savedomainconf CF_Account_ID "$CF_Account_ID"
+      _savedomainconf CF_Zone_ID "$CF_Zone_ID"
+    else
+      _saveaccountconf_mutable CF_Token "$CF_Token"
+      _saveaccountconf_mutable CF_Account_ID "$CF_Account_ID"
+      _clearaccountconf_mutable CF_Zone_ID
+      _clearaccountconf CF_Zone_ID
+    fi
   else
     if [ -z "$CF_Key" ] || [ -z "$CF_Email" ]; then
       CF_Key=""
@@ -42,6 +52,14 @@ dns_cf_add() {
     #save the api key and email to the account conf file.
     _saveaccountconf_mutable CF_Key "$CF_Key"
     _saveaccountconf_mutable CF_Email "$CF_Email"
+
+    _clearaccountconf_mutable CF_Token
+    _clearaccountconf_mutable CF_Account_ID
+    _clearaccountconf_mutable CF_Zone_ID
+    _clearaccountconf CF_Token
+    _clearaccountconf CF_Account_ID
+    _clearaccountconf CF_Zone_ID
+
   fi
 
   _debug "First detect the root zone"
@@ -56,7 +74,7 @@ dns_cf_add() {
   _debug "Getting txt records"
   _cf_rest GET "zones/${_domain_id}/dns_records?type=TXT&name=$fulldomain"
 
-  if ! printf "%s" "$response" | grep \"success\":true >/dev/null; then
+  if ! echo "$response" | tr -d " " | grep \"success\":true >/dev/null; then
     _err "Error"
     return 1
   fi
@@ -91,6 +109,7 @@ dns_cf_rm() {
 
   CF_Token="${CF_Token:-$(_readaccountconf_mutable CF_Token)}"
   CF_Account_ID="${CF_Account_ID:-$(_readaccountconf_mutable CF_Account_ID)}"
+  CF_Zone_ID="${CF_Zone_ID:-$(_readaccountconf_mutable CF_Zone_ID)}"
   CF_Key="${CF_Key:-$(_readaccountconf_mutable CF_Key)}"
   CF_Email="${CF_Email:-$(_readaccountconf_mutable CF_Email)}"
 
@@ -106,17 +125,17 @@ dns_cf_rm() {
   _debug "Getting txt records"
   _cf_rest GET "zones/${_domain_id}/dns_records?type=TXT&name=$fulldomain&content=$txtvalue"
 
-  if ! printf "%s" "$response" | grep \"success\":true >/dev/null; then
-    _err "Error"
+  if ! echo "$response" | tr -d " " | grep \"success\":true >/dev/null; then
+    _err "Error: $response"
     return 1
   fi
 
-  count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2)
+  count=$(echo "$response" | _egrep_o "\"count\": *[^,]*" | cut -d : -f 2 | tr -d " ")
   _debug count "$count"
   if [ "$count" = "0" ]; then
     _info "Don't need to remove."
   else
-    record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | head -n 1)
+    record_id=$(echo "$response" | _egrep_o "\"id\": *\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
     _debug "record_id" "$record_id"
     if [ -z "$record_id" ]; then
       _err "Can not get record id to remove."
@@ -126,7 +145,7 @@ dns_cf_rm() {
       _err "Delete record error."
       return 1
     fi
-    _contains "$response" '"success":true'
+    echo "$response" | tr -d " " | grep \"success\":true >/dev/null
   fi
 
 }
@@ -141,6 +160,28 @@ _get_root() {
   domain=$1
   i=1
   p=1
+
+  # Use Zone ID directly if provided
+  if [ "$CF_Zone_ID" ]; then
+    if ! _cf_rest GET "zones/$CF_Zone_ID"; then
+      return 1
+    else
+      if echo "$response" | tr -d " " | grep \"success\":true >/dev/null; then
+        _domain=$(echo "$response" | _egrep_o "\"name\": *\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
+        if [ "$_domain" ]; then
+          _cutlength=$((${#domain} - ${#_domain} - 1))
+          _sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cutlength")
+          _domain_id=$CF_Zone_ID
+          return 0
+        else
+          return 1
+        fi
+      else
+        return 1
+      fi
+    fi
+  fi
+
   while true; do
     h=$(printf "%s" "$domain" | cut -d . -f $i-100)
     _debug h "$h"
@@ -160,7 +201,7 @@ _get_root() {
     fi
 
     if _contains "$response" "\"name\":\"$h\"" || _contains "$response" '"total_count":1'; then
-      _domain_id=$(echo "$response" | _egrep_o "\[.\"id\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
+      _domain_id=$(echo "$response" | _egrep_o "\[.\"id\": *\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \" | tr -d " ")
       if [ "$_domain_id" ]; then
         _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
         _domain=$h

+ 27 - 3
dnsapi/dns_cloudns.sh

@@ -2,11 +2,14 @@
 
 # Author: Boyan Peychev <boyan at cloudns dot net>
 # Repository: https://github.com/ClouDNS/acme.sh/
+# Editor: I Komang Suryadana
 
 #CLOUDNS_AUTH_ID=XXXXX
 #CLOUDNS_SUB_AUTH_ID=XXXXX
 #CLOUDNS_AUTH_PASSWORD="YYYYYYYYY"
 CLOUDNS_API="https://api.cloudns.net"
+DOMAIN_TYPE=
+DOMAIN_MASTER=
 
 ########  Public functions #####################
 
@@ -61,6 +64,15 @@ dns_cloudns_rm() {
   host="$(echo "$1" | sed "s/\.$zone\$//")"
   record=$2
 
+  _dns_cloudns_get_zone_info "$zone"
+
+  _debug "Type" "$DOMAIN_TYPE"
+  _debug "Cloud Master" "$DOMAIN_MASTER"
+  if _contains "$DOMAIN_TYPE" "cloud"; then
+    zone=$DOMAIN_MASTER
+  fi
+  _debug "ZONE" "$zone"
+
   _dns_cloudns_http_api_call "dns/records.json" "domain-name=$zone&host=$host&type=TXT"
   if ! _contains "$response" "\"id\":"; then
     return 1
@@ -69,7 +81,7 @@ dns_cloudns_rm() {
   for i in $(echo "$response" | tr '{' "\n" | grep "$record"); do
     record_id=$(echo "$i" | tr ',' "\n" | grep -E '^"id"' | sed -re 's/^\"id\"\:\"([0-9]+)\"$/\1/g')
 
-    if [ ! -z "$record_id" ]; then
+    if [ -n "$record_id" ]; then
       _debug zone "$zone"
       _debug host "$host"
       _debug record "$record"
@@ -91,7 +103,7 @@ dns_cloudns_rm() {
 
 ####################  Private functions below ##################################
 _dns_cloudns_init_check() {
-  if [ ! -z "$CLOUDNS_INIT_CHECK_COMPLETED" ]; then
+  if [ -n "$CLOUDNS_INIT_CHECK_COMPLETED" ]; then
     return 0
   fi
 
@@ -134,6 +146,18 @@ _dns_cloudns_init_check() {
   return 0
 }
 
+_dns_cloudns_get_zone_info() {
+  zone=$1
+  _dns_cloudns_http_api_call "dns/get-zone-info.json" "domain-name=$zone"
+  if ! _contains "$response" "\"status\":\"Failed\""; then
+    DOMAIN_TYPE=$(echo "$response" | _egrep_o '"type":"[^"]*"' | cut -d : -f 2 | tr -d '"')
+    if _contains "$DOMAIN_TYPE" "cloud"; then
+      DOMAIN_MASTER=$(echo "$response" | _egrep_o '"cloud-master":"[^"]*"' | cut -d : -f 2 | tr -d '"')
+    fi
+  fi
+  return 0
+}
+
 _dns_cloudns_get_zone_name() {
   i=2
   while true; do
@@ -164,7 +188,7 @@ _dns_cloudns_http_api_call() {
   _debug CLOUDNS_SUB_AUTH_ID "$CLOUDNS_SUB_AUTH_ID"
   _debug CLOUDNS_AUTH_PASSWORD "$CLOUDNS_AUTH_PASSWORD"
 
-  if [ ! -z "$CLOUDNS_SUB_AUTH_ID" ]; then
+  if [ -n "$CLOUDNS_SUB_AUTH_ID" ]; then
     auth_user="sub-auth-id=$CLOUDNS_SUB_AUTH_ID"
   else
     auth_user="auth-id=$CLOUDNS_AUTH_ID"

+ 3 - 3
dnsapi/dns_conoha.sh

@@ -115,9 +115,9 @@ dns_conoha_rm() {
     return 1
   fi
 
-  record_id=$(printf "%s" "$response" | _egrep_o '{[^}]*}' \
-    | grep '"type":"TXT"' | grep "\"data\":\"$txtvalue\"" | _egrep_o "\"id\":\"[^\"]*\"" \
-    | _head_n 1 | cut -d : -f 2 | tr -d \")
+  record_id=$(printf "%s" "$response" | _egrep_o '{[^}]*}' |
+    grep '"type":"TXT"' | grep "\"data\":\"$txtvalue\"" | _egrep_o "\"id\":\"[^\"]*\"" |
+    _head_n 1 | cut -d : -f 2 | tr -d \")
   if [ -z "$record_id" ]; then
     _err "Can not get record id to remove."
     return 1

+ 18 - 18
dnsapi/dns_cyon.sh

@@ -1,7 +1,7 @@
 #!/usr/bin/env sh
 
 ########
-# Custom cyon.ch DNS API for use with [acme.sh](https://github.com/Neilpang/acme.sh)
+# Custom cyon.ch DNS API for use with [acme.sh](https://github.com/acmesh-official/acme.sh)
 #
 # Usage: acme.sh --issue --dns dns_cyon -d www.domain.com
 #
@@ -18,23 +18,23 @@
 ########
 
 dns_cyon_add() {
-  _cyon_load_credentials \
-    && _cyon_load_parameters "$@" \
-    && _cyon_print_header "add" \
-    && _cyon_login \
-    && _cyon_change_domain_env \
-    && _cyon_add_txt \
-    && _cyon_logout
+  _cyon_load_credentials &&
+    _cyon_load_parameters "$@" &&
+    _cyon_print_header "add" &&
+    _cyon_login &&
+    _cyon_change_domain_env &&
+    _cyon_add_txt &&
+    _cyon_logout
 }
 
 dns_cyon_rm() {
-  _cyon_load_credentials \
-    && _cyon_load_parameters "$@" \
-    && _cyon_print_header "delete" \
-    && _cyon_login \
-    && _cyon_change_domain_env \
-    && _cyon_delete_txt \
-    && _cyon_logout
+  _cyon_load_credentials &&
+    _cyon_load_parameters "$@" &&
+    _cyon_print_header "delete" &&
+    _cyon_login &&
+    _cyon_change_domain_env &&
+    _cyon_delete_txt &&
+    _cyon_logout
 }
 
 #########################
@@ -44,7 +44,7 @@ dns_cyon_rm() {
 _cyon_load_credentials() {
   # Convert loaded password to/from base64 as needed.
   if [ "${CY_Password_B64}" ]; then
-    CY_Password="$(printf "%s" "${CY_Password_B64}" | _dbase64 "multiline")"
+    CY_Password="$(printf "%s" "${CY_Password_B64}" | _dbase64)"
   elif [ "${CY_Password}" ]; then
     CY_Password_B64="$(printf "%s" "${CY_Password}" | _base64)"
   fi
@@ -66,7 +66,7 @@ _cyon_load_credentials() {
   _debug "Save credentials to account.conf"
   _saveaccountconf CY_Username "${CY_Username}"
   _saveaccountconf CY_Password_B64 "$CY_Password_B64"
-  if [ ! -z "${CY_OTP_Secret}" ]; then
+  if [ -n "${CY_OTP_Secret}" ]; then
     _saveaccountconf CY_OTP_Secret "$CY_OTP_Secret"
   else
     _clearaccountconf CY_OTP_Secret
@@ -164,7 +164,7 @@ _cyon_login() {
   # todo: instead of just checking if the env variable is defined, check if we actually need to do a 2FA auth request.
 
   # 2FA authentication with OTP?
-  if [ ! -z "${CY_OTP_Secret}" ]; then
+  if [ -n "${CY_OTP_Secret}" ]; then
     _info "  - Authorising with OTP code..."
 
     if ! _exists oathtool; then

+ 18 - 18
dnsapi/dns_da.sh

@@ -9,7 +9,7 @@
 #
 # User must provide login data and URL to DirectAdmin incl. port.
 # You can create login key, by using the Login Keys function
-# ( https://da.example.com:8443/CMD_LOGIN_KEYS ), which only has access to 
+# ( https://da.example.com:8443/CMD_LOGIN_KEYS ), which only has access to
 # - CMD_API_DNS_CONTROL
 # - CMD_API_SHOW_DOMAINS
 #
@@ -115,23 +115,23 @@ _da_api() {
   _debug response "$response"
 
   case "${cmd}" in
-    CMD_API_DNS_CONTROL)
-      # Parse the result in general
-      # error=0&text=Records Deleted&details=
-      # error=1&text=Cannot View Dns Record&details=No domain provided
-      err_field="$(_getfield "$response" 1 '&')"
-      txt_field="$(_getfield "$response" 2 '&')"
-      details_field="$(_getfield "$response" 3 '&')"
-      error="$(_getfield "$err_field" 2 '=')"
-      text="$(_getfield "$txt_field" 2 '=')"
-      details="$(_getfield "$details_field" 2 '=')"
-      _debug "error: ${error}, text: ${text}, details: ${details}"
-      if [ "$error" != "0" ]; then
-        _err "error $response"
-        return 1
-      fi
-      ;;
-    CMD_API_SHOW_DOMAINS) ;;
+  CMD_API_DNS_CONTROL)
+    # Parse the result in general
+    # error=0&text=Records Deleted&details=
+    # error=1&text=Cannot View Dns Record&details=No domain provided
+    err_field="$(_getfield "$response" 1 '&')"
+    txt_field="$(_getfield "$response" 2 '&')"
+    details_field="$(_getfield "$response" 3 '&')"
+    error="$(_getfield "$err_field" 2 '=')"
+    text="$(_getfield "$txt_field" 2 '=')"
+    details="$(_getfield "$details_field" 2 '=')"
+    _debug "error: ${error}, text: ${text}, details: ${details}"
+    if [ "$error" != "0" ]; then
+      _err "error $response"
+      return 1
+    fi
+    ;;
+  CMD_API_SHOW_DOMAINS) ;;
   esac
   return 0
 }

+ 2 - 2
dnsapi/dns_ddnss.sh

@@ -77,7 +77,7 @@ dns_ddnss_rm() {
 
   # Now remove the TXT record from DDNS DNS
   _info "Trying to remove TXT record"
-  if _ddnss_rest GET "key=$DDNSS_Token&host=$_ddnss_domain&txtm=1&txt=."; then
+  if _ddnss_rest GET "key=$DDNSS_Token&host=$_ddnss_domain&txtm=2"; then
     if [ "$response" = "Updated 1 hostname." ]; then
       _info "TXT record has been successfully removed from your DDNSS domain."
       return 0
@@ -119,7 +119,7 @@ _ddnss_rest() {
 
   # DDNSS uses GET to update domain info
   if [ "$method" = "GET" ]; then
-    response="$(_get "$url" | sed 's/<[a-zA-Z\/][^>]*>//g' | _tail_n 1)"
+    response="$(_get "$url" | sed 's/<[a-zA-Z\/][^>]*>//g' | tr -s "\n" | _tail_n 1)"
   else
     _err "Unsupported method"
     return 1

+ 11 - 18
dnsapi/dns_desec.sh

@@ -20,21 +20,17 @@ dns_desec_add() {
   _debug txtvalue "$txtvalue"
 
   DEDYN_TOKEN="${DEDYN_TOKEN:-$(_readaccountconf_mutable DEDYN_TOKEN)}"
-  DEDYN_NAME="${DEDYN_NAME:-$(_readaccountconf_mutable DEDYN_NAME)}"
 
-  if [ -z "$DEDYN_TOKEN" ] || [ -z "$DEDYN_NAME" ]; then
+  if [ -z "$DEDYN_TOKEN" ]; then
     DEDYN_TOKEN=""
-    DEDYN_NAME=""
-    _err "You did not specify DEDYN_TOKEN and DEDYN_NAME yet."
+    _err "You did not specify DEDYN_TOKEN yet."
     _err "Please create your key and try again."
     _err "e.g."
     _err "export DEDYN_TOKEN=d41d8cd98f00b204e9800998ecf8427e"
-    _err "export DEDYN_NAME=foobar.dedyn.io"
     return 1
   fi
-  #save the api token and name to the account conf file.
+  #save the api token to the account conf file.
   _saveaccountconf_mutable DEDYN_TOKEN "$DEDYN_TOKEN"
-  _saveaccountconf_mutable DEDYN_NAME "$DEDYN_NAME"
 
   _debug "First detect the root zone"
   if ! _get_root "$fulldomain" "$REST_API/"; then
@@ -47,7 +43,7 @@ dns_desec_add() {
   # Get existing TXT record
   _debug "Getting txt records"
   txtvalues="\"\\\"$txtvalue\\\"\""
-  _desec_rest GET "$REST_API/$DEDYN_NAME/rrsets/$_sub_domain/TXT/"
+  _desec_rest GET "$REST_API/$_domain/rrsets/$_sub_domain/TXT/"
 
   if [ "$_code" = "200" ]; then
     oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"\\S*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")"
@@ -61,9 +57,9 @@ dns_desec_add() {
   fi
   _debug txtvalues "$txtvalues"
   _info "Adding record"
-  body="[{\"subname\":\"$_sub_domain\", \"type\":\"TXT\", \"records\":[$txtvalues], \"ttl\":60}]"
+  body="[{\"subname\":\"$_sub_domain\", \"type\":\"TXT\", \"records\":[$txtvalues], \"ttl\":3600}]"
 
-  if _desec_rest PUT "$REST_API/$DEDYN_NAME/rrsets/" "$body"; then
+  if _desec_rest PUT "$REST_API/$_domain/rrsets/" "$body"; then
     if _contains "$response" "$txtvalue"; then
       _info "Added, OK"
       return 0
@@ -87,16 +83,13 @@ dns_desec_rm() {
   _debug txtvalue "$txtvalue"
 
   DEDYN_TOKEN="${DEDYN_TOKEN:-$(_readaccountconf_mutable DEDYN_TOKEN)}"
-  DEDYN_NAME="${DEDYN_NAME:-$(_readaccountconf_mutable DEDYN_NAME)}"
 
-  if [ -z "$DEDYN_TOKEN" ] || [ -z "$DEDYN_NAME" ]; then
+  if [ -z "$DEDYN_TOKEN" ]; then
     DEDYN_TOKEN=""
-    DEDYN_NAME=""
-    _err "You did not specify DEDYN_TOKEN and DEDYN_NAME yet."
+    _err "You did not specify DEDYN_TOKEN yet."
     _err "Please create your key and try again."
     _err "e.g."
     _err "export DEDYN_TOKEN=d41d8cd98f00b204e9800998ecf8427e"
-    _err "export DEDYN_NAME=foobar.dedyn.io"
     return 1
   fi
 
@@ -112,7 +105,7 @@ dns_desec_rm() {
   # Get existing TXT record
   _debug "Getting txt records"
   txtvalues=""
-  _desec_rest GET "$REST_API/$DEDYN_NAME/rrsets/$_sub_domain/TXT/"
+  _desec_rest GET "$REST_API/$_domain/rrsets/$_sub_domain/TXT/"
 
   if [ "$_code" = "200" ]; then
     oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"\\S*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")"
@@ -130,8 +123,8 @@ dns_desec_rm() {
   _debug txtvalues "$txtvalues"
 
   _info "Deleting record"
-  body="[{\"subname\":\"$_sub_domain\", \"type\":\"TXT\", \"records\":[$txtvalues], \"ttl\":60}]"
-  _desec_rest PUT "$REST_API/$DEDYN_NAME/rrsets/" "$body"
+  body="[{\"subname\":\"$_sub_domain\", \"type\":\"TXT\", \"records\":[$txtvalues], \"ttl\":3600}]"
+  _desec_rest PUT "$REST_API/$_domain/rrsets/" "$body"
   if [ "$_code" = "200" ]; then
     _info "Deleted, OK"
     return 0

+ 7 - 8
dnsapi/dns_dgon.sh

@@ -22,7 +22,7 @@ dns_dgon_add() {
   txtvalue=$2
 
   DO_API_KEY="${DO_API_KEY:-$(_readaccountconf_mutable DO_API_KEY)}"
-  # Check if API Key Exist
+  # Check if API Key Exists
   if [ -z "$DO_API_KEY" ]; then
     DO_API_KEY=""
     _err "You did not specify DigitalOcean API key."
@@ -77,7 +77,7 @@ dns_dgon_rm() {
   txtvalue=$2
 
   DO_API_KEY="${DO_API_KEY:-$(_readaccountconf_mutable DO_API_KEY)}"
-  # Check if API Key Exist
+  # Check if API Key Exists
   if [ -z "$DO_API_KEY" ]; then
     DO_API_KEY=""
     _err "You did not specify DigitalOcean API key."
@@ -122,12 +122,12 @@ dns_dgon_rm() {
     ## 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\"")"
 
-    if [ ! -z "$record" ]; then
+    if [ -n "$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
+      if [ -n "$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
@@ -192,6 +192,7 @@ _get_base_domain() {
   ## get URL for the list of domains
   ## 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"
+  found=""
 
   ## while we dont have a matching domain we keep going
   while [ -z "$found" ]; do
@@ -205,9 +206,7 @@ _get_base_domain() {
     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
+    i=1
     while [ $i -gt 0 ]; do
       ## get next longest domain
       _domain=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-"$MAX_DOM")
@@ -218,7 +217,7 @@ _get_base_domain() {
       ## 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
+      if [ -n "$found" ]; then
         ## exists - exit loop returning the parts
         sub_point=$(_math $i - 1)
         _sub_domain=$(printf "%s" "$fulldomain" | cut -d . -f 1-"$sub_point")

+ 12 - 12
dnsapi/dns_do.sh

@@ -67,14 +67,14 @@ _dns_do_list_rrs() {
     _err "getRRList origin ${_domain} failed"
     return 1
   fi
-  _rr_list="$(echo "${response}" \
-    | tr -d "\n\r\t" \
-    | sed -e 's/<item xsi:type="ns2:Map">/\n/g' \
-    | grep ">$(_regexcape "$fulldomain")</value>" \
-    | sed -e 's/<\/item>/\n/g' \
-    | grep '>id</key><value' \
-    | _egrep_o '>[0-9]{1,16}<' \
-    | tr -d '><')"
+  _rr_list="$(echo "${response}" |
+    tr -d "\n\r\t" |
+    sed -e 's/<item xsi:type="ns2:Map">/\n/g' |
+    grep ">$(_regexcape "$fulldomain")</value>" |
+    sed -e 's/<\/item>/\n/g' |
+    grep '>id</key><value' |
+    _egrep_o '>[0-9]{1,16}<' |
+    tr -d '><')"
   [ "${_rr_list}" ]
 }
 
@@ -120,10 +120,10 @@ _get_root() {
   i=1
 
   _dns_do_soap getDomainList
-  _all_domains="$(echo "${response}" \
-    | tr -d "\n\r\t " \
-    | _egrep_o 'domain</key><value[^>]+>[^<]+' \
-    | sed -e 's/^domain<\/key><value[^>]*>//g')"
+  _all_domains="$(echo "${response}" |
+    tr -d "\n\r\t " |
+    _egrep_o 'domain</key><value[^>]+>[^<]+' |
+    sed -e 's/^domain<\/key><value[^>]*>//g')"
 
   while true; do
     h=$(printf "%s" "$domain" | cut -d . -f $i-100)

+ 3 - 3
dnsapi/dns_doapi.sh

@@ -1,11 +1,11 @@
 #!/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: 
+#
+# Provide the required LetsEncrypt token like this:
 # DO_LETOKEN="FmD408PdqT1E269gUK57"
 
 DO_API="https://www.do.de/api/letsencrypt"

+ 7 - 7
dnsapi/dns_dp.sh

@@ -53,7 +53,7 @@ dns_dp_rm() {
     return 1
   fi
 
-  if ! _rest POST "Record.List" "login_token=$DP_Id,$DP_Key&format=json&domain_id=$_domain_id&sub_domain=$_sub_domain"; then
+  if ! _rest POST "Record.List" "login_token=$DP_Id,$DP_Key&format=json&lang=en&domain_id=$_domain_id&sub_domain=$_sub_domain"; then
     _err "Record.Lis error."
     return 1
   fi
@@ -70,12 +70,12 @@ dns_dp_rm() {
     return 1
   fi
 
-  if ! _rest POST "Record.Remove" "login_token=$DP_Id,$DP_Key&format=json&domain_id=$_domain_id&record_id=$record_id"; then
+  if ! _rest POST "Record.Remove" "login_token=$DP_Id,$DP_Key&format=json&lang=en&domain_id=$_domain_id&record_id=$record_id"; then
     _err "Record.Remove error."
     return 1
   fi
 
-  _contains "$response" "Action completed successful"
+  _contains "$response" "successful"
 
 }
 
@@ -89,11 +89,11 @@ add_record() {
 
   _info "Adding record"
 
-  if ! _rest POST "Record.Create" "login_token=$DP_Id,$DP_Key&format=json&domain_id=$_domain_id&sub_domain=$_sub_domain&record_type=TXT&value=$txtvalue&record_line=默认"; then
+  if ! _rest POST "Record.Create" "login_token=$DP_Id,$DP_Key&format=json&lang=en&domain_id=$_domain_id&sub_domain=$_sub_domain&record_type=TXT&value=$txtvalue&record_line=%E9%BB%98%E8%AE%A4"; then
     return 1
   fi
 
-  _contains "$response" "Action completed successful" || _contains "$response" "Domain record already exists"
+  _contains "$response" "successful" || _contains "$response" "Domain record already exists"
 }
 
 ####################  Private functions below ##################################
@@ -113,11 +113,11 @@ _get_root() {
       return 1
     fi
 
-    if ! _rest POST "Domain.Info" "login_token=$DP_Id,$DP_Key&format=json&domain=$h"; then
+    if ! _rest POST "Domain.Info" "login_token=$DP_Id,$DP_Key&format=json&lang=en&domain=$h"; then
       return 1
     fi
 
-    if _contains "$response" "Action completed successful"; then
+    if _contains "$response" "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

+ 8 - 8
dnsapi/dns_dpi.sh

@@ -53,7 +53,7 @@ dns_dpi_rm() {
     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
+  if ! _rest POST "Record.List" "login_token=$DPI_Id,$DPI_Key&format=json&domain_id=$_domain_id&sub_domain=$_sub_domain"; then
     _err "Record.Lis error."
     return 1
   fi
@@ -63,19 +63,19 @@ dns_dpi_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."
     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
+  if ! _rest POST "Record.Remove" "login_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"
+  _contains "$response" "Operation successful"
 
 }
 
@@ -89,11 +89,11 @@ add_record() {
 
   _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
+  if ! _rest POST "Record.Create" "login_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"
+  _contains "$response" "Operation successful" || _contains "$response" "Domain record already exists"
 }
 
 ####################  Private functions below ##################################
@@ -113,11 +113,11 @@ _get_root() {
       return 1
     fi
 
-    if ! _rest POST "Domain.Info" "user_token=$DPI_Id,$DPI_Key&format=json&domain=$h"; then
+    if ! _rest POST "Domain.Info" "login_token=$DPI_Id,$DPI_Key&format=json&domain=$h"; then
       return 1
     fi
 
-    if _contains "$response" "Action completed successful"; then
+    if _contains "$response" "Operation 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

+ 11 - 7
dnsapi/dns_duckdns.sh

@@ -12,7 +12,7 @@
 
 DuckDNS_API="https://www.duckdns.org/update"
 
-########  Public functions #####################
+########  Public functions ######################
 
 #Usage: dns_duckdns_add _acme-challenge.domain.duckdns.org "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
 dns_duckdns_add() {
@@ -91,13 +91,12 @@ dns_duckdns_rm() {
 
 ####################  Private functions below ##################################
 
-#fulldomain=_acme-challenge.domain.duckdns.org
-#returns
-# _duckdns_domain=domain
+# fulldomain may be 'domain.duckdns.org' (if using --domain-alias) or '_acme-challenge.domain.duckdns.org'
+# either way, return 'domain'. (duckdns does not allow further subdomains and restricts domains to [a-z0-9-].)
 _duckdns_get_domain() {
 
   # We'll extract the domain/username from full domain
-  _duckdns_domain="$(printf "%s" "$fulldomain" | _lower_case | _egrep_o '[.][^.][^.]*[.]duckdns.org' | cut -d . -f 2)"
+  _duckdns_domain="$(printf "%s" "$fulldomain" | _lower_case | _egrep_o '^(_acme-challenge\.)?([a-z0-9-]+\.)+duckdns\.org' | sed -n 's/^\([^.]\{1,\}\.\)*\([a-z0-9-]\{1,\}\)\.duckdns\.org$/\2/p;')"
 
   if [ -z "$_duckdns_domain" ]; then
     _err "Error extracting the domain."
@@ -113,16 +112,21 @@ _duckdns_rest() {
   param="$2"
   _debug param "$param"
   url="$DuckDNS_API?$param"
+  if [ -n "$DEBUG" ] && [ "$DEBUG" -gt 0 ]; then
+    url="$url&verbose=true"
+  fi
   _debug url "$url"
 
   # DuckDNS uses GET to update domain info
   if [ "$method" = "GET" ]; then
     response="$(_get "$url")"
+    _debug2 response "$response"
+    if [ -n "$DEBUG" ] && [ "$DEBUG" -gt 0 ] && _contains "$response" "UPDATED" && _contains "$response" "OK"; then
+      response="OK"
+    fi
   else
     _err "Unsupported method"
     return 1
   fi
-
-  _debug2 response "$response"
   return 0
 }

+ 5 - 5
dnsapi/dns_durabledns.sh

@@ -147,11 +147,11 @@ _dd_soap() {
 
   # build SOAP XML
   _xml='<?xml version="1.0" encoding="utf-8"?>
-<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" 
-xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" 
-xmlns:tns="urn:'$_urn'" 
-xmlns:types="urn:'$_urn'/encodedTypes" 
-xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
+<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
+xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
+xmlns:tns="urn:'$_urn'"
+xmlns:types="urn:'$_urn'/encodedTypes"
+xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">'"$body"'</soap:Body>
 </soap:Envelope>'

+ 4 - 0
dnsapi/dns_dynu.sh

@@ -216,6 +216,10 @@ _dynu_authentication() {
     _err "Authentication failed."
     return 1
   fi
+  if _contains "$response" "Authentication Exception"; then
+    _err "Authentication failed."
+    return 1
+  fi
   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

+ 1 - 1
dnsapi/dns_euserv.sh

@@ -127,7 +127,7 @@ dns_euserv_rm() {
   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 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"

+ 11 - 11
dnsapi/dns_freedns.sh

@@ -7,7 +7,7 @@
 #
 #Author: David Kerr
 #Report Bugs here: https://github.com/dkerr64/acme.sh
-#or here... https://github.com/Neilpang/acme.sh/issues/2305
+#or here... https://github.com/acmesh-official/acme.sh/issues/2305
 #
 ########  Public functions #####################
 
@@ -303,10 +303,10 @@ _freedns_domain_id() {
       return 1
     fi
 
-    domain_id="$(echo "$htmlpage" | tr -d "[:space:]" | sed 's/<tr>/@<tr>/g' | tr '@' '\n' \
-      | grep "<td>$search_domain</td>\|<td>$search_domain(.*)</td>" \
-      | _egrep_o "edit\.php\?edit_domain_id=[0-9a-zA-Z]+" \
-      | cut -d = -f 2)"
+    domain_id="$(echo "$htmlpage" | tr -d " \t\r\n\v\f" | sed 's/<tr>/@<tr>/g' | tr '@' '\n' |
+      grep "<td>$search_domain</td>\|<td>$search_domain(.*)</td>" |
+      sed -n 's/.*\(edit\.php?edit_domain_id=[0-9a-zA-Z]*\).*/\1/p' |
+      cut -d = -f 2)"
     # The above beauty extracts domain ID from the html page...
     # strip out all blank space and new lines. Then insert newlines
     # before each table row <tr>
@@ -349,17 +349,17 @@ _freedns_data_id() {
       return 1
     fi
 
-    data_id="$(echo "$htmlpage" | tr -d "[:space:]" | sed 's/<tr>/@<tr>/g' | tr '@' '\n' \
-      | grep "<td[a-zA-Z=#]*>$record_type</td>" \
-      | grep "<ahref.*>$search_domain</a>" \
-      | _egrep_o "edit\.php\?data_id=[0-9a-zA-Z]+" \
-      | cut -d = -f 2)"
+    data_id="$(echo "$htmlpage" | tr -d " \t\r\n\v\f" | sed 's/<tr>/@<tr>/g' | tr '@' '\n' |
+      grep "<td[a-zA-Z=#]*>$record_type</td>" |
+      grep "<ahref.*>$search_domain</a>" |
+      sed -n 's/.*\(edit\.php?data_id=[0-9a-zA-Z]*\).*/\1/p' |
+      cut -d = -f 2)"
     # The above beauty extracts data ID from the html page...
     # strip out all blank space and new lines. Then insert newlines
     # before each table row <tr>
     # search for the record type withing each row (e.g. TXT)
     # search for the domain within each row (which is within a <a..>
-    # </a> anchor. And finally extract the domain ID.         
+    # </a> anchor. And finally extract the domain ID.
     if [ -n "$data_id" ]; then
       printf "%s" "$data_id"
       return 0

+ 8 - 8
dnsapi/dns_gandi_livedns.sh

@@ -69,9 +69,9 @@ dns_gandi_livedns_rm() {
 
   _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")"
+    "{\"rrset_ttl\": 300, \"rrset_values\": $_new_rrset_values}" &&
+    _contains "$response" '{"message": "DNS Record Created"}' &&
+    _info "Removing record $(__green "success")"
 }
 
 ####################  Private functions below ##################################
@@ -125,9 +125,9 @@ _dns_gandi_append_record() {
   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")"
+    "{\"rrset_ttl\": 300, \"rrset_values\": $_rrset_values}" &&
+    _contains "$response" '{"message": "DNS Record Created"}' &&
+    _info "Adding record $(__green "success")"
 }
 
 _dns_gandi_existing_rrset_values() {
@@ -145,8 +145,8 @@ _dns_gandi_existing_rrset_values() {
     return 1
   fi
   _debug "Already has TXT record."
-  _rrset_values=$(echo "$response" | _egrep_o 'rrset_values.*\[.*\]' \
-    | _egrep_o '\[".*\"]')
+  _rrset_values=$(echo "$response" | _egrep_o 'rrset_values.*\[.*\]' |
+    _egrep_o '\[".*\"]')
   return 0
 }
 

+ 14 - 11
dnsapi/dns_gcloud.sh

@@ -39,7 +39,7 @@ dns_gcloud_rm() {
   _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 $?
+  echo "$rrdatas" | grep -F -v -- "\"$txtvalue\"" | _dns_gcloud_add_rrs || return $?
   _dns_gcloud_execute_tr || return $?
 
   _info "$fulldomain record added"
@@ -78,8 +78,8 @@ _dns_gcloud_execute_tr() {
   for i in $(seq 1 120); do
     if gcloud dns record-sets changes list \
       --zone="$managedZone" \
-      --filter='status != done' \
-      | grep -q '^.*'; then
+      --filter='status != done' |
+      grep -q '^.*'; then
       _info "_dns_gcloud_execute_tr: waiting for transaction to be comitted ($i/120)..."
       sleep 5
     else
@@ -98,7 +98,7 @@ _dns_gcloud_remove_rrs() {
     --ttl="$ttl" \
     --type=TXT \
     --zone="$managedZone" \
-    --transaction-file="$tr"; then
+    --transaction-file="$tr" --; then
     _debug tr "$(cat "$tr")"
     rm -r "$trd"
     _err "_dns_gcloud_remove_rrs: failed to remove RRs"
@@ -113,7 +113,7 @@ _dns_gcloud_add_rrs() {
     --ttl="$ttl" \
     --type=TXT \
     --zone="$managedZone" \
-    --transaction-file="$tr"; then
+    --transaction-file="$tr" --; then
     _debug tr "$(cat "$tr")"
     rm -r "$trd"
     _err "_dns_gcloud_add_rrs: failed to add RRs"
@@ -131,17 +131,17 @@ _dns_gcloud_find_zone() {
     filter="$filter$part. "
     part="$(echo "$part" | sed 's/[^.]*\.*//')"
   done
-  filter="$filter)"
+  filter="$filter) AND visibility=public"
   _debug filter "$filter"
 
   # List domains and find the zone with the deepest sub-domain (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
+    --filter="$filter" |
+    while read -r dnsName name; do
       printf "%s\t%s\t%s\n" "$(echo "$name" | awk -F"." '{print NF-1}')" "$dnsName" "$name"
-    done \
-      | sort -n -r | _head_n 1 | cut -f2,3 | grep '^.*'); then
+    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
@@ -163,5 +163,8 @@ _dns_gcloud_get_rrdatas() {
     return 1
   fi
   ttl=$(echo "$rrdatas" | cut -f1)
-  rrdatas=$(echo "$rrdatas" | cut -f2 | sed 's/","/"\n"/g')
+  # starting with version 353.0.0 gcloud seems to
+  # separate records with a semicolon instead of commas
+  # see also https://cloud.google.com/sdk/docs/release-notes#35300_2021-08-17
+  rrdatas=$(echo "$rrdatas" | cut -f2 | sed 's/"[,;]"/"\n"/g')
 }

+ 41 - 17
dnsapi/dns_gd.sh

@@ -1,10 +1,12 @@
 #!/usr/bin/env sh
 
 #Godaddy domain api
+# Get API key and secret from https://developer.godaddy.com/
 #
-#GD_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
+# GD_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
+# GD_Secret="asdfsdfsfsdfsdfdfsdf"
 #
-#GD_Secret="asdfsdfsfsdfsdfdfsdf"
+# Ex.: acme.sh --issue --staging --dns dns_gd -d "*.s.example.com" -d "s.example.com"
 
 GD_Api="https://api.godaddy.com/v1"
 
@@ -51,7 +53,8 @@ dns_gd_add() {
   _add_data="{\"data\":\"$txtvalue\"}"
   for t in $(echo "$response" | tr '{' "\n" | grep "\"name\":\"$_sub_domain\"" | tr ',' "\n" | grep '"data"' | cut -d : -f 2); do
     _debug2 t "$t"
-    if [ "$t" ]; then
+    # ignore empty (previously removed) records, to prevent useless _acme-challenge TXT entries
+    if [ "$t" ] && [ "$t" != '""' ]; then
       _add_data="$_add_data,{\"data\":$t}"
     fi
   done
@@ -59,13 +62,25 @@ dns_gd_add() {
 
   _info "Adding record"
   if _gd_rest PUT "domains/$_domain/records/TXT/$_sub_domain" "[$_add_data]"; then
-    _info "Added, sleeping 10 seconds"
-    _sleep 10
-    #todo: check if the record takes effect
-    return 0
+    _debug "Checking updated records of '${fulldomain}'"
+
+    if ! _gd_rest GET "domains/$_domain/records/TXT/$_sub_domain"; then
+      _err "Validating TXT record for '${fulldomain}' with rest error [$?]." "$response"
+      return 1
+    fi
+
+    if ! _contains "$response" "$txtvalue"; then
+      _err "TXT record '${txtvalue}' for '${fulldomain}', value wasn't set!"
+      return 1
+    fi
+  else
+    _err "Add txt record error, value '${txtvalue}' for '${fulldomain}' was not set."
+    return 1
   fi
-  _err "Add txt record error."
-  return 1
+
+  _sleep 10
+  _info "Added TXT record '${txtvalue}' for '${fulldomain}'."
+  return 0
 }
 
 #fulldomain
@@ -91,7 +106,7 @@ dns_gd_rm() {
   fi
 
   if ! _contains "$response" "$txtvalue"; then
-    _info "The record is not existing, skip"
+    _info "The record does not exist, skip"
     return 0
   fi
 
@@ -107,11 +122,20 @@ dns_gd_rm() {
     fi
   done
   if [ -z "$_add_data" ]; then
-    _add_data="{\"data\":\"\"}"
+    # delete empty record
+    _debug "Delete last record for '${fulldomain}'"
+    if ! _gd_rest DELETE "domains/$_domain/records/TXT/$_sub_domain"; then
+      _err "Cannot delete empty TXT record for '$fulldomain'"
+      return 1
+    fi
+  else
+    # remove specific TXT value, keeping other entries
+    _debug2 _add_data "$_add_data"
+    if ! _gd_rest PUT "domains/$_domain/records/TXT/$_sub_domain" "[$_add_data]"; then
+      _err "Cannot update TXT record for '$fulldomain'"
+      return 1
+    fi
   fi
-  _debug2 _add_data "$_add_data"
-
-  _gd_rest PUT "domains/$_domain/records/TXT/$_sub_domain" "[$_add_data]"
 }
 
 ####################  Private functions below ##################################
@@ -156,15 +180,15 @@ _gd_rest() {
   export _H1="Authorization: sso-key $GD_Key:$GD_Secret"
   export _H2="Content-Type: application/json"
 
-  if [ "$data" ]; then
-    _debug data "$data"
+  if [ "$data" ] || [ "$m" = "DELETE" ]; then
+    _debug "data ($m): " "$data"
     response="$(_post "$data" "$GD_Api/$ep" "" "$m")"
   else
     response="$(_get "$GD_Api/$ep")"
   fi
 
   if [ "$?" != "0" ]; then
-    _err "error $ep"
+    _err "error on rest call ($m): $ep"
     return 1
   fi
   _debug2 response "$response"

+ 4 - 4
dnsapi/dns_he.sh

@@ -24,7 +24,7 @@ dns_he_add() {
   if [ -z "$HE_Username" ] || [ -z "$HE_Password" ]; then
     HE_Username=
     HE_Password=
-    _err "No auth details provided. Please set user credentials using the \$HE_Username and \$HE_Password envoronment variables."
+    _err "No auth details provided. Please set user credentials using the \$HE_Username and \$HE_Password environment variables."
     return 1
   fi
   _saveaccountconf_mutable HE_Username "$HE_Username"
@@ -85,7 +85,7 @@ dns_he_rm() {
     _debug "The txt record is not found, just skip"
     return 0
   fi
-  _record_id="$(echo "$response" | tr -d "#" | sed "s/<tr/#<tr/g" | tr -d "\n" | tr "#" "\n" | grep "$_full_domain" | grep '"dns_tr"' | grep "$_txt_value" | cut -d '"' -f 4)"
+  _record_id="$(echo "$response" | tr -d "#" | sed "s/<tr/#<tr/g" | tr -d "\n" | tr "#" "\n" | grep "$_full_domain" | grep '"dns_tr"' | grep -- "$_txt_value" | cut -d '"' -f 4)"
   _debug2 _record_id "$_record_id"
   if [ -z "$_record_id" ]; then
     _err "Can not find record id"
@@ -101,8 +101,8 @@ dns_he_rm() {
   body="$body&hosted_dns_editzone=1"
   body="$body&hosted_dns_delrecord=1"
   body="$body&hosted_dns_delconfirm=delete"
-  _post "$body" "https://dns.he.net/" \
-    | grep '<div id="dns_status" onClick="hideThis(this);">Successfully removed record.</div>' \
+  _post "$body" "https://dns.he.net/" |
+    grep '<div id="dns_status" onClick="hideThis(this);">Successfully removed record.</div>' \
       >/dev/null
   exit_code="$?"
   if [ "$exit_code" -eq 0 ]; then

+ 4 - 4
dnsapi/dns_hexonet.sh

@@ -42,7 +42,7 @@ dns_hexonet_add() {
   _debug _domain "$_domain"
 
   _debug "Getting txt records"
-  _hexonet_rest "&command=QueryDNSZoneRRList&dnszone=${h}.&RRTYPE=TXT"
+  _hexonet_rest "command=QueryDNSZoneRRList&dnszone=${h}.&RRTYPE=TXT"
 
   if ! _contains "$response" "CODE=200"; then
     _err "Error"
@@ -88,7 +88,7 @@ dns_hexonet_rm() {
   _debug _domain "$_domain"
 
   _debug "Getting txt records"
-  _hexonet_rest "&command=QueryDNSZoneRRList&dnszone=${h}.&RRTYPE=TXT&RR=${txtvalue}"
+  _hexonet_rest "command=QueryDNSZoneRRList&dnszone=${h}.&RRTYPE=TXT&RR=${_sub_domain}%20IN%20TXT%20\"${txtvalue}\""
 
   if ! _contains "$response" "CODE=200"; then
     _err "Error"
@@ -100,7 +100,7 @@ dns_hexonet_rm() {
   if [ "$count" = "0" ]; then
     _info "Don't need to remove."
   else
-    if ! _hexonet_rest "&command=UpdateDNSZone&dnszone=${_domain}.&delrr0='${_sub_domain}%20IN%20TXT%20\"${txtvalue}\""; then
+    if ! _hexonet_rest "command=UpdateDNSZone&dnszone=${_domain}.&delrr0=${_sub_domain}%20IN%20TXT%20\"${txtvalue}\""; then
       _err "Delete record error."
       return 1
     fi
@@ -126,7 +126,7 @@ _get_root() {
       return 1
     fi
 
-    if ! _hexonet_rest "&command=QueryDNSZoneRRList&dnszone=${h}."; then
+    if ! _hexonet_rest "command=QueryDNSZoneRRList&dnszone=${h}."; then
       return 1
     fi
 

+ 17 - 8
dnsapi/dns_infoblox.sh

@@ -9,7 +9,6 @@ dns_infoblox_add() {
   ## Nothing to see here, just some housekeeping
   fulldomain=$1
   txtvalue=$2
-  baseurlnObject="https://$Infoblox_Server/wapi/v2.2.2/record:txt?name=$fulldomain&text=$txtvalue&view=$Infoblox_View"
 
   _info "Using Infoblox API"
   _debug fulldomain "$fulldomain"
@@ -19,12 +18,13 @@ dns_infoblox_add() {
   if [ -z "$Infoblox_Creds" ] || [ -z "$Infoblox_Server" ]; then
     Infoblox_Creds=""
     Infoblox_Server=""
-    _err "You didn't specify the credentials, server or infoblox view yet (Infoblox_Creds, Infoblox_Server and Infoblox_View)."
-    _err "Please set them via EXPORT ([username:password], [ip or hostname]) and try again."
+    _err "You didn't specify the Infoblox credentials or server (Infoblox_Creds; Infoblox_Server)."
+    _err "Please set them via EXPORT Infoblox_Creds=username:password or EXPORT Infoblox_server=ip/hostname and try again."
     return 1
   fi
 
   if [ -z "$Infoblox_View" ]; then
+    _info "No Infoblox_View set, using fallback value 'default'"
     Infoblox_View="default"
   fi
 
@@ -33,6 +33,9 @@ dns_infoblox_add() {
   _saveaccountconf Infoblox_Server "$Infoblox_Server"
   _saveaccountconf Infoblox_View "$Infoblox_View"
 
+  ## URLencode Infoblox View to deal with e.g. spaces
+  Infoblox_ViewEncoded=$(printf "%b" "$Infoblox_View" | _url_encode)
+
   ## Base64 encode the credentials
   Infoblox_CredsEncoded=$(printf "%b" "$Infoblox_Creds" | _base64)
 
@@ -40,11 +43,14 @@ dns_infoblox_add() {
   export _H1="Accept-Language:en-US"
   export _H2="Authorization: Basic $Infoblox_CredsEncoded"
 
+  ## Construct the request URL
+  baseurlnObject="https://$Infoblox_Server/wapi/v2.2.2/record:txt?name=$fulldomain&text=$txtvalue&view=${Infoblox_ViewEncoded}"
+
   ## Add the challenge record to the Infoblox grid member
   result="$(_post "" "$baseurlnObject" "" "POST")"
 
   ## Let's see if we get something intelligible back from the unit
-  if [ "$(echo "$result" | _egrep_o "record:txt/.*:.*/$Infoblox_View")" ]; then
+  if [ "$(echo "$result" | _egrep_o "record:txt/.*:.*/${Infoblox_ViewEncoded}")" ]; then
     _info "Successfully created the txt record"
     return 0
   else
@@ -65,6 +71,9 @@ dns_infoblox_rm() {
   _debug fulldomain "$fulldomain"
   _debug txtvalue "$txtvalue"
 
+  ## URLencode Infoblox View to deal with e.g. spaces
+  Infoblox_ViewEncoded=$(printf "%b" "$Infoblox_View" | _url_encode)
+
   ## Base64 encode the credentials
   Infoblox_CredsEncoded="$(printf "%b" "$Infoblox_Creds" | _base64)"
 
@@ -73,18 +82,18 @@ dns_infoblox_rm() {
   export _H2="Authorization: Basic $Infoblox_CredsEncoded"
 
   ## Does the record exist?  Let's check.
-  baseurlnObject="https://$Infoblox_Server/wapi/v2.2.2/record:txt?name=$fulldomain&text=$txtvalue&view=$Infoblox_View&_return_type=xml-pretty"
+  baseurlnObject="https://$Infoblox_Server/wapi/v2.2.2/record:txt?name=$fulldomain&text=$txtvalue&view=${Infoblox_ViewEncoded}&_return_type=xml-pretty"
   result="$(_get "$baseurlnObject")"
 
   ## Let's see if we get something intelligible back from the grid
-  if [ "$(echo "$result" | _egrep_o "record:txt/.*:.*/$Infoblox_View")" ]; then
+  if [ "$(echo "$result" | _egrep_o "record:txt/.*:.*/${Infoblox_ViewEncoded}")" ]; then
     ## Extract the object reference
-    objRef="$(printf "%b" "$result" | _egrep_o "record:txt/.*:.*/$Infoblox_View")"
+    objRef="$(printf "%b" "$result" | _egrep_o "record:txt/.*:.*/${Infoblox_ViewEncoded}")"
     objRmUrl="https://$Infoblox_Server/wapi/v2.2.2/$objRef"
     ## Delete them! All the stale records!
     rmResult="$(_post "" "$objRmUrl" "" "DELETE")"
     ## Let's see if that worked
-    if [ "$(echo "$rmResult" | _egrep_o "record:txt/.*:.*/$Infoblox_View")" ]; then
+    if [ "$(echo "$rmResult" | _egrep_o "record:txt/.*:.*/${Infoblox_ViewEncoded}")" ]; then
       _info "Successfully deleted $objRef"
       return 0
     else

+ 74 - 10
dnsapi/dns_inwx.sh

@@ -34,6 +34,10 @@ dns_inwx_add() {
   _saveaccountconf_mutable INWX_Password "$INWX_Password"
   _saveaccountconf_mutable INWX_Shared_Secret "$INWX_Shared_Secret"
 
+  if ! _inwx_login; then
+    return 1
+  fi
+
   _debug "First detect the root zone"
   if ! _get_root "$fulldomain"; then
     _err "invalid domain"
@@ -55,6 +59,7 @@ dns_inwx_rm() {
 
   INWX_User="${INWX_User:-$(_readaccountconf_mutable INWX_User)}"
   INWX_Password="${INWX_Password:-$(_readaccountconf_mutable INWX_Password)}"
+  INWX_Shared_Secret="${INWX_Shared_Secret:-$(_readaccountconf_mutable INWX_Shared_Secret)}"
   if [ -z "$INWX_User" ] || [ -z "$INWX_Password" ]; then
     INWX_User=""
     INWX_Password=""
@@ -63,9 +68,9 @@ dns_inwx_rm() {
     return 1
   fi
 
-  #save the api key and email to the account conf file.
-  _saveaccountconf_mutable INWX_User "$INWX_User"
-  _saveaccountconf_mutable INWX_Password "$INWX_Password"
+  if ! _inwx_login; then
+    return 1
+  fi
 
   _debug "First detect the root zone"
   if ! _get_root "$fulldomain"; then
@@ -126,8 +131,42 @@ dns_inwx_rm() {
 
 ####################  Private functions below ##################################
 
+_inwx_check_cookie() {
+  INWX_Cookie="${INWX_Cookie:-$(_readaccountconf_mutable INWX_Cookie)}"
+  if [ -z "$INWX_Cookie" ]; then
+    _debug "No cached cookie found"
+    return 1
+  fi
+  _H1="$INWX_Cookie"
+  export _H1
+
+  xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
+  <methodCall>
+  <methodName>account.info</methodName>
+  </methodCall>')
+
+  response="$(_post "$xml_content" "$INWX_Api" "" "POST")"
+
+  if _contains "$response" "<member><name>code</name><value><int>1000</int></value></member>"; then
+    _debug "Cached cookie still valid"
+    return 0
+  fi
+
+  _debug "Cached cookie no longer valid"
+  _H1=""
+  export _H1
+  INWX_Cookie=""
+  _saveaccountconf_mutable INWX_Cookie "$INWX_Cookie"
+  return 1
+}
+
 _inwx_login() {
 
+  if _inwx_check_cookie; then
+    _debug "Already logged in"
+    return 0
+  fi
+
   xml_content=$(printf '<?xml version="1.0" encoding="UTF-8"?>
   <methodCall>
   <methodName>account.login</methodName>
@@ -151,17 +190,25 @@ _inwx_login() {
     </value>
    </param>
   </params>
-  </methodCall>' $INWX_User $INWX_Password)
+  </methodCall>' "$INWX_User" "$INWX_Password")
 
   response="$(_post "$xml_content" "$INWX_Api" "" "POST")"
-  _H1=$(printf "Cookie: %s" "$(grep "domrobot=" "$HTTP_HEADER" | grep "^Set-Cookie:" | _tail_n 1 | _egrep_o 'domrobot=[^;]*;' | tr -d ';')")
+
+  INWX_Cookie=$(printf "Cookie: %s" "$(grep "domrobot=" "$HTTP_HEADER" | grep "^Set-Cookie:" | _tail_n 1 | _egrep_o 'domrobot=[^;]*;' | tr -d ';')")
+  _H1=$INWX_Cookie
   export _H1
+  export INWX_Cookie
+  _saveaccountconf_mutable INWX_Cookie "$INWX_Cookie"
+
+  if ! _contains "$response" "<member><name>code</name><value><int>1000</int></value></member>"; then
+    _err "INWX API: Authentication error (username/password correct?)"
+    return 1
+  fi
 
   #https://github.com/inwx/php-client/blob/master/INWX/Domrobot.php#L71
-  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 _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 "INWX API: Mobile TAN detected."
       _err "Please define a shared secret."
       return 1
     fi
@@ -194,6 +241,11 @@ _inwx_login() {
     </methodCall>' "$tan")
 
     response="$(_post "$xml_content" "$INWX_Api" "" "POST")"
+
+    if ! _contains "$response" "<member><name>code</name><value><int>1000</int></value></member>"; then
+      _err "INWX API: Mobile TAN not correct."
+      return 1
+    fi
   fi
 
 }
@@ -206,11 +258,23 @@ _get_root() {
   i=2
   p=1
 
-  _inwx_login
-
   xml_content='<?xml version="1.0" encoding="UTF-8"?>
   <methodCall>
   <methodName>nameserver.list</methodName>
+  <params>
+   <param>
+    <value>
+     <struct>
+      <member>
+       <name>pagelimit</name>
+       <value>
+        <int>9999</int>
+       </value>
+      </member>
+     </struct>
+    </value>
+   </param>
+  </params>
   </methodCall>'
 
   response="$(_post "$xml_content" "$INWX_Api" "" "POST")"

+ 64 - 46
dnsapi/dns_ispconfig.sh

@@ -32,7 +32,11 @@ dns_ispconfig_rm() {
 ####################  Private functions below ##################################
 
 _ISPC_credentials() {
-  if [ -z "${ISPC_User}" ] || [ -z "$ISPC_Password" ] || [ -z "${ISPC_Api}" ] || [ -z "${ISPC_Api_Insecure}" ]; then
+  ISPC_User="${ISPC_User:-$(_readaccountconf_mutable ISPC_User)}"
+  ISPC_Password="${ISPC_Password:-$(_readaccountconf_mutable ISPC_Password)}"
+  ISPC_Api="${ISPC_Api:-$(_readaccountconf_mutable ISPC_Api)}"
+  ISPC_Api_Insecure="${ISPC_Api_Insecure:-$(_readaccountconf_mutable ISPC_Api_Insecure)}"
+  if [ -z "${ISPC_User}" ] || [ -z "${ISPC_Password}" ] || [ -z "${ISPC_Api}" ] || [ -z "${ISPC_Api_Insecure}" ]; then
     ISPC_User=""
     ISPC_Password=""
     ISPC_Api=""
@@ -40,10 +44,10 @@ _ISPC_credentials() {
     _err "You haven't specified the ISPConfig Login data, URL and whether you want check the ISPC SSL cert. Please try again."
     return 1
   else
-    _saveaccountconf ISPC_User "${ISPC_User}"
-    _saveaccountconf ISPC_Password "${ISPC_Password}"
-    _saveaccountconf ISPC_Api "${ISPC_Api}"
-    _saveaccountconf ISPC_Api_Insecure "${ISPC_Api_Insecure}"
+    _saveaccountconf_mutable ISPC_User "${ISPC_User}"
+    _saveaccountconf_mutable ISPC_Password "${ISPC_Password}"
+    _saveaccountconf_mutable ISPC_Api "${ISPC_Api}"
+    _saveaccountconf_mutable ISPC_Api_Insecure "${ISPC_Api_Insecure}"
     # Set whether curl should use secure or insecure mode
     export HTTPS_INSECURE="${ISPC_Api_Insecure}"
   fi
@@ -75,7 +79,7 @@ _ISPC_getZoneInfo() {
     # suffix . needed for zone -> domain.tld.
     curData="{\"session_id\":\"${sessionID}\",\"primary_id\":{\"origin\":\"${curZone}.\"}}"
     curResult="$(_post "${curData}" "${ISPC_Api}?dns_zone_get")"
-    _debug "Calling _ISPC_getZoneInfo: '${curData}' '${ISPC_Api}?login'"
+    _debug "Calling _ISPC_getZoneInfo: '${curData}' '${ISPC_Api}?dns_zone_get'"
     _debug "Result of _ISPC_getZoneInfo: '$curResult'"
     if _contains "${curResult}" '"id":"'; then
       zoneFound=true
@@ -95,33 +99,47 @@ _ISPC_getZoneInfo() {
     server_id=$(echo "${curResult}" | _egrep_o "server_id.*" | cut -d ':' -f 2 | cut -d '"' -f 2)
     _debug "Server ID: '${server_id}'"
     case "${server_id}" in
-      '' | *[!0-9]*)
-        _err "Server ID is not numeric."
-        return 1
-        ;;
-      *) _info "Retrieved Server ID" ;;
+    '' | *[!0-9]*)
+      _err "Server ID is not numeric."
+      return 1
+      ;;
+    *) _info "Retrieved Server ID" ;;
     esac
     zone=$(echo "${curResult}" | _egrep_o "\"id.*" | cut -d ':' -f 2 | cut -d '"' -f 2)
     _debug "Zone: '${zone}'"
     case "${zone}" in
-      '' | *[!0-9]*)
-        _err "Zone ID is not numeric."
-        return 1
-        ;;
-      *) _info "Retrieved Zone ID" ;;
+    '' | *[!0-9]*)
+      _err "Zone ID is not numeric."
+      return 1
+      ;;
+    *) _info "Retrieved Zone ID" ;;
     esac
-    client_id=$(echo "${curResult}" | _egrep_o "sys_userid.*" | cut -d ':' -f 2 | cut -d '"' -f 2)
-    _debug "Client ID: '${client_id}'"
-    case "${client_id}" in
-      '' | *[!0-9]*)
-        _err "Client ID is not numeric."
-        return 1
-        ;;
-      *) _info "Retrieved Client ID." ;;
+    sys_userid=$(echo "${curResult}" | _egrep_o "sys_userid.*" | cut -d ':' -f 2 | cut -d '"' -f 2)
+    _debug "SYS User ID: '${sys_userid}'"
+    case "${sys_userid}" in
+    '' | *[!0-9]*)
+      _err "SYS User ID is not numeric."
+      return 1
+      ;;
+    *) _info "Retrieved SYS User ID." ;;
     esac
     zoneFound=""
     zoneEnd=""
   fi
+  # Need to get client_id as it is different from sys_userid
+  curData="{\"session_id\":\"${sessionID}\",\"sys_userid\":\"${sys_userid}\"}"
+  curResult="$(_post "${curData}" "${ISPC_Api}?client_get_id")"
+  _debug "Calling _ISPC_ClientGetID: '${curData}' '${ISPC_Api}?client_get_id'"
+  _debug "Result of _ISPC_ClientGetID: '$curResult'"
+  client_id=$(echo "${curResult}" | _egrep_o "response.*" | cut -d ':' -f 2 | cut -d '"' -f 2 | tr -d '{}')
+  _debug "Client ID: '${client_id}'"
+  case "${client_id}" in
+  '' | *[!0-9]*)
+    _err "Client ID is not numeric."
+    return 1
+    ;;
+  *) _info "Retrieved Client ID." ;;
+  esac
 }
 
 _ISPC_addTxt() {
@@ -135,11 +153,11 @@ _ISPC_addTxt() {
   record_id=$(echo "${curResult}" | _egrep_o "\"response.*" | cut -d ':' -f 2 | cut -d '"' -f 2)
   _debug "Record ID: '${record_id}'"
   case "${record_id}" in
-    '' | *[!0-9]*)
-      _err "Couldn't add ACME Challenge TXT record to zone."
-      return 1
-      ;;
-    *) _info "Added ACME Challenge TXT record to zone." ;;
+  '' | *[!0-9]*)
+    _err "Couldn't add ACME Challenge TXT record to zone."
+    return 1
+    ;;
+  *) _info "Added ACME Challenge TXT record to zone." ;;
   esac
 }
 
@@ -153,24 +171,24 @@ _ISPC_rmTxt() {
     record_id=$(echo "${curResult}" | _egrep_o "\"id.*" | cut -d ':' -f 2 | cut -d '"' -f 2)
     _debug "Record ID: '${record_id}'"
     case "${record_id}" in
-      '' | *[!0-9]*)
-        _err "Record ID is not numeric."
+    '' | *[!0-9]*)
+      _err "Record ID is not numeric."
+      return 1
+      ;;
+    *)
+      unset IFS
+      _info "Retrieved 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'"
+      if _contains "${curResult}" '"code":"ok"'; then
+        _info "Removed ACME Challenge TXT record from zone."
+      else
+        _err "Couldn't remove ACME Challenge TXT record from zone."
         return 1
-        ;;
-      *)
-        unset IFS
-        _info "Retrieved 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'"
-        if _contains "${curResult}" '"code":"ok"'; then
-          _info "Removed ACME Challenge TXT record from zone."
-        else
-          _err "Couldn't remove ACME Challenge TXT record from zone."
-          return 1
-        fi
-        ;;
+      fi
+      ;;
     esac
   fi
 }

+ 1 - 1
dnsapi/dns_kinghost.sh

@@ -37,7 +37,7 @@ dns_kinghost_add() {
   _debug "Getting txt records"
   _kinghost_rest GET "dns" "name=$fulldomain&content=$txtvalue"
 
-  #This API call returns "status":"ok" if dns record does not exists
+  #This API call returns "status":"ok" if dns record does not exist
   #We are creating a new txt record here, so we expect the "ok" status
   if ! echo "$response" | grep '"status":"ok"' >/dev/null; then
     _err "Error"

+ 4 - 2
dnsapi/dns_knot.sh

@@ -19,8 +19,9 @@ dns_knot_add() {
 
   _info "Adding ${fulldomain}. 60 TXT \"${txtvalue}\""
 
-  knsupdate -y "${KNOT_KEY}" <<EOF
+  knsupdate <<EOF
 server ${KNOT_SERVER}
+key ${KNOT_KEY}
 zone ${_domain}.
 update add ${fulldomain}. 60 TXT "${txtvalue}"
 send
@@ -49,8 +50,9 @@ dns_knot_rm() {
 
   _info "Removing ${fulldomain}. TXT"
 
-  knsupdate -y "${KNOT_KEY}" <<EOF
+  knsupdate <<EOF
 server ${KNOT_SERVER}
+key ${KNOT_KEY}
 zone ${_domain}.
 update del ${fulldomain}. TXT
 send

+ 13 - 3
dnsapi/dns_lexicon.sh

@@ -5,7 +5,7 @@
 # https://github.com/AnalogJ/lexicon
 lexicon_cmd="lexicon"
 
-wiki="https://github.com/Neilpang/acme.sh/wiki/How-to-use-lexicon-dns-api"
+wiki="https://github.com/acmesh-official/acme.sh/wiki/How-to-use-lexicon-dns-api"
 
 _lexicon_init() {
   if ! _exists "$lexicon_cmd"; then
@@ -63,6 +63,16 @@ _lexicon_init() {
     _saveaccountconf_mutable "$Lx_domaintoken" "$Lx_domaintoken_v"
     eval export "$Lx_domaintoken"
   fi
+
+  # shellcheck disable=SC2018,SC2019
+  Lx_api_key=$(echo LEXICON_"${PROVIDER}"_API_KEY | tr 'a-z' 'A-Z')
+  eval "$Lx_api_key=\${$Lx_api_key:-$(_readaccountconf_mutable "$Lx_api_key")}"
+  Lx_api_key_v=$(eval echo \$"$Lx_api_key")
+  _secure_debug "$Lx_api_key" "$Lx_api_key_v"
+  if [ "$Lx_api_key_v" ]; then
+    _saveaccountconf_mutable "$Lx_api_key" "$Lx_api_key_v"
+    eval export "$Lx_api_key"
+  fi
 }
 
 ########  Public functions #####################
@@ -82,7 +92,7 @@ dns_lexicon_add() {
   _savedomainconf LEXICON_OPTS "$LEXICON_OPTS"
 
   # shellcheck disable=SC2086
-  $lexicon_cmd "$PROVIDER" $LEXICON_OPTS create "${domain}" TXT --name="_acme-challenge.${domain}." --content="${txtvalue}"
+  $lexicon_cmd "$PROVIDER" $LEXICON_OPTS create "${domain}" TXT --name="_acme-challenge.${domain}." --content="${txtvalue}" --output QUIET
 
 }
 
@@ -98,6 +108,6 @@ dns_lexicon_rm() {
   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}"
+  $lexicon_cmd "$PROVIDER" $LEXICON_OPTS delete "${domain}" TXT --name="_acme-challenge.${domain}." --content="${txtvalue}" --output QUIET
 
 }

+ 7 - 6
dnsapi/dns_linode_v4.sh

@@ -36,7 +36,7 @@ dns_linode_v4_add() {
             }"
 
   if _rest POST "/$_domain_id/records" "$_payload" && [ -n "$response" ]; then
-    _resource_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\s*[0-9]+" | cut -d : -f 2 | tr -d " " | _head_n 1)
+    _resource_id=$(printf "%s\n" "$response" | _egrep_o "\"id\": *[0-9]+" | cut -d : -f 2 | tr -d " " | _head_n 1)
     _debug _resource_id "$_resource_id"
 
     if [ -z "$_resource_id" ]; then
@@ -74,9 +74,9 @@ dns_linode_v4_rm() {
   if _rest GET "/$_domain_id/records" && [ -n "$response" ]; then
     response="$(echo "$response" | tr -d "\n" | tr '{' "|" | sed 's/|/&{/g' | tr "|" "\n")"
 
-    resource="$(echo "$response" | _egrep_o "{.*\"name\":\s*\"$_sub_domain\".*}")"
+    resource="$(echo "$response" | _egrep_o "\{.*\"name\": *\"$_sub_domain\".*}")"
     if [ "$resource" ]; then
-      _resource_id=$(printf "%s\n" "$resource" | _egrep_o "\"id\":\s*[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ )
+      _resource_id=$(printf "%s\n" "$resource" | _egrep_o "\"id\": *[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ )
       if [ "$_resource_id" ]; then
         _debug _resource_id "$_resource_id"
 
@@ -106,6 +106,7 @@ dns_linode_v4_rm() {
 ####################  Private functions below ##################################
 
 _Linode_API() {
+  LINODE_V4_API_KEY="${LINODE_V4_API_KEY:-$(_readaccountconf_mutable LINODE_V4_API_KEY)}"
   if [ -z "$LINODE_V4_API_KEY" ]; then
     LINODE_V4_API_KEY=""
 
@@ -115,7 +116,7 @@ _Linode_API() {
     return 1
   fi
 
-  _saveaccountconf LINODE_V4_API_KEY "$LINODE_V4_API_KEY"
+  _saveaccountconf_mutable LINODE_V4_API_KEY "$LINODE_V4_API_KEY"
 }
 
 ####################  Private functions below ##################################
@@ -139,9 +140,9 @@ _get_root() {
         return 1
       fi
 
-      hostedzone="$(echo "$response" | _egrep_o "{.*\"domain\":\s*\"$h\".*}")"
+      hostedzone="$(echo "$response" | _egrep_o "\{.*\"domain\": *\"$h\".*}")"
       if [ "$hostedzone" ]; then
-        _domain_id=$(printf "%s\n" "$hostedzone" | _egrep_o "\"id\":\s*[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ )
+        _domain_id=$(printf "%s\n" "$hostedzone" | _egrep_o "\"id\": *[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ )
         if [ "$_domain_id" ]; then
           _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
           _domain=$h

+ 54 - 30
dnsapi/dns_loopia.sh

@@ -32,8 +32,12 @@ dns_loopia_add() {
 
   _info "Adding record"
 
-  _loopia_add_sub_domain "$_domain" "$_sub_domain"
-  _loopia_add_record "$_domain" "$_sub_domain" "$txtvalue"
+  if ! _loopia_add_sub_domain "$_domain" "$_sub_domain"; then
+    return 1
+  fi
+  if ! _loopia_add_record "$_domain" "$_sub_domain" "$txtvalue"; then
+    return 1
+  fi
 
 }
 
@@ -70,12 +74,13 @@ dns_loopia_rm() {
         <value><string>%s</string></value>
       </param>
     </params>
-  </methodCall>' "$LOOPIA_User" "$LOOPIA_Password" "$_domain" "$_sub_domain")
+  </methodCall>' "$LOOPIA_User" "$Encoded_Password" "$_domain" "$_sub_domain")
 
   response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")"
 
   if ! _contains "$response" "OK"; then
-    _err "Error could not get txt records"
+    err_response=$(echo "$response" | sed 's/.*<string>\(.*\)<\/string>.*/\1/')
+    _err "Error could not get txt records: $err_response"
     return 1
   fi
 }
@@ -101,6 +106,12 @@ _loopia_load_config() {
     return 1
   fi
 
+  if _contains "$LOOPIA_Password" "'" || _contains "$LOOPIA_Password" '"'; then
+    _err "Password contains quoute or double quoute and this is not supported by dns_loopia.sh"
+    return 1
+  fi
+
+  Encoded_Password=$(_xml_encode "$LOOPIA_Password")
   return 0
 }
 
@@ -133,11 +144,12 @@ _loopia_get_records() {
         <value><string>%s</string></value>
       </param>
     </params>
-  </methodCall>' $LOOPIA_User $LOOPIA_Password "$domain" "$sub_domain")
+  </methodCall>' "$LOOPIA_User" "$Encoded_Password" "$domain" "$sub_domain")
 
   response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")"
   if ! _contains "$response" "<array>"; then
-    _err "Error"
+    err_response=$(echo "$response" | sed 's/.*<string>\(.*\)<\/string>.*/\1/')
+    _err "Error: $err_response"
     return 1
   fi
   return 0
@@ -162,7 +174,7 @@ _get_root() {
     <value><string>%s</string></value>
    </param>
   </params>
-  </methodCall>' $LOOPIA_User $LOOPIA_Password)
+  </methodCall>' "$LOOPIA_User" "$Encoded_Password")
 
   response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")"
   while true; do
@@ -206,32 +218,35 @@ _loopia_add_record() {
         <value><string>%s</string></value>
       </param>
       <param>
-        <struct>
-          <member>
-            <name>type</name>
-            <value><string>TXT</string></value>
-          </member>
-          <member>
-            <name>priority</name>
-            <value><int>0</int></value>
-          </member>
-          <member>
-            <name>ttl</name>
-            <value><int>60</int></value>
-          </member>
-          <member>
-            <name>rdata</name>
-            <value><string>%s</string></value>
-          </member>
-        </struct>
+        <value>
+          <struct>
+            <member>
+              <name>type</name>
+              <value><string>TXT</string></value>
+            </member>
+            <member>
+              <name>priority</name>
+              <value><int>0</int></value>
+            </member>
+            <member>
+              <name>ttl</name>
+              <value><int>300</int></value>
+            </member>
+            <member>
+              <name>rdata</name>
+              <value><string>%s</string></value>
+            </member>
+          </struct>
+        </value>
       </param>
     </params>
-  </methodCall>' $LOOPIA_User $LOOPIA_Password "$domain" "$sub_domain" "$txtval")
+  </methodCall>' "$LOOPIA_User" "$Encoded_Password" "$domain" "$sub_domain" "$txtval")
 
   response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")"
 
   if ! _contains "$response" "OK"; then
-    _err "Error"
+    err_response=$(echo "$response" | sed 's/.*<string>\(.*\)<\/string>.*/\1/')
+    _err "Error: $err_response"
     return 1
   fi
   return 0
@@ -255,7 +270,7 @@ _sub_domain_exists() {
         <value><string>%s</string></value>
       </param>
     </params>
-  </methodCall>' $LOOPIA_User $LOOPIA_Password "$domain")
+  </methodCall>' "$LOOPIA_User" "$Encoded_Password" "$domain")
 
   response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")"
 
@@ -290,13 +305,22 @@ _loopia_add_sub_domain() {
         <value><string>%s</string></value>
       </param>
     </params>
-  </methodCall>' $LOOPIA_User $LOOPIA_Password "$domain" "$sub_domain")
+  </methodCall>' "$LOOPIA_User" "$Encoded_Password" "$domain" "$sub_domain")
 
   response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")"
 
   if ! _contains "$response" "OK"; then
-    _err "Error"
+    err_response=$(echo "$response" | sed 's/.*<string>\(.*\)<\/string>.*/\1/')
+    _err "Error: $err_response"
     return 1
   fi
   return 0
 }
+
+_xml_encode() {
+  encoded_string=$1
+  encoded_string=$(echo "$encoded_string" | sed 's/&/\&amp;/')
+  encoded_string=$(echo "$encoded_string" | sed 's/</\&lt;/')
+  encoded_string=$(echo "$encoded_string" | sed 's/>/\&gt;/')
+  printf "%s" "$encoded_string"
+}

+ 2 - 2
dnsapi/dns_me.sh

@@ -2,7 +2,7 @@
 
 # bug reports to dev@1e.ca
 
-# ME_Key=qmlkdjflmkqdjf	
+# ME_Key=qmlkdjflmkqdjf
 # ME_Secret=qmsdlkqmlksdvnnpae
 
 ME_Api=https://api.dnsmadeeasy.com/V2.0/dns/managed
@@ -114,7 +114,7 @@ _get_root() {
     fi
 
     if _contains "$response" "\"name\":\"$h\""; then
-      _domain_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[^,]*" | head -n 1 | cut -d : -f 2 | tr -d '}')
+      _domain_id=$(printf "%s\n" "$response" | sed 's/^{//; s/}$//; s/{.*}//' | sed -r 's/^.*"id":([0-9]+).*$/\1/')
       if [ "$_domain_id" ]; then
         _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
         _domain="$h"

+ 2 - 2
dnsapi/dns_myapi.sh

@@ -7,11 +7,11 @@
 #returns 0 means success, otherwise error.
 #
 #Author: Neilpang
-#Report Bugs here: https://github.com/Neilpang/acme.sh
+#Report Bugs here: https://github.com/acmesh-official/acme.sh
 #
 ########  Public functions #####################
 
-# Please Read this guide first: https://github.com/Neilpang/acme.sh/wiki/DNS-API-Dev-Guide
+# Please Read this guide first: https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide
 
 #Usage: dns_myapi_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
 dns_myapi_add() {

+ 3 - 1
dnsapi/dns_mydevil.sh

@@ -74,7 +74,7 @@ dns_mydevil_rm() {
   validRecords="^${num}${w}${fulldomain}${w}TXT${w}${any}${txtvalue}$"
   for id in $(devil dns list "$domain" | tail -n+2 | grep "${validRecords}" | cut -w -s -f 1); do
     _info "Removing record $id from domain $domain"
-    devil dns del "$domain" "$id" || _err "Could not remove DNS record."
+    echo "y" | devil dns del "$domain" "$id" || _err "Could not remove DNS record."
   done
 }
 
@@ -87,7 +87,9 @@ mydevil_get_domain() {
   domain=""
 
   for domain in $(devil dns list | cut -w -s -f 1 | tail -n+2); do
+    _debug "Checking domain: $domain"
     if _endswith "$fulldomain" "$domain"; then
+      _debug "Fulldomain '$fulldomain' matches '$domain'"
       printf -- "%s" "$domain"
       return 0
     fi

+ 1 - 15
dnsapi/dns_mydnsjp.sh

@@ -150,7 +150,7 @@ _get_root() {
 _mydnsjp_retrieve_domain() {
   _debug "Login to MyDNS.JP"
 
-  response="$(_post "masterid=$MYDNSJP_MasterID&masterpwd=$MYDNSJP_Password" "$MYDNSJP_API/?MENU=100")"
+  response="$(_post "MENU=100&masterid=$MYDNSJP_MasterID&masterpwd=$MYDNSJP_Password" "$MYDNSJP_API/members/")"
   cookie="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2)"
 
   # If cookies is not empty then logon successful
@@ -159,22 +159,8 @@ _mydnsjp_retrieve_domain() {
     return 1
   fi
 
-  _debug "Retrieve DOMAIN INFO page"
-
-  export _H1="Cookie:${cookie}"
-
-  response="$(_get "$MYDNSJP_API/?MENU=300")"
-
-  if [ "$?" != "0" ]; then
-    _err "Fail to retrieve DOMAIN INFO."
-    return 1
-  fi
-
   _root_domain=$(echo "$response" | grep "DNSINFO\[domainname\]" | sed 's/^.*value="\([^"]*\)".*/\1/')
 
-  # Logout
-  response="$(_get "$MYDNSJP_API/?MENU=090")"
-
   _debug _root_domain "$_root_domain"
 
   if [ -z "$_root_domain" ]; then

+ 12 - 8
dnsapi/dns_namecheap.sh

@@ -3,10 +3,10 @@
 # Namecheap API
 # https://www.namecheap.com/support/api/intro.aspx
 #
-# Requires Namecheap API key set in 
-#NAMECHEAP_API_KEY, 
+# Requires Namecheap API key set in
+#NAMECHEAP_API_KEY,
 #NAMECHEAP_USERNAME,
-#NAMECHEAP_SOURCEIP 
+#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 #####################
@@ -157,7 +157,7 @@ _namecheap_set_publicip() {
 
   if [ -z "$NAMECHEAP_SOURCEIP" ]; then
     _err "No Source IP specified for Namecheap API."
-    _err "Use your public ip address or an url to retrieve it (e.g. https://ipconfig.co/ip) and export it as NAMECHEAP_SOURCEIP"
+    _err "Use your public ip address or an url to retrieve it (e.g. https://ifconfig.co/ip) and export it as NAMECHEAP_SOURCEIP"
     return 1
   else
     _saveaccountconf NAMECHEAP_SOURCEIP "$NAMECHEAP_SOURCEIP"
@@ -175,7 +175,7 @@ _namecheap_set_publicip() {
       _publicip=$(_get "$addr")
     else
       _err "No Source IP specified for Namecheap API."
-      _err "Use your public ip address or an url to retrieve it (e.g. https://ipconfig.co/ip) and export it as NAMECHEAP_SOURCEIP"
+      _err "Use your public ip address or an url to retrieve it (e.g. https://ifconfig.co/ip) and export it as NAMECHEAP_SOURCEIP"
       return 1
     fi
   fi
@@ -208,7 +208,7 @@ _namecheap_parse_host() {
   _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)
+  _hostaddress=$(echo "$_host" | _egrep_o ' Address="[^"]*' | cut -d '"' -f 2 | _xml_decode)
   _hostmxpref=$(echo "$_host" | _egrep_o ' MXPref="[^"]*' | cut -d '"' -f 2)
   _hostttl=$(echo "$_host" | _egrep_o ' TTL="[^"]*' | cut -d '"' -f 2)
 
@@ -259,7 +259,7 @@ _set_namecheap_TXT() {
   _debug hosts "$hosts"
 
   if [ -z "$hosts" ]; then
-    _error "Hosts not found"
+    _err "Hosts not found"
     return 1
   fi
 
@@ -313,7 +313,7 @@ _del_namecheap_TXT() {
   _debug hosts "$hosts"
 
   if [ -z "$hosts" ]; then
-    _error "Hosts not found"
+    _err "Hosts not found"
     return 1
   fi
 
@@ -405,3 +405,7 @@ _namecheap_set_tld_sld() {
   done
 
 }
+
+_xml_decode() {
+  sed 's/&quot;/"/g'
+}

+ 1 - 1
dnsapi/dns_namesilo.sh

@@ -110,7 +110,7 @@ _get_root() {
       return 1
     fi
 
-    if _contains "$response" "<domain>$host"; then
+    if _contains "$response" ">$host</domain>"; then
       _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
       _domain="$host"
       return 0

+ 3 - 7
dnsapi/dns_nederhost.sh

@@ -1,6 +1,6 @@
 #!/usr/bin/env sh
 
-#NederHost_Key="sdfgikogfdfghjklkjhgfcdcfghjk"
+#NederHost_Key="sdfgikogfdfghjklkjhgfcdcfghj"
 
 NederHost_Api="https://api.nederhost.nl/dns/v1"
 
@@ -112,12 +112,8 @@ _nederhost_rest() {
   export _H1="Authorization: Bearer $NederHost_Key"
   export _H2="Content-Type: application/json"
 
-  if [ "$m" != "GET" ]; then
-    _debug data "$data"
-    response="$(_post "$data" "$NederHost_Api/$ep" "" "$m")"
-  else
-    response="$(_get "$NederHost_Api/$ep")"
-  fi
+  _debug data "$data"
+  response="$(_post "$data" "$NederHost_Api/$ep" "" "$m")"
 
   _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
   _debug "http response code $_code"

+ 4 - 4
dnsapi/dns_netcup.sh

@@ -119,16 +119,16 @@ login() {
   tmp=$(_post "{\"action\": \"login\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apipassword\": \"$NC_Apipw\", \"customernumber\": \"$NC_CID\"}}" "$end" "" "POST")
   sid=$(echo "$tmp" | tr '{}' '\n' | grep apisessionid | cut -d '"' -f 4)
   _debug "$tmp"
-  if [ "$(_getfield "$msg" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
-    _err "$msg"
+  if [ "$(_getfield "$tmp" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
+    _err "$tmp"
     return 1
   fi
 }
 logout() {
   tmp=$(_post "{\"action\": \"logout\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apisessionid\": \"$sid\", \"customernumber\": \"$NC_CID\"}}" "$end" "" "POST")
   _debug "$tmp"
-  if [ "$(_getfield "$msg" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
-    _err "$msg"
+  if [ "$(_getfield "$tmp" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
+    _err "$tmp"
     return 1
   fi
 }

+ 1 - 1
dnsapi/dns_nsd.sh

@@ -51,7 +51,7 @@ dns_nsd_rm() {
   Nsd_ZoneFile="${Nsd_ZoneFile:-$(_readdomainconf Nsd_ZoneFile)}"
   Nsd_Command="${Nsd_Command:-$(_readdomainconf Nsd_Command)}"
 
-  sed -i "/$fulldomain. $ttlvalue IN TXT \"$txtvalue\"/d" "$Nsd_ZoneFile"
+  _sed_i "/$fulldomain. $ttlvalue IN TXT \"$txtvalue\"/d" "$Nsd_ZoneFile"
   _info "Removed TXT record for $fulldomain"
   _debug "Running $Nsd_Command"
   if eval "$Nsd_Command"; then

+ 2 - 2
dnsapi/dns_nsupdate.sh

@@ -27,7 +27,7 @@ dns_nsupdate_add() {
   [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_2" ] && nsdebug="-D"
   if [ -z "${NSUPDATE_ZONE}" ]; then
     nsupdate -k "${NSUPDATE_KEY}" $nsdebug <<EOF
-server ${NSUPDATE_SERVER}  ${NSUPDATE_SERVER_PORT} 
+server ${NSUPDATE_SERVER}  ${NSUPDATE_SERVER_PORT}
 update add ${fulldomain}. 60 in txt "${txtvalue}"
 send
 EOF
@@ -64,7 +64,7 @@ dns_nsupdate_rm() {
   [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_2" ] && nsdebug="-D"
   if [ -z "${NSUPDATE_ZONE}" ]; then
     nsupdate -k "${NSUPDATE_KEY}" $nsdebug <<EOF
-server ${NSUPDATE_SERVER}  ${NSUPDATE_SERVER_PORT} 
+server ${NSUPDATE_SERVER}  ${NSUPDATE_SERVER_PORT}
 update delete ${fulldomain}. txt
 send
 EOF

+ 95 - 47
dnsapi/dns_one.sh

@@ -1,19 +1,9 @@
 #!/usr/bin/env sh
-# -*- mode: sh; tab-width: 2; indent-tabs-mode: s; coding: utf-8 -*-
-
 # one.com ui wrapper for acme.sh
-# Author: github: @diseq
-# Created: 2019-02-17
-# Fixed by: @der-berni
-# Modified: 2019-05-31
+
 #
 #     export ONECOM_User="username"
 #     export ONECOM_Password="password"
-#
-# Usage:
-#     acme.sh --issue --dns dns_one -d example.com
-#
-#     only single domain supported atm
 
 dns_one_add() {
   fulldomain=$1
@@ -30,32 +20,27 @@ dns_one_add() {
     return 1
   fi
 
-  mysubdomain=$_sub_domain
-  mydomain=$_domain
-  _debug mysubdomain "$mysubdomain"
-  _debug mydomain "$mydomain"
+  subdomain="${_sub_domain}"
+  maindomain=${_domain}
 
-  # get entries
-  response="$(_get "https://www.one.com/admin/api/domains/$mydomain/dns/custom_records")"
-  _debug response "$response"
-
-  # Update the IP address for domain entry
-  postdata="{\"type\":\"dns_custom_records\",\"attributes\":{\"priority\":0,\"ttl\":600,\"type\":\"TXT\",\"prefix\":\"$mysubdomain\",\"content\":\"$txtvalue\"}}"
-  _debug postdata "$postdata"
-  response="$(_post "$postdata" "https://www.one.com/admin/api/domains/$mydomain/dns/custom_records" "" "POST" "application/json")"
-  response="$(echo "$response" | _normalizeJson)"
-  _debug response "$response"
+  _debug subdomain "$subdomain"
+  _debug maindomain "$maindomain"
 
-  id=$(echo "$response" | sed -n "s/{\"result\":{\"data\":{\"type\":\"dns_custom_records\",\"id\":\"\([^\"]*\)\",\"attributes\":{\"prefix\":\"$mysubdomain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"priority\":0,\"ttl\":600}}},\"metadata\":null}/\1/p")
+  #Check if the TXT exists
+  _dns_one_getrecord "TXT" "$subdomain" "$txtvalue"
+  if [ -n "$id" ]; then
+    _info "$(__green "Txt record with the same value found. Skip adding.")"
+    return 0
+  fi
 
+  _dns_one_addrecord "TXT" "$subdomain" "$txtvalue"
   if [ -z "$id" ]; then
-    _err "Add txt record error."
+    _err "Add TXT record error."
     return 1
   else
-    _info "Added, OK ($id)"
+    _info "$(__green "Added, OK ($id)")"
     return 0
   fi
-
 }
 
 dns_one_rm() {
@@ -73,36 +58,27 @@ dns_one_rm() {
     return 1
   fi
 
-  mysubdomain=$_sub_domain
-  mydomain=$_domain
-  _debug mysubdomain "$mysubdomain"
-  _debug mydomain "$mydomain"
-
-  # get entries
-  response="$(_get "https://www.one.com/admin/api/domains/$mydomain/dns/custom_records")"
-  response="$(echo "$response" | _normalizeJson)"
-  _debug response "$response"
+  subdomain="${_sub_domain}"
+  maindomain=${_domain}
 
-  id=$(printf -- "%s" "$response" | sed -n "s/.*{\"type\":\"dns_custom_records\",\"id\":\"\([^\"]*\)\",\"attributes\":{\"prefix\":\"$mysubdomain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"priority\":0,\"ttl\":600}.*/\1/p")
+  _debug subdomain "$subdomain"
+  _debug maindomain "$maindomain"
 
+  #Check if the TXT exists
+  _dns_one_getrecord "TXT" "$subdomain" "$txtvalue"
   if [ -z "$id" ]; then
     _err "Txt record not found."
     return 1
   fi
 
   # delete entry
-  response="$(_post "$postdata" "https://www.one.com/admin/api/domains/$mydomain/dns/custom_records/$id" "" "DELETE" "application/json")"
-  response="$(echo "$response" | _normalizeJson)"
-  _debug response "$response"
-
-  if [ "$response" = '{"result":null,"metadata":null}' ]; then
-    _info "Removed, OK"
+  if _dns_one_delrecord "$id"; then
+    _info "$(__green Removed, OK)"
     return 0
   else
     _err "Removing txt record error."
     return 1
   fi
-
 }
 
 #_acme-challenge.www.domain.com
@@ -111,7 +87,7 @@ dns_one_rm() {
 # _domain=domain.com
 _get_root() {
   domain="$1"
-  i=2
+  i=1
   p=1
   while true; do
     h=$(printf "%s" "$domain" | cut -d . -f $i-100)
@@ -177,3 +153,75 @@ _dns_one_login() {
 
   return 0
 }
+
+_dns_one_getrecord() {
+  type="$1"
+  name="$2"
+  value="$3"
+  if [ -z "$type" ]; then
+    type="TXT"
+  fi
+  if [ -z "$name" ]; then
+    _err "Record name is empty."
+    return 1
+  fi
+
+  response="$(_get "https://www.one.com/admin/api/domains/$maindomain/dns/custom_records")"
+  response="$(echo "$response" | _normalizeJson)"
+  _debug response "$response"
+
+  if [ -z "${value}" ]; then
+    id=$(printf -- "%s" "$response" | sed -n "s/.*{\"type\":\"dns_custom_records\",\"id\":\"\([^\"]*\)\",\"attributes\":{\"prefix\":\"${name}\",\"type\":\"${type}\",\"content\":\"[^\"]*\",\"priority\":0,\"ttl\":600}.*/\1/p")
+    response=$(printf -- "%s" "$response" | sed -n "s/.*{\"type\":\"dns_custom_records\",\"id\":\"[^\"]*\",\"attributes\":{\"prefix\":\"${name}\",\"type\":\"${type}\",\"content\":\"\([^\"]*\)\",\"priority\":0,\"ttl\":600}.*/\1/p")
+  else
+    id=$(printf -- "%s" "$response" | sed -n "s/.*{\"type\":\"dns_custom_records\",\"id\":\"\([^\"]*\)\",\"attributes\":{\"prefix\":\"${name}\",\"type\":\"${type}\",\"content\":\"${value}\",\"priority\":0,\"ttl\":600}.*/\1/p")
+  fi
+  if [ -z "$id" ]; then
+    return 1
+  fi
+  return 0
+}
+
+_dns_one_addrecord() {
+  type="$1"
+  name="$2"
+  value="$3"
+  if [ -z "$type" ]; then
+    type="TXT"
+  fi
+  if [ -z "$name" ]; then
+    _err "Record name is empty."
+    return 1
+  fi
+
+  postdata="{\"type\":\"dns_custom_records\",\"attributes\":{\"priority\":0,\"ttl\":600,\"type\":\"${type}\",\"prefix\":\"${name}\",\"content\":\"${value}\"}}"
+  _debug postdata "$postdata"
+  response="$(_post "$postdata" "https://www.one.com/admin/api/domains/$maindomain/dns/custom_records" "" "POST" "application/json")"
+  response="$(echo "$response" | _normalizeJson)"
+  _debug response "$response"
+
+  id=$(echo "$response" | sed -n "s/{\"result\":{\"data\":{\"type\":\"dns_custom_records\",\"id\":\"\([^\"]*\)\",\"attributes\":{\"prefix\":\"$subdomain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"priority\":0,\"ttl\":600}}},\"metadata\":null}/\1/p")
+
+  if [ -z "$id" ]; then
+    return 1
+  else
+    return 0
+  fi
+}
+
+_dns_one_delrecord() {
+  id="$1"
+  if [ -z "$id" ]; then
+    return 1
+  fi
+
+  response="$(_post "" "https://www.one.com/admin/api/domains/$maindomain/dns/custom_records/$id" "" "DELETE" "application/json")"
+  response="$(echo "$response" | _normalizeJson)"
+  _debug response "$response"
+
+  if [ "$response" = '{"result":null,"metadata":null}' ]; then
+    return 0
+  else
+    return 1
+  fi
+}

+ 12 - 9
dnsapi/dns_openprovider.sh

@@ -3,7 +3,7 @@
 # This is the OpenProvider API wrapper for acme.sh
 #
 # Author: Sylvia van Os
-# Report Bugs here: https://github.com/Neilpang/acme.sh/issues/2104
+# Report Bugs here: https://github.com/acmesh-official/acme.sh/issues/2104
 #
 #     export OPENPROVIDER_USER="username"
 #     export OPENPROVIDER_PASSWORDHASH="hashed_password"
@@ -59,16 +59,17 @@ dns_openprovider_add() {
         break
       fi
 
-      items="$(echo "$items" | sed "s|${item}||")"
+      tmpitem="$(echo "$item" | sed 's/\*/\\*/g')"
+      items="$(echo "$items" | sed "s|${tmpitem}||")"
 
       results_retrieved="$(_math "$results_retrieved" + 1)"
       new_item="$(echo "$item" | sed -n 's/.*<item>.*\(<name>\(.*\)\.'"$_domain_name"'\.'"$_domain_extension"'<\/name>.*\(<type>.*<\/type>\).*\(<value>.*<\/value>\).*\(<prio>.*<\/prio>\).*\(<ttl>.*<\/ttl>\)\).*<\/item>.*/<item><name>\2<\/name>\3\4\5\6<\/item>/p')"
       if [ -z "$new_item" ]; then
-        # Base record
+        # Domain apex
         new_item="$(echo "$item" | sed -n 's/.*<item>.*\(<name>\(.*\)'"$_domain_name"'\.'"$_domain_extension"'<\/name>.*\(<type>.*<\/type>\).*\(<value>.*<\/value>\).*\(<prio>.*<\/prio>\).*\(<ttl>.*<\/ttl>\)\).*<\/item>.*/<item><name>\2<\/name>\3\4\5\6<\/item>/p')"
       fi
 
-      if [ -z "$(echo "$new_item" | _egrep_o ".*<type>(A|AAAA|CNAME|MX|SPF|SRV|TXT|TLSA|SSHFP|CAA)<\/type>.*")" ]; then
+      if [ -z "$(echo "$new_item" | _egrep_o ".*<type>(A|AAAA|CNAME|MX|SPF|SRV|TXT|TLSA|SSHFP|CAA|NS)<\/type>.*")" ]; then
         _debug "not an allowed record type, skipping" "$new_item"
         continue
       fi
@@ -86,7 +87,7 @@ dns_openprovider_add() {
 
   _debug "Creating acme record"
   acme_record="$(echo "$fulldomain" | sed -e "s/.$_domain_name.$_domain_extension$//")"
-  _openprovider_request "$(printf '<modifyZoneDnsRequest><domain><name>%s</name><extension>%s</extension></domain><type>master</type><records><array>%s<item><name>%s</name><type>TXT</type><value>%s</value><ttl>86400</ttl></item></array></records></modifyZoneDnsRequest>' "$_domain_name" "$_domain_extension" "$existing_items" "$acme_record" "$txtvalue")"
+  _openprovider_request "$(printf '<modifyZoneDnsRequest><domain><name>%s</name><extension>%s</extension></domain><type>master</type><records><array>%s<item><name>%s</name><type>TXT</type><value>%s</value><ttl>600</ttl></item></array></records></modifyZoneDnsRequest>' "$_domain_name" "$_domain_extension" "$existing_items" "$acme_record" "$txtvalue")"
 
   return 0
 }
@@ -136,7 +137,8 @@ dns_openprovider_rm() {
         break
       fi
 
-      items="$(echo "$items" | sed "s|${item}||")"
+      tmpitem="$(echo "$item" | sed 's/\*/\\*/g')"
+      items="$(echo "$items" | sed "s|${tmpitem}||")"
 
       results_retrieved="$(_math "$results_retrieved" + 1)"
       if ! echo "$item" | grep -v "$fulldomain"; then
@@ -147,11 +149,11 @@ dns_openprovider_rm() {
       new_item="$(echo "$item" | sed -n 's/.*<item>.*\(<name>\(.*\)\.'"$_domain_name"'\.'"$_domain_extension"'<\/name>.*\(<type>.*<\/type>\).*\(<value>.*<\/value>\).*\(<prio>.*<\/prio>\).*\(<ttl>.*<\/ttl>\)\).*<\/item>.*/<item><name>\2<\/name>\3\4\5\6<\/item>/p')"
 
       if [ -z "$new_item" ]; then
-        # Base record
+        # domain apex
         new_item="$(echo "$item" | sed -n 's/.*<item>.*\(<name>\(.*\)'"$_domain_name"'\.'"$_domain_extension"'<\/name>.*\(<type>.*<\/type>\).*\(<value>.*<\/value>\).*\(<prio>.*<\/prio>\).*\(<ttl>.*<\/ttl>\)\).*<\/item>.*/<item><name>\2<\/name>\3\4\5\6<\/item>/p')"
       fi
 
-      if [ -z "$(echo "$new_item" | _egrep_o ".*<type>(A|AAAA|CNAME|MX|SPF|SRV|TXT|TLSA|SSHFP|CAA)<\/type>.*")" ]; then
+      if [ -z "$(echo "$new_item" | _egrep_o ".*<type>(A|AAAA|CNAME|MX|SPF|SRV|TXT|TLSA|SSHFP|CAA|NS)<\/type>.*")" ]; then
         _debug "not an allowed record type, skipping" "$new_item"
         continue
       fi
@@ -205,7 +207,8 @@ _get_root() {
         break
       fi
 
-      items="$(echo "$items" | sed "s|${item}||")"
+      tmpitem="$(echo "$item" | sed 's/\*/\\*/g')"
+      items="$(echo "$items" | sed "s|${tmpitem}||")"
 
       results_retrieved="$(_math "$results_retrieved" + 1)"
 

+ 46 - 42
dnsapi/dns_ovh.sh

@@ -32,49 +32,49 @@ SYS_CA='https://ca.api.soyoustart.com/1.0'
 #'runabove-ca'
 RAV_CA='https://api.runabove.com/1.0'
 
-wiki="https://github.com/Neilpang/acme.sh/wiki/How-to-use-OVH-domain-api"
+wiki="https://github.com/acmesh-official/acme.sh/wiki/How-to-use-OVH-domain-api"
 
-ovh_success="https://github.com/Neilpang/acme.sh/wiki/OVH-Success"
+ovh_success="https://github.com/acmesh-official/acme.sh/wiki/OVH-Success"
 
 _ovh_get_api() {
   _ogaep="$1"
 
   case "${_ogaep}" in
 
-    ovh-eu | ovheu)
-      printf "%s" $OVH_EU
-      return
-      ;;
-    ovh-ca | ovhca)
-      printf "%s" $OVH_CA
-      return
-      ;;
-    kimsufi-eu | kimsufieu)
-      printf "%s" $KSF_EU
-      return
-      ;;
-    kimsufi-ca | kimsufica)
-      printf "%s" $KSF_CA
-      return
-      ;;
-    soyoustart-eu | soyoustarteu)
-      printf "%s" $SYS_EU
-      return
-      ;;
-    soyoustart-ca | soyoustartca)
-      printf "%s" $SYS_CA
-      return
-      ;;
-    runabove-ca | runaboveca)
-      printf "%s" $RAV_CA
-      return
-      ;;
-
-    *)
-
-      _err "Unknown parameter : $1"
-      return 1
-      ;;
+  ovh-eu | ovheu)
+    printf "%s" $OVH_EU
+    return
+    ;;
+  ovh-ca | ovhca)
+    printf "%s" $OVH_CA
+    return
+    ;;
+  kimsufi-eu | kimsufieu)
+    printf "%s" $KSF_EU
+    return
+    ;;
+  kimsufi-ca | kimsufica)
+    printf "%s" $KSF_CA
+    return
+    ;;
+  soyoustart-eu | soyoustarteu)
+    printf "%s" $SYS_EU
+    return
+    ;;
+  soyoustart-ca | soyoustartca)
+    printf "%s" $SYS_CA
+    return
+    ;;
+  runabove-ca | runaboveca)
+    printf "%s" $RAV_CA
+    return
+    ;;
+
+  *)
+
+    _err "Unknown parameter : $1"
+    return 1
+    ;;
   esac
 }
 
@@ -92,7 +92,7 @@ _initAuth() {
 
   if [ "$OVH_AK" != "$(_readaccountconf OVH_AK)" ]; then
     _info "It seems that your ovh key is changed, let's clear consumer key first."
-    _clearaccountconf OVH_CK
+    _clearaccountconf_mutable OVH_CK
   fi
   _saveaccountconf_mutable OVH_AK "$OVH_AK"
   _saveaccountconf_mutable OVH_AS "$OVH_AS"
@@ -118,13 +118,14 @@ _initAuth() {
     #return and wait for retry.
     return 1
   fi
+  _saveaccountconf_mutable OVH_CK "$OVH_CK"
 
   _info "Checking authentication"
 
   if ! _ovh_rest GET "domain" || _contains "$response" "INVALID_CREDENTIAL" || _contains "$response" "NOT_CREDENTIAL"; then
     _err "The consumer key is invalid: $OVH_CK"
     _err "Please retry to create a new one."
-    _clearaccountconf OVH_CK
+    _clearaccountconf_mutable OVH_CK
     return 1
   fi
   _info "Consumer key is ok."
@@ -198,6 +199,8 @@ dns_ovh_rm() {
       if ! _ovh_rest DELETE "domain/zone/$_domain/record/$rid"; then
         return 1
       fi
+      _ovh_rest POST "domain/zone/$_domain/refresh"
+      _debug "Refresh:$response"
       return 0
     fi
   done
@@ -233,8 +236,7 @@ _ovh_authentication() {
   _secure_debug consumerKey "$consumerKey"
 
   OVH_CK="$consumerKey"
-  _saveaccountconf OVH_CK "$OVH_CK"
-
+  _saveaccountconf_mutable OVH_CK "$OVH_CK"
   _info "Please open this link to do authentication: $(__green "$validationUrl")"
 
   _info "Here is a guide for you: $(__green "$wiki")"
@@ -248,7 +250,7 @@ _ovh_authentication() {
 # _domain=domain.com
 _get_root() {
   domain=$1
-  i=2
+  i=1
   p=1
   while true; do
     h=$(printf "%s" "$domain" | cut -d . -f $i-100)
@@ -261,7 +263,9 @@ _get_root() {
       return 1
     fi
 
-    if ! _contains "$response" "This service does not exist" >/dev/null && ! _contains "$response" "NOT_GRANTED_CALL" >/dev/null; then
+    if ! _contains "$response" "This service does not exist" >/dev/null &&
+      ! _contains "$response" "This call has not been granted" >/dev/null &&
+      ! _contains "$response" "NOT_GRANTED_CALL" >/dev/null; then
       _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
       _domain="$h"
       return 0

+ 7 - 6
dnsapi/dns_pdns.sh

@@ -103,7 +103,7 @@ set_record() {
     _build_record_string "$oldchallenge"
   done
 
-  if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root" "{\"rrsets\": [{\"changetype\": \"REPLACE\", \"name\": \"$full.\", \"type\": \"TXT\", \"ttl\": $PDNS_Ttl, \"records\": [$_record_string]}]}"; then
+  if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root" "{\"rrsets\": [{\"changetype\": \"REPLACE\", \"name\": \"$full.\", \"type\": \"TXT\", \"ttl\": $PDNS_Ttl, \"records\": [$_record_string]}]}" "application/json"; then
     _err "Set txt record error."
     return 1
   fi
@@ -126,7 +126,7 @@ rm_record() {
 
   if _contains "$_existing_challenges" "$txtvalue"; then
     #Delete all challenges (PowerDNS API does not allow to delete content)
-    if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root" "{\"rrsets\": [{\"changetype\": \"DELETE\", \"name\": \"$full.\", \"type\": \"TXT\"}]}"; then
+    if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root" "{\"rrsets\": [{\"changetype\": \"DELETE\", \"name\": \"$full.\", \"type\": \"TXT\"}]}" "application/json"; then
       _err "Delete txt record error."
       return 1
     fi
@@ -140,7 +140,7 @@ rm_record() {
         fi
       done
       #Recreate the existing challenges
-      if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root" "{\"rrsets\": [{\"changetype\": \"REPLACE\", \"name\": \"$full.\", \"type\": \"TXT\", \"ttl\": $PDNS_Ttl, \"records\": [$_record_string]}]}"; then
+      if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root" "{\"rrsets\": [{\"changetype\": \"REPLACE\", \"name\": \"$full.\", \"type\": \"TXT\", \"ttl\": $PDNS_Ttl, \"records\": [$_record_string]}]}" "application/json"; then
         _err "Set txt record error."
         return 1
       fi
@@ -175,13 +175,13 @@ _get_root() {
   i=1
 
   if _pdns_rest "GET" "/api/v1/servers/$PDNS_ServerId/zones"; then
-    _zones_response="$response"
+    _zones_response=$(echo "$response" | _normalizeJson)
   fi
 
   while true; do
     h=$(printf "%s" "$domain" | cut -d . -f $i-100)
 
-    if _contains "$_zones_response" "\"name\": \"$h.\""; then
+    if _contains "$_zones_response" "\"name\":\"$h.\""; then
       _domain="$h."
       if [ -z "$h" ]; then
         _domain="=2E"
@@ -203,12 +203,13 @@ _pdns_rest() {
   method=$1
   ep=$2
   data=$3
+  ct=$4
 
   export _H1="X-API-Key: $PDNS_Token"
 
   if [ ! "$method" = "GET" ]; then
     _debug data "$data"
-    response="$(_post "$data" "$PDNS_Url$ep" "" "$method")"
+    response="$(_post "$data" "$PDNS_Url$ep" "" "$method" "$ct")"
   else
     response="$(_get "$PDNS_Url$ep")"
   fi

+ 5 - 4
dnsapi/dns_rackspace.sh

@@ -7,9 +7,10 @@
 
 RACKSPACE_Endpoint="https://dns.api.rackspacecloud.com/v1.0"
 
+# 20210923 - RS changed the fields in the API response; fix sed
 # 20190213 - The name & id fields swapped in the API response; fix sed
 # 20190101 - Duplicating file for new pull request to dev branch
-# Original - tcocca:rackspace_dnsapi https://github.com/Neilpang/acme.sh/pull/1297
+# Original - tcocca:rackspace_dnsapi https://github.com/acmesh-official/acme.sh/pull/1297
 
 ########  Public functions #####################
 #Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
@@ -73,14 +74,14 @@ _get_root_zone() {
       #not valid
       return 1
     fi
-    if ! _rackspace_rest GET "$RACKSPACE_Tenant/domains"; then
+    if ! _rackspace_rest GET "$RACKSPACE_Tenant/domains/search?name=$h"; then
       return 1
     fi
     _debug2 response "$response"
     if _contains "$response" "\"name\":\"$h\"" >/dev/null; then
       # Response looks like:
-      #   {"ttl":300,"accountId":12345,"id":1111111,"name":"example.com","emailAddress": ...<and so on>
-      _domain_id=$(echo "$response" | sed -n "s/^.*\"id\":\([^,]*\),\"name\":\"$h\",.*/\1/p")
+      #   {"id":"12345","accountId":"1111111","name": "example.com","ttl":3600,"emailAddress": ... <and so on>
+      _domain_id=$(echo "$response" | sed -n "s/^.*\"id\":\"\([^,]*\)\",\"accountId\":\"[0-9]*\",\"name\":\"$h\",.*/\1/p")
       _debug2 domain_id "$_domain_id"
       if [ -n "$_domain_id" ]; then
         _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)

+ 69 - 5
dnsapi/dns_regru.sh

@@ -5,7 +5,6 @@
 #
 # REGRU_API_Password="test"
 #
-_domain=$_domain
 
 REGRU_API_URL="https://api.reg.ru/api/regru2"
 
@@ -27,10 +26,20 @@ dns_regru_add() {
   _saveaccountconf_mutable REGRU_API_Username "$REGRU_API_Username"
   _saveaccountconf_mutable REGRU_API_Password "$REGRU_API_Password"
 
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+  _debug _domain "$_domain"
+
+  _subdomain=$(echo "$fulldomain" | sed -r "s/.$_domain//")
+  _debug _subdomain "$_subdomain"
+
   _info "Adding TXT record to ${fulldomain}"
-  response="$(_get "$REGRU_API_URL/zone/add_txt?input_data={%22username%22:%22${REGRU_API_Username}%22,%22password%22:%22${REGRU_API_Password}%22,%22domains%22:[{%22dname%22:%22${_domain}%22}],%22subdomain%22:%22_acme-challenge%22,%22text%22:%22${txtvalue}%22,%22output_content_type%22:%22plain%22}&input_format=json")"
+  _regru_rest POST "zone/add_txt" "input_data={%22username%22:%22${REGRU_API_Username}%22,%22password%22:%22${REGRU_API_Password}%22,%22domains%22:[{%22dname%22:%22${_domain}%22}],%22subdomain%22:%22${_subdomain}%22,%22text%22:%22${txtvalue}%22,%22output_content_type%22:%22plain%22}&input_format=json"
 
-  if _contains "${response}" 'success'; then
+  if ! _contains "${response}" 'error'; then
     return 0
   fi
   _err "Could not create resource record, check logs"
@@ -51,13 +60,68 @@ dns_regru_rm() {
     return 1
   fi
 
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+  _debug _domain "$_domain"
+
+  _subdomain=$(echo "$fulldomain" | sed -r "s/.$_domain//")
+  _debug _subdomain "$_subdomain"
+
   _info "Deleting resource record $fulldomain"
-  response="$(_get "$REGRU_API_URL/zone/remove_record?input_data={%22username%22:%22${REGRU_API_Username}%22,%22password%22:%22${REGRU_API_Password}%22,%22domains%22:[{%22dname%22:%22${_domain}%22}],%22subdomain%22:%22_acme-challenge%22,%22content%22:%22${txtvalue}%22,%22record_type%22:%22TXT%22,%22output_content_type%22:%22plain%22}&input_format=json")"
+  _regru_rest POST "zone/remove_record" "input_data={%22username%22:%22${REGRU_API_Username}%22,%22password%22:%22${REGRU_API_Password}%22,%22domains%22:[{%22dname%22:%22${_domain}%22}],%22subdomain%22:%22${_subdomain}%22,%22content%22:%22${txtvalue}%22,%22record_type%22:%22TXT%22,%22output_content_type%22:%22plain%22}&input_format=json"
 
-  if _contains "${response}" 'success'; then
+  if ! _contains "${response}" 'error'; then
     return 0
   fi
   _err "Could not delete resource record, check logs"
   _err "${response}"
   return 1
 }
+
+####################  Private functions below ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _domain=domain.com
+_get_root() {
+  domain=$1
+
+  _regru_rest POST "service/get_list" "username=${REGRU_API_Username}&password=${REGRU_API_Password}&output_format=xml&servtype=domain"
+  domains_list=$(echo "${response}" | grep dname | sed -r "s/.*dname=\"([^\"]+)\".*/\\1/g")
+
+  for ITEM in ${domains_list}; do
+    IDN_ITEM=${ITEM}
+    case "${domain}" in
+    *${IDN_ITEM}*)
+      _domain="$(_idn "${ITEM}")"
+      _debug _domain "${_domain}"
+      return 0
+      ;;
+    esac
+  done
+
+  return 1
+}
+
+#returns
+# response
+_regru_rest() {
+  m=$1
+  ep="$2"
+  data="$3"
+  _debug "$ep"
+
+  export _H1="Content-Type: application/x-www-form-urlencoded"
+
+  if [ "$m" != "GET" ]; then
+    _debug data "$data"
+    response="$(_post "$data" "$REGRU_API_URL/$ep" "" "$m")"
+  else
+    response="$(_get "$REGRU_API_URL/$ep?$data")"
+  fi
+
+  _debug response "${response}"
+  return 0
+}

+ 2 - 2
dnsapi/dns_selectel.sh

@@ -76,7 +76,7 @@ dns_selectel_rm() {
     return 1
   fi
 
-  _record_seg="$(echo "$response" | _egrep_o "\"content\" *: *\"$txtvalue\"[^}]*}")"
+  _record_seg="$(echo "$response" | _egrep_o "[^{]*\"content\" *: *\"$txtvalue\"[^}]*}")"
   _debug2 "_record_seg" "$_record_seg"
   if [ -z "$_record_seg" ]; then
     _err "can not find _record_seg"
@@ -120,7 +120,7 @@ _get_root() {
       return 1
     fi
 
-    if _contains "$response" "\"name\": \"$h\","; then
+    if _contains "$response" "\"name\" *: *\"$h\","; then
       _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
       _domain=$h
       _debug "Getting domain id for $h"

+ 35 - 9
dnsapi/dns_servercow.sh

@@ -1,7 +1,7 @@
 #!/usr/bin/env sh
 
 ##########
-# Custom servercow.de DNS API v1 for use with [acme.sh](https://github.com/Neilpang/acme.sh)
+# Custom servercow.de DNS API v1 for use with [acme.sh](https://github.com/acmesh-official/acme.sh)
 #
 # Usage:
 # export SERVERCOW_API_Username=username
@@ -49,16 +49,42 @@ dns_servercow_add() {
   _debug _sub_domain "$_sub_domain"
   _debug _domain "$_domain"
 
-  if _servercow_api POST "$_domain" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":20}"; then
-    if printf -- "%s" "$response" | grep "ok" >/dev/null; then
-      _info "Added, OK"
-      return 0
-    else
-      _err "add txt record error."
-      return 1
+  # check whether a txt record already exists for the subdomain
+  if printf -- "%s" "$response" | grep "{\"name\":\"$_sub_domain\",\"ttl\":20,\"type\":\"TXT\"" >/dev/null; then
+    _info "A txt record with the same name already exists."
+    # trim the string on the left
+    txtvalue_old=${response#*{\"name\":\"$_sub_domain\",\"ttl\":20,\"type\":\"TXT\",\"content\":\"}
+    # trim the string on the right
+    txtvalue_old=${txtvalue_old%%\"*}
+
+    _debug txtvalue_old "$txtvalue_old"
+
+    _info "Add the new txtvalue to the existing txt record."
+    if _servercow_api POST "$_domain" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":[\"$txtvalue\",\"$txtvalue_old\"],\"ttl\":20}"; then
+      if printf -- "%s" "$response" | grep "ok" >/dev/null; then
+        _info "Added additional txtvalue, OK"
+        return 0
+      else
+        _err "add txt record error."
+        return 1
+      fi
     fi
+    _err "add txt record error."
+    return 1
+  else
+    _info "There is no txt record with the name yet."
+    if _servercow_api POST "$_domain" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":20}"; then
+      if printf -- "%s" "$response" | grep "ok" >/dev/null; then
+        _info "Added, OK"
+        return 0
+      else
+        _err "add txt record error."
+        return 1
+      fi
+    fi
+    _err "add txt record error."
+    return 1
   fi
-  _err "add txt record error."
 
   return 1
 }

+ 12 - 9
dnsapi/dns_ultra.sh

@@ -5,7 +5,8 @@
 #
 # ULTRA_PWD="some_password_goes_here"
 
-ULTRA_API="https://restapi.ultradns.com/v2/"
+ULTRA_API="https://api.ultradns.com/v3/"
+ULTRA_AUTH_API="https://api.ultradns.com/v2/"
 
 #Usage: add _acme-challenge.www.domain.com "some_long_string_of_characters_go_here_from_lets_encrypt"
 dns_ultra_add() {
@@ -121,7 +122,7 @@ _get_root() {
       return 1
     fi
     if _contains "${response}" "${h}." >/dev/null; then
-      _domain_id=$(echo "$response" | _egrep_o "${h}")
+      _domain_id=$(echo "$response" | _egrep_o "${h}" | head -1)
       if [ "$_domain_id" ]; then
         _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
         _domain="${h}"
@@ -142,23 +143,25 @@ _ultra_rest() {
   ep="$2"
   data="$3"
   _debug "$ep"
-  _debug TOKEN "${AUTH_TOKEN}"
+  if [ -z "$AUTH_TOKEN" ]; then
+    _ultra_login
+  fi
+  _debug TOKEN "$AUTH_TOKEN"
 
-  _ultra_login
   export _H1="Content-Type: application/json"
-  export _H2="Authorization: Bearer ${AUTH_TOKEN}"
+  export _H2="Authorization: Bearer $AUTH_TOKEN"
 
   if [ "$m" != "GET" ]; then
-    _debug data "${data}"
-    response="$(_post "${data}" "${ULTRA_API}"/"${ep}" "" "${m}")"
+    _debug data "$data"
+    response="$(_post "$data" "$ULTRA_API$ep" "" "$m")"
   else
-    response="$(_get "$ULTRA_API/$ep")"
+    response="$(_get "$ULTRA_API$ep")"
   fi
 }
 
 _ultra_login() {
   export _H1=""
   export _H2=""
-  AUTH_TOKEN=$(_post "grant_type=password&username=${ULTRA_USR}&password=${ULTRA_PWD}" "${ULTRA_API}authorization/token" | cut -d, -f3 | cut -d\" -f4)
+  AUTH_TOKEN=$(_post "grant_type=password&username=${ULTRA_USR}&password=${ULTRA_PWD}" "${ULTRA_AUTH_API}authorization/token" | cut -d, -f3 | cut -d\" -f4)
   export AUTH_TOKEN
 }

+ 2 - 8
dnsapi/dns_unoeuro.sh

@@ -5,7 +5,7 @@
 #
 #UNO_User="UExxxxxx"
 
-Uno_Api="https://api.unoeuro.com/1"
+Uno_Api="https://api.simply.com/1"
 
 ########  Public functions #####################
 
@@ -24,12 +24,6 @@ dns_unoeuro_add() {
     return 1
   fi
 
-  if ! _contains "$UNO_User" "UE"; then
-    _err "It seems that the UNO_User=$UNO_User is not a valid username."
-    _err "Please check and retry."
-    return 1
-  fi
-
   #save the api key and email to the account conf file.
   _saveaccountconf_mutable UNO_Key "$UNO_Key"
   _saveaccountconf_mutable UNO_User "$UNO_User"
@@ -52,7 +46,7 @@ dns_unoeuro_add() {
   fi
   _info "Adding record"
 
-  if _uno_rest POST "my/products/$h/dns/records" "{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"data\":\"$txtvalue\",\"ttl\":120}"; then
+  if _uno_rest POST "my/products/$h/dns/records" "{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"data\":\"$txtvalue\",\"ttl\":120,\"priority\":0}"; then
     if _contains "$response" "\"status\": 200" >/dev/null; then
       _info "Added, OK"
       return 0

+ 1 - 1
dnsapi/dns_vscale.sh

@@ -102,7 +102,7 @@ _get_root() {
         return 1
       fi
 
-      hostedzone="$(echo "$response" | _egrep_o "{.*\"name\":\s*\"$h\".*}")"
+      hostedzone="$(echo "$response" | tr "{" "\n" | _egrep_o "\"name\":\s*\"$h\".*}")"
       if [ "$hostedzone" ]; then
         _domain_id=$(printf "%s\n" "$hostedzone" | _egrep_o "\"id\":\s*[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ )
         if [ "$_domain_id" ]; then

+ 17 - 19
dnsapi/dns_vultr.sh

@@ -3,10 +3,10 @@
 #
 #VULTR_API_KEY=000011112222333344445555666677778888
 
-VULTR_Api="https://api.vultr.com/v1"
+VULTR_Api="https://api.vultr.com/v2"
 
 ########  Public functions #####################
-
+#
 #Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
 dns_vultr_add() {
   fulldomain=$1
@@ -31,14 +31,14 @@ dns_vultr_add() {
   _debug _domain "$_domain"
 
   _debug 'Getting txt records'
-  _vultr_rest GET "dns/records?domain=$_domain"
+  _vultr_rest GET "domains/$_domain/records"
 
-  if printf "%s\n" "$response" | grep "\"type\":\"TXT\",\"name\":\"$fulldomain\"" >/dev/null; then
+  if printf "%s\n" "$response" | grep -- "\"type\":\"TXT\",\"name\":\"$fulldomain\"" >/dev/null; then
     _err 'Error'
     return 1
   fi
 
-  if ! _vultr_rest POST 'dns/create_record' "domain=$_domain&name=$_sub_domain&data=\"$txtvalue\"&type=TXT"; then
+  if ! _vultr_rest POST "domains/$_domain/records" "{\"name\":\"$_sub_domain\",\"data\":\"$txtvalue\",\"type\":\"TXT\"}"; then
     _err "$response"
     return 1
   fi
@@ -71,14 +71,14 @@ dns_vultr_rm() {
   _debug _domain "$_domain"
 
   _debug 'Getting txt records'
-  _vultr_rest GET "dns/records?domain=$_domain"
+  _vultr_rest GET "domains/$_domain/records"
 
-  if printf "%s\n" "$response" | grep "\"type\":\"TXT\",\"name\":\"$fulldomain\"" >/dev/null; then
+  if printf "%s\n" "$response" | grep -- "\"type\":\"TXT\",\"name\":\"$fulldomain\"" >/dev/null; then
     _err 'Error'
     return 1
   fi
 
-  _record_id="$(echo "$response" | tr '{}' '\n' | grep '"TXT"' | grep "$txtvalue" | tr ',' '\n' | grep -i 'RECORDID' | cut -d : -f 2)"
+  _record_id="$(echo "$response" | tr '{}' '\n' | grep '"TXT"' | grep -- "$txtvalue" | tr ',' '\n' | grep -i 'id' | cut -d : -f 2)"
   _debug _record_id "$_record_id"
   if [ "$_record_id" ]; then
     _info "Successfully retrieved the record id for ACME challenge."
@@ -87,7 +87,7 @@ dns_vultr_rm() {
     return 0
   fi
 
-  if ! _vultr_rest POST 'dns/delete_record' "domain=$_domain&RECORDID=$_record_id"; then
+  if ! _vultr_rest DELETE "domains/$_domain/records/$_record_id"; then
     _err "$response"
     return 1
   fi
@@ -106,24 +106,22 @@ _get_root() {
   domain=$1
   i=1
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
-    _debug h "$h"
-    if [ -z "$h" ]; then
+    _domain=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    _debug h "$_domain"
+    if [ -z "$_domain" ]; then
       return 1
     fi
 
-    if ! _vultr_rest GET "dns/list"; then
+    if ! _vultr_rest GET "domains"; then
       return 1
     fi
 
-    if printf "%s\n" "$response" | grep '^\[.*\]' >/dev/null; then
+    if printf "%s\n" "$response" | grep '^\{.*\}' >/dev/null; then
       if _contains "$response" "\"domain\":\"$_domain\""; then
         _sub_domain="$(echo "$fulldomain" | sed "s/\\.$_domain\$//")"
-        _domain=$_domain
         return 0
       else
-        _err 'Invalid domain'
-        return 1
+        _debug "Go to next level of $_domain"
       fi
     else
       _err "$response"
@@ -143,8 +141,8 @@ _vultr_rest() {
 
   api_key_trimmed=$(echo $VULTR_API_KEY | tr -d '"')
 
-  export _H1="Api-Key: $api_key_trimmed"
-  export _H2='Content-Type: application/x-www-form-urlencoded'
+  export _H1="Authorization: Bearer $api_key_trimmed"
+  export _H2='Content-Type: application/json'
 
   if [ "$m" != "GET" ]; then
     _debug data "$data"

+ 69 - 57
dnsapi/dns_yandex.sh

@@ -6,6 +6,9 @@
 # Values to export:
 # export PDD_Token="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
 
+# Sometimes cloudflare / google doesn't pick new dns records fast enough.
+# You can add --dnssleep XX to params as workaround.
+
 ########  Public functions #####################
 
 #Usage: dns_myapi_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
@@ -13,97 +16,106 @@ dns_yandex_add() {
   fulldomain="${1}"
   txtvalue="${2}"
   _debug "Calling: dns_yandex_add() '${fulldomain}' '${txtvalue}'"
+
   _PDD_credentials || return 1
-  export _H1="PddToken: $PDD_Token"
 
-  _PDD_get_domain "$fulldomain" || return 1
-  _debug "Found suitable domain in pdd: $curDomain"
-  curData="domain=${curDomain}&type=TXT&subdomain=${curSubdomain}&ttl=360&content=${txtvalue}"
-  curUri="https://pddimp.yandex.ru/api2/admin/dns/add"
-  curResult="$(_post "${curData}" "${curUri}")"
-  _debug "Result: $curResult"
+  _PDD_get_domain || return 1
+  _debug "Found suitable domain: $domain"
+
+  _PDD_get_record_ids || return 1
+  _debug "Record_ids: $record_ids"
+
+  if [ -n "$record_ids" ]; then
+    _info "All existing $subdomain records from $domain will be removed at the very end."
+  fi
+
+  data="domain=${domain}&type=TXT&subdomain=${subdomain}&ttl=300&content=${txtvalue}"
+  uri="https://pddimp.yandex.ru/api2/admin/dns/add"
+  result="$(_post "${data}" "${uri}" | _normalizeJson)"
+  _debug "Result: $result"
+
+  if ! _contains "$result" '"success":"ok"'; then
+    if _contains "$result" '"success":"error"' && _contains "$result" '"error":"record_exists"'; then
+      _info "Record already exists."
+    else
+      _err "Can't add $subdomain to $domain."
+      return 1
+    fi
+  fi
 }
 
 #Usage: dns_myapi_rm   _acme-challenge.www.domain.com
 dns_yandex_rm() {
   fulldomain="${1}"
   _debug "Calling: dns_yandex_rm() '${fulldomain}'"
+
   _PDD_credentials || return 1
-  export _H1="PddToken: $PDD_Token"
 
   _PDD_get_domain "$fulldomain" || return 1
-  _debug "Found suitable domain in pdd: $curDomain"
+  _debug "Found suitable domain: $domain"
 
-  record_id=$(pdd_get_record_id "${fulldomain}")
-  _debug "Result: $record_id"
+  _PDD_get_record_ids "${domain}" "${subdomain}" || return 1
+  _debug "Record_ids: $record_ids"
 
-  for rec_i in $record_id; do
-    curUri="https://pddimp.yandex.ru/api2/admin/dns/del"
-    curData="domain=${curDomain}&record_id=${rec_i}"
-    curResult="$(_post "${curData}" "${curUri}")"
-    _debug "Result: $curResult"
+  for record_id in $record_ids; do
+    data="domain=${domain}&record_id=${record_id}"
+    uri="https://pddimp.yandex.ru/api2/admin/dns/del"
+    result="$(_post "${data}" "${uri}" | _normalizeJson)"
+    _debug "Result: $result"
+
+    if ! _contains "$result" '"success":"ok"'; then
+      _info "Can't remove $subdomain from $domain."
+    fi
   done
 }
 
 ####################  Private functions below ##################################
 
 _PDD_get_domain() {
-  fulldomain="${1}"
-  __page=1
-  __last=0
-  while [ $__last -eq 0 ]; do
-    uri1="https://pddimp.yandex.ru/api2/admin/domain/domains?page=${__page}&on_page=20"
-    res1="$(_get "$uri1" | _normalizeJson)"
-    _debug2 "res1" "$res1"
-    __found="$(echo "$res1" | sed -n -e 's#.* "found": \([^,]*\),.*#\1#p')"
-    _debug "found: $__found results on page"
-    if [ "0$__found" -lt 20 ]; then
-      _debug "last page: $__page"
-      __last=1
+  subdomain_start=1
+  while true; do
+    domain_start=$(_math $subdomain_start + 1)
+    domain=$(echo "$fulldomain" | cut -d . -f "$domain_start"-)
+    subdomain=$(echo "$fulldomain" | cut -d . -f -"$subdomain_start")
+
+    _debug "Checking domain $domain"
+    if [ -z "$domain" ]; then
+      return 1
     fi
 
-    __all_domains="$__all_domains $(echo "$res1" | tr "," "\n" | grep '"name"' | cut -d: -f2 | sed -e 's@"@@g')"
+    uri="https://pddimp.yandex.ru/api2/admin/dns/list?domain=$domain"
+    result="$(_get "${uri}" | _normalizeJson)"
+    _debug "Result: $result"
 
-    __page=$(_math $__page + 1)
-  done
-
-  k=2
-  while [ $k -lt 10 ]; do
-    __t=$(echo "$fulldomain" | cut -d . -f $k-100)
-    _debug "finding zone for domain $__t"
-    for d in $__all_domains; do
-      if [ "$d" = "$__t" ]; then
-        p=$(_math $k - 1)
-        curSubdomain="$(echo "$fulldomain" | cut -d . -f "1-$p")"
-        curDomain="$__t"
-        return 0
-      fi
-    done
-    k=$(_math $k + 1)
+    if _contains "$result" '"success":"ok"'; then
+      return 0
+    fi
+    subdomain_start=$(_math $subdomain_start + 1)
   done
-  _err "No suitable domain found in your account"
-  return 1
 }
 
 _PDD_credentials() {
   if [ -z "${PDD_Token}" ]; then
     PDD_Token=""
-    _err "You need to export PDD_Token=xxxxxxxxxxxxxxxxx"
-    _err "You can get it at https://pddimp.yandex.ru/api2/admin/get_token"
+    _err "You need to export PDD_Token=xxxxxxxxxxxxxxxxx."
+    _err "You can get it at https://pddimp.yandex.ru/api2/admin/get_token."
     return 1
   else
     _saveaccountconf PDD_Token "${PDD_Token}"
   fi
+  export _H1="PddToken: $PDD_Token"
 }
 
-pdd_get_record_id() {
-  fulldomain="${1}"
+_PDD_get_record_ids() {
+  _debug "Check existing records for $subdomain"
+
+  uri="https://pddimp.yandex.ru/api2/admin/dns/list?domain=${domain}"
+  result="$(_get "${uri}" | _normalizeJson)"
+  _debug "Result: $result"
 
-  _PDD_get_domain "$fulldomain"
-  _debug "Found suitable domain in pdd: $curDomain"
+  if ! _contains "$result" '"success":"ok"'; then
+    return 1
+  fi
 
-  curUri="https://pddimp.yandex.ru/api2/admin/dns/list?domain=${curDomain}"
-  curResult="$(_get "${curUri}" | _normalizeJson)"
-  _debug "Result: $curResult"
-  echo "$curResult" | _egrep_o "{[^{]*\"content\":[^{]*\"subdomain\":\"${curSubdomain}\"" | sed -n -e 's#.* "record_id": \(.*\),[^,]*#\1#p'
+  record_ids=$(echo "$result" | _egrep_o "{[^{]*\"subdomain\":\"${subdomain}\"[^}]*}" | sed -n -e 's#.*"record_id": \([0-9]*\).*#\1#p')
 }

+ 2 - 2
dnsapi/dns_zone.sh

@@ -136,10 +136,10 @@ _get_root() {
     if [ -z "$h" ]; then
       return 1
     fi
-    if ! _zone_rest GET "dns/$h/a"; then
+    if ! _zone_rest GET "dns/$h"; then
       return 1
     fi
-    if _contains "$response" "\"name\":\"$h\"" >/dev/null; then
+    if _contains "$response" "\"identificator\":\"$h\"" >/dev/null; then
       _domain=$h
       return 0
     fi

+ 46 - 40
notify/mail.sh

@@ -6,6 +6,7 @@
 #MAIL_FROM="yyyy@gmail.com"
 #MAIL_TO="yyyy@gmail.com"
 #MAIL_NOVALIDATE=""
+#MAIL_MSMTP_ACCOUNT=""
 
 mail_send() {
   _subject="$1"
@@ -61,7 +62,7 @@ mail_send() {
   fi
 
   contenttype="text/plain; charset=utf-8"
-  subject="=?UTF-8?B?$(echo "$_subject" | _base64)?="
+  subject="=?UTF-8?B?$(printf -- "%b" "$_subject" | _base64)?="
   result=$({ _mail_body | eval "$(_mail_cmnd)"; } 2>&1)
 
   # shellcheck disable=SC2181
@@ -76,18 +77,17 @@ mail_send() {
 }
 
 _mail_bin() {
-  if [ -n "$MAIL_BIN" ]; then
-    _MAIL_BIN="$MAIL_BIN"
-  elif _exists "sendmail"; then
-    _MAIL_BIN="sendmail"
-  elif _exists "ssmtp"; then
-    _MAIL_BIN="ssmtp"
-  elif _exists "mutt"; then
-    _MAIL_BIN="mutt"
-  elif _exists "mail"; then
-    _MAIL_BIN="mail"
-  else
-    _err "Please install sendmail, ssmtp, mutt or mail first."
+  _MAIL_BIN=""
+
+  for b in $MAIL_BIN sendmail ssmtp mutt mail msmtp; do
+    if _exists "$b"; then
+      _MAIL_BIN="$b"
+      break
+    fi
+  done
+
+  if [ -z "$_MAIL_BIN" ]; then
+    _err "Please install sendmail, ssmtp, mutt, mail or msmtp first."
     return 1
   fi
 
@@ -95,39 +95,45 @@ _mail_bin() {
 }
 
 _mail_cmnd() {
+  _MAIL_ARGS=""
+
   case $(basename "$_MAIL_BIN") in
-    sendmail)
-      if [ -n "$MAIL_FROM" ]; then
-        echo "'$_MAIL_BIN' -f '$MAIL_FROM' '$MAIL_TO'"
-      else
-        echo "'$_MAIL_BIN' '$MAIL_TO'"
-      fi
-      ;;
-    ssmtp)
-      echo "'$_MAIL_BIN' '$MAIL_TO'"
-      ;;
-    mutt | mail)
-      echo "'$_MAIL_BIN' -s '$_subject' '$MAIL_TO'"
-      ;;
-    *)
-      _err "Command $MAIL_BIN is not supported, use sendmail, ssmtp, mutt or mail."
-      return 1
-      ;;
+  sendmail)
+    if [ -n "$MAIL_FROM" ]; then
+      _MAIL_ARGS="-f '$MAIL_FROM'"
+    fi
+    ;;
+  mutt | mail)
+    _MAIL_ARGS="-s '$_subject'"
+    ;;
+  msmtp)
+    if [ -n "$MAIL_FROM" ]; then
+      _MAIL_ARGS="-f '$MAIL_FROM'"
+    fi
+
+    if [ -n "$MAIL_MSMTP_ACCOUNT" ]; then
+      _MAIL_ARGS="$_MAIL_ARGS -a '$MAIL_MSMTP_ACCOUNT'"
+    fi
+    ;;
+  *) ;;
   esac
+
+  echo "'$_MAIL_BIN' $_MAIL_ARGS '$MAIL_TO'"
 }
 
 _mail_body() {
   case $(basename "$_MAIL_BIN") in
-    sendmail | ssmtp)
-      if [ -n "$MAIL_FROM" ]; then
-        echo "From: $MAIL_FROM"
-      fi
-
-      echo "To: $MAIL_TO"
-      echo "Subject: $subject"
-      echo "Content-Type: $contenttype"
-      echo
-      ;;
+  sendmail | ssmtp | msmtp)
+    if [ -n "$MAIL_FROM" ]; then
+      echo "From: $MAIL_FROM"
+    fi
+
+    echo "To: $MAIL_TO"
+    echo "Subject: $subject"
+    echo "Content-Type: $contenttype"
+    echo "MIME-Version: 1.0"
+    echo
+    ;;
   esac
 
   echo "$_content"

+ 1 - 1
notify/mailgun.sh

@@ -7,7 +7,7 @@
 
 #MAILGUN_REGION="us|eu"          #optional, use "us" as default
 #MAILGUN_API_DOMAIN="xxxxxx.com"  #optional, use the default sandbox domain
-#MAILGUN_FROM="xxx@xxxxx.com"    #optional, use the default sendbox account
+#MAILGUN_FROM="xxx@xxxxx.com"    #optional, use the default sandbox account
 
 _MAILGUN_BASE_US="https://api.mailgun.net/v3"
 _MAILGUN_BASE_EU="https://api.eu.mailgun.net/v3"

+ 9 - 1
notify/sendgrid.sh

@@ -37,11 +37,19 @@ sendgrid_send() {
   fi
   _saveaccountconf_mutable SENDGRID_FROM "$SENDGRID_FROM"
 
+  SENDGRID_FROM_NAME="${SENDGRID_FROM_NAME:-$(_readaccountconf_mutable SENDGRID_FROM_NAME)}"
+  _saveaccountconf_mutable SENDGRID_FROM_NAME "$SENDGRID_FROM_NAME"
+
   export _H1="Authorization: Bearer $SENDGRID_API_KEY"
   export _H2="Content-Type: application/json"
 
   _content="$(echo "$_content" | _json_encode)"
-  _data="{\"personalizations\": [{\"to\": [{\"email\": \"$SENDGRID_TO\"}]}],\"from\": {\"email\": \"$SENDGRID_FROM\"},\"subject\": \"$_subject\",\"content\": [{\"type\": \"text/plain\", \"value\": \"$_content\"}]}"
+
+  if [ -z "$SENDGRID_FROM_NAME" ]; then
+    _data="{\"personalizations\": [{\"to\": [{\"email\": \"$SENDGRID_TO\"}]}],\"from\": {\"email\": \"$SENDGRID_FROM\"},\"subject\": \"$_subject\",\"content\": [{\"type\": \"text/plain\", \"value\": \"$_content\"}]}"
+  else
+    _data="{\"personalizations\": [{\"to\": [{\"email\": \"$SENDGRID_TO\"}]}],\"from\": {\"email\": \"$SENDGRID_FROM\", \"name\": \"$SENDGRID_FROM_NAME\"},\"subject\": \"$_subject\",\"content\": [{\"type\": \"text/plain\", \"value\": \"$_content\"}]}"
+  fi
   response="$(_post "$_data" "https://api.sendgrid.com/v3/mail/send")"
 
   if [ "$?" = "0" ] && [ -z "$response" ]; then

+ 393 - 9
notify/smtp.sh

@@ -2,14 +2,398 @@
 
 # support smtp
 
+# Please report bugs to https://github.com/acmesh-official/acme.sh/issues/3358
+
+# This implementation uses either curl or Python (3 or 2.7).
+# (See also the "mail" notify hook, which supports other ways to send mail.)
+
+# SMTP_FROM="from@example.com"  # required
+# SMTP_TO="to@example.com"  # required
+# SMTP_HOST="smtp.example.com"  # required
+# SMTP_PORT="25"  # defaults to 25, 465 or 587 depending on SMTP_SECURE
+# SMTP_SECURE="tls"  # one of "none", "ssl" (implicit TLS, TLS Wrapper), "tls" (explicit TLS, STARTTLS)
+# SMTP_USERNAME=""  # set if SMTP server requires login
+# SMTP_PASSWORD=""  # set if SMTP server requires login
+# SMTP_TIMEOUT="30"  # seconds for SMTP operations to timeout
+# SMTP_BIN="/path/to/python_or_curl"  # default finds first of python3, python2.7, python, pypy3, pypy, curl on PATH
+
+SMTP_SECURE_DEFAULT="tls"
+SMTP_TIMEOUT_DEFAULT="30"
+
+# subject content statuscode
 smtp_send() {
-  _subject="$1"
-  _content="$2"
-  _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped
-  _debug "_subject" "$_subject"
-  _debug "_content" "$_content"
-  _debug "_statusCode" "$_statusCode"
-
-  _err "Not implemented yet."
-  return 1
+  SMTP_SUBJECT="$1"
+  SMTP_CONTENT="$2"
+  # UNUSED: _statusCode="$3" # 0: success, 1: error 2($RENEW_SKIP): skipped
+
+  # Load and validate config:
+  SMTP_BIN="$(_readaccountconf_mutable_default SMTP_BIN)"
+  if [ -n "$SMTP_BIN" ] && ! _exists "$SMTP_BIN"; then
+    _err "SMTP_BIN '$SMTP_BIN' does not exist."
+    return 1
+  fi
+  if [ -z "$SMTP_BIN" ]; then
+    # Look for a command that can communicate with an SMTP server.
+    # (Please don't add sendmail, ssmtp, mutt, mail, or msmtp here.
+    # Those are already handled by the "mail" notify hook.)
+    for cmd in python3 python2.7 python pypy3 pypy curl; do
+      if _exists "$cmd"; then
+        SMTP_BIN="$cmd"
+        break
+      fi
+    done
+    if [ -z "$SMTP_BIN" ]; then
+      _err "The smtp notify-hook requires curl or Python, but can't find any."
+      _err 'If you have one of them, define SMTP_BIN="/path/to/curl_or_python".'
+      _err 'Otherwise, see if you can use the "mail" notify-hook instead.'
+      return 1
+    fi
+  fi
+  _debug SMTP_BIN "$SMTP_BIN"
+  _saveaccountconf_mutable_default SMTP_BIN "$SMTP_BIN"
+
+  SMTP_FROM="$(_readaccountconf_mutable_default SMTP_FROM)"
+  SMTP_FROM="$(_clean_email_header "$SMTP_FROM")"
+  if [ -z "$SMTP_FROM" ]; then
+    _err "You must define SMTP_FROM as the sender email address."
+    return 1
+  fi
+  if _email_has_display_name "$SMTP_FROM"; then
+    _err "SMTP_FROM must be only a simple email address (sender@example.com)."
+    _err "Change your SMTP_FROM='$SMTP_FROM' to remove the display name."
+    return 1
+  fi
+  _debug SMTP_FROM "$SMTP_FROM"
+  _saveaccountconf_mutable_default SMTP_FROM "$SMTP_FROM"
+
+  SMTP_TO="$(_readaccountconf_mutable_default SMTP_TO)"
+  SMTP_TO="$(_clean_email_header "$SMTP_TO")"
+  if [ -z "$SMTP_TO" ]; then
+    _err "You must define SMTP_TO as the recipient email address(es)."
+    return 1
+  fi
+  if _email_has_display_name "$SMTP_TO"; then
+    _err "SMTP_TO must be only simple email addresses (to@example.com,to2@example.com)."
+    _err "Change your SMTP_TO='$SMTP_TO' to remove the display name(s)."
+    return 1
+  fi
+  _debug SMTP_TO "$SMTP_TO"
+  _saveaccountconf_mutable_default SMTP_TO "$SMTP_TO"
+
+  SMTP_HOST="$(_readaccountconf_mutable_default SMTP_HOST)"
+  if [ -z "$SMTP_HOST" ]; then
+    _err "You must define SMTP_HOST as the SMTP server hostname."
+    return 1
+  fi
+  _debug SMTP_HOST "$SMTP_HOST"
+  _saveaccountconf_mutable_default SMTP_HOST "$SMTP_HOST"
+
+  SMTP_SECURE="$(_readaccountconf_mutable_default SMTP_SECURE "$SMTP_SECURE_DEFAULT")"
+  case "$SMTP_SECURE" in
+  "none") smtp_port_default="25" ;;
+  "ssl") smtp_port_default="465" ;;
+  "tls") smtp_port_default="587" ;;
+  *)
+    _err "Invalid SMTP_SECURE='$SMTP_SECURE'. It must be 'ssl', 'tls' or 'none'."
+    return 1
+    ;;
+  esac
+  _debug SMTP_SECURE "$SMTP_SECURE"
+  _saveaccountconf_mutable_default SMTP_SECURE "$SMTP_SECURE" "$SMTP_SECURE_DEFAULT"
+
+  SMTP_PORT="$(_readaccountconf_mutable_default SMTP_PORT "$smtp_port_default")"
+  case "$SMTP_PORT" in
+  *[!0-9]*)
+    _err "Invalid SMTP_PORT='$SMTP_PORT'. It must be a port number."
+    return 1
+    ;;
+  esac
+  _debug SMTP_PORT "$SMTP_PORT"
+  _saveaccountconf_mutable_default SMTP_PORT "$SMTP_PORT" "$smtp_port_default"
+
+  SMTP_USERNAME="$(_readaccountconf_mutable_default SMTP_USERNAME)"
+  _debug SMTP_USERNAME "$SMTP_USERNAME"
+  _saveaccountconf_mutable_default SMTP_USERNAME "$SMTP_USERNAME"
+
+  SMTP_PASSWORD="$(_readaccountconf_mutable_default SMTP_PASSWORD)"
+  _secure_debug SMTP_PASSWORD "$SMTP_PASSWORD"
+  _saveaccountconf_mutable_default SMTP_PASSWORD "$SMTP_PASSWORD"
+
+  SMTP_TIMEOUT="$(_readaccountconf_mutable_default SMTP_TIMEOUT "$SMTP_TIMEOUT_DEFAULT")"
+  _debug SMTP_TIMEOUT "$SMTP_TIMEOUT"
+  _saveaccountconf_mutable_default SMTP_TIMEOUT "$SMTP_TIMEOUT" "$SMTP_TIMEOUT_DEFAULT"
+
+  SMTP_X_MAILER="$(_clean_email_header "$PROJECT_NAME $VER --notify-hook smtp")"
+
+  # Run with --debug 2 (or above) to echo the transcript of the SMTP session.
+  # Careful: this may include SMTP_PASSWORD in plaintext!
+  if [ "${DEBUG:-$DEBUG_LEVEL_NONE}" -ge "$DEBUG_LEVEL_2" ]; then
+    SMTP_SHOW_TRANSCRIPT="True"
+  else
+    SMTP_SHOW_TRANSCRIPT=""
+  fi
+
+  SMTP_SUBJECT=$(_clean_email_header "$SMTP_SUBJECT")
+  _debug SMTP_SUBJECT "$SMTP_SUBJECT"
+  _debug SMTP_CONTENT "$SMTP_CONTENT"
+
+  # Send the message:
+  case "$(basename "$SMTP_BIN")" in
+  curl) _smtp_send=_smtp_send_curl ;;
+  py*) _smtp_send=_smtp_send_python ;;
+  *)
+    _err "Can't figure out how to invoke '$SMTP_BIN'."
+    _err "Check your SMTP_BIN setting."
+    return 1
+    ;;
+  esac
+
+  if ! smtp_output="$($_smtp_send)"; then
+    _err "Error sending message with $SMTP_BIN."
+    if [ -n "$smtp_output" ]; then
+      _err "$smtp_output"
+    fi
+    return 1
+  fi
+
+  return 0
+}
+
+# Strip CR and NL from text to prevent MIME header injection
+# text
+_clean_email_header() {
+  printf "%s" "$(echo "$1" | tr -d "\r\n")"
+}
+
+# Simple check for display name in an email address (< > or ")
+# email
+_email_has_display_name() {
+  _email="$1"
+  expr "$_email" : '^.*[<>"]' >/dev/null
+}
+
+##
+## curl smtp sending
+##
+
+# Send the message via curl using SMTP_* variables
+_smtp_send_curl() {
+  # Build curl args in $@
+  case "$SMTP_SECURE" in
+  none)
+    set -- --url "smtp://${SMTP_HOST}:${SMTP_PORT}"
+    ;;
+  ssl)
+    set -- --url "smtps://${SMTP_HOST}:${SMTP_PORT}"
+    ;;
+  tls)
+    set -- --url "smtp://${SMTP_HOST}:${SMTP_PORT}" --ssl-reqd
+    ;;
+  *)
+    # This will only occur if someone adds a new SMTP_SECURE option above
+    # without updating this code for it.
+    _err "Unhandled SMTP_SECURE='$SMTP_SECURE' in _smtp_send_curl"
+    _err "Please re-run with --debug and report a bug."
+    return 1
+    ;;
+  esac
+
+  set -- "$@" \
+    --upload-file - \
+    --mail-from "$SMTP_FROM" \
+    --max-time "$SMTP_TIMEOUT"
+
+  # Burst comma-separated $SMTP_TO into individual --mail-rcpt args.
+  _to="${SMTP_TO},"
+  while [ -n "$_to" ]; do
+    _rcpt="${_to%%,*}"
+    _to="${_to#*,}"
+    set -- "$@" --mail-rcpt "$_rcpt"
+  done
+
+  _smtp_login="${SMTP_USERNAME}:${SMTP_PASSWORD}"
+  if [ "$_smtp_login" != ":" ]; then
+    set -- "$@" --user "$_smtp_login"
+  fi
+
+  if [ "$SMTP_SHOW_TRANSCRIPT" = "True" ]; then
+    set -- "$@" --verbose
+  else
+    set -- "$@" --silent --show-error
+  fi
+
+  raw_message="$(_smtp_raw_message)"
+
+  _debug2 "curl command:" "$SMTP_BIN" "$*"
+  _debug2 "raw_message:\n$raw_message"
+
+  echo "$raw_message" | "$SMTP_BIN" "$@"
+}
+
+# Output an RFC-822 / RFC-5322 email message using SMTP_* variables.
+# (This assumes variables have already been cleaned for use in email headers.)
+_smtp_raw_message() {
+  echo "From: $SMTP_FROM"
+  echo "To: $SMTP_TO"
+  echo "Subject: $(_mime_encoded_word "$SMTP_SUBJECT")"
+  echo "Date: $(_rfc2822_date)"
+  echo "Content-Type: text/plain; charset=utf-8"
+  echo "X-Mailer: $SMTP_X_MAILER"
+  echo
+  echo "$SMTP_CONTENT"
+}
+
+# Convert text to RFC-2047 MIME "encoded word" format if it contains non-ASCII chars
+# text
+_mime_encoded_word() {
+  _text="$1"
+  # (regex character ranges like [a-z] can be locale-dependent; enumerate ASCII chars to avoid that)
+  _ascii='] $`"'"[!#%&'()*+,./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ~^_abcdefghijklmnopqrstuvwxyz{|}~-"
+  if expr "$_text" : "^.*[^$_ascii]" >/dev/null; then
+    # At least one non-ASCII char; convert entire thing to encoded word
+    printf "%s" "=?UTF-8?B?$(printf "%s" "$_text" | _base64)?="
+  else
+    # Just printable ASCII, no conversion needed
+    printf "%s" "$_text"
+  fi
+}
+
+# Output current date in RFC-2822 Section 3.3 format as required in email headers
+# (e.g., "Mon, 15 Feb 2021 14:22:01 -0800")
+_rfc2822_date() {
+  # Notes:
+  #   - this is deliberately not UTC, because it "SHOULD express local time" per spec
+  #   - the spec requires weekday and month in the C locale (English), not localized
+  #   - this date format specifier has been tested on Linux, Mac, Solaris and FreeBSD
+  _old_lc_time="$LC_TIME"
+  LC_TIME=C
+  date +'%a, %-d %b %Y %H:%M:%S %z'
+  LC_TIME="$_old_lc_time"
+}
+
+##
+## Python smtp sending
+##
+
+# Send the message via Python using SMTP_* variables
+_smtp_send_python() {
+  _debug "Python version" "$("$SMTP_BIN" --version 2>&1)"
+
+  # language=Python
+  "$SMTP_BIN" <<PYTHON
+# This code is meant to work with either Python 2.7.x or Python 3.4+.
+try:
+    try:
+        from email.message import EmailMessage
+        from email.policy import default as email_policy_default
+    except ImportError:
+        # Python 2 (or < 3.3)
+        from email.mime.text import MIMEText as EmailMessage
+        email_policy_default = None
+    from email.utils import formatdate as rfc2822_date
+    from smtplib import SMTP, SMTP_SSL, SMTPException
+    from socket import error as SocketError
+except ImportError as err:
+    print("A required Python standard package is missing. This system may have"
+          " a reduced version of Python unsuitable for sending mail: %s" % err)
+    exit(1)
+
+show_transcript = """$SMTP_SHOW_TRANSCRIPT""" == "True"
+
+smtp_host = """$SMTP_HOST"""
+smtp_port = int("""$SMTP_PORT""")
+smtp_secure = """$SMTP_SECURE"""
+username = """$SMTP_USERNAME"""
+password = """$SMTP_PASSWORD"""
+timeout=int("""$SMTP_TIMEOUT""")  # seconds
+x_mailer="""$SMTP_X_MAILER"""
+
+from_email="""$SMTP_FROM"""
+to_emails="""$SMTP_TO"""  # can be comma-separated
+subject="""$SMTP_SUBJECT"""
+content="""$SMTP_CONTENT"""
+
+try:
+    msg = EmailMessage(policy=email_policy_default)
+    msg.set_content(content)
+except (AttributeError, TypeError):
+    # Python 2 MIMEText
+    msg = EmailMessage(content)
+msg["Subject"] = subject
+msg["From"] = from_email
+msg["To"] = to_emails
+msg["Date"] = rfc2822_date(localtime=True)
+msg["X-Mailer"] = x_mailer
+
+smtp = None
+try:
+    if smtp_secure == "ssl":
+        smtp = SMTP_SSL(smtp_host, smtp_port, timeout=timeout)
+    else:
+        smtp = SMTP(smtp_host, smtp_port, timeout=timeout)
+    smtp.set_debuglevel(show_transcript)
+    if smtp_secure == "tls":
+        smtp.starttls()
+    if username or password:
+        smtp.login(username, password)
+    smtp.sendmail(msg["From"], msg["To"].split(","), msg.as_string())
+
+except SMTPException as err:
+    # Output just the error (skip the Python stack trace) for SMTP errors
+    print("Error sending: %r" % err)
+    exit(1)
+
+except SocketError as err:
+    print("Error connecting to %s:%d: %r" % (smtp_host, smtp_port, err))
+    exit(1)
+
+finally:
+    if smtp is not None:
+        smtp.quit()
+PYTHON
+}
+
+##
+## Conf helpers
+##
+
+#_readaccountconf_mutable_default name default_value
+# Given a name like MY_CONF:
+#   - if MY_CONF is set and non-empty, output $MY_CONF
+#   - if MY_CONF is set _empty_, output $default_value
+#     (lets user `export MY_CONF=` to clear previous saved value
+#     and return to default, without user having to know default)
+#   - otherwise if _readaccountconf_mutable MY_CONF is non-empty, return that
+#     (value of SAVED_MY_CONF from account.conf)
+#   - otherwise output $default_value
+_readaccountconf_mutable_default() {
+  _name="$1"
+  _default_value="$2"
+
+  eval "_value=\"\$$_name\""
+  eval "_name_is_set=\"\${${_name}+true}\""
+  # ($_name_is_set is "true" if $$_name is set to anything, including empty)
+  if [ -z "${_value}" ] && [ "${_name_is_set:-}" != "true" ]; then
+    _value="$(_readaccountconf_mutable "$_name")"
+  fi
+  if [ -z "${_value}" ]; then
+    _value="$_default_value"
+  fi
+  printf "%s" "$_value"
+}
+
+#_saveaccountconf_mutable_default name value default_value base64encode
+# Like _saveaccountconf_mutable, but if value is default_value
+# then _clearaccountconf_mutable instead
+_saveaccountconf_mutable_default() {
+  _name="$1"
+  _value="$2"
+  _default_value="$3"
+  _base64encode="$4"
+
+  if [ "$_value" != "$_default_value" ]; then
+    _saveaccountconf_mutable "$_name" "$_value" "$_base64encode"
+  else
+    _clearaccountconf_mutable "$_name"
+  fi
 }

+ 7 - 7
notify/xmpp.sh

@@ -71,13 +71,13 @@ _xmpp_bin() {
 
 _xmpp_cmnd() {
   case $(basename "$_XMPP_BIN") in
-    sendxmpp)
-      echo "'$_XMPP_BIN' '$XMPP_TO' $XMPP_BIN_ARGS"
-      ;;
-    *)
-      _err "Command $XMPP_BIN is not supported, use sendxmpp."
-      return 1
-      ;;
+  sendxmpp)
+    echo "'$_XMPP_BIN' '$XMPP_TO' $XMPP_BIN_ARGS"
+    ;;
+  *)
+    _err "Command $XMPP_BIN is not supported, use sendxmpp."
+    return 1
+    ;;
   esac
 }
 

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